You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Index and open questions for the agent-identity work. Not an epic — the questions below are deliberately not distributed into the epics, because a question spread across seven issues is a question nobody answers.
The set
All of them are linked as sub-issues of this one, so GitHub tracks the progress bar.
Sequencing: #367 is the only unblocked one and everything waits on its owner field. #371 branches off #367 without needing #368. #370 needs #368 and #369 both. #375 runs before any work in which mecatl signs something.
Removed from this table: #385 (event-log integrity) closed as not planned — it depended on a snapshot MAC from #374 that was never actually implemented (#374 shipped only Redis auth/TLS). #376 (the escalation proof) closed and moved under #478 ("mecatl as its own SPIFFE issuer") rather than tracked standalone here — its remaining blocker (toolhive#6204, scope intersection) is SPIFFE/identity plumbing #478 already owns.
Decided
SPIFFE client authentication: JWT-SVID assertion. Familiar, least new machinery, and vMCP already handles JWTs. X.509-SVID would need work in both repos; WIT-SVID is overkill for now. Update:Embedded auth server should accept SPIFFE SVIDs as client credentials toolhive#6199 commits ToolHive to bothspiffe_jwt and spiffe_x509, resolving to one authenticated principal with one client association and one authorization outcome, with WIT-SVID explicitly out of scope. So the "X.509 needs work in both repos" half of the rationale no longer holds on the ToolHive side; spiffe_jwt remains our pick on mecatl-side cost, and the choice is now reversible rather than constrained.
External authorization is definition-based. Instance is useful for auditing, not for authorization. Reinforced since: no IETF draft ties consent or authorization to a running instance, so instance-based external authz has no counterparty specification today.
The driver protocol is declared trusted now, with generic enforcement middleware in the mid term. The half owed immediately is the written deployment requirement, on One caller cannot reach another's work #368.
The access token is a profiled JWT, offline-verified. The claims contract is the sticky part; the carrier is reversible, so introspection stays available later as an additive check.
Per-agent consent is a requirement, and the consent model is spike work. The authorization half is definition-keyed and AS-mediated, buildable on the two-leg shape now. The record — who holds it, who signs it, what it is keyed on — needs a proper design pass and is deliberately left open on A scheduled run acts with bounded, consented authority #373. What is established is that no IETF draft ties consent to a running instance and every draft that scopes it puts a signed artifact keyed on the definition, so the gate does not require mecatl to sign anything.
The escalation proof runs in e2e, and the cost is accepted.
Bundle distribution or a static key for the FS grant verifier. The grant issuer is in-process with the harness but the FS service is a separate process, so one in-house verifier exists. A static public key satisfies it until you want rotation without redeploying verifiers, or a second consumer.
The consent model on A scheduled run acts with bounded, consented authority #373: who holds the record, who signs it, what it is keyed on, where consent is captured for unattended work, and whether it must verify with the authorization server unreachable. Spike first, deliberately unsettled.
The per-spawn round trip. The design's call 2 (per-spawn narrowing) carries no caching note, so every subagent spawn depends on the authorization server being reachable, including a child that only reads files. Cache it, move narrowing to the internal issuer, or accept it. On The harness acts for a user against an external system #372.
Sequenced, no longer deferred
No prescribed naming scheme; the flag validates and the docs recommend. Since the boundary is per-deployment, the name is a config value, and mecatl is deployed by other people — prescribing a shape for someone else's org domain is presumptuous. --identity-domain is required when identity is enabled, and validated against the spec grammar: lowercase, [a-z0-9.\-_], at most 255 bytes, no userinfo or port, no percent-encoding. The docs recommend a domain you own, because names are nominally self-registered with no global uniqueness guarantee and a collision means being unable to federate. Never derive the name from a bundle-endpoint hostname; the federation spec gives a concrete subdomain-squatting attack for that. Accepted cost: deployments will not look alike.
The trust-domain boundary is one domain per deployment. Tenants are expected to get their own deployment and probably their own cluster, so this yields per-tenant separation for free in the cmd/mecak8s shape without paying for many name-to-bundle mappings up front. What the boundary buys: a compromised deployment's signing key can never forge another deployment's identities, and that holds even if the issuing server's authorization logic is fully broken. Path prefixes inside one shared domain cannot give that, because the same key signs both and separation rests entirely on the registration logic being correct.
mecatl as its own SPIFFE issuer. Un-deferred, because the filesystem grant service is on the roadmap and it is the consumer the original deferral named: the grants are over mecatl's own workspace resources on a hot path, and the holder requesting attenuation is a goroutine no external attestor can see. Two amendments to how it lands. Scope the first slice to the shared infrastructure — trust domain, signing path, bundle publication — and let the claim vocabulary land with the FS service, per the doc's own split ("the signing infrastructure, the bundle endpoint and the trust domain are shared, and only the claim set differs"). And sequence it behind Spike: can a model-spawned shell reach the key? #375, because the issuer's day-one payoff is that a verifier can check a narrowing happened rather than trusting the harness, and if the signing key stays reachable from a shell the model drives then verifying against it is trusting the harness again.
Not for the vMCP hop. Nothing in hops 3 through 7 consumes anything only an mecatl-run domain could supply; call 1 needs mecatl to be a SPIFFE workload, which any SPIRE deployment gives it.
Not for consent. See decision 5.
Own domain rather than an intermediate inside an existing one is the current position, not a ratified decision: a delegated intermediate is a full member of that domain's chain with no sub-domain fencing, so chaining in would put a shell-reachable key inside another domain's trust base. The counter-argument, stated for fairness, is that own-domain bounds only the external blast radius — a compromised pod still impersonates every session inside mecatl's own domain, which is where most damage sits in a single-deployment world.
Deferred, with triggers
A deferral with no stated trigger is a decision nobody revisits.
Per-entry access control inside a session. Deferred because the memory entry type carries no owner and one project store is shared across every session, so a check can only be all-or-nothing per scope. Trigger: the entry type gains an owner, which is Sensitivity is a label that survives delegation #369's territory.
Resource-level authority — structured resource fields so the gate tests containment over the resolved target rather than a scope string. Trigger: one agent must be confined to a subset of a backend another may reach in full, which a scope string like cap:github.write cannot express.
The deliverable before then is a short spike doc defining the containment predicate, so the trigger cannot arrive with the algorithm still undefined. Scope for it:
MCP resolved targets are the primary case, not the filesystem grant service. The motivation is that ToolHive's gate cannot test containment over a resolved target today, and "the resolved target as a value only admission can construct" is one of the three unfiled ToolHive enforcement pieces below. The FS grant service is a second consumer of the same predicate over a different resource vocabulary — a path subtree instead of a repository — not the first.
One predicate, defined once. Given a parent's authorization_details and a child's request, is the child contained? Canonicalized resource prefixes plus an operations set, whichever vocabulary the resource comes from. The spike should say what happens on an unrecognized field, and whether operations are a flat set or a lattice.
Both refusal obligations named. mecatl must refuse to request a widening at the spawn seam, because it is the only party that knows the parent's authority. ToolHive must refuse to honour one at the gate, because it cannot assume mecatl did. Neither is sufficient alone: mecatl-only means a compromised harness widens freely, gate-only means the harness cannot tell a child what it may ask for. Same "one decision function, two enforcement points" discipline as the driver protocol on One caller cannot reach another's work #368.
Check AuthZEN's COAZ draft before inventing a vocabulary. It maps MCP tool calls into a Subject-Action-Resource-Context model for per-call PDP authorization, which is the same mapping this predicate needs, and it is an adopted OIDF working-group draft rather than an individual one. Borrow it if it fits.
Sender-constrained token binding. Deferred, with narrow aud plus short exp as the floor — which is what the design already has, and what the JWT-SVID spec treats as the replay defence for a keyless bearer token rather than a supplement to one. A cnf binding only stops an attacker holding the token but not the connection, and an injected in-pod model has both if the loop makes the gateway call, so binding's value is contingent on the broker becoming a data-path proxy while its deployment cost (the gateway terminates TLS itself and requires client certificates, so no TLS-terminating ingress in front) is paid always. Triggers: the broker becomes a proxy for another reason, or a leak makes off-pod replay demonstrated rather than modelled. If built, the method is mTLS (RFC 8705) rather than DPoP, on cost. Note this also parks the unstated question of who makes the gateway call — see The harness acts for a user against an external system #372's hazards.
Cryptographic per-instance attribution. Definition granularity is settled for external authorization, but the cost is real and stated in the outbound doc: attribution is downgraded from verifiable to operational, and X-Correlation-Id is unsigned, so an instance can only be named by a party that already trusts mecatl's log. No relying party's specification currently knows what to do with an instance-named credential.
Two triggers, because they fire for different reasons and one of them is ours.
A subagent stops being a goroutine. This is the checkable one, and it collapses every objection at once: a separate process is visible to an external attestor, can hold its own key, and can therefore do proof of possession, at which point "no party outside the pod can address, present or revoke it" stops being true. Everything decided here about keyless bearer children and X.509 being structurally unavailable turns over on that single fact. Anyone proposing out-of-process subagents for an unrelated reason — sandboxing, resource limits, language isolation — should land on this row.
A downstream service needs to name a specific run in a claim its own authorization server can act on. Phrased as a state rather than an event, because "an auditor asks" is not something anyone can check on a Tuesday. This one is sufficient but the structural trigger is not necessary for it: the requirement could land while children are still goroutines, and that is the expensive version — instance credentials for in-process principals with every caveat above still intact.
What it would need either way is a published JWKS, not federation-grade machinery. Worth carrying with the row so the trigger does not drag the whole issuer-operator load in behind it.
Neither is the seam a single-mint design would need, which is a gateway accepting an externally minted token as the terminal credential. Nothing is filed for that, and single-mint is not the chosen path.
Of the three enforcement pieces, one is now filed, one is deliberately deferred, and one is genuinely missing. Intersection, binding and an unforgeable target are what separate delegation that constrains from attribution that describes; without them the chain is a log field.
Tokens sender-constrained end to end — nothing filed, and we are deliberately not asking for it yet; see the deferral above. The method, if it is ever wanted, is mTLS binding rather than DPoP.
The resolved target as a value only admission can construct — the one still genuinely missing, though the admission seam itself shipped. Note Integrate SPIFFE principals with OAuth grants and discovery toolhive#6204 does carry "require and authorize the target MCP resource", which is the RFC 8707 half; the internal value admission alone can construct is the part with no owner.
One half-covered: the credential read happening behind the gate (only the store and refresh halves exist). The other former half — registration saying which definitions a client may act as — is absorbed: with act.sub being the workload by design on #6204, a definition is expressed as a scope (agent:code-reviewer), so #6200's per-association scope configuration is the allowlist. Two things to confirm are anticipated there: that a scope namespace enumerated per association is in scope (one mecatl deployment is a single SPIFFE ID carrying several agent:* entries), and that matching is exact-element rather than prefix — agent:deploy must not admit agent:deployer.
Two of their open issues are worth reading regardless: stacklok/toolhive#5293, the proxy forwarding upstream credential tokens in tool result bodies unchanged — into bodies the model reads; and stacklok/toolhive#6081, Cedar taking its principal from an unverified id_token, which is the premise of the whole gate.
Index and open questions for the agent-identity work. Not an epic — the questions below are deliberately not distributed into the epics, because a question spread across seven issues is a question nobody answers.
The set
All of them are linked as sub-issues of this one, so GitHub tracks the progress bar.
Sequencing: #367 is the only unblocked one and everything waits on its owner field. #371 branches off #367 without needing #368. #370 needs #368 and #369 both. #375 runs before any work in which mecatl signs something.
Removed from this table: #385 (event-log integrity) closed as not planned — it depended on a snapshot MAC from #374 that was never actually implemented (#374 shipped only Redis auth/TLS). #376 (the escalation proof) closed and moved under #478 ("mecatl as its own SPIFFE issuer") rather than tracked standalone here — its remaining blocker (toolhive#6204, scope intersection) is SPIFFE/identity plumbing #478 already owns.
Decided
spiffe_jwtandspiffe_x509, resolving to one authenticated principal with one client association and one authorization outcome, with WIT-SVID explicitly out of scope. So the "X.509 needs work in both repos" half of the rationale no longer holds on the ToolHive side;spiffe_jwtremains our pick on mecatl-side cost, and the choice is now reversible rather than constrained.Still open
Sequenced, no longer deferred
--identity-domainis required when identity is enabled, and validated against the spec grammar: lowercase,[a-z0-9.\-_], at most 255 bytes, no userinfo or port, no percent-encoding. The docs recommend a domain you own, because names are nominally self-registered with no global uniqueness guarantee and a collision means being unable to federate. Never derive the name from a bundle-endpoint hostname; the federation spec gives a concrete subdomain-squatting attack for that. Accepted cost: deployments will not look alike.cmd/mecak8sshape without paying for many name-to-bundle mappings up front. What the boundary buys: a compromised deployment's signing key can never forge another deployment's identities, and that holds even if the issuing server's authorization logic is fully broken. Path prefixes inside one shared domain cannot give that, because the same key signs both and separation rests entirely on the registration logic being correct.Deferred, with triggers
A deferral with no stated trigger is a decision nobody revisits.
Per-entry access control inside a session. Deferred because the memory entry type carries no owner and one project store is shared across every session, so a check can only be all-or-nothing per scope. Trigger: the entry type gains an owner, which is Sensitivity is a label that survives delegation #369's territory.
Resource-level authority — structured resource fields so the gate tests containment over the resolved target rather than a scope string. Trigger: one agent must be confined to a subset of a backend another may reach in full, which a scope string like
cap:github.writecannot express.The deliverable before then is a short spike doc defining the containment predicate, so the trigger cannot arrive with the algorithm still undefined. Scope for it:
authorization_detailsand a child's request, is the child contained? Canonicalized resource prefixes plus an operations set, whichever vocabulary the resource comes from. The spike should say what happens on an unrecognized field, and whether operations are a flat set or a lattice.Sender-constrained token binding. Deferred, with narrow
audplus shortexpas the floor — which is what the design already has, and what the JWT-SVID spec treats as the replay defence for a keyless bearer token rather than a supplement to one. Acnfbinding only stops an attacker holding the token but not the connection, and an injected in-pod model has both if the loop makes the gateway call, so binding's value is contingent on the broker becoming a data-path proxy while its deployment cost (the gateway terminates TLS itself and requires client certificates, so no TLS-terminating ingress in front) is paid always. Triggers: the broker becomes a proxy for another reason, or a leak makes off-pod replay demonstrated rather than modelled. If built, the method is mTLS (RFC 8705) rather than DPoP, on cost. Note this also parks the unstated question of who makes the gateway call — see The harness acts for a user against an external system #372's hazards.Cryptographic per-instance attribution. Definition granularity is settled for external authorization, but the cost is real and stated in the outbound doc: attribution is downgraded from verifiable to operational, and
X-Correlation-Idis unsigned, so an instance can only be named by a party that already trusts mecatl's log. No relying party's specification currently knows what to do with an instance-named credential.Two triggers, because they fire for different reasons and one of them is ours.
What it would need either way is a published JWKS, not federation-grade machinery. Worth carrying with the row so the trigger does not drag the whole issuer-operator load in behind it.
ToolHive dependencies
Checked against every open and closed issue in
stacklok/toolhive. Covered: token exchange (stacklok/toolhive#5194),actnesting (stacklok/toolhive#6113), Cedar (stacklok/toolhive#6081, stacklok/toolhive#6053, stacklok/toolhive#6049, stacklok/toolhive#6048, stacklok/toolhive#5845, stacklok/toolhive#5582), and SPIFFE client authentication with its per-identity scope association (stacklok/toolhive#6199, sub-issues stacklok/toolhive#6200–6206 — which also carries theclient_credentialsgrant and the provisionable confidential client, option D of stacklok/toolhive#6082).Prerequisites of the two-leg shape we picked, both confirmed against the code:
actor_tokensupport. Call 3 sendsactor_tokenand the server rejects it outright today (pkg/authserver/server/tokenexchange/handler.go:296-301, "not yet supported").MultiIssuerTokenValidatorimplementsSubjectTokenValidator, so it sits on the subject token presented into the AS's own exchange endpoint.Neither is the seam a single-mint design would need, which is a gateway accepting an externally minted token as the terminal credential. Nothing is filed for that, and single-mint is not the chosen path.
Of the three enforcement pieces, one is now filed, one is deliberately deferred, and one is genuinely missing. Intersection, binding and an unforgeable target are what separate delegation that constrains from attribution that describes; without them the chain is a log field.
spiffee-authserverprototype gave every auto-registered client every supported scope, and #6200 names that as a weakness not to lift).resource", which is the RFC 8707 half; the internal value admission alone can construct is the part with no owner.One half-covered: the credential read happening behind the gate (only the store and refresh halves exist). The other former half — registration saying which definitions a client may act as — is absorbed: with
act.subbeing the workload by design on #6204, a definition is expressed as a scope (agent:code-reviewer), so #6200's per-association scope configuration is the allowlist. Two things to confirm are anticipated there: that a scope namespace enumerated per association is in scope (one mecatl deployment is a single SPIFFE ID carrying severalagent:*entries), and that matching is exact-element rather than prefix —agent:deploymust not admitagent:deployer.Two of their open issues are worth reading regardless: stacklok/toolhive#5293, the proxy forwarding upstream credential tokens in tool result bodies unchanged — into bodies the model reads; and stacklok/toolhive#6081, Cedar taking its principal from an unverified
id_token, which is the premise of the whole gate.Reasoning