<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Grantor</title>
    <description>The latest articles on DEV Community by Grantor (@grantor).</description>
    <link>https://dev.to/grantor</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4088755%2F2922eac3-ea94-4429-89a6-8e22cf470d4f.png</url>
      <title>DEV Community: Grantor</title>
      <link>https://dev.to/grantor</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/grantor"/>
    <language>en</language>
    <item>
      <title>How one AI agent hands another less than everything</title>
      <dc:creator>Grantor</dc:creator>
      <pubDate>Tue, 08 Sep 2026 13:45:00 +0000</pubDate>
      <link>https://dev.to/grantor/how-one-ai-agent-hands-another-less-than-everything-5b09</link>
      <guid>https://dev.to/grantor/how-one-ai-agent-hands-another-less-than-everything-5b09</guid>
      <description>&lt;p&gt;A2A and MCP solved how agents talk and what they can call. Neither says&lt;br&gt;
anything about the moment that actually matters in a multi-agent system:&lt;br&gt;
agent A asks agent B to do something on A's behalf. B now needs some of&lt;br&gt;
A's authority — and today "some" isn't on the menu. A shares its&lt;br&gt;
credentials (B gets everything), or B uses its own standing credentials&lt;br&gt;
(which quietly accumulate every permission any task ever needed).&lt;/p&gt;

&lt;p&gt;The missing primitive is a &lt;strong&gt;delegation&lt;/strong&gt;: a signed statement that hands&lt;br&gt;
another identity a &lt;em&gt;bounded slice&lt;/em&gt; of your authority. Here's the grammar,&lt;br&gt;
one grant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"res"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp://acme/tools/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"act"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"call"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cav"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1786015180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"max_uses"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"nb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search|fetch"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A resource pattern, the actions allowed on it, and caveats: an expiry, a&lt;br&gt;
use budget, and named request predicates (&lt;code&gt;nb&lt;/code&gt; — the request's &lt;code&gt;tool&lt;/code&gt;&lt;br&gt;
argument must be exactly &lt;code&gt;search&lt;/code&gt; or &lt;code&gt;fetch&lt;/code&gt;). Agent A signs a&lt;br&gt;
&lt;code&gt;Delegation&lt;/code&gt; link handing that grant to B's identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;delegationBinding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parentRefHashRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delegate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@grantor/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bindingHex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;delegationBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;grantsJson&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                     &lt;span class="nx"&gt;epochLabelHex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;epoch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parentRefHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signatureHex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bindingHex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// A's own secp256k1/EIP-191 signer&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;grantsJson&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="nx"&gt;epochLabelHex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;epoch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parentRefHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signatureHex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;B attaches the link to its own deed (its normal login credential) and&lt;br&gt;
presents the pair. The verifying service makes one call —&lt;br&gt;
&lt;code&gt;verifyWithCapabilityAt&lt;/code&gt; — and gets back B's proven identity plus the&lt;br&gt;
grants that survived the chain walk, then enforces each request with&lt;br&gt;
&lt;code&gt;CapabilityGuard.authorize&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rules the verification actually enforces are where this stops being a&lt;br&gt;
JSON convention and becomes a security boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The chain's root must be a registered on-chain key.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every link must narrow its parent.&lt;/strong&gt; Subset of the resources, subset
of the actions, caveats no looser. B can re-delegate to C — &lt;code&gt;search&lt;/code&gt;
only, 5 uses — but a link that widens &lt;em&gt;anything&lt;/em&gt; is a &lt;code&gt;BadDelegation&lt;/code&gt; at
verify time, no matter what any broker claimed at signing time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A pin can't be narrowed — only carried.&lt;/strong&gt; A grant can also pin the
canonical hash of the &lt;em&gt;tool descriptor&lt;/em&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The last link must name the presenter.&lt;/strong&gt; The chain ends at B's proven
&lt;code&gt;sub&lt;/code&gt;; a stolen delegation attached to someone else's deed doesn't
verify.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Revocation is the part I'd underline. Each link is signed against a&lt;br&gt;
&lt;strong&gt;revocation cohort&lt;/strong&gt; (&lt;code&gt;epoch_label&lt;/code&gt;). One transaction —&lt;br&gt;
&lt;code&gt;GrantorRegistry.bumpEpoch&lt;/code&gt; — invalidates every link in that cohort:&lt;br&gt;
A's grant to B, B's narrower grant to C, the whole subtree, everywhere,&lt;br&gt;
on the next check. No calling around to services that cached a token, no&lt;br&gt;
per-child cleanup. Label cohorts per project, per incident domain, per&lt;br&gt;
whatever blast radius you want a single kill-switch for.&lt;/p&gt;

&lt;p&gt;And one property with no OAuth analogue at all: &lt;strong&gt;anonymous delegation.&lt;/strong&gt;&lt;br&gt;
A fleet member can delegate without revealing &lt;em&gt;which member it is&lt;/em&gt; —&lt;br&gt;
&lt;code&gt;delegateZk&lt;/code&gt; authenticates the link with a zero-knowledge membership proof&lt;br&gt;
over the tenant's on-chain registry instead of a recoverable signature.&lt;br&gt;
The verifier learns "a real, current, paid-up member of this fleet granted&lt;br&gt;
this," plus a stable per-tenant pseudonym — and with a per-chain salt, two&lt;br&gt;
delegations from the same member can't even be linked to each other.&lt;/p&gt;

&lt;p&gt;Honest edges: &lt;code&gt;max_uses&lt;/code&gt; rides in the grant but counting real uses needs&lt;br&gt;
durable state only your app can own, so the guard checks it as a bound and&lt;br&gt;
your service (or the broker below) does the metering; the ZK-delegation&lt;br&gt;
proving path is real but the deeper structure-hiding variant is explicitly&lt;br&gt;
preview-labeled; all of it is an unaudited developer preview.&lt;/p&gt;

&lt;p&gt;You can watch this exact arc — grant, delegate narrower, attempt, DENIED,&lt;br&gt;
revoked live on-chain — run in your browser against the Base mainnet&lt;br&gt;
registry: &lt;a href="https://chaingrantor.com/playground.html" rel="noopener noreferrer"&gt;https://chaingrantor.com/playground.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you'd rather not hand-roll the signing, &lt;code&gt;npx -y @grantor/mcp serve&lt;/code&gt;&lt;br&gt;
packages exactly this arc — grant, delegate, check, revoke — as five MCP&lt;br&gt;
tools with the key custody handled for you (and &lt;code&gt;grantor-mcp wrap&lt;/code&gt; makes&lt;br&gt;
it enforcing for any stdio MCP server); that's what post #1 walked&lt;br&gt;
through: &lt;a href="https://dev.to/grantor/give-your-ai-sub-agent-a-budget-not-your-keys-2e7h"&gt;https://dev.to/grantor/give-your-ai-sub-agent-a-budget-not-your-keys-2e7h&lt;/a&gt;&lt;br&gt;
Full grammar and verification docs:&lt;br&gt;
&lt;a href="https://chaingrantor.com/docs/guide/capabilities" rel="noopener noreferrer"&gt;https://chaingrantor.com/docs/guide/capabilities&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP standardized what agents can call. A2A standardized how they talk.&lt;br&gt;
Delegation is what authority looks like when it moves between them — and&lt;br&gt;
it should only ever get smaller.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>mcp</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Auth for your MCP server, without running an authorization server</title>
      <dc:creator>Grantor</dc:creator>
      <pubDate>Tue, 01 Sep 2026 00:05:19 +0000</pubDate>
      <link>https://dev.to/grantor/auth-for-your-mcp-server-without-running-an-authorization-server-1o6h</link>
      <guid>https://dev.to/grantor/auth-for-your-mcp-server-without-running-an-authorization-server-1o6h</guid>
      <description>&lt;p&gt;You built a remote MCP server. It's useful, so now strangers' agents want&lt;br&gt;
to call it, and you need to answer the question every remote server hits:&lt;br&gt;
who's allowed in?&lt;/p&gt;

&lt;p&gt;The MCP spec's answer is OAuth 2.1: put an authorization server in front,&lt;br&gt;
register clients, issue tokens. Which is correct, and heavy. An&lt;br&gt;
authorization server is a stateful, security-critical service with a user&lt;br&gt;
database — you run it or you rent it, and either way it's now load-bearing&lt;br&gt;
infrastructure for what might be a weekend project. It also assumes your&lt;br&gt;
callers can do an OAuth dance, and that you &lt;em&gt;want&lt;/em&gt; accounts.&lt;/p&gt;

&lt;p&gt;Here's the shape with no authorization server anywhere: the credential —&lt;br&gt;
called a &lt;strong&gt;deed&lt;/strong&gt; — certifies itself. An agent mints one locally by signing&lt;br&gt;
a challenge you issued (or by proving fleet membership in zero knowledge —&lt;br&gt;
more below). Your server verifies it with a library: local crypto plus one&lt;br&gt;
&lt;code&gt;eth_call&lt;/code&gt; to a public registry contract on Base, served by any RPC&lt;br&gt;
provider. No token service, no client registration, no user table. Your&lt;br&gt;
whole auth stack is three routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DeedVerifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sessionJwt&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@grantor/verify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;grantorExpress&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@grantor/verify/express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Registry&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@grantor/verify/registry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DeedVerifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;RPC_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;CHAIN_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TENANT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;AUDIENCE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ORIGIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MAX_TTL_SECS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CACHE_TTL_SECS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;grantorExpress&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;challengeEndpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/auth/challenge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CHAIN_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;modes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user-sig&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;agent-zk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;vouchSignature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VOUCH_SIGNATURE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;vouchEpoch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VOUCH_EPOCH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;vouchExp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VOUCH_EXP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/auth/challenge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one call also auto-publishes &lt;code&gt;GET /.well-known/grantor-deed&lt;/code&gt; — a&lt;br&gt;
discovery document naming your tenant, chain, modes, and challenge&lt;br&gt;
endpoint — and self-checks it at startup, so a misconfiguration fails your&lt;br&gt;
boot, not your first user's login.&lt;/p&gt;

&lt;p&gt;The second route exchanges a deed for a session, the way a token endpoint&lt;br&gt;
would — an MCP client authenticates once, not per request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/auth/token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;deed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;challenge&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deed&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// claims.sub is a verified, pseudonymous subject — recomputed by the&lt;/span&gt;
  &lt;span class="c1"&gt;// verifier, not read from the deed. Mint YOUR session from it:&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;session_jwt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sessionJwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AUDIENCE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TENANT_ID&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;SIGNING_KEY_PEM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TTL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;iss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ORIGIN&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sessionJwt&lt;/code&gt; mints a plain ES256 JWT with &lt;em&gt;your&lt;/em&gt; key — any JOSE library&lt;br&gt;
verifies it without ever importing this SDK. The third route is your&lt;br&gt;
existing MCP transport, gated on that session. That's the entire surface.&lt;/p&gt;

&lt;p&gt;Three properties you don't usually get from a weekend auth setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rejections teach the caller.&lt;/strong&gt; Every 401 carries &lt;code&gt;WWW-Authenticate:
Grantor-Deed …&lt;/code&gt; plus &lt;code&gt;discovery&lt;/code&gt;/&lt;code&gt;learn&lt;/code&gt; fields pointing at your
discovery document and a machine-readable onboarding manifest. The guard
also serves RFC 9728 protected-resource metadata, so an MCP-spec OAuth
client discovers what your server needs the standard way. A capable
agent that gets rejected can read its way to authenticated — no human
writes an integration ticket. (All of it opt-out with one flag if you
want silent 401s.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can authenticate a whole fleet without an allowlist.&lt;/strong&gt; With
&lt;code&gt;agent-zk&lt;/code&gt; in &lt;code&gt;modes&lt;/code&gt;, any agent enrolled in your tenant's on-chain
registry proves membership in zero knowledge — no per-agent config on your
server, and you learn a stable pseudonym per agent, not a wallet address.
Membership answers &lt;em&gt;who may call&lt;/em&gt;; it does not answer &lt;em&gt;what they may do&lt;/em&gt;.
For consequential tools, pair it with a capability grant — a resource plus
caveats, narrowed at every delegation hop and checked locally per
request — so a valid subject doesn't silently gain reach as you add tools.
&lt;code&gt;npx -y @grantor/mcp wrap&lt;/code&gt; puts that in front of any stdio MCP server
without code changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing is enforced where verification happens.&lt;/strong&gt; The verifier checks
the tenant's on-chain status during the same read, and fails closed if
the chain is unreachable. Nobody can verify deeds against a lapsed
tenant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A session is a bearer token, and behaves like one.&lt;/strong&gt; &lt;code&gt;sessionJwt&lt;/code&gt; mints
from verified claims with &lt;em&gt;your&lt;/em&gt; key and ignores the deed's own expiry — so
an already-issued session outlives a tenant lapse or a revoked grant until
it expires. Deed verification is a library call plus one &lt;code&gt;eth_call&lt;/code&gt;, not a
round trip to an introspection service, so re-verifying per call on
consequential routes costs you a chain read, not an architecture. Short
sessions for cheap reads; re-verify where it matters. Full timing model:
&lt;a href="https://chaingrantor.com/docs/guide/revocation-latency.html" rel="noopener noreferrer"&gt;https://chaingrantor.com/docs/guide/revocation-latency.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it costs, honestly: you need a tenant on the registry (&lt;code&gt;createTenant&lt;/code&gt;&lt;br&gt;
plus USDC funding on Base — a few contract calls, no signup form, because&lt;br&gt;
there is no server to sign up with) and a signed origin vouch for wherever&lt;br&gt;
your server runs, which is what lets a well-behaved agent refuse to&lt;br&gt;
authenticate to a hostile origin impersonating you. The whole thing is an&lt;br&gt;
unaudited developer preview. The guard ships in TypeScript, Python, Go,&lt;br&gt;
and Rust, so this isn't an Express-only story.&lt;/p&gt;

&lt;p&gt;Full guide, transcribed from a runnable reference server:&lt;br&gt;
&lt;a href="https://chaingrantor.com/docs/guide/mcp-server" rel="noopener noreferrer"&gt;https://chaingrantor.com/docs/guide/mcp-server&lt;/a&gt; — and #1 in this series&lt;br&gt;
covers the other direction, gating what your own sub-agents may do&lt;br&gt;
(including wrapping any third-party MCP server so enforcement is&lt;br&gt;
structural, not voluntary):&lt;br&gt;
&lt;a href="https://dev.to/grantor/give-your-ai-sub-agent-a-budget-not-your-keys-2e7h"&gt;https://dev.to/grantor/give-your-ai-sub-agent-a-budget-not-your-keys-2e7h&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OAuth told us auth needs an authorization server. For agents, it needs an&lt;br&gt;
authorization — the server part turns out to be optional.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Update: two corrections above, prompted by a good question in the comments.&lt;br&gt;
The fleet bullet originally read "membership is the authorization," which is&lt;br&gt;
true of identity and false of authorization; and the post didn't state that a&lt;br&gt;
minted session outlives on-chain revocation. Full timing model, including what&lt;br&gt;
is and isn't cached:&lt;br&gt;
&lt;a href="https://chaingrantor.com/docs/guide/revocation-latency.html" rel="noopener noreferrer"&gt;https://chaingrantor.com/docs/guide/revocation-latency.html&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Give your AI agent GitHub access that expires in an hour</title>
      <dc:creator>Grantor</dc:creator>
      <pubDate>Tue, 25 Aug 2026 21:16:33 +0000</pubDate>
      <link>https://dev.to/grantor/give-your-ai-agent-github-access-that-expires-in-an-hour-c65</link>
      <guid>https://dev.to/grantor/give-your-ai-agent-github-access-that-expires-in-an-hour-c65</guid>
      <description>&lt;p&gt;You set up an agent to triage issues overnight. It needs GitHub, so you do&lt;br&gt;
the obvious thing: create a PAT, drop it in the environment, go to bed.&lt;/p&gt;

&lt;p&gt;Now inventory what you actually deployed. The token is in the environment —&lt;br&gt;
so it's in the environment of every sub-agent the orchestrator spawns, every&lt;br&gt;
tool process they exec, every log line that dumps &lt;code&gt;env&lt;/code&gt; on a crash. It's&lt;br&gt;
valid for however long you set at creation (days, realistically — nobody&lt;br&gt;
regenerates a PAT per task). And if one worker goes sideways at 3am, your&lt;br&gt;
revocation story is "delete the token", which also kills every &lt;em&gt;other&lt;/em&gt;&lt;br&gt;
agent, pipeline, and script using it.&lt;/p&gt;

&lt;p&gt;GitHub's own answers help less than you'd hope. Fine-grained PATs scope&lt;br&gt;
nicely but you mint them by hand in a web UI — there's no "create a&lt;br&gt;
one-hour token for this sub-agent" API call. GitHub Apps &lt;em&gt;do&lt;/em&gt; mint&lt;br&gt;
short-lived installation tokens, but now you're operating token-minting&lt;br&gt;
infrastructure, and the token still doesn't subdivide: two sub-agents under&lt;br&gt;
one installation hold the same authority.&lt;/p&gt;

&lt;p&gt;Here's the reframe that makes this tractable: &lt;strong&gt;the sub-agent never needed&lt;br&gt;
the token. It needed permission to perform three specific GitHub actions,&lt;br&gt;
for an hour.&lt;/strong&gt; Those are different things, and conflating them is the whole&lt;br&gt;
problem.&lt;/p&gt;

&lt;p&gt;So separate them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The PAT lives in exactly &lt;strong&gt;one&lt;/strong&gt; process — the tool server that actually
calls the GitHub API (your GitHub MCP server, running in the parent's
trust domain). It never enters an agent's context window or environment.&lt;/li&gt;
&lt;li&gt;Sub-agents get a &lt;strong&gt;capability&lt;/strong&gt;: a signed, bounded permission to invoke
named tools on that server. The tool server checks the capability before
every call.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The broker that does step 2 is an MCP server you run locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; @grantor/mcp serve
&lt;span class="c"&gt;# or wire it into Claude Code:&lt;/span&gt;
claude mcp add grantor-mcp &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; @grantor/mcp serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Want to watch a denial happen before wiring anything real?&lt;br&gt;
&lt;code&gt;npx -y @grantor/mcp demo&lt;/code&gt; — thirty seconds, zero setup, and the refusal&lt;br&gt;
happens in your own terminal.)&lt;/p&gt;

&lt;p&gt;The overnight-triage grant looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grant    {tools: ["issue_read", "search_issues", "add_issue_comment"],
          max_uses: 50, ttl_secs: 3600}
         → {child_id: "…", sub: "…"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are GitHub's official MCP server's real tool names&lt;br&gt;
(&lt;code&gt;ghcr.io/github/github-mcp-server&lt;/code&gt;). Any stdio MCP server wraps the same&lt;br&gt;
way — the broker doesn't care what's behind the &lt;code&gt;--&lt;/code&gt;.&lt;br&gt;
The sub-agent gets the &lt;code&gt;child_id&lt;/code&gt; (never a key; the broker holds those),&lt;br&gt;
and the tool server gates every invocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;check    {child_id, tool: "add_issue_comment"}
         → {allow: true, remaining_uses: 49}

check    {child_id, tool: "merge_pull_request"}
         → {allow: false, code: "CapabilityDenied"}   # never granted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An hour later the grant's expiry caveat has passed and every &lt;code&gt;check&lt;/code&gt; on&lt;br&gt;
that child is denied (&lt;code&gt;CapabilityDenied&lt;/code&gt;, naming the failed caveat) — with&lt;br&gt;
the PAT untouched. If the triage agent spawns its own worker to draft comments,&lt;br&gt;
it can pass down a strictly narrower slice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;delegate {parent: child_id, tools: ["add_issue_comment"], max_uses: 10}
         → {child_id: "…"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Asking for a tool the parent doesn't hold is refused before anything is&lt;br&gt;
signed; asking for more uses or a longer expiry silently clamps to the&lt;br&gt;
parent's bound.&lt;/p&gt;

&lt;p&gt;And if your GitHub tool server speaks MCP, you don't even need it to call&lt;br&gt;
&lt;code&gt;check&lt;/code&gt; voluntarily — put it behind the enforcing proxy and the boundary&lt;br&gt;
is structural. Don't know what your server exposes? Ask it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; @grantor/mcp tools &lt;span class="nt"&gt;--&lt;/span&gt; docker run &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; GITHUB_PERSONAL_ACCESS_TOKEN ghcr.io/github/github-mcp-server
&lt;span class="c"&gt;# lists its 44 tools, marks the write-shaped ones, and prints a&lt;/span&gt;
&lt;span class="c"&gt;# ready-to-paste wrap suggestion (a name heuristic — review it)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reviewing is not optional theater: the suggestion holds &lt;code&gt;add_issue_comment&lt;/code&gt;&lt;br&gt;
back as write-shaped — correctly — and the triage bot's whole job is&lt;br&gt;
commenting, so you add that one back on purpose. The heuristic proposes;&lt;br&gt;
you decide. Then wrap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;grantor-mcp wrap &lt;span class="nt"&gt;--tools&lt;/span&gt; issue_read,search_issues,add_issue_comment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-uses&lt;/span&gt; 50 &lt;span class="nt"&gt;--ttl-secs&lt;/span&gt; 3600 &lt;span class="nt"&gt;--&lt;/span&gt; docker run &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; GITHUB_PERSONAL_ACCESS_TOKEN ghcr.io/github/github-mcp-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Note where the PAT lives: &lt;code&gt;-e GITHUB_PERSONAL_ACCESS_TOKEN&lt;/code&gt; with no value&lt;br&gt;
forwards it from your shell into the server's container — the one process&lt;br&gt;
from step 1 — and nowhere an agent can read. The official server also has&lt;br&gt;
its own &lt;code&gt;--read-only&lt;/code&gt; and toolset flags, and they compose fine; what the&lt;br&gt;
wrap adds is the part flags can't: per-sub-agent budgets, expiry,&lt;br&gt;
delegation that only narrows, and one-call revocation.)&lt;/p&gt;

&lt;p&gt;Now the server never sees a request that wasn't authorized, and the&lt;br&gt;
sub-agent's tool list only contains what you granted. When the agent&lt;br&gt;
reaches for the merge button anyway, this is what lands in your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─ DENIED ───────────────────────────────────────────────────────────────────┐
│  requested      merge_pull_request                                         │
│  grant allows   issue_read, search_issues, add_issue_comment               │
│  refused as     CapabilityDenied — tool not granted                        │
│                                                                            │
│  Denied by the grant, not by a prompt — the server never saw the call.     │
│  revoke everything:   grantor-mcp revoke --child 7c9e12aa-triage           │
│  what just happened:  https://chaingrantor.com/docs/guide/first-denial.html│
└────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this isn't a policy file the broker consults — the&lt;br&gt;
delegation chain is cryptographically signed link by link, and every&lt;br&gt;
&lt;code&gt;check&lt;/code&gt; re-verifies the narrowing math, so a child can't widen its slice&lt;br&gt;
even if the broker's pre-check drifted.&lt;/p&gt;

&lt;p&gt;The 3am story is one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;revoke   {child_id}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your own tenant that bumps a revocation epoch on a public smart contract&lt;br&gt;
on Base, and every capability in that cohort — the triage agent &lt;em&gt;and&lt;/em&gt; the&lt;br&gt;
worker it delegated to — fails its next &lt;code&gt;check&lt;/code&gt;, no matter which process&lt;br&gt;
holds it. The PAT keeps working for everything else you own. That's the&lt;br&gt;
inversion worth noticing: revocation used to be the expensive, blast-radius&lt;br&gt;
operation, and now it's the cheap, surgical one.&lt;/p&gt;

&lt;p&gt;Honest limits, same as last time: this is an unaudited developer preview;&lt;br&gt;
the zero-setup run points at a live shared sandbox tenant on the production&lt;br&gt;
registry (real chain, real verification, a publish-on-purpose demo key that&lt;br&gt;
controls nothing outside the sandbox); use-metering is local to your broker&lt;br&gt;
process. And the capability bounds &lt;em&gt;access to your tool server&lt;/em&gt; — GitHub&lt;br&gt;
itself still sees one PAT, so the tool server remains the enforcement&lt;br&gt;
point. That's the design, not a gap: one place holds the secret, everything&lt;br&gt;
else holds bounded, expiring, revocable permission to use it.&lt;/p&gt;

&lt;p&gt;Production is one contract call away (USDC on Base, no signup — the&lt;br&gt;
register page even takes ETH or any liquid token and swaps it on-chain&lt;br&gt;
inside your own transaction). The 60-second walkthrough of everything&lt;br&gt;
above, ending in a mid-flight revocation:&lt;br&gt;
&lt;a href="https://chaingrantor.com/docs/guide/first-denial" rel="noopener noreferrer"&gt;https://chaingrantor.com/docs/guide/first-denial&lt;/a&gt; — broker docs:&lt;br&gt;
&lt;a href="https://chaingrantor.com/docs/guide/mcp-broker" rel="noopener noreferrer"&gt;https://chaingrantor.com/docs/guide/mcp-broker&lt;/a&gt; — or watch the whole&lt;br&gt;
grant → narrow → deny → revoke arc run live against mainnet in your&lt;br&gt;
browser at &lt;a href="https://chaingrantor.com/playground.html" rel="noopener noreferrer"&gt;https://chaingrantor.com/playground.html&lt;/a&gt;. The first post in&lt;br&gt;
this series covers the general budget model:&lt;br&gt;
&lt;a href="https://dev.to/grantor/give-your-ai-sub-agent-a-budget-not-your-keys-2e7h"&gt;https://dev.to/grantor/give-your-ai-sub-agent-a-budget-not-your-keys-2e7h&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your agents don't need credentials. They need permissions with an expiry&lt;br&gt;
date.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>github</category>
      <category>agents</category>
    </item>
    <item>
      <title>Give your AI sub-agent a budget, not your keys</title>
      <dc:creator>Grantor</dc:creator>
      <pubDate>Fri, 21 Aug 2026 19:06:18 +0000</pubDate>
      <link>https://dev.to/grantor/give-your-ai-sub-agent-a-budget-not-your-keys-2e7h</link>
      <guid>https://dev.to/grantor/give-your-ai-sub-agent-a-budget-not-your-keys-2e7h</guid>
      <description>&lt;p&gt;Spawn a sub-agent in CrewAI, LangGraph, AutoGen, or a Claude sub-agent&lt;br&gt;
setup and check what it actually holds: your credentials. The parent's&lt;br&gt;
keys, at full scope, forever. The throwaway agent you created to summarize&lt;br&gt;
three PDFs can call every tool your orchestrator can, and the only&lt;br&gt;
"revocation" is rotating keys everywhere at once.&lt;/p&gt;

&lt;p&gt;We accept this because handing a child &lt;em&gt;less&lt;/em&gt; than everything has been&lt;br&gt;
genuinely hard: OAuth scopes need an authorization server someone runs;&lt;br&gt;
role systems need an admin; API keys don't subdivide. So the ecosystem&lt;br&gt;
quietly standardized on "copy the parent's environment" and moved on.&lt;/p&gt;

&lt;p&gt;Here's a different shape, as an MCP server you run locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; @grantor/mcp serve
&lt;span class="c"&gt;# or wire it into Claude Code:&lt;/span&gt;
claude mcp add grantor-mcp &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; @grantor/mcp serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives any MCP-speaking framework five tools. The whole model fits in&lt;br&gt;
one transcript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grant    {tools: ["search","fetch"], max_uses: 20, ttl_secs: 3600}
         → {child_id: "…", sub: "…"}          # a bounded child identity

check    {child_id, tool: "search"}
         → {allow: true, remaining_uses: 19}   # gate EVERY action on this

check    {child_id, tool: "write"}
         → {allow: false, code: "CapabilityDenied"}   # not granted → denied

delegate {parent: child_id, tools: ["search"], max_uses: 5}
         → {child_id: "…"}                     # a narrower grand-child

revoke   {child_id}
         → revoked                             # authority withdrawn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting properties are in what you &lt;em&gt;can't&lt;/em&gt; do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A child can never widen its slice.&lt;/strong&gt; Asking &lt;code&gt;delegate&lt;/code&gt; for a tool the
parent doesn't hold is refused before anything is signed. Asking for
more uses or a longer expiry silently clamps to the parent's bound.
This isn't a policy file the framework consults — the delegation chain
is cryptographically signed link by link, and verification re-checks the
narrowing math on every &lt;code&gt;check&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budgets actually run out.&lt;/strong&gt; &lt;code&gt;max_uses: 20&lt;/code&gt; means the 21st &lt;code&gt;check&lt;/code&gt; is
denied with &lt;code&gt;UsesExhausted&lt;/code&gt;, not logged-and-allowed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revocation is real.&lt;/strong&gt; In your own tenant, &lt;code&gt;revoke&lt;/code&gt; bumps a revocation
epoch on a public smart contract; every capability in that cohort fails
its next &lt;code&gt;check&lt;/code&gt; no matter which process holds it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the part that makes this different from every "policy engine" you've&lt;br&gt;
seen: &lt;strong&gt;there is no server.&lt;/strong&gt; No authorization service, no policy backend,&lt;br&gt;
no vendor API in the hot path. Authority anchors to a public registry&lt;br&gt;
contract on Base; verification is a local computation plus one &lt;code&gt;eth_call&lt;/code&gt;&lt;br&gt;
that any RPC provider can serve. The broker runs on your machine, next to&lt;br&gt;
the framework it guards, and holds the child keys so your agents never see&lt;br&gt;
key material at all.&lt;/p&gt;

&lt;p&gt;The zero-setup run above works because the package ships pointed at a&lt;br&gt;
live shared sandbox tenant on the production registry — real chain, real&lt;br&gt;
verification, publish-on-purpose demo key that controls nothing outside&lt;br&gt;
the sandbox. Honest limits: it's an unaudited developer preview, the&lt;br&gt;
sandbox broker self-issues its anti-replay challenge (it's holder and&lt;br&gt;
verifier in one process), and use-metering is local to the broker.&lt;/p&gt;

&lt;p&gt;Production is one contract call away (USDC on Base, no signup — your&lt;br&gt;
agent can even read the machine-readable onboarding manifest and do it&lt;br&gt;
itself). Docs: &lt;a href="https://chaingrantor.com/docs/guide/mcp-broker" rel="noopener noreferrer"&gt;https://chaingrantor.com/docs/guide/mcp-broker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP standardized what agents can&lt;br&gt;
call. A2A standardized how they talk. Nobody standardized what they're&lt;br&gt;
allowed to do — that's the layer this fills.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>mcp</category>
      <category>security</category>
    </item>
  </channel>
</rss>
