Documentation
¶
Index ¶
- Constants
- Variables
- func ActiveVersionOf(p RootKeyProvider) int
- func AssertHPKEKeyPair(p *HPKEKeyPairPayload, prevGeneration int64) error
- func BatchHash(batch Batch) string
- func BuildKEKSecretData(ctx context.Context, kms RootKeyProvider, keyID string, kek []byte) (map[string][]byte, error)
- func CanonicalBatch(batch Batch) []byte
- func ContextWithDecryptUser(ctx context.Context, userID string) context.Context
- func ContextWithMatchedSigningKey(ctx context.Context, matchedSigningKey []byte) context.Context
- func ContextWithSessionID(ctx context.Context, sessionID string) context.Context
- func DecryptSecret(dek, ciphertext []byte) ([]byte, error)
- func DeriveKEKFromKey(keyMaterial, salt []byte, info string) ([]byte, error)
- func DeriveKEKFromPassword(password, salt []byte) ([]byte, error)
- func DeriveSealedKEK(password, salt []byte, info string) ([]byte, error)
- func DeriveServerKey(master []byte, purpose string) []byte
- func EncryptSecret(dek, plaintext []byte) ([]byte, error)
- func GenerateDEK() ([]byte, error)
- func GenerateSalt() ([]byte, error)
- func GenerateStagingKEK() ([]byte, error)
- func HPKEKeyID(generation int64) string
- func InspectStagingEnvelope(envelope string) (alg, keyID string, err error)
- func IsRelayDegrade(d *BuildDegrade) bool
- func LegacyBatchJSON(batch Batch) []byte
- func ManifestHash(ownerID string, entries []ManifestEntry) string
- func ManifestHashWithRelayRevision(ownerID string, entries []ManifestEntry, relayRevision string) string
- func ProviderDefaultBaseURL(kind string) string
- func ProviderRelayStageable(kind string) bool
- func RelayFallbackMetrics() (fallback, degraded *prometheus.CounterVec)
- func RelayFrontableProvider(pd LLMProviderData) bool
- func RelayHandoffSecretName(workspaceName string) string
- func SealRootKey(path string, passphrase, rootKey []byte) error
- func StagedKeyMaterialRules(id string, material []byte) []redact.DynamicRule
- func StagedKeyRedactionID(envelope string) string
- func ValidateKind(kind string) error
- func ValidateModelAllowlist(ids []string) error
- func ValidateSlug(slug string) error
- type APIKeyRecord
- type APIKeyStore
- type AWSKMSProvider
- type AdminKeyDeriverdeprecated
- type AsyncAuditLogger
- func (l *AsyncAuditLogger) AddBindings(ctx context.Context, workspaceID string, secretIDs []string) error
- func (l *AsyncAuditLogger) BindCredentialToAllUserWorkspaces(ctx context.Context, credentialID, userID string) error
- func (l *AsyncAuditLogger) CreateSecret(ctx context.Context, secret *UserSecret) error
- func (l *AsyncAuditLogger) CurrentRevision(ctx context.Context, workspaceID string) (int64, string, bool, error)
- func (l *AsyncAuditLogger) DeleteSecret(ctx context.Context, userID, secretID string) error
- func (l *AsyncAuditLogger) EnsureRevision(ctx context.Context, workspaceID, manifestHash string) (int64, error)
- func (l *AsyncAuditLogger) GetBindings(ctx context.Context, workspaceID string) ([]*UserSecret, error)
- func (l *AsyncAuditLogger) GetBindingsForSecret(ctx context.Context, secretID string) ([]string, error)
- func (l *AsyncAuditLogger) GetSecret(ctx context.Context, userID, secretID string) (*UserSecret, error)
- func (l *AsyncAuditLogger) GetSecretByName(ctx context.Context, userID, name string) (*UserSecret, error)
- func (l *AsyncAuditLogger) GetWorkspaceCredentials(ctx context.Context, workspaceID string) ([]CredentialBinding, error)
- func (l *AsyncAuditLogger) GetWorkspaceMCPServers(ctx context.Context, workspaceID string) ([]MCPServerBindingRow, error)
- func (l *AsyncAuditLogger) HasUserProviderCredential(ctx context.Context, userID, slug string) (bool, error)
- func (l *AsyncAuditLogger) ListGlobalDefaultSecrets(ctx context.Context, userID string) ([]*UserSecret, error)
- func (l *AsyncAuditLogger) ListSecrets(ctx context.Context, userID string) ([]*UserSecret, error)
- func (l *AsyncAuditLogger) LogAudit(_ context.Context, entry *AuditEntry) (retErr error)
- func (l *AsyncAuditLogger) QueryAudit(ctx context.Context, userID string, query AuditQuery) ([]*AuditEntry, error)
- func (l *AsyncAuditLogger) ReEncryptUserSecrets(ctx context.Context, userID string, newKeyVersion int, ...) error
- func (l *AsyncAuditLogger) SeedWorkspaceCredentials(ctx context.Context, workspaceID, userID string, orgID *string) error
- func (l *AsyncAuditLogger) SetBindings(ctx context.Context, workspaceID string, secretIDs []string) error
- func (l *AsyncAuditLogger) Stats() AsyncAuditStats
- func (l *AsyncAuditLogger) Stop()
- func (l *AsyncAuditLogger) SyncGlobalDefaultBindings(ctx context.Context, workspaceID string, secretIDs []string) ([]string, []string, error)
- func (l *AsyncAuditLogger) UpdateSecret(ctx context.Context, secret *UserSecret) error
- func (l *AsyncAuditLogger) UpsertFreeTierCredential(ctx context.Context, ciphertext []byte) error
- type AsyncAuditStats
- type AuditEntry
- type AuditQuery
- type AuditWriter
- type AuditedProvider
- type AutoApplyRule
- type Batch
- type BatchEntry
- type BatchRevision
- type BindingsMutationResult
- type BindingsResponse
- type BoundSecret
- type BuildDegrade
- type CiphertextAudit
- type CiphertextClass
- type CompositeProvider
- type CreateSecretRequest
- type CredentialBinding
- type CredentialBindingInfo
- type CredentialRow
- type CredentialStore
- type DEKCache
- type GPCKMSProvider
- type HPKEKeyPairPayload
- type HPKEPubPayload
- type HPKEStagingResolver
- type HPKEStagingSealer
- type InjectedSecret
- type JWTSession
- type JWTSessionJanitor
- type JWTSessionStore
- type KEKMigrationError
- type KEKMigrationResult
- type KEKRotationError
- type KEKRotationResult
- type KEKSecretData
- type KMSStagingProvider
- type KeyService
- func (s *KeyService) CacheDEK(ctx context.Context, sessionID string, dek []byte, ttl time.Duration) error
- func (s *KeyService) DEKAvailable(ctx context.Context, sessionID string) bool
- func (s *KeyService) DeleteDurableSessionsForUser(ctx context.Context, userID string) error
- func (s *KeyService) EvictDEK(ctx context.Context, sessionID string) error
- func (s *KeyService) GetCachedDEKForUser(ctx context.Context, userID string) (dek []byte, jti string, err error)
- func (s *KeyService) GetDEK(ctx context.Context, sessionID string, _ []byte) ([]byte, error)
- func (s *KeyService) GetDEKServerSide(ctx context.Context, userID string) (dek []byte, jti string, err error)
- func (s *KeyService) HasKeys(ctx context.Context, userID string) (bool, error)
- func (s *KeyService) InitializeUserKeysServerKEK(ctx context.Context, userID, dekSource string) error
- func (s *KeyService) JWTSessionStoreSet() bool
- func (s *KeyService) SetAPIKeyStore(store APIKeyStore, provider RootKeyProvider)
- func (s *KeyService) SetJWTSessionStore(store JWTSessionStore)
- func (s *KeyService) SetLogger(l pkginterfaces.LoggerInterface)
- func (s *KeyService) SetSecretStore(store SecretStore)
- func (s *KeyService) UnlockDEK(ctx context.Context, userID string, password []byte, sessionID string, ...) error
- type KeyStore
- type LLMModelConfig
- type LLMProviderData
- type MCPAutoApplyRule
- type MCPServerBindingRow
- type MCPServerRow
- type ManifestEntry
- type MigrationCoordinator
- func (c *MigrationCoordinator) AuditAll(ctx context.Context, target string) (map[string]CiphertextAudit, error)
- func (c *MigrationCoordinator) AuditTable(ctx context.Context, table, target string) (CiphertextAudit, error)
- func (c *MigrationCoordinator) MigrateAll(ctx context.Context, dryRun bool) (map[string]KEKMigrationResult, error)
- func (c *MigrationCoordinator) MigrateTable(ctx context.Context, table, resumeFromID string, dryRun bool) (KEKMigrationResult, error)
- type MigrationRow
- type MigrationStore
- type OrgAutoApplyStore
- type OwnerType
- type PgJWTSessionStore
- func (s *PgJWTSessionStore) DeleteExpiredJWTSessions(ctx context.Context, before time.Time) (int64, error)
- func (s *PgJWTSessionStore) DeleteJWTSession(ctx context.Context, jti uuid.UUID) error
- func (s *PgJWTSessionStore) DeleteJWTSessionsForUser(ctx context.Context, userID string) (int64, error)
- func (s *PgJWTSessionStore) ListActiveJWTSessionsForUser(ctx context.Context, userID string, limit int) ([]*JWTSession, error)
- type PgKeyStore
- func (s *PgKeyStore) CompareAndSwapWrappedDEK(ctx context.Context, userID string, expectedWrapped, newWrapped []byte, ...) (bool, error)
- func (s *PgKeyStore) CreateUserKey(ctx context.Context, record *UserKeyRecord) error
- func (s *PgKeyStore) DeleteExpiredRetainedWraps(ctx context.Context, now time.Time) (int64, error)
- func (s *PgKeyStore) GetUserKey(ctx context.Context, userID string) (*UserKeyRecord, error)
- func (s *PgKeyStore) ListUserKeysForReconcile(ctx context.Context, limit, offset int) ([]UserKeyReconcileRow, error)
- func (s *PgKeyStore) UpdateWrappedDEK(ctx context.Context, userID string, wrappedDEK []byte, salt []byte, ...) error
- type PgSecretStore
- func (s *PgSecretStore) AddBindings(ctx context.Context, workspaceID string, secretIDs []string) error
- func (s *PgSecretStore) BackfillFreeTierBindings(ctx context.Context) (int64, error)
- func (s *PgSecretStore) BackfillMCPServerAutoApply(ctx context.Context, serverID string) (int64, error)
- func (s *PgSecretStore) BindAllOrgCredentialsToOrgWorkspaces(ctx context.Context, orgID string) error
- func (s *PgSecretStore) BindCredentialToAllOrgWorkspaces(ctx context.Context, credentialID, orgID string) error
- func (s *PgSecretStore) BindCredentialToAllUserWorkspaces(ctx context.Context, credentialID, userID string) error
- func (s *PgSecretStore) BindCredentialToWorkspace(ctx context.Context, credentialID, workspaceID string) error
- func (s *PgSecretStore) BindMCPServerToWorkspace(ctx context.Context, serverID, workspaceID string) error
- func (s *PgSecretStore) CountMCPServersByOwner(ctx context.Context, ownerType, ownerID string) (int, error)
- func (s *PgSecretStore) CountWorkspaceMCPServers(ctx context.Context, workspaceID string) (int, error)
- func (s *PgSecretStore) CreateAutoApply(ctx context.Context, credentialID, targetType string, targetID *string, ...) error
- func (s *PgSecretStore) CreateCredential(ctx context.Context, ownerType, ownerID string, row *CredentialRow) error
- func (s *PgSecretStore) CreateMCPServer(ctx context.Context, row *MCPServerRow) error
- func (s *PgSecretStore) CreateMCPServerAutoApply(ctx context.Context, serverID, targetType string, targetID *string) error
- func (s *PgSecretStore) CreateOrgAutoApply(ctx context.Context, credentialID, orgID string, withinPriority int) error
- func (s *PgSecretStore) CreateSecret(ctx context.Context, secret *UserSecret) error
- func (s *PgSecretStore) CurrentRevision(ctx context.Context, workspaceID string) (int64, string, bool, error)
- func (s *PgSecretStore) DeleteAutoApply(ctx context.Context, credentialID, targetType string, targetID *string) error
- func (s *PgSecretStore) DeleteCredential(ctx context.Context, ownerType, ownerID, credID string) error
- func (s *PgSecretStore) DeleteMCPServer(ctx context.Context, ownerType, ownerID, serverID string) error
- func (s *PgSecretStore) DeleteMCPServerAutoApply(ctx context.Context, serverID, targetType string, targetID *string) error
- func (s *PgSecretStore) DeleteOrgAutoApply(ctx context.Context, credentialID, orgID string) error
- func (s *PgSecretStore) DeleteSecret(ctx context.Context, userID, secretID string) error
- func (s *PgSecretStore) EnsureRevision(ctx context.Context, workspaceID, manifestHash string) (int64, error)
- func (s *PgSecretStore) GetBindings(ctx context.Context, workspaceID string) ([]*UserSecret, error)
- func (s *PgSecretStore) GetBindingsForSecret(ctx context.Context, secretID string) ([]string, error)
- func (s *PgSecretStore) GetCredential(ctx context.Context, ownerType, ownerID, credID string) (*CredentialRow, error)
- func (s *PgSecretStore) GetCredentialBindings(ctx context.Context, credentialID, ownerID string) ([]string, error)
- func (s *PgSecretStore) GetCredentialBindingsWithSource(ctx context.Context, credentialID, ownerID string) ([]CredentialBindingInfo, error)
- func (s *PgSecretStore) GetMCPServer(ctx context.Context, ownerType, ownerID, serverID string) (*MCPServerRow, error)
- func (s *PgSecretStore) GetSecret(ctx context.Context, userID, secretID string) (*UserSecret, error)
- func (s *PgSecretStore) GetSecretByName(ctx context.Context, userID, name string) (*UserSecret, error)
- func (s *PgSecretStore) GetWorkspaceCredentials(ctx context.Context, workspaceID string) ([]CredentialBinding, error)
- func (s *PgSecretStore) GetWorkspaceMCPServers(ctx context.Context, workspaceID string) ([]MCPServerBindingRow, error)
- func (s *PgSecretStore) GetWorkspaceOrgIDForMCP(ctx context.Context, workspaceID string) (string, error)
- func (s *PgSecretStore) GetWorkspaceUserIDForMCP(ctx context.Context, workspaceID string) (string, error)
- func (s *PgSecretStore) HasUserProviderCredential(ctx context.Context, userID, slug string) (bool, error)
- func (s *PgSecretStore) ListAutoApply(ctx context.Context, credentialID string) ([]AutoApplyRule, error)
- func (s *PgSecretStore) ListCredentials(ctx context.Context, ownerType, ownerID string) ([]*CredentialRow, error)
- func (s *PgSecretStore) ListGlobalDefaultSecrets(ctx context.Context, userID string) ([]*UserSecret, error)
- func (s *PgSecretStore) ListMCPServerAutoApply(ctx context.Context, serverID string) ([]MCPAutoApplyRule, error)
- func (s *PgSecretStore) ListMCPServers(ctx context.Context, ownerType, ownerID string) ([]*MCPServerRow, error)
- func (s *PgSecretStore) ListOrgAutoApply(ctx context.Context, orgID string) ([]*AutoApplyRule, error)
- func (s *PgSecretStore) ListSecrets(ctx context.Context, userID string) ([]*UserSecret, error)
- func (s *PgSecretStore) LogAudit(ctx context.Context, entry *AuditEntry) error
- func (s *PgSecretStore) QueryAudit(ctx context.Context, userID string, query AuditQuery) ([]*AuditEntry, error)
- func (s *PgSecretStore) ReEncryptOrgCredentials(ctx context.Context, tx pgx.Tx, orgID string, oldDEK, newDEK []byte) (int, error)
- func (s *PgSecretStore) ReEncryptUserSecrets(ctx context.Context, userID string, newKeyVersion int, ...) error
- func (s *PgSecretStore) SeedWorkspaceCredentials(ctx context.Context, workspaceID, userID string, orgID *string) error
- func (s *PgSecretStore) SeedWorkspaceMCPServers(ctx context.Context, workspaceID, userID string, orgID *string) error
- func (s *PgSecretStore) SetBindings(ctx context.Context, workspaceID string, secretIDs []string) error
- func (s *PgSecretStore) SyncGlobalDefaultBindings(ctx context.Context, workspaceID string, secretIDs []string) (added, removed []string, err error)
- func (s *PgSecretStore) UnbindCredentialFromWorkspace(ctx context.Context, credentialID, workspaceID string) error
- func (s *PgSecretStore) UnbindMCPServerFromWorkspace(ctx context.Context, serverID, workspaceID string) error
- func (s *PgSecretStore) UpdateCredential(ctx context.Context, ownerType, ownerID, credID string, row *CredentialRow) error
- func (s *PgSecretStore) UpdateMCPServer(ctx context.Context, ownerType, ownerID, serverID string, row *MCPServerRow) error
- func (s *PgSecretStore) UpdateSecret(ctx context.Context, secret *UserSecret) error
- func (s *PgSecretStore) UpsertFreeTierCredential(ctx context.Context, ciphertext []byte) error
- type PostgresSecretProvider
- func (p *PostgresSecretProvider) DEKAvailable(ctx context.Context, owner SecretOwner) bool
- func (p *PostgresSecretProvider) Decrypt(ctx context.Context, owner SecretOwner, ciphertext []byte, keyVersion int) ([]byte, error)
- func (p *PostgresSecretProvider) Encrypt(ctx context.Context, owner SecretOwner, plaintext []byte) ([]byte, int, error)
- func (p *PostgresSecretProvider) RotateKey(ctx context.Context, owner SecretOwner) (int, error)
- type ReconcileKeyStore
- type RedactStagedKeys
- type RedisDEKCache
- type RelayHandoff
- type RelayHandoffProvider
- type RelayTokenSource
- type RetainedWrap
- type RevisionStore
- type RootKeyProvider
- type RotationCoordinator
- type RotationRow
- type RotationStore
- type SealedKeyProvider
- type SecretBinding
- type SecretOwner
- type SecretProvider
- type SecretResponse
- type SecretService
- func (s *SecretService) AddBindings(ctx context.Context, userID, workspaceID string, secretIDs []string) (BindingsMutationResult, error)
- func (s *SecretService) BuildWorkspaceBatch(ctx context.Context, ownerUserID, workspaceID string) (*Batch, *BuildDegrade, error)
- func (s *SecretService) CreateSecret(ctx context.Context, userID, sessionID string, matchedSigningKey []byte, ...) (*SecretResponse, error)
- func (s *SecretService) CurrentRevision(ctx context.Context, workspaceID string) (int64, string, bool, error)
- func (s *SecretService) DecryptSecretValue(ctx context.Context, userID, sessionID string, matchedSigningKey []byte, ...) ([]byte, error)
- func (s *SecretService) EnsureRevision(ctx context.Context, workspaceID, manifestHash string) (int64, error)
- func (s *SecretService) ForceRevokeSecret(ctx context.Context, userID, secretID string) ([]string, error)
- func (s *SecretService) GetBindings(ctx context.Context, userID, workspaceID string) (*BindingsResponse, error)
- func (s *SecretService) GetBindingsForSecret(ctx context.Context, userID, secretID string) ([]string, error)
- func (s *SecretService) GetSecret(ctx context.Context, userID, secretID string) (*SecretResponse, error)
- func (s *SecretService) GetSecretByName(ctx context.Context, userID, name string) (*SecretResponse, error)
- func (s *SecretService) ListSecrets(ctx context.Context, userID string) ([]*SecretResponse, error)
- func (s *SecretService) ManifestFor(ctx context.Context, ownerUserID, workspaceID string) (string, error)
- func (s *SecretService) QueryAudit(ctx context.Context, userID string, query AuditQuery) ([]*AuditEntry, error)
- func (s *SecretService) RelayFallbackForTest() bool
- func (s *SecretService) RelayOnlyEnabled() bool
- func (s *SecretService) RelayTokensForTest() RelayTokenSource
- func (s *SecretService) ResolveLLMProviders(ctx context.Context, ownerUserID, workspaceID string) ([]LLMProviderData, error)
- func (s *SecretService) SeedGlobalDefaultSecrets(ctx context.Context, workspaceID, userID string) error
- func (s *SecretService) SetAdminProvider(p RootKeyProvider)
- func (s *SecretService) SetBindings(ctx context.Context, userID, workspaceID string, secretIDs []string) (BindingsMutationResult, error)
- func (s *SecretService) SetOrgProvider(p RootKeyProvider)
- func (s *SecretService) SetRelayDeliveryFallback(enabled bool)
- func (s *SecretService) SetRelayTokenSource(src RelayTokenSource)
- func (s *SecretService) SyncGlobalDefaultBindings(ctx context.Context, ownerUserID, workspaceID string) (added, removed []string, err error)
- func (s *SecretService) UpdateSecret(ctx context.Context, userID, sessionID string, matchedSigningKey []byte, ...) error
- type SecretStore
- type SecretType
- type SetBindingsRequest
- type StagedKeyRedactor
- type StagingResolver
- type StagingSealer
- type StaticKeyProvider
- type UpdateSecretRequest
- type UserKeyReconcileRow
- type UserKeyRecord
- type UserSecret
- type VersionedProvider
- type WorkspaceBatchBuilder
Constants ¶
const ( // DegradeDEKUnwrapFailed: the owner's user_keys row exists but could // not be unwrapped (or the key service is unwired). User-owned // entries are skipped-with-audit; server-KEK entries still deliver. DegradeDEKUnwrapFailed = "dek_unwrap_failed" // DegradeOwnerNoKeys: the owner has no user_keys record, so no // DEK-encrypted content of theirs exists — the batch is complete in // content terms and the reason records why user rows were skipped. DegradeOwnerNoKeys = "owner_no_keys" )
Degrade reason codes (machine-readable, epic #1158 law 5: loud or absent). Only "owner has no DEK-encrypted content bound" is quiet in content terms; every other non-delivery carries one of these.
const ( // RelayMetadataKey marks a token-emitted llm-provider entry. RelayMetadataKey = "relay" // RelayMetadataExpiresAtKey carries the token's RFC3339 expiry // (honored pod-side for the token_expired degrade code). RelayMetadataExpiresAtKey = "relayExpiresAt" // RelayMetadataRevisionKey carries the staged revision the token // came from (the controller's lineage conjunct compares it against // the workspace's staged-revision annotation). RelayMetadataRevisionKey = "relayRevision" )
Relay batch-entry metadata keys (string-valued — the materializer's wire shape flattens metadata to map[string]string). agentd's relay liveness registry reads these off the durable batch file.
const ( // RelayHandoffSecretPrefix builds the Secret name from the K8s // Workspace's name. RelayHandoffSecretPrefix = "workspace-relay-" // RelayHandoffDataKey is the Secret's single data key. RelayHandoffDataKey = "handoff" )
Handoff Secret contract (US-72.3 worklog D2 — written by the controller's staging pass, read here): name `workspace-relay-<workspaceName>` in the workspace namespace, single data key `handoff` carrying the RelayHandoff JSON.
const ( StagingAlgAESGCM = "aes-256-gcm" StagingAlgHPKE = "hpke" )
const ( KEKSecretKeyID = "key-id" KEKSecretWrapped = "wrapped-kek" )
Keys of the `llm-relay-kek` Secret (llm-relay namespace). The Secret carries the KMS-wrapped KEK blob and the key id — never plaintext KEK bytes.
const ( // RelayEnvWorkspaceLabel identifies the workspace a staged envelope // belongs to (value: the Workspace's K8s name — also the routing-path // workspaceID the token scope binds). RelayEnvWorkspaceLabel = "llmsafespaces.dev/workspace-id" // RelayEnvProviderLabel identifies the provider slug (the decrypted // LLMProviderData slug — the provider-map key the batch renders). RelayEnvProviderLabel = "llmsafespaces.dev/provider-slug" // RelayEnvDataKey holds the stg:v1 envelope wire string. RelayEnvDataKey = "envelope" // RelayEnvModelsKey optionally holds a JSON array of allowlisted model // IDs — the catalog the router serves GET /models from with zero // upstream fetches (design 0058 §4.5). RelayEnvModelsKey = "models" )
Envelope Secret contract (US-72.3 writes, US-72.2's router reads via its llm-relay informer). Single-sourced here so the two sides cannot drift: the router's cache keys off these labels/data keys and ignores anything that does not carry them.
const ( // RelayPubSecretName is the controller-readable HPKE public-key Secret // (payload: HPKEPubPayload under RelayPubDataKey). RelayPubSecretName = "llm-relay-hpke-pub" // RelayKeyPairSecretName is the router-private keypair Secret. RelayKeyPairSecretName = "llm-relay-hpke-key" // RelayMintKeyName is the controller-created, router-read mint-key // Secret (data key "mint-key") authenticating the internal mint/rotate // API (design 0058 §4.4). RelayMintKeyName = "llm-relay-mint-key" // RelayMintKeyDataKey is the mint-key Secret's data key. RelayMintKeyDataKey = "mint-key" // RelayKEKSecretName is the KMS-mode KEK Secret (prod deployments). RelayKEKSecretName = "llm-relay-kek" // RelayPubDataKey is the data key both keypair Secrets use for their // JSON payload. RelayPubDataKey = "payload" )
llm-relay Secret names shared by controller and router.
const ( BindSourceManual = "manual" BindSourceGlobalDefault = "global_default" )
Binding provenance (migration 000033). The source records WHY a binding row exists: a user's explicit claim (manual — workspace bindings UI, SetWorkspaceEnv, SetBindings) or the global_default policy (global_default — workspace-create seeding and the reconcile loop's policy-convergence step). Policy convergence never adds, rewrites, or removes a manual row.
const DEKCachePrefix = "dek:"
DEKCachePrefix is the Redis key prefix for cached DEKs (dek:<session>). The rotate-kek and migrate-kek CLIs import it so their post-rotation cache flush deletes exactly these keys — never the rate-limit, session, or revocation keys sharing the Redis instance.
const DefaultJWTSessionJanitorInterval = 60 * time.Second
DefaultJWTSessionJanitorInterval is the period between expiry-pruning passes on the jwt_sessions table. 60s is small relative to the JWT lifetimes the table holds (24h default, 30d remember-me), so a row pruned a minute late costs nothing observable. The DELETE WHERE expires_at < NOW() query is O(log N) thanks to idx_jwt_sessions_expires_at.
const DegradeRelayFallbackDelivery = "relay_fallback_delivery"
DegradeRelayFallbackDelivery (design 0061 §4, M2): the migration-mode fallback outcome — staging was not ready at batch time and the batch delivered PRE-FLIP RAW-KEY entries instead (counted per provider by relay_fallback_deliveries_total, audited relay_fallback_delivery). NON-NIL by contract: the M4 CredentialsStaged hook surfaces it as False/relay_fallback_delivery so the migration is per-workspace visible (the counter alerts; the condition explains).
const DegradeRelayStagingNotReady = "relay_staging_not_ready"
DegradeRelayStagingNotReady is the loud-degrade reason emitted when relay-only is enabled but the controller-staged handoff Secret is absent or unreadable (staging not ready: the controller has not completed a pass, or the Secret was deleted out-of-band).
const MaxSecretValueBytes = 2 << 20
MaxSecretValueBytes caps a single secret's plaintext (W8): file-class delivery stages bytes onto the pod tmpfs and serves them under the /v1/spawn-files budget; a value larger than this can never deliver. Enforced at bind time (create + update) so the author gets a 400, not a spawn-time degrade.
const ServerKeyHKDFSalt = "llmsafespaces-server"
ServerKeyHKDFSalt is the HKDF domain-separation salt binding every purpose-scoped server key to the llmsafespaces-server domain. It is part of the at-rest key contract: changing it re-derives every purpose key and renders every existing ciphertext undecryptable without a rotation run.
const SlugRegex = `^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$`
SlugRegex is the canonical slug-validation regex. Must be byte-identical to the DB CHECK regex in 000001_initial_schema.up.sql:
CHECK (slug ~ '^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$')
Semantics: 1-64 chars, lowercase alphanumeric + hyphens, must start AND end with alphanumeric (no leading/trailing hyphen).
const StagedKeyRedactionReplacement = "[REDACTED-STAGED-KEY]"
StagedKeyRedactionReplacement is the marker dynamic staged-key rules emit.
Variables ¶
var ( ErrDecryptionFailed = errors.New("decryption failed: ciphertext tampered or wrong key") ErrInvalidCiphertext = errors.New("ciphertext too short") ErrInvalidSaltLength = errors.New("salt must be 32 bytes") )
var ( // ErrSecretNotFound is returned when a secret does not exist or // is not owned by the requesting user. Both cases are conflated // to avoid leaking workspace existence cross-user. ErrSecretNotFound = &pkgerrors.StatusError{ Status: http.StatusNotFound, Code: "secret_not_found", Message: "secret not found", } // ErrDuplicateSecret is returned when CreateSecret would violate // the (user_id, name) uniqueness constraint. ErrDuplicateSecret = &pkgerrors.StatusError{ Status: http.StatusConflict, Code: "duplicate_secret", Message: "secret with this name already exists", } // in the cache (typically because the JWT's jti has expired or // the user has not logged in since the cache was flushed). ErrDEKUnavailable = &pkgerrors.StatusError{ Status: http.StatusForbidden, Code: "dek_unavailable", Message: "encryption key not available; re-authenticate", } // ErrInvalidSecretType is returned when a CreateSecret request // names a type outside ValidSecretTypes. ErrInvalidSecretType = &pkgerrors.StatusError{ Status: http.StatusBadRequest, Code: "invalid_secret_type", Message: "invalid secret type", } // ErrInvalidMetadata is returned when the metadata blob is // missing a required field for the secret type, fails JSON // validation, or contains an adversarial mount_path. ErrInvalidMetadata = &pkgerrors.StatusError{ Status: http.StatusBadRequest, Code: "invalid_metadata", Message: "invalid secret metadata", } // ErrInvalidPassword is returned by RevealSecret when the // password reconfirmation step fails. The handler maps this to // a uniform 403 — the same status used for missing DEK — so // the response shape does not differentiate between "wrong // password" and "session expired", reducing what an attacker // who has stolen a JWT can learn. ErrInvalidPassword = &pkgerrors.StatusError{ Status: http.StatusForbidden, Code: "invalid_password", Message: "access denied", } // ErrUserKeysMissing is returned when the user_keys row for the // caller does not exist (e.g. legacy account that pre-dates // Epic 10 key initialisation, or a half-failed Register). ErrUserKeysMissing = &pkgerrors.StatusError{ Status: http.StatusPreconditionFailed, Code: "user_keys_missing", Message: "user key material not initialized; please re-login", } // (unwrap or provisioning) is attempted but the master-KEK RootKeyProvider // is not wired. This is fail-closed: a server_kek user whose DEK cannot be // unwrapped must not silently fall through to a weaker path. In production // the provider is always wired (app.go constructs apiKeyProv); this fires // only on misconfiguration or in tests that forget to wire it. ErrServerKEKUnavailable = &pkgerrors.StatusError{ Status: http.StatusServiceUnavailable, Code: "server_kek_unavailable", Message: "server key provider is not available; contact an administrator", } // ErrInvalidLLMProvider is returned when LLMProviderData validation // fails (missing provider, missing API key, etc.). ErrInvalidLLMProvider = &pkgerrors.StatusError{ Status: http.StatusBadRequest, Code: "invalid_llm_provider", Message: "invalid LLM provider data", } // ErrWorkspaceNotOwned is returned by binding operations when // the caller does not own the target workspace. Both // "workspace doesn't exist" and "workspace owned by someone // else" map to this single sentinel so the response shape does // not leak workspace existence cross-user. Handlers map to 404. // // The message says only "workspace not found" — NOT "or not // owned by caller" — because a future caller that logs // err.Error() would otherwise leak the same distinction the // type system was designed to hide. Callers that need to // classify the failure mode use errors.Is. ErrWorkspaceNotOwned = &pkgerrors.StatusError{ Status: http.StatusNotFound, Code: "workspace_not_found", Message: "workspace not found", } // ErrCiphertextDecryptFailed is returned when the DEK was // successfully obtained but the stored ciphertext cannot be // decrypted with it (AEAD authentication failure). This is // distinct from ErrDEKUnavailable: the key material is present, // but it does not match the ciphertext. // // Most common cause: the user's DEK was rotated or the user_keys // row was rewritten without re-encrypting the secrets that were // encrypted under the old DEK. Less common: ciphertext corruption, // schema version skew (key_version mismatch), or storage tampering. // // The DEK itself is fine — re-authenticating will not help. ErrCiphertextDecryptFailed = &pkgerrors.StatusError{ Status: http.StatusConflict, Code: "ciphertext_decrypt_failed", Message: "this secret cannot be decrypted with your current encryption key — " + "the ciphertext was likely encrypted with a previous key. " + "Re-create the secret to recover; if you have not changed your password, " + "contact an administrator and reference your audit log.", } )
Sentinel errors returned by the secrets package. Each carries its HTTP status code and user-facing message via StatusError, so the generic error handler (respondWithError in router.go) maps them automatically — no handler-level switch needed.
errors.Is still works for sentinel checks (pointer identity via chain traversal). errors.As can extract the *StatusError for generic typed handling.
Wrapping (`fmt.Errorf("...: %w", ErrSecretNotFound)`) is supported and recommended so the classification survives upstream formatting.
var ( ErrNotStagingEnvelope = errors.New("not a staging envelope") ErrStagingEnvelopeVersion = errors.New("unsupported staging envelope version") ErrStagingEnvelopeAlgorithm = errors.New("unknown staging envelope algorithm") ErrStagingAlgorithmMismatch = errors.New("envelope algorithm does not match this staging provider mode") ErrStagingKeyIDMismatch = errors.New("envelope keyID does not match this provider's key") ErrStagingEnvelopeAuth = errors.New("staging envelope authentication failed") ErrInvalidStagingKeyID = errors.New("staging keyID must be non-empty and contain no ':'") )
var ErrAutoBindingProtected = &pkgerrors.StatusError{ Status: http.StatusConflict, Code: "auto_binding_protected", Message: "auto-binding cannot be removed via unbind; delete the credential or workspace to remove it", }
ErrAutoBindingProtected is returned when a caller attempts to Unbind a credential that is bound via an auto-apply rule (source_type='auto'). Auto-bindings are managed by SeedWorkspaceCredentials and can only be removed by deleting the underlying credential or the workspace.
This sentinel lives in pkg/ (not api/internal/errors) because it is shared between the API server and the agentd daemon. It uses StatusError so the generic error handler maps it to HTTP 409 automatically.
var ErrNotMyCiphertext = errors.New("ciphertext prefix does not match this provider")
ErrNotMyCiphertext is returned by a RootKeyProvider's Decrypt when the ciphertext's prefix does not match the provider's format. It is distinct from ErrDecryptionFailed (crypto.go), which means "the prefix matched but the key was wrong" — a genuine decrypt failure rather than a routing signal.
Used by CompositeProvider (composite_provider.go) to dispatch Decrypt across multiple providers without false-positive error logs: a provider returning ErrNotMyCiphertext tells the composite "try the next one," while ErrDecryptionFailed tells it "this row is mine but corrupt — stop." Without this distinction, a multi-provider decrypt would log a spurious failure for every provider that didn't match the prefix.
Plain sentinel (not *StatusError) because this is an internal routing signal — it never reaches the HTTP layer.
var ErrRevisionConvergeFailed = errors.New("workspace secret revision failed to converge")
ErrRevisionConvergeFailed is returned by RevisionStore.EnsureRevision when the conditional seq mint did not converge within its bounded retry budget — a concurrent writer kept winning the race. The caller should treat the revision as unavailable (surface, retry later); never fabricate a seq locally (the DB row is the single writer).
Plain sentinel (not *StatusError) because this is an internal consistency signal; it surfaces as a generic 500 at the HTTP boundary.
var MetadataRequirementsBySecretType = map[SecretType][]string{ SecretTypeAPIKey: {}, SecretTypeLLMProvider: {}, SecretTypeSSHKey: {"key_type"}, SecretTypeGitCredential: {}, SecretTypeSecretFile: {"mount_path"}, SecretTypeEnvSecret: {"var_name"}, }
MetadataRequirementsBySecretType is a self-documenting map of which metadata keys each secret type requires. Surfaced in error responses (Bug 7 in worklog 0085) so callers don't have to reverse-engineer the schema from 400s.
var ValidKinds = []string{
"openai",
"anthropic",
"google",
"opencode",
"bedrock",
"azure_openai",
"vertex",
"cohere",
"mistral",
"perplexity",
"groq",
"xai",
"openrouter",
"together",
"openai_compatible",
}
ValidKinds is the canonical SDK-class enum. Adding a new kind requires a coordinated migration that extends the DB CHECK constraint.
Order matches the CHECK constraint declaration in the migration so the two are visually aligned during review.
var ValidSecretTypes = map[SecretType]bool{ SecretTypeAPIKey: true, SecretTypeLLMProvider: true, SecretTypeSSHKey: true, SecretTypeGitCredential: true, SecretTypeSecretFile: true, SecretTypeEnvSecret: true, SecretTypeMcpServer: true, }
ValidSecretTypes is the set of allowed secret types.
Functions ¶
func ActiveVersionOf ¶
func ActiveVersionOf(p RootKeyProvider) int
ActiveVersionOf returns the active key version of a provider, or 1 if the provider does not implement VersionedProvider (e.g. nil or a future external provider). This is the safe default — version 1 is the initial migration default for all tables.
func AssertHPKEKeyPair ¶ added in v0.33.0
func AssertHPKEKeyPair(p *HPKEKeyPairPayload, prevGeneration int64) error
AssertHPKEKeyPair is the self-contained integrity assert of design 0058 §4.2: derive the public key from the loaded private key and compare against the co-located copy, plus generation monotonicity. prevGeneration is the highest generation previously observed by the caller (0 on first load); equality passes — informer re-lists redeliver the same object, so only a strict decrease is corruption. It never reads across Secrets: the pub Secret belongs to the controller, and the deliberate private-then-pub torn window of a routine rotation must never read as corruption to a peer.
func BatchHash ¶ added in v0.26.0
BatchHash returns the hex SHA-256 over CanonicalBatch — the value-bearing tier used for transport integrity and conditional pulls.
func BuildKEKSecretData ¶ added in v0.33.0
func BuildKEKSecretData(ctx context.Context, kms RootKeyProvider, keyID string, kek []byte) (map[string][]byte, error)
BuildKEKSecretData wraps kek with the KMS provider and returns the `llm-relay-kek` Secret data map: the wrapped blob plus the key id. This is the ONLY writer of that Secret's shape; plaintext KEK bytes never appear in it (pinned by TestKEKNeverInSecret).
func CanonicalBatch ¶ added in v0.26.0
CanonicalBatch returns the canonical JSON encoding of the batch's entries: sorted by (Type, SecretID, Name), with object keys sorted by encoding/json — deterministic under any entry order or map insertion order (epic #1158 invariant I6). Per-entry metadata marshals as stored; key order within it is stable because PG jsonb output and the sole metadata-producing path (mcpServerMetadata, a Go map) both emit sorted keys. The revision stamp is deliberately excluded: canonical bytes describe content, not bookkeeping. The input Batch is never mutated.
func ContextWithDecryptUser ¶
ContextWithDecryptUser returns a context carrying the user ID for decrypt audit attribution. Callers should pass the resulting context to the provider's Decrypt method.
func ContextWithMatchedSigningKey ¶
ContextWithMatchedSigningKey carries the JWT signing key that validated the caller's token, so PostgresSecretProvider.Encrypt/Decrypt can pass it through to KeyService.GetDEK for durable-DEK rehydrate (Epic 56). Pass nil for API-key / sessionless paths — KeyService.GetDEK will surface ErrDEKUnavailable when rehydrate is needed.
Note: this is the SecretProvider's own typed-context key, NOT the gin.Context value set by AuthMiddleware. Handlers extract from gin and stash here before invoking provider methods.
Current state (PR #421): no production caller sets this value because PostgresSecretProvider itself has no production caller — *SecretService (not *PostgresSecretProvider) is the live path for user-secret CRUD. The matched-key plumbing is preserved on the SecretProvider interface for symmetry with the design doc and so a future revival of the SecretProvider path inherits Epic 56 rehydrate by setting this key; without that revival it is harmless dead code, not a regression.
func ContextWithSessionID ¶
ContextWithSessionID adds a session ID to the context for the SecretProvider.
func DecryptSecret ¶
func DeriveKEKFromKey ¶
func DeriveKEKFromPassword ¶
DeriveKEKFromPassword derives a 32-byte KEK from a passphrase and salt via Argon2id. Despite the name it is a generic KDF primitive — the sealed root-key provider uses it to unseal the master key (unsealKeyV0), independent of any user password tier. The user-password DEK tier was removed; this function stays because the sealed provider depends on it.
func DeriveSealedKEK ¶
DeriveSealedKEK derives the KEK used to wrap the sealed root-key file's root key. It domain-separates via HKDF: Argon2id has no native info/context parameter, so HKDF derives a 32-byte sub-salt from the stored salt bound to info, and that sub-salt feeds Argon2id's salt input. Different info values therefore produce cryptographically independent KEKs for an identical passphrase + salt, while retaining Argon2id's memory-hardness against the (typically low-entropy) passphrase. See US-50.11.
func DeriveServerKey ¶ added in v0.34.2
DeriveServerKey derives the 32-byte purpose-scoped server key from master KEK material via HKDF-SHA256 with ServerKeyHKDFSalt. Each purpose string ("provider-credentials", "org-credentials", "master-kek", "dek-cache", …) yields a cryptographically independent key.
This is the single derivation shared by the API server's per-purpose providers and the rotate-kek / migrate-kek CLIs (issue #832 consolidated three byte-identical copies into this export; the golden fixtures in testdata/derive_server_key_golden.txt prove the pre-consolidation outputs are preserved byte-for-byte).
Returns nil — never a weak key — when master is shorter than the 32-byte AES-256-GCM minimum. Callers treat nil as a fail-closed configuration error.
func EncryptSecret ¶
func GenerateDEK ¶
func GenerateSalt ¶
func GenerateStagingKEK ¶ added in v0.33.0
GenerateStagingKEK returns fresh 32-byte local KEK material for the `llm-relay-kek` Secret bootstrap. The caller wraps it immediately via BuildKEKSecretData and discards the plaintext.
func HPKEKeyID ¶ added in v0.33.0
HPKEKeyID derives the envelope keyID for an HPKE keypair generation. Both controller (seal) and router (resolve) derive it from the generation counter, so envelope keyIDs and Secret generations cannot drift apart.
func InspectStagingEnvelope ¶ added in v0.34.2
InspectStagingEnvelope returns the envelope's algorithm discriminator and plaintext keyID without any cryptographic work. The BYO resolve router (US-72.2) uses it to route each cached envelope to the right resolver: `aes-256-gcm` → the KMS-mode provider (one per deployment), `hpke` → the keypair-generation resolver keyed by keyID. The values remain integrity-bound at resolve time — a tampered discriminator fails AEAD authentication in Resolve; this call only reads the routing metadata.
func IsRelayDegrade ¶ added in v0.34.8
func IsRelayDegrade(d *BuildDegrade) bool
IsRelayDegrade reports whether a BuildDegrade belongs to the relay tier (design 0061 §6/M4): the CredentialsStaged condition mirrors the RELAY outcome only — a DEK-tier degrade (dek_unwrap_failed, owner_no_keys) says nothing about relay staging (admin/org providers were still relay-rewritten and delivered) and must NOT surface as a relay-staging condition. The vocabulary lives HERE behind the builder seam: M2's relay_fallback_delivery joined this set (one place), and consumers never enumerate reasons themselves.
func LegacyBatchJSON ¶ added in v0.26.0
LegacyBatchJSON renders the batch as the bare []InjectedSecret array the mixed fleet already consumes (W15): Value becomes Plaintext; Type/Name/Metadata keep their exact wire semantics, including native JSON types inside mcp-server metadata. An empty batch renders as "[]" so old pods CLEAR their live materializations.
func ManifestHash ¶ added in v0.26.0
func ManifestHash(ownerID string, entries []ManifestEntry) string
ManifestHash returns the hex SHA-256 over the canonical manifest lines: the first line is "owner|<ownerID>", then one line per entry sorted by (Type, SecretID, Name), each "secretId\x00version\x00type\x00name\x00<metadata>" where <metadata> is the canonical (sorted-key, normalized) metadata JSON or empty. Deterministic under any input ordering (I6); scoped to the owner so identical entry sets never collide across owners.
func ManifestHashWithRelayRevision ¶ added in v0.34.7
func ManifestHashWithRelayRevision(ownerID string, entries []ManifestEntry, relayRevision string) string
ManifestHashWithRelayRevision is ManifestHash plus the relay-only manifest tier line (US-72.4, design 0058 §4.4): "relay|<revision>" is hashed immediately after the owner line, so a staged-handoff revision change (a token renewed at ~TTL/2) changes the manifest hash and the existing conditional-pull machinery delivers the fresh batch — no pod restart, no new delivery path (K6). An empty relayRevision must yield EXACTLY ManifestHash (the tier degrades cleanly to rows-only).
func ProviderDefaultBaseURL ¶ added in v0.34.4
ProviderDefaultBaseURL returns the default upstream endpoint for a kind, or "" when the kind has none (openai_compatible without an explicit BaseURL, and every non-stageable kind).
func ProviderRelayStageable ¶ added in v0.34.4
ProviderRelayStageable reports whether a credential of this kind can be fronted by the BYO resolve router (plain HTTP + Bearer upstream auth).
func RelayFallbackMetrics ¶ added in v0.34.8
func RelayFallbackMetrics() (fallback, degraded *prometheus.CounterVec)
RelayFallbackMetrics exposes the M2 counters for the API's registration (relay_handoff.go MustRegisters them once at relay-install).
func RelayFrontableProvider ¶ added in v0.34.14
func RelayFrontableProvider(pd LLMProviderData) bool
RelayFrontableProvider reports whether a DECRYPTED provider is one the BYO relay can front: a stageable kind with a resolvable upstream endpoint (explicit BaseURL or a kind default). This is the single predicate the controller's staging pass (relayDesiredSet) and the one builder's relay tier share — the staged set and the builder's per-provider fallback classification key identically by construction, so a provider the controller would have staged can never be mistaken for the non-frontable mixed-fleet class (design 0058 worklog D5) at batch time.
func RelayHandoffSecretName ¶ added in v0.34.7
RelayHandoffSecretName derives the handoff Secret's name.
func SealRootKey ¶
func StagedKeyMaterialRules ¶ added in v0.33.0
func StagedKeyMaterialRules(id string, material []byte) []redact.DynamicRule
StagedKeyMaterialRules builds one exact-value rule per plausible encoding of the staged key material (raw, standard base64, URL-safe base64), so a key echoed verbatim or base64-wrapped through a payload is caught even when no static pattern matches its shape.
func StagedKeyRedactionID ¶ added in v0.33.0
StagedKeyRedactionID derives the redaction rule-group ID for an envelope. Both controller (seal time) and router (resolve time) derive it from the envelope alone, so the two sides converge on one ID per staged credential without sharing state, and revocation (either side) removes exactly that credential's rules.
func ValidateKind ¶
ValidateKind returns nil if kind is one of the recognized SDK-class enum values, otherwise an error describing the rejection. Empty kind is rejected with a distinct message so the handler can map both cases to HTTP 400 with a clear field-specific error.
func ValidateModelAllowlist ¶ added in v0.34.18
ValidateModelAllowlist rejects model_allowlist entries that can never reference a real catalog model: empty or whitespace-affected ids (#1575 ask 2 — mis-form protection moved from a silent delivery-time strip to a real create/update error). The literal "default" is NOT rejected: providers legitimately serve models named "default" (routers/aliases commonly do), and delivery decides it against the live catalog.
func ValidateSlug ¶
ValidateSlug returns nil if slug matches the canonical slug regex, otherwise an error describing the rejection.
The error message names the constraint (length, charset, anchors) rather than echoing the regex verbatim, which is more useful to API consumers than "regex did not match".
Types ¶
type APIKeyRecord ¶
type APIKeyRecord struct {
ID string
WrappedDEK []byte
KekSalt []byte
KeyCiphertext []byte
DecryptAccess bool
}
APIKeyRecord is the subset of API key data needed for DEK re-wrap.
type APIKeyStore ¶
type APIKeyStore interface {
ListAPIKeysWithDecrypt(ctx context.Context, userID string) ([]*APIKeyRecord, error)
UpdateAPIKeyDEK(ctx context.Context, keyID string, wrappedDEK, kekSalt []byte, synced bool) error
}
APIKeyStore abstracts database operations for API key DEK re-wrap.
type AWSKMSProvider ¶ added in v0.3.0
type AWSKMSProvider struct {
// contains filtered or unexported fields
}
AWSKMSProvider implements RootKeyProvider using AWS KMS for encrypt/decrypt operations. The key material never leaves AWS — every Encrypt and Decrypt call is a network round-trip to the KMS API. This converts an API-pod RCE from "permanent KEK exfiltration" to "ephemeral compromise bounded by the RCE window" (Epic 57 US-57.1, threat-model row 2.4).
Auth is via file-mounted static AWS credentials (D2), not IRSA — narrower trust surface per US-50.1's file-mount pattern.
One provider instance holds one KMS key ID. Per-purpose domain separation (D4) is achieved by constructing multiple instances, one per purpose, each with its own key ARN. The chart exposes per-purpose key ARN configuration.
func NewAWSKMSProvider ¶ added in v0.3.0
func NewAWSKMSProvider(client *kms.Client, keyID string) *AWSKMSProvider
NewAWSKMSProvider constructs a provider from an SDK client and KMS key ID (full ARN, e.g. "arn:aws:kms:us-east-1:123:key/abc-def"). The client must be pre-configured with credentials, region, and (optionally) a custom endpoint for testing.
func (*AWSKMSProvider) Decrypt ¶ added in v0.3.0
Decrypt strips the aws-kms:v1: prefix and calls KMS Decrypt. A foreign prefix returns ErrNotMyCiphertext so CompositeProvider can route to the next provider. A matching prefix with a corrupt base64 body returns ErrDecryptionFailed (same semantics as the local providers).
Unlike the local providers, there is NO legacy un-prefixed fallback path. KMS ciphertexts always carry the aws-kms:v1: prefix from day one — there are no pre-US-57.1 KMS rows to be backward-compatible with.
type AdminKeyDeriver
deprecated
AdminKeyDeriver derives a server-side encryption key for admin credentials. The label parameter scopes the derived key (e.g. "provider-credentials"). Returns nil when LLMSAFESPACES_MASTER_SECRET is not set.
Deprecated: US-50.2 unifies admin/org credential crypto under RootKeyProvider. New code must not use this type; it is retained for one release so callers can fall back to the legacy path if a production issue surfaces. Removed in a follow-up release. See design/stories/epic-50-master-kek-hardening/README.md.
type AsyncAuditLogger ¶
type AsyncAuditLogger struct {
// contains filtered or unexported fields
}
AsyncAuditLogger wraps a SecretStore and logs audit entries asynchronously. The hot path (Log) never blocks: a full channel drops the entry and increments DroppedCount, which operators can scrape via Stats() and surface as an alert. Drains-on-Stop are idempotent (Stop may be called multiple times without panicking).
AsyncAuditLogger is itself a SecretStore — every CRUD method delegates to the wrapped store, while LogAudit is the only method that becomes asynchronous. This means callers can compose:
auditedStore := NewAsyncAuditLogger(pgStore, 4096) svc := NewSecretService(keys, auditedStore)
and every audit write becomes non-blocking without further wiring.
func NewAsyncAuditLogger ¶
func NewAsyncAuditLogger(store SecretStore, bufSize int, logger pkginterfaces.LoggerInterface) *AsyncAuditLogger
NewAsyncAuditLogger creates an async audit logger with a buffered channel. logger is optional — when set, drop+failure events surface at Warn so operators can detect audit-pipeline degradation.
func (*AsyncAuditLogger) AddBindings ¶
func (*AsyncAuditLogger) BindCredentialToAllUserWorkspaces ¶
func (l *AsyncAuditLogger) BindCredentialToAllUserWorkspaces(ctx context.Context, credentialID, userID string) error
func (*AsyncAuditLogger) CreateSecret ¶
func (l *AsyncAuditLogger) CreateSecret(ctx context.Context, secret *UserSecret) error
func (*AsyncAuditLogger) CurrentRevision ¶ added in v0.26.0
func (l *AsyncAuditLogger) CurrentRevision(ctx context.Context, workspaceID string) (int64, string, bool, error)
CurrentRevision delegates to the inner store's revision row when it implements RevisionStore (production wraps PgSecretStore). Required so the batch builder's RevisionStore assertion succeeds through the AsyncAuditLogger wrapper.
func (*AsyncAuditLogger) DeleteSecret ¶
func (l *AsyncAuditLogger) DeleteSecret(ctx context.Context, userID, secretID string) error
func (*AsyncAuditLogger) EnsureRevision ¶ added in v0.26.0
func (l *AsyncAuditLogger) EnsureRevision(ctx context.Context, workspaceID, manifestHash string) (int64, error)
EnsureRevision delegates to the inner store's revision mint when it implements RevisionStore.
func (*AsyncAuditLogger) GetBindings ¶
func (l *AsyncAuditLogger) GetBindings(ctx context.Context, workspaceID string) ([]*UserSecret, error)
func (*AsyncAuditLogger) GetBindingsForSecret ¶
func (*AsyncAuditLogger) GetSecret ¶
func (l *AsyncAuditLogger) GetSecret(ctx context.Context, userID, secretID string) (*UserSecret, error)
func (*AsyncAuditLogger) GetSecretByName ¶
func (l *AsyncAuditLogger) GetSecretByName(ctx context.Context, userID, name string) (*UserSecret, error)
func (*AsyncAuditLogger) GetWorkspaceCredentials ¶
func (l *AsyncAuditLogger) GetWorkspaceCredentials(ctx context.Context, workspaceID string) ([]CredentialBinding, error)
func (*AsyncAuditLogger) GetWorkspaceMCPServers ¶ added in v0.7.0
func (l *AsyncAuditLogger) GetWorkspaceMCPServers(ctx context.Context, workspaceID string) ([]MCPServerBindingRow, error)
GetWorkspaceMCPServers delegates to the inner store's MCP server query. Epic 53: required so loadMCPServers works through the AsyncAuditLogger wrapper used in production (app.go wraps PgSecretStore in AsyncAuditLogger).
func (*AsyncAuditLogger) HasUserProviderCredential ¶
func (*AsyncAuditLogger) ListGlobalDefaultSecrets ¶
func (l *AsyncAuditLogger) ListGlobalDefaultSecrets(ctx context.Context, userID string) ([]*UserSecret, error)
func (*AsyncAuditLogger) ListSecrets ¶
func (l *AsyncAuditLogger) ListSecrets(ctx context.Context, userID string) ([]*UserSecret, error)
func (*AsyncAuditLogger) LogAudit ¶
func (l *AsyncAuditLogger) LogAudit(_ context.Context, entry *AuditEntry) (retErr error)
LogAudit on AsyncAuditLogger never blocks and never panics, even after Stop(). Entries are sent to the background goroutine via a buffered channel; a full channel drops the entry and increments the drop counter.
There is a small race window between the closed-flag check and the channel send where a concurrent Stop could close the channel out from under a sender. The deferred recover catches the resulting "send on closed channel" panic, increments the drop counter, and returns normally. Without the recover, a request emitting an audit entry concurrently with shutdown would crash the process.
The returned error is always nil — failures are observable via Stats() and the logger.
func (*AsyncAuditLogger) QueryAudit ¶
func (l *AsyncAuditLogger) QueryAudit(ctx context.Context, userID string, query AuditQuery) ([]*AuditEntry, error)
func (*AsyncAuditLogger) ReEncryptUserSecrets ¶
func (*AsyncAuditLogger) SeedWorkspaceCredentials ¶
func (*AsyncAuditLogger) SetBindings ¶
func (*AsyncAuditLogger) Stats ¶
func (l *AsyncAuditLogger) Stats() AsyncAuditStats
Stats returns a snapshot of the dropped / written / failed counters. Safe to call concurrently with logging.
func (*AsyncAuditLogger) Stop ¶
func (l *AsyncAuditLogger) Stop()
Stop drains the channel and waits for completion. Idempotent: a second call returns immediately without panicking on the already- closed channel. Sets the closed flag BEFORE closing the channel so any concurrent LogAudit invocations see closed=true and take the drop path rather than panicking on a send-to-closed-channel.
stopCtx is canceled AFTER the worker drains so a stuck-on-DB LogAudit eventually returns. There is still a small window between the closed-flag check and the channel send where a concurrent Stop could close the channel out from under a sender; the deferred recover in LogAudit catches that panic.
func (*AsyncAuditLogger) SyncGlobalDefaultBindings ¶ added in v0.34.11
func (*AsyncAuditLogger) UpdateSecret ¶
func (l *AsyncAuditLogger) UpdateSecret(ctx context.Context, secret *UserSecret) error
func (*AsyncAuditLogger) UpsertFreeTierCredential ¶
func (l *AsyncAuditLogger) UpsertFreeTierCredential(ctx context.Context, ciphertext []byte) error
type AsyncAuditStats ¶
type AsyncAuditStats struct {
Dropped uint64 // entries dropped because the channel was full
Written uint64 // entries successfully persisted
Failed uint64 // entries that reached the worker but the store rejected
}
AsyncAuditStats is the snapshot returned by AsyncAuditLogger.Stats.
type AuditEntry ¶
type AuditEntry struct {
ID int64 `json:"id"`
UserID string `json:"userId"`
Action string `json:"action"`
SecretID *string `json:"secretId,omitempty"`
WorkspaceID *string `json:"workspaceId,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
AuditEntry represents a secret audit log entry.
type AuditQuery ¶
type AuditQuery struct {
Action string
SecretID string
WorkspaceID string
Since *time.Time
Until *time.Time
Limit int
Offset int
}
AuditQuery defines filters for querying the audit log.
type AuditWriter ¶
type AuditWriter interface {
LogAudit(ctx context.Context, entry *AuditEntry) error
}
AuditWriter is the narrow interface for writing audit entries. Implemented by SecretStore, AsyncAuditLogger, and test doubles. US-50.12.
type AuditedProvider ¶
type AuditedProvider struct {
// contains filtered or unexported fields
}
AuditedProvider wraps a RootKeyProvider and logs every Decrypt call to the audit writer (US-50.12). Encrypt is NOT logged — it is not a sensitive read. The audit write is fire-and-forget (goroutine per call) so Decrypt never blocks on the audit pipeline. In production the AuditWriter is AsyncAuditLogger (already buffered), so the goroutine's LogAudit is a near-instant channel send.
What is logged: caller label, user ID (from context or "_system"), key version, timestamp, success/failure. Metadata is a JSON object.
What is NOT logged: plaintext, ciphertext, key material — never.
func NewAuditedProvider ¶
func NewAuditedProvider(inner RootKeyProvider, audit AuditWriter, label string) *AuditedProvider
NewAuditedProvider wraps inner with an audit-decorating provider. label identifies the purpose string for log attribution.
func (*AuditedProvider) ActiveVersion ¶
func (p *AuditedProvider) ActiveVersion() int
ActiveVersion delegates to the inner provider so the wrapper satisfies VersionedProvider. This is load-bearing: production callers invoke ActiveVersionOf(provider) at encrypt time to stamp the key_version column (auth.go, admin_provider_credentials.go, org_credentials.go). Without this delegation, wrapping with NewAuditedProvider would silently downgrade every key_version to the default 1, corrupting rotation tracking.
type AutoApplyRule ¶
AutoApplyRule is a row from credential_auto_apply (exported for handler use).
type Batch ¶ added in v0.26.0
type Batch struct {
Entries []BatchEntry `json:"entries"`
Revision BatchRevision `json:"revision"`
}
Batch is the canonical value-bearing secret batch for a workspace, stamped with its two-tier revision (design 0057 R1). Entries keep construction order (the legacy wire order); hashing goes through CanonicalBatch, which sorts.
type BatchEntry ¶ added in v0.26.0
type BatchEntry struct {
SecretID string `json:"secretId"`
Version int64 `json:"version"`
Type SecretType `json:"type"`
Name string `json:"name"`
Value string `json:"value"`
Metadata json.RawMessage `json:"metadata,omitempty"`
}
BatchEntry is one identified, versioned secret in a workspace batch — the W12 entry contract: (secret_id, version, type, name, value) plus stored (never derived) metadata where the type requires it. No timestamps, no derived content.
type BatchRevision ¶ added in v0.26.0
type BatchRevision struct {
Seq int64 `json:"seq"`
ManifestHash string `json:"manifestHash"`
BatchHash string `json:"batchHash"`
}
BatchRevision is the two-tier revision of a built batch: Seq + ManifestHash identify the intended (decrypt-free) set as minted by the workspace revision row; BatchHash covers the actual value-bearing bytes for transport integrity and conditional pulls.
type BindingsMutationResult ¶
type BindingsMutationResult struct {
// LLMProviderAffected is true if any llm-provider secret was added or removed,
// OR if the diff could not be computed (conservative fallback).
LLMProviderAffected bool
AddedTypes []string
RemovedTypes []string
}
BindingsMutationResult describes what changed in a SetBindings or AddBindings call. Returned as a value (not pointer) so callers never need a nil check.
type BindingsResponse ¶
type BindingsResponse struct {
Bindings []BoundSecret `json:"bindings"`
}
BindingsResponse is the API response for workspace bindings.
type BoundSecret ¶
type BoundSecret struct {
SecretID string `json:"secretId"`
Name string `json:"name"`
Type SecretType `json:"type"`
}
BoundSecret is a secret reference in a binding response.
type BuildDegrade ¶ added in v0.26.0
type BuildDegrade struct {
Reason string
}
BuildDegrade is the loud-degrade companion of a built batch: a machine-readable reason the batch lacks user-DEK entries. Never silent (I10) — the accompanying audit events name the workspace and the underlying error.
type CiphertextAudit ¶ added in v0.4.0
type CiphertextAudit struct {
Table string
Total int
Target int // rows whose prefix matches the migration target
Legacy int // un-prefixed pre-US-57.1 rows
Local int // lkms:v1:-prefixed rows (local provider, post-US-57.1)
OtherKMS int // rows with a KMS prefix that isn't the target
}
CiphertextAudit summarizes the prefix distribution of one table after (or during) a migration. Used as the gate for removing the static fallback from the composite provider.
func (CiphertextAudit) IsComplete ¶ added in v0.4.0
func (a CiphertextAudit) IsComplete() bool
IsComplete returns true when every row in the table is on the target KMS provider. This is the safe-to-remove-static-fallback condition: the composite's static fallback can decrypt only legacy + lkms rows; any non-zero Legacy/Local/OtherKMS count means the fallback is still load-bearing.
Equivalent to `a.Target == a.Total` — when Total is 0, Target is also 0 (an empty table is trivially complete). The simpler form reads unambiguously without relying on && / || precedence.
func (CiphertextAudit) Outstanding ¶ added in v0.4.0
func (a CiphertextAudit) Outstanding() int
Outstanding returns the count of rows the static fallback still owns (Legacy + Local + OtherKMS). Equivalent to Total - Target but named for the operational question.
type CiphertextClass ¶ added in v0.4.0
type CiphertextClass int
CiphertextClass categorizes a row's ciphertext by which provider wrote it.
const ( // ClassLegacy is an un-prefixed raw blob — the pre-US-57.1 production // format produced by Static/Sealed providers before the composite added // self-identifying prefixes. ClassLegacy CiphertextClass = iota // ClassLocal is an `lkms:v1:`-prefixed ciphertext written by a local // provider (Static/Sealed) after US-57.1. ClassLocal // ClassAWSKMS is an `aws-kms:v1:`-prefixed ciphertext from AWSKMSProvider. ClassAWSKMS // ClassGCPKMS is a `gcp-kms:v1:`-prefixed ciphertext from GPCKMSProvider. ClassGCPKMS )
func ClassifyCiphertext ¶ added in v0.4.0
func ClassifyCiphertext(ciphertext []byte) CiphertextClass
ClassifyCiphertext inspects a ciphertext's prefix and returns the provider class that wrote it. Side-effect-free — does not call any provider, does not decrypt, does not touch the network. Safe to call on every row in the database at any time.
Classification is by prefix only. A corrupt ciphertext with a valid prefix (e.g. `aws-kms:v1:` followed by garbage) still classifies as ClassAWSKMS — that's the same ambiguity Decrypt has, and the audit's job is prefix accounting, not integrity checking. Integrity is the decrypt path's responsibility.
func (CiphertextClass) String ¶ added in v0.4.0
func (c CiphertextClass) String() string
String returns the human-readable class name for log/CLI output.
type CompositeProvider ¶ added in v0.3.0
type CompositeProvider struct {
// contains filtered or unexported fields
}
CompositeProvider dispatches Encrypt/Decrypt across one primary and zero or more fallback RootKeyProvider instances. It exists to enable zero- downtime migration between provider types (local static/sealed ↔ cloud KMS) by routing Decrypt based on the ciphertext's self-identifying prefix.
Design (Epic 57 US-57.1, D3):
Encrypt always delegates to the primary. New writes carry the primary's prefix; the composite does not transform the output.
Decrypt iterates [primary, fallback...], calling each provider's Decrypt. Dispatch is governed by the providers themselves: each returns ErrNotMyCiphertext when the ciphertext's prefix doesn't match (see unwrapPrefix in root_key.go), ErrDecryptionFailed when the prefix matched but the key was wrong, or success. The composite stops at the first non-ErrNotMyCiphertext result — whether success or genuine decrypt failure — because continuing past a prefix match would produce spurious calls against every fallback for every corrupt row.
The composite does NOT implement VersionedProvider. Versioning is per-provider: Static has it; KMS providers track versions cloud- side. Callers needing ActiveVersion() type-assert on the primary via ActiveVersionOf, which returns 1 for non-VersionedProvider primaries — the safe default for the key_version column.
Thread safety: providers are constructed once at boot and never reassigned; the composite holds no mutable state. Safe for concurrent use after construction.
func NewCompositeProvider ¶ added in v0.3.0
func NewCompositeProvider(primary RootKeyProvider, fallbacks ...RootKeyProvider) (*CompositeProvider, error)
NewCompositeProvider constructs a composite from a required primary and zero or more optional fallbacks. Fallbacks are tried in the order supplied. Returns an error if primary is nil — a composite with no primary has no Encrypt target and would panic on the first write.
Returns an error if any fallback in the variadic tail is nil. A nil fallback would panic on Decrypt the first time dispatch reached that slot (typically the first foreign-prefix ciphertext under traffic), turning a "remove the static mount after migration" operator action into an API-pod crash loop. Failing closed at construction surfaces the misconfiguration at boot.
Callers that want a primary with no fallbacks should pass no variadic args at all: NewCompositeProvider(primary). The composite's Decrypt then routes only by the primary's prefix; foreign ciphertexts surface as ErrNotMyCiphertext — the same behavior the composite already exhibits when every fallback has been exhausted.
func (*CompositeProvider) Decrypt ¶ added in v0.3.0
Decrypt iterates primary first, then fallbacks in registration order. It returns the first non-ErrNotMyCiphertext result — whether success or a genuine decrypt failure. If every provider returns ErrNotMyCiphertext (no provider recognizes the ciphertext's prefix), the composite returns ErrNotMyCiphertext so the caller knows the row is unroutable rather than corrupt.
The "stop on first non-routing result" invariant is load-bearing: a provider whose prefix matched but whose key didn't (ErrDecryptionFailed) must terminate dispatch, otherwise a single corrupt row would generate N-1 spurious decrypt calls (one per remaining provider) on every read. TestCompositeProvider_DecryptStopsOnPrefixMatch_NotErrNotMyCiphertext pins this behavior.
type CreateSecretRequest ¶
type CreateSecretRequest struct {
Name string `json:"name" binding:"required,min=1,max=255"`
Type SecretType `json:"type" binding:"required"`
Value string `json:"value" binding:"required"` // plaintext, encrypted before storage
Metadata json.RawMessage `json:"metadata"`
GlobalDefault bool `json:"globalDefault,omitempty"`
}
CreateSecretRequest is the API request for creating a secret.
type CredentialBinding ¶
type CredentialBinding struct {
ID string
OwnerType string
OwnerID string
Kind string // SDK-class enum (openai, anthropic, openai_compatible, ...)
Slug string // slug-safe per-owner identity; the agent-config.json provider-map key
Ciphertext []byte
KeyVersion int
Version int64 // value-version counter (US-70.2): create = 1, every value-affecting mutation bumps
ModelAllowlist []string
ModelContextLimits map[string]int // model_id → context window size in tokens
ModelOutputLimits map[string]int // model_id → max output tokens
SourceType string // "explicit" or "auto"
WithinPriority int
}
CredentialBinding is a joined row from workspace_credential_bindings + provider_credentials.
Identity model (Epic 55):
- Kind: SDK-class discriminator. Enum constrained by the DB CHECK. Selects the adapter that opencode loads (openai, anthropic, bedrock, openai_compatible, ...). Multiple credentials of the same Kind can coexist (e.g. two OpenAI-compatible LiteLLM endpoints) — Kind does NOT have to be unique per owner.
- Slug: stable per-owner identity. UNIQUE(owner_type, owner_id, slug) in the DB. This is also the literal key used in agent-config.json's provider map, so opencode sessions persist this value as providerID. A user with two `openai_compatible` credentials picks distinct slugs (e.g. "litellm-prod-us-west" and "litellm-prod-eu-central") to disambiguate them on the wire.
type CredentialBindingInfo ¶
type CredentialBindingInfo struct {
WorkspaceID string `json:"workspaceId"`
SourceType string `json:"sourceType"` // "explicit" or "auto"
}
CredentialBindingInfo is a minimal binding row used for the ListBindings API. It carries the workspace ID and the source type so the UI can distinguish auto-seeded bindings (which cannot be manually unbound) from explicit ones.
type CredentialRow ¶
type CredentialRow struct {
ID string
OwnerType string
OwnerID string
Name string // display label, free-form
Kind string // SDK-class enum
Slug string // per-owner unique identity; reaches opencode as providerID
Ciphertext []byte
KeyVersion int
ModelAllowlist []string
ModelContextLimits map[string]int // model_id → context window size in tokens
ModelOutputLimits map[string]int // model_id → max response tokens
Version int64 // value-version counter (US-70.2): create = 1, every value-affecting mutation bumps
CreatedAt time.Time
UpdatedAt time.Time
}
CredentialRow is the DB row shape for all provider credential types. Maps to the provider_credentials table; owner_type discriminates the owner scope ("admin", "user", "org") and owner_id the concrete owner ("_platform", a user id, or an org id). Defined here to avoid an import cycle (handlers → secrets → handlers).
Epic 55 identity model:
- Kind is the SDK-class enum (openai, anthropic, openai_compatible, ...). Multiple credentials of the same Kind can exist per owner.
- Slug is the per-owner unique identity AND the literal provider-map key in agent-config.json. Slug-safe regex enforced by the DB CHECK.
- Name is the free-form display label shown in the UI.
type CredentialStore ¶
type CredentialStore interface {
// GetWorkspaceCredentials returns all credential bindings for a workspace,
// ordered by: (source_type='explicit') DESC, within_priority DESC, created_at ASC.
GetWorkspaceCredentials(ctx context.Context, workspaceID string) ([]CredentialBinding, error)
// UpsertFreeTierCredential atomically upserts the platform free-tier
// credential row and its auto-apply rule in a single transaction.
UpsertFreeTierCredential(ctx context.Context, ciphertext []byte) error
// SeedWorkspaceCredentials inserts credential bindings for a new workspace:
// admin auto-apply rules (all, user target types), user-owned credentials,
// and org auto-apply rules + org credentials when orgID is non-nil.
SeedWorkspaceCredentials(ctx context.Context, workspaceID, userID string, orgID *string) error
// BindCredentialToAllUserWorkspaces binds a credential to every workspace
// owned by userID. Called on credential create to maintain the invariant
// that all credentials are bound to all of a user's workspaces.
BindCredentialToAllUserWorkspaces(ctx context.Context, credentialID, userID string) error
// HasUserProviderCredential returns true if the user owns a credential
// with the given slug. The lookup is per-slug because slug is the
// per-owner unique identity (Epic 55); kind alone is not unique per
// owner.
HasUserProviderCredential(ctx context.Context, userID, slug string) (bool, error)
// GetWorkspaceMCPServers returns all bound+enabled MCP servers for a
// workspace, carrying ciphertext for downstream decryption. Epic 53.
GetWorkspaceMCPServers(ctx context.Context, workspaceID string) ([]MCPServerBindingRow, error)
}
CredentialStore abstracts database operations for provider credentials.
type DEKCache ¶
type DEKCache interface {
CacheDEK(ctx context.Context, sessionID string, dek []byte, ttl time.Duration) error
GetDEK(ctx context.Context, sessionID string) ([]byte, error)
EvictDEK(ctx context.Context, sessionID string) error
}
DEKCache abstracts session-based DEK caching (Redis).
type GPCKMSProvider ¶ added in v0.4.0
type GPCKMSProvider struct {
// contains filtered or unexported fields
}
GPCKMSProvider implements RootKeyProvider using Google Cloud KMS for encrypt/decrypt operations. Parallel to AWSKMSProvider — same threat-model properties (KEK never leaves Google's HSM), different SDK and auth.
Auth is via file-mounted service-account JSON (D2: file-mount, not Workload Identity Federation — narrower trust surface).
One provider instance holds one KMS key resource name. Per-purpose domain separation (D4) is achieved by constructing multiple instances, one per purpose.
func NewGPCKMSProvider ¶ added in v0.4.0
func NewGPCKMSProvider(client *kms.KeyManagementClient, keyName string) *GPCKMSProvider
NewGPCKMSProvider constructs a provider from an SDK client and KMS key resource name (e.g. "projects/my-project/locations/us-east1/keyRings/ my-ring/cryptoKeys/my-key").
type HPKEKeyPairPayload ¶ added in v0.33.0
type HPKEKeyPairPayload struct {
PrivateKey []byte `json:"privateKey"`
PublicKey []byte `json:"publicKey"`
Generation int64 `json:"generation"`
}
HPKEKeyPairPayload is the `llm-relay-hpke-key` Secret payload: the private key, its co-located public key copy, and the monotonic generation counter. Design 0058 §4.2: payloads are generation-tagged and self-contained so a replica can assert integrity without reading any other Secret.
func GenerateHPKEKeyPairPayload ¶ added in v0.33.0
func GenerateHPKEKeyPairPayload(generation int64) (*HPKEKeyPairPayload, error)
GenerateHPKEKeyPairPayload generates a fresh keypair at the given generation (first boot uses 1; rotation increments).
func ParseHPKEKeyPairPayload ¶ added in v0.33.0
func ParseHPKEKeyPairPayload(data []byte) (*HPKEKeyPairPayload, error)
func (*HPKEKeyPairPayload) Marshal ¶ added in v0.33.0
func (p *HPKEKeyPairPayload) Marshal() ([]byte, error)
func (*HPKEKeyPairPayload) Public ¶ added in v0.33.0
func (p *HPKEKeyPairPayload) Public() HPKEPubPayload
Public returns the pub-Secret view of this keypair payload.
type HPKEPubPayload ¶ added in v0.33.0
type HPKEPubPayload struct {
PublicKey []byte `json:"publicKey"`
Generation int64 `json:"generation"`
}
HPKEPubPayload is the `llm-relay-hpke-pub` Secret payload: the public key the controller seals against, plus the matching generation.
func ParseHPKEPubPayload ¶ added in v0.33.0
func ParseHPKEPubPayload(data []byte) (*HPKEPubPayload, error)
func (*HPKEPubPayload) Marshal ¶ added in v0.33.0
func (p *HPKEPubPayload) Marshal() ([]byte, error)
type HPKEStagingResolver ¶ added in v0.33.0
type HPKEStagingResolver struct {
// contains filtered or unexported fields
}
HPKEStagingResolver is the dev-mode router capability: open envelopes sealed to the router's keypair. Dual-key resolve windows and prior-key retention are router-side machinery (US-72.2) built on this primitive.
func NewHPKEStagingResolver ¶ added in v0.33.0
func NewHPKEStagingResolver(privateKey []byte, keyID string, redaction StagedKeyRedactor) (*HPKEStagingResolver, error)
func (*HPKEStagingResolver) KeyID ¶ added in v0.33.0
func (r *HPKEStagingResolver) KeyID() string
type HPKEStagingSealer ¶ added in v0.33.0
type HPKEStagingSealer struct {
// contains filtered or unexported fields
}
HPKEStagingSealer is the dev-mode controller capability: seal provider keys to the router's public key. It holds no private key and implements no Resolve — the controller cannot decrypt what it sealed.
func NewHPKEStagingSealer ¶ added in v0.33.0
func NewHPKEStagingSealer(publicKey []byte, keyID string, redaction StagedKeyRedactor) (*HPKEStagingSealer, error)
func (*HPKEStagingSealer) KeyID ¶ added in v0.33.0
func (s *HPKEStagingSealer) KeyID() string
type InjectedSecret ¶
type InjectedSecret struct {
Type SecretType `json:"type"`
Name string `json:"name"`
Metadata json.RawMessage `json:"metadata"`
Plaintext string `json:"plaintext"`
}
InjectedSecret is a single secret entry in the legacy secrets.json / bare-array wire shape that the mixed fleet of pods already consumes. New server-side code builds a Batch and renders it with LegacyBatchJSON; this type is the W15 mixed-fleet pin.
type JWTSession ¶
type JWTSession struct {
JTI uuid.UUID
UserID string
WrappedDEK []byte
KEKSalt []byte
CreatedAt time.Time
ExpiresAt time.Time
}
JWTSession is the in-memory shape of a jwt_sessions row.
Layout matches migration 000045:
jwt_sessions(jti UUID PK, user_id TEXT FK, wrapped_dek BYTEA,
kek_salt BYTEA, created_at TIMESTAMPTZ, expires_at TIMESTAMPTZ)
Rows are written only by pre-US-70.5 logins (the durable write half is deleted); WrappedDEK/KEKSalt are historical columns kept for the age-out window and never unwrapped by current code.
JTI is the canonical UUID form that auth.go generates via uuid.New().String() and embeds in the JWT's "jti" claim.
type JWTSessionJanitor ¶
type JWTSessionJanitor struct {
// contains filtered or unexported fields
}
JWTSessionJanitor periodically prunes expired rows from jwt_sessions so the table stays bounded as login traffic accrues. Mirrors the pattern in handlers.PendingOrgCleaner.
Failure handling: a single tick failure is logged and the next tick retries. We do NOT bail on persistent errors — recovering PG that was briefly unavailable is the same recovery path as a transient network blip, and surfacing the error any other way (panic, channel emit) adds complexity without benefit.
func NewJWTSessionJanitor ¶
func NewJWTSessionJanitor(store JWTSessionStore, interval time.Duration, logger pkginterfaces.LoggerInterface) *JWTSessionJanitor
NewJWTSessionJanitor builds a janitor with the given store and interval. Pass interval=0 to use DefaultJWTSessionJanitorInterval.
func (*JWTSessionJanitor) Run ¶
func (j *JWTSessionJanitor) Run(ctx context.Context)
Run blocks until ctx is canceled. Safe to call exactly once per janitor; mirror PendingOrgCleaner.Run.
type JWTSessionStore ¶
type JWTSessionStore interface {
// DeleteJWTSession removes the row for a specific jti. Used by EvictDEK
// (logout, cache miss handling, etc.) so the durable row does not
// outlive its Redis counterpart.
DeleteJWTSession(ctx context.Context, jti uuid.UUID) error
// DeleteJWTSessionsForUser removes all rows for a user. Used by
// RevokeAllUserSessions (password reset / explicit logout-everywhere).
// Returns the number of rows deleted.
DeleteJWTSessionsForUser(ctx context.Context, userID string) (int64, error)
// DeleteExpiredJWTSessions prunes rows with expires_at < before. The
// janitor goroutine calls this on a ticker. Returns the number of rows
// deleted. Bounded by the idx_jwt_sessions_expires_at index for O(log N)
// scan even at 1M rows.
DeleteExpiredJWTSessions(ctx context.Context, before time.Time) (int64, error)
// ListActiveJWTSessionsForUser returns non-expired rows for userID,
// ordered created_at DESC (most-recent first) and bounded by limit
// (limit <= 0 means unlimited-per-caller-convention, though the
// caller MUST supply a sensible bound in production).
//
// Used by KeyService.GetCachedDEKForUser to enumerate candidate jtis
// for the warm-cache walk (US-70.5: the rows are an index of live
// sessions; the DEK itself is read from the Redis cache only).
//
// "Active" means expires_at is strictly AFTER the store's clock
// (SQL: expires_at > NOW()). A row at the exact expires_at is
// expired — matches DeleteExpiredJWTSessions's < semantics.
//
// Bounded by idx_jwt_sessions_user_id + a filter on expires_at
// (partial index on expires_at could speed the AND but is not
// required at current data scale).
//
// Returns nil (or []) with nil error when no matching rows exist —
// callers use empty to signal "no live session" (ErrDEKUnavailable).
ListActiveJWTSessionsForUser(ctx context.Context, userID string, limit int) ([]*JWTSession, error)
}
JWTSessionStore abstracts the durable jwt_sessions table for tests. US-70.5 deleted the write and rehydrate halves; the remaining surface enumerates live sessions (for the warm-cache walk) and drives the table toward empty (revocation deletes + the janitor) as pre-demolition rows age out.
type KEKMigrationError ¶ added in v0.4.0
KEKMigrationError records a per-row failure.
type KEKMigrationResult ¶ added in v0.4.0
type KEKMigrationResult struct {
Processed int
Failed int
Errors []KEKMigrationError
// LastRowID is the ID of the last row the coordinator successfully
// migrated. The runbook's interrupted-run procedure prints it per table
// so the operator can resume with --resume-from <last-row-id>. The
// cursor deliberately does not advance past a failed row: resuming
// re-attempts every row after the cursor. Empty when no row was
// successfully migrated.
LastRowID string
}
KEKMigrationResult summarizes a migration run.
type KEKRotationError ¶
KEKRotationError records a per-row failure.
type KEKRotationResult ¶
type KEKRotationResult struct {
Processed int
Skipped int // rows already at target version
Failed int
Errors []KEKRotationError
// LastRowID is the ID of the last row the coordinator successfully
// processed (or skipped as already-rotated). The runbook's
// interrupted-run procedure prints it per table so the operator can
// resume an interrupted APPLY run with --resume-from <last-row-id>. The
// cursor deliberately does not advance past a failed row: resuming
// re-attempts every row after the cursor that is still below the target
// version (already-rotated rows are filtered out by the store, so
// nothing is double-rotated). In a dry-run the value is informational
// only — nothing was written, so a subsequent apply run must NOT resume
// from it. Empty when no row was successfully processed.
LastRowID string
}
KEKRotationResult summarizes a rotation run.
type KEKSecretData ¶ added in v0.33.0
KEKSecretData is the parsed `llm-relay-kek` Secret payload.
func ParseKEKSecretData ¶ added in v0.33.0
func ParseKEKSecretData(data map[string][]byte) (KEKSecretData, error)
type KMSStagingProvider ¶ added in v0.33.0
type KMSStagingProvider struct {
// contains filtered or unexported fields
}
KMSStagingProvider is the prod-mode envelope provider. Construction performs the single KMS Decrypt that unwraps the local KEK; Seal and Resolve are local AES-256-GCM (D2: KMS is O(boot), never O(request)). The controller uses Seal; the router uses Resolve; both hold the same trusted-plane KEK.
func NewKMSStagingProvider ¶ added in v0.33.0
func NewKMSStagingProvider(ctx context.Context, kms RootKeyProvider, secretData map[string][]byte, redaction StagedKeyRedactor) (*KMSStagingProvider, error)
func (*KMSStagingProvider) KeyID ¶ added in v0.33.0
func (p *KMSStagingProvider) KeyID() string
type KeyService ¶
type KeyService struct {
// contains filtered or unexported fields
}
KeyService manages user key lifecycle.
func NewKeyService ¶
func NewKeyService(store KeyStore, cache DEKCache) *KeyService
NewKeyService creates a new KeyService.
func (*KeyService) CacheDEK ¶
func (s *KeyService) CacheDEK(ctx context.Context, sessionID string, dek []byte, ttl time.Duration) error
CacheDEK stores a DEK in the session cache. Used by API key auth to cache an unwrapped DEK under a deterministic sessionID.
func (*KeyService) DEKAvailable ¶
func (s *KeyService) DEKAvailable(ctx context.Context, sessionID string) bool
DEKAvailable checks if a DEK is cached for the given session.
func (*KeyService) DeleteDurableSessionsForUser ¶
func (s *KeyService) DeleteDurableSessionsForUser(ctx context.Context, userID string) error
DeleteDurableSessionsForUser removes every jwt_sessions row for a user. Called by auth.Service.RevokeAllUserSessions (password reset, admin force-logout) so rows from pre-US-70.5 logins cannot outlive the user's explicit invalidation of every outstanding session. Best-effort: failure is logged but does not propagate — the Redis revocation markers are already in place and the JWT itself is functionally dead.
Returns nil even on failure — callers do not need to handle the error path; the contract is "drive jwt_sessions toward consistency with the auth-layer revocation, log if we can't".
func (*KeyService) EvictDEK ¶
func (s *KeyService) EvictDEK(ctx context.Context, sessionID string) error
EvictDEK removes the cached DEK for a session AND the durable jwt_sessions row, if one exists (pre-US-70.5 rows age out via the janitor; login no longer writes them). Called on logout / explicit revocation. Non-JTI sessionIDs (API-key sessions like "apikey:hash") only evict the Redis cache — the api_keys table is the durable home for those.
func (*KeyService) GetCachedDEKForUser ¶ added in v0.27.0
func (s *KeyService) GetCachedDEKForUser(ctx context.Context, userID string) (dek []byte, jti string, err error)
GetCachedDEKForUser retrieves the user's DEK from the Redis session cache (K1) without requiring a caller-supplied sessionID. Designed for background paths (keyrewrap recovery, GetDEKServerSide's legacy row heal) that do not run in an authenticated user-request context.
Resolution order (US-70.5, the K3 demolition):
- jwtSessions.ListActiveJWTSessionsForUser(userID, LIMIT) → candidate jtis, most-recent first. If empty → ErrDEKUnavailable.
- For each jti, read the Redis dek:<jti> cache. First hit → return (dek, jti, nil).
- No cache hit on any enumerated jti → ErrDEKUnavailable. The method NEVER re-derives KEKs from durable jwt_sessions rows and never unwraps — a row that could be unwrapped is not a source.
Login populates the cache (K1 survives); login no longer writes jwt_sessions rows (K4 write half removed), so the enumeration reaches pre-demolition rows only — the warm-cache recovery window ages out with them, after which callers see ErrDEKUnavailable and surface their no-source outcome (keyrewrap: unwrappable_no_source).
Rows are bounded (LIMIT jwtSessionUserLookupLimit).
Errors: ErrDEKUnavailable covers every legitimate "no warm cache reachable" case. Genuine infrastructure errors (PG connection failure) are returned verbatim so operators can distinguish debug-worthy outages from expected "user logged out" cases.
func (*KeyService) GetDEK ¶
GetDEK retrieves the DEK for a session from the Redis cache (K1). US-70.5 deleted the durable jwt_sessions rehydrate fallback (K2): a cache miss or cache error surfaces ErrDEKUnavailable, and the client recovers via the soft-unlock endpoint (which re-derives the DEK from user_keys server-side and re-populates the cache).
matchedSigningKey is retained in the signature for call-site stability and ignored — the same convention as UnlockDEK's password parameter.
func (*KeyService) GetDEKServerSide ¶ added in v0.25.1
func (s *KeyService) GetDEKServerSide(ctx context.Context, userID string) (dek []byte, jti string, err error)
GetDEKServerSide unwraps the user's DEK directly from the user_keys record via the master RootKeyProvider — no session, no jwt_sessions walk.
This is the pod-delivery accessor under the "if the pod exists, it has its secrets" contract: the server-KEK-only model made every user DEK server-recoverable at rest, so pod delivery no longer gates on an active session. Suspend/resume, expired jwt_sessions, and logged-out owners all still receive their bound secrets.
Self-heal (legacy rows): when the master provider cannot unwrap record.WrappedDEK — a pre-US-57.1 un-prefixed blob, or a wrap from a key version the provider no longer derives — the method falls back to GetCachedDEKForUser (warm session cache) for the DEK material and opportunistically re-wraps the row at the provider's active version via UpdateWrappedDEK. The heal is login-independent: any background path touching the row converges it to current format, which is exactly the gap that stranded pre-rotation users (their last login predated the re-wrap window, so a login-gated migration could never reach them). A failed heal write is logged, not returned — the DEK is still delivered this boot; the next attempt retries. No warm cache reachable → the unwrap error propagates and the caller degrades to sessionless (loudly).
Returns (dek, jti, error). The jti is a fresh UUID referencing the Redis cache entry this method populates; callers that need a session-indexed handle may use it with GetDEK(jti). The batch builder consumes the returned dek directly.
Failure semantics (each degrades the caller to sessionless):
- rootKeyProvider unwired → ErrServerKEKUnavailable
- no user_keys record → ErrDEKUnavailable (the user never created secrets; the sessionless payload already contains everything that exists for them)
- store/unwrap failure → wrapped error (infra); includes the session-fallback error when the self-heal path also found no session DEK
- cache write failure → error (without the handle, session-indexed consumers cannot reach the DEK; one clean degrade beats per-binding audit noise)
func (*KeyService) InitializeUserKeysServerKEK ¶ added in v0.7.0
func (s *KeyService) InitializeUserKeysServerKEK(ctx context.Context, userID, dekSource string) error
InitializeUserKeysServerKEK provisions a DEK wrapped by the master-KEK RootKeyProvider. The dekSource ("server_kek" or "passkey") distinguishes the auth source; both share the same unwrap path (rootKeyProvider.Decrypt). The store atomically flips users.dek_source alongside the user_keys insert. Fail-closed when no provider is wired.
func (*KeyService) JWTSessionStoreSet ¶
func (s *KeyService) JWTSessionStoreSet() bool
JWTSessionStoreSet reports whether a JWT-session store has been wired. Exposed so app.go wiring + tests can assert post-init invariants without reaching into private state.
func (*KeyService) SetAPIKeyStore ¶
func (s *KeyService) SetAPIKeyStore(store APIKeyStore, provider RootKeyProvider)
SetAPIKeyStore wires the API key store for DEK re-wrap on rotation.
func (*KeyService) SetJWTSessionStore ¶
func (s *KeyService) SetJWTSessionStore(store JWTSessionStore)
SetJWTSessionStore wires the durable jwt_sessions table backing the GetCachedDEKForUser enumeration. Optional — tests and pre-Epic-56 callers may leave it nil; GetCachedDEKForUser then returns ErrDEKUnavailable.
Like SetSecretStore, silent rebinding to a different store is refused: the enumeration would otherwise read from a store that holds no rows for the active session set, surfacing as a wave of ErrDEKUnavailable across all live JWTs. Idempotent same-store calls are allowed.
func (*KeyService) SetLogger ¶
func (s *KeyService) SetLogger(l pkginterfaces.LoggerInterface)
SetLogger installs the logger used to surface non-fatal failures (e.g. cache-evict errors during session revocation). Optional; if nil, those events are silent. Validator pass-5 finding N-3.
Note: the evict-failure log includes the sessionID (JWT jti). The jti is sensitive — an attacker with log read access can correlate user activity across requests, though it does NOT enable token replay (the JWT signature is never logged). Volume is bounded to Redis-outage events. If the log retention crosses a tenant boundary, hash sessionID before logging.
func (*KeyService) SetSecretStore ¶
func (s *KeyService) SetSecretStore(store SecretStore)
SetSecretStore wires the SecretStore used for secret operations. Optional; without it secret operations will fail.
Once set, the store cannot be silently reassigned: a silent SetSecretStore twice with different stores panics; calling with the same store (idempotent re-init) is allowed.
func (*KeyService) UnlockDEK ¶
func (s *KeyService) UnlockDEK(ctx context.Context, userID string, password []byte, sessionID string, ttl time.Duration) error
UnlockDEK unwraps the DEK via the master RootKeyProvider and caches it in Redis (K1). Called during login and by the soft-unlock endpoint. sessionID is the JWT's jti claim. The password parameter is ignored (server-KEK-only model) and retained for call-site stability.
US-70.5 removed the durable jwt_sessions write half: login populates the Redis cache only. Background recovery is GetCachedDEKForUser (warm-cache walk); the jwt_sessions enumeration it uses ages out with pre-demolition rows.
type KeyStore ¶
type KeyStore interface {
GetUserKey(ctx context.Context, userID string) (*UserKeyRecord, error)
CreateUserKey(ctx context.Context, record *UserKeyRecord) error
UpdateWrappedDEK(ctx context.Context, userID string, wrappedDEK []byte, salt []byte, keyVersion int) error
}
KeyStore abstracts database operations for user keys.
type LLMModelConfig ¶
type LLMModelConfig struct {
ID string `json:"id"`
Label string `json:"label,omitempty"`
ContextLimit int `json:"contextLimit,omitempty"`
OutputLimit int `json:"outputLimit,omitempty"`
Attachment *bool `json:"attachment,omitempty"`
}
LLMModelConfig declares an allowlisted model and its display/limit metadata. At minimum, ID is required. Label is shown in pickers when set.
ContextLimit and OutputLimit drive the opencode `limit` block in agent-config.json. opencode's published JSON Schema (https://opencode.ai/config.json) declares the model `limit` object with `"required": ["context", "output"]` and `"additionalProperties": false`. Therefore FormatOpenCodeConfig emits a `limit` block ONLY when BOTH are non-zero — emitting a partial block (only context, or only output) makes opencode reject the entire config with SchemaError: Missing key, which causes every endpoint that calls Config.state() (including POST /session) to return 500.
ContextLimit is the total context window size in tokens. When set together with OutputLimit it is written into agent-config.json as limit.context, which makes opencode's /config/providers return ctx=N, which feeds ModelContextLimit() in agentd → context.total_tokens in /v1/statusz → CRD status.contextTotal → the frontend's "used / total" context bar.
OutputLimit is the maximum response tokens for the model. opencode uses this for compaction sizing. Like ContextLimit it cannot be auto-discovered from a provider's /v1/models endpoint and must be configured explicitly by the workspace/credential owner.
Attachment is the credential owner's declaration that the model accepts image (attachment) input. Tri-state: nil = undeclared (opencode's own catalog decides), true/false = declared truth that overrides the catalog. This is the ONLY way a custom-endpoint model can be marked vision-capable: opencode synthesizes an all-text-only capabilities block for models absent from its bundled models.dev catalog (probe-verified 2026-10-05 against pinned opencode 1.18.x: config `attachment` lands in the catalog's capabilities.attachment; config cannot reach capabilities.input.image). The vision gates (call_with_model's pre-check, the transcript repair) refuse images for models the catalog marks text-only — a synthesized false is indistinguishable from a declared one, so a custom vision model MUST declare attachment:true or image calls on it are refused.
type LLMProviderData ¶
type LLMProviderData struct {
Kind string `json:"kind"`
Slug string `json:"slug"`
APIKey string `json:"apiKey"`
BaseURL string `json:"baseURL,omitempty"`
Models []LLMModelConfig `json:"models,omitempty"`
Default string `json:"default,omitempty"`
SmallModel string `json:"smallModel,omitempty"`
// Delivery-only fields (#1575), never stored in the encrypted blob:
// for custom-endpoint providers the live catalog is fetched POD-SIDE
// by the model enricher after delivery, so the batch builder attaches
// the credential's model allowlist and per-model limits here and the
// enricher filters the FETCHED list against them. Older pods ignore
// the keys (additive JSON).
ModelAllowlist []string `json:"modelAllowlist,omitempty"`
ModelContextLimits map[string]int `json:"modelContextLimits,omitempty"`
ModelOutputLimits map[string]int `json:"modelOutputLimits,omitempty"`
}
LLMProviderData holds structured credentials for one LLM provider. The Plaintext value of an "llm-provider" secret is the JSON encoding of this struct.
Epic 55 identity model:
- Kind is the SDK-class enum (openai, anthropic, openai_compatible, ...). Required. Determines which adapter opencode loads.
- Slug is the per-owner unique identity AND the literal key used in agent-config.json's provider map. opencode persists this as `providerID` on sessions.
APIKey is required. BaseURL is optional; when empty the provider's default endpoint is used. Models is an optional allowlist. When empty or nil all models from the provider are visible (no filtering). When non-empty only the listed models are shown. Default is the model ID to use when no per-session model is specified. SmallModel is the model ID used for lightweight/cheap operations (e.g. summarization).
func (LLMProviderData) Validate ¶
func (d LLMProviderData) Validate() error
Validate checks that required fields are set in LLMProviderData.
type MCPAutoApplyRule ¶ added in v0.7.0
MCPAutoApplyRule is a row from mcp_server_auto_apply.
type MCPServerBindingRow ¶ added in v0.7.0
type MCPServerBindingRow struct {
ServerID string
Name string
Transport string
URL string
Command string
Args []string
TimeoutMs *int
Ciphertext []byte
OwnerType string
KeyVersion int
Version int64 // value-version counter (US-70.2)
Enabled bool
SourceType string
}
MCPServerBindingRow is a joined row from mcp_server_bindings + mcp_servers, used by the injection pipeline. Ciphertext is carried for downstream decrypt.
type MCPServerRow ¶ added in v0.7.0
type MCPServerRow struct {
ID string
OwnerType string
OwnerID string
Name string
Transport string
URL string
Command string
Args []string
TimeoutMs *int
Ciphertext []byte
KeyVersion int
Version int64 // value-version counter (US-70.2): create = 1, every value-affecting mutation bumps
Enabled bool
CreatedAt time.Time
UpdatedAt time.Time
}
MCPServerRow is the DB row shape for mcp_servers.
type ManifestEntry ¶ added in v0.26.0
type ManifestEntry struct {
SecretID string `json:"secretId"`
Version int64 `json:"version"`
Type SecretType `json:"type"`
Name string `json:"name"`
Metadata json.RawMessage `json:"metadata,omitempty"`
}
ManifestEntry is BatchEntry minus the value: the decrypt-free tier. Hashing it lets any replica decide whether a workspace's secret set changed without touching a single ciphertext.
type MigrationCoordinator ¶ added in v0.4.0
type MigrationCoordinator struct {
// contains filtered or unexported fields
}
MigrationCoordinator re-encrypts KEK-protected rows from source (CompositeProvider: KMS-primary + local-fallback) to target (KMS-only provider). It is table-agnostic and supports dry-run + resume-from-cursor for safe operation.
func NewMigrationCoordinator ¶ added in v0.4.0
func NewMigrationCoordinator( store MigrationStore, sources map[string]RootKeyProvider, targets map[string]RootKeyProvider, ) *MigrationCoordinator
NewMigrationCoordinator constructs a coordinator. Each map is keyed by purpose string and should contain the same set of keys.
func (*MigrationCoordinator) AuditAll ¶ added in v0.4.0
func (c *MigrationCoordinator) AuditAll(ctx context.Context, target string) (map[string]CiphertextAudit, error)
AuditAll walks all three KEK-protected tables and returns their audits as a map keyed by table name. The static fallback can be safely removed only when every table's IsComplete() returns true.
func (*MigrationCoordinator) AuditTable ¶ added in v0.4.0
func (c *MigrationCoordinator) AuditTable(ctx context.Context, table, target string) (CiphertextAudit, error)
AuditTable walks the given table and returns the prefix distribution. `target` is the operator's intended final KMS ("aws-kms" or "gcp-kms") and determines which prefix counts as Target vs OtherKMS. No writes, no provider calls, no decrypts — pure prefix accounting. Safe to run at any time, including against a live deployment mid-traffic.
func (*MigrationCoordinator) MigrateAll ¶ added in v0.4.0
func (c *MigrationCoordinator) MigrateAll(ctx context.Context, dryRun bool) (map[string]KEKMigrationResult, error)
MigrateAll re-encrypts all three tables sequentially. The Redis DEK cache is flushed after all tables complete successfully.
func (*MigrationCoordinator) MigrateTable ¶ added in v0.4.0
func (c *MigrationCoordinator) MigrateTable(ctx context.Context, table, resumeFromID string, dryRun bool) (KEKMigrationResult, error)
MigrateTable re-encrypts every row in the given table from source format to target format. If dryRun is true, no writes occur — only counts are reported. resumeFromID allows resuming after an interrupted run.
type MigrationRow ¶ added in v0.4.0
type MigrationRow struct {
ID string
Table string
OwnerType string
Ciphertext []byte
KeyVersion int
}
MigrationRow is a generic row from any KEK-protected table. The coordinator re-encrypts Ciphertext from the source format (any provider — local or KMS) to the target format (KMS only).
type MigrationStore ¶ added in v0.4.0
type MigrationStore interface {
// ListMigrationRows returns rows from the given table, ordered by
// ID ASC, starting after resumeFromID (empty = from the beginning).
// limit caps the batch size (0 = unlimited).
ListMigrationRows(ctx context.Context, table, resumeFromID string, limit int) ([]MigrationRow, error)
// UpdateMigrationRow writes newCiphertext + newKeyVersion atomically
// for the given row. Each call is its own transaction.
UpdateMigrationRow(ctx context.Context, table, rowID string, newCiphertext []byte, newKeyVersion int) error
// FlushDEKCache flushes the Redis DEK cache so stale DEKs (wrapped
// under the old KEK) are evicted.
FlushDEKCache(ctx context.Context) error
}
MigrationStore abstracts the three KEK-protected tables for the migration CLI. Each method returns rows in a consistent order (by ID ASC). Mirrors RotationStore — see rotation.go.
type OrgAutoApplyStore ¶
type OrgAutoApplyStore interface {
BindCredentialToAllOrgWorkspaces(ctx context.Context, credentialID, orgID string) error
CreateOrgAutoApply(ctx context.Context, credentialID, orgID string, withinPriority int) error
ListOrgAutoApply(ctx context.Context, orgID string) ([]*AutoApplyRule, error)
DeleteOrgAutoApply(ctx context.Context, credentialID, orgID string) error
}
OrgAutoApplyStore is the DB interface for org-scoped auto-apply and workspace-binding operations (the credential CRUD itself is served by the owner-parameterized CredentialStore methods on PgSecretStore).
type PgJWTSessionStore ¶
type PgJWTSessionStore struct {
// contains filtered or unexported fields
}
PgJWTSessionStore is the production JWTSessionStore backed by Postgres.
func NewPgJWTSessionStore ¶
func NewPgJWTSessionStore(pool *pgxpool.Pool) *PgJWTSessionStore
NewPgJWTSessionStore creates a new PostgreSQL-backed JWTSessionStore.
func (*PgJWTSessionStore) DeleteExpiredJWTSessions ¶
func (s *PgJWTSessionStore) DeleteExpiredJWTSessions(ctx context.Context, before time.Time) (int64, error)
DeleteExpiredJWTSessions prunes rows whose expires_at is strictly before the provided cutoff. The janitor passes time.Now(); tests pass a fixed clock for determinism. Returns the count for logging and monitoring (a sudden spike in deletes flags a clock skew or a large rotation event).
func (*PgJWTSessionStore) DeleteJWTSession ¶
DeleteJWTSession removes the row for jti. Idempotent: deleting a non-existent row is not an error (the DELETE returns rowsAffected=0).
func (*PgJWTSessionStore) DeleteJWTSessionsForUser ¶
func (s *PgJWTSessionStore) DeleteJWTSessionsForUser(ctx context.Context, userID string) (int64, error)
DeleteJWTSessionsForUser removes every row for userID. Used by RevokeAllUserSessions so a password-reset cascade leaves no durable DEK row behind. Returns the number of rows deleted so the caller can audit-log the magnitude.
func (*PgJWTSessionStore) ListActiveJWTSessionsForUser ¶
func (s *PgJWTSessionStore) ListActiveJWTSessionsForUser(ctx context.Context, userID string, limit int) ([]*JWTSession, error)
ListActiveJWTSessionsForUser returns non-expired rows for userID. See interface godoc for full contract. Uses idx_jwt_sessions_user_id (schema 000001) plus a filter on expires_at > NOW(). At current data scale (thousands of sessions per active user max) the trailing filter on expires_at is a scan of the per-user rows only; no compound index needed. If per-user row counts grow past ~10k a partial index (WHERE expires_at > NOW()) can be added later without changing this query.
The NOW() comparison is inline in the SQL so the database's clock is authoritative — same source of truth the janitor uses when it prunes.
When limit <= 0, no LIMIT clause is added. Callers should always pass a sensible bound (KeyService.GetCachedDEKForUser passes 5, per jwtSessionUserLookupLimit).
type PgKeyStore ¶
type PgKeyStore struct {
// contains filtered or unexported fields
}
PgKeyStore implements KeyStore using PostgreSQL.
func NewPgKeyStore ¶
func NewPgKeyStore(pool *pgxpool.Pool) *PgKeyStore
NewPgKeyStore creates a new PostgreSQL-backed key store.
func (*PgKeyStore) CompareAndSwapWrappedDEK ¶ added in v0.27.0
func (s *PgKeyStore) CompareAndSwapWrappedDEK(ctx context.Context, userID string, expectedWrapped, newWrapped []byte, newVersion int, previous *RetainedWrap) (bool, error)
CompareAndSwapWrappedDEK conditionally rewrites the row's active wrap (US-70.4 ReconcileKeyStore). The UPDATE applies only when the row's current wrapped_dek byte-equals expectedWrapped — the guard against a concurrent legitimate rotation (user rotates while the reconciler heals): the loser sees won=false with nil error and backs off, never retries. On win, the row gains the retained previous wrap (W10: old wrap bytes as ciphertext under the CURRENT KEK) with its retention deadline, and updated_at=now() moves the row to the end of the walk order. rotated_at is stamped alongside — a heal is a wrap rotation in UpdateWrappedDEK's sense, and the column feeds the migration-000030 updated_at backfill semantics.
func (*PgKeyStore) CreateUserKey ¶
func (s *PgKeyStore) CreateUserKey(ctx context.Context, record *UserKeyRecord) error
CreateUserKey stores a user's key material. On conflict (the user already has a row), the row is overwritten — user_keys.user_id is the PRIMARY KEY and a plain INSERT would fail with unique_violation. Overwriting with a freshly-generated DEK (see InitializeUserKeysServerKEK) is exactly the desired reset behavior: the prior wraps and anything encrypted under the prior DEK become permanently undecryptable.
When record.DEKSource == "server_kek" (SSO provisioning, Epic 58) or "passkey" (passkey-only login, Epic 59), the users.dek_source flag is flipped in the SAME transaction as the user_keys insert. The atomicity is load-bearing: if the two writes split, a later GetUserKey JOIN could return a record whose dek_source disagrees with the actual wrap, making the next unlock pick the wrong unwrap method and fail.
func (*PgKeyStore) DeleteExpiredRetainedWraps ¶ added in v0.27.0
DeleteExpiredRetainedWraps NULLs the previous-wrap columns of every row past its retention deadline (US-70.4 ReconcileKeyStore). The active wrap is untouched and updated_at is deliberately NOT bumped — the walk ordering tracks active-wrap lifecycle, not retention bookkeeping. Returns the number of rows cleaned.
func (*PgKeyStore) GetUserKey ¶
func (s *PgKeyStore) GetUserKey(ctx context.Context, userID string) (*UserKeyRecord, error)
func (*PgKeyStore) ListUserKeysForReconcile ¶ added in v0.27.0
func (s *PgKeyStore) ListUserKeysForReconcile(ctx context.Context, limit, offset int) ([]UserKeyReconcileRow, error)
ListUserKeysForReconcile returns one oldest-first walk window of user_keys (US-70.4 ReconcileKeyStore). Oldest = highest risk: rows untouched the longest predate the most KEK rotation windows, which is exactly the mike-class incident shape. updated_at is stamped by every CAS heal, moving healed rows to the END of the walk (they are healthy); the user_id tiebreak keeps the ordering deterministic for offset pagination.
func (*PgKeyStore) UpdateWrappedDEK ¶
func (s *PgKeyStore) UpdateWrappedDEK(ctx context.Context, userID string, wrappedDEK []byte, salt []byte, keyVersion int) error
UpdateWrappedDEK updates the wrapped DEK for a user. When the context carries an active *pgx.Tx (threaded through by SecretStore.ReEncryptUserSecrets via withTx), the UPDATE runs inside that transaction so the user_keys row and the user_secrets re-encrypt commit or roll back atomically. Otherwise the UPDATE runs on the pool directly. See Bug 9 in worklog 0094.
type PgSecretStore ¶
type PgSecretStore struct {
// contains filtered or unexported fields
}
PgSecretStore implements SecretStore using PostgreSQL.
func NewPgSecretStore ¶
func NewPgSecretStore(pool *pgxpool.Pool) *PgSecretStore
NewPgSecretStore creates a new PostgreSQL-backed secret store.
func (*PgSecretStore) AddBindings ¶
func (s *PgSecretStore) AddBindings(ctx context.Context, workspaceID string, secretIDs []string) error
AddBindings atomically adds secretIDs to a workspace's binding set without touching existing bindings. Takes the same advisory lock as SetBindings so the two cannot interleave dangerously.
The INSERT uses ON CONFLICT DO NOTHING so re-binding an already- bound secret is idempotent rather than a constraint violation.
func (*PgSecretStore) BackfillFreeTierBindings ¶
func (s *PgSecretStore) BackfillFreeTierBindings(ctx context.Context) (int64, error)
BackfillFreeTierBindings inserts workspace_credential_bindings for all existing workspaces that lack the free-tier opencode credential binding. Idempotent — uses ON CONFLICT DO NOTHING. Returns the number of rows inserted.
func (*PgSecretStore) BackfillMCPServerAutoApply ¶ added in v0.7.0
func (s *PgSecretStore) BackfillMCPServerAutoApply(ctx context.Context, serverID string) (int64, error)
BackfillMCPServerAutoApply binds an MCP server to all workspaces matching its auto-apply rules. Idempotent. Returns rows inserted.
The workspace match uses workspaces.user_id (for target_type='user') and workspaces.org_id (for target_type='org') directly — both columns exist on the workspaces table. target_type='all' matches every workspace.
func (*PgSecretStore) BindAllOrgCredentialsToOrgWorkspaces ¶
func (s *PgSecretStore) BindAllOrgCredentialsToOrgWorkspaces(ctx context.Context, orgID string) error
BindAllOrgCredentialsToOrgWorkspaces binds every org credential to every non-deleted org workspace. Used after invitation acceptance (F7) to ensure newly-migrated workspaces receive the org's shared credentials immediately. Existing bindings are skipped (ON CONFLICT DO NOTHING). Best-effort: callers should log errors but not fail the user-facing operation.
func (*PgSecretStore) BindCredentialToAllOrgWorkspaces ¶
func (s *PgSecretStore) BindCredentialToAllOrgWorkspaces(ctx context.Context, credentialID, orgID string) error
func (*PgSecretStore) BindCredentialToAllUserWorkspaces ¶
func (s *PgSecretStore) BindCredentialToAllUserWorkspaces(ctx context.Context, credentialID, userID string) error
BindCredentialToAllUserWorkspaces binds a user credential to every workspace owned by userID. Called when a user creates a new personal credential so that the invariant "all credentials bound to all workspaces" is maintained. Idempotent.
func (*PgSecretStore) BindCredentialToWorkspace ¶
func (s *PgSecretStore) BindCredentialToWorkspace(ctx context.Context, credentialID, workspaceID string) error
BindCredentialToWorkspace explicitly binds a credential to a workspace.
func (*PgSecretStore) BindMCPServerToWorkspace ¶ added in v0.7.0
func (s *PgSecretStore) BindMCPServerToWorkspace(ctx context.Context, serverID, workspaceID string) error
BindMCPServerToWorkspace explicitly binds an MCP server to a workspace.
func (*PgSecretStore) CountMCPServersByOwner ¶ added in v0.7.0
func (s *PgSecretStore) CountMCPServersByOwner(ctx context.Context, ownerType, ownerID string) (int, error)
CountMCPServersByOwner returns the number of MCP servers owned by (ownerType, ownerID). Used for plan-tier quota enforcement (user scope).
func (*PgSecretStore) CountWorkspaceMCPServers ¶ added in v0.7.0
func (s *PgSecretStore) CountWorkspaceMCPServers(ctx context.Context, workspaceID string) (int, error)
CountWorkspaceMCPServers returns the number of bound MCP servers for a workspace. Used for org quota enforcement (max_mcp_servers_per_workspace).
func (*PgSecretStore) CreateAutoApply ¶
func (s *PgSecretStore) CreateAutoApply(ctx context.Context, credentialID, targetType string, targetID *string, priority int) error
CreateAutoApply inserts an auto-apply rule.
func (*PgSecretStore) CreateCredential ¶
func (s *PgSecretStore) CreateCredential(ctx context.Context, ownerType, ownerID string, row *CredentialRow) error
CreateCredential inserts a provider credential scoped by (ownerType, ownerID). The caller supplies a pre-generated ID (uuid.New().String()), matching the admin/user pattern; the DB DEFAULT gen_random_uuid() is only a fallback.
func (*PgSecretStore) CreateMCPServer ¶ added in v0.7.0
func (s *PgSecretStore) CreateMCPServer(ctx context.Context, row *MCPServerRow) error
CreateMCPServer inserts a row into mcp_servers. The caller supplies a pre-generated ID and pre-encrypted ciphertext.
func (*PgSecretStore) CreateMCPServerAutoApply ¶ added in v0.7.0
func (s *PgSecretStore) CreateMCPServerAutoApply(ctx context.Context, serverID, targetType string, targetID *string) error
CreateMCPServerAutoApply inserts an auto-apply rule.
func (*PgSecretStore) CreateOrgAutoApply ¶
func (*PgSecretStore) CreateSecret ¶
func (s *PgSecretStore) CreateSecret(ctx context.Context, secret *UserSecret) error
func (*PgSecretStore) CurrentRevision ¶ added in v0.26.0
func (s *PgSecretStore) CurrentRevision(ctx context.Context, workspaceID string) (int64, string, bool, error)
CurrentRevision returns the stored revision row for a workspace, or ok=false when no revision exists yet (first build). workspaceID is the delivery-path workspace identifier (the CR name).
func (*PgSecretStore) DeleteAutoApply ¶
func (s *PgSecretStore) DeleteAutoApply(ctx context.Context, credentialID, targetType string, targetID *string) error
DeleteAutoApply removes an auto-apply rule.
func (*PgSecretStore) DeleteCredential ¶
func (s *PgSecretStore) DeleteCredential(ctx context.Context, ownerType, ownerID, credID string) error
DeleteCredential deletes a credential by ID scoped to (ownerType, ownerID). FK cascades handle bindings. Returns pgx.ErrNoRows if no row was deleted so callers can distinguish 404 (L-1 fix).
func (*PgSecretStore) DeleteMCPServer ¶ added in v0.7.0
func (s *PgSecretStore) DeleteMCPServer(ctx context.Context, ownerType, ownerID, serverID string) error
DeleteMCPServer deletes an MCP server by ID scoped to (ownerType, ownerID). FK cascades handle bindings + auto-apply. Returns pgx.ErrNoRows if not found.
func (*PgSecretStore) DeleteMCPServerAutoApply ¶ added in v0.7.0
func (s *PgSecretStore) DeleteMCPServerAutoApply(ctx context.Context, serverID, targetType string, targetID *string) error
DeleteMCPServerAutoApply removes an auto-apply rule.
func (*PgSecretStore) DeleteOrgAutoApply ¶
func (s *PgSecretStore) DeleteOrgAutoApply(ctx context.Context, credentialID, orgID string) error
func (*PgSecretStore) DeleteSecret ¶
func (s *PgSecretStore) DeleteSecret(ctx context.Context, userID, secretID string) error
func (*PgSecretStore) EnsureRevision ¶ added in v0.26.0
func (s *PgSecretStore) EnsureRevision(ctx context.Context, workspaceID, manifestHash string) (int64, error)
EnsureRevision converges the workspace's revision to manifestHash. The DB row is the single writer of the seq: every mutation of the stored hash happens in the same conditional UPDATE that increments seq, so concurrently minted seqs are distinct and monotonic by construction.
Loop (bounded by ensureRevisionMaxAttempts):
- UPDATE ... SET seq = seq + 1, manifest_hash = $new WHERE workspace_id = $ws AND manifest_hash <> $new RETURNING seq — a row means this caller won the right to publish manifestHash at that seq. No row means either first-ever (INSERT ... ON CONFLICT DO NOTHING mints seq 1) or someone else already published a hash — fall through to the SELECT arm.
- SELECT seq, manifest_hash — hash == ours means identical content is already current (idempotent rebuild): return that seq. Any other hash means a concurrent mutation won the race: retry from 1.
- Exhausted → ErrRevisionConvergeFailed (wrapped, never a fabricated seq).
func (*PgSecretStore) GetBindings ¶
func (s *PgSecretStore) GetBindings(ctx context.Context, workspaceID string) ([]*UserSecret, error)
func (*PgSecretStore) GetBindingsForSecret ¶
func (*PgSecretStore) GetCredential ¶
func (s *PgSecretStore) GetCredential(ctx context.Context, ownerType, ownerID, credID string) (*CredentialRow, error)
GetCredential returns a single credential by ID scoped to (ownerType, ownerID), or nil if not found. Filtering on both owner_type AND owner_id preserves the L-4 defensive multi-admin safety of the former admin path.
func (*PgSecretStore) GetCredentialBindings ¶
func (s *PgSecretStore) GetCredentialBindings(ctx context.Context, credentialID, ownerID string) ([]string, error)
GetCredentialBindings returns workspace IDs the credential is bound to, scoped to workspaces owned by ownerID.
func (*PgSecretStore) GetCredentialBindingsWithSource ¶
func (s *PgSecretStore) GetCredentialBindingsWithSource(ctx context.Context, credentialID, ownerID string) ([]CredentialBindingInfo, error)
GetCredentialBindingsWithSource returns workspace IDs and source type for bindings, scoped to workspaces owned by ownerID (M-1 fix: allows UI to distinguish auto vs explicit).
func (*PgSecretStore) GetMCPServer ¶ added in v0.7.0
func (s *PgSecretStore) GetMCPServer(ctx context.Context, ownerType, ownerID, serverID string) (*MCPServerRow, error)
GetMCPServer returns a single MCP server by ID scoped to (ownerType, ownerID), or nil if not found.
func (*PgSecretStore) GetSecret ¶
func (s *PgSecretStore) GetSecret(ctx context.Context, userID, secretID string) (*UserSecret, error)
func (*PgSecretStore) GetSecretByName ¶
func (s *PgSecretStore) GetSecretByName(ctx context.Context, userID, name string) (*UserSecret, error)
func (*PgSecretStore) GetWorkspaceCredentials ¶
func (s *PgSecretStore) GetWorkspaceCredentials(ctx context.Context, workspaceID string) ([]CredentialBinding, error)
GetWorkspaceCredentials returns all credential bindings for a workspace, ordered by: (source_type='explicit') DESC, within_priority DESC, created_at ASC.
func (*PgSecretStore) GetWorkspaceMCPServers ¶ added in v0.7.0
func (s *PgSecretStore) GetWorkspaceMCPServers(ctx context.Context, workspaceID string) ([]MCPServerBindingRow, error)
GetWorkspaceMCPServers returns all bound+enabled MCP servers for a workspace, carrying ciphertext for downstream decryption by the injection pipeline. Disabled servers are skipped (omitted, not emitted as the disable form).
func (*PgSecretStore) GetWorkspaceOrgIDForMCP ¶ added in v0.7.0
func (s *PgSecretStore) GetWorkspaceOrgIDForMCP(ctx context.Context, workspaceID string) (string, error)
GetWorkspaceOrgIDForMCP resolves the org_id of a workspace, used by the MCP bind quota check to look up max_mcp_servers_per_workspace. Returns "" for personal workspaces (no org quota applies — only plan-tier quota).
func (*PgSecretStore) GetWorkspaceUserIDForMCP ¶ added in v0.7.0
func (s *PgSecretStore) GetWorkspaceUserIDForMCP(ctx context.Context, workspaceID string) (string, error)
GetWorkspaceUserIDForMCP resolves the user_id of a workspace, used by the MCP bind handler to verify the caller owns the target workspace.
func (*PgSecretStore) HasUserProviderCredential ¶
func (s *PgSecretStore) HasUserProviderCredential(ctx context.Context, userID, slug string) (bool, error)
HasUserProviderCredential returns true if the user owns a credential with the given slug.
func (*PgSecretStore) ListAutoApply ¶
func (s *PgSecretStore) ListAutoApply(ctx context.Context, credentialID string) ([]AutoApplyRule, error)
ListAutoApply returns all auto-apply rules for a credential.
func (*PgSecretStore) ListCredentials ¶
func (s *PgSecretStore) ListCredentials(ctx context.Context, ownerType, ownerID string) ([]*CredentialRow, error)
ListCredentials returns all credentials owned by (ownerType, ownerID), ordered by created_at ASC.
func (*PgSecretStore) ListGlobalDefaultSecrets ¶
func (s *PgSecretStore) ListGlobalDefaultSecrets(ctx context.Context, userID string) ([]*UserSecret, error)
ListGlobalDefaultSecrets returns all secrets owned by userID that have global_default=true. Used when seeding bindings on workspace creation.
func (*PgSecretStore) ListMCPServerAutoApply ¶ added in v0.7.0
func (s *PgSecretStore) ListMCPServerAutoApply(ctx context.Context, serverID string) ([]MCPAutoApplyRule, error)
ListMCPServerAutoApply returns all auto-apply rules for a server.
func (*PgSecretStore) ListMCPServers ¶ added in v0.7.0
func (s *PgSecretStore) ListMCPServers(ctx context.Context, ownerType, ownerID string) ([]*MCPServerRow, error)
ListMCPServers returns all MCP servers owned by (ownerType, ownerID), ordered by created_at ASC. Never decrypts — display fields only.
func (*PgSecretStore) ListOrgAutoApply ¶
func (s *PgSecretStore) ListOrgAutoApply(ctx context.Context, orgID string) ([]*AutoApplyRule, error)
func (*PgSecretStore) ListSecrets ¶
func (s *PgSecretStore) ListSecrets(ctx context.Context, userID string) ([]*UserSecret, error)
func (*PgSecretStore) LogAudit ¶
func (s *PgSecretStore) LogAudit(ctx context.Context, entry *AuditEntry) error
func (*PgSecretStore) QueryAudit ¶
func (s *PgSecretStore) QueryAudit(ctx context.Context, userID string, query AuditQuery) ([]*AuditEntry, error)
func (*PgSecretStore) ReEncryptOrgCredentials ¶
func (s *PgSecretStore) ReEncryptOrgCredentials(ctx context.Context, tx pgx.Tx, orgID string, oldDEK, newDEK []byte) (int, error)
ReEncryptOrgCredentials re-encrypts all provider_credentials rows where owner_type='org' AND owner_id=orgID atomically within tx.
func (*PgSecretStore) ReEncryptUserSecrets ¶
func (s *PgSecretStore) ReEncryptUserSecrets( ctx context.Context, userID string, newKeyVersion int, transform func([]byte) ([]byte, error), commit func(ctx context.Context) error, ) error
ReEncryptUserSecrets walks every user_secrets row owned by userID inside a single SERIALIZABLE transaction, retrying on serialization failure. After the walk completes the commit closure runs in the same transaction so callers can update related rows (e.g. user_keys.wrapped_dek) atomically. If commit returns non-nil the entire transaction rolls back: secrets stay encrypted with the old DEK and user_keys stays unchanged.
A row-count cap of maxRotateRows is enforced to prevent a malicious or pathological account from holding the rotation transaction open indefinitely.
See Bug 9 in worklog 0085 / 0094.
func (*PgSecretStore) SeedWorkspaceCredentials ¶
func (s *PgSecretStore) SeedWorkspaceCredentials(ctx context.Context, workspaceID, userID string, orgID *string) error
SeedWorkspaceCredentials inserts credential bindings for a new workspace. Idempotent — uses ON CONFLICT DO NOTHING throughout.
- orgID nil: personal workspace — org auto-apply rules are not applied.
- orgID non-nil: org workspace — org auto-apply rules and all org credentials are bound.
Issue #593 Option A: when the workspace owner has users.role='admin', every admin credential is bound unconditionally — including ones with no credential_auto_apply rule. Rationale: POST /admin/provider-credentials creates a credential but does not auto-create an auto-apply rule, so an admin's own workspaces would otherwise never see admin credentials they added via the admin UI without a second manual API call. The cascade is gated on role='admin' via a SQL EXISTS subquery so non-admin owners are unaffected (they only see admin credentials that have target_type='all' or target_type='user' auto-apply rules — same as before).
func (*PgSecretStore) SeedWorkspaceMCPServers ¶ added in v0.7.0
func (s *PgSecretStore) SeedWorkspaceMCPServers(ctx context.Context, workspaceID, userID string, orgID *string) error
SeedWorkspaceMCPServers inserts MCP server bindings for a new workspace. Idempotent (ON CONFLICT DO NOTHING). Mirrors SeedWorkspaceCredentials.
Seeding logic:
- Platform (target_type='all') rules always apply.
- User-scope (target_type='user') rules always apply — the workspace's user_id is available regardless of org membership. Whether the user is ALLOWED to create personal MCP servers is gated at CRUD time by the allow_user_mcp_servers org policy, not at seed time.
- Org-scope (target_type='org') rules apply when orgID is non-empty.
func (*PgSecretStore) SetBindings ¶
func (s *PgSecretStore) SetBindings(ctx context.Context, workspaceID string, secretIDs []string) error
SetBindings replaces the binding set for workspaceID atomically.
Concurrency: takes a transaction-scoped advisory lock keyed on the workspace ID's hash so two concurrent SetBindings calls for the same workspace serialize. We use pg_try_advisory_xact_lock with a short retry loop rather than pg_advisory_xact_lock (which would block holding a pool connection indefinitely): under a thundering herd of concurrent SetBindings on the same workspace, blocking would exhaust pool connections and stall the entire API. The try-lock fails fast, sleeps a short jittered interval, and retries up to setBindingsLockMaxAttempts times.
The lock auto-releases on commit/rollback so a panicking writer cannot deadlock future writers. Different workspaces hash to different lock numbers and proceed in parallel.
func (*PgSecretStore) SyncGlobalDefaultBindings ¶ added in v0.34.11
func (s *PgSecretStore) SyncGlobalDefaultBindings(ctx context.Context, workspaceID string, secretIDs []string) (added, removed []string, err error)
SyncGlobalDefaultBindings converges the workspace's auto-source binding rows to exactly secretIDs (see the SecretStore contract). One transaction + the workspace advisory lock so it serializes against Set/AddBindings; the inserts use ON CONFLICT DO NOTHING so a manual row for the same secret survives with its provenance intact.
func (*PgSecretStore) UnbindCredentialFromWorkspace ¶
func (s *PgSecretStore) UnbindCredentialFromWorkspace(ctx context.Context, credentialID, workspaceID string) error
UnbindCredentialFromWorkspace removes an EXPLICIT credential binding. Returns ErrAutoBindingProtected if the binding is auto-managed (H-1 fix).
func (*PgSecretStore) UnbindMCPServerFromWorkspace ¶ added in v0.7.0
func (s *PgSecretStore) UnbindMCPServerFromWorkspace(ctx context.Context, serverID, workspaceID string) error
UnbindMCPServerFromWorkspace removes an explicit MCP server binding. Returns ErrAutoBindingProtected if the binding is auto-managed.
func (*PgSecretStore) UpdateCredential ¶
func (s *PgSecretStore) UpdateCredential(ctx context.Context, ownerType, ownerID, credID string, row *CredentialRow) error
UpdateCredential updates an existing credential scoped to (ownerType, ownerID).
It uses COALESCE for model_allowlist, model_context_limits, and model_output_limits so a nil value means "don't change this column" — the org handler relies on this: a nil modelContextLimits/modelOutputLimits must reach the DB as SQL NULL so COALESCE preserves the existing column value. An empty slice/map is a valid "clear the column" value and is written as-is. Do NOT normalize nil → {} here (it would convert a "don't change" into a "clear all" via COALESCE).
For the admin handler (which allows provider changes and re-encrypts up-front), the caller passes the fully-resolved row with non-nil fields; COALESCE is a no-op there because the caller always supplies concrete values.
updated_at is read back via RETURNING (M-8 fix); the DB trigger sets it to now(). Every update bumps the value-version counter (US-70.2) so the manifest tier sees any config-affecting mutation; key re-encryptions that leave values unchanged (ReEncryptOrgCredentials) bump key_version only.
func (*PgSecretStore) UpdateMCPServer ¶ added in v0.7.0
func (s *PgSecretStore) UpdateMCPServer(ctx context.Context, ownerType, ownerID, serverID string, row *MCPServerRow) error
UpdateMCPServer updates an existing MCP server scoped to (ownerType, ownerID). Partial update: nil fields preserve existing values. Ciphertext is only rewritten when non-nil (secret rotation). Args is only rewritten when non-nil (a nil []string is passed as SQL NULL so the CASE preserves the existing value; passing JSON "null" would overwrite args with JSONB null — see review finding). Every update bumps the value-version counter (US-70.2) — the manifest tier must see any config-affecting mutation.
func (*PgSecretStore) UpdateSecret ¶
func (s *PgSecretStore) UpdateSecret(ctx context.Context, secret *UserSecret) error
func (*PgSecretStore) UpsertFreeTierCredential ¶
func (s *PgSecretStore) UpsertFreeTierCredential(ctx context.Context, ciphertext []byte) error
UpsertFreeTierCredential atomically upserts the platform free-tier opencode credential and its auto-apply rule in a single transaction.
type PostgresSecretProvider ¶
type PostgresSecretProvider struct {
// contains filtered or unexported fields
}
PostgresSecretProvider implements SecretProvider using the KeyService and SecretStore.
func NewPostgresSecretProvider ¶
func NewPostgresSecretProvider(keys *KeyService, store SecretStore) *PostgresSecretProvider
NewPostgresSecretProvider creates a new PostgresSecretProvider.
func (*PostgresSecretProvider) DEKAvailable ¶
func (p *PostgresSecretProvider) DEKAvailable(ctx context.Context, owner SecretOwner) bool
func (*PostgresSecretProvider) Decrypt ¶
func (p *PostgresSecretProvider) Decrypt(ctx context.Context, owner SecretOwner, ciphertext []byte, keyVersion int) ([]byte, error)
func (*PostgresSecretProvider) Encrypt ¶
func (p *PostgresSecretProvider) Encrypt(ctx context.Context, owner SecretOwner, plaintext []byte) ([]byte, int, error)
func (*PostgresSecretProvider) RotateKey ¶
func (p *PostgresSecretProvider) RotateKey(ctx context.Context, owner SecretOwner) (int, error)
type ReconcileKeyStore ¶ added in v0.27.0
type ReconcileKeyStore interface {
// ListUserKeysForReconcile returns one walk window, ORDER BY
// updated_at ASC (oldest = highest risk first), user_id ASC as the
// deterministic tiebreak. offset advances by limit per window; a
// window returning fewer than limit rows ends the pass.
ListUserKeysForReconcile(ctx context.Context, limit, offset int) ([]UserKeyReconcileRow, error)
// CompareAndSwapWrappedDEK conditionally rewrites the row's active
// wrap: the UPDATE applies only when the row's current wrapped_dek
// byte-equals expectedWrapped. Returns won=false (nil error) when
// zero rows matched — a concurrent legitimate rotation won the race
// and the caller must back off, never retry-storm. previous == nil
// clears the retention columns; non-nil retains the prior wrap per
// W10.
CompareAndSwapWrappedDEK(ctx context.Context, userID string, expectedWrapped, newWrapped []byte, newVersion int, previous *RetainedWrap) (won bool, err error)
// DeleteExpiredRetainedWraps NULLs the previous-wrap columns of every
// row whose wrapped_dek_retained_until has passed. The active wrap is
// untouched (and updated_at is NOT bumped — the walk ordering tracks
// active-wrap lifecycle, not retention bookkeeping). Returns the
// number of rows cleaned.
DeleteExpiredRetainedWraps(ctx context.Context, now time.Time) (int64, error)
}
ReconcileKeyStore is the user_keys data-access surface the re-wrap reconciler needs beyond the plain KeyStore: batched oldest-first listing, compare-and-swap heal writes with retention, and retention cleanup. Implemented by PgKeyStore; test doubles live with the reconciler service.
type RedactStagedKeys ¶ added in v0.33.0
RedactStagedKeys adapts a *redact.Redactor to StagedKeyRedactor.
func (RedactStagedKeys) RegisterStagedKey ¶ added in v0.33.0
func (a RedactStagedKeys) RegisterStagedKey(id string, material []byte) error
func (RedactStagedKeys) UnregisterStagedKey ¶ added in v0.33.0
func (a RedactStagedKeys) UnregisterStagedKey(id string)
type RedisDEKCache ¶
type RedisDEKCache struct {
// contains filtered or unexported fields
}
RedisDEKCache implements DEKCache using Redis. If a master key is provided, DEKs are encrypted before storage in Redis.
func NewRedisDEKCache ¶
func NewRedisDEKCache(client *redis.Client, masterKey ...[]byte) *RedisDEKCache
NewRedisDEKCache creates a new Redis-backed DEK cache. masterKey is optional — if nil or empty, DEKs are stored as plain hex.
type RelayHandoff ¶ added in v0.34.7
type RelayHandoff struct {
Revision string `json:"revision"`
RouterURL string `json:"routerURL"`
Providers []RelayHandoffProvider `json:"providers"`
}
RelayHandoff is the handoff Secret's single `handoff` JSON document (US-72.3 worklog D2 — the JSON tags are the wire contract with the controller's staging pass; parse-only here).
func (*RelayHandoff) Find ¶ added in v0.34.7
func (h *RelayHandoff) Find(slug string) (RelayHandoffProvider, bool)
Find returns the staged entry for a provider slug (the DECRYPTED provider-data slug — the same key the controller staged by).
func (*RelayHandoff) RouterBase ¶ added in v0.34.7
func (h *RelayHandoff) RouterBase() string
RouterBase returns the router origin (no trailing slash) every provider baseURL is built on.
type RelayHandoffProvider ¶ added in v0.34.7
type RelayHandoffProvider struct {
ProviderSlug string `json:"providerSlug"`
Kind string `json:"kind"`
Token string `json:"token"`
RouterPath string `json:"routerPath"`
BaseURL string `json:"baseURL"`
ModelAllowlist []string `json:"modelAllowlist"`
KeyID string `json:"keyID"`
ExpiresAt string `json:"expiresAt"`
}
RelayHandoffProvider is one staged provider's token entry.
type RelayTokenSource ¶ added in v0.34.7
type RelayTokenSource interface {
RelayHandoff(ctx context.Context, workspaceID string) (*RelayHandoff, error)
}
RelayTokenSource resolves the controller-staged handoff for a workspace (production: the k8s `workspace-relay-<wsName>` Secret in the workspace namespace, data key `handoff`). A nil handoff with a nil error means "staged Secret absent" — staging not ready (strict mutes; migration falls back to raw keys, counted — M2). A nil RelayTokenSource on the service means the deployment flag is OFF: legacy behavior, byte-identical.
type RetainedWrap ¶ added in v0.27.0
type RetainedWrap struct {
Ciphertext []byte
// KEKVersion is the master-KEK version Ciphertext is wrapped under
// (the provider's active version at heal time).
KEKVersion int
// Until is the retention deadline; the reconciler's pass NULLs the
// previous columns once it passes.
Until time.Time
}
RetainedWrap is the time-boxed retention record the re-wrap reconciler (US-70.4) writes alongside every CAS heal. Ciphertext is the row's PREVIOUS wrapped_dek bytes, re-encrypted under the CURRENT master KEK (W10): retention adds zero plaintext at rest, and a bad heal stays reversible with the keys the deployment already has — decrypt Ciphertext with the current provider to recover the old wrap bytes, then restore the row by hand (runbook: helm/KEK-ROTATION.md class of operator action).
type RevisionStore ¶ added in v0.26.0
type RevisionStore interface {
// CurrentRevision returns the stored revision for a workspace.
// ok=false (with nil error) means no revision row exists yet.
CurrentRevision(ctx context.Context, workspaceID string) (seq int64, manifestHash string, ok bool, err error)
// EnsureRevision converges the workspace's revision to manifestHash:
// when the stored hash already equals manifestHash the stored seq is
// returned unchanged; otherwise the row's conditional UPDATE mints
// the next seq. Concurrent callers racing different manifests each
// receive distinct, monotonically increasing seqs.
EnsureRevision(ctx context.Context, workspaceID, manifestHash string) (int64, error)
}
RevisionStore abstracts the per-workspace secret-delivery revision row (US-70.2, design 0057 R1). The DB row is the single writer of the monotonic seq — API replicas never mint seqs on their own.
workspaceID is the workspace identifier the delivery path actually uses (the CR name), NOT the uuid workspaces.id.
type RootKeyProvider ¶
type RotationCoordinator ¶
type RotationCoordinator struct {
// contains filtered or unexported fields
}
RotationCoordinator re-wraps KEK-protected rows from old providers to new providers. It is table-agnostic: the caller provides the store + provider sets.
func NewRotationCoordinator ¶
func NewRotationCoordinator(store RotationStore, oldProviders, newProviders map[string]RootKeyProvider) *RotationCoordinator
NewRotationCoordinator constructs a coordinator with the old (source) and new (target) provider sets. Each map is keyed by purpose string.
func (*RotationCoordinator) RotateAll ¶
func (c *RotationCoordinator) RotateAll(ctx context.Context, targetVersion int, dryRun bool) (map[string]KEKRotationResult, error)
RotateAll rotates all KEK-protected tables sequentially. The Redis DEK cache is flushed after all tables complete successfully.
func (*RotationCoordinator) RotateTable ¶
func (c *RotationCoordinator) RotateTable(ctx context.Context, table, resumeFromID string, targetVersion int, dryRun bool) (KEKRotationResult, error)
RotateTable rotates all rows in the given table that are below targetVersion. If dryRun is true, no writes occur — only counts are reported. resumeFromID allows resuming from a specific row ID after an interrupted run.
type RotationRow ¶
type RotationRow struct {
ID string
Table string // "provider_credentials", "api_keys", "org_sso_configs", "user_keys"
OwnerType string // for provider_credentials: "admin" or "org"; "" for other tables
Ciphertext []byte
KeyVersion int
}
RotationRow is a generic row from any KEK-protected table. The coordinator re-wraps Ciphertext from oldKeyVersion to newKeyVersion.
type RotationStore ¶
type RotationStore interface {
// ListRotationRows returns rows from the given table whose key_version is
// below targetVersion, ordered by ID ASC, starting after resumeFromID
// (empty = from the beginning). limit caps the batch size (0 = unlimited).
ListRotationRows(ctx context.Context, table, resumeFromID string, targetVersion, limit int) ([]RotationRow, error)
// UpdateRotationRow re-wraps a single row: writes newCiphertext and
// newKeyVersion atomically. Each call is its own transaction.
UpdateRotationRow(ctx context.Context, table, rowID string, newCiphertext []byte, newKeyVersion int) error
// FlushDEKCache flushes the Redis DEK cache so stale DEKs (wrapped under
// the old KEK) are evicted.
FlushDEKCache(ctx context.Context) error
}
RotationStore abstracts the three KEK-protected tables for the rotation CLI. Each method returns rows in a consistent order (by ID ASC) so resume-from- cursor works deterministically.
type SealedKeyProvider ¶
type SealedKeyProvider struct {
// contains filtered or unexported fields
}
SealedKeyProvider holds the unsealed root key in process memory. It defends against attackers who can read the sealed file or the node disk but NOT the passphrase: the on-disk file is Argon2id-wrapped and is useless without the passphrase. It does NOT defend against process-level compromise of the API pod — once the key is unsealed at boot it lives in memory, and an attacker who can run code in the pod can call Decrypt exactly as the application does. See pkg/secrets/README.md for the full threat model.
US-50.4 multi-key support (NewStaticKeyProviderMultiVersion) is NOT mirrored here yet. The sealed provider is constructed once at boot from a single sealed file; multi-file rotation-window support for the sealed path will be added alongside US-50.5 (rotate-kek CLI) when the rotation workflow is exercised end-to-end. The StaticKeyProvider covers the default Helm path.
func NewSealedKeyProvider ¶
func NewSealedKeyProvider(sealedKeyPath, passphrasePath string) (*SealedKeyProvider, error)
type SecretBinding ¶
type SecretBinding struct {
SecretID string `json:"secretId"`
WorkspaceID string `json:"workspaceId"`
CreatedAt time.Time `json:"createdAt"`
}
SecretBinding represents a secret-to-workspace binding.
type SecretOwner ¶
SecretOwner identifies the owner of a secret (user or org).
type SecretProvider ¶
type SecretProvider interface {
// Encrypt encrypts plaintext with the owner's current DEK.
Encrypt(ctx context.Context, owner SecretOwner, plaintext []byte) (ciphertext []byte, keyVersion int, err error)
// Decrypt decrypts ciphertext using the appropriate DEK version.
Decrypt(ctx context.Context, owner SecretOwner, ciphertext []byte, keyVersion int) (plaintext []byte, err error)
// RotateKey generates a new DEK for the owner. Old DEK retained for lazy migration.
RotateKey(ctx context.Context, owner SecretOwner) (newKeyVersion int, err error)
// DEKAvailable returns true if the owner's DEK is currently cached (active session).
DEKAvailable(ctx context.Context, owner SecretOwner) bool
}
SecretProvider defines the encryption/decryption interface for user secrets. V1: PostgresSecretProvider (HKDF + AES-GCM + session cache) Future: VaultSecretProvider, HSMSecretProvider
type SecretResponse ¶
type SecretResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Type SecretType `json:"type"`
Metadata json.RawMessage `json:"metadata"`
GlobalDefault bool `json:"globalDefault"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
SecretResponse is the API response for a secret (never includes value).
type SecretService ¶
type SecretService struct {
// contains filtered or unexported fields
}
SecretService provides encrypted secret CRUD operations.
func NewSecretService ¶
func NewSecretService(keys *KeyService, store SecretStore) *SecretService
NewSecretService creates a new SecretService.
As a side-effect we register the SecretStore on the KeyService so SecretService encrypts/decrypts user secrets using the user DEK (Bug 9 in worklog 0085). The two services share a store anyway; this just makes the linkage explicit at construction time.
func (*SecretService) AddBindings ¶
func (s *SecretService) AddBindings(ctx context.Context, userID, workspaceID string, secretIDs []string) (BindingsMutationResult, error)
AddBindings adds secretIDs to a workspace's binding set without removing any existing bindings. The store-level implementation takes a workspace-scoped advisory lock so concurrent SetBindings / AddBindings calls cannot lose updates (worklog 0094 pass-2 finding O1). Each secret's ownership is verified before the binding is recorded; an unowned secret produces ErrSecretNotFound.
Used by SetWorkspaceEnv to merge newly-created env-secrets into the workspace bindings without the Get-then-Set window the previous implementation suffered from. Workspace ownership is enforced by WorkspaceAccessMiddleware on PUT /:id/env (design 0041 D5).
func (*SecretService) BuildWorkspaceBatch ¶ added in v0.26.0
func (s *SecretService) BuildWorkspaceBatch(ctx context.Context, ownerUserID, workspaceID string) (*Batch, *BuildDegrade, error)
BuildWorkspaceBatch builds the complete secret batch for a workspace: every effective credential binding, user secret binding, and bound MCP server the workspace's owner has — the "one builder, one truth" contract (I2). Session identity is absent from construction: user entries decrypt via GetDEKServerSide (K5), so suspend/resume, expired sessions, and background pushes all produce the SAME batch for the same row state.
Structure (design 0057 R1, two-tier):
- rows → []ManifestEntry — built from ROWS, pre-decrypt: id, version, type, name, stored metadata. The manifest tier never decrypts anything, so any replica computes the identical hash. LLM credentials dedup by slug keeping the first row in binding priority order (the intended winner); user_secrets rows filter to the workspace owner's non-llm-provider entries; MCP rows are the bound+enabled servers.
- manifest hash → EnsureRevision — the DB row mints the monotonic seq; replicas never mint seqs.
- decrypt tier — the (admin/org/user) decrypt matrix with today's per-entry audit-and-continue semantics: a failed credential falls back to the next binding of the same slug; a failed user_secret or MCP server is skipped with its audit event.
THE REVISION DESCRIBES THE INTENDED SET: per-entry decrypt failures do NOT reshape the manifest — the failed entry stays in the manifest hash as the loud-degrade contract, and the batch omits only its value. The revision converges when the row heals (its version bumps).
Degrade (I10): on DEK-tier failure the returned batch carries the server-KEK entries plus a BuildDegrade reason (dek_unwrap_failed / owner_no_keys); user rows are skipped with the *_skipped_no_session audit vocabulary. Never a silent partial.
Workspace ownership is enforced by the callers' HTTP layer or token review; this method does not re-check it.
func (*SecretService) CreateSecret ¶
func (s *SecretService) CreateSecret(ctx context.Context, userID, sessionID string, matchedSigningKey []byte, req CreateSecretRequest) (*SecretResponse, error)
CreateSecret encrypts and stores a new secret.
func (*SecretService) CurrentRevision ¶ added in v0.26.0
func (s *SecretService) CurrentRevision(ctx context.Context, workspaceID string) (int64, string, bool, error)
CurrentRevision returns the workspace's stored revision row (see RevisionStore). Exposed on the service so the conditional pod-bootstrap path can stamp the 304 ETag with the row's seq without holding the store directly.
func (*SecretService) DecryptSecretValue ¶
func (s *SecretService) DecryptSecretValue(ctx context.Context, userID, sessionID string, matchedSigningKey []byte, secretID string) ([]byte, error)
DecryptSecretValue decrypts a secret's value (used for pod injection).
func (*SecretService) EnsureRevision ¶ added in v0.27.0
func (s *SecretService) EnsureRevision(ctx context.Context, workspaceID, manifestHash string) (int64, error)
EnsureRevision converges the workspace's stored revision row to manifestHash (see RevisionStore): same hash returns the stored seq unchanged; a different hash conditionally mints the next seq with the store's bounded CAS semantics. Exposed on the service so the reconcile loop can mint observed drift as a new seq (US-70.3) with the same single-writer guarantees the build path has.
func (*SecretService) ForceRevokeSecret ¶ added in v0.27.0
func (s *SecretService) ForceRevokeSecret(ctx context.Context, userID, secretID string) ([]string, error)
ForceRevokeSecret is the I12 revocation operation and the ONLY delete path (validation m2 removed the plain DeleteSecret wrapper — zero non-test callers; its "delete" audit wording with name+type is preserved by the final audit below): deleting a secret revokes it from EVERY workspace it is bound to. It removes the row (the store's ON DELETE CASCADE drops every user_secret_bindings row), then force-refreshes each affected workspace's stored revision by recomputing the manifest tier (zero decrypts) and running EnsureRevision — WITHOUT the refresh, the stored row would still carry the pre-revoke hash and the reconcile loop would see the workspace as converged while the pod keeps serving the revoked plaintext. Returns the affected workspace IDs so the handler can notify each live pod (notify failure costs latency, never correctness — suspended pods converge on boot by construction).
func (*SecretService) GetBindings ¶
func (s *SecretService) GetBindings(ctx context.Context, userID, workspaceID string) (*BindingsResponse, error)
GetBindings returns secrets bound to a workspace. Workspace ownership is enforced by WorkspaceAccessMiddleware on GET /:id/bindings (design 0041 D5); the service trusts that decision and does not re-fetch the workspace.
func (*SecretService) GetBindingsForSecret ¶
func (s *SecretService) GetBindingsForSecret(ctx context.Context, userID, secretID string) ([]string, error)
GetBindingsForSecret returns workspace IDs that a secret is bound to.
Ownership-failure modes (secret not found, secret owned by someone else) are conflated to a uniform empty result so the response shape does not leak existence cross-tenant. Genuine system errors (DB outage on the lookup) propagate so the handler can return 5xx instead of a misleading empty 200.
func (*SecretService) GetSecret ¶
func (s *SecretService) GetSecret(ctx context.Context, userID, secretID string) (*SecretResponse, error)
GetSecret returns secret metadata (never the value).
func (*SecretService) GetSecretByName ¶
func (s *SecretService) GetSecretByName(ctx context.Context, userID, name string) (*SecretResponse, error)
GetSecretByName returns secret metadata by name (never the value).
func (*SecretService) ListSecrets ¶
func (s *SecretService) ListSecrets(ctx context.Context, userID string) ([]*SecretResponse, error)
ListSecrets returns all secret metadata for a user (never values).
func (*SecretService) ManifestFor ¶ added in v0.26.0
func (s *SecretService) ManifestFor(ctx context.Context, ownerUserID, workspaceID string) (string, error)
ManifestFor computes the workspace's CURRENT manifest hash from rows alone — the decrypt-free tier. This is the seam the conditional pod-bootstrap 304 decision runs on (US-70.2): comparing the client's last manifest hash against this value never touches a ciphertext, the DEK key store, or the admin/org root providers. It also does NOT mint a revision: an unchanged manifest means the stored row already holds the matching seq, and a changed manifest mints its seq in the 200 path's build.
US-72.4: under the relay-only flag the staged handoff's revision participates (design 0058 §4.4) — identical tiering as the build path (workspaceManifestHash), so a 304 here and a seq mint there can never disagree. A handoff read failure degrades to the rows-only hash: a false 304 is impossible, but under flag-on the rows-only manifest makes the client refetch a batch whose provider class the not-ready build mutes — the resync pull then PERSISTS that provider-less batch to the working pod's durable state, wiping the relay tokens until the handoff read recovers and the next pull restores the class (self-healing, bounded by the resync cadence; the fail-closed choice itself is pinned by TestRelayBatch_HandoffSourceError_TreatedAsNotReady).
func (*SecretService) QueryAudit ¶
func (s *SecretService) QueryAudit(ctx context.Context, userID string, query AuditQuery) ([]*AuditEntry, error)
QueryAudit returns audit log entries for the current user.
func (*SecretService) RelayFallbackForTest ¶ added in v0.34.8
func (s *SecretService) RelayFallbackForTest() bool
RelayFallbackForTest exposes the installed fallback mode (the RelayTokensForTest precedent — cross-package wiring tests assert the app seam installed MIGRATION, the builder's non-default).
func (*SecretService) RelayOnlyEnabled ¶ added in v0.34.8
func (s *SecretService) RelayOnlyEnabled() bool
RelayOnlyEnabled reports whether the relay-only token source is installed (the deployment's relayOnlyKeyDelivery flag reached the API server). Design 0061 §6/M4: the pod-bootstrap condition hook gates on this before writing CredentialsStaged — under flag-off the condition must stay absent (the W15 contract), so the relay paths' callers assert the flag rather than duplicate it.
func (*SecretService) RelayTokensForTest ¶ added in v0.34.7
func (s *SecretService) RelayTokensForTest() RelayTokenSource
RelayTokensForTest exposes the installed relay source for cross-package wiring tests (the app.New seam test cannot reach the unexported field; the GetOutboxForTest precedent). Nil means the flag is off.
func (*SecretService) ResolveLLMProviders ¶ added in v0.34.4
func (s *SecretService) ResolveLLMProviders(ctx context.Context, ownerUserID, workspaceID string) ([]LLMProviderData, error)
ResolveLLMProviders (US-72.3) resolves the decrypted llm-provider set a workspace's batch would deliver: the same rows (loadWorkspaceRows), the same dedup-by-slug-first-winner, the same decrypt matrix and the same model-allowlist application as buildCredentialEntries — so the envelope set the controller stages is by construction exactly the provider set of the batch. Per-entry failures audit and continue (a corrupted ciphertext must not poison the healthy ones); with no DEK available the user-owned entries are skipped with the sessionless vocabulary, matching the builder's degrade semantics.
This backs the cluster-internal GET /api/v1/internal/workspaces/:workspaceID/llm-providers endpoint the controller staging polls — trusted-plane only, never a pod delivery path (K6).
func (*SecretService) SeedGlobalDefaultSecrets ¶
func (s *SecretService) SeedGlobalDefaultSecrets(ctx context.Context, workspaceID, userID string) error
SeedGlobalDefaultSecrets binds all secrets with global_default=true owned by userID to the given workspace. Called by the workspace service on workspace creation as a best-effort operation (failure is logged but does not roll back the workspace) — the reconcile loop's policy-convergence step re-asserts the same policy for existing workspaces every pass, so a failed seed self-heals. Rows created here carry bind_source=global_default (removable when the flag flips off) and each add is audited (matching every user-initiated bind path); re-running is idempotent.
func (*SecretService) SetAdminProvider ¶
func (s *SecretService) SetAdminProvider(p RootKeyProvider)
SetAdminProvider installs the RootKeyProvider for admin (owner_type='admin') provider credentials. When non-nil, the batch builder decrypts admin bindings through it; when nil, admin bindings are skipped with an audit event.
func (*SecretService) SetBindings ¶
func (s *SecretService) SetBindings(ctx context.Context, userID, workspaceID string, secretIDs []string) (BindingsMutationResult, error)
SetBindings sets which secrets are bound to a workspace. The caller must own every secret being bound; an unowned secret produces ErrSecretNotFound (mapped to 404 by the handler). Workspace ownership itself is enforced by WorkspaceAccessMiddleware on PUT /:id/bindings (design 0041 D5) — the service trusts that decision so it can also be called from background paths where the caller is implicitly authorized (e.g. workspace.Service.refreshEphemeralSecrets).
func (*SecretService) SetOrgProvider ¶
func (s *SecretService) SetOrgProvider(p RootKeyProvider)
SetOrgProvider installs the RootKeyProvider for org (owner_type='org') provider credentials. When non-nil, the batch builder decrypts org bindings through it; when nil, org bindings are skipped with an audit event.
func (*SecretService) SetRelayDeliveryFallback ¶ added in v0.34.8
func (s *SecretService) SetRelayDeliveryFallback(enabled bool)
SetRelayDeliveryFallback installs the fallback mode (design 0061 §4): true = MIGRATION (not-ready delivers pre-flip raw keys + the fallback counter); false/zero-value = STRICT (the fail-closed class-mute — the steady-state posture; the safe default until the API installs migration explicitly).
func (*SecretService) SetRelayTokenSource ¶ added in v0.34.7
func (s *SecretService) SetRelayTokenSource(src RelayTokenSource)
SetRelayTokenSource installs (or with nil, removes) the relay-only token source. Called once at API construction from the relayOnlyKeyDelivery config flag.
func (*SecretService) SyncGlobalDefaultBindings ¶ added in v0.34.11
func (s *SecretService) SyncGlobalDefaultBindings(ctx context.Context, ownerUserID, workspaceID string) (added, removed []string, err error)
SyncGlobalDefaultBindings converges the workspace's global-default policy into binding rows: the owner's current global_default secrets end up bound with bind_source=global_default, and auto rows for secrets that lost the flag (or were deleted) are removed. Manual binding rows are never touched — the user's explicit claim is the stronger one. Adds and removes are audited with the source recorded in the audit metadata.
This is the policy→state materialization seam (the "one builder, one truth" contract stays intact: the batch builder keeps reading binding rows only). Called by the workspace-create seeding path and, for every Active workspace, by the secretsreconcile loop each pass — the loop is the correctness path; notify-driven mutation is only a latency optimization.
func (*SecretService) UpdateSecret ¶
func (s *SecretService) UpdateSecret(ctx context.Context, userID, sessionID string, matchedSigningKey []byte, secretID string, req UpdateSecretRequest) error
UpdateSecret re-encrypts and updates a secret's value.
type SecretStore ¶
type SecretStore interface {
CreateSecret(ctx context.Context, secret *UserSecret) error
GetSecret(ctx context.Context, userID, secretID string) (*UserSecret, error)
GetSecretByName(ctx context.Context, userID, name string) (*UserSecret, error)
ListSecrets(ctx context.Context, userID string) ([]*UserSecret, error)
// ListGlobalDefaultSecrets returns all secrets owned by userID that have
// global_default=true. Used when seeding bindings on workspace creation.
ListGlobalDefaultSecrets(ctx context.Context, userID string) ([]*UserSecret, error)
UpdateSecret(ctx context.Context, secret *UserSecret) error
DeleteSecret(ctx context.Context, userID, secretID string) error
// ReEncryptUserSecrets re-encrypts every row owned by userID in a
// single atomic operation. The transform closure receives the old
// ciphertext for a row and must return the new ciphertext (decrypted
// with the old DEK and re-encrypted with the new one). After all
// rows are re-encrypted but before the transaction commits, the
// commit closure is invoked with the same tx so the caller can
// piggyback related updates (e.g. user_keys.wrapped_dek) into the
// same atomic unit; if commit returns non-nil the entire transaction
// rolls back. Implementations MUST run all of this in a single
// SERIALIZABLE transaction with retry on serialization failure.
//
// A partial state would leave rows decryptable only by a key the
// system has discarded — the failure mode of Bug 9 in worklog 0085.
ReEncryptUserSecrets(
ctx context.Context,
userID string,
newKeyVersion int,
transform func(oldCiphertext []byte) (newCiphertext []byte, err error),
commit func(ctx context.Context) error,
) error
// Bindings
SetBindings(ctx context.Context, workspaceID string, secretIDs []string) error
// AddBindings atomically adds secretIDs to a workspace's binding
// set without removing any existing bindings. Implementations
// MUST take the same workspace-scoped advisory lock as
// SetBindings so concurrent Add+Set callers serialize. Existing
// bindings to the same secret are silently ignored
// (INSERT ... ON CONFLICT DO NOTHING semantics).
//
// Used by SetWorkspaceEnv to add new env-secrets without racing
// on a Get-then-Set window — see worklog 0094 pass-2 finding O1.
AddBindings(ctx context.Context, workspaceID string, secretIDs []string) error
// SyncGlobalDefaultBindings converges the workspace's
// bind_source='global_default' rows to exactly secretIDs in one
// atomic operation: inserts missing auto rows, removes auto rows
// whose secret is not in secretIDs (the flag flipped off — rows
// for deleted secrets are already gone via the table's ON DELETE
// CASCADE FK), and never touches manual rows (a manual row for
// the same secret is the stronger claim and shadows the insert).
// Returns the secret IDs actually added and removed so callers
// can audit and count the convergence. Implementations MUST take
// the same workspace-scoped advisory lock as SetBindings.
SyncGlobalDefaultBindings(ctx context.Context, workspaceID string, secretIDs []string) (added, removed []string, err error)
GetBindings(ctx context.Context, workspaceID string) ([]*UserSecret, error)
GetBindingsForSecret(ctx context.Context, secretID string) ([]string, error)
// Audit
LogAudit(ctx context.Context, entry *AuditEntry) error
QueryAudit(ctx context.Context, userID string, query AuditQuery) ([]*AuditEntry, error)
}
SecretStore abstracts database operations for user secrets.
type SecretType ¶
type SecretType string
SecretType defines the type of secret.
const ( // APIKeySunsetDate is the fixed date on which new api-key secrets // become uncreatable (US-44.9). Six months after the Epic 44 ship // date. Existing api-key secrets remain functional after this date; // only creation is blocked by the CreateSecret gate. APIKeySunsetDate = "2026-12-19" // SecretTypeAPIKey is for generic API-key secrets (legacy). // New code should use SecretTypeLLMProvider for structured provider // credentials. Kept for backward compatibility with existing secrets. SecretTypeAPIKey SecretType = "api-key" // SecretTypeLLMProvider is for structured LLM provider credentials // (Anthropic, OpenAI, etc.). Each secret holds one provider with // its API key, optional base URL, model visibility allowlist, and // default model selection. Multiple llm-provider secrets bound to // the same workspace are merged by the agent's FormatProviderConfig. SecretTypeLLMProvider SecretType = "llm-provider" SecretTypeSSHKey SecretType = "ssh-key" SecretTypeGitCredential SecretType = "git-credential" SecretTypeSecretFile SecretType = "secret-file" SecretTypeEnvSecret SecretType = "env-secret" // SecretTypeMcpServer (Epic 53) carries an external MCP server config // through the injection pipeline. Metadata holds transport/url/command/args; // Plaintext is the JSON-encoded env+headers map (decrypted at injection). SecretTypeMcpServer SecretType = "mcp-server" )
func ValidSecretTypesList ¶
func ValidSecretTypesList() []SecretType
ValidSecretTypesList returns the canonical list of valid secret types, in stable order. Used to format the error message returned when a caller submits an invalid type, so the response is self-documenting.
type SetBindingsRequest ¶
type SetBindingsRequest struct {
SecretIDs []string `json:"secretIds" binding:"required"`
}
SetBindingsRequest is the API request for setting workspace bindings.
type StagedKeyRedactor ¶ added in v0.33.0
type StagedKeyRedactor interface {
RegisterStagedKey(id string, material []byte) error
UnregisterStagedKey(id string)
}
StagedKeyRedactor is the seam between staged key material and the redaction engine (owner direction 2026-09-17; design 0058 §4.9 addendum). The staging provider is the one place the platform legitimately knows a staged key's plaintext at seal and resolve time; it registers the material as exact-value redaction rules so the payload pipeline (static rules + dynamic staged-key rules, applied at the router's sanitization stage in US-72.2) can never echo a key back through the relay.
Lifecycle contract (binding on the wiring stories, US-72.2/US-72.3):
- Register at seal (controller) and resolve (router). The rule-group ID is envelope-derived (StagedKeyRedactionID), so re-registering the SAME envelope replaces its group in place — the resolve side is thereby bounded by the live envelope set the informer holds.
- The RE-SEAL path (rotation, design 0058 §4.2) mints a fresh envelope and therefore a fresh group. Nothing in this seam can know the replacement relationship — only the re-sealer does. The controller staging reconcile (US-72.3) MUST unregister the superseded envelope's group in the same pass: UnregisterStagedKey(StagedKeyRedactionID(oldEnvelope)). Pinned by TestResealRegistrationLifecycle.
- Revocation (Secret deletion, D2) unregisters via the same helper from the informer's delete path.
- A nil seam skips registration (fail-open, for unit-test adoption and staged rollouts); production construction paths pin a non-nil redactor by test.
type StagingResolver ¶ added in v0.33.0
type StagingResolver interface {
Resolve(ctx context.Context, envelope string) ([]byte, error)
KeyID() string
}
StagingResolver is the router-side capability: recover the provider key from an envelope. The HPKE sealer deliberately does NOT implement it — in dev mode the controller holds no decrypt capability by construction.
type StagingSealer ¶ added in v0.33.0
type StagingSealer interface {
Seal(ctx context.Context, providerKey []byte) (string, error)
KeyID() string
}
StagingSealer is the controller-side capability: seal a provider key into an envelope. Implementations: KMSStagingProvider (prod), HPKEStagingSealer (dev).
type StaticKeyProvider ¶
type StaticKeyProvider struct {
// contains filtered or unexported fields
}
StaticKeyProvider holds one or more versioned keys. Encrypt always uses the highest-version (active) key; Decrypt tries each key newest-to-oldest and returns the first success. This enables zero-downtime KEK rotation (US-50.4, design D4): during the transition window the provider holds both old and new keys so ciphertexts encrypted under either version decrypt correctly.
func NewStaticKeyProvider ¶
func NewStaticKeyProvider(key []byte) (*StaticKeyProvider, error)
NewStaticKeyProvider constructs a single-key provider at version 1. This is the backward-compatible constructor used everywhere except the rotation window.
func NewStaticKeyProviderMultiVersion ¶
func NewStaticKeyProviderMultiVersion(activeVersion int, keyByVersion map[int][]byte) (*StaticKeyProvider, error)
NewStaticKeyProviderMultiVersion constructs a multi-key provider for the rotation transition window (US-50.4). activeVersion is the highest version (the one Encrypt uses); keyByVersion maps every version to its key material. At least one entry at activeVersion must exist. Entries are stored sorted by version descending so Decrypt tries the newest first.
func (*StaticKeyProvider) ActiveVersion ¶
func (p *StaticKeyProvider) ActiveVersion() int
ActiveVersion returns the highest version the provider can encrypt with (US-50.3 uses this to populate key_version columns on encrypt).
type UpdateSecretRequest ¶
type UpdateSecretRequest struct {
Value string `json:"value" binding:"required"`
Metadata json.RawMessage `json:"metadata,omitempty"`
// GlobalDefault, when non-nil, updates whether this secret is automatically
// bound to newly-created workspaces. Nil means "leave unchanged".
GlobalDefault *bool `json:"globalDefault,omitempty"`
}
UpdateSecretRequest is the API request for updating a secret value.
type UserKeyReconcileRow ¶ added in v0.27.0
type UserKeyReconcileRow struct {
UserID string
KeyVersion int
// WrappedDEK is the active wrap the current master provider must be
// able to decrypt; a failure is the reconciler's entry condition.
WrappedDEK []byte
// WrappedDEKPrevious is nil unless a prior heal retained a wrap that
// has not yet passed wrapped_dek_retained_until.
WrappedDEKPrevious []byte
WrappedDEKPreviousKEKVersion *int
WrappedDEKRetainedUntil *time.Time
UpdatedAt time.Time
}
UserKeyReconcileRow is one user_keys row as the re-wrap reconciler sees it: the active wrap, the retained-wrap shadow columns, and the mutable ordering timestamp that drives the oldest-first walk.
type UserKeyRecord ¶
type UserKeyRecord struct {
UserID string
KeyVersion int
WrappedDEK []byte
WrappedDEKRecovery []byte // nil if user opted out
Salt []byte // nil for server_kek rows (no Argon2id derivation)
RecoverySalt []byte // nil if user opted out
// DEKSource is the encryption source, read from users.dek_source via
// the PgKeyStore.GetUserKey JOIN. Always "server_kek" or "passkey" —
// both unwrap via the master-KEK RootKeyProvider.
DEKSource string
CreatedAt time.Time
RotatedAt *time.Time
}
UserKeyRecord represents a row in the user_keys table.
type UserSecret ¶
type UserSecret struct {
ID string `json:"id"`
UserID string `json:"userId"`
Name string `json:"name"`
Type SecretType `json:"type"`
Ciphertext []byte `json:"-"` // never exposed via API
KeyVersion int `json:"keyVersion"`
Version int64 `json:"version"` // value-version counter (US-70.2): create = 1, every value-affecting mutation bumps
Metadata json.RawMessage `json:"metadata"`
GlobalDefault bool `json:"globalDefault"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
UserSecret represents an encrypted secret record.
type VersionedProvider ¶
type VersionedProvider interface {
ActiveVersion() int
}
VersionedProvider is implemented by providers that expose an active key version (US-50.3/50.4). Callers that need the version for key_version column writes assert this interface on the concrete provider — it is intentionally NOT on RootKeyProvider so a future external provider (Vault Transit, which handles versioning server-side) doesn't need to implement it.
type WorkspaceBatchBuilder ¶ added in v0.26.0
type WorkspaceBatchBuilder interface {
BuildWorkspaceBatch(ctx context.Context, ownerUserID, workspaceID string) (*Batch, *BuildDegrade, error)
}
WorkspaceBatchBuilder is the one batch builder contract (design 0052 §4.1, US-70.2): session identity decides whether a caller may invoke delivery, never what decrypts. Implementations: *SecretService.
Source Files
¶
- audited_provider.go
- batch.go
- bindings_diff.go
- composite_provider.go
- credential_identity.go
- credential_store.go
- crypto.go
- derive_server_key.go
- errors.go
- injection.go
- jwt_session_janitor.go
- jwt_session_store.go
- key_service.go
- kms_aws_provider.go
- kms_gcp_provider.go
- mcp_store.go
- migration.go
- org_credential_store.go
- pg_credential_store.go
- pg_key_store.go
- pg_secret_store.go
- postgres_provider.go
- provider.go
- provider_endpoints.go
- reconcile_store.go
- redis_cache.go
- relay_batch.go
- revision_store.go
- root_key.go
- rotation.go
- secret_service.go
- staging_inspect.go
- staging_provider.go
- store.go
- types.go