feat(auth)!: migrate service accounts to successor agents (theme-8 phase 1) - #1386
Draft
Manuel-Jentic wants to merge 7 commits into
Draft
Manuel-Jentic wants to merge 7 commits into
Manuel-Jentic wants to merge 7 commits into
Conversation
…index Theme-8 Phase 1 schema trio, one additive admin migration (c0d1e2f3a4b5): - service_accounts.migrated_to_actor_id + migrated_at — the migration job's idempotency stamp (successor agnt_ id or the literal 'skipped' sentinel) and its timestamp (NF-3 verify criterion, N3 sweep age gate). - service_account_migration_acks — the operator-acknowledgement sentinel the theme-8 Phase-4 drop migrations require (toolkit_flattening_acks precedent; admin DB so the drop reads it where it drops). - uq_agent_credentials_api_key_hash — partial (NULL-exempt) unique index with BOTH dialect kwargs; the dialect-independent double-mint backstop for concurrent migration runs. Round-trip (upgrade/downgrade) and native partial-index enforcement are pinned against real SQLite; the Postgres twin runs in the integration suite. Co-authored-by: Cursor <cursoragent@cursor.com>
Theme-8 Phase 1 (H-2): the sak_ and jntc_live_ resolver arms now try agent_credentials first — a migrated key's digest lives there, so the unchanged plaintext resolves as its successor agent. Keys not yet migrated fall back to the unchanged service-account arm and resolve identically for the whole coexistence window (never a hard repoint: old-image pods must keep serving migrated keys through a rolling upgrade). Each fallback hit logs a service_account_fallback_resolve WARNING and bumps the new SERVICE_ACCOUNT_FALLBACK_RESOLVE telemetry event (L-C — pod-local stdout is not alertable); trending to zero is the sweep-readiness signal. The theme-5 deprecated_toolkit_key_used WARNING stays on the jntc_live_ fallback path, reworded to point at the successor agent. IMPL-DECISION 4 deviation, documented: ApiKeyResolver construction runs before the lifespan wires ctx.telemetry, so the injected sink is None in practice — the resolver falls back to the process-global active sink at record time; the constructor parameter stays for direct test injection. Co-authored-by: Cursor <cursoragent@cursor.com>
Theme-8 Phase 1 (N-2, NF-1/NF-2): once a service account carries the migration stamp, the live actor is its successor agent — a status flip, scope replacement, key rotation, or archive against the stamped row would silently no-op against the key (new pods resolve agent-first) or diverge the copied state (the archive's revoke_all would delete the grant rows the copy keeps; a rotation would break digest parity, the NF-3 verify criterion). Uniform service-layer predicate (migrated_to_actor_id IS NOT NULL) on the 7 mutation verbs: approve, deny, disable, enable (via _check_transition), archive, replace_scopes, and register_api_key. New ServiceAccountMigratedError names the successor; mapped to 409 service_account_migrated (IMPL-DECISION 7: the resource still exists and is readable during the window — 410 belongs to Phase 2's surface removal). create stays unguarded (F5: new rows are unstamped; the boot re-run migrates them). register_client_secret is unrouted (NF-4) — no guard on dead code. Co-authored-by: Cursor <cursoragent@cursor.com>
Theme-8 Phase 1 (F3, in Phase 1 deliberately): the self-contained-JWT
path builds Identity from claim-supplied scopes with no DB read, so a
service_account claim would sail past the SA→agent grant migration and
token revocation for the whole coexistence window. _ALLOWED_ACTOR_TYPES
shrinks to {AGENT}; refusals take the existing uniform-401 path with the
jwt_refused WARNING (reason jwt_actor_type_not_allowed). No rolling-skew
issue: old pods stay permissive (status quo), only new pods enforce.
BREAKING CHANGE: trusted-issuer JWTs claiming actor_type=service_account
are refused (401). Re-issue such tokens against the successor agent
(actor_type=agent) created by the theme-8 service-account migration.
Co-authored-by: Cursor <cursoragent@cursor.com>
Theme-8 Phase 1 core (W2/W3/W4/W9). Per service account, one admin transaction (copy → revoke → stamp → audit): a raw-SQL successor agent (never AgentService.create/approve — F1: DEFAULT_AGENT_SCOPES must not leak onto a zero-grant successor), COPIES of the stored scope grants (theme-8-retired service-accounts:* scopes excluded), toolkit and credential binding twins, the API-key digest copy (SA side stays live until the sweep — H-B), family revocation of the SA's opaque sessions (H-1), the idempotency stamp (migrated_to_actor_id + migrated_at; 'skipped' for pending/rejected/archived rows, disabled SAs get a disabled successor — OQ-1), and per-SA audit rows under the system actor. The control-DB toolkit_keys.migrated_actor_id re-stamp (M-E) follows the admin commit. Concurrency (H-A x F6): pg advisory-lock fast path, in-transaction stamp re-check, and the uq_agent_credentials_api_key_hash partial unique index as the dialect-independent backstop — a losing double-mint fails its whole transaction and is reported, never a partial write. The deferred sweep (N3) archives stamped rows, deletes the SA-keyed grant/binding originals, and NULLs the SA-side digest — gated on services.service_account_sweep_min_stamp_age_hours (default 24; 0 disables the gate, negative disables the automatic arm). verify runs the acceptance queries; --verify --acknowledge writes the service_account_migration_acks sentinel only when the verification passed in the same invocation (Phase-4 gate). Wiring: the migrate-service-accounts CLI (--diff-only, --report, --sweep-migrated, --verify, --acknowledge) and a fire-and-forget boot job on the control surface (stamp makes re-runs cheap no-ops and catches SAs created during the window — F5), followed by the age-gated automatic sweep. IMPL-DECISION 5 deviation, documented: audit rows use actor_type='system:job' (the toolkit_flattening precedent) — the guide's bare 'system' literal is forbidden by the no-system-actor arch test. Co-authored-by: Cursor <cursoragent@cursor.com>
Theme-8 Phase 1 (W8): after the SA→agent migration, no code path may create a service-account row — the boot-time retirement job would otherwise be an SA producer the migration then has to chase. Successor creation retargets to agents/agent_credentials (raw SQL, still exactly capabilities:execute — the F1 constraint applies identically), grant twins write actor_type='agent', and the outcome field is renamed successor_actor_id (pre-theme-8 stamps may still carry sva_ ids until the migration job re-stamps them). set_service_account_status becomes set_actor_status, dual-table during the window (IMPL-DECISION 8): agents first — after the M-E re-stamp, toolkit_keys.migrated_actor_id holds agnt_ ids and the old UPDATE would match zero rows, silently no-opping the operator's compromise-response lever — with a service_accounts fallback for unmigrated stragglers. find-by-name reuses a pre-theme-8 SA crash remnant rather than double-minting its digest onto an agent. Co-authored-by: Cursor <cursoragent@cursor.com>
Operator runbook for the service-account → agent migration: snapshot first (post-sweep reversal needs it — M-D), diff-only preview, the dual-kill note (disable the successor agent to cut a key in the window — NF-1), the fallback-counter sweep-readiness signal, sweep age gating (N3), stated window semantics (token death at migration, pre-sweep client-credentials trade-off), the successor-key rotation caveat (F2), and the verify/acknowledge sentinel gating the Phase-4 drops. Opens the service-accounts deprecation window in the Deprecations table. Co-authored-by: Cursor <cursoragent@cursor.com>
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.
Theme-8 Phase 1: migrate every service account to a successor agent — copy-then-sweep, with the full SA writer surface stamp-guarded for the coexistence window.
Part of epic #1379. Plan: jentic-one-plans
themes/theme-8-remove-service-accounts.md(rev 5, converged after 5 review passes) +themes/theme-8-IMPL.md— see plans#48 (approved).What this ships
c0d1e2f3a4b5on headb9d0e1f2a3b4):migrated_to_actor_id/migrated_atstamp pair onservice_accounts, theservice_account_migration_ackssentinel table, and a unique partial index onagent_credentials.api_key_hash(bothpostgresql_whereandsqlite_where) as the cross-dialect race backstop.DEFAULT_AGENT_SCOPES), grant/binding rows copied — twins coexist so old-image pods keep serving through a rolling upgrade; live SA access/refresh tokens revoked transactionally (incl. skip-but-stamp dispositions); control-DBtoolkit_keysre-stamp self-heals on re-run.service_account_sweep_min_stamp_age_hours, default 24): archives stamped SAs (including pre-archived ones' lingering rows) only after the fleet is past the window.sak_/jntc_live_digests resolve to the successor agent; unmigrated SAs fall back exactly as before (WARNING +service_account_fallback_resolvecounter — trending to zero is the sweep-readiness signal). Fails closed on both kill levers: an inactive successor stops the fallback, and a stamped SA is never a valid identity source.service_account_migrated) on all 7 SA mutation verbs + the raw-SQL key-retirement writers; kill-switch semantics preserved pre-migration.feat(broker)!):_ALLOWED_ACTOR_TYPES→{AGENT}.jentic_one migrate-service-accountswith--diff-only/--report/--sweep-migrated/--verify/--acknowledge; boot wiring on both lifespans; full runbook indocs/development/releasing.md.Review
Adversarially reviewed against the converged plan invariants. NEEDS-FIXES findings all fixed and folded in: H1 (SA fallback defeated the dual-kill lever — resolver now fails closed when the successor digest exists or the SA is stamped), M1–M5 (restamp healing, pre-archived sweepability,
jntc_live_deprecation signal kept on the agent arm, stamp-guarded raw writers, skip-stamp token revoke), L1–L3 (real two-session race test on both dialects, per-row failure isolation, sweep re-check).Suites green on both dialects (unit+arch, web, Postgres + SQLite integration). Two
test_verify.pyPostgres failures are pre-existing shared-fixture rot (proven with changes stashed).Breaking changes
actor_type=service_accountare refused (broker).service_account_migrated.