Skip to content

feat(a2a): AAuth identity badge + execution attribution (ent#862 PR2+PR3) - #3913

Merged
vybe merged 6 commits into
devfrom
feature/ent862-aauth-graduate-new
Oct 26, 2026
Merged

vybe merged 6 commits into
devfrom
feature/ent862-aauth-graduate-new

Conversation

@trinity-ability

@trinity-ability trinity-ability commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Completes the AAuth agent identity graduation for Trinity-to-Trinity A2A calls (abilityai/trinity-enterprise#862). PR1 (flag split, Settings panel, outbound endpoint UI) merged in #3699.

PR2 — Inbound allow-list UI (A2aPanel.vue + MCP description)

  • Shows AAuth / Local badge on each identity in the inbound allow-list (blue for aauth:, neutral for local)
  • Validates aauth:<agent>@<peer-host> format inline with a warning on malformed entries
  • Updates placeholder text and section description to document both formats
  • Updates set_a2a_inbound_allowlist MCP tool description to describe the aauth: format

PR3 — AAuth caller attribution on execution rows

  • Adds a2a_caller_identity TEXT column to schedule_executions (dual-track: SQLite migration a2a_caller_identity + Alembic 0103_a2a_caller_identity)
  • Stamps the verified AAuth identity onto the execution row at _mint_a2a_execution (routers/a2a.py), read back through ExecutionResponse
  • Exposes a2a_caller_identity on ExecutionResponse (API)
  • Shows AAuth Caller field in the Execution Origin card on ExecutionDetail.vue (semantic tokens only, no raw palette classes)

Test plan

  • Inbound allow-list: add aauth:agent@peer.example.com — badge shows "AAuth" in blue; add user@example.com — badge shows "Local" in neutral
  • Inbound allow-list: type aauth:bad (no @host) — inline warning appears
  • Run an AAuth-authenticated inbound A2A call; execution detail page shows "AAuth Caller: aauth:agent@peer.example.com"
  • Run a plain (bearer/anonymous) A2A call; execution detail shows no AAuth Caller row
  • raw-color-baseline.json ratchet: npm run test:unit passes (no new raw palette classes added)
  • check_alembic_heads.py passes (single head 0103_a2a_caller_identity)
  • Step 13 live test (requires operator): an agent on instance A (instance-a.example.com) calls an agent on instance B (instance-b.example.com) over AAuth; transcript posted to ent#862

🤖 Generated with Claude Code

Refs abilityai/trinity-enterprise#862 (PR2 + PR3 of the graduation; the step-13 live transcript and docs remain)

vybe and others added 3 commits October 13, 2026 21:31
… — flag split, Settings surface, outbound endpoint UI (Abilityai/trinity-enterprise#862, PR1)

Two switches now: `aauth_enabled` governs Flows 1–4 (well-knowns, signing,
inbound verification, card) and is the single input of `active_issuer()`;
`aauth_prototype_enabled` is narrowed to the person-token and resource-calling
prototypes, gated once at `require_live` on BOTH switches. Forwarded in every
backend compose file with an empty default so the Settings row stays the
observable source; `aauth_available` on the feature-flags surface.

`GET/PUT /api/settings/aauth`: admin on read, admin AND interactive on write
(no MCP key of any scope), diff-only body, named 422 for an invalid issuer,
person server or resource allow-list, audited `aauth_config_change`. The
generic `PUT/DELETE /api/settings/{key}` refuses all five `aauth_*` keys —
the two Flow 5–6 keys were the one MEDIUM `/cso --diff` found (pre-existing;
cso-diff-2026-10-13-ent862-aauth-settings).

Settings → Integrations gains an AAuth panel (honest "On, inactive" state,
env-sourced values read-only, confirm before changing a live issuer, a
federation-readiness checklist, the stated limits) and an outbound A2A
endpoints panel over the OSS registry with `auth_scheme`; MCP
`register_a2a_endpoint` takes `auth_scheme`. The two-instance demo script
moves its generic settings writes to the dedicated route.

Tests: tests/unit/test_ent862_aauth_settings.py (flag matrix, route principal
matrix, generic-door refusals, compose wiring); mounted vitest specs for both
panels (each proven red by mutation); MCP a2a.test.ts auth_scheme cases.

Part 1 of 3 for Abilityai/trinity-enterprise#862.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…w-list UI (ent#862 PR2)

A2aPanel.vue — inbound allow-list section:
- Updated description to document both entry formats: email/username for
  local Trinity accounts and `aauth:<agent>@<peer-host>` for AAuth peers
- Added AAuth/Local badge (BaseBadge) to visually distinguish the two kinds
  of listed identity at a glance
- Updated input placeholder to show both formats
- Added client-side format warning when the entered value starts with
  `aauth:` but does not match `aauth:<local>@<host>` — fires before submit

MCP set_a2a_inbound_allowlist description updated to document the
`aauth:<agent>@<peer-host>` format alongside email/username.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Store the `aauth:<agent>@<peer-host>` identity on `schedule_executions`
so inbound AAuth-authenticated A2A calls are attributed end-to-end.

- `db/schema.py` + `db/tables.py`: add `a2a_caller_identity TEXT` column
- `db/migrations.py`: SQLite migration `a2a_caller_identity`
- `migrations/versions/0099_a2a_caller_identity.py`: Alembic mirror
- `db/write_params.py`: `TaskExecutionFields.a2a_caller_identity`
- `models.py`: `ExecutionResponse.a2a_caller_identity`
- `services/task_execution_service.py`: thread parameter through `execute_task`
- `routers/a2a.py`: pass `aauth.identity` into `_run_a2a_task` → `execute_task`
- `views/ExecutionDetail.vue`: show AAuth Caller in Execution Origin card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

✅ Alembic head check clear — merging this PR into dev leaves one head (0102_nevermined_delegations).

Previously flagged; resolved.

Advisory — this check does not block merge. · head_sha: 2757123a591b50aca7b8f4bae7f5b2902aa57325 · run

@vybe

vybe commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Heads-up on Alembic numbering from the ent#965 chain (trinity-pm): draft PR #3914 (feature/ent966-nevermined-delegation-backend, base dev) adds 0102_nevermined_delegations after dev's 0101_held_gate_turns. This PR adds 0099_a2a_caller_identity.py, which already shares its number with dev's 0099_agent_peer_skill_grants; when it renumbers, please take 0103 (behind the chain) so the graph stays one head on the merge train. No file-level conflict between the two branches beyond the ones this PR already has against dev (db/migrations.py, routers/a2a.py, tests/registry.json).

Trinity Agent (trinity) and others added 2 commits October 23, 2026 03:06
… 0101

- Alembic 0099_a2a_caller_identity -> 0102_a2a_caller_identity, down_revision
  0101_held_gate_turns (dev added 0099..0101; the branch had forked a second head).
- SQLite MIGRATIONS entry ordered after dev's three new entries.
- a2a.py: #3749 mints the execution row before dispatch, so the AAuth peer
  identity is now stamped in _mint_a2a_execution (execute_task ignores row fields
  when handed an execution_id); the now-dead execute_task kwarg is dropped.
- tests: pin that the minted row records the AAuth identity (and None for bearer).

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

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

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

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

@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.

@trinity-ability

Copy link
Copy Markdown
Contributor Author

/validate-pr — REQUEST CHANGES (Lane B + schema)

This PR requires the following changes before merge:

  • The new a2a_caller_identity column is written but never read back, so the API always returns null. GET …/executions/{id} builds ExecutionResponse(**execution.model_dump()) (routers/schedules.py:944) from the db_models.ScheduleExecution that db/schedules/executions.py::_row_to_execution returns. Neither of those knows about the field. As a result the "AAuth Caller" row in ExecutionDetail.vue can never render. Verified with a scratch probe on the PR head: create_task_execution(..., TaskExecutionFields(a2a_caller_identity="aauth:echo@a.example")) → get_execution(id) → ExecutionResponse(...).a2a_caller_identity is None. Fix: add a2a_caller_identity: Optional[str] = None to ScheduleExecution and map it in _row_to_execution, following the turn_integrity / chain_depth pattern. Add a read-back test (create, then get_execution, then assert on the response field). The current tests stop at the mint kwargs and the raw DB row, which is why they pass.
  • Alembic fork. feat(nevermined): fund an agent with a card — organization config, widget session, delegation bind/revoke, MCP tools (abilityai/trinity-enterprise#966) #3914 landed 0102_nevermined_delegations (down_revision 0101_held_gate_turns) on dev. This PR's 0102_a2a_caller_identity has the same parent, so merging gives two heads and upgrade head applies nothing (alembic-head-watch is red). The two revisions touch different tables (schedule_executions vs nevermined), so this is a mechanical fix: rename to 0103_a2a_caller_identity, re-parent it onto 0102_nevermined_delegations, and update the docstring reference in migrations.py.
  • Merge conflict in src/backend/db/migrations.py (the tail of the MIGRATIONS list). Merge dev and keep both entries.
  • Add a closing keyword. The body only says abilityai/trinity-enterprise#862, and this is PR2+PR3 of a multi-PR issue. If the PR does not complete the issue, Refs is fine. Either way, status-in-dev on ent#862 has to be set by hand because the issue is cross-tracker.

Non-blocking:

Please address these items and request re-review.

@trinity-ability trinity-ability added the status-needs-fix PR has an unaddressed review/validation finding; cleared by the author's next push (#2815) label Oct 26, 2026
… re-chain onto 0102_nevermined_delegations

- Resolve the MIGRATIONS tail conflict by keeping both entries
  (nevermined_delegations, then a2a_caller_identity).
- Rename the Alembic revision to 0103_a2a_caller_identity and re-parent it
  onto 0102_nevermined_delegations (#3914), so dev keeps a single head.
- Add a2a_caller_identity to db_models.ScheduleExecution and map it in
  _row_to_execution. Before this, GET .../executions/{id} always returned
  null because the column was written but never read back.
- Read-back test: create_task_execution -> get_execution -> ExecutionResponse.
- test_ent966: assert nevermined_delegations is registered, not that it is
  the last runner entry, since any later migration would break that.

Refs abilityai/trinity-enterprise#862

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot removed the status-needs-fix PR has an unaddressed review/validation finding; cleared by the author's next push (#2815) label Oct 26, 2026

@vybe vybe 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.

merge-train 2026-10-26: validated at head (no train branch — lane B batch, merged individually on Tier 1; Tier 2 verified on dev after).

@vybe
vybe merged commit 17adf5a into dev Oct 26, 2026
30 checks passed
webmixgamer added a commit that referenced this pull request Oct 26, 2026
Conflicts:
- docs/memory/architecture/security.md: dev (#3935) extended the "two human-only
  rules" paragraph; kept dev's paragraph with this branch's class rename
  (agent_callable -> agent_callable_self / agent_callable_any), then the
  self-gate paragraph.
- docs/memory/feature-flows.md: both sides added an index row; kept both,
  newest first.
- tests/unit/_route_census.py: both sides removed baseline rows (17 here, the
  whitelist add in #3935); FROZEN_BASELINE_COUNT = 330, the merged baseline's
  exact size.

Checked: #3913's routers/a2a.py change is inbound-path attribution; both outbound
handlers still open with assert_own_agent and keep the switch dependency, and no
new hand-copied self-gate arrived.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vybe pushed a commit that referenced this pull request Oct 27, 2026
…e PR, must end at schema.py (#3395) (#3974)

* ci(schema-parity): an install built by the merge-base, upgraded by the PR, must end at schema.py (#3395)

test_schema_parity.py models a fresh install only. On that path a migration
whose target table does not exist yet raises `no such table`, which the runner
skips as a fresh install, and init_schema then creates the table with every
column schema.py lists. So a schema.py column whose migration was never
registered (or never written) is present on every fresh install and missing
on every existing one, and no fresh-install comparison can see it. 213 of the
306 ADD COLUMN sites in db/migrations.py target a table only init_schema
creates (schedule_executions, users, chat_messages, ...), so a reverse diff on
the fresh path, as the issue proposes, is blind on 70% of them.

scripts/ci/check_sqlite_upgrade_parity.py models the existing install: it
resolves the merge-base of the PR's base and head, git-archives the
merge-base's src/backend, runs that tree's lifecycle (migrations, init_schema,
migrations) on a temp file DB in its own interpreter, then runs the checkout's
lifecycle on the same DB in a second interpreter, and requires every table,
column, index and trigger the checkout's init_schema declares to exist, with
migration_health at (n, n, None). Pure stdlib. Exit 0 pass, 1 gap (each
object named; a head lifecycle that raises on the upgraded install counts),
2 could not run (no base/head, no merge-base, the base tree failing to build
its install) - never a silent pass.

tests/unit/test_sqlite_upgrade_parity_guard.py drives the comparison core over
synthetic two-file backends (no git, no backend deps), so it runs in the unit
tier: registered migration passes; written-but-unregistered and
no-migration-at-all are gaps naming t.b; a new init_schema-only table is no
false red; an unbuildable base exits 2. Plus head-lifecycle-raises,
unrecorded-migration, the git fail-closed paths, and main() over a throwaway
git repo. Registered in tests/registry.json.

Mutation (the issue's exact class): replay PR #3913 (17adf5a against
17adf5a^) with `("a2a_caller_identity", _migrate_a2a_caller_identity),`
deleted from MIGRATIONS while the function and the schema.py column stay:
exit 1, "column schedule_executions.a2a_caller_identity: ... never gets it on
upgrade". With the function deleted too (R1'): exit 1, same column. As
shipped (R0): exit 0, 1 migration newly recorded, (170, 170, None). dev
against its own merge-base: exit 0. A PR adding an init_schema-only table plus
index on dev (N1): exit 0, 93 tables / 199 indexes.

* ci(schema-parity): run the SQLite upgrade-parity guard on every schema-relevant PR (#3395)

One step in the existing schema-parity job, after the dependency install
(the merge-base's lifecycle runs under them) and behind the job's self-skip,
on pull_request only (the merge-base comes from the PR's base/head SHAs; the
checkout already uses fetch-depth: 0). Same dummy env as the parity pytest.
The script and its self-test join the change-detection regex and push.paths,
the way check_alembic_parity.py is listed. The job id, and so the required
check name `schema-parity`, is unchanged. Header comment gains one paragraph.

Mutation: see the previous commit - the PR #3913 replay with its MIGRATIONS
line deleted exits 1 naming schedule_executions.a2a_caller_identity.

* ci(schema-parity): the upgrade-parity guard opens rows as production does and states its limits (#3395)

Review fixes on the guard and its self-test.

Guard:
- The lifecycle connection uses row_factory = sqlite3.Row, as
  db/connection.py::get_db_connection does, so a migration that reads a row
  by column name is not reported as a boot failure.
- A checkout whose db/schema.py or db/migrations.py does not import is
  "could not run" (exit 2), not a schema gap.
- Paths are resolved, tar extraction errors exit 2, and a single positional
  argument is a usage error rather than being silently ignored.
- The base-failure message says what to do (land a dependency bump on its
  own; update an old branch).
- The docstring states the limits it had left implicit: the install it
  builds has no rows (init_database's seeding steps are not run, so
  backfills, rebuild copies and constraints on existing rows are not
  exercised); only missing objects are reported; the ledger check is
  stricter than /health on purpose (a renamed migration re-runs, a removed
  one keeps being counted). The 213-of-306 count names its method.

Self-test (11 -> 22 tests): an index, a trigger and a table dropped by a
migration after init_schema; a renamed migration; a duplicate registration;
the restated order (migrations before init_schema, the second pass); rows by
name; a head that does not import; relative paths; a one-argument call.
Twelve guard mutations (each finding loop, both ledger checks, the
first_pending check, the second pass, the order, base failure -> pass,
merge-base -> head, head import -> gap, no row_factory) each turn at least
one test red; before this commit seven of the first ten survived.

* ci(schema-parity): the job runs the upgrade-parity self-test it is triggered by (#3395)

The self-test was in push.paths and the self-skip regex but not in the
parity pytest command, so a PR touching only it ran the job without running
it. It now sits next to test_alembic_parity_guard.py on that line. The test
also runs in backend-unit-test, as before.

* test(registry): the upgrade-parity self-test entry lists the cases added in review (#3395)

* ci(schema-parity): a broken dev no longer blocks the PR that repairs it (#3395)

When the merge-base's own lifecycle raised, the upgrade-parity guard exited 2,
so the PR that repairs a broken dev (whose merge-base is that broken commit)
was blocked by a required check, with branch protection the only way out. The
guard now walks back the merge-base's first-parent ancestors, at most
MAX_BASE_WALKBACK (20), to the newest one that builds, upgrades from it, and
warns naming every commit skipped. Not fail-open: the head still has to upgrade
over the longer range. Exit 2 stays the answer when the bound is exhausted.

Also from review: the stated limits and workflow header say the guard runs the
open-source runner only, never the enterprise one; database.md and Invariant #3
name the guard; the script is executable (ruff EXE001); the exhausted-walk
message no longer suggests the circular "land the bump first" order.

---------

Co-authored-by: trinity-ability <309458136+trinity-ability@users.noreply.github.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