diff --git a/go/internal/secrets/secrets.go b/go/internal/secrets/secrets.go index 6d55e3f8..271ef733 100644 --- a/go/internal/secrets/secrets.go +++ b/go/internal/secrets/secrets.go @@ -1,25 +1,31 @@ // Package secrets is the Server-side secret resolve surface for the agent -// container runtime (RIG-1327 T3). It wraps SecretSpec resolution behind a -// Resolver interface and owns the resolve-surface value types the Runner fetch -// (T4) and materializer (T5) consume. +// container runtime. It wraps secret resolution behind a Resolver interface and +// owns the resolve-surface value types the Runner fetch path and the container +// materializer consume. // // The split of concerns: -// - internal/store owns the persisted NAMES registry (SecretDeclaration) — -// which secrets are declared and how each is delivered/routed, never a -// value. -// - this package reads that registry, generates the SecretSpec manifest the -// resolver resolves against, calls SecretSpec to resolve the actual values -// from the configured provider (keyring/1Password/Vault/…), and hands back -// ResolvedSecrets (name + value + content-hash version + delivery/kind). +// - internal/store owns two separate registries. The secrets table holds user +// secrets: one row carries the declaration (name, delivery/routing) AND the +// encrypted value columns. Boot/server secret names live in the physically +// separate server_secrets table, declaration only — so the inject-all +// container path, which reads secrets, can never see them by construction. +// - this package reads those registries and hands back ResolvedSecrets (name + +// value + content-hash version + delivery/kind): SpecResolver by generating +// a manifest and resolving names against the provider, StoreResolver by +// decrypting the user rows. // // It maps store enums to its own resolve-surface enums at this edge, exactly as // the comms service maps store↔proto (store/types.go) — so store stays a leaf // and the two evolve independently. The dependency runs one way: secrets → // store (no cycle). // -// Values live only in the provider and this process's memory during a resolve; -// they are never persisted by Compass and never logged. Every value-bearing -// type here redacts under %s/%v/%#v (the store.Credentials pattern). +// Value locations differ by kind. User secret values are persisted in the +// Postgres secrets table as AES-256-GCM ciphertext (the internal/envelope seam +// encrypts them; StoreResolver is the DB-backed resolver over those rows). +// Boot/server secret values are never persisted by Compass — they stay in the +// configured provider (keyring/1Password/Vault/…) and are read through +// SpecResolver. Either way a value is never logged: every value-bearing type +// here redacts under %s/%v/%#v (the store.Credentials pattern). package secrets import ( @@ -32,7 +38,7 @@ import ( ) // DeliveryKind is how a resolved secret is delivered into a container — the -// load-bearing file-vs-env split that fixes how it rotates (T5/T6). It mirrors +// load-bearing file-vs-env split that fixes how it rotates. It mirrors // store.SecretDelivery; the two are mapped at this package's edge. type DeliveryKind uint8 @@ -45,7 +51,7 @@ const ( DeliveryEnv ) -// SecretKind is the routing class the T5 materializer switches on. It mirrors +// SecretKind is the routing class the materializer switches on. It mirrors // store.SecretKind; the two are mapped at this package's edge. type SecretKind uint8 @@ -62,8 +68,8 @@ const ( // nameGrammar is SecretSpec's env-var-name grammar. A declared secret name must // match it: it becomes both a manifest key and, downstream, a path segment -// under $HOME/.compass/secrets/ and a token in a root-adjacent setup script -// (T5). Validated at the store door (store.UpsertSecret) and re-checked here as +// under $HOME/.compass/secrets/ and a token in a root-adjacent setup script. +// Validated at the store door (store.UpsertSecret) and re-checked here as // defense in depth before a name is ever emitted into a generated manifest. var nameGrammar = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) @@ -112,15 +118,15 @@ func ValidateProfile(profile string) error { // of the value, hex-encoded. The registry stores no values and SecretSpec // resolve returns values (not versions), so a content hash is the only // deterministic version producer. A same-value re-set hashes identically, so -// T6's rotation diff sees no change and does nothing — correct, since nothing +// the rotation diff sees no change and does nothing — correct, since nothing // the container holds is stale. // // It is NEVER logged: String/GoString redact the value AND omit the version, so // the hash cannot serve as an offline confirmation oracle for a low-entropy -// secret. T6 diffs the struct field directly, not a log line, so dropping it -// from the log surface costs nothing. (A keyed hash — HMAC under a server key — -// is a post-MVP defense-in-depth option, redundant once the version is unlogged; -// it would amend the frozen SHA-256 algorithm, so it is deferred, not folded.) +// secret. The rotation diff reads the struct field directly, not a log line, so +// dropping it from the log surface costs nothing. (A keyed hash — HMAC under a +// server key — is a defense-in-depth option, redundant once the version is +// unlogged; it would amend the frozen SHA-256 algorithm, so it is deferred.) func Version(value string) string { sum := sha256.Sum256([]byte(value)) return hex.EncodeToString(sum[:]) @@ -134,7 +140,7 @@ type ResolvedSecret struct { Name string Value string Version string - // Delivery is the file-vs-env split (T5/T6 rotation shape). + // Delivery is the file-vs-env split (rotation shape). Delivery DeliveryKind // Kind is the materializer routing class. Kind SecretKind diff --git a/go/internal/store/secrets.go b/go/internal/store/secrets.go index 6c514b38..057aa3a4 100644 --- a/go/internal/store/secrets.go +++ b/go/internal/store/secrets.go @@ -10,7 +10,7 @@ import ( ) // SecretDelivery is how a declared secret is delivered into a container — the -// load-bearing file-vs-env split that determines how it rotates (T5/T6). Stored +// load-bearing file-vs-env split that determines how it rotates. Stored // as the small int the secrets resolve surface uses (secrets.DeliveryKind), // mapped at that package's edge like every other store↔proto enum (types.go). type SecretDelivery int32 @@ -24,7 +24,7 @@ const ( SecretDeliveryEnv SecretDelivery = 1 ) -// SecretKind is the routing class the T5 materializer switches on: a generic +// SecretKind is the routing class the materializer switches on: a generic // declared secret, a provider (LLM) credential that rides the OMP SDK auth // surface, or a gh credential placed into ~/.config/gh/hosts.yml. type SecretKind int32 @@ -44,18 +44,18 @@ const ( // secretNamePattern is SecretSpec's env-var-name grammar. A declared name is // validated against it at the store door (UpsertSecret) — before it can reach // a row — because it later becomes a path segment under $HOME/.compass/secrets/ -// and a line in a root-adjacent setup script (T5): constrained at the door, not +// and a line in a root-adjacent setup script: constrained at the door, not // escaped downstream. The identical grammar is re-exported and re-checked by // internal/secrets (defense in depth at materialization). var secretNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) -// SecretDeclaration is one names-only registry row: a declared secret's name, -// how it is delivered/routed, and who declared it — NEVER its value. The value -// lives only in the SecretSpec provider; the Server resolves it at fetch time -// (internal/secrets) and never persists it. +// SecretDeclaration is the value-free view of a user-secret row: the name, how +// it is delivered/routed, and who declared it. The struct carries no value, but +// the underlying row does — the value is AES-256-GCM ciphertext beside it, read +// and decrypted through StoreResolver rather than resolved from a provider. type SecretDeclaration struct { Name string - // Delivery is the file-vs-env split (T5/T6 rotation shape). + // Delivery is the file-vs-env split that fixes how the secret rotates. Delivery SecretDelivery // Kind is the materializer routing class. Kind SecretKind @@ -65,7 +65,7 @@ type SecretDeclaration struct { // else ""). Host string // DeclaredBy is the account that declared the secret (write path is - // user-only, enforced at the T7 RPC edge). + // user-only, enforced at the RPC edge). DeclaredBy AccountID CreatedAt time.Time UpdatedAt time.Time @@ -77,7 +77,7 @@ type SecretDeclaration struct { // and no provider, a generic row (kind=0) neither. A caller that violates it // gets an actionable ErrInvalidArgument here rather than a raw constraint // violation from the INSERT — and an out-of-invariant row can never reach the -// T5 materializer, where an empty provider id would silently misroute. +// materializer, where an empty provider id would silently misroute. func validateKindRouting(kind SecretKind, provider, host string) error { switch kind { case SecretKindGeneric: diff --git a/go/server/secrets_service.go b/go/server/secrets_service.go index 130fb069..91694f1c 100644 --- a/go/server/secrets_service.go +++ b/go/server/secrets_service.go @@ -1,24 +1,24 @@ //go:build unix // The SecretsService implementation — the account-facing side of the compass.v1 -// secrets contract (RIG-1327 T7). It sits beside CompassService/CommsService on -// the same account doors (socket + dev + network), behind the bearer + admin-gate +// secrets contract. It sits beside CompassService/CommsService on the same +// account doors (socket + dev + network), behind the bearer + admin-gate // interceptors that classify the three procedures authenticatedOpen (admin_gate.go): // the door admits any authenticated account and THIS handler enforces the fine // authz the frozen record pins. // -// - SetSecret / DeleteSecret are USER-ONLY (record §911-927): an agent-token -// caller is CodePermissionDenied, the same fail-closed posture as the -// admin-gated IssueToken. This is the load-bearing regression the record -// calls out (§927). -// - ListSecrets is open to user AND agent (record §904-910): the Setup agent -// drives it. It returns value-free SecretStatus — never a value, and never -// resolves values to compute is_set. +// - SetSecret / DeleteSecret are user-only: an agent-token caller is +// CodePermissionDenied (the requireUser gate), the same fail-closed posture +// as the admin-gated IssueToken. A tenant-scoped write additionally requires +// an admin (D8), at the coordinate the D9 selector resolves. +// - ListSecrets is open to user AND agent: the Setup agent drives it. It +// returns value-free SecretStatus — never a value, and never resolves values +// to compute is_set. // // A successful Set/Delete bumps the secrets version (a fire-and-forget hub push -// to live sessions, secretsSignaler) so live containers re-fetch (T6 cleanup). -// A secret value is never logged here (it is [debug_redact] on the wire; the -// server side keeps the same posture). +// to live sessions, secretsSignaler) so live containers re-fetch. A secret value +// is never logged here (it is [debug_redact] on the wire; the server side keeps +// the same posture). package server import ( @@ -91,8 +91,8 @@ var errNoResolver = errors.New("no secret resolver configured on this server") var errNoServerResolver = errors.New("no server secret resolver configured on this server") // SetSecret writes a user secret's declaration and encrypted value in ONE atomic -// upsert at the caller-resolved scope coordinate. USER-ONLY (record §911-927): an -// agent-token caller is CodePermissionDenied. `value` is never logged. +// upsert at the caller-resolved scope coordinate. User-only: an agent-token +// caller is CodePermissionDenied. `value` is never logged. // // The declaration and the value are the same row now (A1), so the write is a // single StoreResolver.Upsert transaction — the old declare-then-Set-then-rollback @@ -142,7 +142,7 @@ func (s *secretsService) SetSecret( } // ListSecrets returns the value-free status of every declared secret. Open to -// USER AND AGENT (record §904-910): the Setup agent drives it, so no kind +// USER AND AGENT (the Setup agent drives it), so no kind // restriction. It reads the declaration registry and maps each row to a // SecretStatus — NEVER a value (SecretStatus has no value field), and never // resolves values to compute is_set. @@ -182,7 +182,7 @@ func (s *secretsService) ListSecrets( // DeleteSecret removes a user secret's row (declaration and value are the same // row post-A1) at the caller-resolved coordinate, then bumps the secrets version. -// USER-ONLY (record §915-918): an agent-token caller is CodePermissionDenied. A +// User-only: an agent-token caller is CodePermissionDenied. A // name that was never declared at that coordinate is CodeNotFound. A reserved- // prefix name is rejected CodeInvalidArgument ahead of any store call — the F1 // name partition keeps reserved names out of the user table, so a delete on one @@ -288,13 +288,12 @@ func (s *secretsService) requireCaller(ctx context.Context) (store.AccountID, er // requireUser returns the authenticated caller id AND role only when the caller // is a USER account; an agent account is CodePermissionDenied (the user-only -// write gate, record §919-927 — the same fail-closed posture as admin-gated -// IssueToken). No caller is CodeUnauthenticated (fail closed). The account kind -// is read from the store (an agent account has the Agent subtype set; a user -// does not — IsAgent). The role is returned so a handler can gate a tenant-scope -// write (D9) without a second GetAccount; a caller with no user payload (the -// reserved system account) is the least-privilege member, so it cannot pass the -// admin gate. +// write gate — the same fail-closed posture as admin-gated IssueToken). No +// caller is CodeUnauthenticated (fail closed). The account kind is read from the +// store (an agent account has the Agent subtype set; a user does not — IsAgent). +// The role is returned so a handler can gate a tenant-scope write (D8) without a +// second GetAccount; a caller with no user payload (the reserved system account) +// is the least-privilege member, so it cannot pass the admin gate. func (s *secretsService) requireUser(ctx context.Context) (store.AccountID, store.UserRole, error) { callerID, err := s.requireCaller(ctx) if err != nil {