Documentation
¶
Overview ¶
Package secrets is a local, encrypted secrets manager for the a-novel CLI.
Threat model. A developer (or an AI agent) runs `a-novel test`/`run`/`ui` with API secrets (e.g. OPENAI_API_KEY) injected into the child process env only. The secret is never printed, logged, committed, or passed as a CLI argument. The goal is to prevent accidental exposure, not to defend against a local attacker who already has read access to the user's home directory.
Design.
- A 32-byte local key (crypto/rand) lives at SecretsRoot()/key, file mode 0600 inside a 0700 directory. It is generated once and never overwritten.
- The store at SecretsRoot()/store.enc holds a flat JSON map {secretID: value} encrypted as a whole with AES-256-GCM. Each write uses a fresh random 12-byte nonce, stored as `nonce || ciphertext`. Writes are atomic (temp file + rename) at mode 0600.
- No value is ever logged or printed. Errors never embed a secret value, and the Store is never formatted with %v.
Only the Go standard library is used for crypto (crypto/aes, crypto/cipher, crypto/rand) — no third-party crypto.
Index ¶
Constants ¶
const MappingFile = ".a-novel/secrets.yaml"
MappingFile is the per-repo, value-free secrets manifest, committed at the service repo root. It declares which secrets the service needs — each entry pairs a target environment-variable name with a secret ID in the local store (never a value) and an optional human description — so `a-novel test`/`run`/ `ui` know which secrets to decrypt, under which env-var name to inject them, and what to say when one isn't set yet:
# .a-novel/secrets.yaml
secrets:
- env: OPENAI_API_KEY
id: openai-key
description: OpenAI API key used by narrative generation.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Declaration ¶
type Declaration struct {
// Env is the environment-variable name the secret is injected under.
Env string `yaml:"env"`
// ID is the secret's key in the local store.
ID string `yaml:"id"`
// Description is an optional, human-readable note shown in the
// missing-secret warning. Value-free.
Description string `yaml:"description"`
}
Declaration is one entry in MappingFile: a target env var, the secret ID that supplies its value, and an optional description surfaced when the secret is unset. It never carries a value.
type Resolution ¶
type Resolution struct {
// Env maps env-var name → decrypted secret value. This is secret material:
// inject it into a child process's environment only; never print or log it.
Env map[string]string
// Missing lists declarations whose secret is absent from the store (or whose
// store/key isn't set up yet). Carries env/id/description — never a value.
Missing []Declaration
}
Resolution is the outcome of resolving a repo's MappingFile against the local store: the env pairs ready to inject, and the declarations whose secret isn't set yet (surfaced as a descriptive warning, never a hard failure).
func InjectForRepo ¶
func InjectForRepo(repoRoot string) (Resolution, error)
InjectForRepo reads the value-free manifest at repoRoot/.a-novel/secrets.yaml, decrypts each declared secret from the local store, and returns the env pairs to inject plus the declarations still missing.
It is deliberately forgiving so secrets setup never blocks test/run:
- no manifest → empty Resolution, no error;
- no local key / store (secrets never initialized) → every declared secret is reported Missing so the developer is told what to set, but no error is returned and no key is created;
- a declared secret not in the store yet → reported Missing, not injected, so a repo can still run the tests that don't need it.
Only a malformed/unreadable manifest or a corrupt store returns an error.
Env values are secret material: callers must inject them into the child env only and never print or log them.
func (Resolution) Warnings ¶
func (r Resolution) Warnings() []string
Warnings renders one actionable, value-free line per missing declaration, suitable for stderr or a service log. The secret value is never included.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is an open, decrypted view of the secrets store. It holds the plaintext map in memory; callers mutate it via Set/Remove and persist with Save. The map is unexported so no caller can accidentally range-print the values.
func Open ¶
Open loads (and, if missing, initializes) the local key and the encrypted store under paths.SecretsRoot(). A first call materializes the key but not the store file — an empty store has no file on disk until the first Save.