Metadata-Version: 2.5
Name: attenu-guard
Version: 0.20.0
Summary: Attenu Guard: enforces per-agent permissions on every tool call and sub-agent handoff — a sub-agent never holds more than its parent — and writes a hash-chained audit log you verify offline. 19 agent frameworks and the A2A protocol, unmodified.
Project-URL: Homepage, https://attenu.io
Project-URL: Documentation, https://attenu.io/docs/
Project-URL: Source, https://github.com/attenu-io/attenu-guard
Project-URL: Issues, https://github.com/attenu-io/attenu-guard/issues
Project-URL: Changelog, https://github.com/attenu-io/attenu-guard/blob/main/CHANGELOG.md
Author: Attenu
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: a2a,ag2,agent-framework,agents,agno,ai,audit-log,authorization,autogen,camel-ai,claude-agent-sdk,crewai,delegation,google-adk,langchain,langflow,langgraph,least-privilege,llama-index,mcp,openai-agents,owasp,pydantic-ai,security,semantic-kernel,smolagents,strands,sub-agents
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Security
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Logging
Requires-Python: >=3.9
Provides-Extra: a2a
Requires-Dist: a2a-sdk>=1.1; extra == 'a2a'
Provides-Extra: ag2
Requires-Dist: ag2>=1.0; extra == 'ag2'
Provides-Extra: agent-framework
Requires-Dist: agent-framework>=1.15; extra == 'agent-framework'
Provides-Extra: agno
Requires-Dist: agno>=2.9; extra == 'agno'
Provides-Extra: all
Requires-Dist: cryptography>=41; extra == 'all'
Requires-Dist: pyyaml>=6; extra == 'all'
Provides-Extra: autogen
Requires-Dist: autogen-agentchat>=0.7; extra == 'autogen'
Requires-Dist: autogen-ext>=0.7; extra == 'autogen'
Provides-Extra: camel
Requires-Dist: camel-ai>=0.2.90; extra == 'camel'
Requires-Dist: mcp<2; extra == 'camel'
Provides-Extra: claude-agent-sdk
Requires-Dist: claude-agent-sdk>=0.2; extra == 'claude-agent-sdk'
Provides-Extra: crewai
Requires-Dist: crewai>=1.15; extra == 'crewai'
Provides-Extra: crypto
Requires-Dist: cryptography>=41; extra == 'crypto'
Provides-Extra: deepagents
Requires-Dist: deepagents>=0.7; extra == 'deepagents'
Requires-Dist: langchain>=1.3; extra == 'deepagents'
Requires-Dist: langgraph>=1.2; extra == 'deepagents'
Provides-Extra: google-adk
Requires-Dist: google-adk>=2.7; extra == 'google-adk'
Provides-Extra: haystack
Requires-Dist: haystack-ai>=3.1; extra == 'haystack'
Provides-Extra: langchain
Requires-Dist: langchain-core>=1.5; extra == 'langchain'
Requires-Dist: langchain>=1.3; extra == 'langchain'
Requires-Dist: langgraph>=1.2; extra == 'langchain'
Provides-Extra: langflow
Requires-Dist: langchain-core>=1.5; extra == 'langflow'
Requires-Dist: lfx>=1.11; extra == 'langflow'
Provides-Extra: langgraph
Requires-Dist: langgraph>=0.2; extra == 'langgraph'
Provides-Extra: llama-index
Requires-Dist: llama-index-core>=0.14; extra == 'llama-index'
Provides-Extra: mcp
Requires-Dist: mcp>=1.28; extra == 'mcp'
Provides-Extra: omnigent
Requires-Dist: omnigent>=0.10; extra == 'omnigent'
Provides-Extra: openai-agents
Requires-Dist: openai-agents>=0.21; extra == 'openai-agents'
Provides-Extra: openhands
Requires-Dist: openhands-sdk>=1.44; extra == 'openhands'
Requires-Dist: openhands-tools>=1.44; extra == 'openhands'
Provides-Extra: pydantic-ai
Requires-Dist: pydantic-ai-slim>=2.31; extra == 'pydantic-ai'
Provides-Extra: semantic-kernel
Requires-Dist: protobuf; extra == 'semantic-kernel'
Requires-Dist: semantic-kernel>=1.36; extra == 'semantic-kernel'
Provides-Extra: smolagents
Requires-Dist: smolagents>=1.26; extra == 'smolagents'
Provides-Extra: strands
Requires-Dist: strands-agents>=1.52; extra == 'strands'
Provides-Extra: test
Requires-Dist: hypothesis>=6; extra == 'test'
Requires-Dist: pytest>=7; extra == 'test'
Provides-Extra: yaml
Requires-Dist: pyyaml>=6; extra == 'yaml'
Description-Content-Type: text/markdown

# attenu-guard

**Delegation without escalation.** attenu-guard checks every agent tool call in your process, narrows permissions
at each handoff, and logs every decision in a verifiable chain. Works with 19 frameworks. No dependencies.

When one AI agent hands work to another, the frameworks we tested don't check the second one's permissions
against the first one's. attenu-guard gives the child the permissions you declare for its task, never more than
the parent holds, and refuses the rest, with no network call in the deny path.

**What you get**

1. One check on every tool call, inside your process, across 19 Python frameworks and A2A, with a TypeScript port.
2. Permissions that only narrow at each handoff, down any chain, with typed limits (rows, spend, calls) and cascade revocation.
3. A hash-chained, signable ledger of every allow and deny, for security audit, verified offline with the vendor absent.
4. Observe mode records everything and blocks nothing. Each decision goes on the ledger as it happens, and to any sink you add. You write the code that forwards it to your own endpoint.
5. No network in the deny path, and the library itself makes no network calls. Revoke at runtime.
6. Tested: 1,415 test functions, a red-team harness and property invariants in CI.

Apache-2.0 · Python 3.9+ · zero runtime dependencies · TypeScript port:
[attenu-guard-ts](https://github.com/attenu-io/attenu-guard-ts), on npm as `attenu-guard`

[attenu.io](https://attenu.io) · [Docs](docs/) · [Attenu Derive — what each agent may do, read from your app](https://github.com/attenu-io/attenu-derive) · [Internet-Draft](https://datatracker.ietf.org/doc/draft-asor-wimse-agent-delegation-chain/) · [Changelog](CHANGELOG.md)

Where it came from: a sub-agent in Open SWE pushed a workflow file its parent was held on. Reported, fixed by LangChain the same day, advisory published. [The write-up](https://attenu.io/blog/open-swe-sub-agent-pushed-a-workflow-file/).

```bash
pip install attenu-guard
attenu-guard demo   # one delegation, one poisoned instruction, the export refused, the chain verified
```

```python
from attenu_guard import Authority, Guard, RowLimit, AuthorityDenied

# The orchestrator holds broad authority. Every decision lands in log.jsonl.
parent = Guard.issue("orchestrator", Authority(
    scopes={"crm.*", "mail.send"}, ceilings=[RowLimit(100_000)], ttl=3600),
    audit_path="log.jsonl")

# It delegates a narrow task. The child gets the meet of what the parent held
# and what the task needs, computed and enforced, not suggested.
child = parent.delegate("summarizer", Authority(
    scopes={"crm.read"}, ceilings=[RowLimit(5_000)], ttl=900), task="summarize Q3")

print(child.check("crm.read", context={"rows": 4_200}))

try:
    child.enforce("crm.export")
except AuthorityDenied as denied:
    print(denied)
```

Prints:

```
Decision(allowed=True, reasons=(), determining_node='chain:n1', call_id=None)
denied: scope_not_granted requested=crm.export: scope 'crm.export' not covered by held scopes ['crm.read']
```

Then render the log as a delegation tree and verify its hash chain:

```bash
attenu-guard view log.jsonl
```

```
orchestrator  [root]
    summarizer  «summarize Q3»
        · crm.read ✓
        · crm.export ✗ scope_not_granted

audit chain: 4 events · verification: OK
```

The guard will not overwrite an existing ledger, so delete `log.jsonl` before running the
snippet again.

`check()` returns a `Decision` with machine-readable reason codes for your audit trail,
`enforce()` is the hard-stop gate that raises, and `would_allow()` is a dry-run that writes
nothing. The export is refused because the sub-agent never held that permission, so an injected
instruction has nothing to widen.

Adapters for 19 frameworks and the A2A protocol, each integrated **unmodified**, each with offline
tests, and all but OpenHands and AstrBot with an offline demo ([matrix](docs/INTEGRATIONS.md)); install one with
`pip install 'attenu-guard[<extra>]'`. MCP is a recipe rather than an adapter: a server that
verifies the delegation chain before it runs a tool
([server verifier](examples/integrations/mcp/server_verifier/README.md)).

Enforced live on Google's published `adk-samples` customer-service and financial-advisor apps,
denying a real model mid-run
([evidence](https://github.com/attenu-io/attenu-derive/blob/main/docs/LIVE-ENFORCE.md)). The same
adapters run live on CrewAI and LangGraph, on crews and graphs we built for a travel-booking domain
rather than on third-party apps.
[A3](https://github.com/attenu-io/attenu-derive/blob/main/docs/A3-FRAMEWORKS.md) states that limit.

> **Have a bundle to check?** `pipx run attenu-guard verify bundle.json` checks integrity,
> child ⊆ parent and containment from the file alone, no account, no network. The
> [auditor's walkthrough](examples/verify/README.md) has three sample bundles (clean, tampered,
> widened) and takes a minute.

> **Just want to see it run?** Clone the repo (`git clone https://github.com/attenu-io/attenu-guard && cd attenu-guard`);
> no install needed, the examples bootstrap the `src/` path themselves: `python3 examples/poisoned_summarizer.py`, and
> `python3 tests/run_properties.py` for the invariants.

![attenu-guard demo — the poisoned summariser: one legitimate read allowed, the exfiltration blocked, the subtree revoked, the audit chain verified](https://raw.githubusercontent.com/attenu-io/attenu-guard/main/docs/assets/demo.gif)

An open enforcement layer for [OWASP ASI07 (insecure inter-agent communication) and ASI08 (cascading failures)](https://genai.owasp.org/download/52117/): delegated authority stays inside the parent's limits, and every check the guard records stays verifiable offline.

## Proof

**Does this happen in shipped code?** Three projects merged native fixes for sub-agent defects we reported:

1. **LangChain Open SWE.** A sub-agent pushed a workflow file its parent was held on. Advisory [GHSA-wpjg-64mw-3qj9](https://github.com/langchain-ai/open-swe/security/advisories/GHSA-wpjg-64mw-3qj9) (published 2026-09-18, CWE-862, no CVE) credits "Reported by Rafael Asor (Attenu)"; reported 2026-09-07, native fix [open-swe#2496](https://github.com/langchain-ai/open-swe/pull/2496) merged the same day. [The write-up](https://attenu.io/blog/open-swe-sub-agent-pushed-a-workflow-file/).
2. **Yuxi.** The default approval mode hid the write, edit and execute tools from sub-agents without intercepting them. Native fix [xerrors/Yuxi#1001](https://github.com/xerrors/Yuxi/pull/1001), merged 2026-09-08 with its tests.
3. **minion-agent.** A managed sub-agent's executor and step-limit configuration was dropped on the way in. Native fix [femto/minion-agent#7](https://github.com/femto/minion-agent/pull/7), merged 2026-09-08 with regression tests.
4. **And more.** Further reports are inside their disclosure windows; they are listed here when they close.

**Has anyone outside checked the claims?** Three verifiers that share no code with this repository scored the published test vectors:

1. [@safal207](https://github.com/safal207/ContractGraph-QA/tree/6893d0fd403f3cca3f1614aa259799250cedc005/proofs/attenu-delegation-20-independent), Node.js: 20 of 20 Delegation Token vectors (HS256 profile, draft steps 1 to 5). Python, stdlib only: 18 of 18 observer-envelope vectors, revision v1.1.
2. @XuebinMa, Rust (agent-guard): [19 of 19 observer-envelope vectors, revision v1.2](https://github.com/a2aproject/A2A/issues/1575#issuecomment-5569510706). [Bundle vectors revision v1.4](https://github.com/a2aproject/A2A/issues/1575#issuecomment-5596516701): 17 of 20 on the first run, 20 of 20 after one change.
3. [Kieran Sweeney's Cred harness](https://github.com/cred-ninja/protocol/commit/a4fc76e08a0c0f19e5c53ab5803c0802c8c44878): 17 of 20 Delegation Token vectors, 0 fail, 3 declared gaps, each a stated design choice on his side.

**Does it hold up today?** Run 2026-09-28 on v0.18.0: `tests/red_team.py` 17 attacks, 15 defended, 2 documented limitations, 0 broken; `tests/run_properties.py` 4,000 random trees per invariant, all held.

Each third-party number is the author's reported corpus score at a pin, not completeness, runtime correctness or certification. Pins and boundaries: [`tests/vectors/README.md`](tests/vectors/README.md).

## Compared with

Legend: **✓** stated on the project's own pages or shown by a run in this repository · **✗** its own pages or a run say no · **—** not stated on the pages read (silence is not a no). Framework hooks = Deep Agents, CrewAI, the OpenAI Agents SDK and Google ADK, each through its own hook API. Every quotation and every run, with 24 dated sources: [`docs/COMPARISON.md`](docs/COMPARISON.md).

| | attenu-guard | Tenuo | Keel | Framework hooks | OPA · Cedar · Casbin |
|---|---|---|---|---|---|
| Every tool call checked, in your process | ✓ | ✓ | n/a, the spec names no runtime | ✓ | ✓ |
| Child holds no more than its parent, enforced at the handoff | ✓ built into `delegate()` | ✓ | ✓ | ✗ a sub-agent ran a tool its parent never held, all four ([`examples/baselines/`](examples/baselines/), 2026-09-28) | you write the rule |
| Holds down a chain of any depth | ✓ | — | ✓ | ✗ | — |
| Revoke a parent, every descendant denies at once | ✓ | — | — | — | — |
| Limits with units that only shrink (rows, spend, calls) | ✓ | — | — | — | — |
| Every allow and deny on a hash-chained log | ✓ | receipts, opt-in | — | — | — |
| Verified offline, no server in the path | ✓ | ✓ | ✓ | — | n/a |
| Observe mode: record everything, block nothing | ✓ | ✓ | — | — | — |
| Test vectors scored by outside verifiers | ✓ three | — | — | n/a | n/a |
| Agent-framework adapters | 19 + A2A | its README lists its own | — | n/a | — |
| Python runtime dependencies | none | `pyyaml`, `pydantic`, compiled wheels | n/a | n/a | — |
| Code you can run | Apache-2.0, enforcement and verifier | Apache-2.0 | spec and verifier public; reference server Keel-internal | n/a | Apache-2.0 |

Tenuo and Keel enforce the same handoff rule we do. OAuth token exchange, SPIFFE, macaroons and Biscuit, and the four neighbouring Internet-Drafts (draft-niyikiza and draft-sweeney, both cited by our draft; draft-jackson, which cites ours; draft-schrock) are on the same page, each in its own words.

## What happens at a handoff today

In the frameworks we audited, the handoff itself is not something the system can
see: identity tokens describe two parties — user and agent — so "child ⊆ parent"
cannot be written down, and policy checks fire when a tool is invoked rather than
at the moment permissions are passed down. Verified against released code, and
pinned by tests that fail the day the behaviour changes:

| System | What it does at a handoff (framework rows verified against the released code — see [`docs/INTEGRATIONS.md`](docs/INTEGRATIONS.md); the identity and protocol rows are from their published specs) |
|---|---|
| OpenAI Agents SDK 0.22 | passes the **entire** conversation to the sub-agent (`Handoff.input_filter=None` by default: *"the new agent sees the entire conversation history"*); no parent/child relation exists, so nothing checks child ⊆ parent |
| LangChain `deepagents` 0.7 | a sub-agent's `permissions` **replace** the parent's rules entirely (`graph.py`) — a child can be granted what its parent is denied |
| Google ADK 2.10.0 | `disallow_transfer_to_peers` is enforced at runtime on both paths as of 2.10.0 (`workflow/utils/_transfer_utils.py` raises `WorkflowDataError`; re-run 2026-09-28). That flag controls who may transfer, not what authority passes: a transfer target and an `AgentTool` still ran `crm_export`, a tool the coordinator never held (`examples/baselines/google_adk/baseline.py`, re-run 2026-09-28 on 2.10.0). Either way ADK checks *who may transfer*; it does not check *what authority passes*, and no record exists to verify afterwards |
| CrewAI 1.15 | a delegated coworker runs with its **own full tool list**; the tool-hook dispatcher swallows exceptions and runs the tool (**fail-open**) unless you raise its one blessed exception |
| AutoGen 0.7 | `Handoff` carries target/description/message only; the receiver offers the model its own full tool list |
| Microsoft Entra | the parent→child construct has two settings, *all allowed* or *none*; it cannot express child ⊆ parent ([details](https://github.com/attenu-io/attenu-guard/blob/main/docs/SUB-AGENT-PERMISSIONS.md)) |
| MCP | scope flow is **accumulation**-biased (step-up unions); the request carries no agent authority at all: `CallToolRequestParams` has `name`, `arguments`, `meta`, `task` and nothing that says which agent is calling with how much of it. Shipped here as a recipe rather than an adapter, a server that verifies the chain before it runs a tool ([`server_verifier`](examples/integrations/mcp/server_verifier/README.md)) |
| A2A | authenticates the hop, carries **no** delegated authority |

attenu-guard makes child ⊆ parent a computed, enforced, offline-verifiable
invariant — in your framework, in your process — no proxy, and no network call in the deny path.

## Features in detail

- **`Authority`** — an immutable capability (scopes + a list of typed, extensible `Ceiling` bounds + TTL) with `meet`, the lattice operation that can only ever *shrink*, and `is_narrower_than`, the provable subsumption relation.
- **`Guard`** — `issue()` a root, `delegate()` a sub-agent with attenuated authority, `check()` → `Decision`, `enforce()` → raises, `would_allow()` → dry-run, `revoke()` a whole subtree.
- **Typed ceilings** — `RowLimit`, `SpendCap`, `CallLimit`, `EgressRank`, `Allow`, `Deny`, `Prefix`, or your own via `register_ceiling`. Unknown ceiling types **fail closed**, never silently unbounded.
- **Chain invariants** — depth, fanout, and aggregate budget ceilings; **cascade revocation** (revoke any node, every descendant denies immediately).
- **Hash-chained audit log** — an open, versioned [schema](schema/agent-audit.schema.json); `attenu-guard view log.jsonl` renders the tree and verifies it; tampering is provable offline. Every `deny` says **why** (`disposition`: `held_pending_grant` — waiting on a human · `withheld_tier2` · `unresolved` — no authority known for the tool · `out_of_authority` — real over-reach), so "held" never reads as "denied"; `evidence.export_bundle` / `verify_bundle` / `delegation_graph` / `denials` give an auditor an **offline-verifiable** bundle and the folds a console renders. The bundle proves authorization and lifecycle, not execution or outcome — see [what the evidence does not prove](docs/THREAT-MODEL.md#what-the-evidence-does-not-prove). `AuditLog(sinks=…)` hands each entry to every **sink** you add, after the file write; the library itself never sends it over the network. `sinks.SpoolSink` is a bounded, fsync'd, resumable write-ahead spool carrying the ingest idempotency key `(boot_id, chain_id, seq, hash)`; [`attenu_guard.identity`](src/attenu_guard/identity.py) gives a product an identity before it has a key (`.attenu/product.json`, per-process `boot_id`, assigned chain ids).
  To forward every entry to your own endpoint as it is recorded, with a sink you write, see [`docs/STREAMING.md`](docs/STREAMING.md). A sink runs inside `check()`, so the one there only queues; a thread does the POST.
- **Wire format** ([`attenu_guard.wire`](src/attenu_guard/wire.py)) — `serialize`/`load` the delegation chain as signed **Delegation Tokens** and verify child ⊆ parent **offline**, across services, with no authorization server in the path. This is the reference implementation of the current working Internet-Draft in [`docs/`](docs/draft-asor-wimse-agent-delegation-chain-01.md); 20 interop test vectors live in [`tests/vectors/`](tests/vectors/) and ship inside the installed package as `attenu_guard.vectors`, so an implementation in any language can score its own verifier with nothing but `pip install attenu-guard`.
- **Scenario harness** — declarative JSON/YAML authorization tests (`attenu-guard scenarios file.json`); see [`scenarios/`](scenarios/).
- **Adapters** — shipped, tested integrations for the major agent frameworks as [`attenu_guard.adapters.<name>`](src/attenu_guard/adapters/): LangGraph, LangChain `create_agent` / deepagents, OpenAI Agents SDK, Google ADK, Pydantic AI, CrewAI, AutoGen, Microsoft Agent Framework, AG2, Claude Agent SDK, smolagents, AWS Strands, LlamaIndex, Semantic Kernel, Agno, Haystack, CAMEL-AI, OpenHands, AstrBot — and, for the **A2A** protocol, a client interceptor plus a guarded `AgentExecutor` that carries the attenuated chain across a hop between processes. Each has offline tests, and all but OpenHands and AstrBot have an offline demo under [`examples/integrations/`](examples/integrations/); install one with `pip install 'attenu-guard[<extra>]'`. Hooks, versions and what each framework enforces itself: [`docs/INTEGRATIONS.md`](docs/INTEGRATIONS.md).

## Observe mode: record first, gate later

You do not have to decide a policy to get value out of this. Point it at agents that
already exist, let every call through, and read what they did. Nothing is refused, so
there is nothing to get wrong.

```python
from attenu_guard import Authority, Guard

# Wide on purpose: you are watching, not gating.
# Note the scope grammar: "*" is only valid as the final segment, so list the
# families your app uses. A bare "*" is rejected.
WIDE = Authority(scopes={"crm.*", "mail.*", "fs.*"}, ceilings=[], ttl=3600)

root  = Guard.issue("orchestrator", WIDE, max_depth=4)
child = root.delegate("summarizer", WIDE, task="summarize")

root.check("crm.read")            # allowed, and recorded
child.check("crm.read")
child.check("mail.send")

for e in root.audit_log().entries:
    print(e.get("event"), e.get("node"), e.get("scope"))
```

```
root chain:n0 None
spawn chain:n1 None
allow chain:n0 crm.read
allow chain:n1 crm.read
allow chain:n1 mail.send
```

Every hand-off and every call is on the hash-chained log, with the parent each child
came from. Read it, and you can see whether a child ever made a call its parent would
not have been allowed to make. Then narrow the authorities and the same code starts
refusing.

**On somebody else's app**, you will not know the tool names in advance. The adapters
take two hooks for exactly this: `default_policy(tool_name) -> ToolPolicy` and
`default_subagent_authority(name) -> Authority`. When nothing was declared for a tool
or a sub-agent, the hook's result is used as if it had been, so the call is authorized
and **recorded with a generated scope** instead of denied. `default_policy` takes
precedence over `allow_unlisted`, which silently passes unlisted tools without
recording a scope for them.

**What this does not do.** A wide authority is a choice you are making, not a magic
pass: a tool that *is* listed and falls outside the authority still denies. Observe
mode changes what you declare, not how `check()` decides. And a call that never
reaches the hook is neither recorded nor refused — see
[`docs/THREAT-MODEL.md`](docs/THREAT-MODEL.md).

## Canonicalization and compatibility

Versions 0.7 and later use [RFC 8785 JCS](https://www.rfc-editor.org/rfc/rfc8785) for every
signed or hash-linked artifact: Delegation Token protected headers and payloads,
parent commitments, integrity seals, audit entries, anchors, and evidence bundles.
Tokens and metadata-bearing artifacts emit `"c14n":"JCS"` as an informational
label. Verifiers enforce JCS from canonical bytes and hashes, not from that label;
non-canonical input, duplicate object member names, non-finite numbers, and lone
UTF-16 surrogates are rejected.

This is a deliberate wire-format break from versions through 0.6.1. Current versions have
no legacy or dual-format reader. Producers and verifiers in different languages must
move together; the 20 packaged [interop vectors](tests/vectors/README.md) pin the
required bytes and rejection reasons.

## Prove the safety claims yourself

```bash
git clone https://github.com/attenu-io/attenu-guard && cd attenu-guard
python3 tests/run_properties.py      # 4,000 random delegation trees per invariant, zero deps
python3 tests/red_team.py            # 17 adversarial attacks, black- & white-box; 0 must break
python3 examples/poisoned_summarizer.py
attenu-guard demo                    # needs pip install attenu-guard
```

The property suite asserts — over thousands of random chains — that attenuation
never widens, holds transitively down a chain, that a revoked subtree authorizes
nothing, and that audit tampering is detected. The red-team harness (see
[`docs/RED-TEAM.md`](docs/RED-TEAM.md)) additionally tries to *break* the protocol
— privilege escalation, chain splicing, expired-grant reuse — and every genuine
finding is fixed and pinned as a regression. If you can break one, the core claim
is false; please [tell us](SECURITY.md).

## Writing

Runs, not claims, on what each agent framework does at a handoff — one post per finding, versions pinned,
output pasted: [attenu.io/blog](https://attenu.io/blog/) ([feed](https://attenu.io/feed.xml)). Start with
[Does a sub-agent inherit its parent's permissions? Five frameworks, five answers.](https://attenu.io/blog/sub-agent-permissions-five-frameworks/)

## Standards

The protocol is designed to be IETF-acceptable: it reuses the OAuth/JOSE stack
(JWT, RFC 9396 authorization_details, DPoP, Token Status List) and invents only
the one missing piece — cryptographically-linked, subsumption-enforced, offline
multi-hop attenuation. See [`docs/STANDARDS-ALIGNMENT.md`](docs/STANDARDS-ALIGNMENT.md)
and the Internet-Draft [draft-asor-wimse-agent-delegation-chain](https://datatracker.ietf.org/doc/draft-asor-wimse-agent-delegation-chain/) (published revision `-01`, individual submission, targeting the WIMSE working group; source in [`docs/`](docs/draft-asor-wimse-agent-delegation-chain-01.md)).

## What this is *not*

This library does **not** decide authority *for* you. You write the `Authority` for
each delegation — or you let [`attenu-derive`](https://github.com/attenu-io/attenu-derive),
the open engine, compute it from your app's declared structure (agents, roster, tools,
what each task calls) and approve it before it is enforced. The library is the
enforcement shim and the open schema; it is useful entirely on its own, forever, with
no account and no network. Attenu also has a console for denials and decisions. It is not
public yet, and it is never in the deny path.

attenu-guard only sees calls that go through its hook. A tool called some other way is neither refused
nor logged, and an `allow` means authorized, not executed. See
[docs/THREAT-MODEL.md](https://github.com/attenu-io/attenu-guard/blob/main/docs/THREAT-MODEL.md) and the documented limitations in
[docs/RED-TEAM.md](https://github.com/attenu-io/attenu-guard/blob/main/docs/RED-TEAM.md).

## License

Apache-2.0. Contributions under the [DCO](CONTRIBUTING.md). Security policy in
[SECURITY.md](SECURITY.md).
