Skip to content

feat(a2a): AAuth agent identity across instances, behind an off-by-default flag (abilityai/trinity-enterprise#623) - #2956

Merged
vybe merged 9 commits into
devfrom
feature/623-aauth-agent-identity
Oct 13, 2026
Merged

vybe merged 9 commits into
devfrom
feature/623-aauth-agent-identity

Conversation

@vybe

@vybe vybe commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Ready for review since 2026-10-13 (engineer /validate-pr: APPROVE at 33a22bdd) — it merges together with its stack (#3659 → the ent#879 PR) on the operator's call; the gating decision (OSS-core seam vs entitlement-gated module) is still task G on trinity-enterprise#558.

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 polymorphic EdDSA, which the draft forbids verifiers to accept), httpsig (RFC 9421 profile + a strict RFC 8941 subset), keys (one instance key, AES-256-GCM in system_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-*.json documents; uniform 404 unless AAuth is live; per-IP limited.
  • routers/a2a.py — a route class sends a request carrying Signature-Key to 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.
  • Outbound: auth_scheme on the endpoint registry (bearer default | aauth, which carries no credential), signer threaded to the RPC, and a path-relative card fetch for aauth endpoints.
  • 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 — new external_agent actor 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)
  • Existing A2A suites unchanged (test_157_*, test_736_*, test_180_*, test_a2a_card_service) — 493 passing
  • Full unit suite: 16,390 passed (two order-dependent failures reproduced on clean dev — pre-existing)
  • Cross-implementation: a request signed by the JS reference @hellocoop/httpsig 2.6.0 verifies here (checked-in vector), and ours verifies there
  • Live between two independent instances: whoami.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 run

Open before this can merge

  1. Gating decision — OSS-core seam vs entitlement-gated module (deferred to after the 2026-09-18 call).
  2. Trinity→Trinity A2A is broken independently of this PR — the outbound client reads a peer's card at the origin root while Trinity serves it under /a2a/{name}/, so bearer federation fails at the card fetch too. Fixed here for aauth endpoints only; the general fix wants its own issue.
  3. Stated prototype limits (also in the flow doc): one software-held instance key, no rotation; the backend signs on the agent's behalf; identity is keyed by agent name; inbound AAuth is inert without the private allow-list module; authorization stays coarse — an allow-list entry means the peer may ask the agent for anything it can do.
  4. src/frontend/nginx.conf gains one location for the discovery documents — add the ui label if the e2e gate is wanted before merge.

Docs: docs/memory/feature-flows/a2a-aauth-agent-identity.md, requirements docs/memory/requirements/mcp.md §32.6, architecture catalogs, and a learnings.md entry (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


Refresh 2026-10-13 (#3644) — the branch now contains current dev

Brought up to dev d427814a by merge commit 06866d2e (no rewrite: this PR stayed open). The real merge conflicted in 13 files, not the 7 merge-tree predicted; every resolution keeps both parents' behaviour. Follow-up commits on the branch:

Commit What
3cb3e9c1 the AAuth seam test fixture stubs the door's new ent#838 scope read and ent#679 pricing read (17 fixture-only reds after the merge)
391fef34 review C1: duplicate credential_cleared key in the endpoint audit row (merge kept both lines); request.url.path → scope["path"] ×3 (#3102 fence); gitleaks allowlist fingerprint for a false positive on a dataclass attribute name
3f045bbd the three AAuth discovery routes classified in the route census (#2996); the nginx AAuth location forwards the mapped $fwd_proto like /a2a/ (#3215)
a1c551a8 the AAuth flag read fails closed — the card builder's call into services/aauth/config raised under an unreadable settings service and 500'd the card route (4 of the #3215 card/door tests); an unreadable flag now reads OFF, so the card and the bearer-less 401 keep dev's bytes; regression test + two tests for the merge-added internal-scope gate on AAuth callers

Routing after the merge. routers/a2a.py now has one shared _dispatch_jsonrpc reached by the bearer path, the keyless (ent#838) path and the AAuth route class. Two behaviours were added to keep dev's intent: an AAuth caller passes the ent#838 internal-scope gate exactly as a bearer caller does, and the AAuth challenge headers also ride dev's uniform returned 401 (flag-on only; a module constant, so nothing enumerates agents). The requirement moved to §32.8 of docs/memory/requirements/mcp.md (dev took 32.6 and 32.7).

Review + security (orchestrator, trinity-pm). /review (Fable, report-only, merge-scoped): 19 both-sides files set-compared, 12 byte-identical, 5 differ only by the expected adaptations; one medium (C1, fixed), two lows (I1 fixed with tests, I2 noted in code). Inline STRIDE pass over the six resolved service/router diffs: no findings. Spot checks: test_ent623_aauth_a2a 33, test_ent838_a2a_trusted_networks 37, test_ent679_* 178 passed.

Tests. On a1c551a8: regression diff green (the seven post-09-18 guards that were red on 3cb3e9c1 now pass), pytest (head), frontend-e2e, journey-smoke, schema-parity, CodeQL green. secret-scan red on the allowlist comment quoting the pattern — reworded in the next commit.

Stack. This branch is the base of the AAuth Night chain: feature/878-aauth-person-token (trinity-enterprise#878, person-token binding via Hellō) and feature/879-aauth-resource-client (trinity-enterprise#879, outbound resource client + Senzing sanctions check) open as stacked draft PRs on it. The stack merges together on the operator's call; gating stays task G on trinity-enterprise#558. The enterprise half remains trinity-enterprise#677.

vybe and others added 2 commits September 18, 2026 08:02
…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>
@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

✅ Nightly unit-suite clean when this PR is merged into dev, all 3 seeds (head_sha: a1c551a89a9057099e60926906eff0242db0ba40).

@github-actions

Copy link
Copy Markdown

⚠️ Live-instance suite skipped — merge conflict against dev.

Resolve by merging dev locally and pushing the result; the next nightly re-tests.

vybe added a commit that referenced this pull request Oct 9, 2026
…orable secret value is a named 422 (#3325) (#3417)

Fixes #3325 — independent fix, one of ten small bug fixes in the trinity-pm `chain-easy-1008` run. Base is `dev`. **Draft until the operator merges.**

## What
- `src/backend/services/credential_encryption.py::decrypt`: every malformed envelope shape raises `ValueError`, as the function promises — JSON that is not an object (`[]`, `null`), and a non-string nonce or ciphertext. Importing such a `.credentials.enc` now returns **400** instead of a 500 with a stack trace in the log. The envelope format and every successful decrypt are unchanged.
- `src/backend/services/secret_settings.py::encrypt_secret_setting`: the key is checked first, in its own `try`, and only that check can raise `MissingEncryptionKeyError`. A value that cannot be encoded (for example a lone surrogate) raises the new `SecretSettingValueError(ValueError)`. Its message names the setting, never the value.
- `src/backend/error_handlers.py` + `src/backend/main.py`: one app-level handler maps `SecretSettingValueError` to **422**. `src/backend/routers/settings/credentials.py`: three pass-through lines in the routes whose catch-all would have turned it into a 500 (Anthropic, GitHub PAT, `PUT /slack`).
- Tests: the three strict-xfail markers in `test_ec_credential_crypto_edges.py` are removed; new `tests/unit/test_3325_credential_error_types.py` (registered in `tests/registry.json`) asserts no value in the message or the exception chain, a missing key reported before a bad value, `PUT /api/settings/api-keys/anthropic` → 422, and that `main.py` registers the handler.

## Rulings carried (orchestrator, on the operator's behalf — plan file)
- TD-1: one app-level handler plus the three pass-through lines — as recommended.
- TD-2: a `.credentials.enc` that decrypts correctly but whose contents are not an object is deferred — as recommended; forging one needs the platform key.
- TD-3: 422, matching the existing 422 for refused cleartext writes — as recommended.

## Review + security
`/review` reading pass (claude-fable-5-1, report-only): **MERGEABLE**, no critical findings. The secret cannot leave through the new error: it is raised after the `except` block, so `__context__` is cleared, not merely hidden; the handler returns only the message; nothing logs it; audit rows are written only after a successful write. The only residual carrier is traceback frame locals, which nothing in the backend renders. The handler is keyed on the subclass, so no other `ValueError` is swallowed or relabelled. `/cso --diff`: no findings.

Evidence sweep (claude-opus-5-5): **GREEN**. 23 unit files import the touched modules; the 16 most direct ran shuffled (seed 12345), one file per process, all passed. All 7 tests in the new file ran, 0 skipped. With both service files taken from `dev`, exactly the three formerly-xfail tests fail (8 cases). Seven less direct files are left to CI and named in the review file. `git merge-tree` against `dev` f6bedcd is clean.

## Tests
Targeted counts above. Not run here: the full unit island (CI).

## Before merge
- Startup auto-import (`lifecycle.py`) on a malformed envelope now lands in its `ValueError` retry arm: it retries without a warning per attempt and ends in the same "failed" result, with the existing error log after the last attempt.
- Enterprise callers of `decrypt` get `ValueError` where they got `AttributeError` / `TypeError` on a malformed envelope. Two catch broadly, three propagate as before; no change needed there.
- The new test file has a module-level `importorskip` for `sqlalchemy` and `cryptography`: it skips silently where those are missing. They are present on the engineer and in CI.
- Ten open PRs share `tests/registry.json` or `src/backend/main.py` with this branch (#3412, #3411, #3407, #3304, #3271, #3256, #2984, #2956, #2713, #2709). A test-merge of this head against each adds no conflict beyond what that PR already has with `dev`.

## Handoffs (not in this diff)
- TD-2 above: the decrypted-but-not-an-object case can still 500 on import.
- `PUT /api/settings/slack` saves the client ID before a later secret write fails (partial update, pre-existing).
- The GitHub PAT, Resend and Gemini routes are covered by the app-level handler but not exercised by the new route test.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
vybe added a commit that referenced this pull request Oct 9, 2026
…L is refused, not stored (#3323) (#3420)

Fixes #3323 — independent fix, one of ten small bug fixes in the trinity-pm `chain-easy-1008` run. Base is `dev`. **Draft until the operator merges.**

## What
- `src/backend/utils/url_validation.py::reject_embedded_credentials`: strips its input and treats a leading `//` as already having a host, the same rule `strip_url_credentials` uses. `//<token>@github.com/o/r` is now refused like the `https://` form, including with leading whitespace or mixed case. All three write paths that store a skills-library URL go through this one function (`routers/skills.py` create and update, legacy adoption in `services/skill_service.py`), so the token is no longer saved in plaintext to `skill_sources.url` or the audit details.
- If `urlparse` refuses an input that starts with `//`, the function falls back to the module's existing userinfo regex instead of raising, so this fix adds no new 500.
- Tests: the D3 strict-xfail marker is removed; four more refused shapes and a four-case "must not raise / must not over-reach" test are added.

## Rulings carried (orchestrator, on the operator's behalf — plan file)
- TD-1: the fallback applies only to inputs starting with `//`; the bare `ValueError` for every other malformed input is #3322's and is unchanged (D4 stays a strict xfail) — as recommended.
- TD-2: keep the username/password check; `//@host` carries no secret and is not refused — as recommended.
- TD-3, TD-4: deferred, see Handoffs — as recommended.

## Review + security
`/review` reading pass (claude-fable-5-1, report-only): **MERGEABLE**, zero critical, five informational. The pre-fix and post-fix modules were executed side by side over forty inputs: nothing that was refused before now passes, credential-free inputs are unchanged, the fallback regex is anchored and linear. `/cso --diff`: no findings.

Fixed after review:
- `546f329a` — the `tests/registry.json` descriptions no longer call #3323 a strict xfail.
- `a75a5e04` — the reject stub in `test_ent183_skill_packages.py` mirrors the fixed `//` rule. It is deliberately stricter than production on a malformed bracket (`//[oops/x` raises).

Evidence sweep after the fix (claude-opus-5-5): **GREEN**. 28 unit files import the module; the 14 most direct ran shuffled (seed 12345), one file per process. Three did not come back clean, none because of this branch:
- `test_ent236_skills_lifecycle.py::TestNonRepoDirectoryRecovers::test_real_repo_still_pulls` and `test_skill_service_user_agent.py` (collection error) fail the same way on `dev` f6bedcd in the engineer's environment.
- `test_ent237_skill_sources.py` hit the 240 s per-file limit at about 72 % with no failure printed — left to CI.

`git merge-tree` against `dev` f6bedcd is clean.

## Tests
895 passed across the 11 files that completed cleanly. Not run here: the 14 less direct importer files, `test_ent237_skill_sources.py` to completion, and the full unit island (CI).

## Before merge
- Ten open PRs share `tests/registry.json` with this branch (#3417, #3412, #3411, #3407, #3304, #3271, #3256, #2984, #2956, #2709). A test-merge of this head against each adds no conflict beyond what that PR already has with `dev`.
- One stale docstring remains at `tests/unit/test_ec_url_validation_properties.py:36` (still calls D3 a strict xfail).

## Handoffs (not in this diff)
- TD-3 (P3): shapes that still pass both reject and strip — `/\t/tok@`, `/\n/tok@`, `///tok@`, `https:/tok@` (`urlparse` removes tab and newline after the check). Fixing it needs the same change in `strip_url_credentials` and cases in both test families.
- TD-4: a property test that reject and strip always agree over the shared `test_2052` corpus; it needs exceptions for empty userinfo.
- #3322 (bare `ValueError` / `UnicodeError` → 500) is the sibling issue in the same file and is untouched.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
vybe added a commit that referenced this pull request Oct 9, 2026
…t the database (#3385) (#3421)

Fixes #3385 — one of ten small bug fixes in the trinity-pm `chain-easy-1008` run. **Stacked on #3414** (the fix for #3314): base is `feature/3314-queue-fingerprint-falsy`, because both issues edit `_clamp_ingested_item` and the fingerprint helpers. Merge #3414 first, then retarget this to `dev`. **Draft until the operator merges.**

## What
- `src/backend/services/operator_queue_service.py`: new `OPERATOR_QUEUE_TYPE_MAX = 64` and `_bounded_type`. An agent-written `type` longer than 64 characters is shortened with the existing `…[truncated]` marker at ingest (`_clamp_ingested_item`); the ask is never refused or lost. `_comparable_type` applies the same bound, so a shortened row does not read as a rewrite (#2915) and a row stored before this fix at full length still compares equal.
- `src/backend/db/operator_queue.py::_insert_values`: `_DB_BELT_TYPE_MAX_BYTES = 1024`; above it the insert raises `ValueError`, like the id / title / question / context belts. All three create paths go through it.
- Docs: five lines in `architecture/api-endpoints.md`, `feature-flows/operating-room.md` and `requirements/security.md` that list the clamp and belt fields now name `type`.
- Tests in `tests/unit/test_1632_operator_queue_caps.py`: an oversized type is shortened with the marker; every platform-emitted type and every type `ask_operator` accepts is unchanged; the belt rejects an oversized type and passes 64 four-byte characters; `test_3385_clamped_type_is_not_a_rewrite` (clamped row vs raw entry, legacy full-length row, a real rewrite inside the first 52 characters).

## Rulings carried (orchestrator, on the operator's behalf — plan file)
- T1: a fixed constant, no environment variable — as recommended.
- T2: 64 characters — as recommended. The longest type the platform emits is `workspace_problem_report`, 24.
- T3: the DB belt raises, like its siblings — as recommended.
- Orchestrator: stacked on #3314 instead of branching from `dev`, so the two fixes do not hand the operator a conflict in the same two functions.

## Review + security
`/review` reading pass (claude-fable-5-1, report-only, against the stacked base): **MERGEABLE**, zero critical. Census of every literal `type` the platform and the enterprise submodule pass to the insert paths: longest is 24 characters, none is altered. Every truncated value ends in the marker, which no set member can match, so truncation cannot move a value into or out of a set that decides control flow (`_BUDGETED_ALERT_TYPES` and neighbours). The new `ValueError` is reachable only from the file sync loop, which quarantines it under its existing handler. `/cso --diff`: no findings; the error text carries the constant, never the value.

Fixed after review: `eab91c10` — I1, the five doc lines. Noted, not changed: I2, a non-string `type` over 1 KiB now raises the named `ValueError` where the base failed at the bind — same quarantine outcome.

Evidence sweep after the fix (claude-opus-5-5): **GREEN**. 13 operator-queue files ran shuffled (seed 12345), one file per process: 836 passed, 7 xfailed. With both production files taken from the #3314 branch, the two `TestClampType` cases, the belt test and the clamped-row fingerprint case go red. `git merge-tree` against `dev` f6bedcd is clean; the base branch has not moved (`923b1edc`).

## Tests
Counts above. Not run here: `test_ent815_queue_walk`, `test_ent815_broad_list_agent_scope`, `test_ent751_gate_entries` (2–4 minutes each; they ran green on the #3314 base in that PR's sweep) and the full unit island.

## Before merge
- **This PR's checks are weaker than a dev-based PR's:** a PR whose base is a feature branch runs a reduced CI set and `backend-unit-test` does not run. The first full run happens when it retargets to `dev` after #3414 merges — read that run before merging.
- Accepted trade-off: an agent that rewrites an oversized type only after character 52 is not detected as a change. Titles and questions already behave this way.
- Thirteen open PRs share a file with this branch (mostly `tests/registry.json` and `docs/memory/architecture/api-endpoints.md`; #3256 also `db/operator_queue.py` and the three docs). A test-merge of this head against each of #3420, #3417, #3412, #3411, #3407, #3304, #3271, #3256, #2984, #2956, #2713 and #2709 adds no conflict beyond what that PR already has with `dev`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Trinity Agent (trinity) and others added 7 commits October 12, 2026 23:18
…pricing reads after the dev merge (#3644)

dev's inbound door now asks db.get_a2a_scope (trinity-enterprise#838) and,
for a bearer-less caller, db.get_nevermined_config_with_key (ent#679), and it
resolves its principal through get_user_or_anonymous — which calls
get_current_user instead of depending on it, so the fixture's override has to
sit on the optional dependency. 17 of 30 tests in this file failed on the
merged tree for exactly those three reasons; no product code changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e dev merge (#3644)

- integrations.py: the settings-change audit row carried `credential_cleared`
  twice after the dev merge; Python keeps the last key, so switching a bearer
  endpoint to `aauth` recorded `false` while the store dropped the secret. The
  ent#623 line stays; the stale dev line goes. Router-level test proves the
  row says `true` on the switch (red against the duplicate).
- a2a.py: the three `request.url.path` reads become `request.scope["path"]`
  (#3102 fence); the signature base keeps `raw_path` first.
- .gitleaksignore: fingerprint for the `private_key: Ed25519PrivateKey =
  field(repr=False)` dataclass field at keys.py:43 (generic-api-key false
  positive). The range scan re-flags the adding commit, so an inline
  `gitleaks:allow` on the head cannot clear it; the fingerprint can.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…on forwards the mapped scheme (#3644)

- _route_census.py: the three ent#623 discovery documents
  (/.well-known/aauth-agent.json, -jwks.json, -resource.json) are OWN_AUTH —
  anonymous by design, public material only, uniform 404 while the prototype
  flag is off. The AAuth route class on POST /a2a/{name} is the same registered
  route the census already lists; nothing new to classify there.
- nginx.conf: the /.well-known/aauth- location sent `X-Forwarded-Proto $scheme`,
  which is always http behind the TLS terminator (#3215). It now mirrors the
  /a2a/ block's headers (mapped $fwd_proto); the sibling count moves 3 → 4.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ettings are unreadable (#3644)

(d) `test_3215_public_base_url` drives the real card route with a settings
stand-in that knows only the public URL. The branch made dev's single card
builder (ent#180) consult `aauth_config.active_issuer()`, whose flag read
called `settings_service._resolve_bool_flag` unguarded, so the card 500ed and
the four card/door tests that follow it failed. `is_enabled()` now treats an
unreadable flag as OFF (AAuth inert, dev's bytes), the direction `get_issuer`
already takes; regression test on the real card route.

I1: two tests execute the merge-added ent#838 gate on the AAuth door — an
internal-scope agent answers an outside AAuth caller with the keyed caller's
uniform 404 (no fetch, no audit row), and admits a trusted one as its verified
identity, never its address.

I2: comment on `fetch_card_for` naming its overlap with ent#838's
own-path-then-origin card order; code unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…it allowlists (#3644)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…range scan still sees (#3644)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@trinity-ability trinity-ability left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/validate-pr — #2956 at 33a22bddb (chain-aauth-demo, trinity#3644)

Verdict: APPROVE. The code is ready to merge. This verdict covers the code only; the draft status and the gating decision (trinity-enterprise#558 task G) are the operator's call.

Lane B (by path: no file under docker/, auth, credentials or CI; no schema). 42 files, +4208/−28. Targets dev. Merges cleanly with the live dev tip, which is 25 commits ahead (git merge-tree clean).

Summary

Category Status Notes
Commit messages ✅ conventional prefixes, and each one says why
Base / size ✅ dev; 42 files
Status auto-promotion ⚠️ Refs abilityai/trinity-enterprise#623 with no closing keyword. Deliberate for a stacked prototype; trinity#3644 needs a manual status change at merge
Requirements / architecture ✅ requirements/mcp.md §32.8; api-endpoints.md and backend.md catalog rows
Feature flow ⚠️ a2a-aauth-agent-identity.md is detailed but does not use the template sections (Overview / User Story / Error Handling / Testing). Index row is present
Security §4.1–4.10 ✅ no keys; emails are on the reserved .example domain; 93.184.216.34 is a fixture, 1.1.1.1 is a dev-script resolver, 100.70.1.2 is test CGNAT; no symlinks or host paths; enterprise-docs grep is clean; no new os.getenv
Infrastructure ✅ one location in src/frontend/nginx.conf (now forwards $fwd_proto, #3215); scripts/dev/aauth_prototype/* is dev-only
Build packaging ✅ services/aauth/ sits under services/, so no new top-level directory
Structural review ✅ merge-scoped /review plus an inline STRIDE pass (3644-aauth-refresh-review.md). The prototype had its own review on 2026-09-18
Merge gate ✅ Tier 1 green (regression diff, pytest head, schema-parity, CodeQL ×2, verify-non-root, gitleaks); journey-smoke and e2e also green on the PR. dev-ci: success; no open dev-ci red issue
Test adequacy ✅ non-happy paths covered: unlisted identity → 403, tampered digest → 401, replay, flag off, provider failure, internal scope

Execution coverage (§5.4): mutations run in the PR worktree

Baseline: the AAuth, ent#838, #3215, ent#761, #1920 and card suites give 234 passed; the #2996 census gives 69 passed.

Fix / wiring Mutation Result
C1 credential_cleared on an aauth switch (integrations.py:679) restore dev's value, and separately put the duplicate key back ❌ red: test_ent761_outbound_control_oss.py, 1 failure each
I1 ent#838 internal gate on the AAuth door (a2a.py:1341) if False: ❌ red: test_ent623_aauth_a2a.py
a1c551a8 flag read fails closed (aauth/config.py) re-raise instead of returning False ❌ red: 1 failure across test_ent623_aauth_a2a + test_3215
Production wiring app.include_router(aauth_router) (main.py:1426) comment it out ❌ red: #2996 route census

Every change from the review has a test that goes red when the change is reverted, and the router registration in main.py is pinned by the census.

Warnings (do not block merge)

  • I2 is still open by choice. fetch_card_for overlaps ent#838's own-path-then-origin order: an aauth endpoint can fetch the own-path card twice, and it falls back on any status, not just 404. The code now has a comment saying so. Worth opening a follow-up issue before the stack merges.
  • /verify-local and /cso --diff over the whole prototype are still unchecked in the Test Plan. This lane does not require them, but a P1 theme-security feature does under the pipeline table. Run them before the operator merges the stack.
  • The flag is off by default, so all of this ships inert until the gating decision. Inbound AAuth also stays inert without the private allow-list provider (trinity-enterprise#677).

Report-only run: nothing was committed and the branch was not edited.

@vybe
vybe marked this pull request as ready for review October 13, 2026 15:29
@vybe
vybe merged commit 4bc8561 into dev Oct 13, 2026
28 checks passed
@vybe
vybe deleted the feature/623-aauth-agent-identity branch October 13, 2026 19:00
vybe pushed a commit that referenced this pull request Oct 15, 2026
…st split (ent#623) (#3748)

Pulls in Abilityai/trinity-enterprise#677. Without it the public a2a_gate
seam (#2956) finds no inbound_identities method on the allow-list provider
and refuses every AAuth caller, and an aauth: allow-list entry wrongly
gates bearer callers.

Co-authored-by: trinity-ability <309458136+trinity-ability@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants