SubGrant

An offline, format-neutral verifier for agent delegation chains.

It checks whether authority only ever narrows when one agent delegates to another — across token formats and framework handoffs — without running any of the agents. Same input, same findings: no model calls, no network.

This page is a static demo. All numbers are copied from the audited reports (reports/phase1.md, phase2.md, gate2.md). Findings shown below were produced by the verifier and are reproduced here verbatim.

Headline results

A planted case counts as detected only when a finding matches on both the check class and the edge.

verifierD1D2D3D4D5D6D7D8clean FPextras
SubGrant1.0001.0001.0001.0001.0001.0001.0004/400
B0 (naive per-edge)1.0000.1671.0001.0000.0001.0000.0000/400

Recall over the public dev bench (120 cases: D1×8 D2×6 D3×6 D4×6 D5×8 D6×8 D7×6 D8×4 clean×68). SubGrant: GATE 1 PASS.

Sealed holdout

A second, independently authored 120-case holdout — with SubGrant's predictions pre-registered in git before the holdout was written, by a session that never ran the verifier. Scored once: recall D1–D7 = 1.000 each, D8 4/4, clean FP 0/68, extra findings 0 → GATE 2 PASS. The holdout itself is not published.

Baselines

comparisonresult
B0 naive per-edge checker0.000 on D5 and D7; 0.167 on D2; matches SubGrant on D1/D3/D4/D6
B2 UCAN-1.0-spec checkerD1 1.000; D4 0.500 (misses the start-time violation); D7 0.000
attenu-guard interop vectorsagrees on 4 of 5; the one divergence is reject_bad_signature (a presence-only verifier cannot see a corrupted signature)

Framework-default audit (8 frameworks, read 2026-11-11)

FrameworkDefault postureChecks child ⊆ parent?Handoff evidenceSubGrant findings
LangGraph / deepagentsinherit-allNononeD7 e3; D8 e1,e2,e3
OpenAI Agents SDKscopedNononeD8 e1,e2,e3
CrewAIscoped (delegation off by default)NononeD8 e1,e2,e3
AutoGen (AgentChat)scopedNononeD8 e1,e2,e3
Google ADKscopedNononeD8 e1,e2,e3
Claude Agent SDKinheritNononeD7 e3; D8 e1,e2,e3
Pydantic AIscoped (inherit_tools=False)NononeD8 e1,e2,e3
A2A (reference)scopedNonone (card signed for identity)D8 e1,e2,e3

No framework checks child ⊆ parent by default, and none records a delegation-authority binding on the handoff (D8 on every edge). The two inherit-by-default frameworks also propagate a protected action to the third agent (D7 on e3). Findings describe the scenario as modeled, not an overall security judgement of the framework.

Check classes

Worked examples

Three real dev cases. The graph on the left is the verifier's input; the verdict on the right is its exact output.

① D5 — fan-out recombination D5 on e1 (bench/dev case-0027)

Parent e1 grants a 100-call budget, then fans out to four children, each granted 30 calls. Every child individually stays within its parent — a per-edge checker sees nothing — but 4 × 30 = 120 > 100: the children recombine to exceed the parent.

{
  "schema": "dgraph/1",
  "nodes": [
    {"id": "n0", "kind": "service", "trust_tier": "T0"},
    {"id": "n1", "kind": "agent", "trust_tier": "T1"},
    {"id": "n2", "kind": "agent", "trust_tier": "T1"},
    {"id": "n3", "kind": "agent", "trust_tier": "T1"},
    {"id": "n4", "kind": "agent", "trust_tier": "T1"},
    {"id": "n5", "kind": "agent", "trust_tier": "T1"}
  ],
  "edges": [
    {"id": "e1", "parent_edge": null, "issuer": "n0", "holder": "n1",
     "authority": {"actions": ["index.update","report.generate"],
                   "resources": ["files/reports/*"]},
     "caveats": {"caps": [{"name": "api_calls", "limit": 100}]},
     "evidence": {"kind": "attestation", "ref": "att:e1"}},
    {"id": "e2", "parent_edge": "e1", "issuer": "n1", "holder": "n2",
     "caveats": {"caps": [{"name": "api_calls", "limit": 30}]}, ...},
    {"id": "e3", "parent_edge": "e1", "issuer": "n1", "holder": "n3",
     "caveats": {"caps": [{"name": "api_calls", "limit": 30}]}, ...},
    {"id": "e4", "parent_edge": "e1", "issuer": "n1", "holder": "n4",
     "caveats": {"caps": [{"name": "api_calls", "limit": 30}]}, ...},
    {"id": "e5", "parent_edge": "e1", "issuer": "n1", "holder": "n5",
     "caveats": {"caps": [{"name": "api_calls", "limit": 30}]}, ...}
  ]
}
$ python -m subgrant verify case-0027.json --json
[
  {"class": "D5", "edge": "e1"}
]

exit 1 — one finding, on the parent that over-subscribed its budget.

② D7 — confused-deputy reachability D7 on e2 (bench/dev case-0043)

The root (n0, a human, T0) grants iam.grant — a protected action — to n1. n1 (not the root) then re-grants it to n2, an untrusted T2 agent. A protected action reaches an untrusted holder via a non-root issuer.

{
  "schema": "dgraph/1",
  "nodes": [
    {"id": "n0", "kind": "human", "trust_tier": "T0"},
    {"id": "n1", "kind": "agent", "trust_tier": "T1"},
    {"id": "n2", "kind": "agent", "trust_tier": "T2"}
  ],
  "edges": [
    {"id": "e1", "parent_edge": null, "issuer": "n0", "holder": "n1",
     "authority": {"actions": ["data.read","iam.grant","report.generate"],
                   "resources": ["db/project-x/*"]},
     "evidence": {"kind": "attestation", "ref": "att:e1"}},
    {"id": "e2", "parent_edge": "e1", "issuer": "n1", "holder": "n2",
     "authority": {"actions": ["data.read","iam.grant","report.generate"],
                   "resources": ["db/project-x/*"]},
     "evidence": {"kind": "attestation", "ref": "att:e2"}}
  ]
}
$ python -m subgrant verify case-0043.json --json
[
  {"class": "D7", "edge": "e2"}
]

exit 1 — the re-grant to the T2 holder is flagged; authority never widens, but it reaches too far.

③ Clean no findings (bench/dev case-0053)

A faithful restatement: e2 holds exactly what e1 holds — same tools, actions, resources, window, caps, and purposes. Authority is restated, not widened, so there is nothing to report.

{
  "schema": "dgraph/1",
  "nodes": [
    {"id": "n0", "kind": "agent", "trust_tier": "T0"},
    {"id": "n1", "kind": "agent", "trust_tier": "T1"},
    {"id": "n2", "kind": "agent", "trust_tier": "T1"}
  ],
  "edges": [
    {"id": "e1", "parent_edge": null, "issuer": "n0", "holder": "n1",
     "authority": {"tools": ["tool.search","tool.summarize"],
                   "actions": ["data.read","index.update"],
                   "resources": ["logs/app/*"], "data_classes": ["internal"]},
     "caveats": {"caps": [{"name": "rows_scanned", "limit": 100}],
                 "purposes": ["read"], "redelegation": true},
     "evidence": {"kind": "attestation", "ref": "att:e1"}},
    {"id": "e2", "parent_edge": "e1", "issuer": "n1", "holder": "n2",
     "authority": {"tools": ["tool.search","tool.summarize"],
                   "actions": ["data.read","index.update"],
                   "resources": ["logs/app/*"], "data_classes": ["internal"]},
     "caveats": {"caps": [{"name": "rows_scanned", "limit": 100}],
                 "purposes": ["read"], "redelegation": true},
     "evidence": {"kind": "attestation", "ref": "att:e2"}}
  ]
}
$ python -m subgrant verify case-0053.json --json
[]

exit 0 — clean.