A2A and MCP solved how agents talk and what they can call. Neither says
anything about the moment that actually matters in a multi-agent system:
agent A asks agent B to do something on A's behalf. B now needs some of
A's authority — and today "some" isn't on the menu. A shares its
credentials (B gets everything), or B uses its own standing credentials
(which quietly accumulate every permission any task ever needed).
The missing primitive is a delegation: a signed statement that hands
another identity a bounded slice of your authority. Here's the grammar,
one grant:
{
"res": "mcp://acme/tools/*",
"act": ["call"],
"cav": { "exp": 1786015180, "max_uses": 100, "nb": { "tool": "search|fetch" } }
}
A resource pattern, the actions allowed on it, and caveats: an expiry, a
use budget, and named request predicates (nb — the request's tool
argument must be exactly search or fetch). Agent A signs a
Delegation link handing that grant to B's identity:
import { delegationBinding, parentRefHashRoot, delegate } from "@grantor/sdk";
const bindingHex = delegationBinding(tenantId, iss, to, grantsJson, exp,
epochLabelHex, epoch, parentRefHash);
const signatureHex = await sign(bindingHex); // A's own secp256k1/EIP-191 signer
const link = delegate(tenantId, iss, to, grantsJson, exp,
epochLabelHex, epoch, parentRefHash, undefined, signatureHex);
B attaches the link to its own deed (its normal login credential) and
presents the pair. The verifying service makes one call —
verifyWithCapabilityAt — and gets back B's proven identity plus the
grants that survived the chain walk, then enforces each request with
CapabilityGuard.authorize.
The rules the verification actually enforces are where this stops being a
JSON convention and becomes a security boundary:
- The chain's root must be a registered on-chain key. A delegates because the registry for its tenant says A's key exists and hasn't been revoked. A chain rooted anywhere else fails, period.
-
Every link must narrow its parent. Subset of the resources, subset
of the actions, caveats no looser. B can re-delegate to C —
searchonly, 5 uses — but a link that widens anything is aBadDelegationat verify time, no matter what any broker claimed at signing time. - A pin can't be narrowed — only carried. A grant can also pin the canonical hash of the tool descriptor it was signed for. Predicate equality is exact, so a child cannot hand on a "smaller" pinned set the way it can drop a resource: it inherits the parent's pin verbatim or the link fails to narrow. A descriptor pin therefore travels the whole subtree automatically, and no intermediate agent can quietly drop it.
-
The last link must name the presenter. The chain ends at B's proven
sub; a stolen delegation attached to someone else's deed doesn't verify.
Revocation is the part I'd underline. Each link is signed against a
revocation cohort (epoch_label). One transaction —
GrantorRegistry.bumpEpoch — invalidates every link in that cohort:
A's grant to B, B's narrower grant to C, the whole subtree, everywhere,
on the next check. No calling around to services that cached a token, no
per-child cleanup. Label cohorts per project, per incident domain, per
whatever blast radius you want a single kill-switch for.
And one property with no OAuth analogue at all: anonymous delegation.
A fleet member can delegate without revealing which member it is —
delegateZk authenticates the link with a zero-knowledge membership proof
over the tenant's on-chain registry instead of a recoverable signature.
The verifier learns "a real, current, paid-up member of this fleet granted
this," plus a stable per-tenant pseudonym — and with a per-chain salt, two
delegations from the same member can't even be linked to each other.
Honest edges: max_uses rides in the grant but counting real uses needs
durable state only your app can own, so the guard checks it as a bound and
your service (or the broker below) does the metering; the ZK-delegation
proving path is real but the deeper structure-hiding variant is explicitly
preview-labeled; all of it is an unaudited developer preview.
You can watch this exact arc — grant, delegate narrower, attempt, DENIED,
revoked live on-chain — run in your browser against the Base mainnet
registry: https://chaingrantor.com/playground.html
If you'd rather not hand-roll the signing, npx -y @grantor/mcp serve
packages exactly this arc — grant, delegate, check, revoke — as five MCP
tools with the key custody handled for you (and grantor-mcp wrap makes
it enforcing for any stdio MCP server); that's what post #1 walked
through: https://dev.to/grantor/give-your-ai-sub-agent-a-budget-not-your-keys-2e7h
Full grammar and verification docs:
https://chaingrantor.com/docs/guide/capabilities
MCP standardized what agents can call. A2A standardized how they talk.
Delegation is what authority looks like when it moves between them — and
it should only ever get smaller.
Top comments (0)