Authority consolidation — S0 target design (SPRINT_20260722_016 AUTH-1)
Status: S0 DRAFT for review (2026-08-03). This S0 is unusual: substantial pieces of the program already LANDED out of stage order because they unblocked other programs — the draft consolidates them into the recipe frame rather than pretending the stages ran in sequence. Verification base: the sprint’s dig notes (2026-07-22/23/30), the live baselines, the 2026-08-03 boundary register, and ledger rounds 45–63. ⚠ marks remaining verification.
1. Current → target shape
| Current | Target | |
|---|---|---|
| Deployables | authority (own DB stellaops_authority — done), issuer-directory (schema issuer in the shared platform DB; the “352 kB” is index and catalogue overhead — measured read-only 2026-08-11, all four domain tables hold 0 rows, so AUTH-9’s copy is a no-op and its parity gate is 0 = 0. Re-measure per estate.) | authority absorbing issuer-directory (role vs scoped container = the S0 bootstrap-surface decision, §5.1) |
| DAL | Authority.Persistence (25 authority.* table declarations in the source lineage; 22 extant on a fresh chain because 001 drops three deprecated local-audit tables; LIVE-APPLIED chain — no re-baseline, round-21 deviation stands: issuer tables arrive as forward migrations appended to the existing chain) + IssuerDirectory.Persistence (5 domain tables — issuers, issuer_keys, trust_overrides, operator_provider_change_receipts, audit — plus the per-schema schema_migrations ledger, which does NOT carry) | issuer tables → new forward migrations in Authority.Persistence. LANDED 2026-08-04 (AUTH-3): one migration, 024_authority_issuer_directory_tables.sql |
2. Already landed out of stage order (consolidated into the frame)
| Piece | Stage it belongs to | Evidence |
|---|---|---|
IIssuerDirectoryClient widened to the full issuer surface (8 ops + update), closed graph | AUTH-5 (client half) | 2026-08-02; VexLens repointed same day — 003’s defect-3 closed; the client is the seam consumers keep through the fold |
Tenants producer: Authority’s FIRST domain outbox (AddEventingReliability → eventing schema in stellaops_authority), migration 023 row_version, same-transaction tenants catalog emission, startup backfill, /catalog-changes/tenants under catalog:replicate | AUTH-4 (X6 re-author, event half) | round 58; producer tests 3/3, e2e 5/5 |
SCR-4 lifecycle events (tenant.suspended transition-only, tenant.deleted + tombstone same-transaction) on tenant_lifecycle | AUTH-4 adjacent | round 62 |
Dual-mode consumer seam (AddStellaOpsTenantResolverDualMode) in Auth.ServerIntegration | the X14 exit’s library half (027 SCR-1) | round 59 |
Host-side mapping of the folded endpoint groups (IssuerDirectoryFold composition root in StellaOps.IssuerDirectory.Api; AddFoldedIssuerDirectory / AddFoldedIssuerDirectoryPolicies / MapFoldedIssuerDirectory in the Authority host, behind default-off IssuerDirectory:Fold:Enabled) | AUTH-5 (S5) — the D-AUTH7-5 ruling | 2026-08-11, 7758aa221b; AuthorityIssuerFoldHostingTests 6/6, two guards red-proven |
The fold is composed, not ported (2026-08-11). Four things the legacy host’s Program.cs does are deliberately absent from the composition root, and each one would have been a defect on the auth root rather than a carry — sprint findings D-AUTH5B-14…17:
- the folded persistence gets its own
PostgresOptionsinstance, because both it andAddAuthorityPostgresStorageconfigure the container’s UNNAMED one and the later registration wins — so the naive wiring would have silently movedAuthorityDataSourceand the tenants outbox onto theissuerschema; - no
AddStartupMigrations— Authority keeps ONE ledger (AUTH-3) and024owns those tables; - no CSAF publisher seed — it defaults to true in the legacy host and would write rows into the destination AUTH-9’s parity gate measures;
- tenants resolve from
authority.tenants, notshared.tenants. Authority’s own database carries a legacy, unmaintainedshared.tenants(3 rows againstauthority.tenants’s 5, measured 2026-08-11), so the carried resolver would have been silently wrong rather than loudly broken.
Consequence: AUTH-4’s X6 re-author was half done (events existed; the legacy cross-DB path did not), and AUTH-9’s forcing function (“tenant create propagates via the new contract to a consumer replica”) is now executable on a scratch stack whenever a consumer flips.
Closed 2026-08-05 (AUTH-4, 0b472b375a). The other half of X6 is the legacy cross-database READ, and it is deleted: GET /console/tenants no longer reads shared.tenants out of the Platform database to build a global admin’s switcher. It had the replica taking PRECEDENCE over authority.tenants, so a stale display name or status in the replica outranked the owner’s own row; and a row present only there is by construction not an Authority tenant, so listing it was a phantom. The catalogue is now owner-truth first (authority.tenants, then the configured catalogue, then home/selected). What remains of the X6 legacy path is the WRITE (SharedTenantsPropagator) plus the narrow ReadSlugsAsync its reconciliation check needs; both stay until the SCR-3 consumer flips complete and are deleted whole at AUTH-10.
3. Table inventory + dispositions
authority(25 declarations in lineage; 22 extant after a fresh001chain dropsaudit,airgap_audit, andoffline_kit_audit): NOT re-baselined (round-21 deviation). The P13 disposition is carried by forward migration026_authority_p13_retention_class_headers.sql; it conditionally classifies those three deprecated tables if an upgraded estate still retains them. Seed companionS050_authority_client_tenants_p13_comment_convergence.sqlruns after appliedS001so that seed baseline cannot stripclient_tenants’ header. Existing comment prose is preserved.issuerschema (5 domain tables → Authority chain):issuers(operational),issuer_keys(operational),trust_overrides(operational),operator_provider_change_receipts(audit),audit(audit). S9 copies 352 kB; parity = row counts + spot checks.- Correction (2026-08-04, AUTH-3): an earlier draft of this line counted “6 tables” and listed
operator_signing_keymachinery as one of them. There is nooperator_signing_keytable — verified against the liveissuerschema and against the four legacy migrations. The operator decision-signing key is a ROW SUBSET ofissuer_keys(metadata->>'purpose' = 'decisionsigning'), and its dual-control + append-only machinery is the'pending'status value plus the BEFORE DELETE trigger. Soissuer_keysis operational for ordinary keys and evidence class for the decision-signing rows; the sixth item in the old count was the per-schemaschema_migrationsledger, which does not carry (Authority keeps one ledger,authority.schema_migrations). - Guard semantics VERIFIED (2026-08-03):
003_operator_signing_key_append_only_guard.sql= a BEFORE DELETE trigger onissuer_keys(prevent_operator_decision_signing_key_delete, RAISE EXCEPTION); the current DDL is search_path-relative, so the fold must recreate function+trigger against the folded table home and prove it with a delete-attempt red test. DONE at AUTH-3 (the guard arrives WITH the tables — it is table-level contract, and creating an evidence-class table without its guard would leave a window): re-authored schema-qualified in024, placement asserted onpg_trigger/pg_proc, and red-proven — an ordinary-key delete succeeds while the decision-signing delete is refused by constraint name, including through theON DELETE CASCADEfromissuer.issuers. - Schema placement decision (AUTH-3, 2026-08-04): the
issuerschema is REPRODUCED insidestellaops_authority, not collapsed into theauthorityschema. Reason: S9 copiesissueras ONE unit and gates the window on row-count + spot-check parity, which a rename would make impossible to run mechanically. Same shape and same reasoning as the JobEngine sibling, which reproducedscheduler+packsunder one ledger. Both schemas are Authority-owned and live in Authority’s own database, so CoC §8.2 is satisfied. - Finding the S0 guard analysis missed (AUTH-3, 2026-08-04) — the search_path trap has a live collision target. The draft said only that the legacy DDL is “search_path-relative”. It is worse than that: Authority’s
001_v1_authority_baseline.sqlalready definesauthority.update_updated_at(), and three live triggers execute it (authority.roles,authority.tenants,authority.users). The legacy001_initial_schema.sqldeclares a helper of the SAME NAME, unqualified. Carried verbatim it resolves through the runner’sSET LOCAL search_path TO "authority", publicand REPLACES the running service’s function — a behaviour-changing edit to live objects, shipped on the next image rebuild. Demonstrated, not theorised: with a verbatim carry the migration succeeds with no error at all,issuer.update_updated_atnever exists, and onlyauthority.update_updated_atremains. The bodies are identical today so nothing would have visibly broken — the collision would simply have sat there until one of them changed. Every object in024is therefore explicitly qualified, andFoldedUpdatedAtHelper_DoesNotClobberAuthorityOwngoes red against the unqualified form.
- Correction (2026-08-04, AUTH-3): an earlier draft of this line counted “6 tables” and listed
- The X7 IdP platform-read (
LegacyPlatformIdentityProviderDataSourceimporting migration-087 rows from the platform DB) dies at the fold: Authority-owned IdP config (authority_identity_provider_configs, migration 019/020 lineage) is already the store; the import becomes a one-time reconcile then deletes — VERIFIED (2026-08-03): zero Platform code touches identity-provider tables (no writers remain; the platform copy is frozen), so reconcile-then-delete is safe. The import env (STELLAOPS_AUTHORITY_AUTHORITY__IDENTITYPROVIDERIMPORT__PLATFORMCONNECTIONSTRING— set in stella-services.yml, pointing at the GENERIC connection) is removed in the same change.- DONE at AUTH-4 (2026-08-05,
70fd9d4f7b). Importer, both data-source types, the result/conflict contracts, the hosted service, the DI wiring and the wholeAuthority:IdentityProviderImport:*surface are gone, together with the env in all THREE compose files (the live services stack and both release bundles — the draft named only stella-services.yml).platform.identity_provider_configsis untouched and now frozen and unread on both sides; dropping it stays a separate ADR-004 destructive window. - The draft understated the exposure. It framed X7 as a vestigial read. It was also a STARTUP DEPENDENCY of the auth root: the hosted service caught only import-CONFLICT exceptions, so an
NpgsqlExceptionfrom opening an unreachable Platform database propagated out ofIHostedService.StartAsyncand failed Authority’s startup — an estate whose platform DB was down could not log in, for an import that had nothing left to import. Enabled by default, and the connection resolver always resolves something, so no ordinary deployment had opted out. Proven removed rather than disabled:ServiceAccountConformanceMintTestsboots Authority end to end with no Platform database and now passes without theAuthority:IdentityProviderImport:Enabled=falseescape hatch it previously needed.
- DONE at AUTH-4 (2026-08-05,
4. Boundary state (2026-08-03)
authoritykey: ONE pin (→ attestor) — types VERIFIED (2026-08-03):AuthorityDsseStatementSigner/AuthoritySignerAdapterconsumeStellaOps.Attestation+Attestor.Envelope— the SAME carrier libraries the 011 sweep identified as the estate-wide dominant edge; this pin rides 011’s library-classification pass, no bespoke work here. The tenants/catalog work added ZERO pins (Catalog.Replication is neutral — conformance-verified).- Consumers of Authority: the four approved closed client SDKs (Auth.Abstractions/Client/ ServerIntegration + IssuerDirectory.Client) remain source seams; the VexLens/Platform PERSISTENCE defects: VexLens FIXED (2026-08-02); Platform’s
Authority.Persistenceuse ENUMERATED (2026-08-03): the real edge isPlatform.Database’sAuthorityMigrationModulePlugin(central-migrator fan-in — dies with 026, as suspected; removed 2026-09-08 by86056143f2, and the whole plugin mechanism was deleted 2026-09-14 by SPRINT_20260722_021 PLT-4);Platform.WebService’s DIRECT ProjectReference has zero source usage (no usings, no FQN) — a redundant edge whose removal changes nothing while the Platform.Database path exists, so it simply falls with 026 rather than warranting its own change.
4a. What issuer-directory IS (owner-requested explainer, review item A8)
What it does. The trust-administration surface for advisory/VEX ISSUERS: registers issuers and their signing keys (issuers, issuer_keys), holds per-tenant TRUST OVERRIDES (how much weight a tenant grants an issuer), and custodies the operator decision-signing key with dual-control approval and a database-enforced append-only guard (§3). Every mutation lands in its audit tables with provider-change receipts.
Who consumes it. The VEX consensus plane (issuer trust weights feed the DC-38 evaluator’s tier/trust inputs), the Console trust-admin pages (trust.client.ts), Excititor/VexLens via IssuerDirectory.Client (the closed SDK). Operator use cases: curate which advisory sources are trusted and how much; rotate signing keys under two-person control; tenant-scoped trust tuning.
Why fold it into Authority as a ROLE (the lean recommendation). It is identity/trust administration — Authority’s domain family; its whole dataset is ~352 kB; a dedicated container buys no blast-radius win while adding a deployable, a boot-order edge, and a route surface. As an Authority role it inherits the hardened host and bootstrap path. The alternative (scoped container) is justified only if trust-admin someday needs independent rollout/availability — nothing in the evidence demands it. OWNER RULING (2026-08-03, A8 closed): fold as an Authority ROLE — approved as recommended.
5. S0 decisions to settle
- Bootstrap-surface / container shape (§1): issuer-directory as an authority ROLE (lean recommendation — the 352 kB domain does not justify a second deployable; Authority is the bootstrap root and the fold removes one boot-order edge) vs scoped container. Write the bootstrap-order argument either way.
- AUTH-11 break-glass design (constraints recorded in the sprint) — design together with the bootstrap surface.
- The pipeline signing gap for the standard IdP plugin (recorded 2026-08-02: publish signs nothing; scratch ran enforcement off) — enforcement stays ON everywhere durable; the fix belongs to the plugin packaging pipeline, tracked here because the fold touches the same surface.
- P13-header audit scope for the live chain (§3) — source disposition closed by
026+S050; exact-current image application remains inside AUTH-9.
Remaining verification CLOSED (updated 2026-08-24) — the P13 audit now distinguishes the 25-declaration lineage from the 22 tables extant on a fresh chain and is forward-only in 026 + S050; applying it to the standing database remains part of the exact-current AUTH-9 image/window. Everything else is verified:
- Routes/Console: ONE gateway route (
/issuer-directoryprefix, also in the auth-passthrough list) — a small, stable surface for the fold’s route change.- CORRECTED 2026-08-05 (AUTH-5/AUTH-6, D-AUTH5B-6): this line previously read “Console
trust.client.ts+vex-trust-override.api.tscover the trust-admin pages (AUTH-6 scope)”. Neither client touches this plane.trust.client.ts:147calls/api/v1/administration/trust-signing, andvex-trust-override.api.ts:67builds onAPI_PATH_PREFIXES.findingsLedger— the Findings ledger, already centralised by FND-5. Repointing either at the issuer fold would move a working surface onto the wrong service. The real and only Console consumer iscore/api/decision-signing-enrollment.client.ts(operator decision-signing key enrollment:/issuer-directory/operator-signing-keys/{issuerId}/keys, plus/{keyId},/coverage,/enroll-invite), which AUTH-6 moved ontoAPI_PATH_PREFIXES.issuerDirectory. Backend consumers do not use this route at all: the six production users ofStellaOps.IssuerDirectory.ClientresolveIssuerDirectory:Client:BaseAddressdirectly (docker-compose.stella-services.yml:1517,1604), so retiring the container also needs those base addresses repointed — AUTH-8’s compose delta, invisible in the route table. - The route change is a RETARGET, not a collapse (D-AUTH5B-1): four Authority routes are OIDC protocol paths (
/connect,/connect/revocation,/.well-known,/jwks) pinned by the issuer metadata document, so the family cannot take the siblings’ one-regex-group shape. Every path the folded host serves already lives under/issuer-directory, so only the target host changes. Staged intools/scripts/auth5-issuer-directory-route-swap.jsonwith its applier, guard and runbook (docs/runbooks/authority/authority-cutover-auth5-route-swap.md).
- CORRECTED 2026-08-05 (AUTH-5/AUTH-6, D-AUTH5B-6): this line previously read “Console
- Deploy reality:
stellaops-issuer-directoryRUNS as its own live container — the fold retires a live deployable (window + drain at S7/S8); its publish key and theIDENTITYPROVIDERIMPORT__*env retire in the same compose delta (§3). - Platform exact-use, guard translation, X7 writer check, attestor pin types: §3/§4 above.
