Repository navigation
feat(a2a): AAuth identity badge + execution attribution (ent#862 PR2+PR3) - #3913
Conversation
… — 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>
|
✅ Alembic head check clear — merging this PR into Previously flagged; resolved. Advisory — this check does not block merge. · head_sha: |
|
Heads-up on Alembic numbering from the ent#965 chain (trinity-pm): draft PR #3914 ( |
… 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>
|
✅ Nightly unit-suite clean when this PR is merged into |
|
Resolve by merging |
|
/validate-pr — REQUEST CHANGES (Lane B + schema) This PR requires the following changes before merge:
Non-blocking:
Please address these items and request re-review. |
… 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>
vybe
left a comment
There was a problem hiding this comment.
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).
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>
…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>
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)
AAuth/Localbadge on each identity in the inbound allow-list (blue foraauth:, neutral for local)aauth:<agent>@<peer-host>format inline with a warning on malformed entriesset_a2a_inbound_allowlistMCP tool description to describe theaauth:formatPR3 — AAuth caller attribution on execution rows
a2a_caller_identity TEXTcolumn toschedule_executions(dual-track: SQLite migrationa2a_caller_identity+ Alembic0103_a2a_caller_identity)identityonto the execution row at_mint_a2a_execution(routers/a2a.py), read back throughExecutionResponsea2a_caller_identityonExecutionResponse(API)ExecutionDetail.vue(semantic tokens only, no raw palette classes)Test plan
aauth:agent@peer.example.com— badge shows "AAuth" in blue; adduser@example.com— badge shows "Local" in neutralaauth:bad(no@host) — inline warning appearsraw-color-baseline.jsonratchet:npm run test:unitpasses (no new raw palette classes added)check_alembic_heads.pypasses (single head0103_a2a_caller_identity)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)