From 90eb24270c14213721427626ea7fca1ca1837df4 Mon Sep 17 00:00:00 2001 From: mintaka Date: Fri, 4 Sep 2026 13:53:50 -0400 Subject: [PATCH 1/8] docs(server): design the SubjectService token principal (RIG-2863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design addendum capturing Matt's ruling that the token-subject model admits a THIRD principal class, `SubjectService SubjectKind = 2` — one class for every first-party supervised compute tier that authenticates back to the Server (the RIG-1715 LLM gateway now, a future MCP gateway later), never a kind per tier. The current code seals `SubjectKind` "to exactly these two" (SubjectAccount=0, SubjectRunner=1), citing a since-retired v0.6 milestone record. Per the corpus convention (a frozen record is amended by a NEW record, never rewritten), this addendum is the amendment surface for that seal. Shipped BEFORE the implementation (RIG-3122 PR2) because the enum number and the `tokens.subject_kind` CHECK constraint land in `0001_init.sql` and are painful to rename once token rows exist. The record's Plan is the PR2 executor contract: the `SubjectService` const + seal-comment update, the CHECK `IN (0,1)`->`IN (0,1,2)` edit, the nolint text, the service-door mount via `ResolveToken(...want=SubjectService)`, and the 3x3 cross-door pgtest matrix. Isolation is per-Subject-ID + per-surface authz over the shared resolver's `want != Kind` gate — no new resolver, mirroring how SubjectAccount and SubjectRunner isolate today. Ledger: DL-325 (Topology & tiers). Ledger-impact: DL-325 (new) Spec-impact: none Refs RIG-2863 Co-authored-by: Matt Wilkinson --- docs/designs/DECISIONS.md | 1 + .../compass-service-subject-principal.md | 214 ++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 docs/designs/server/compass-service-subject-principal.md diff --git a/docs/designs/DECISIONS.md b/docs/designs/DECISIONS.md index 8b01f91f..b0554f66 100644 --- a/docs/designs/DECISIONS.md +++ b/docs/designs/DECISIONS.md @@ -58,6 +58,7 @@ check enforces the mechanical half. Full rationale: | DL-011 | The Server tier is built by promoting `compass-daemon` in place, not a new server crate | Active (Matt, 2026-07-08) | [architecture lineage](meta/compass-architecture-lineage/design.md) | | DL-012 | The authenticated network door is rustls-via-tonic TLS + bearer-token accounts (in-memory token store for T2) | Active (Matt, 2026-07-08) | [architecture lineage](meta/compass-architecture-lineage/design.md) | | DL-312 | Servers are stateless L7-load-balanced instances (any Server handles any client request); Runners are individually subject-addressable over a message-bus fabric (one FABRIC connection per party — plus the reduced Connect authn/RPC edge per DL-316), never single-Server-owns-Runners and never an N×M direct-stream mesh; delivery to a live client routes over a per-connection subject to the one Server holding the socket; session→Runner routing truth stays durable in Postgres (`agent_placements` + session bindings), and the Runner remains a pure forwarder with Server-side fail-closed account resolution | Active (Matt, 2026-08-31) | [multi-tenancy & NATS substrate](infra/runtime/compass-managed-multitenancy/design.md#q2--serverrunner-connection-topology-load-balanced-servers-subject-addressable-runners) | +| DL-325 | The token-subject model admits a THIRD principal class, `SubjectService SubjectKind = 2` — ONE class for every first-party supervised compute tier authenticating back to the Server (LLM gateway now, MCP gateway later), never a kind per tier: tiers present distinct Subject IDs (`llm-gateway`, `mcp-gateway`) isolated by per-surface authz (the account-door `owner_user_id` precedent), and the shared `ResolveToken` `want != Kind` gate auto-rejects cross-door presentation. `tokens.subject_kind` CHECK extends `IN (0, 1)` → `IN (0, 1, 2)` in `0001_init.sql` before data exists. Named `SubjectService`, not `SubjectStack`/`SubjectGateway`. Amends the retired v0.6 record's two-kind seal | Active (Matt, 2026-09-04) | [service subject principal](server/compass-service-subject-principal.md) | ## Transport diff --git a/docs/designs/server/compass-service-subject-principal.md b/docs/designs/server/compass-service-subject-principal.md new file mode 100644 index 00000000..709bb084 --- /dev/null +++ b/docs/designs/server/compass-service-subject-principal.md @@ -0,0 +1,214 @@ +# Design addendum: Service subject principal (`SubjectService = 2`) + +Status: Active +Tracking: RIG-2863 / RIG-3122 (parent RIG-1715, RIG-2845) + +Ledger: DL-325 (this PR), Topology & tiers. Amends the two-kind token-subject +seal frozen by the retired v0.6 milestone record (see Problem / Intent); no +rows superseded. Per the corpus convention, the frozen record is amended by +this NEW record, never rewritten. + +## Problem / Intent + +The token-subject model is sealed to exactly two principal classes. The seal +lives in code, citing its frozen record: + +- `go/internal/store/types.go:90-91` — "Sealed to exactly these two + (design.md: 1175-1183)." with the two consts at `types.go:94-99`: + `SubjectAccount SubjectKind = 0` / `SubjectRunner SubjectKind = 1`. +- `go/internal/store/migrations/0001_init.sql:377` — + `subject_kind SMALLINT NOT NULL CHECK (subject_kind IN (0, 1))`, under the + header comment (`0001_init.sql:371-373`): "Subject-typed token store + (design.md:1175-1183) … subject_kind is 0 account / 1 runner". +- `go/internal/store/tokens.go:20` — + `SubjectKind: int16(subj.Kind), //nolint:gosec // G115: SubjectKind is a + CHECK-constrained 0/1 enum (tokens.subject_kind), always within int16`. + +The `design.md:1175-1183` those comments cite is the RETIRED v0.6 milestone +record, `docs/designs/product/compass-0.6/design.md` — deleted in commit +`2ccb7b2e` ("refactor(design): retire the v0.3–v0.8 milestone records, +consolidate to an architecture-lineage record (RIG-2453)"). Verified against +`git show 2ccb7b2e~1:docs/designs/product/compass-0.6/design.md`, lines +1177-1185: "Tokens (subject-typed, so a Runner subject and an account subject +share the store but never collide — the OQ7 prefix-separation T4 depends on): +… `SubjectKind ∈ {Account, Runner}` … (returns the subject *with its kind*, so +a door can reject a cross-kind token …)". That record's decision authority now +lives in the ledger (the DL-012 bearer-token-door lineage, +`meta/compass-architecture-lineage/design.md` §"The authenticated door"); +**this addendum is the amendment surface for the seal**. + +The seal blocks a first-party principal class the system now needs: the RIG-1715 +LLM gateway (`docs/designs/server/compass-server-llm-gateway/design.md`) is a +supervised compute tier that authenticates BACK to the Server — its +"RPC-to-Server-for-creds" surface is "a narrow, stack-token-authenticated +Server surface (list credentials for pool / write back refreshed OAuth tokens / +CAS disable)" (llm-gateway record, §Approach, lines 348-350). That stack token +is neither an account subject nor a Runner subject; a future MCP gateway +presents the same problem. Intent: admit a THIRD token-subject principal class, +`SubjectService SubjectKind = 2`, for every first-party supervised compute tier +that authenticates back to the Server. + +## Approach + +**Matt-ruled (frozen; this record captures the ruling, it does not reopen +it):** `SubjectService = 2` is ONE principal CLASS for all supervised compute +tiers — the LLM gateway now, an MCP gateway later — NOT a kind per tier. + +- **`SubjectKind` separates principal CLASSES, not instances.** Each supervised + tier presents its OWN minted service token with a DISTINCT Subject ID (e.g. + `llm-gateway` vs `mcp-gateway`). Tiers are isolated from each other by + Subject ID + per-surface authorization, not by minting a new kind per tier. + The precedent is the account door: `SubjectAccount` is one class covering + users AND agents, with per-surface authz layered on the resolved identity — + e.g. the owner resolution over `agent_accounts.owner_user_id` + (`go/internal/comms/comms.go:114-115`: "an agent caller resolves to its + owner_user_id, a user caller to itself") discriminating what a given account + subject may do. A service door authorizes the resolved `Subject.ID` against + the surface's allowlist the same way. +- **The existing cross-door kind-gate isolates the new class for free.** + `ResolveToken` (`go/internal/auth/token.go:102-116`) is the ONE shared + resolver: `if subj.Kind != want { return store.Subject{}, ErrWrongKind }` + (`token.go:112-114`). Its doc states the door contract (`token.go:98-101`): + "Both the account door (want=SubjectAccount) and the Runner door + (want=SubjectRunner) share this one resolver, so the security-critical + resolve+kind-gate lives and is tested in exactly one place; each door adds + only its own trivial typed wrap on the returned Subject." A service door is + one more trivial typed wrap passing `want=store.SubjectService` — an account + or Runner token presented at the service door (and a service token at the + account/runner doors) auto-rejects as `ErrWrongKind`, exactly as + `SubjectAccount` and `SubjectRunner` isolate today (existing wraps: + `go/internal/runnerhub/auth.go:79` `b.resolve(ctx, token, + store.SubjectRunner)`; the account-bearer interceptor in + `go/internal/auth/interceptor.go`). No new resolver. +- **Named `SubjectService`** — NOT `SubjectStack` (reads as the deployment + stack), NOT `SubjectGateway` (narrower than the class). + +Shipping this addendum BEFORE the implementation (PR2) is deliberate: the enum +NUMBER and the `tokens.subject_kind` CHECK constraint land in `0001_init.sql` +and are painful to rename once token rows exist. + +## Global Constraints + +- The two existing kinds keep their numbers: `SubjectAccount = 0`, + `SubjectRunner = 1` (`types.go:96-98`). `SubjectService = 2` is additive; + numbers are append-only, never reused. +- The one-resolver invariant holds: every door authenticates through + `auth.ResolveToken` (`token.go:102`); no door grows its own resolve or + kind-check. +- The token-existence-oracle posture holds: every door maps + `ErrTokenNotFound` / `ErrTokenRevoked` / `ErrWrongKind` to the same bare + `CodeUnauthenticated` (`token.go:92-97`). +- `0001_init.sql` is the initial migration — editable only while it remains + unshipped-to-data; the CHECK edit lands there, not in a new migration + (the reason this record precedes PR2). + +## Plan + +PR2 executor contract — the five edits, each grounded on the current line: + +### T1 — `SubjectService` const + seal-comment update + +`go/internal/store/types.go`: add the third const to the block at +`types.go:94-99` and update the seal sentence at `types.go:90-91` ("Sealed to +exactly these two (design.md: 1175-1183).") to name three kinds and cite THIS +record. Also extend the `Subject.ID` doc (`types.go:106-107`: "ID is the +AccountID (SubjectAccount) or the Runner id (SubjectRunner)…") to name the +service id space (a stable service name, e.g. `llm-gateway`). + +Interfaces: + +```go +// produces (append to the existing const block, types.go:94-99): +// SubjectService is a first-party supervised compute tier (LLM gateway, +// future MCP gateway) authenticating back to the Server. One class for all +// tiers; instances are distinguished by Subject.ID, isolated per-surface. +SubjectService SubjectKind = 2 +// seal comment becomes: "Sealed to exactly these three +// (docs/designs/server/compass-service-subject-principal.md)." +``` + +### T2 — CHECK constraint admits 2 + +`go/internal/store/migrations/0001_init.sql:377`: the column line changes and +the table header comment at `0001_init.sql:372-373` ("subject_kind is 0 account +/ 1 runner") gains `/ 2 service`. + +Interfaces: + +```sql +-- consumes: CHECK (subject_kind IN (0, 1)) -- 0001_init.sql:377 +-- produces: CHECK (subject_kind IN (0, 1, 2)) +``` + +### T3 — nolint text tracks the enum + +`go/internal/store/tokens.go:20`: the gosec waiver's justification names the +constrained set; it must not go stale. + +Interfaces: + +```go +// consumes: //nolint:gosec // G115: SubjectKind is a CHECK-constrained 0/1 enum (tokens.subject_kind), always within int16 +// produces: //nolint:gosec // G115: SubjectKind is a CHECK-constrained 0/1/2 enum (tokens.subject_kind), always within int16 +``` + +### T4 — service-door mount + +The service surface (first consumer: the LLM gateway's stack-token RPC surface +per the llm-gateway record) authenticates via the shared resolver with the new +want — a typed wrap in the pattern of `runnerhub`'s +(`go/internal/runnerhub/auth.go:79`: `b.resolve(ctx, token, +store.SubjectRunner)`), never a second resolver. + +Interfaces: + +```go +// consumes: auth.ResolveToken(ctx, st, presented, want store.SubjectKind) (store.Subject, error) // token.go:102 +// produces: the service door's bearer authenticate calling +subj, err := auth.ResolveToken(ctx, st, presented, store.SubjectService) +// all three sentinels map to bare CodeUnauthenticated (token.go:92-97); +// per-surface authz then checks subj.ID against the surface's service allowlist. +``` + +### T5 — cross-door pgtest + +Extend the store/auth pgtest coverage (harness per +`go/internal/store/harness_test.go`) with the three-kind cross-door matrix: a +`SubjectService` token resolves at `want=SubjectService`; presented at +`want=SubjectAccount` and `want=SubjectRunner` it fails `ErrWrongKind`; an +account token and a Runner token presented at `want=SubjectService` each fail +`ErrWrongKind`. Also a store-level round-trip: `PutTokenHash` with +`Subject{Kind: SubjectService, ID: "llm-gateway"}` persists (proving the T2 +CHECK admits 2) and `ResolveTokenHash` returns the kind intact. + +Interfaces: + +```go +// consumes: auth.ErrWrongKind (token.go:82), store.PutTokenHash (tokens.go:14), +// store.ResolveTokenHash (tokens.go:36), pgtest harness +// produces: pgtest cases in the existing auth/store pgtest files; no new harness. +``` + +## Tasks + +- [ ] T1: `SubjectService SubjectKind = 2` + seal comment (`types.go:90-99`) + `Subject.ID` doc +- [ ] T2: `0001_init.sql:377` CHECK `IN (0, 1)` → `IN (0, 1, 2)` + header comment `:372-373` +- [ ] T3: `tokens.go:20` nolint `0/1` → `0/1/2` +- [ ] T4: service-door mount via `ResolveToken(..., store.SubjectService)` + per-surface Subject-ID authz +- [ ] T5: cross-door pgtest matrix (3×3 kind-gate + CHECK-admits-2 round-trip) + +## Open Questions + +- **CHECK shape: inline IN-list vs lookup table (non-load-bearing for THIS + slice, surfaced for the record).** The current constraint is an inline + `CHECK (subject_kind IN (0, 1))` (`0001_init.sql:377`); this record extends + it in place. A `subject_kinds` lookup table with an FK would make future kind + additions a row-insert instead of a constraint edit — but every kind addition + is a Matt-ruled design event anyway (this record exists precisely because + one is), so the schema ceremony buys nothing over the one-line CHECK edit. + Recommendation: keep the inline IN-list. Only escalate if Matt expects + kind churn beyond design-gated additions. +- Non-load-bearing deferral: the canonical Subject-ID registry for service + principals (e.g. `llm-gateway`, `mcp-gateway` as named constants vs + config-supplied strings) is a PR2 implementation detail of the T4 surface's + allowlist; it does not affect the schema or the enum. From b5d825fe20c920ccd21c57ea4b300ac5a4e3cc52 Mon Sep 17 00:00:00 2001 From: mintaka Date: Fri, 4 Sep 2026 15:12:29 -0400 Subject: [PATCH 2/8] docs(server): fix review findings on the SubjectService design (RIG-2863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of #868 returned 0 high / 1 medium / 2 low. Citations (16/16) and the SubjectService ledger row were a clean pass; the blocker was executor-contract coherence. - F1 (medium): the Plan framed all five tasks as one "PR2 executor contract", but T4 (the service-door mount) targets a Server surface that does not exist in the tree yet, and no corpus task owns minting a SubjectService token — so the class as written ships unreachable. Reframed the Plan into two scopes: the enum half (T1/T2/T3/T5) lands now as PR2 (the urgent half — enum number + CHECK are painful to change once token rows exist); the door half (T4) lands WITH the RIG-2863 (RIG-1715 T2) service surface it mounts on, ordered after it. Added OQ-3 naming where token issuance lives (the RIG-2863 T4 slice, an IssueServiceToken mirroring MintRunnerToken) so an executor never improvises a mint path on a security-critical door. - F2 (low): citation span `lines 348-350` -> `348-351` (the quoted "disable)" falls on 351). - F3 (low, pre-existing): noted that `runnerhub.RunnerTokenRegistered` is the one token-store consumer outside the kind-gated door contract — flagged for the T4 executor to kind-check or document, out of scope for this record. Ledger-collision renumber: main's #870 (RIG-3096) merged its own DL-324 (forge live-test app-actor) after this stack forked, and the sibling encryption PR (#859) took DL-325/DL-326. Renumbered this record's SubjectService row to the next free id, DL-327 (was DL-325), leaving main's DL-324 untouched. Additive review-fix commit. Ledger-impact: DL-327 (SubjectService, renumbered from DL-325) Spec-impact: none Refs RIG-2863 Co-authored-by: Matt Wilkinson --- docs/designs/DECISIONS.md | 2 +- .../compass-service-subject-principal.md | 33 ++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/docs/designs/DECISIONS.md b/docs/designs/DECISIONS.md index b0554f66..20a584f6 100644 --- a/docs/designs/DECISIONS.md +++ b/docs/designs/DECISIONS.md @@ -58,7 +58,7 @@ check enforces the mechanical half. Full rationale: | DL-011 | The Server tier is built by promoting `compass-daemon` in place, not a new server crate | Active (Matt, 2026-07-08) | [architecture lineage](meta/compass-architecture-lineage/design.md) | | DL-012 | The authenticated network door is rustls-via-tonic TLS + bearer-token accounts (in-memory token store for T2) | Active (Matt, 2026-07-08) | [architecture lineage](meta/compass-architecture-lineage/design.md) | | DL-312 | Servers are stateless L7-load-balanced instances (any Server handles any client request); Runners are individually subject-addressable over a message-bus fabric (one FABRIC connection per party — plus the reduced Connect authn/RPC edge per DL-316), never single-Server-owns-Runners and never an N×M direct-stream mesh; delivery to a live client routes over a per-connection subject to the one Server holding the socket; session→Runner routing truth stays durable in Postgres (`agent_placements` + session bindings), and the Runner remains a pure forwarder with Server-side fail-closed account resolution | Active (Matt, 2026-08-31) | [multi-tenancy & NATS substrate](infra/runtime/compass-managed-multitenancy/design.md#q2--serverrunner-connection-topology-load-balanced-servers-subject-addressable-runners) | -| DL-325 | The token-subject model admits a THIRD principal class, `SubjectService SubjectKind = 2` — ONE class for every first-party supervised compute tier authenticating back to the Server (LLM gateway now, MCP gateway later), never a kind per tier: tiers present distinct Subject IDs (`llm-gateway`, `mcp-gateway`) isolated by per-surface authz (the account-door `owner_user_id` precedent), and the shared `ResolveToken` `want != Kind` gate auto-rejects cross-door presentation. `tokens.subject_kind` CHECK extends `IN (0, 1)` → `IN (0, 1, 2)` in `0001_init.sql` before data exists. Named `SubjectService`, not `SubjectStack`/`SubjectGateway`. Amends the retired v0.6 record's two-kind seal | Active (Matt, 2026-09-04) | [service subject principal](server/compass-service-subject-principal.md) | +| DL-327 | The token-subject model admits a THIRD principal class, `SubjectService SubjectKind = 2` — ONE class for every first-party supervised compute tier authenticating back to the Server (LLM gateway now, MCP gateway later), never a kind per tier: tiers present distinct Subject IDs (`llm-gateway`, `mcp-gateway`) isolated by per-surface authz (the account-door `owner_user_id` precedent), and the shared `ResolveToken` `want != Kind` gate auto-rejects cross-door presentation. `tokens.subject_kind` CHECK extends `IN (0, 1)` → `IN (0, 1, 2)` in `0001_init.sql` before data exists. Named `SubjectService`, not `SubjectStack`/`SubjectGateway`. Amends the retired v0.6 record's two-kind seal | Active (Matt, 2026-09-04) | [service subject principal](server/compass-service-subject-principal.md) | ## Transport diff --git a/docs/designs/server/compass-service-subject-principal.md b/docs/designs/server/compass-service-subject-principal.md index 709bb084..f341e50f 100644 --- a/docs/designs/server/compass-service-subject-principal.md +++ b/docs/designs/server/compass-service-subject-principal.md @@ -3,7 +3,7 @@ Status: Active Tracking: RIG-2863 / RIG-3122 (parent RIG-1715, RIG-2845) -Ledger: DL-325 (this PR), Topology & tiers. Amends the two-kind token-subject +Ledger: DL-327 (this PR), Topology & tiers. Amends the two-kind token-subject seal frozen by the retired v0.6 milestone record (see Problem / Intent); no rows superseded. Per the corpus convention, the frozen record is amended by this NEW record, never rewritten. @@ -42,7 +42,7 @@ LLM gateway (`docs/designs/server/compass-server-llm-gateway/design.md`) is a supervised compute tier that authenticates BACK to the Server — its "RPC-to-Server-for-creds" surface is "a narrow, stack-token-authenticated Server surface (list credentials for pool / write back refreshed OAuth tokens / -CAS disable)" (llm-gateway record, §Approach, lines 348-350). That stack token +CAS disable)" (llm-gateway record, §Approach, lines 348-351). That stack token is neither an account subject nor a Runner subject; a future MCP gateway presents the same problem. Intent: admit a THIRD token-subject principal class, `SubjectService SubjectKind = 2`, for every first-party supervised compute tier @@ -104,7 +104,15 @@ and are painful to rename once token rows exist. ## Plan -PR2 executor contract — the five edits, each grounded on the current line: +PR2 executor contract — TWO scopes, each grounded on the current line. **The +enum half (T1/T2/T3/T5) lands NOW as PR2** — every file+line it names exists +today, and it is the urgent half (the enum number + `tokens.subject_kind` +CHECK are painful to change once token rows exist, per Problem / Intent). **The +door half (T4) lands WITH the service surface**, which does not exist in the +tree yet: it is delivered by RIG-2863 (RIG-1715 T2 — the AuthStorage-over-compass +adapter + the LLM gateway's stack-token RPC surface, currently Backlog), and +T4 mounts on it there, ordered AFTER that surface. SubjectService token +ISSUANCE (the mint path, no corpus task owns it yet) is OQ-3 below. ### T1 — `SubjectService` const + seal-comment update @@ -152,7 +160,7 @@ Interfaces: // produces: //nolint:gosec // G115: SubjectKind is a CHECK-constrained 0/1/2 enum (tokens.subject_kind), always within int16 ``` -### T4 — service-door mount +### T4 — service-door mount (lands WITH the RIG-2863 service surface, NOT in PR2) The service surface (first consumer: the LLM gateway's stack-token RPC surface per the llm-gateway record) authenticates via the shared resolver with the new @@ -194,7 +202,7 @@ Interfaces: - [ ] T1: `SubjectService SubjectKind = 2` + seal comment (`types.go:90-99`) + `Subject.ID` doc - [ ] T2: `0001_init.sql:377` CHECK `IN (0, 1)` → `IN (0, 1, 2)` + header comment `:372-373` - [ ] T3: `tokens.go:20` nolint `0/1` → `0/1/2` -- [ ] T4: service-door mount via `ResolveToken(..., store.SubjectService)` + per-surface Subject-ID authz +- [ ] T4 (RIG-2863 T2, NOT PR2): service-door mount via `ResolveToken(..., store.SubjectService)` + per-surface Subject-ID authz, mounted on the stack-token RPC surface that slice delivers - [ ] T5: cross-door pgtest matrix (3×3 kind-gate + CHECK-admits-2 round-trip) ## Open Questions @@ -208,6 +216,21 @@ Interfaces: one is), so the schema ceremony buys nothing over the one-line CHECK edit. Recommendation: keep the inline IN-list. Only escalate if Matt expects kind churn beyond design-gated additions. +- **OQ-3 (deferral naming the owning slice — NON-load-bearing for PR2, the enum + half): SubjectService token ISSUANCE lives in the RIG-2863 T4 slice.** Both + existing kinds have a real mint path — `IssueAccountToken` (`token.go:51-55`) + for `SubjectAccount`, `runnerhub.MintRunnerToken` (`mint.go:103`) + the + `compass-mint-runner-token` CLI for `SubjectRunner`. No corpus task mints a + `SubjectService` token yet (this record's T5 only writes rows test-side via + `PutTokenHash`), so the enum + door would otherwise ship with no principal able + to pass the door. Resolution (driver call, boring-consistent — mirrors the + existing mint paths, no design fork): issuance lands in the RIG-2863 (RIG-1715 + T2) slice ALONGSIDE the service surface T4 mounts on — an `IssueServiceToken` + (boot/store fn or an operator CLI, mirroring `MintRunnerToken`), minting under + a distinct Subject ID per tier (`llm-gateway`, later `mcp-gateway`). PR2 (the + enum half, T1/T2/T3/T5) does NOT depend on it; it is recorded here so the T4 + slice owns it explicitly rather than an executor improvising a mint path on a + security-critical door. - Non-load-bearing deferral: the canonical Subject-ID registry for service principals (e.g. `llm-gateway`, `mcp-gateway` as named constants vs config-supplied strings) is a PR2 implementation detail of the T4 surface's From 2b5704b89c5c55fac5c5c3c595e0ef813bc05dd7 Mon Sep 17 00:00:00 2001 From: mintaka Date: Fri, 4 Sep 2026 16:02:14 -0400 Subject: [PATCH 3/8] docs(server): fix round-2 review findings on the SubjectService design (RIG-2863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-2 review verified all four round-1 fixes good (F1 two-scope Plan, F2 citation, DL-327 renumber, security lens a clean pass — the kind-gate isolates the new class for free) and found no high, three mediums + two lows. All are internal-consistency / constraint-accuracy defects (two are residue the F1 reframe left behind); all verified against the tree before folding; none is a design fork. - **M1 (slice-shorthand normalization):** the record used three shorthands for the RIG-2863 (= RIG-1715 T2) slice, one of which — "the RIG-2863 T4 slice" — collides with the real RIG-1715 T4 (compass-obs usage-metering), risking an executor routing IssueServiceToken to the wrong lane. Normalized all sites: Tasks T4 line now "(lands in RIG-2863 = RIG-1715 T2, NOT PR2)"; OQ-3 heading now "lands in the RIG-2863 slice (RIG-1715 T2), alongside this record's T4". - **M2 (registry deferral retargeted):** the Subject-ID registry bullet still called the registry "a PR2 implementation detail of the T4 surface's allowlist" after this record moved T4 out of PR2 — an executor holding the PR2 contract would build allowlist scaffolding into the enum-only PR. Retargeted: it lands WITH T4 in the RIG-2863 slice, PR2 does not touch it. - **M3 (0001 in-place-edit safe condition):** the constraint + the DL-327 row said the CHECK edit is safe "before data exists", but migrate() is version-keyed (skips any version in schema_migrations, store.go:157-176), not data-keyed — a DB that has applied v1 keeps the stale CHECK regardless of row count and boots clean past the refuse-to-serve guard. Restated the condition as disposable-env / version-keyed (wipe+re-migrate or new ALTER; pre-GA posture per RIG-3106 #830, RIG-2861 T1 #715) in both the record and DL-327. - **L1:** T5 now names both harnesses + build tags precisely (`go/internal/auth/token_test.go:134-154`, harness `harness_pgtest_test.go`, tag `pgtest && unix`; store round-trip in `tokens_test.go`, tag `pgtest`) — an auth case added without the `unix` tag silently never compiles. - **L2:** added a doc-refresh clause to T4 (which is where the third door wrap actually appears) naming the two-door prose the third door invalidates (token.go:98-99, :79-81, interceptor.go:140-141, runnerhub/auth.go:4-14). Ledger-impact: DL-327 (row's in-place-edit safe-condition restated) Refs RIG-2863 Co-authored-by: Matt Wilkinson --- docs/designs/DECISIONS.md | 2 +- .../compass-service-subject-principal.md | 45 +++++++++++++++---- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/docs/designs/DECISIONS.md b/docs/designs/DECISIONS.md index 20a584f6..2c0b9068 100644 --- a/docs/designs/DECISIONS.md +++ b/docs/designs/DECISIONS.md @@ -58,7 +58,7 @@ check enforces the mechanical half. Full rationale: | DL-011 | The Server tier is built by promoting `compass-daemon` in place, not a new server crate | Active (Matt, 2026-07-08) | [architecture lineage](meta/compass-architecture-lineage/design.md) | | DL-012 | The authenticated network door is rustls-via-tonic TLS + bearer-token accounts (in-memory token store for T2) | Active (Matt, 2026-07-08) | [architecture lineage](meta/compass-architecture-lineage/design.md) | | DL-312 | Servers are stateless L7-load-balanced instances (any Server handles any client request); Runners are individually subject-addressable over a message-bus fabric (one FABRIC connection per party — plus the reduced Connect authn/RPC edge per DL-316), never single-Server-owns-Runners and never an N×M direct-stream mesh; delivery to a live client routes over a per-connection subject to the one Server holding the socket; session→Runner routing truth stays durable in Postgres (`agent_placements` + session bindings), and the Runner remains a pure forwarder with Server-side fail-closed account resolution | Active (Matt, 2026-08-31) | [multi-tenancy & NATS substrate](infra/runtime/compass-managed-multitenancy/design.md#q2--serverrunner-connection-topology-load-balanced-servers-subject-addressable-runners) | -| DL-327 | The token-subject model admits a THIRD principal class, `SubjectService SubjectKind = 2` — ONE class for every first-party supervised compute tier authenticating back to the Server (LLM gateway now, MCP gateway later), never a kind per tier: tiers present distinct Subject IDs (`llm-gateway`, `mcp-gateway`) isolated by per-surface authz (the account-door `owner_user_id` precedent), and the shared `ResolveToken` `want != Kind` gate auto-rejects cross-door presentation. `tokens.subject_kind` CHECK extends `IN (0, 1)` → `IN (0, 1, 2)` in `0001_init.sql` before data exists. Named `SubjectService`, not `SubjectStack`/`SubjectGateway`. Amends the retired v0.6 record's two-kind seal | Active (Matt, 2026-09-04) | [service subject principal](server/compass-service-subject-principal.md) | +| DL-327 | The token-subject model admits a THIRD principal class, `SubjectService SubjectKind = 2` — ONE class for every first-party supervised compute tier authenticating back to the Server (LLM gateway now, MCP gateway later), never a kind per tier: tiers present distinct Subject IDs (`llm-gateway`, `mcp-gateway`) isolated by per-surface authz (the account-door `owner_user_id` precedent), and the shared `ResolveToken` `want != Kind` gate auto-rejects cross-door presentation. `tokens.subject_kind` CHECK extends `IN (0, 1)` → `IN (0, 1, 2)` in `0001_init.sql` in place (safe only before any non-disposable database has applied v1: `migrate()` is version-keyed, so an already-migrated DB keeps the old CHECK — pre-GA disposable-env posture, else a new ALTER migration). Named `SubjectService`, not `SubjectStack`/`SubjectGateway`. Amends the retired v0.6 record's two-kind seal | Active (Matt, 2026-09-04) | [service subject principal](server/compass-service-subject-principal.md) | ## Transport diff --git a/docs/designs/server/compass-service-subject-principal.md b/docs/designs/server/compass-service-subject-principal.md index f341e50f..1c05697d 100644 --- a/docs/designs/server/compass-service-subject-principal.md +++ b/docs/designs/server/compass-service-subject-principal.md @@ -98,9 +98,20 @@ and are painful to rename once token rows exist. - The token-existence-oracle posture holds: every door maps `ErrTokenNotFound` / `ErrTokenRevoked` / `ErrWrongKind` to the same bare `CodeUnauthenticated` (`token.go:92-97`). -- `0001_init.sql` is the initial migration — editable only while it remains - unshipped-to-data; the CHECK edit lands there, not in a new migration - (the reason this record precedes PR2). +- `0001_init.sql` is edited in place (the CHECK widen lands there, not in a + new migration — the reason this record precedes PR2). This is safe ONLY + while every environment is disposable: `migrate()` skips any version already + recorded in `schema_migrations` (`store.go:157-164`), and the refuse-to-serve + guard compares max-embedded vs recorded version (both `1`, `store.go:169-176`), + so a database that has ALREADY applied v1 keeps the old + `CHECK (subject_kind IN (0, 1))` and boots clean past the guard — token-table + emptiness is irrelevant, the constraint is materialized in that database's + catalog at v1-apply time regardless of row count, and a later SubjectService + insert then fails the stale CHECK at runtime. So every existing dev/CI/ + self-host database must be wiped and re-migrated, or the change moves to a + new `NNNN_*.sql ALTER`. This is the established pre-GA in-place-edit posture + (RIG-3106 #830 RLS enforcement, RIG-2861 T1 #715 tenant schema; the pgtest + harness resets per run). ## Plan @@ -178,10 +189,23 @@ subj, err := auth.ResolveToken(ctx, st, presented, store.SubjectService) // per-surface authz then checks subj.ID against the surface's service allowlist. ``` +Doc refresh (T4 owns it, since the third door wrap is where the two-door prose +goes stale): the enum grows to three kinds but there are still only two DOORS +after PR2, so the existing two-door enumerations stay literally true until T4 +adds the third — at which point refresh them in the same slice: +`token.go:98-99` (the shared-resolver door enumeration — "Both the account door +… and the Runner door … share this one resolver"), `token.go:79-81` +(`ErrWrongKind`'s account-vs-Runner examples), `interceptor.go:140-141` (the +cross-door failure enumeration), and the `runnerhub/auth.go:4-14` package doc +(the two-door cross-door-rejection framing). + ### T5 — cross-door pgtest -Extend the store/auth pgtest coverage (harness per -`go/internal/store/harness_test.go`) with the three-kind cross-door matrix: a +Extend the existing cross-door cases in `go/internal/auth/token_test.go:134-154` +(auth harness `go/internal/auth/harness_pgtest_test.go`, build tag +`pgtest && unix`) with the three-kind cross-door matrix, and the store +round-trip in `go/internal/store/tokens_test.go` (harness +`go/internal/store/harness_test.go`, build tag `pgtest`). The matrix: a `SubjectService` token resolves at `want=SubjectService`; presented at `want=SubjectAccount` and `want=SubjectRunner` it fails `ErrWrongKind`; an account token and a Runner token presented at `want=SubjectService` each fail @@ -202,7 +226,7 @@ Interfaces: - [ ] T1: `SubjectService SubjectKind = 2` + seal comment (`types.go:90-99`) + `Subject.ID` doc - [ ] T2: `0001_init.sql:377` CHECK `IN (0, 1)` → `IN (0, 1, 2)` + header comment `:372-373` - [ ] T3: `tokens.go:20` nolint `0/1` → `0/1/2` -- [ ] T4 (RIG-2863 T2, NOT PR2): service-door mount via `ResolveToken(..., store.SubjectService)` + per-surface Subject-ID authz, mounted on the stack-token RPC surface that slice delivers +- [ ] T4 (lands in RIG-2863 = RIG-1715 T2, NOT PR2): service-door mount via `ResolveToken(..., store.SubjectService)` + per-surface Subject-ID authz, mounted on the stack-token RPC surface that slice delivers - [ ] T5: cross-door pgtest matrix (3×3 kind-gate + CHECK-admits-2 round-trip) ## Open Questions @@ -217,7 +241,8 @@ Interfaces: Recommendation: keep the inline IN-list. Only escalate if Matt expects kind churn beyond design-gated additions. - **OQ-3 (deferral naming the owning slice — NON-load-bearing for PR2, the enum - half): SubjectService token ISSUANCE lives in the RIG-2863 T4 slice.** Both + half): SubjectService token ISSUANCE lands in the RIG-2863 slice (RIG-1715 T2), + alongside this record's T4.** Both existing kinds have a real mint path — `IssueAccountToken` (`token.go:51-55`) for `SubjectAccount`, `runnerhub.MintRunnerToken` (`mint.go:103`) + the `compass-mint-runner-token` CLI for `SubjectRunner`. No corpus task mints a @@ -233,5 +258,7 @@ Interfaces: security-critical door. - Non-load-bearing deferral: the canonical Subject-ID registry for service principals (e.g. `llm-gateway`, `mcp-gateway` as named constants vs - config-supplied strings) is a PR2 implementation detail of the T4 surface's - allowlist; it does not affect the schema or the enum. + config-supplied strings) is an implementation detail of the T4 surface's + allowlist, and lands WITH T4 in the RIG-2863 slice (RIG-1715 T2) — not in + PR2. It does not affect the schema or the enum, so PR2 (T1/T2/T3/T5) does + not touch it. From 23f747347421b38ee91d27c7aff36a2eec4a87fe Mon Sep 17 00:00:00 2001 From: mintaka Date: Fri, 4 Sep 2026 17:05:08 -0400 Subject: [PATCH 4/8] docs(server): fix round-3 review findings on the SubjectService design (RIG-2863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-3 adversarial review of PR #868 returned 0H/2M/3L; the two mediums are internal-consistency defects the round-2 M3 restatement left behind (not regressions — all five round-2 fixes verified good against the tree). Both verified against main `8fef2846` before folding. None is a design fork. Additive commit atop the bookmark. - **M1 (contradicting data-keyed framing):** the round-2 M3 fix corrected the in-place-edit safe-condition in the Global Constraints bullet and DL-327 to version-keyed, but left the original DATA-keyed framing ("painful … once token rows exist") standing at the two sites that carry the record's urgency rationale (:86-88 Problem/Intent, :120-122 inside the PR2 executor contract), where it directly contradicted the corrected bullet — and UNDERSTATED the urgency (any applied-v1 DB is already stale, not just one holding rows). Restated both in version-keyed terms ("painful … once any non-disposable database has applied v1, per Global Constraints"). - **M2 (incomplete T4 doc-refresh list):** the round-2 doc-refresh clause presented four sites as the complete set ("refresh THEM in the same slice") but two more two-door enumerations are equally invalidated by the third door and were omitted: `types.go:89` (the cross-door EXAMPLE clause — a separate sentence from the seal sentence T1 rewrites at :90-91, so no task owned it) and `network_door.go:229-230, :299-301` (the door-mount cross-rejection comments at the site the service door mounts on). Added both and reframed the list as "at least these six sites" so the completeness claim is honest. - **L3 (undefined "PR2"):** defined PR2 once at first use (:86) — "the enum-half implementation PR, T1/T2/T3/T5, throughout this record" — the central scope discriminator was used 10× and never defined. - **L4 (missing negative CHECK test):** added a T5 store round-trip asserting `PutTokenHash` with `Subject{Kind: SubjectKind(3)}` FAILS a constraint violation — the one case that catches a fat-fingered `IN (0,1,2,3)` or a dropped constraint (the admits-2 round-trip passes identically either way). - **L5 (llm-gateway path drift):** left as-is — the cited `server/` path is correct; the drift lives in the target record + tracker, not this diff (per reviewer, not this PR's defect to fix). markdownlint clean; design-ledger-gate OK (292 rows, 123 headers); no bare `RIG-2863 T` shorthand remains; DL-327 already version-keyed (no ledger edit). Record 15703 B (<50 KB, DL-327 needs no anchor). Ledger-impact: DL-327 (text unchanged) Refs RIG-2863 Co-authored-by: Matt Wilkinson --- .../compass-service-subject-principal.md | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/docs/designs/server/compass-service-subject-principal.md b/docs/designs/server/compass-service-subject-principal.md index 1c05697d..189e5a2d 100644 --- a/docs/designs/server/compass-service-subject-principal.md +++ b/docs/designs/server/compass-service-subject-principal.md @@ -83,9 +83,11 @@ tiers — the LLM gateway now, an MCP gateway later — NOT a kind per tier. - **Named `SubjectService`** — NOT `SubjectStack` (reads as the deployment stack), NOT `SubjectGateway` (narrower than the class). -Shipping this addendum BEFORE the implementation (PR2) is deliberate: the enum +Shipping this addendum BEFORE the implementation (PR2 — the enum-half +implementation PR, T1/T2/T3/T5, throughout this record) is deliberate: the enum NUMBER and the `tokens.subject_kind` CHECK constraint land in `0001_init.sql` -and are painful to rename once token rows exist. +and are painful to change once any non-disposable database has APPLIED v1 (the +condition is version-keyed, not row-count-keyed — see Global Constraints). ## Global Constraints @@ -118,7 +120,8 @@ and are painful to rename once token rows exist. PR2 executor contract — TWO scopes, each grounded on the current line. **The enum half (T1/T2/T3/T5) lands NOW as PR2** — every file+line it names exists today, and it is the urgent half (the enum number + `tokens.subject_kind` -CHECK are painful to change once token rows exist, per Problem / Intent). **The +CHECK are painful to change once any non-disposable database has applied v1, +per Global Constraints). **The door half (T4) lands WITH the service surface**, which does not exist in the tree yet: it is delivered by RIG-2863 (RIG-1715 T2 — the AuthStorage-over-compass adapter + the LLM gateway's stack-token RPC surface, currently Backlog), and @@ -192,12 +195,20 @@ subj, err := auth.ResolveToken(ctx, st, presented, store.SubjectService) Doc refresh (T4 owns it, since the third door wrap is where the two-door prose goes stale): the enum grows to three kinds but there are still only two DOORS after PR2, so the existing two-door enumerations stay literally true until T4 -adds the third — at which point refresh them in the same slice: -`token.go:98-99` (the shared-resolver door enumeration — "Both the account door -… and the Runner door … share this one resolver"), `token.go:79-81` -(`ErrWrongKind`'s account-vs-Runner examples), `interceptor.go:140-141` (the -cross-door failure enumeration), and the `runnerhub/auth.go:4-14` package doc -(the two-door cross-door-rejection framing). +adds the third — at which point refresh them in the same slice (at least these +six sites): `token.go:98-99` (the shared-resolver door enumeration — "Both the +account door … and the Runner door … share this one resolver"), +`token.go:79-81` (`ErrWrongKind`'s account-vs-Runner examples), +`interceptor.go:140-141` (the cross-door failure enumeration), the +`runnerhub/auth.go:4-14` package doc (the two-door cross-door-rejection +framing), `types.go:89` (the cross-door EXAMPLE clause — "a Runner token on +CompassService/CommsService, an account token on RunnerService" — a separate +sentence in the same comment as, but distinct from, the "Sealed to exactly +these two" seal sentence T1 rewrites at `:90-91`, so the two tasks do not +collide), and `network_door.go:229-230, :299-301` (the door-mount +cross-rejection comments at the sites the service door mounts on — "an account +token is Unauthenticated there, and a Runner token is Unauthenticated on the +CompassService/CommsService doors above (OQ7 cross-door rejection)"). ### T5 — cross-door pgtest @@ -209,9 +220,12 @@ round-trip in `go/internal/store/tokens_test.go` (harness `SubjectService` token resolves at `want=SubjectService`; presented at `want=SubjectAccount` and `want=SubjectRunner` it fails `ErrWrongKind`; an account token and a Runner token presented at `want=SubjectService` each fail -`ErrWrongKind`. Also a store-level round-trip: `PutTokenHash` with +`ErrWrongKind`. Also two store-level round-trips: `PutTokenHash` with `Subject{Kind: SubjectService, ID: "llm-gateway"}` persists (proving the T2 -CHECK admits 2) and `ResolveTokenHash` returns the kind intact. +CHECK admits 2) and `ResolveTokenHash` returns the kind intact; and +`PutTokenHash` with `Subject{Kind: SubjectKind(3), ID: "nope"}` FAILS with a +constraint violation (proving the widened CHECK is still a closed set of +exactly `{0, 1, 2}` — not dropped or over-widened to admit 3). Interfaces: From 9139edb4156bb4b944b8eee081ad2b34b60c6935 Mon Sep 17 00:00:00 2001 From: mintaka Date: Fri, 4 Sep 2026 18:00:46 -0400 Subject: [PATCH 5/8] docs(server): fix round-4 review findings on the SubjectService design (RIG-2863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-4 adversarial review returned 1H/3M/2L on the SubjectService design addendum. All round-3 fixes verified good; every new finding grounded against the tree at eb5ef7a1 before folding. Folded as a new commit atop the bookmark (additive review-fix convention). - H1 (service-door tenant posture — undesigned security fork handed to T4): the record fixed the auth PRINCIPAL but never its TENANT posture. A service door is a request path; under RLS a request-path statement arms compass_app + a tenant GUC (tenant_tx.go:135-143) and resolveTenant falls back to the bootstrap tenant (tenant.go:54-59), so a service door that sets no tenant runs bootstrap-scoped and sees one tenant's rows — silently breaking the record's first consumer, the LLM gateway stack-token surface, which must serve EVERY tenant's creds (llm-gateway design.md:333-337). The only cross-tenant escape, WithSystemRole, is explicitly fenced from request paths (tenant_tx.go:41-47). Added OQ-4 surfacing the fork with the three shapes T4 must choose among — (a) per-request tenant + store.WithTenant, (b) a Matt-ruled BYPASSRLS widening this record does NOT grant, (c) formal deferral to the RIG-2863 slice as a blocking T4 prerequisite — mirroring OQ-3's issuance deferral. Not decided here: it's a T4-time ruling for Matt at the design gate. Also corrected the account-door precedent to note account isolation is the tenant GUC layered with owner_user_id, not owner_user_id alone. PR2 (the enum half) does not depend on it. - M1 (network_door.go citation unresolvable): the round-3 fix fused a composite quote present at neither cited site and gave no resolvable path (bare network_door.go, which lives at go/server/, not go/internal/). Split into the two real sites with each site's actual text and the full go/server/ path. - M2 (types.go:89 under-range → false non-collision claim): the example clause spans :89-90 and SHARES line 90 with the seal sentence T1 rewrites, so the "do not collide" claim was false. Corrected the citation to :89-90 and made T1's edit sentence-scoped (rewrite the seal sentence in place, preserving the clause tail on line 90) rather than a destructive :90-91 line-range replace. - M3 (incomplete doc-refresh list): the "at least six sites" hedge left four+ more two-door enumerations unnamed, including one in the struct T1 edits and a proto comment stating a literal door COUNT. Replaced the fragile line-pinned list with a discovery RULE (a regex over go/ and proto/ comments that enumerate/count the door set) and kept the enumeration as evidence, now including types.go:86-88, types.go:101-103, mint.go:5-7, handler.go:68-69 / :260-261, and proto/compass/v1/runner.proto:53-56 / :183-184. - L1: specified the T5 negative-case assertion (assert err != nil; a 23514 falls through to a bare wrap, so do NOT add a store sentinel/SQLSTATE const — an unscoped API change). - L2: noted in Global Constraints that RunnerTokenRegistered (mint.go:80-93) is a kind-agnostic store-level existence check outside the one-resolver invariant, whose false-"registered" surface the third class widens by one. markdownlint clean; design-ledger-gate OK (292 rows, 123 headers valid; record 21740 B < 50 KB, DL-327 needs no anchor). Ledger-impact: none (DL-327 unchanged). Spec-impact: none (design record only). Refs RIG-2863 Co-authored-by: Matt Wilkinson --- .../compass-service-subject-principal.md | 124 ++++++++++++++---- 1 file changed, 102 insertions(+), 22 deletions(-) diff --git a/docs/designs/server/compass-service-subject-principal.md b/docs/designs/server/compass-service-subject-principal.md index 189e5a2d..c250c797 100644 --- a/docs/designs/server/compass-service-subject-principal.md +++ b/docs/designs/server/compass-service-subject-principal.md @@ -63,8 +63,11 @@ tiers — the LLM gateway now, an MCP gateway later — NOT a kind per tier. e.g. the owner resolution over `agent_accounts.owner_user_id` (`go/internal/comms/comms.go:114-115`: "an agent caller resolves to its owner_user_id, a user caller to itself") discriminating what a given account - subject may do. A service door authorizes the resolved `Subject.ID` against - the surface's allowlist the same way. + subject may do — layered ON TOP of the tenant GUC the request path arms + (`tenant_tx.go:135-143`), which is where the account door's isolation + substantially comes from, not `owner_user_id` alone. A service door + authorizes the resolved `Subject.ID` against the surface's allowlist the same + way; its TENANT posture is not settled by this record — see OQ-4. - **The existing cross-door kind-gate isolates the new class for free.** `ResolveToken` (`go/internal/auth/token.go:102-116`) is the ONE shared resolver: `if subj.Kind != want { return store.Subject{}, ErrWrongKind }` @@ -94,9 +97,20 @@ condition is version-keyed, not row-count-keyed — see Global Constraints). - The two existing kinds keep their numbers: `SubjectAccount = 0`, `SubjectRunner = 1` (`types.go:96-98`). `SubjectService = 2` is additive; numbers are append-only, never reused. -- The one-resolver invariant holds: every door authenticates through - `auth.ResolveToken` (`token.go:102`); no door grows its own resolve or - kind-check. +- The one-resolver invariant holds over DOORS: every door authenticates + through `auth.ResolveToken` (`token.go:102`); no door grows its own resolve + or kind-check. One non-door path is deliberately outside it: + `runnerhub.RunnerTokenRegistered` (`mint.go:80-93`) is a KIND-AGNOSTIC + store-level existence check — it resolves a hash and returns true for ANY + resolving subject, never comparing `Kind` — used by the runner-credential + provisioning heal paths, not a door. The third class widens its + false-"registered" surface by one: a `SubjectService` token hash that + appeared in a runner's token file would report registered, so the heal path + would keep it instead of rotating and the runner would then fail the kind + gate at `runnerhub/auth.go:79`. Not an escalation (the door still fails + closed) and reaching it needs an operator pasting a service token into runner + state; a non-load-bearing follow-up for the T4/issuance slice could compare + the resolved `Kind` before treating a token as registered. - The token-existence-oracle posture holds: every door maps `ErrTokenNotFound` / `ErrTokenRevoked` / `ErrWrongKind` to the same bare `CodeUnauthenticated` (`token.go:92-97`). @@ -131,9 +145,14 @@ ISSUANCE (the mint path, no corpus task owns it yet) is OQ-3 below. ### T1 — `SubjectService` const + seal-comment update `go/internal/store/types.go`: add the third const to the block at -`types.go:94-99` and update the seal sentence at `types.go:90-91` ("Sealed to -exactly these two (design.md: 1175-1183).") to name three kinds and cite THIS -record. Also extend the `Subject.ID` doc (`types.go:106-107`: "ID is the +`types.go:94-99` and update the seal SENTENCE ("Sealed to exactly these two +(design.md: 1175-1183).", spanning `types.go:90-91`) to name three kinds and +cite THIS record. Edit it SENTENCE-scoped, not by wiping the `:90-91` line +range: line 90 also carries the TAIL of the preceding cross-door example clause +("account token on RunnerService).", the clause T4 later refreshes), so a +wholesale line-range replace would truncate that clause. Rewrite only the seal +sentence in place. +Also extend the `Subject.ID` doc (`types.go:106-107`: "ID is the AccountID (SubjectAccount) or the Runner id (SubjectRunner)…") to name the service id space (a stable service name, e.g. `llm-gateway`). @@ -195,20 +214,43 @@ subj, err := auth.ResolveToken(ctx, st, presented, store.SubjectService) Doc refresh (T4 owns it, since the third door wrap is where the two-door prose goes stale): the enum grows to three kinds but there are still only two DOORS after PR2, so the existing two-door enumerations stay literally true until T4 -adds the third — at which point refresh them in the same slice (at least these -six sites): `token.go:98-99` (the shared-resolver door enumeration — "Both the -account door … and the Runner door … share this one resolver"), +adds the third — at which point refresh them in the same slice. The CONTRACT is +a discovery rule (the line-pinned list below is evidence, not the boundary): +refresh every comment under `go/` and `proto/` that ENUMERATES or COUNTS the +door/kind set — matching roughly `/cross-door|cross-kind|account (token| +subject)|Runner (token|subject)|these two|exactly two|two mandatory/` — since +any two-door enumeration or literal door COUNT goes stale when the third door +mounts. The sites known at authoring time (verified at `eb5ef7a1`): +`go/internal/auth/token.go:98-99` (the shared-resolver door enumeration — "Both +the account door … and the Runner door … share this one resolver"), `token.go:79-81` (`ErrWrongKind`'s account-vs-Runner examples), -`interceptor.go:140-141` (the cross-door failure enumeration), the -`runnerhub/auth.go:4-14` package doc (the two-door cross-door-rejection -framing), `types.go:89` (the cross-door EXAMPLE clause — "a Runner token on -CompassService/CommsService, an account token on RunnerService" — a separate -sentence in the same comment as, but distinct from, the "Sealed to exactly -these two" seal sentence T1 rewrites at `:90-91`, so the two tasks do not -collide), and `network_door.go:229-230, :299-301` (the door-mount -cross-rejection comments at the sites the service door mounts on — "an account -token is Unauthenticated there, and a Runner token is Unauthenticated on the -CompassService/CommsService doors above (OQ7 cross-door rejection)"). +`go/internal/auth/interceptor.go:140-141` (the cross-door failure enumeration), +the `go/internal/runnerhub/auth.go:4-14` package doc (the two-door +cross-door-rejection framing), `go/internal/store/types.go:89-90` (the +cross-door EXAMPLE clause — "reject a cross-kind token (a Runner token on +CompassService/CommsService, an account token on RunnerService)" — which SHARES +line 90 with the "Sealed to exactly these two" seal sentence T1 rewrites, so +T1 must edit that seal sentence IN PLACE, sentence-scoped not line-range-scoped, +preserving the leading "account token on RunnerService)." on line 90), +`types.go:86-88` (the `SubjectKind` doc opener — "a Runner subject and an +account subject share the token store but never collide"), `types.go:101-103` +(the `Subject` doc's two-kind enumeration — "the id of the account or Runner it +authenticates"; T1 edits only the `:106-107` ID doc and leaves this stale), +`go/internal/runnerhub/mint.go:5-7` ("a Runner subject and an account subject +share one store but can never collide"), +`go/internal/runnerhub/handler.go:68-69` ("an account token never reaches here +… the RunnerService cross-door rejection") and `:260-261` ("an account token is +Unauthenticated here, the OQ7 cross-door rule"), +`go/server/network_door.go:229-231` ("an account token is Unauthenticated +there, and a Runner token is Unauthenticated on the account/comms doors: the +OQ7 cross-door rule") and `:299-301` ("an account token is Unauthenticated here +and a Runner token is Unauthenticated on the CompassService/CommsService doors +above (OQ7 cross-door rejection)"), and — the sharpest, because it states a +literal COUNT that becomes factually wrong when a third door mounts — +`proto/compass/v1/runner.proto:53-56` ("the RunnerService side of the TWO +mandatory cross-door rejection tests") and `:183-184` ("account-subject tokens +rejected"). `network_door.go` lives at `go/server/`, NOT `go/internal/` like +the rest — the one cited file outside `go/internal/`. ### T5 — cross-door pgtest @@ -225,7 +267,14 @@ account token and a Runner token presented at `want=SubjectService` each fail CHECK admits 2) and `ResolveTokenHash` returns the kind intact; and `PutTokenHash` with `Subject{Kind: SubjectKind(3), ID: "nope"}` FAILS with a constraint violation (proving the widened CHECK is still a closed set of -exactly `{0, 1, 2}` — not dropped or over-widened to admit 3). +exactly `{0, 1, 2}` — not dropped or over-widened to admit 3). Assert only +`err != nil` on that call — the ID is non-empty and the hash fresh, so the +widened CHECK (SQLSTATE 23514) is the sole possible failure source. Do NOT add +a new store sentinel or SQLSTATE constant for it: the store maps only 23505 → +`ErrConflict` and 23503 → `ErrInvalidArgument` (`errors.go:9-12`), and a 23514 +falls through to the bare wrap at `tokens.go:26` — introducing a typed sentinel +is a store API change this record does not scope (T2/T3 touch only the CHECK +and the nolint text). Interfaces: @@ -270,6 +319,37 @@ Interfaces: enum half, T1/T2/T3/T5) does NOT depend on it; it is recorded here so the T4 slice owns it explicitly rather than an executor improvising a mint path on a security-critical door. +- **OQ-4 (service-door tenant posture — load-bearing for T4, NON-load-bearing + for PR2). The record fixes the auth PRINCIPAL but not its TENANT posture; T4 + must not improvise one.** A service door is a request path, and under RLS a + request-path store statement arms `SET LOCAL ROLE compass_app` plus a tenant + GUC (`tenant_tx.go:135-143`), with `resolveTenant` falling back to the + BOOTSTRAP tenant when the context carries none (`tenant.go:54-59`). So a + service-door RPC that resolves a `SubjectService` token but sets no tenant + runs bootstrap-scoped and sees only that tenant's rows — which silently + breaks this record's own first consumer: the LLM gateway's stack-token + surface must serve EVERY tenant's provider credentials, isolating them by + per-tenant pool scoping enforced server-side, not by the process (a + compromised gateway holding one stack token can read every tenant's creds — + `docs/designs/server/compass-server-llm-gateway/design.md:333-337`; pools + resolve from `owner_user_id`, :377-378). The only cross-tenant escape is + `WithSystemRole` (BYPASSRLS), and it is explicitly fenced from request paths: + "applied ONLY at the four named background-loop entrypoints … a request-path + call NEVER sets it" (`tenant_tx.go:41-47`; role scope :19-22). A service door + is a request path, so this record does NOT authorize it to take that escape. + The three shapes T4 must choose among (the executor may NOT improvise): + (a) the door resolves a tenant per request from a request-carried selector, + validated against the Subject.ID allowlist, and calls `store.WithTenant` — + request path stays tenant-scoped and fail-closed; (b) the surface is + deliberately cross-tenant, which requires a NEW, explicitly Matt-ruled + widening of the `WithSystemRole` background-loop exemption + (`tenant_tx.go:41-47`) to a request-path door — a security-boundary change + this record does NOT grant; or (c) tenancy is formally deferred to the + RIG-2863 (RIG-1715 T2) slice as a BLOCKING prerequisite of T4, so the surface + cannot ship without a ruling. Resolution: shape chosen with the T4 surface in + the RIG-2863 slice; PR2 (the enum half, T1/T2/T3/T5) does NOT depend on it — + the enum and CHECK carry no tenant posture. Recorded here (not left implicit) + so the T4 executor is handed a named fork, not an undesigned security choice. - Non-load-bearing deferral: the canonical Subject-ID registry for service principals (e.g. `llm-gateway`, `mcp-gateway` as named constants vs config-supplied strings) is an implementation detail of the T4 surface's From 65ffcb8b39a8914e43518ebe57f30c847a3db779 Mon Sep 17 00:00:00 2001 From: mintaka Date: Fri, 4 Sep 2026 18:50:22 -0400 Subject: [PATCH 6/8] docs(server): fix round-5 review findings on the SubjectService design (RIG-2863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-5 review of the SubjectService design addendum returned 0 high / 2 medium / 3 low. All six round-4 fixes verified holding against the tree at `eb5ef7a1`; the two mediums are defects the round-4 M3 rewrite (the T4 doc-refresh discovery rule) introduced in itself. All driver-foldable, no design fork. - M1 (discovery-rule regex missed its own first evidence site): the rule's regex matched only `account (token|subject)` / `Runner (token|subject)`, but its first cited site `token.go:98-99` reads "account **door**" / "Runner **door**" (and `:71`/`:94` "the other **door**"). A T4 executor running the rule — which the record elevates to THE contract over the line-pinned list — would miss the single most load-bearing door enumeration in the codebase. Added a `door` alternative and `(the )?other door`. - M2 (rule missed the door-COUNT axis entirely): scoped to the subject-KIND split, the rule slid past `doc.go:7` ("Two doors reach the same compass.v1 service"), `interceptor.go:86`/`:129`, and `service.go:539`/`:604` ("one on both doors") — all door-count enumerations that go stale when a third door mounts. Widened the regex with `both doors|two doors`, added `doc.go:7-19` (package-level count), `interceptor.go:86`/`:129`, `service.go:539`/`:604` to the evidence list, and named the two enumeration axes (kind vs socket-vs-network door count) explicitly so an executor does not assume kind-keyed matching is exhaustive. - L1 (types.go:101-103 mis-scheduled to T4): the `Subject` struct doc's two-kind enumeration is a KIND-axis enumeration that goes stale the moment T1 lands the third kind — leaving a "the account or Runner" struct doc directly above an `ID` field doc T1 has just extended to a third id space. Moved it from the T4 door-refresh list into T1 (same comment block T1 already edits). - L2 (runner.proto:183-184 mis-attributed): the literal-COUNT rationale belongs to `:53-56` alone; `:183-184` ("account-subject tokens rejected") is an ordinary stale two-kind enumeration, not a count. Split the justification. - L3 (T2 CI consequences unnamed): a frozen executor contract naming the exact `0001_init.sql` line to edit should name the gates that edit trips. Added to T2: after the CHECK widen, run `moon run compass-go:sqlc-gen` and confirm no drift (the `sqlc-drift` gate fails closed on any stale `internal/store/db` byte), and expect `sql-migration-gate` (squawk + sqruff over `go/internal/store/migrations/*.sql`) to re-run over the edited file. Additive review-fix commit atop the round-4 fold (record-only; DECISIONS.md untouched). markdownlint 0 errors; design-ledger-gate OK (292 rows, 123 headers; record 23313 B < 50 KB so DL-327 carries no anchor). All findings verified against the tree before folding. Refs RIG-2863 Co-authored-by: Matt Wilkinson --- .../compass-service-subject-principal.md | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/docs/designs/server/compass-service-subject-principal.md b/docs/designs/server/compass-service-subject-principal.md index c250c797..ef1920c2 100644 --- a/docs/designs/server/compass-service-subject-principal.md +++ b/docs/designs/server/compass-service-subject-principal.md @@ -155,6 +155,12 @@ sentence in place. Also extend the `Subject.ID` doc (`types.go:106-107`: "ID is the AccountID (SubjectAccount) or the Runner id (SubjectRunner)…") to name the service id space (a stable service name, e.g. `llm-gateway`). +Also refresh the `Subject` struct doc at `types.go:101-103` (the two-kind +enumeration — "the id of the account or Runner it authenticates") in the same +comment block: it is a KIND-axis enumeration that goes stale the moment the +third kind lands (three kinds exist after T1), so it belongs in T1, not the T4 +door-refresh list — leaving it would put a "the account or Runner" struct doc +directly above an `ID` field doc T1 has just extended to a third id space. Interfaces: @@ -181,6 +187,12 @@ Interfaces: -- produces: CHECK (subject_kind IN (0, 1, 2)) ``` +After the edit, run `moon run compass-go:sqlc-gen` and confirm no drift (the +checked-in `internal/store/db` tree is the source of truth; the `sqlc-drift` +gate fails closed on any stale byte — a CHECK-only widen regenerates +identically, but confirm), and expect `sql-migration-gate` (squawk + sqruff +over `go/internal/store/migrations/*.sql`) to re-run over the edited file. + ### T3 — nolint text tracks the enum `go/internal/store/tokens.go:20`: the gosec waiver's justification names the @@ -218,12 +230,24 @@ adds the third — at which point refresh them in the same slice. The CONTRACT i a discovery rule (the line-pinned list below is evidence, not the boundary): refresh every comment under `go/` and `proto/` that ENUMERATES or COUNTS the door/kind set — matching roughly `/cross-door|cross-kind|account (token| -subject)|Runner (token|subject)|these two|exactly two|two mandatory/` — since -any two-door enumeration or literal door COUNT goes stale when the third door -mounts. The sites known at authoring time (verified at `eb5ef7a1`): +subject|door)|Runner (token|subject|door)|both doors|two doors|(the )?other +door|these two|exactly two|two mandatory/` — since any two-door enumeration or +literal door COUNT goes stale when the third door mounts. Note TWO enumeration +axes exist and the regex covers both: the subject-KIND split (account/Runner) +AND the door-COUNT split (socket vs network, "two doors") — a third mounted +door falsifies a literal door count as surely as a third kind falsifies a +two-kind enumeration, so an executor must not assume kind-keyed matching is +exhaustive. The sites known at authoring time (verified at `eb5ef7a1`): `go/internal/auth/token.go:98-99` (the shared-resolver door enumeration — "Both the account door … and the Runner door … share this one resolver"), `token.go:79-81` (`ErrWrongKind`'s account-vs-Runner examples), +`go/internal/auth/doc.go:7-19` (the package-level door COUNT — "Two doors reach +the same compass.v1 service, and each authenticates differently" — the +socket-vs-network axis; a third mounted door makes the literal count wrong), +`go/internal/auth/interceptor.go:86` ("the caller uniformly via CallerFrom on +both doors") and `:129` ("both doors reject identically"), +`go/server/service.go:539` and `:604` ("an interceptor must attach one on both +doors"), `go/internal/auth/interceptor.go:140-141` (the cross-door failure enumeration), the `go/internal/runnerhub/auth.go:4-14` package doc (the two-door cross-door-rejection framing), `go/internal/store/types.go:89-90` (the @@ -233,9 +257,7 @@ line 90 with the "Sealed to exactly these two" seal sentence T1 rewrites, so T1 must edit that seal sentence IN PLACE, sentence-scoped not line-range-scoped, preserving the leading "account token on RunnerService)." on line 90), `types.go:86-88` (the `SubjectKind` doc opener — "a Runner subject and an -account subject share the token store but never collide"), `types.go:101-103` -(the `Subject` doc's two-kind enumeration — "the id of the account or Runner it -authenticates"; T1 edits only the `:106-107` ID doc and leaves this stale), +account subject share the token store but never collide"), `go/internal/runnerhub/mint.go:5-7` ("a Runner subject and an account subject share one store but can never collide"), `go/internal/runnerhub/handler.go:68-69` ("an account token never reaches here @@ -248,8 +270,9 @@ and a Runner token is Unauthenticated on the CompassService/CommsService doors above (OQ7 cross-door rejection)"), and — the sharpest, because it states a literal COUNT that becomes factually wrong when a third door mounts — `proto/compass/v1/runner.proto:53-56` ("the RunnerService side of the TWO -mandatory cross-door rejection tests") and `:183-184` ("account-subject tokens -rejected"). `network_door.go` lives at `go/server/`, NOT `go/internal/` like +mandatory cross-door rejection tests"); its sibling `:183-184` +("account-subject tokens rejected") is an ordinary stale two-kind enumeration, +not a count. `network_door.go` lives at `go/server/`, NOT `go/internal/` like the rest — the one cited file outside `go/internal/`. ### T5 — cross-door pgtest From dee01024ce7581df37f8e7f414c8a98b3f8966b5 Mon Sep 17 00:00:00 2001 From: mintaka Date: Fri, 4 Sep 2026 19:32:32 -0400 Subject: [PATCH 7/8] docs(server): fix round-6 review findings on the SubjectService design (RIG-2863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-6 review returned 0 high / 2 medium / 3 low on PR #868; all five grounded against the tree at `eb5ef7a1` and folded additively. Both mediums were defects the round-5 T4 discovery-rule rewrite introduced in itself. ### Mediums - **M2 — the "two doors after PR2" premise is FALSE at the tree.** The record claimed "there are still only two DOORS after PR2, so the two-door enumerations stay literally true until T4". But a third Kind-gated `compass.v1` door already exists at `eb5ef7a1`: RunnerService, mounted at `network_door.go:313` behind `runnerhub/auth.go:79`'s `SubjectRunner` bearer; `serve.go:572`/`:647`/`:662` count "three compass.v1 doors". Corrected the premise and re-scoped: this record changes the subject-KIND axis, not the door count. The `auth`-package "two doors" sentences (`doc.go:7`, `interceptor.go:86`/`:129`, `service.go:539`/`:604`) are scoped to that package's OWN network+socket `CallerFrom` pair (`doc.go:2` scopes the package to "the network door"; RunnerService authenticates in `runnerhub`, not `auth`) — narrow-and-true within scope, and OUT of scope for both PR2 and T4. Restructured the evidence list into kind-axis refresh targets vs the out-of-scope door-count sites. - **M1 — discovery regex case-sensitive against its own new evidence site.** The round-5 widened regex carried lowercase `two doors`, but `doc.go:7` opens capital-T "Two doors" at sentence start, so the count sentence the rule was widened to catch was invisible to it (matched only incidentally via `:18`'s "both doors"). Since the door-count axis is now out of scope (M2), the rule is scoped to the kind axis and stated case-INSENSITIVE. ### Lows - **L1** — a `runner.proto` comment edit is mirrored into the checked-in, non-gitignored generated tree (`runner.connect.go:98`/`:367`), drift-gated by `compass-proto:drift` (in `proto:ci`); noted regenerate-and-commit, never hand-edit. - **L2** — named `RunnerTokenRegistered`'s two callers: the operator CLI (`compass-mint-runner-token/main.go:157`) and the automated stack-boot heal (`stack/adapters/token.go:87`); clarified neither is an untrusted-input path. - **L3** — scoped the discovery rule to NON-GENERATED comments (exclude `go/gen/**`, `go/internal/gen/**`), with test prose in scope. Design record only; no code or proto change. OQ-4 (round-4 HIGH, service-door tenant posture) re-verified byte-identical and undisturbed — still surfaces-not-decides the three-shape fork. Security lens re-confirmed: the additive `SubjectService = 2` enum is inert at every door (sole resolver `auth.ResolveToken`; both doors pass explicit `want`; no `SubjectKind` switch; no proto mirror). markdownlint 0, design-ledger-gate OK (292 rows, record 25130 B < 50KB so DL-327 carries no anchor). Spec-impact: none. Refs RIG-2863 Co-authored-by: Matt Wilkinson --- .../compass-service-subject-principal.md | 88 ++++++++++++------- 1 file changed, 57 insertions(+), 31 deletions(-) diff --git a/docs/designs/server/compass-service-subject-principal.md b/docs/designs/server/compass-service-subject-principal.md index ef1920c2..7b843106 100644 --- a/docs/designs/server/compass-service-subject-principal.md +++ b/docs/designs/server/compass-service-subject-principal.md @@ -103,14 +103,20 @@ condition is version-keyed, not row-count-keyed — see Global Constraints). `runnerhub.RunnerTokenRegistered` (`mint.go:80-93`) is a KIND-AGNOSTIC store-level existence check — it resolves a hash and returns true for ANY resolving subject, never comparing `Kind` — used by the runner-credential - provisioning heal paths, not a door. The third class widens its + provisioning heal paths, not a door — its two callers are + `go/cmd/compass-mint-runner-token/main.go:157` (the operator CLI) and + `go/internal/stack/adapters/token.go:87` (the AUTOMATED stack-boot heal, + not operator-driven). The third class widens its false-"registered" surface by one: a `SubjectService` token hash that appeared in a runner's token file would report registered, so the heal path would keep it instead of rotating and the runner would then fail the kind gate at `runnerhub/auth.go:79`. Not an escalation (the door still fails - closed) and reaching it needs an operator pasting a service token into runner - state; a non-load-bearing follow-up for the T4/issuance slice could compare - the resolved `Kind` before treating a token as registered. + closed); reaching it via the CLI leg needs an operator pasting a service token + into runner state, while the `token.go:87` heal leg reaches it only from a + token already in a runner's own resolved state — so neither is an untrusted + input path. A non-load-bearing follow-up for the T4/issuance slice could + compare the resolved `Kind` before treating a token as registered, landing in + exactly those two callsites. - The token-existence-oracle posture holds: every door maps `ErrTokenNotFound` / `ErrTokenRevoked` / `ErrWrongKind` to the same bare `CodeUnauthenticated` (`token.go:92-97`). @@ -223,34 +229,39 @@ subj, err := auth.ResolveToken(ctx, st, presented, store.SubjectService) // per-surface authz then checks subj.ID against the surface's service allowlist. ``` -Doc refresh (T4 owns it, since the third door wrap is where the two-door prose -goes stale): the enum grows to three kinds but there are still only two DOORS -after PR2, so the existing two-door enumerations stay literally true until T4 -adds the third — at which point refresh them in the same slice. The CONTRACT is -a discovery rule (the line-pinned list below is evidence, not the boundary): -refresh every comment under `go/` and `proto/` that ENUMERATES or COUNTS the -door/kind set — matching roughly `/cross-door|cross-kind|account (token| -subject|door)|Runner (token|subject|door)|both doors|two doors|(the )?other -door|these two|exactly two|two mandatory/` — since any two-door enumeration or -literal door COUNT goes stale when the third door mounts. Note TWO enumeration -axes exist and the regex covers both: the subject-KIND split (account/Runner) -AND the door-COUNT split (socket vs network, "two doors") — a third mounted -door falsifies a literal door count as surely as a third kind falsifies a -two-kind enumeration, so an executor must not assume kind-keyed matching is -exhaustive. The sites known at authoring time (verified at `eb5ef7a1`): +Doc refresh (T4 owns it, since the third door wrap is where the subject-KIND +prose goes stale): this record changes the subject-KIND axis (account/Runner +grows a third kind), NOT the door count. The door count is already three at +`eb5ef7a1` — the `compass.v1` surface has three doors (`serve.go:572`, +`:647`, `:662`: shipped Unix socket, optional dev loopback, optional +authenticated network) plus the Kind-gated RunnerService mount +(`network_door.go:313` behind `runnerhub/auth.go:79`'s `SubjectRunner` +bearer) — so a "two doors" claim is NOT made stale by this record's third kind. +The two-door COUNT sentences that remain (`auth/doc.go:7`, and the +`both doors` sites at `interceptor.go:86`/`:129`, `service.go:539`/`:604`) +are scoped to the `auth` PACKAGE's OWN pair — network + socket, the two doors +whose handlers read the caller via `CallerFrom` (`auth/doc.go:2` scopes the +package to "the network door"; RunnerService authenticates in `runnerhub`, not +`auth`) — so they are narrow-and-true within that scope, independently of +SubjectService; they are OUT OF SCOPE for both PR2 and T4. The CONTRACT is a +discovery rule for the subject-KIND sites only (the line-pinned list below is +evidence, not the boundary): refresh every NON-GENERATED comment under `go/` +and `proto/` that ENUMERATES the account/Runner subject KINDS — matching +(case-INSENSITIVELY) roughly `/cross-door|cross-kind|account (token|subject| +door)|Runner (token|subject|door)|(the )?other door|two mandatory/` — since any +two-KIND subject enumeration goes stale when the third kind lands. Exclude +`go/gen/**` and `go/internal/gen/**` (regenerate those from the proto instead, +never hand-edit — see T4's proto note below); test prose IS in scope. The +door-COUNT axis is deliberately NOT swept here: it is orthogonal to the kind +axis this record adds, and the `auth`-scoped count sentences above are correct +within their package scope. The subject-KIND sites known at authoring time (verified at `eb5ef7a1` — these +are the T4 refresh targets): `go/internal/auth/token.go:98-99` (the shared-resolver door enumeration — "Both the account door … and the Runner door … share this one resolver"), `token.go:79-81` (`ErrWrongKind`'s account-vs-Runner examples), -`go/internal/auth/doc.go:7-19` (the package-level door COUNT — "Two doors reach -the same compass.v1 service, and each authenticates differently" — the -socket-vs-network axis; a third mounted door makes the literal count wrong), -`go/internal/auth/interceptor.go:86` ("the caller uniformly via CallerFrom on -both doors") and `:129` ("both doors reject identically"), -`go/server/service.go:539` and `:604` ("an interceptor must attach one on both -doors"), `go/internal/auth/interceptor.go:140-141` (the cross-door failure enumeration), -the `go/internal/runnerhub/auth.go:4-14` package doc (the two-door -cross-door-rejection framing), `go/internal/store/types.go:89-90` (the +the `go/internal/runnerhub/auth.go:4-14` package doc (the cross-door-rejection +framing), `go/internal/store/types.go:89-90` (the cross-door EXAMPLE clause — "reject a cross-kind token (a Runner token on CompassService/CommsService, an account token on RunnerService)" — which SHARES line 90 with the "Sealed to exactly these two" seal sentence T1 rewrites, so @@ -268,12 +279,27 @@ there, and a Runner token is Unauthenticated on the account/comms doors: the OQ7 cross-door rule") and `:299-301` ("an account token is Unauthenticated here and a Runner token is Unauthenticated on the CompassService/CommsService doors above (OQ7 cross-door rejection)"), and — the sharpest, because it states a -literal COUNT that becomes factually wrong when a third door mounts — +literal COUNT of cross-door rejection tests that grows when a third KIND lands — `proto/compass/v1/runner.proto:53-56` ("the RunnerService side of the TWO mandatory cross-door rejection tests"); its sibling `:183-184` ("account-subject tokens rejected") is an ordinary stale two-kind enumeration, -not a count. `network_door.go` lives at `go/server/`, NOT `go/internal/` like -the rest — the one cited file outside `go/internal/`. +not a count. Editing `runner.proto:53-56` forces a regenerate: that prose is +mirrored verbatim into the checked-in generated tree +(`go/internal/gen/compass/v1/compassv1internalconnect/runner.connect.go:98`, +`:367`), which is NOT gitignored and is drift-gated — `compass-proto:drift` +(in `proto:ci`) fails closed on any byte diff — so regenerate and commit that +tree in the same slice, never hand-edit it. `network_door.go` lives at +`go/server/`, NOT `go/internal/` like the rest — the one cited file outside +`go/internal/`. + +The door-COUNT sites are deliberately OUT of scope (this record changes the kind +axis, not the door count): `go/internal/auth/doc.go:7-19` ("Two doors reach the +same compass.v1 service"), `interceptor.go:86` ("both doors") and `:129` +("both doors reject identically"), `go/server/service.go:539`/`:604` ("attach +one on both doors"). All are scoped to the `auth` package's OWN network+socket +pair (the `CallerFrom` doors) and are narrow-and-true within that scope — a +third KIND does not falsify them, and the third door (RunnerService) already +exists at `eb5ef7a1`, so they are neither PR2 nor T4 work. ### T5 — cross-door pgtest From d7a17ff7e96434b4b4a33e6cf2f6298cf9934762 Mon Sep 17 00:00:00 2001 From: mintaka Date: Fri, 4 Sep 2026 20:34:49 -0400 Subject: [PATCH 8/8] docs(server): fix round-7 review findings on the SubjectService design (RIG-2863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-7 review returned 0H/2M/3L; both mediums were self-inflicted regressions from the round-6 fold (the recurring defect-while-fixing pattern). All five folded, each grounded against the tree at `eb5ef7a1`. - M2 (regression): the round-6 out-of-scope rationale claimed the surviving "two doors" count sentences are scoped to the `auth` package's OWN network+socket `CallerFrom` pair. False at the tree — THREE `compass.v1` doors mount the auth ambient/`CallerFrom` pair (socket serve.go:708-710, dev loopback serve.go:745-747, network network_door.go:279-283), all read via CallerFrom (interceptor.go:44), and it contradicted the record's own three-doors sentence two lines up. Replaced the false package-scoping with the sound axis argument alone (door-count is orthogonal to subject-kind), plus a neutral note that `auth/doc.go:7`'s literal count is a pre-existing doc question this record neither creates nor owns. - M1 (regression): the amended discovery regex, scoped to go/ and proto/ with .sql in scope by its own wording, structurally could not reach 0001_init.sql:372-373 ("subject_kind is 0 account / 1 runner") — a real two-kind gloss T2 already mandates editing. Extended the regex with `0 account|1 runner`, stated .sql migration comments explicitly in scope, broadened the exclusion to the checked-in sqlc tree (go/internal/store/db/**), and named 0001_init.sql:372-373 in the evidence list (cross-referencing that T2 owns the edit). - L1: bare `token.go:87` shorthand collided with the record's own convention (bare token.go = go/internal/auth/token.go, a real unrelated line); disambiguated to stack/adapters/token.go:87. - L2: `proto:ci` corrected to `compass-proto:ci` (.moon/workspace.yml:18 maps compass-proto -> proto; the CI target uses the project id). - L3: re-wrapped the round-6 range-edit's over-long prose lines to the record's ~81-char norm. Record 25130 -> 25804 B (<50KB, DL-327 correctly anchor-free). markdownlint 0, design-ledger-gate OK (292 rows, 123 headers). Amended M2 regex verified to now match 0001_init.sql:373 against the tree. Refs RIG-2863 Spec-impact: design record only; no code or ledger-row change. Co-authored-by: Matt Wilkinson --- .../compass-service-subject-principal.md | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/docs/designs/server/compass-service-subject-principal.md b/docs/designs/server/compass-service-subject-principal.md index 7b843106..96649722 100644 --- a/docs/designs/server/compass-service-subject-principal.md +++ b/docs/designs/server/compass-service-subject-principal.md @@ -112,9 +112,10 @@ condition is version-keyed, not row-count-keyed — see Global Constraints). would keep it instead of rotating and the runner would then fail the kind gate at `runnerhub/auth.go:79`. Not an escalation (the door still fails closed); reaching it via the CLI leg needs an operator pasting a service token - into runner state, while the `token.go:87` heal leg reaches it only from a - token already in a runner's own resolved state — so neither is an untrusted - input path. A non-load-bearing follow-up for the T4/issuance slice could + into runner state, while the `stack/adapters/token.go:87` heal leg reaches it + only from a token already in a runner's own resolved state — so neither is + an untrusted input path. A non-load-bearing follow-up for the T4/issuance + slice could compare the resolved `Kind` before treating a token as registered, landing in exactly those two callsites. - The token-existence-oracle posture holds: every door maps @@ -239,22 +240,27 @@ authenticated network) plus the Kind-gated RunnerService mount bearer) — so a "two doors" claim is NOT made stale by this record's third kind. The two-door COUNT sentences that remain (`auth/doc.go:7`, and the `both doors` sites at `interceptor.go:86`/`:129`, `service.go:539`/`:604`) -are scoped to the `auth` PACKAGE's OWN pair — network + socket, the two doors -whose handlers read the caller via `CallerFrom` (`auth/doc.go:2` scopes the -package to "the network door"; RunnerService authenticates in `runnerhub`, not -`auth`) — so they are narrow-and-true within that scope, independently of -SubjectService; they are OUT OF SCOPE for both PR2 and T4. The CONTRACT is a +live on the door-COUNT axis, which is ORTHOGONAL to the subject-KIND axis this +record adds — a third KIND cannot falsify or refresh a door-count claim — so +they are OUT OF SCOPE for both PR2 and T4. Whether `auth/doc.go:7`'s literal +"two doors" count is even accurate at `eb5ef7a1` (the dev-loopback door at +`serve.go:745-747` also mounts the `auth` ambient/`CallerFrom` pair, so three +`compass.v1` doors read a caller through this package, not two) is a +pre-existing doc question this record neither creates nor owns. The CONTRACT is a discovery rule for the subject-KIND sites only (the line-pinned list below is evidence, not the boundary): refresh every NON-GENERATED comment under `go/` -and `proto/` that ENUMERATES the account/Runner subject KINDS — matching -(case-INSENSITIVELY) roughly `/cross-door|cross-kind|account (token|subject| -door)|Runner (token|subject|door)|(the )?other door|two mandatory/` — since any -two-KIND subject enumeration goes stale when the third kind lands. Exclude -`go/gen/**` and `go/internal/gen/**` (regenerate those from the proto instead, -never hand-edit — see T4's proto note below); test prose IS in scope. The +and `proto/` — INCLUDING `.sql` migration comments — that ENUMERATES the +account/Runner subject KINDS, matching (case-INSENSITIVELY) roughly +`/cross-door|cross-kind|account (token|subject|door)|Runner (token|subject| +door)|(the )?other door|two mandatory|0 account|1 runner/` — since any two-KIND +subject enumeration goes stale when the third kind lands. Exclude `go/gen/**`, +`go/internal/gen/**`, and the checked-in sqlc tree `go/internal/store/db/**` +(regenerate those from their sources — the proto for the first two, the +migrations via `sqlc-gen` for the last — never hand-edit; see T4's proto note +below); test prose IS in scope. The door-COUNT axis is deliberately NOT swept here: it is orthogonal to the kind -axis this record adds, and the `auth`-scoped count sentences above are correct -within their package scope. The subject-KIND sites known at authoring time (verified at `eb5ef7a1` — these +axis this record adds, so a third KIND cannot make a door-count sentence stale. +The subject-KIND sites known at authoring time (verified at `eb5ef7a1` — these are the T4 refresh targets): `go/internal/auth/token.go:98-99` (the shared-resolver door enumeration — "Both the account door … and the Runner door … share this one resolver"), @@ -287,19 +293,23 @@ not a count. Editing `runner.proto:53-56` forces a regenerate: that prose is mirrored verbatim into the checked-in generated tree (`go/internal/gen/compass/v1/compassv1internalconnect/runner.connect.go:98`, `:367`), which is NOT gitignored and is drift-gated — `compass-proto:drift` -(in `proto:ci`) fails closed on any byte diff — so regenerate and commit that -tree in the same slice, never hand-edit it. `network_door.go` lives at +(in `compass-proto:ci`) fails closed on any byte diff — so regenerate and +commit that tree in the same slice, never hand-edit it. `network_door.go` lives at `go/server/`, NOT `go/internal/` like the rest — the one cited file outside -`go/internal/`. +`go/internal/`. Finally, `go/internal/store/migrations/0001_init.sql:372-373` +— the CHECK comment "subject_kind is 0 account / 1 runner" — is the migration's +own two-KIND enumeration; T2 already owns editing it to add "/ 2 service", so it +is named here only to give the `.sql`-in-scope clause a concrete target, NOT for +T4 to re-edit. The door-COUNT sites are deliberately OUT of scope (this record changes the kind axis, not the door count): `go/internal/auth/doc.go:7-19` ("Two doors reach the same compass.v1 service"), `interceptor.go:86` ("both doors") and `:129` ("both doors reject identically"), `go/server/service.go:539`/`:604` ("attach -one on both doors"). All are scoped to the `auth` package's OWN network+socket -pair (the `CallerFrom` doors) and are narrow-and-true within that scope — a -third KIND does not falsify them, and the third door (RunnerService) already -exists at `eb5ef7a1`, so they are neither PR2 nor T4 work. +one on both doors"). They live on the door-COUNT axis, ORTHOGONAL to the +subject-KIND axis this record adds — a third KIND cannot falsify or refresh a +door-count claim — so they are neither PR2 nor T4 work. (Whether the literal +count is accurate is a pre-existing doc question this record does not own.) ### T5 — cross-door pgtest