Skip to content

docs(server): scope user secrets per tenant, user, and agent (RIG-3655) - #1110

Merged
trunk-io[bot] merged 1 commit into
mainfrom
compass-server/rig-3655-scope-amendment
Sep 12, 2026
Merged

trunk-io[bot] merged 1 commit into
mainfrom
compass-server/rig-3655-scope-amendment

Conversation

@rigel-mintaka

@rigel-mintaka rigel-mintaka commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

This PR is part of a stack containing 7 PRs:

  1. main
  2. "docs(server): scope user secrets per tenant, user, and agent (RIG-3655)" (this PR)
  3. feat(envelope): AES-256-GCM seam for user-secret values (RIG-3655 T1) #1111
  4. feat(store): scope and encrypt user secrets at rest (RIG-3655 T2) #1118
  5. feat(secrets): add the DB-backed StoreResolver (RIG-3655 T3) #1138
  6. docs(server): require admin for tenant-scoped secret writes (RIG-3655) #1141
  7. feat(server): resolve the at-rest master key at boot (RIG-3655 T4) #1146
  8. test(server): prove the armed forge-secret boot path with the real resolver (RIG-3656) #1155

Matt ruled that user secrets need three scope levels, not the flat per-tenant
namespace the frozen record specified.

Per-tenant isolation already existed: secrets carries tenant_id and sits in
the tenant_tables array in 0001_init.sql, so it is RLS-enabled and forced
under tenant_isolation. Per-user and per-agent did not. name was the sole
primary key, so within a tenant the namespace was flat and two users could not
hold different values for one name. FetchSecrets says as much in its own
comment and resolves with no selector.

The model is three tiers (0 tenant, 1 user, 2 agent) with most-specific-wins
resolution, so a tenant row is a real shared value and a user or agent row
shadows it. The primary key becomes (name, scope_kind, scope_id).

Resolution collapses in SQL via DISTINCT ON (name) ... ORDER BY name, scope_kind DESC, not in Go: a shadowed row never leaves Postgres, so the
resolver never decrypts a value it would discard, and the precedence lives in
one ORDER BY rather than a Go merge a later writer can reorder.

This amends the record before T2 writes any migration, because the AAD is bound
into the ciphertext -- adding a scope field after rows exist would force a
re-encrypt of every one. The tuple is fixed now, while the table is empty.

Agent scoping is cheap because the identity is already at the call site and
currently thrown away: Hub.HasLiveSession and Hub.HasContainerBinding look
up sessionAccounts/containerAccounts, both map[string]store.AccountID,
and return a bool. agent_accounts.owner_user_id is one FK hop to the owning
user, so all three tiers derive from what the handler already holds.

Scope is an additional filter inside a tenant, never a replacement for RLS.

Four questions I could not answer from the code are in Open questions for Matt,
covering write authorization per scope, declared_by's meaning beside
scope_id, the lifecycle of scoped rows on account deletion, and whether the
delete verb needs scope addressing.

Ledger-impact: adds DL-357 (scope model), DL-358 (resolution) and DL-359 (the
AAD tuple, refining DL-351).

Refs RIG-3655

Co-authored-by: Matt Wilkinson matt@rigel.build

@linear-code

linear-code Bot commented Sep 11, 2026

Copy link
Copy Markdown

RIG-3655

@trunk-io

trunk-io Bot commented Sep 11, 2026

Copy link
Copy Markdown

😎 This pull request was merged.

@rigel-mintaka
rigel-mintaka added this pull request to stack #1112 September 11, 2026 20:05
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-server-rig-3655-scop.compass-eng-docs.pages.dev

Deployed from compass-server/rig-3655-scope-amendment at 4895a11.

Changed pages:

Matt ruled that user secrets need three scope levels, not the flat per-tenant
namespace the frozen record specified.

Per-tenant isolation already existed: `secrets` carries `tenant_id` and sits in
the `tenant_tables` array in `0001_init.sql`, so it is RLS-enabled and forced
under `tenant_isolation`. Per-user and per-agent did not. `name` was the sole
primary key, so within a tenant the namespace was flat and two users could not
hold different values for one name. `FetchSecrets` says as much in its own
comment and resolves with no selector.

The model is three tiers (0 tenant, 1 user, 2 agent) with most-specific-wins
resolution, so a tenant row is a real shared value and a user or agent row
shadows it. The primary key becomes `(name, scope_kind, scope_id)`.

Resolution collapses in SQL via `DISTINCT ON (name) ... ORDER BY name,
scope_kind DESC`, not in Go: a shadowed row never leaves Postgres, so the
resolver never decrypts a value it would discard, and the precedence lives in
one ORDER BY rather than a Go merge a later writer can reorder.

This amends the record before T2 writes any migration, because the AAD is bound
into the ciphertext -- adding a scope field after rows exist would force a
re-encrypt of every one. The tuple is fixed now, while the table is empty.

Agent scoping is cheap because the identity is already at the call site and
currently thrown away: `Hub.HasLiveSession` and `Hub.HasContainerBinding` look
up `sessionAccounts`/`containerAccounts`, both `map[string]store.AccountID`,
and return a bool. `agent_accounts.owner_user_id` is one FK hop to the owning
user, so all three tiers derive from what the handler already holds.

Scope is an additional filter inside a tenant, never a replacement for RLS.

Four questions I could not answer from the code are in Open questions for Matt,
covering write authorization per scope, `declared_by`'s meaning beside
`scope_id`, the lifecycle of scoped rows on account deletion, and whether the
delete verb needs scope addressing.

Ledger-impact: adds DL-357 (scope model), DL-358 (resolution) and DL-359 (the
AAD tuple, refining DL-351).

Refs RIG-3655

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@trunk-io
trunk-io Bot merged commit 77d1ec7 into main Sep 12, 2026
17 checks passed
@trunk-io
trunk-io Bot deleted the compass-server/rig-3655-scope-amendment branch September 12, 2026 18:36
@trunk-io

trunk-io Bot commented Sep 12, 2026

Copy link
Copy Markdown

This pull request was merged into main as part of stacked PR 1155.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants