Repository navigation
spec: v0.3 - #18
Merged
Merged
spec: v0.3#18
Conversation
The v0.3 contract, and the scaffolding items 1-7 build against. No implementation: src/ is untouched and the 1269 tests of v0.1 and v0.2 pass unchanged. v0.1 and v0.2 could see the action and nothing else. A policy condition naming the principal is refused at load, deliberately, and that rule survives this release. v0.3 adds a second and separate axis - who is acting, and what are they entitled to - and combines the two results as the stricter of the pair. Three rules the document is built on. Authority is opt-in as a section and fail-closed once present: no authority: key means v0.2 behaviour exactly, one authority: key means every principal needs a grant. Attenuation is structural: a delegated grant is valid only if it is provably a subset of its parent, on every dimension, at creation and again at every evaluation, and omitting a dimension the parent constrains is a rejected delegation rather than an unconstrained one. Identity is consumed, never invented: principals come from an IdentityProvider, and claims are receipt data rather than action identity, so an approval survives a token rotation. Four decisions are worth a reviewer's attention because the first draft got them wrong and an adversarial pass caught them. A delegation may change who acts, not how many. Its subject may carry no wildcard, and where its parent names a user the child must name one the parent admits. Without both rules one delegation hands a grant to every agent in the deployment - along with the right to pass it on - or strips the human the authority was bound to, and every containment check still reports "contained". A grant carries no decision. An earlier draft gave one an optional decision: allow | approve so that "this delegate always needs a human" was expressible. It deadlocked the gateway's approval pre-check, which keys off the policy decision; it needed a containment row whose default was the loosest value, which is the shape the containment table exists to refuse; and it made whether an action needs a human depend on who proposed it, which is the sentence CLAIMS.md maps to code. Authority permits or denies. Per-action autonomy stays in policy. Revocation and expiry are live; an edit to the document is not. Grants are read when the document is loaded, so narrowing a root grant takes effect on the next load, and the runtime kill switch is ctrlrun revoke, which covers delegations only. The prose that promised otherwise is corrected rather than the behaviour invented. The JWT provider must check typ. Checking iss, aud and exp alone accepts an OIDC ID token from the same issuer, signed with the same key, whenever the operator configured audience to that client id - RFC 8725 section 2's cross-JWT confusion, live in the first draft. token_type is a required argument with no default, and the symmetric-key guard moved to the configuration end, where it can actually refuse something. Section 1.3 records what was read, with URLs, dates and status, and the document makes no compliance, conformance or alignment claim anywhere. The short version: NIST has published nothing normative on agent identity, and its one dedicated document is a concept paper with no RFC 2119 keyword in it whose open questions are the ones this specification answers. MCP at 2026-07-28 defines no principal, actor or delegation primitive, and puts an explicit warning label on the only two fields that look like identity - which is why --principal-from-client-info is removed here. In OAuth the stable ground is JWT verification; everything agent-shaped is a draft and most of it is an individual submission with no IETF standing. A SPIFFE JWT-SVID is identity and explicitly not authorization. And section 5's "contained at creation and re-checked at evaluation" is a different guarantee from the append-only one macaroons and UCAN give, not a strictly stronger one; two of its dimensions have no prior art at all, and it says so. Scaffolding: version 0.3.0.dev0, an identity extra that is empty until item 5 adds the verifier and the pyjwt line it needs, a CHANGELOG Unreleased section that says plainly that nothing has shipped, and CI plus the publish gate installing the new extra so item 5 does not have to touch them.
ruff format reformats Python code blocks inside Markdown, and the v0.3 blocks are valid Python, so it does. The v0.1 and v0.2 spec blocks escape it only because they carry unparseable placeholders (action_id: str = <generated>), which is luck rather than a convention. Aligned trailing comments go; nothing else changes.
Both are defects this document introduced, not carried over.
§4.6 contradicted itself on which grant is named when several match. It said the
action "passes on the first that does" -- order-dependent -- and then said the
result MUST NOT depend on the order grants appear in the document, with T70b
asserting an identical grant_id after permuting the file. Both cannot hold. Any
matching grant authorizes the action, so which one is named needs its own rule, and
evidence must not turn on incidental file order for the same reason §4.3 fixes the
deny-reason precedence. AuthorityResult.grant_id is now the matching grant whose id
sorts first by codepoint, which is a property of the set rather than of the
document.
§5.4's subject row banned a wildcard in a delegation's subject but did not require
agent to be present, and §4.2 makes an absent subject.agent match any agent. So a
child of {agent: finance-agent, user: alice} could declare {user: alice@example.com}
with no agent key, carry no wildcard, satisfy the user rule, pass containment, and
hand the grant to every agent acting for alice. That is the grantee-population
widening the row was added to prevent, reached by omission instead of by an
asterisk -- and §5.4's own rule already answers it: omission is never unlimited. A
delegation's subject must now declare a concrete agent.
Also: §9 gained the row for §5.6 rule 3, an expired ancestor, which T77 tests and
the table had no line for; and §6.3 said blocked_reason may be one of §4.3's
authority reasons, which since authority_grant joined that set would have admitted
a pass reason into a field that records what stopped an action.
T76 and T81 gain the omitted-agent case; T70b asserts the sorted id rather than
whichever grant the implementation happened to reach first.
arpanghoshal
force-pushed
the
item-0-spec-v0.3
branch
from
September 4, 2026 05:12
e25a014 to
ede2a3c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Item 0 of the v0.3 build list: the contract, and the scaffolding that lets items 1–7 be built
against it. No implementation.
src/is untouched; the 1269 tests of v0.1 and v0.2 passunchanged.
What v0.3 is
v0.1 and v0.2 could see the action and nothing else. A policy condition naming the principal is
refused at load, deliberately, and that rule survives this release. v0.3 adds a second and
separate axis — who is acting, and what are they entitled to — and combines the two results as
the stricter of the pair.
authority:section → v0.2 behaviour exactly.One
authority:section → every principal needs a grant, and no grant means DENY. There is nohalf-way and no flag that restores one.
its parent, on every dimension, at creation and again at every evaluation. Omitting a
dimension the parent constrains is a rejected delegation, never "unconstrained".
IdentityProvider. Claims arereceipt data rather than action identity, so an approval survives a token rotation.
What is in the diff
docs/SPEC-v0.3.md— §1 scope and what was read · §2Principal· §3IdentityProvider·§4 authority · §5 delegation · §6 observe mode · §7 events · §8 gateway · §9 the fail-closed
table · §10 acceptance tests T60–T93 · §11 the frozen API · §12 schema versions · §13 out of
scope.
pyproject.toml— version0.3.0.dev0and anidentityextra, empty until item 5 adds the JWTverifier and the
pyjwtline it needs.CHANGELOG.md— an Unreleased section that says plainlythat nothing has shipped. CI and the publish gate install the new extra, so item 5 does not have
to touch them.
The four things a reviewer should look at first
Each is a place the first draft was wrong and an adversarial review pass caught it. They are
the reason this PR is worth reading rather than skimming.
A delegation may change who acts, not how many. §5.4 now has a
subjectrow. Whichconcrete agent the child names is free — that is what delegation is for — but it MUST name one:
present, with no wildcard, and with a
userthe parent's pattern admits wherever the parentdeclares one. Without all of that, one delegation hands the grant to every agent in the
deployment along with the right to pass it on (
subject: {agent: "*"}), or strips the human theauthority was bound to (parent
{agent: finance-agent, user: alice}→ child{agent: finance-agent}) — and every containment check still reports "contained".A later review pass found the same escalation reachable by omission: §4.2 makes an absent
subject.agentmean "any agent", so a child declaring only{user: alice@example.com}carriesno wildcard, satisfies the
userrule, and still widens the grantee population. §5.4's own ruledecides it — omission is never "unlimited" — so the row demands the key, not just the absence of
an asterisk.
A grant carries no
decision:. An earlier draft gave one an optionalallow | approve, sothat a delegation could say "may refund, but always with a human", and so T70's "3×3" was
literally 3×3. It is gone. It deadlocked the gateway's approval pre-check, which keys off the
policy decision (
v0.2 §6.10); it needed a containment row whose default was the loosest value,which is the shape §5.4 exists to refuse; and it made whether an action needs a human depend on
who proposed it — the sentence
docs/CLAIMS.mdmaps to code. Authority permits or denies, thetable is 2×3, and §4.2 records the deviation from the brief.
Revocation and expiry are live; an edit to the document is not. §5.6's first draft claimed a
narrowed root grant propagated to every delegation beneath it. Nothing reloads: an
Authorityisbuilt when the document is loaded. Revocation and expiry are live, because they are read from
the store and the clock on every evaluation. The file half now says what actually happens, the
runtime kill switch is named (
ctrlrun revoke, delegations only), and hot reload and aroot-grant revoke are in §13 rather than implied by prose.
The JWT provider must check
typ. Checkingiss,audandexpalone accepts an OIDC IDtoken from the same issuer, signed with the same key, whenever the operator configured
audienceto that client id — RFC 8725 §2's cross-JWT confusion, live in the first draft.token_typeis now a required argument with no default,audis checked by membership on bothwire shapes, and the symmetric-key guard moved to the configuration end where it can refuse
something (
secret=is its own parameter, three-way exclusive withjwks_urlandpublic_key).Other decisions worth a look
context()?context()fills in where it declines — except once anauthority:section is loaded, when a decline is not backfilled at all. Omitting a credential must not be an easier route than forging one.prefix*, and a final**whose prefix segments are all literal.*never crosses a separator;**is the only spelling for "everything". Chosen so containment is decidable, and**counts as a segment so every pattern contains itself.Control.delegate/Control.revoke.Authorityonly plans — reads and returns, never writes — soARCHITECTURE§6 keeps one composer and the delegation events reach everyEventSinkrather than only theeventstable.Control.evaluateAMBIGUOUSis exactly whatv0.1 §5.2reserves for a human.idsorts first — a property of the set, not of the file. An earlier draft said "the first that does", which contradicted its own order-independence guarantee.ctrlrun.receipt/v2—resultgains a value a v1 reader does not know. §12.2 says what actually breaks: a ≤ 0.2 process reading a store an observe-mode 0.3 process wrote raises, so upgrade readers before switching a writer.int-only. The fail-open is already closed by a load error; the spec now says so instead of leaving it to be rediscovered.Standards posture
§1.3 records what was read, with URLs, dates and status. The document makes no compliance,
conformance or alignment claim anywhere.
concept paper (Initial Public Draft, 2026-02-05) with no RFC 2119 keyword in it, whose open
questions are the ones this spec answers. IR 8587's one consumer-facing MUST is quoted as the
draft guidance it is.
warning label on the only two fields that look like identity — which is why
--principal-from-client-infois removed here.agent-shaped is a draft and most of it is an individual submission with no IETF standing. RFC
8693 §4.1 is why the delegation chain lives in CTRLRun's own store rather than in a token.
macaroons and UCAN give, not a strictly stronger one, and two of its dimensions have no prior
art at all. Both are said rather than implied.
Checks
pytest1269 passed ·mypy --strict srcclean ·ruff checkclean ·pip install -e '.[identity]'resolves.Next: item 1, tests first.