Documentation
¶
Overview ¶
Package secrets seals and unseals sensitive values (BYOK API keys, OAuth credentials, OIDC client secrets) at rest. The Sealer interface lets us swap the AES-GCM master-key implementation for a KMS-backed one later without touching call sites.
Wire format of an AES-GCM sealed blob (single byte version prefix for forward compatibility):
v1: 0x01 | nonce(12) | ciphertext+tag
Authenticated additional data (AAD) binds the ciphertext to a caller-supplied context string (e.g. "api_key:<id>") so a sealed value cannot be silently moved between records.
Index ¶
- Constants
- Variables
- func AnthropicAuthorizeURL(clientID, redirectURI, challenge, state string) string
- func AnthropicRedirectURI() string
- func ApplyAnthropicRefresh(payload []byte, r RefreshResult) ([]byte, error)
- func ApplyCodexRefresh(payload []byte, r RefreshResult) ([]byte, error)
- func BuildAnthropicCredentials(r RefreshResult) ([]byte, error)
- func CodexAuthJSONPath() string
- func DecodeBase64Lenient(b64 string) ([]byte, error)
- func DefaultFileMountPath(name string) string
- func FingerprintSHA256(secret string) string
- func GuardThirdPartyOAuth(ctx context.Context, provider Provider, kind OAuthKind) error
- func IntersectHosts(a, b []string) []string
- func Last4(secret string) string
- func LoadOrCreateMasterKey(dataDir string, logf func(string, ...any)) ([]byte, error)
- func NewApiKeyID() string
- func NewGenericSecretID() string
- func NewOAuthState() (string, error)
- func NewPKCE() (verifier, challenge string, err error)
- func NewSecretsRef() string
- func OpenGenericSecret(sealer Sealer, secretID string, sealed []byte) ([]byte, error)
- func OpenOAuthPayload(sealer Sealer, userID string, kind OAuthKind, sealed []byte) ([]byte, error)
- func OpenOAuthVerifier(sealer Sealer, ownerKey string, kind OAuthKind, sealed []byte) (string, error)
- func OrgOwnerKey(tenantID string) string
- func RefreshRecord(ctx context.Context, sealer Sealer, hc *http.Client, ...) error
- func RelativeToSecretFilesMountDir(mountPath string) (string, bool)
- func RequiredSecretsError(missing []string, scope string) error
- func Resolve(ctx context.Context, store ApiKeyStore, teamID, userID string, ...) (map[Provider]Resolution, error)
- func ResolveFileMountPath(name, override string) string
- func ResolveGeneric(ctx context.Context, secretStore GenericSecretStore, teamID, userID string, ...) (map[string]GenericResolution, error)
- func ResolveGenericWithBindings(ctx context.Context, secretStore GenericSecretStore, ...) (map[string]GenericResolution, error)
- func SanitizeFileName(name string) string
- func SealAPIKey(sealer Sealer, keyID string, plaintext []byte) ([]byte, error)
- func SealGenericSecret(sealer Sealer, secretID string, plaintext []byte) ([]byte, error)
- func SealInto(sealer Sealer, rec *GenericSecret, value string) error
- func SealOAuthPayload(sealer Sealer, userID string, kind OAuthKind, payload []byte) ([]byte, error)
- func SealOAuthVerifier(sealer Sealer, ownerKey string, kind OAuthKind, verifier string) ([]byte, error)
- func SealRunBundle(sealer Sealer, runID string, b RunBundle) ([]byte, error)
- func SplitAnthropicCode(pasted string) (code, state string)
- func UnresolvedRequired(required []string, resolved map[string]bool) []string
- func ValidGenericSecretName(name string) bool
- func WithCredentials(parent context.Context, c Credentials) context.Context
- type AESGCMSealer
- type AnthropicCredentialsView
- type ApiKey
- type ApiKeyStore
- type BotSecretBinding
- type BotSecretBindingStore
- type CodexCredentialsView
- type Credentials
- type FileGenericSecretStore
- func (s *FileGenericSecretStore) Create(_ context.Context, rec GenericSecret) error
- func (s *FileGenericSecretStore) Delete(_ context.Context, id string) error
- func (s *FileGenericSecretStore) Get(_ context.Context, id string) (GenericSecret, error)
- func (s *FileGenericSecretStore) GetByName(name string) (GenericSecret, bool)
- func (s *FileGenericSecretStore) ListByTeam(_ context.Context, teamID, _ string) ([]GenericSecret, error)
- func (s *FileGenericSecretStore) ListByUser(ctx context.Context, teamID, userID string) ([]GenericSecret, error)
- func (s *FileGenericSecretStore) MarkUsed(_ context.Context, id string, at time.Time) error
- func (s *FileGenericSecretStore) Update(_ context.Context, rec GenericSecret) error
- func (s *FileGenericSecretStore) UpsertByName(sealer Sealer, name, value string, hosts []string, applyHosts bool) (GenericSecret, bool, error)
- type GenericResolution
- type GenericSecret
- type GenericSecretStore
- type LayeredGenericSecretStore
- func (l *LayeredGenericSecretStore) Create(ctx context.Context, rec GenericSecret) error
- func (l *LayeredGenericSecretStore) Delete(ctx context.Context, id string) error
- func (l *LayeredGenericSecretStore) ForScope(scope string) *FileGenericSecretStore
- func (l *LayeredGenericSecretStore) Get(ctx context.Context, id string) (GenericSecret, error)
- func (l *LayeredGenericSecretStore) Global() *FileGenericSecretStore
- func (l *LayeredGenericSecretStore) ListByTeam(ctx context.Context, teamID, userID string) ([]GenericSecret, error)
- func (l *LayeredGenericSecretStore) ListByUser(ctx context.Context, teamID, userID string) ([]GenericSecret, error)
- func (l *LayeredGenericSecretStore) ListScoped(ctx context.Context, teamID, userID string) ([]ScopedSecret, error)
- func (l *LayeredGenericSecretStore) MarkUsed(ctx context.Context, id string, at time.Time) error
- func (l *LayeredGenericSecretStore) Project() (*FileGenericSecretStore, bool)
- func (l *LayeredGenericSecretStore) Update(ctx context.Context, rec GenericSecret) error
- type MemoryApiKeyStore
- func (m *MemoryApiKeyStore) ClearDefault(_ context.Context, teamID, userID string, provider Provider, exceptID string) error
- func (m *MemoryApiKeyStore) Create(_ context.Context, k ApiKey) error
- func (m *MemoryApiKeyStore) Delete(_ context.Context, id string) error
- func (m *MemoryApiKeyStore) Get(_ context.Context, id string) (ApiKey, error)
- func (m *MemoryApiKeyStore) ListByTeam(_ context.Context, teamID, userID string) ([]ApiKey, error)
- func (m *MemoryApiKeyStore) ListByUser(_ context.Context, teamID, userID string) ([]ApiKey, error)
- func (m *MemoryApiKeyStore) MarkUsed(_ context.Context, id string, at time.Time) error
- func (m *MemoryApiKeyStore) Update(_ context.Context, k ApiKey) error
- type MemoryBotSecretBindingStore
- func (m *MemoryBotSecretBindingStore) Create(_ context.Context, b BotSecretBinding) error
- func (m *MemoryBotSecretBindingStore) Delete(_ context.Context, id string) error
- func (m *MemoryBotSecretBindingStore) Get(_ context.Context, id string) (BotSecretBinding, error)
- func (m *MemoryBotSecretBindingStore) ListByTenant(_ context.Context, tenantID string) ([]BotSecretBinding, error)
- func (m *MemoryBotSecretBindingStore) ListByTenantBot(_ context.Context, tenantID, botID string) ([]BotSecretBinding, error)
- func (m *MemoryBotSecretBindingStore) Update(_ context.Context, b BotSecretBinding) error
- type MemoryGenericSecretStore
- func (m *MemoryGenericSecretStore) Create(_ context.Context, s GenericSecret) error
- func (m *MemoryGenericSecretStore) Delete(_ context.Context, id string) error
- func (m *MemoryGenericSecretStore) Get(_ context.Context, id string) (GenericSecret, error)
- func (m *MemoryGenericSecretStore) ListByTeam(_ context.Context, teamID, userID string) ([]GenericSecret, error)
- func (m *MemoryGenericSecretStore) ListByUser(_ context.Context, teamID, userID string) ([]GenericSecret, error)
- func (m *MemoryGenericSecretStore) MarkUsed(_ context.Context, id string, at time.Time) error
- func (m *MemoryGenericSecretStore) Update(_ context.Context, s GenericSecret) error
- type MemoryOAuthPendingStore
- type MemoryOAuthStore
- func (s *MemoryOAuthStore) Delete(_ context.Context, userID string, kind OAuthKind) error
- func (s *MemoryOAuthStore) ExpiringBefore(_ context.Context, t time.Time) ([]OAuthRecord, error)
- func (s *MemoryOAuthStore) Get(_ context.Context, userID string, kind OAuthKind) (OAuthRecord, error)
- func (s *MemoryOAuthStore) ListByUser(_ context.Context, userID string) ([]OAuthRecord, error)
- func (s *MemoryOAuthStore) Upsert(_ context.Context, rec OAuthRecord) error
- type MemoryRunSecretsStore
- type MongoApiKeyStore
- func (s *MongoApiKeyStore) ClearDefault(ctx context.Context, teamID, userID string, provider Provider, exceptID string) error
- func (s *MongoApiKeyStore) Create(ctx context.Context, k ApiKey) error
- func (s *MongoApiKeyStore) Delete(ctx context.Context, id string) error
- func (s *MongoApiKeyStore) EnsureSchema(ctx context.Context) error
- func (s *MongoApiKeyStore) Get(ctx context.Context, id string) (ApiKey, error)
- func (s *MongoApiKeyStore) ListByTeam(ctx context.Context, teamID, userID string) ([]ApiKey, error)
- func (s *MongoApiKeyStore) ListByUser(ctx context.Context, teamID, userID string) ([]ApiKey, error)
- func (s *MongoApiKeyStore) MarkUsed(ctx context.Context, id string, at time.Time) error
- func (s *MongoApiKeyStore) Update(ctx context.Context, k ApiKey) error
- type MongoBotSecretBindingStore
- func (s *MongoBotSecretBindingStore) Create(ctx context.Context, b BotSecretBinding) error
- func (s *MongoBotSecretBindingStore) Delete(ctx context.Context, id string) error
- func (s *MongoBotSecretBindingStore) EnsureSchema(ctx context.Context) error
- func (s *MongoBotSecretBindingStore) Get(ctx context.Context, id string) (BotSecretBinding, error)
- func (s *MongoBotSecretBindingStore) ListByTenant(ctx context.Context, tenantID string) ([]BotSecretBinding, error)
- func (s *MongoBotSecretBindingStore) ListByTenantBot(ctx context.Context, tenantID, botID string) ([]BotSecretBinding, error)
- func (s *MongoBotSecretBindingStore) Update(ctx context.Context, b BotSecretBinding) error
- type MongoGenericSecretStore
- func (s *MongoGenericSecretStore) Create(ctx context.Context, rec GenericSecret) error
- func (s *MongoGenericSecretStore) Delete(ctx context.Context, id string) error
- func (s *MongoGenericSecretStore) EnsureSchema(ctx context.Context) error
- func (s *MongoGenericSecretStore) Get(ctx context.Context, id string) (GenericSecret, error)
- func (s *MongoGenericSecretStore) ListByTeam(ctx context.Context, teamID, userID string) ([]GenericSecret, error)
- func (s *MongoGenericSecretStore) ListByUser(ctx context.Context, teamID, userID string) ([]GenericSecret, error)
- func (s *MongoGenericSecretStore) MarkUsed(ctx context.Context, id string, at time.Time) error
- func (s *MongoGenericSecretStore) Update(ctx context.Context, rec GenericSecret) error
- type MongoOAuthPendingStore
- type MongoOAuthStore
- func (s *MongoOAuthStore) Delete(ctx context.Context, userID string, kind OAuthKind) error
- func (s *MongoOAuthStore) EnsureSchema(ctx context.Context) error
- func (s *MongoOAuthStore) ExpiringBefore(ctx context.Context, t time.Time) ([]OAuthRecord, error)
- func (s *MongoOAuthStore) Get(ctx context.Context, userID string, kind OAuthKind) (OAuthRecord, error)
- func (s *MongoOAuthStore) ListByUser(ctx context.Context, userID string) ([]OAuthRecord, error)
- func (s *MongoOAuthStore) Upsert(ctx context.Context, rec OAuthRecord) error
- type MongoRunSecretsStore
- func (s *MongoRunSecretsStore) Delete(ctx context.Context, id string) error
- func (s *MongoRunSecretsStore) EnsureSchema(ctx context.Context) error
- func (s *MongoRunSecretsStore) Get(ctx context.Context, id string) (RunSecretsRecord, error)
- func (s *MongoRunSecretsStore) Put(ctx context.Context, rec RunSecretsRecord) error
- type OAuthKind
- type OAuthPending
- type OAuthPendingStore
- type OAuthRecord
- type OAuthRefreshWorker
- type OAuthStore
- type Provider
- type RefreshResult
- func ExchangeAnthropicCode(ctx context.Context, hc *http.Client, ...) (RefreshResult, error)
- func RefreshAnthropic(ctx context.Context, hc *http.Client, clientID, refreshToken string) (RefreshResult, error)
- func RefreshCodex(ctx context.Context, hc *http.Client, clientID, refreshToken string) (RefreshResult, error)
- type Resolution
- type RunBundle
- type RunSecretsRecord
- type RunSecretsStore
- type ScopedSecret
- type Sealer
Constants ¶
const ApiKeysCollectionName = "api_keys"
const BotSecretBindingsCollectionName = "bot_secret_bindings"
const DefaultAnthropicOAuthClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
Browser OAuth (authorization-code + PKCE) for the Claude Code subscription "forfait". This reproduces exactly what `claude login` does — same public client, same PKCE S256, same endpoints — but driven from the studio so a cloud operator never has to run the CLI in a pod nor paste a credentials.json file.
Why the code-paste (headless) redirect and not a silent callback: the public Claude Code OAuth client only permits two redirect URIs — Anthropic's own code-display page, or a localhost loopback. A remote studio is neither, so it cannot register itself as a redirect target. The headless page shows the user a `code#state` string they paste back; iterion exchanges it server-side. This is the only flow that works from a cloud-hosted studio.
All values are env-overridable because this is an undocumented, reverse-engineered surface: if Anthropic rotates the client/flow, an operator can re-point it without a rebuild. DefaultAnthropicOAuthClientID is the public Claude Code OAuth client id. It is a PUBLIC PKCE client (no client secret), the same id the `claude` CLI embeds — not a confidential value. Shipping it as a default lets the browser connect flow work out of the box; an operator can still override it via ITERION_OAUTH_FORFAIT_ANTHROPIC_CLIENT_ID if Anthropic rotates the client.
const DefaultOAuthPendingTTL = 10 * time.Minute
DefaultOAuthPendingTTL bounds how long a started browser-OAuth flow can wait for the user to paste the code back.
const DefaultRunSecretsTTL = 24 * time.Hour
DefaultRunSecretsTTL bounds how long a sealed bundle can live untouched. Resume paths re-publish so the runner can always re- fetch even after a TTL eviction (the publisher will re-resolve).
const GenericSecretsCollectionName = "generic_secrets"
const LocalScopeTeam = "local"
LocalScopeTeam is the synthetic team id stamped on every secret held by the local (desktop / headless) file-backed store. Resolution reuses the cloud path verbatim — ResolveGeneric requires a non-empty team id and the Mongo store keys on a tenant pulled from context; the file store has no tenant, so it pins this constant instead, and ResolveGeneric is called as ResolveGeneric(ctx, store, LocalScopeTeam, "", names, sealer).
const LocalSecretsFileName = "secrets.json"
LocalSecretsFileName is the basename of the sealed store, used for both the global (~/.iterion) and per-project (<repo>/.iterion) files.
const ( // MasterKeyFileName is the basename of the keyfile fallback used when // the OS keychain is unavailable (headless servers with no dbus/session). MasterKeyFileName = "secrets.key" )
const OAuthCollectionName = "oauth_credentials"
const OAuthPendingCollectionName = "oauth_pending"
const OrgOwnerPrefix = "org:"
OrgOwnerPrefix marks an OAuthRecord whose owner is a team/org rather than an individual user. An org-scoped forfait is stored as an ordinary OAuthRecord whose UserID is OrgOwnerKey(tenantID) — this reuses the whole store/seal/refresh machinery (AAD, Mongo id, ExpiringBefore) without a schema change. The cloud publisher uses these as a FALLBACK when the run's owner has no personal record, covering automated runs (webhook/dispatcher/cron) whose owner is a synthetic identity. See OrgOwnerKey.
const RunSecretsCollectionName = "run_secrets"
const SecretFilesMountDir = "/run/iterion/secrets"
const XAIDefaultBaseURL = "https://api.x.ai"
XAIDefaultBaseURL is the host claw's OpenAI-compatible client targets for xAI Grok. The openai provider appends `/v1/chat/completions`, so this value must NOT include a trailing `/v1` (unlike the public OpenAI-SDK convention of `base_url=https://api.x.ai/v1`). Override with `XAI_BASE_URL` when pointing at a proxy or regional endpoint.
const ZAIDefaultBaseURL = "https://api.z.ai/api/anthropic"
ZAIDefaultBaseURL is the Anthropic-compatible endpoint z.ai's Coding Plan publishes. Centralised here so the delegate and the model registry agree without re-deriving the URL.
Variables ¶
var ( ErrBindingNotFound = errors.New("secrets: bot secret binding not found") ErrBindingTenantMissing = errors.New("secrets: bot secret binding store called without tenant context") )
var ( ErrGenericSecretNotFound = errors.New("secrets: generic secret not found") ErrGenericSecretTenantMissing = errors.New("secrets: generic secret store called without tenant context") )
var ( ErrSealedFormat = errors.New("secrets: invalid sealed format") ErrSealedVersion = errors.New("secrets: unsupported sealed version") ErrSealedAuthenticate = errors.New("secrets: authentication failed") )
Errors returned by Open when the input is malformed, the master key is wrong, or the ciphertext was tampered with. All three look the same on the wire to avoid oracle attacks; we still tag them internally for logging.
var (
ErrApiKeyNotFound = errors.New("secrets: api key not found")
)
Sentinel errors raised by Store implementations.
var ErrApiKeyTenantMissing = errors.New("secrets: ApiKey store called without tenant context")
ErrApiKeyTenantMissing is returned when an ApiKeyStore call lacks the tenant_id needed to scope its query. Callers must propagate it up — falling back to an unscoped query would expose every tenant's keys.
var ErrNotRefreshable = errors.New("secrets: credential has no refresh token")
ErrNotRefreshable marks a credential whose sealed payload carries no refresh token: no refresh exchange can ever succeed for it, so callers must skip it (worker) or tell the user to re-connect (HTTP) instead of retrying forever.
var ErrOAuthForfaitInThirdParty = errors.New("secrets: refusing to use Claude Code OAuth-forfait via third-party SDK (CGU violation)")
ErrOAuthForfaitInThirdParty is the sentinel error guarding the claw backend (and any other in-process LLM SDK consumer) from using a Claude Pro/Max OAuth bearer token. Reusing the forfait outside the official Claude Code CLI surface violates Anthropic's Consumer Terms — see memory feedback_no_anthropic_oauth_in_third_party.
Callers should invoke GuardThirdPartyOAuth right before consuming a credential for an in-process LLM call. The delegate backends (claude_code, codex) which spawn the upstream CLI are exempt: the CLI itself remains the authorised consumer in that path.
var ErrOAuthNotFound = errors.New("secrets: oauth record not found")
ErrOAuthNotFound is the sentinel for missing records.
var ErrOAuthPendingNotFound = errors.New("secrets: oauth pending not found")
ErrOAuthPendingNotFound is the sentinel for a missing/expired pending.
var ErrRunSecretsNotFound = errors.New("secrets: run secrets not found")
ErrRunSecretsNotFound is returned by Get when the ref is unknown (already consumed or never published).
Functions ¶
func AnthropicAuthorizeURL ¶
AnthropicAuthorizeURL builds the claude.ai authorization URL the studio opens in a new tab. challenge is the PKCE S256 challenge; state is round-tripped and validated on completion.
func AnthropicRedirectURI ¶
func AnthropicRedirectURI() string
AnthropicRedirectURI returns the redirect URI used for the headless code-paste flow (env-overridable).
func ApplyAnthropicRefresh ¶
func ApplyAnthropicRefresh(payload []byte, r RefreshResult) ([]byte, error)
ApplyAnthropicRefresh updates a credentials.json blob with fresh tokens. Returns the new JSON to seal back into the OAuthRecord.
func ApplyCodexRefresh ¶
func ApplyCodexRefresh(payload []byte, r RefreshResult) ([]byte, error)
ApplyCodexRefresh updates an auth.json blob with fresh tokens.
func BuildAnthropicCredentials ¶
func BuildAnthropicCredentials(r RefreshResult) ([]byte, error)
BuildAnthropicCredentials renders a fresh credentials.json blob (the `{claudeAiOauth:{…}}` shape the Claude Code CLI reads) from an exchange/refresh result, so the browser flow produces the exact same sealed payload the file-paste path would. Reuses ApplyAnthropicRefresh over an empty object — no duplicate serialisation.
func CodexAuthJSONPath ¶
func CodexAuthJSONPath() string
CodexAuthJSONPath returns the on-disk location of Codex CLI's auth.json, honouring the `CODEX_HOME` env var (Codex's documented override) and falling back to `~/.codex/auth.json`. Returns an empty string when no home directory is resolvable, leaving callers to treat it as "no auth".
func DecodeBase64Lenient ¶
DecodeBase64Lenient accepts std/URL/raw variants of base64. Exposed for callers that need to decode an operator-supplied key (notably pkg/auth's JWT secret loader).
func DefaultFileMountPath ¶
DefaultFileMountPath returns the stable in-sandbox file path for a workflow secret mounted as a file. The path is deterministic so prompts can reference it before the sandbox container is started.
func FingerprintSHA256 ¶
FingerprintSHA256 returns a stable 16-char hex fingerprint of a secret value. Useful for logs that need to correlate two records using the same key without ever revealing the secret.
func GuardThirdPartyOAuth ¶
GuardThirdPartyOAuth returns ErrOAuthForfaitInThirdParty when the given ctx has an OAuth-forfait connection for kind but no matching API key for provider — i.e. the only available credential is the forfait, which is forbidden in this code path.
Returns nil when the ctx has no credentials, when an API key IS available, or when no OAuth credential of that kind is present.
func IntersectHosts ¶
IntersectHosts returns the stricter of two egress host policies. An empty list means "no restriction"; the result never broadens either input:
- both empty -> empty (unrestricted)
- one empty, one set -> the set (the restriction wins)
- both set -> their intersection
func Last4 ¶
Last4 returns the last four characters of a secret for display. If the input is shorter than 8, it returns "****" — never reveal more than half of a short value.
func LoadOrCreateMasterKey ¶
LoadOrCreateMasterKey resolves the 32-byte AES-GCM master key that seals the local secret store, in this precedence order:
- ITERION_SECRETS_KEY (base64) — explicit operator override (parity with cloud, scriptable/CI). Must decode to exactly 32 bytes.
- An existing keyfile at <dataDir>/secrets.key — a prior run already chose the keyfile; reuse it so a store sealed on a headless host stays openable even if a keychain later becomes available (no orphaning).
- The OS keychain entry, when present.
- Keychain empty but functional → generate a fresh key and store it in the keychain.
- Keychain unavailable → generate a fresh key and write the keyfile (0600), logging the fallback explicitly (no silent recovery).
logf is an optional Warn-level sink (pass logger.Warn; nil is a no-op).
func NewApiKeyID ¶
func NewApiKeyID() string
NewApiKeyID returns a fresh UUID-string id for an ApiKey record. Centralised so the routes layer doesn't reach for uuid directly.
func NewGenericSecretID ¶
func NewGenericSecretID() string
func NewOAuthState ¶
NewOAuthState returns a random URL-safe state token for CSRF binding.
func NewPKCE ¶
NewPKCE returns a fresh (verifier, challenge) pair. verifier is a 43-char base64url-encoded 32-byte random value; challenge is base64url(SHA256(verifier)), per RFC 7636 S256.
func NewSecretsRef ¶
func NewSecretsRef() string
NewSecretsRef returns a fresh opaque ref for a RunSecretsRecord. Random UUID rather than the run id so an attacker who can guess run ids cannot enumerate sealed bundles.
func OpenGenericSecret ¶
func OpenOAuthPayload ¶
OpenOAuthPayload is the inverse: returns the raw JSON blob.
func OpenOAuthVerifier ¶
func OpenOAuthVerifier(sealer Sealer, ownerKey string, kind OAuthKind, sealed []byte) (string, error)
OpenOAuthVerifier is the inverse of SealOAuthVerifier.
func OrgOwnerKey ¶
OrgOwnerKey returns the synthetic owner key under which a team/org's shared forfait credential is stored.
func RefreshRecord ¶
func RefreshRecord(ctx context.Context, sealer Sealer, hc *http.Client, anthropicClientID, codexClientID string, rec *OAuthRecord) error
RefreshRecord drives the refresh exchange for one OAuthRecord and rewrites its sealed payload in place with the new tokens. It is the single refresh primitive shared by the HTTP handler (manual refresh) and the background OAuthRefreshWorker — keep them on this function so the two paths can never drift.
The (userID, kind) AAD is derived from rec, so an org-scoped record (rec.UserID == OrgOwnerKey(tenantID)) refreshes identically to a personal one. Returns an error when the provider rejects the refresh or no client_id is configured for the record's kind.
func RelativeToSecretFilesMountDir ¶
RelativeToSecretFilesMountDir returns mountPath relative to the default file-secret directory when mountPath lives directly under it. The caller is expected to validate mountPath as a clean absolute file path first.
func RequiredSecretsError ¶ added in v0.43.0
RequiredSecretsError formats the launch-blocking error naming every unresolved required secret. `scope` describes the resolution context so the operator knows where the lookup came up empty (e.g. "this team/bot" for a cloud launch, "this workspace" for a local run). Returns nil when nothing is missing so callers can `if err := RequiredSecretsError(...); err != nil`.
func Resolve ¶
func Resolve( ctx context.Context, store ApiKeyStore, teamID, userID string, providers []Provider, keyOverrides map[Provider]string, sealer Sealer, ) (map[Provider]Resolution, error)
Resolve returns at most one ApiKey for each requested provider, applying the priority chain documented in the cloud admin plan:
- KeyOverrides[provider] — caller-pinned key id (validated to belong to the team and to be visible to userID).
- (team, userID, provider, default=true)
- (team, userID, provider) — first match
- (team, "", provider, default=true)
- (team, "", provider) — first match
Providers without a hit are simply omitted. Callers consult the returned map and either inject what's there or fall back to env.
When sealer is non-nil, every Resolution.Plaintext is decrypted; on decrypt failure the resolution is skipped and an error is logged to logErr. Pass nil sealer to get sealed blobs only.
func ResolveFileMountPath ¶
func ResolveGeneric ¶
func ResolveGenericWithBindings ¶
func ResolveGenericWithBindings( ctx context.Context, secretStore GenericSecretStore, bindingStore BotSecretBindingStore, teamID, userID, botID string, names []string, secretOverrides map[string]string, sealer Sealer, logger *iterlog.Logger, ) (map[string]GenericResolution, error)
ResolveGenericWithBindings resolves each requested workflow-secret name with the priority user-scoped > bot-binding > team-scoped:
- a developer's personal secret of that name still wins (interactive opt-in);
- else a bot binding maps the name to a stored team-scoped secret — the canonical route for unattended (webhook) runs whose synthetic actor owns no user secrets;
- else a team-scoped secret of that name (the existing fallback).
Binding-sourced resolutions carry the binding's AllowedHosts so the caller can intersect the egress policy.
func SanitizeFileName ¶ added in v0.50.0
SanitizeFileName reduces a secret name to a safe basename for a secret file (letters, digits, `_`, `.`, `-`). It is the shared rule behind DefaultFileMountPath (sandbox mount) and the host-side materialisation used by non-sandbox runs, so the same secret lands under the same filename on either path.
func SealAPIKey ¶
SealAPIKey produces the sealed blob for storage. Pass the caller's shared Sealer (e.g. ITERION_SECRETS_KEY-driven AESGCMSealer) and the freshly-generated key ID; the AAD ties the ciphertext to that record so it cannot be moved.
func SealGenericSecret ¶
func SealInto ¶
func SealInto(sealer Sealer, rec *GenericSecret, value string) error
SealInto seals value for rec.ID and stamps the three derived fields (SealedSecret + Last4 + Fingerprint) onto rec. rec.ID must already be set (the AAD binds the ciphertext to it). This owns the seal-and-stamp triple so the CLI and every REST surface share one definition instead of hand-copying it (a missed field would silently ship a stale last4/fingerprint).
func SealOAuthPayload ¶
SealOAuthPayload encrypts the raw credentials JSON. AAD binds the ciphertext to (userID, kind) so a sealed payload moved between users or kinds cannot be opened.
func SealOAuthVerifier ¶
func SealOAuthVerifier(sealer Sealer, ownerKey string, kind OAuthKind, verifier string) ([]byte, error)
SealOAuthVerifier seals a PKCE verifier bound to (ownerKey, kind) so a persisted pending record never stores a usable verifier at rest.
func SealRunBundle ¶
SealRunBundle marshals + seals a RunBundle for a given run. Returns the sealed blob; the caller stores it as RunSecretsRecord.SealedBundle.
func SplitAnthropicCode ¶
SplitAnthropicCode splits the `code#state` string Anthropic's headless page shows the user. When no `#` is present the whole input is the code and state is empty (the caller then relies on its server-side pending record for CSRF binding).
func UnresolvedRequired ¶ added in v0.43.0
UnresolvedRequired returns, sorted, the names in `required` that are absent from `resolved` (the set of workflow-secret names that resolved to a non-empty value for this run).
It is the launch-time gate behind the required-secret contract: a workflow declares a secret non-`optional` precisely because the run cannot do correct work without it (push with no auth, call an API unauthenticated). If such a secret resolves to nothing — no store match, no binding, no override — the launch must fail loudly here rather than the runner silently skipping the empty value (`optional/unresolved → skip`) and the bot proceeding blind. `optional: true` secrets are never passed in `required`, so they keep the skip behaviour.
func ValidGenericSecretName ¶
ValidGenericSecretName reports whether name is a legal secret name: a non-empty identifier of [A-Za-z_][A-Za-z0-9_]* up to 128 chars (the same rule the cloud secret routes enforce). A valid name is also a valid POSIX env-var name, which the file-mount / env-indirection paths rely on.
func WithCredentials ¶
func WithCredentials(parent context.Context, c Credentials) context.Context
WithCredentials returns a child ctx carrying the resolved credentials. Empty / zero-value Credentials are still stored so callers can detect "we are inside a per-run scope with no keys" vs "no credentials ctx at all" (env fallback).
Types ¶
type AESGCMSealer ¶
type AESGCMSealer struct {
// contains filtered or unexported fields
}
AESGCMSealer is an AES-256-GCM implementation of Sealer driven by a single master key. The master key MUST be 32 bytes; supply it via NewAESGCMSealer or NewAESGCMSealerFromBase64.
func NewAESGCMSealer ¶
func NewAESGCMSealer(masterKey []byte) (*AESGCMSealer, error)
NewAESGCMSealer constructs a sealer from a 32-byte master key. The key is consumed immediately by aes.NewCipher; callers should avoid retaining the slice elsewhere.
func NewAESGCMSealerFromBase64 ¶
func NewAESGCMSealerFromBase64(b64 string) (*AESGCMSealer, error)
NewAESGCMSealerFromBase64 decodes a standard or URL-safe base64 master key (with or without padding) and constructs a sealer. Suitable for reading directly from ITERION_SECRETS_KEY.
func NewLocalSealer ¶
func NewLocalSealer(dataDir string, logf func(string, ...any)) (*AESGCMSealer, error)
NewLocalSealer resolves the master key (LoadOrCreateMasterKey) and builds the AES-GCM sealer for the local secret store. Convenience for the studio/ CLI wiring so callers don't repeat the key→sealer dance.
type AnthropicCredentialsView ¶
type AnthropicCredentialsView struct {
ClaudeAIOauth struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
ExpiresAt int64 `json:"expiresAt"` // ms epoch
Scopes []string `json:"scopes,omitempty"`
} `json:"claudeAiOauth"`
}
AnthropicCredentialsView is the minimal shape we extract from a Claude Code credentials.json blob to drive expiry tracking + refresh. We do NOT replace the user-supplied JSON with this struct on store — extra fields the CLI cares about round-trip via the sealed payload.
func ParseAnthropicView ¶
func ParseAnthropicView(payload []byte) (AnthropicCredentialsView, error)
ParseAnthropicView extracts the lightweight metadata view from a raw credentials.json blob. Returns the parsed view; errors when the JSON is malformed but never inspects scopes / expiry validity.
type ApiKey ¶
type ApiKey struct {
ID string `bson:"_id" json:"id"`
TenantID string `bson:"tenant_id" json:"tenant_id"`
ScopeTeamID string `bson:"scope_team" json:"scope_team_id"`
ScopeUserID string `bson:"scope_user,omitempty" json:"scope_user_id,omitempty"`
Provider Provider `bson:"provider" json:"provider"`
Name string `bson:"name" json:"name"`
Last4 string `bson:"last4,omitempty" json:"last4,omitempty"`
SealedSecret []byte `bson:"sealed_secret" json:"-"`
IsDefault bool `bson:"is_default,omitempty" json:"is_default,omitempty"`
CreatedBy string `bson:"created_by" json:"created_by"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
LastUsedAt *time.Time `bson:"last_used_at,omitempty" json:"last_used_at,omitempty"`
ExpiresAt *time.Time `bson:"expires_at,omitempty" json:"expires_at,omitempty"`
Fingerprint string `bson:"fingerprint,omitempty" json:"fingerprint,omitempty"`
}
ApiKey is a BYOK record: a single API key (or AWS-style credential blob, JSON-encoded inside SealedSecret) attached to a team and optionally scoped to a single user. The plaintext secret is never persisted — only the AES-GCM-sealed blob.
Scope semantics:
- ScopeUserID == "": team-wide. Any member of the team picks it up when their per-user keys do not provide the requested provider.
- ScopeUserID != "": user-only. Visible only to that user even when listed by other team members (the API list endpoint hides them).
Default flag: per (team, user, provider) tuple at most ONE entry is flagged is_default. Resolution prefers it over non-default keys.
type ApiKeyStore ¶
type ApiKeyStore interface {
Create(ctx context.Context, k ApiKey) error
Get(ctx context.Context, id string) (ApiKey, error)
Update(ctx context.Context, k ApiKey) error
Delete(ctx context.Context, id string) error
// ListByTeam returns every key visible from teamID — i.e. team-
// scoped keys plus the requesting user's user-scoped keys. The
// requestingUserID filter MUST be applied; passing "" returns
// only team-wide keys (admin path).
ListByTeam(ctx context.Context, teamID, requestingUserID string) ([]ApiKey, error)
// ListByUser returns the requesting user's user-scoped keys
// inside a given team.
ListByUser(ctx context.Context, teamID, userID string) ([]ApiKey, error)
// MarkUsed updates last_used_at without altering anything else.
MarkUsed(ctx context.Context, id string, at time.Time) error
// ClearDefault removes the is_default flag from any other key in
// the same (team, user, provider) tuple. Used when a new key is
// created with is_default=true or an existing one is promoted.
ClearDefault(ctx context.Context, teamID, userID string, provider Provider, exceptID string) error
}
ApiKeyStore is the persistence interface for BYOK records.
type BotSecretBinding ¶
type BotSecretBinding struct {
ID string `bson:"_id" json:"id"`
TenantID string `bson:"tenant_id" json:"tenant_id"`
BotID string `bson:"bot_id" json:"bot_id"`
SecretID string `bson:"secret_id" json:"secret_id"` // -> GenericSecret._id
SecretNameForWorkflow string `bson:"secret_name_for_workflow" json:"secret_name_for_workflow"`
// AllowedHosts, when non-empty, intersects (never broadens) the
// workflow secret's declared egress hosts. This is an ENFORCED egress
// control: the resolver carries it on GenericResolution.AllowedHosts,
// the publisher threads it onto RunBundle.GenericSecretHosts, the
// runner puts it on Credentials.GenericHosts, and the secret guard
// intersects it with the workflow's `secrets.<name>.hosts`
// (model.effectiveSecretHosts). Empty = no binding-level restriction.
AllowedHosts []string `bson:"allowed_hosts,omitempty" json:"allowed_hosts,omitempty"`
CreatedBy string `bson:"created_by" json:"created_by"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
}
BotSecretBinding is a policy wrapper over an existing generic secret: it makes a stored org/user secret resolvable for a specific bot under the name the bot's workflow declares in its `secrets:` block. It does NOT store secret material — only a reference (SecretID) plus optional tightening (AllowedHosts narrows the egress policy).
type BotSecretBindingStore ¶
type BotSecretBindingStore interface {
Create(ctx context.Context, b BotSecretBinding) error
Get(ctx context.Context, id string) (BotSecretBinding, error)
Update(ctx context.Context, b BotSecretBinding) error
Delete(ctx context.Context, id string) error
ListByTenantBot(ctx context.Context, tenantID, botID string) ([]BotSecretBinding, error)
ListByTenant(ctx context.Context, tenantID string) ([]BotSecretBinding, error)
}
BotSecretBindingStore persists bot-secret bindings.
type CodexCredentialsView ¶
type CodexCredentialsView struct {
AuthMode string `json:"auth_mode,omitempty"`
Tokens struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token,omitempty"`
ExpiresIn int64 `json:"expires_in,omitempty"`
AccountID string `json:"account_id,omitempty"`
} `json:"tokens"`
LastRefresh string `json:"last_refresh,omitempty"`
}
CodexCredentialsView is the analogous shape for the Codex CLI's auth.json. Field names mirror the Codex SDK.
AuthMode is "apikey" or "chatgpt" — Codex CLI sets it based on how the user signed in. Tokens.AccountID is only populated in "chatgpt" mode and is required by the ChatGPT-Codex backend (sent verbatim in the `ChatGPT-Account-ID` request header).
func LoadCodexCredentialsFrom ¶ added in v0.50.0
func LoadCodexCredentialsFrom(dir string) (CodexCredentialsView, error)
LoadCodexCredentialsFrom reads and parses Codex CLI's auth.json from an EXPLICIT CODEX_HOME-shaped directory (`<dir>/auth.json`), rather than the process's default location. This is the cloud path: the runner materialises a tenant's resolved codex OAuth-forfait into a per-run temp dir (Credentials.OAuthDir("codex")), and the in-process claw model factory reads it from there instead of the pod's (empty) ~/.codex. Empty dir → error.
func LoadCodexCredentialsFromDisk ¶
func LoadCodexCredentialsFromDisk() (CodexCredentialsView, error)
LoadCodexCredentialsFromDisk reads and parses Codex CLI's auth.json from its standard location. Returns the parsed view on success; on missing or malformed file it returns the zero view plus a non-nil error. Callers gating on availability should use `errors.Is(err, fs.ErrNotExist)` to distinguish "no auth installed" from "auth file is corrupted".
The reader does not validate token expiry — refresh is delegated to Codex CLI's background process; iterion just reads whatever access_token is currently materialised on disk.
func ParseCodexView ¶
func ParseCodexView(payload []byte) (CodexCredentialsView, error)
ParseCodexView extracts the analogous view from auth.json.
func (CodexCredentialsView) IsChatGPTMode ¶
func (v CodexCredentialsView) IsChatGPTMode() bool
IsChatGPTMode reports whether the auth blob authorises ChatGPT-Codex backend access (forfait), with the access token + account id required to actually issue requests.
type Credentials ¶
type Credentials struct {
APIKeys map[Provider]string
// Generic maps workflow/user secret names to plaintext values. It is
// populated by the cloud runner from sealed per-run bundles and used
// by declared workflow secrets whose value is intentionally empty
// (meaning "resolve by name from the user's/team's stored secrets").
Generic map[string]string
// GenericHosts maps a generic secret name to the egress host
// allowlist a bot-secret binding imposes on it (empty/absent = no
// binding restriction). The secret guard intersects it with the
// workflow's declared `secrets.<name>.hosts` so a binding can only
// narrow, never broaden, where the credential may egress.
GenericHosts map[string][]string
// GenericRefs maps a generic secret name to its generic-secret store
// record ID (IDs only, never values). It lets the runner re-read the
// server-refreshed record mid-run and rewrite the secret's
// materialised file before a short-TTL credential (e.g. a 1h GitHub
// App installation token) expires under a long run.
GenericRefs map[string]string
// OAuthCredentialFiles maps "claude_code" / "codex" → the
// absolute path of a temp directory holding the materialised
// credentials.json or auth.json. The delegate backends pass
// this directory via CLAUDE_CONFIG_DIR / CODEX_HOME to the
// CLI subprocess. Empty when no OAuth-forfait is in play.
OAuthCredentialFiles map[string]string
// ForgeAppBotLogin, when set, is the GitHub-App bot login whose
// installation token pushes this run's commits (see RunBundle). The
// runner uses it to seed the App-bot git committer identity, which a
// bare installation token can't self-resolve. Empty for PAT/OAuth runs.
ForgeAppBotLogin string
}
Credentials carries the resolved per-run BYOK plaintext keyed by provider. Stamped into context by the runner right before the engine starts, consumed by pkg/backend/model/registry.go and the claude_code/codex delegate backends.
Plaintexts here are sensitive: never log them, never include them in events. The runner zeroes the slice after the run completes (best effort — Go does not give us secure-erase guarantees, but the bundle's TTL bounds exposure on the wire and at rest).
func CredentialsFromContext ¶
func CredentialsFromContext(ctx context.Context) (Credentials, bool)
CredentialsFromContext returns the resolved credentials and a flag indicating whether a per-run scope was active at all.
func ResolveLocalCredentials ¶
func ResolveLocalCredentials(ctx context.Context, store GenericSecretStore, sealer Sealer, names []string, logger *iterlog.Logger) (Credentials, error)
ResolveLocalCredentials resolves the named secrets from a local (file-backed) GenericSecretStore into a Credentials value ready to stamp into a run ctx via WithCredentials. It is the in-process, single-hop equivalent of the cloud runner's injectCredentials (which decrypts a sealed per-run bundle shipped over NATS): here the store IS local, so resolution reads + opens the sealed values directly.
names is typically the workflow's declared `secrets:` keys — the only names a `{{secrets.X}}` reference can legally use (compile-checked). Returns an empty (non-nil-map) Credentials when store/sealer is nil or names is empty, so callers can WithCredentials unconditionally.
func (Credentials) APIKey ¶
func (c Credentials) APIKey(p Provider) string
APIKey returns the plaintext API key for the requested provider (or "" when none is configured for the run).
func (Credentials) GenericSecret ¶
func (c Credentials) GenericSecret(name string) string
func (Credentials) OAuthDir ¶
func (c Credentials) OAuthDir(kind string) string
OAuthDir returns the temp dir holding sealed credentials for kind (claude_code / codex), or "" when no OAuth bundle was injected.
type FileGenericSecretStore ¶
type FileGenericSecretStore struct {
// contains filtered or unexported fields
}
FileGenericSecretStore is a filesystem-backed GenericSecretStore for the local (desktop / CLI / non-cloud studio) path. Values are AES-GCM sealed by the caller (SealGenericSecret) before Create/Update — the file never holds plaintext. The store keeps an in-memory index guarded by a mutex and persists the whole file atomically (0600) on every mutation, mirroring the plugin registry's load/save idiom.
func NewFileGenericSecretStore ¶
func NewFileGenericSecretStore(path string) (*FileGenericSecretStore, error)
NewFileGenericSecretStore opens (or lazily creates) a sealed secrets file at path. A missing file is treated as an empty store; a malformed file is a hard error (we never silently discard a store the operator may be able to recover).
func (*FileGenericSecretStore) Create ¶
func (s *FileGenericSecretStore) Create(_ context.Context, rec GenericSecret) error
func (*FileGenericSecretStore) Delete ¶
func (s *FileGenericSecretStore) Delete(_ context.Context, id string) error
func (*FileGenericSecretStore) Get ¶
func (s *FileGenericSecretStore) Get(_ context.Context, id string) (GenericSecret, error)
func (*FileGenericSecretStore) GetByName ¶
func (s *FileGenericSecretStore) GetByName(name string) (GenericSecret, bool)
GetByName returns the secret with the given name, or ErrGenericSecretNotFound. Not part of GenericSecretStore — used by the CLI/handlers to implement upsert-by-name (a friendlier `secret set` than create-only).
func (*FileGenericSecretStore) ListByTeam ¶
func (s *FileGenericSecretStore) ListByTeam(_ context.Context, teamID, _ string) ([]GenericSecret, error)
ListByTeam returns every secret in the file. userID is accepted for interface parity but the local store has no user scoping — a local secret is always "team"-wide (ScopeUserID empty). teamID must be LocalScopeTeam.
func (*FileGenericSecretStore) ListByUser ¶
func (s *FileGenericSecretStore) ListByUser(ctx context.Context, teamID, userID string) ([]GenericSecret, error)
ListByUser mirrors ListByTeam for the local store (no per-user scoping).
func (*FileGenericSecretStore) Update ¶
func (s *FileGenericSecretStore) Update(_ context.Context, rec GenericSecret) error
func (*FileGenericSecretStore) UpsertByName ¶
func (s *FileGenericSecretStore) UpsertByName(sealer Sealer, name, value string, hosts []string, applyHosts bool) (GenericSecret, bool, error)
UpsertByName atomically creates or rotates (by name) a secret, sealing value with sealer, under the cross-process lock — so a concurrent create of the same name from another goroutine/process cannot produce a duplicate record. On rotate, the egress host lock is overwritten only when applyHosts is true (callers preserve it otherwise, so a value rotation never silently broadens egress). Returns the resulting record and whether it was newly created.
type GenericResolution ¶
type GenericResolution struct {
Name string
SecretID string
Plaintext []byte
SealedBlob []byte
SourceScope string // "user" | "binding" | "team"
// AllowedHosts is the egress host allowlist a bot-secret binding
// imposes on this secret (empty = no extra restriction). The publisher
// threads it onto RunBundle.GenericSecretHosts; the runner's secret
// guard intersects it with the workflow's declared hosts
// (model.effectiveSecretHosts) so a binding can only narrow, never
// broaden, the policy.
AllowedHosts []string
}
type GenericSecret ¶
type GenericSecret struct {
ID string `bson:"_id" json:"id"`
TenantID string `bson:"tenant_id" json:"tenant_id"`
ScopeTeamID string `bson:"scope_team" json:"scope_team_id"`
ScopeUserID string `bson:"scope_user,omitempty" json:"scope_user_id,omitempty"`
Name string `bson:"name" json:"name"`
Last4 string `bson:"last4,omitempty" json:"last4,omitempty"`
SealedSecret []byte `bson:"sealed_secret" json:"-"`
CreatedBy string `bson:"created_by" json:"created_by"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
LastUsedAt *time.Time `bson:"last_used_at,omitempty" json:"last_used_at,omitempty"`
Fingerprint string `bson:"fingerprint,omitempty" json:"fingerprint,omitempty"`
// AllowedHosts, when non-empty, pins the egress host allowlist this
// secret may ever be materialised toward (parent-domain match, e.g.
// "github.com" also permits "api.github.com"). A managed forge token is
// created with its forge host here so a prompt-injected bot cannot
// exfiltrate it off-forge. It is the secret's OWN egress lock: it travels
// through every resolution tier (buildGenericResolution) and is
// intersected — never broadened — with any binding/workflow host policy
// downstream (IntersectHosts / model.effectiveSecretHosts).
AllowedHosts []string `bson:"allowed_hosts,omitempty" json:"allowed_hosts,omitempty"`
}
type GenericSecretStore ¶
type GenericSecretStore interface {
Create(ctx context.Context, s GenericSecret) error
Get(ctx context.Context, id string) (GenericSecret, error)
Update(ctx context.Context, s GenericSecret) error
Delete(ctx context.Context, id string) error
ListByTeam(ctx context.Context, teamID, requestingUserID string) ([]GenericSecret, error)
ListByUser(ctx context.Context, teamID, userID string) ([]GenericSecret, error)
MarkUsed(ctx context.Context, id string, at time.Time) error
}
type LayeredGenericSecretStore ¶
type LayeredGenericSecretStore struct {
// contains filtered or unexported fields
}
LayeredGenericSecretStore composes an optional per-project store over a global store. Resolution (ListByTeam) merges both with the project layer winning by secret Name, giving the "project overrides global" precedence. Mutations by ID search project first then global; Create targets an explicit layer chosen by the caller (Global()/Project()).
func LocalStoreForProject ¶
func LocalStoreForProject(projectStoreDir string) (*LayeredGenericSecretStore, error)
LocalStoreForProject builds the layered local store for a project store dir, sourcing the machine-global dir from GlobalIterionDataDir(). It is the single place that couples "the global layer lives under GlobalIterionDataDir" so the CLI, the studio wiring, the project-switch rebuild, and the dispatcher all agree without repeating it.
func NewLayeredGenericSecretStore ¶
func NewLayeredGenericSecretStore(global, project *FileGenericSecretStore) *LayeredGenericSecretStore
NewLayeredGenericSecretStore wraps a required global store and an optional project store (pass nil for none).
func NewLocalLayeredStore ¶
func NewLocalLayeredStore(globalDir, projectStoreDir string) (*LayeredGenericSecretStore, error)
NewLocalLayeredStore builds the machine-global store at <globalDir>/secrets.json plus an optional per-project store at <projectStoreDir>/secrets.json — but only when that dir is distinct from the global one (else it would layer the same file on itself). The project layer overrides the global by name. Used by the CLI, the local studio wiring, and the studio project-switch rebuild.
func (*LayeredGenericSecretStore) Create ¶
func (l *LayeredGenericSecretStore) Create(ctx context.Context, rec GenericSecret) error
func (*LayeredGenericSecretStore) Delete ¶
func (l *LayeredGenericSecretStore) Delete(ctx context.Context, id string) error
func (*LayeredGenericSecretStore) ForScope ¶
func (l *LayeredGenericSecretStore) ForScope(scope string) *FileGenericSecretStore
ForScope returns the concrete store for a scope selector ("project" → the project store when active, else global; anything else → global).
func (*LayeredGenericSecretStore) Get ¶
func (l *LayeredGenericSecretStore) Get(ctx context.Context, id string) (GenericSecret, error)
func (*LayeredGenericSecretStore) Global ¶
func (l *LayeredGenericSecretStore) Global() *FileGenericSecretStore
Global returns the machine-wide store (the default write target).
func (*LayeredGenericSecretStore) ListByTeam ¶
func (l *LayeredGenericSecretStore) ListByTeam(ctx context.Context, teamID, userID string) ([]GenericSecret, error)
ListByTeam merges global + project, project winning by Name, sorted by Name. Delegates to ListScoped (the single source of the merge/precedence rule) and drops the scope tag.
func (*LayeredGenericSecretStore) ListByUser ¶
func (l *LayeredGenericSecretStore) ListByUser(ctx context.Context, teamID, userID string) ([]GenericSecret, error)
func (*LayeredGenericSecretStore) ListScoped ¶
func (l *LayeredGenericSecretStore) ListScoped(ctx context.Context, teamID, userID string) ([]ScopedSecret, error)
ListScoped returns every secret tagged with its owning layer, project overriding global by name (a name present in both appears once, scope "project"). Sorted by Name.
func (*LayeredGenericSecretStore) Project ¶
func (l *LayeredGenericSecretStore) Project() (*FileGenericSecretStore, bool)
Project returns the per-project store and whether one is active.
func (*LayeredGenericSecretStore) Update ¶
func (l *LayeredGenericSecretStore) Update(ctx context.Context, rec GenericSecret) error
type MemoryApiKeyStore ¶
type MemoryApiKeyStore struct {
// contains filtered or unexported fields
}
MemoryApiKeyStore is the in-process store used by tests of the resolution chain.
func NewMemoryApiKeyStore ¶
func NewMemoryApiKeyStore() *MemoryApiKeyStore
func (*MemoryApiKeyStore) ClearDefault ¶
func (*MemoryApiKeyStore) Create ¶
func (m *MemoryApiKeyStore) Create(_ context.Context, k ApiKey) error
func (*MemoryApiKeyStore) Delete ¶
func (m *MemoryApiKeyStore) Delete(_ context.Context, id string) error
func (*MemoryApiKeyStore) ListByTeam ¶
func (*MemoryApiKeyStore) ListByUser ¶
type MemoryBotSecretBindingStore ¶
type MemoryBotSecretBindingStore struct {
// contains filtered or unexported fields
}
func NewMemoryBotSecretBindingStore ¶
func NewMemoryBotSecretBindingStore() *MemoryBotSecretBindingStore
func (*MemoryBotSecretBindingStore) Create ¶
func (m *MemoryBotSecretBindingStore) Create(_ context.Context, b BotSecretBinding) error
func (*MemoryBotSecretBindingStore) Delete ¶
func (m *MemoryBotSecretBindingStore) Delete(_ context.Context, id string) error
func (*MemoryBotSecretBindingStore) Get ¶
func (m *MemoryBotSecretBindingStore) Get(_ context.Context, id string) (BotSecretBinding, error)
func (*MemoryBotSecretBindingStore) ListByTenant ¶
func (m *MemoryBotSecretBindingStore) ListByTenant(_ context.Context, tenantID string) ([]BotSecretBinding, error)
func (*MemoryBotSecretBindingStore) ListByTenantBot ¶
func (m *MemoryBotSecretBindingStore) ListByTenantBot(_ context.Context, tenantID, botID string) ([]BotSecretBinding, error)
func (*MemoryBotSecretBindingStore) Update ¶
func (m *MemoryBotSecretBindingStore) Update(_ context.Context, b BotSecretBinding) error
type MemoryGenericSecretStore ¶
type MemoryGenericSecretStore struct {
// contains filtered or unexported fields
}
func NewMemoryGenericSecretStore ¶
func NewMemoryGenericSecretStore() *MemoryGenericSecretStore
func (*MemoryGenericSecretStore) Create ¶
func (m *MemoryGenericSecretStore) Create(_ context.Context, s GenericSecret) error
func (*MemoryGenericSecretStore) Delete ¶
func (m *MemoryGenericSecretStore) Delete(_ context.Context, id string) error
func (*MemoryGenericSecretStore) Get ¶
func (m *MemoryGenericSecretStore) Get(_ context.Context, id string) (GenericSecret, error)
func (*MemoryGenericSecretStore) ListByTeam ¶
func (m *MemoryGenericSecretStore) ListByTeam(_ context.Context, teamID, userID string) ([]GenericSecret, error)
func (*MemoryGenericSecretStore) ListByUser ¶
func (m *MemoryGenericSecretStore) ListByUser(_ context.Context, teamID, userID string) ([]GenericSecret, error)
func (*MemoryGenericSecretStore) Update ¶
func (m *MemoryGenericSecretStore) Update(_ context.Context, s GenericSecret) error
type MemoryOAuthPendingStore ¶
type MemoryOAuthPendingStore struct {
// contains filtered or unexported fields
}
MemoryOAuthPendingStore — for tests / local mode.
func NewMemoryOAuthPendingStore ¶
func NewMemoryOAuthPendingStore() *MemoryOAuthPendingStore
func (*MemoryOAuthPendingStore) Put ¶
func (s *MemoryOAuthPendingStore) Put(_ context.Context, rec OAuthPending) error
func (*MemoryOAuthPendingStore) Take ¶
func (s *MemoryOAuthPendingStore) Take(_ context.Context, ownerKey string, kind OAuthKind) (OAuthPending, error)
type MemoryOAuthStore ¶
type MemoryOAuthStore struct {
// contains filtered or unexported fields
}
MemoryOAuthStore — for tests.
func NewMemoryOAuthStore ¶
func NewMemoryOAuthStore() *MemoryOAuthStore
func (*MemoryOAuthStore) ExpiringBefore ¶
func (s *MemoryOAuthStore) ExpiringBefore(_ context.Context, t time.Time) ([]OAuthRecord, error)
func (*MemoryOAuthStore) Get ¶
func (s *MemoryOAuthStore) Get(_ context.Context, userID string, kind OAuthKind) (OAuthRecord, error)
func (*MemoryOAuthStore) ListByUser ¶
func (s *MemoryOAuthStore) ListByUser(_ context.Context, userID string) ([]OAuthRecord, error)
func (*MemoryOAuthStore) Upsert ¶
func (s *MemoryOAuthStore) Upsert(_ context.Context, rec OAuthRecord) error
type MemoryRunSecretsStore ¶
type MemoryRunSecretsStore struct {
// contains filtered or unexported fields
}
MemoryRunSecretsStore is the test variant.
func NewMemoryRunSecretsStore ¶
func NewMemoryRunSecretsStore() *MemoryRunSecretsStore
func (*MemoryRunSecretsStore) Delete ¶
func (s *MemoryRunSecretsStore) Delete(ctx context.Context, id string) error
func (*MemoryRunSecretsStore) Get ¶
func (s *MemoryRunSecretsStore) Get(ctx context.Context, id string) (RunSecretsRecord, error)
func (*MemoryRunSecretsStore) Put ¶
func (s *MemoryRunSecretsStore) Put(_ context.Context, rec RunSecretsRecord) error
type MongoApiKeyStore ¶
type MongoApiKeyStore struct {
// contains filtered or unexported fields
}
MongoApiKeyStore implements ApiKeyStore on Mongo.
func NewMongoApiKeyStore ¶
func NewMongoApiKeyStore(db *mongo.Database) *MongoApiKeyStore
func (*MongoApiKeyStore) ClearDefault ¶
func (*MongoApiKeyStore) Create ¶
func (s *MongoApiKeyStore) Create(ctx context.Context, k ApiKey) error
func (*MongoApiKeyStore) Delete ¶
func (s *MongoApiKeyStore) Delete(ctx context.Context, id string) error
func (*MongoApiKeyStore) EnsureSchema ¶
func (s *MongoApiKeyStore) EnsureSchema(ctx context.Context) error
EnsureSchema creates the indexes used by the store.
func (*MongoApiKeyStore) ListByTeam ¶
func (*MongoApiKeyStore) ListByUser ¶
type MongoBotSecretBindingStore ¶
type MongoBotSecretBindingStore struct {
// contains filtered or unexported fields
}
func NewMongoBotSecretBindingStore ¶
func NewMongoBotSecretBindingStore(db *mongo.Database) *MongoBotSecretBindingStore
func (*MongoBotSecretBindingStore) Create ¶
func (s *MongoBotSecretBindingStore) Create(ctx context.Context, b BotSecretBinding) error
func (*MongoBotSecretBindingStore) Delete ¶
func (s *MongoBotSecretBindingStore) Delete(ctx context.Context, id string) error
func (*MongoBotSecretBindingStore) EnsureSchema ¶
func (s *MongoBotSecretBindingStore) EnsureSchema(ctx context.Context) error
func (*MongoBotSecretBindingStore) Get ¶
func (s *MongoBotSecretBindingStore) Get(ctx context.Context, id string) (BotSecretBinding, error)
func (*MongoBotSecretBindingStore) ListByTenant ¶
func (s *MongoBotSecretBindingStore) ListByTenant(ctx context.Context, tenantID string) ([]BotSecretBinding, error)
func (*MongoBotSecretBindingStore) ListByTenantBot ¶
func (s *MongoBotSecretBindingStore) ListByTenantBot(ctx context.Context, tenantID, botID string) ([]BotSecretBinding, error)
func (*MongoBotSecretBindingStore) Update ¶
func (s *MongoBotSecretBindingStore) Update(ctx context.Context, b BotSecretBinding) error
type MongoGenericSecretStore ¶
type MongoGenericSecretStore struct {
// contains filtered or unexported fields
}
func NewMongoGenericSecretStore ¶
func NewMongoGenericSecretStore(db *mongo.Database) *MongoGenericSecretStore
func (*MongoGenericSecretStore) Create ¶
func (s *MongoGenericSecretStore) Create(ctx context.Context, rec GenericSecret) error
func (*MongoGenericSecretStore) Delete ¶
func (s *MongoGenericSecretStore) Delete(ctx context.Context, id string) error
func (*MongoGenericSecretStore) EnsureSchema ¶
func (s *MongoGenericSecretStore) EnsureSchema(ctx context.Context) error
func (*MongoGenericSecretStore) Get ¶
func (s *MongoGenericSecretStore) Get(ctx context.Context, id string) (GenericSecret, error)
func (*MongoGenericSecretStore) ListByTeam ¶
func (s *MongoGenericSecretStore) ListByTeam(ctx context.Context, teamID, userID string) ([]GenericSecret, error)
func (*MongoGenericSecretStore) ListByUser ¶
func (s *MongoGenericSecretStore) ListByUser(ctx context.Context, teamID, userID string) ([]GenericSecret, error)
func (*MongoGenericSecretStore) Update ¶
func (s *MongoGenericSecretStore) Update(ctx context.Context, rec GenericSecret) error
type MongoOAuthPendingStore ¶
type MongoOAuthPendingStore struct {
// contains filtered or unexported fields
}
MongoOAuthPendingStore — production impl with a TTL guard.
func NewMongoOAuthPendingStore ¶
func NewMongoOAuthPendingStore(db *mongo.Database) *MongoOAuthPendingStore
func (*MongoOAuthPendingStore) EnsureSchema ¶
func (s *MongoOAuthPendingStore) EnsureSchema(ctx context.Context) error
func (*MongoOAuthPendingStore) Put ¶
func (s *MongoOAuthPendingStore) Put(ctx context.Context, rec OAuthPending) error
func (*MongoOAuthPendingStore) Take ¶
func (s *MongoOAuthPendingStore) Take(ctx context.Context, ownerKey string, kind OAuthKind) (OAuthPending, error)
type MongoOAuthStore ¶
type MongoOAuthStore struct {
// contains filtered or unexported fields
}
MongoOAuthStore — production impl.
func NewMongoOAuthStore ¶
func NewMongoOAuthStore(db *mongo.Database) *MongoOAuthStore
func (*MongoOAuthStore) EnsureSchema ¶
func (s *MongoOAuthStore) EnsureSchema(ctx context.Context) error
func (*MongoOAuthStore) ExpiringBefore ¶
func (s *MongoOAuthStore) ExpiringBefore(ctx context.Context, t time.Time) ([]OAuthRecord, error)
func (*MongoOAuthStore) Get ¶
func (s *MongoOAuthStore) Get(ctx context.Context, userID string, kind OAuthKind) (OAuthRecord, error)
func (*MongoOAuthStore) ListByUser ¶
func (s *MongoOAuthStore) ListByUser(ctx context.Context, userID string) ([]OAuthRecord, error)
func (*MongoOAuthStore) Upsert ¶
func (s *MongoOAuthStore) Upsert(ctx context.Context, rec OAuthRecord) error
type MongoRunSecretsStore ¶
type MongoRunSecretsStore struct {
// contains filtered or unexported fields
}
MongoRunSecretsStore implements RunSecretsStore on Mongo with a 24h TTL guard.
func NewMongoRunSecretsStore ¶
func NewMongoRunSecretsStore(db *mongo.Database) *MongoRunSecretsStore
func (*MongoRunSecretsStore) Delete ¶
func (s *MongoRunSecretsStore) Delete(ctx context.Context, id string) error
func (*MongoRunSecretsStore) EnsureSchema ¶
func (s *MongoRunSecretsStore) EnsureSchema(ctx context.Context) error
func (*MongoRunSecretsStore) Get ¶
func (s *MongoRunSecretsStore) Get(ctx context.Context, id string) (RunSecretsRecord, error)
func (*MongoRunSecretsStore) Put ¶
func (s *MongoRunSecretsStore) Put(ctx context.Context, rec RunSecretsRecord) error
type OAuthKind ¶
type OAuthKind string
OAuthKind enumerates the third-party CLIs whose OAuth subscription (forfait) iterion can drive on behalf of an authenticated user. The names match the delegate.Backend slug so cloudpublisher and the runner can resolve them mechanically.
type OAuthPending ¶
type OAuthPending struct {
ID string `bson:"_id" json:"id"`
OwnerKey string `bson:"owner_key" json:"owner_key"`
Kind OAuthKind `bson:"kind" json:"kind"`
SealedVerifier []byte `bson:"sealed_verifier" json:"-"`
State string `bson:"state" json:"state"`
RedirectURI string `bson:"redirect_uri" json:"redirect_uri"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
ExpiresAt time.Time `bson:"expires_at" json:"expires_at"`
}
OAuthPending is the short-lived server-side state held between the browser OAuth /authorize/start and /authorize/complete calls. It keeps the PKCE verifier (sealed) and the expected state so the completion can finish the exchange and validate CSRF — without trusting anything the client echoes back except the code.
Keyed by (OwnerKey, Kind): one in-flight connect per owner+kind. A second start simply overwrites the first. Persisted (Mongo) rather than in-process so the start and complete may land on different replicas (HA-safe). Mirrors the TTL pattern of run_secrets.
type OAuthPendingStore ¶
type OAuthPendingStore interface {
// Put upserts the pending record (overwriting any existing one for
// the same owner+kind).
Put(ctx context.Context, rec OAuthPending) error
// Take reads and deletes the pending record (one-shot), returning
// ErrOAuthPendingNotFound when absent or expired.
Take(ctx context.Context, ownerKey string, kind OAuthKind) (OAuthPending, error)
}
OAuthPendingStore persists in-flight browser-OAuth connects.
type OAuthRecord ¶
type OAuthRecord struct {
ID string `bson:"_id" json:"id"`
UserID string `bson:"user_id" json:"user_id"`
Kind OAuthKind `bson:"kind" json:"kind"`
SealedPayload []byte `bson:"sealed_payload" json:"-"`
Scopes []string `bson:"scopes,omitempty" json:"scopes,omitempty"`
AccessTokenExpiresAt *time.Time `bson:"access_token_expires_at,omitempty" json:"access_token_expires_at,omitempty"`
LastRefreshedAt *time.Time `bson:"last_refreshed_at,omitempty" json:"last_refreshed_at,omitempty"`
// NotRefreshable marks a payload that carries no refresh token: the
// refresh worker and manual refresh must skip it — only a re-connect
// can renew it. Inverted polarity so legacy records (field absent =
// false) keep being attempted; the first ErrNotRefreshable outcome
// self-heals them by setting this flag.
NotRefreshable bool `bson:"not_refreshable,omitempty" json:"not_refreshable,omitempty"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
}
OAuthRecord is the per-(user, kind) sealed credential bundle.
SealedPayload is opaque to iterion — it holds the verbatim credentials.json (Anthropic) or auth.json (OpenAI Codex) blob the user uploaded, sealed with the master key bound to the record id. We never decrypt for display; the only consumer is the runner, which materialises the file in a tmpdir and points the CLI at it.
AccessTokenExpiresAt is captured separately from the sealed blob so the refresh worker can identify expiring records without decrypting. Best-effort: providers without an access-token expiry (or when the user pasted only the refresh token) leave it zero and the worker skips them.
type OAuthRefreshWorker ¶
type OAuthRefreshWorker struct {
Store OAuthStore
Sealer Sealer
HTTP *http.Client
AnthropicClientID string
CodexClientID string
// Lead is how far ahead of expiry a record is refreshed (a record
// expiring within Lead is rotated now). Defaults to 30m.
Lead time.Duration
}
OAuthRefreshWorker proactively rotates OAuth-forfait access tokens before they expire, so neither an interactive run nor an automated (webhook/dispatcher/cron) run ever reads a stale credential. This is the analogue of forge.RefreshWorker for the forfait store.
It covers BOTH personal and org-scoped records uniformly: org records are ordinary OAuthRecords keyed under OrgOwnerKey(tenant), so they surface from ExpiringBefore like any other — and the org credential is exactly the one that powers 24/7 automation, so keeping it fresh is the whole point.
func (*OAuthRefreshWorker) RunOnce ¶
func (w *OAuthRefreshWorker) RunOnce(ctx context.Context) (int, error)
RunOnce refreshes every record expiring within Lead. It is best-effort: a single record's failure (provider rejection, missing client id) is logged via the returned error aggregate but does not abort the sweep. Returns the number of records successfully refreshed.
type OAuthStore ¶
type OAuthStore interface {
Upsert(ctx context.Context, rec OAuthRecord) error
Get(ctx context.Context, userID string, kind OAuthKind) (OAuthRecord, error)
ListByUser(ctx context.Context, userID string) ([]OAuthRecord, error)
Delete(ctx context.Context, userID string, kind OAuthKind) error
// ExpiringBefore returns records whose access token is set and
// expires before t — used by the background refresh worker.
ExpiringBefore(ctx context.Context, t time.Time) ([]OAuthRecord, error)
}
OAuthStore is the persistence interface for sealed OAuth records.
type Provider ¶
type Provider string
Provider enumerates the supported LLM credential providers. The string values are stable wire identifiers; do not rename without a migration. Naming mirrors what the model registry consumes.
const ( ProviderAnthropic Provider = "anthropic" ProviderOpenAI Provider = "openai" ProviderBedrock Provider = "bedrock" ProviderVertex Provider = "vertex" ProviderAzure Provider = "azure" ProviderOpenRouter Provider = "openrouter" ProviderXAI Provider = "xai" // ProviderZAI is z.ai's Coding-Plan token. The provider exposes an // Anthropic-compatible HTTP surface, so credentials flow through the // existing Anthropic codepath with ANTHROPIC_BASE_URL pointed at // z.ai's endpoint and the ZAI token used as ANTHROPIC_AUTH_TOKEN. // Kept as a distinct Provider so per-tenant BYOK can pick "z.ai" // vs "anthropic" explicitly. ProviderZAI Provider = "zai" )
func ParseProvider ¶
ParseProvider returns Provider when s matches one of the known names (case-insensitive) or an error otherwise.
type RefreshResult ¶
type RefreshResult struct {
AccessToken string
RefreshToken string
ExpiresAt time.Time
Scopes []string
IDToken string
}
RefreshResult carries the bits a successful refresh produces. Pass them through ApplyAnthropicRefresh / ApplyCodexRefresh to rebuild the credentials JSON the CLI expects.
func ExchangeAnthropicCode ¶
func ExchangeAnthropicCode(ctx context.Context, hc *http.Client, clientID, code, verifier, redirectURI, state string) (RefreshResult, error)
ExchangeAnthropicCode trades an authorization code for tokens against the Anthropic OAuth token endpoint (grant_type=authorization_code). It mirrors RefreshAnthropic's request/response handling and reuses the same retry + validation primitives. redirectURI must match the one used to obtain the code; state is sent when non-empty.
func RefreshAnthropic ¶
func RefreshAnthropic(ctx context.Context, hc *http.Client, clientID, refreshToken string) (RefreshResult, error)
RefreshAnthropic exchanges a refresh_token for a new access_token against the Anthropic OAuth endpoint. clientID is provided per deployment (the publicly-known Claude Code OAuth client).
func RefreshCodex ¶
func RefreshCodex(ctx context.Context, hc *http.Client, clientID, refreshToken string) (RefreshResult, error)
RefreshCodex mirrors RefreshAnthropic for the OpenAI Codex CLI. clientID is the Codex CLI's published OAuth client; deployments using a custom Codex fork override it.
type Resolution ¶
type Resolution struct {
Provider Provider
KeyID string
// Plaintext is filled by the resolver only if the caller passes
// a Sealer; without one the resolver returns the sealed blob
// untouched (handlers that do not need plaintext yet).
Plaintext []byte
SealedBlob []byte
SourceScope string // "user" or "team" — for audit logging
}
Resolution describes a single resolved key the publisher needs to inject for one provider on a given run.
type RunBundle ¶
type RunBundle struct {
APIKeys map[Provider]string `json:"api_keys,omitempty"`
// GenericSecrets maps workflow secret names to plaintext payloads
// resolved from the tenant/user secret store at publish time.
GenericSecrets map[string]string `json:"generic_secrets,omitempty"`
// GenericSecretHosts maps a workflow secret name to the egress host
// allowlist a bot-secret binding imposes on it (empty/absent = no
// binding-level restriction). The runner intersects this with the
// workflow's own declared `secrets.<name>.hosts` so a binding can
// only NARROW egress, never broaden it. This is what makes a
// binding's AllowedHosts an enforced control rather than metadata.
GenericSecretHosts map[string][]string `json:"generic_secret_hosts,omitempty"`
// GenericSecretRefs maps a workflow secret name to the ID of the
// generic-secret store record it was resolved from (IDs only, never
// values). A short-lived credential (a GitHub App installation token
// lives 1h) can expire while the run executes; the server-side
// refresh worker keeps the STORE record fresh, so these refs let the
// runner re-read the current value mid-run and rewrite the secret's
// materialised file — the bundle snapshot alone would go stale.
GenericSecretRefs map[string]string `json:"generic_secret_refs,omitempty"`
// OAuthCredentials maps "claude_code" / "codex" → opaque blob
// that the runner materialises as a credentials.json /
// auth.json before spawning the CLI subprocess.
OAuthCredentials map[string][]byte `json:"oauth_credentials,omitempty"`
// ForgeAppBotLogin is the GitHub-App bot login (e.g.
// "iterion-forge-1234[bot]") when the run's forge_token was resolved
// from a github_app connection. An installation token can't `GET /user`
// (403), so the runner can't self-resolve the committer identity from
// the token alone — this login lets it look up the bot's numeric id via
// `GET /users/<login>` (which an installation token CAN read) and seed
// the canonical `<id>+<login>@users.noreply.github.com` committer, so a
// bot's commits are attributed to the App bot, not the neutral fallback.
// Empty for PAT/OAuth connections (the token's own /user resolves them).
ForgeAppBotLogin string `json:"forge_app_bot_login,omitempty"`
}
RunBundle is the per-run sealed payload the runner needs in order to execute. It carries every API-key + OAuth credential the publisher pre-resolved, keyed by provider/kind.
The structure is JSON-marshalled and then sealed once with the run-scoped AAD ("run_secrets:<run_id>"). Runners decrypt with the shared master key.
type RunSecretsRecord ¶
type RunSecretsRecord struct {
ID string `bson:"_id" json:"id"`
TenantID string `bson:"tenant_id" json:"tenant_id"`
RunID string `bson:"run_id" json:"run_id"`
SealedBundle []byte `bson:"sealed_bundle" json:"-"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
// ExpiresAt drives the Mongo TTL — the runner deletes the
// record on success, but a TTL guard ensures abandoned bundles
// never linger past 24h.
ExpiresAt time.Time `bson:"expires_at" json:"expires_at"`
}
RunSecretsRecord is the persisted form of a sealed bundle. _id is the SecretsRef the publisher writes into the queue.RunMessage; the runner uses that ref to fetch + decrypt right before executing the run.
type RunSecretsStore ¶
type RunSecretsStore interface {
Put(ctx context.Context, rec RunSecretsRecord) error
Get(ctx context.Context, id string) (RunSecretsRecord, error)
Delete(ctx context.Context, id string) error
}
RunSecretsStore persists sealed RunBundle records keyed by an opaque ref carried in the NATS message.
type ScopedSecret ¶
type ScopedSecret struct {
Secret GenericSecret
Scope string
}
ScopedSecret pairs a secret with the layer it resolved from ("global" | "project"), so a UI/CLI can show which layer owns each entry.
type Sealer ¶
type Sealer interface {
// Seal returns a sealed blob. AAD is optional context binding;
// pass the same value to Open.
Seal(plaintext, aad []byte) ([]byte, error)
// Open returns the plaintext or an error. AAD must match the
// value supplied at Seal.
Open(sealed, aad []byte) ([]byte, error)
}
Sealer seals and opens secret payloads. Implementations MUST provide authenticated encryption: tampering with sealed bytes must surface as an Open error, never as silently-corrupted plaintext.