Conversation
…fault flag (Abilityai/trinity-enterprise#623) An agent on one Trinity calls an agent on another and is authenticated as ITSELF — by its own key under AAuth's agent-identity-only mode — with no bearer secret of either instance configured on the other. Prototype for the 2026-09-18 call with the protocol's author; `aauth_prototype_enabled` is OFF by default and flag-off behaviour is unchanged. Each instance is its own Agent Provider (AAuth self-hosted bootstrap): it publishes `/.well-known/aauth-{agent,jwks,resource}.json`, self-issues `aa-agent+jwt` tokens binding a per-agent key via `cnf.jwk`, and signs requests per RFC 9421 with the key conveyed by `Signature-Key: sig=jwt;jwt="…"`. Inbound (`POST /a2a/{name}`): a request carrying `Signature-Key` while AAuth is live takes a separate entry point, so the bearer dependency — which 401s a request with no token before any handler runs — is not made conditional and the bearer path stays byte-identical. Verification runs before the body is parsed as JSON-RPC, cheapest-and-local first: header shape → token claims → exposure + trusted-issuer pre-gate → issuer metadata/JWKS (pinned public-HTTPS egress, same-origin `jwks_uri`, one 8 s deadline) → token signature → HTTP signature (which covers the Content-Digest HEADER) → body digest → replay claim. Only then is the body read. An AAuth caller never becomes a Trinity `User`: access is exposure plus an EXPLICIT allow-list entry, fail-closed, and no owner role travels with it. Refusals answer RFC 9457 problem details with `Signature-Error`; a verified-but-unlisted caller gets 403. Two properties are load-bearing and easy to lose: * B never fetches metadata for an issuer host no operator listed, so an unauthenticated caller cannot use this backend as a fetcher; and * a refusal is only auditable once the token's own signature verifies — before that `sub` is a string the caller typed, and attributing an audit row to it would hand any stranger the audit table. Outbound: a registered endpoint may be marked `auth_scheme: aauth` (no credential — switching to it clears one, non-default ports refused). Only the agent's OWN key may place such a call: `AuthorizedAgentByName` also admits the owner, shared users and admins, and for AAuth that would let a human make the backend sign as the agent. The request is signed over the exact bytes sent, to the registered host rather than the pinned IP. Also here: the served card declares `aauth` AFTER `bearerAuth` (an AAuth-unaware client that takes the first entry is unaffected); a new `external_agent` audit actor type, so a remote caller is not recorded as the platform itself; and a path-relative card fetch for aauth endpoints, because a Trinity peer serves its card under `/a2a/{name}/` while the client only ever looked at the origin root — which is why Trinity-to-Trinity A2A did not work at all. The general fix for that wants its own public issue. Verified live between two independent instances (the second in its own Docker daemon — see the learnings entry on why a shared daemon eats the first one's agent volumes): `whoami.aauth.dev`, a verifier that is not ours, echoes the calling agent's identity; the remote task runs; an unlisted identity is refused 403; a tampered body is refused 401; a bearer call to the same endpoint still works. A request signed by the JS reference `@hellocoop/httpsig` verifies here (checked-in vector), and ours verifies there. Stated prototype limits: one software-held instance key (AES-256-GCM at rest, no rotation), the backend signs on the agent's behalf, identity is keyed by agent name, `message/stream` is refused for AAuth callers, and inbound AAuth is inert without the private allow-list module. Requirement: docs/memory/requirements/mcp.md §32.6. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…trinity-enterprise#623) The end-to-end flow, written to be read by someone who did not build it: roles, layers, public surface, config state, and the four flows (enablement, outbound sign, inbound verify-and-decide, served card) with the ordering constraints spelled out — why the trusted-issuer pre-gate runs before any network I/O, why a refusal only becomes auditable once the token verifies, and why the body is read after the signature that covers its digest rather than before. Also states the trust model plainly: what the callee actually verifies (key possession + this exact request), what it does not (who runs the peer), that the unit of trust is the peer instance rather than one agent, and that authorization stays coarse — an allow-list entry means the peer may ask this agent for anything it can do. Closes with the prototype's limits and the diagrams worth drawing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Resolve by running |
|
Resolve by merging |
This branch has not been deployed
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.
Draft — a time-boxed prototype, not a merge candidate yet. The gating decision (OSS-core seam vs entitlement-gated module) and the merge path are open; see "Open before this can merge" below.
Summary
An agent on one Trinity instance calls an agent on another and is authenticated as itself — by a key only it holds — instead of by a shared bearer secret. This is AAuth's smallest access mode (agent-identity-only, self-hosted bootstrap: no Person Server, no Access Server, no missions) applied to the A2A seam we already ship. The transport is unchanged: still A2A JSON-RPC over
POST /a2a/{name}.Everything is behind
aauth_prototype_enabled, off by default. With the flag off, inbound, outbound and the served card behave as before.What it replaces. Federation previously meant instance B minting a Trinity MCP API key and instance A storing it. That credential resolves to its owner, carrying the owner's role — the class behind six prior incidents — so the callee's answer to "who is calling?" was "somebody holding a key owned by a user here". Now it is
aauth:<agent>@<instance-host>: per agent, no role attached, proved per request, revoked by deleting one line on the callee.Changes
services/aauth/—config(flag + issuer),jose(Ed25519 JWS; PyJWT only registers the polymorphicEdDSA, which the draft forbids verifiers to accept),httpsig(RFC 9421 profile + a strict RFC 8941 subset),keys(one instance key, AES-256-GCM insystem_settings, insert-if-absent so two workers cannot fork it),signer,discovery,verifier,inbound,whoami(operator CLI).routers/aauth.py— the three/.well-known/aauth-*.jsondocuments; uniform 404 unless AAuth is live; per-IP limited.routers/a2a.py— a route class sends a request carryingSignature-Keyto a separate entry point, so the bearer dependency is not made conditional and the bearer path, its 401s and its dependency overrides are untouched. Both entry points share one JSON-RPC method table.auth_schemeon the endpoint registry (bearerdefault |aauth, which carries no credential), signer threaded to the RPC, and a path-relative card fetch foraauthendpoints.a2a_gate.explicit_inbound_identities— the fail-closed allow-list accessor (inverse of the bearer gate beside it). Its provider half is abilityai/trinity-enterprise#677.platform_audit_service— newexternal_agentactor type, so a remote caller is not recorded as the platform itself.Ordering decisions worth reviewing, all with tests: the trusted-issuer pre-gate runs before any network I/O; a refusal is auditable only after the token's signature verifies; the body is read only after the signature covering its digest verifies; replay and the allow-list fail closed.
Test Plan
tests/unit/test_ent623_aauth_{core,a2a}.py— 136 new tests; mutation-checked (breaking the pre-gate, digest, replay, flag gate, allow-list membership or task scoping each fails a test)test_157_*,test_736_*,test_180_*,test_a2a_card_service) — 493 passingdev— pre-existing)@hellocoop/httpsig2.6.0 verifies here (checked-in vector), and ours verifies therewhoami.aauth.dev(a non-Trinity verifier) echoed the calling agent's identity; the remote task ran; an unlisted identity got 403; a tampered body got 401 before the envelope was parsed; a bearer call on the same endpoint still worked/verify-local,/cso --diff— not runOpen before this can merge
/a2a/{name}/, so bearer federation fails at the card fetch too. Fixed here foraauthendpoints only; the general fix wants its own issue.src/frontend/nginx.confgains onelocationfor the discovery documents — add theuilabel if the e2e gate is wanted before merge.Docs:
docs/memory/feature-flows/a2a-aauth-agent-identity.md, requirementsdocs/memory/requirements/mcp.md§32.6, architecture catalogs, and alearnings.mdentry (a second Trinity backend on the same Docker daemon deletes the first one's agent volumes).Refs abilityai/trinity-enterprise#623
🤖 Generated with Claude Code