Documentation
¶
Overview ¶
Package secrets implements the encrypted-at-rest secret store described in .agents/SECRETS.md.
Storage: one YAML file (`secrets.yaml`) containing a version tag and a map of name → entry. Each entry's value is serialised as
AES256GCM:v1:<base64 nonce>:<base64 ciphertext>
Key derivation: PBKDF2(passphrase, salt="baifo-secrets-v1", iter=200000, len=32). The passphrase comes from `baifo.encryption_key` (see internal/config).
When `baifo.encryption_key` is empty the store runs in plaintext mode: the file is still well-formed YAML but values are serialised verbatim with the `plain:v1:<base64>` prefix. This mode is meant for local development; the `encrypted: false` flag at the top of the file makes the mode explicit so accidents (key configured later) surface as a loud error instead of "can't decrypt".
Moving between modes is explicit and supported via Encode / Decode (exposed in the TUI as `/secret encode` and `/secret decode`).
Index ¶
- Variables
- func Expand(ctx context.Context, store *Store, allower Allower, args any) (any, context.Context, error)
- func ExpandedPairs(ctx context.Context) map[string]string
- func Redact(ctx context.Context, result any) (any, error)
- type AllowAll
- type AllowList
- type AllowNone
- type Allower
- type Entry
- type LogRedactor
- type Store
- func (s *Store) Decode() (int, error)
- func (s *Store) Delete(name string) error
- func (s *Store) Encode() (int, error)
- func (s *Store) Encrypted() bool
- func (s *Store) Generation() uint64
- func (s *Store) Get(name string) (string, error)
- func (s *Store) List() ([]Entry, error)
- func (s *Store) Set(name, value, description string) error
- func (s *Store) Snapshot() (map[string]string, error)
Constants ¶
This section is empty.
Variables ¶
var ErrNotFound = errors.New("secret not found")
ErrNotFound is returned by Get and Delete when the requested secret does not exist.
Functions ¶
func Expand ¶
func Expand(ctx context.Context, store *Store, allower Allower, args any) (any, context.Context, error)
Expand walks an arbitrary tool-call argument value and substitutes every ${secret:NAME} placeholder with the real secret value, provided (a) the agent's allowlist permits NAME and (b) the secret exists.
Placeholders for unknown or disallowed names are left in place so the downstream tool surfaces a clear "Authorization header malformed: literal ${secret:foo}" type of error, which the LLM can recover from.
The function also returns a context derived from ctx that stores the (name → raw value) pairs actually substituted. Pass that derived context to Redact after the tool returns.
func ExpandedPairs ¶
ExpandedPairs returns the (name → raw value) pairs that Expand substituted during this call. Callers that need to scrub a result outside the standard Redact pipeline can use this to read the pairs directly. Returns an empty map when nothing was expanded.
func Redact ¶
Redact walks an arbitrary tool-call result and replaces every raw secret value that was substituted in this call (looked up via ctx) with its ${secret:NAME} placeholder. Secrets that were not actually expanded in this call are NOT scanned: that would be expensive and false-positive prone.
Types ¶
type AllowAll ¶
type AllowAll struct{}
AllowAll permits every secret. Reserved for the root agent, which is the user-facing coordinator and the frontier of trust: it must be able to dereference any secret the operator stored. Sub-agents (static or dynamic) never receive this Allower — they go through AllowerFor with their explicit allowlist.
type AllowList ¶
type AllowList struct {
// contains filtered or unexported fields
}
AllowList implements Allower with a whitelist. Names not in the list are rejected. Used when an agent's allowed_secrets is non-empty.
func NewAllowList ¶
NewAllowList returns an Allower that permits exactly the given names.
type AllowNone ¶
type AllowNone struct{}
AllowNone forbids every secret. This is the least-privilege default for any agent built from an explicit (possibly empty) allowlist: static templates with no allowed_secrets, dynamic workers whose spawn call omitted the field, etc. The model still sees the placeholder name in tool args; the expander leaves it untouched.
type Allower ¶
Allower decides whether a given agent is allowed to see the named secret. See AllowerFor for how an allowlist slice maps to the concrete implementations: AllowAll, AllowNone, AllowList.
func AllowerFor ¶
AllowerFor returns an Allower for an explicit allowlist.
- len(allowed) == 0 → AllowNone (least-privilege default for any agent built from an explicit list, regardless of whether the slice is nil or just empty).
- non-empty list → AllowList of those names.
The root agent does NOT go through this function — it is wired directly to AllowAll in the builder. See agent.Spec.UnrestrictedSecrets for the toggle.
type Entry ¶
Entry is the public representation of a secret as listed by the CLI. The raw value is never embedded here.
type LogRedactor ¶
type LogRedactor struct {
// contains filtered or unexported fields
}
LogRedactor replaces every stored secret value that appears in a log attribute with its ${secret:NAME} placeholder. It implements the logging.Redactor interface (structural, no import of logging to avoid an import cycle) and is safe for concurrent use.
The secret snapshot is cached between log lines and only refreshed when the Store's generation counter changes, so the expensive decrypt-all path (PBKDF2 + AES-GCM per entry) is amortised across as many log lines as happen between mutations. A nil Store or enabled=false makes every Redact call a cheap no-op.
func NewLogRedactor ¶
func NewLogRedactor(store *Store, enabled bool, minLen int) *LogRedactor
NewLogRedactor creates a LogRedactor for the given Store. Pass a nil store when the store is not yet available at logging init time; call SetStore once the store is ready. enabled gates all redaction; minLen is the minimum byte length a secret value must have to be considered (mirrors SecretsConfig.EffectiveMinScrubLength).
func (*LogRedactor) Redact ¶
func (r *LogRedactor) Redact(attr slog.Attr) slog.Attr
Redact implements logging.Redactor. It walks the attribute value, replacing any occurrence of a cached secret value with its ${secret:NAME} placeholder. Group attributes are recursed into so structured log lines are covered as well. Non-string typed values are rendered to a string only when a secret is actually found in them, preserving the original type otherwise.
func (*LogRedactor) SetConfig ¶
func (r *LogRedactor) SetConfig(enabled bool, minLen int)
SetConfig updates the enabled flag and minLen floor in place and invalidates the cached snapshot. Both changes take effect on the next Redact call.
func (*LogRedactor) SetStore ¶
func (r *LogRedactor) SetStore(store *Store)
SetStore replaces the underlying secrets Store and invalidates the cached snapshot. Safe to call concurrently; designed for use by ReloadFromDisk to point the live redactor at a freshly opened store without reconstructing the handler chain.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the entry point. When aead is nil the store runs in plaintext mode; otherwise every value is AES-256-GCM sealed before hitting disk.
func NewStore ¶
NewStore opens (or initialises) the secret store living in <dir>. The passphrase is the value of baifo.encryption_key from baifo.yaml. An empty passphrase puts the store in plaintext mode (intended for local development); a non-empty passphrase enables AES-256-GCM.
If the file already exists, its `encrypted` flag must match the configured mode — otherwise NewStore returns a clear error explaining how to reconcile (use `/secret encode` or `/secret decode`). This guards against the silent footgun of "key was unset, secrets now unreadable" or "key was set, secrets still plaintext on disk".
func (*Store) Decode ¶
Decode unwraps every encrypted entry into the plaintext format. Requires the store to currently hold the AEAD (so it can decrypt), but flips the on-disk flag to false so future Sets stay plaintext and `baifo.encryption_key` can be removed from the config.
IMPORTANT: this is intentionally destructive of confidentiality. Callers (the TUI) MUST present a confirmation prompt before invoking. Returns the number of entries converted.
func (*Store) Encode ¶
Encode re-seals every plaintext entry into the encrypted format. Requires the store to be in encrypted mode (i.e. encryption_key is configured). It is a no-op when there is nothing to convert, so it is safe to call repeatedly. Returns the number of entries converted.
func (*Store) Encrypted ¶
Encrypted reports whether the store will write new values encrypted. Mirrors the on-disk flag so callers can render a badge.
func (*Store) Generation ¶
Generation returns the current mutation counter. It increments atomically on every successful write (Set, Delete, Encode, Decode) so external caches can detect stale snapshots without decrypting every entry on each check. The value starts at zero and wraps on overflow (2^64 iterations of baifo are not expected in a session).
func (*Store) Get ¶
Get decrypts and returns the value of the secret named name. Returns ErrNotFound when the name is unknown.
func (*Store) List ¶
List returns the public metadata of every stored secret, sorted by name. Values are never returned by this method.
func (*Store) Set ¶
Set encrypts and writes (or replaces) the secret named name. If the secret already exists, its CreatedAt is preserved and only RotatedAt is bumped.
func (*Store) Snapshot ¶
Snapshot returns every secret's name → plaintext value in one shot. Unlike List, this method DOES decrypt every entry, so the returned map carries raw values. Callers that handle the result MUST keep it in memory only, never log it, and let the map go out of scope as soon as possible.
The intended consumer is the AfterToolCallback redactor's comprehensive-scrub pass (see SECRETS.md), which needs to scan a tool result for every stored value at once instead of just the values the BeforeToolCallback substituted. Decrypting once per tool call (vs. N times via Get) avoids thrashing AES-GCM.
An entry whose ciphertext is malformed is silently skipped — the redactor has no recourse to recover from a bad seal and we prefer degraded redaction over a hard failure that would block every tool call. The CLI's `baifo secrets list` will still surface the broken entry separately.