secrets

package
v1.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 29, 2026 License: MIT Imports: 13 Imported by: 0

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

View Source
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

func Init

func Init() (bool, error)

Init creates the secrets directory and the local key if they don't already exist. It is idempotent: an existing key is never overwritten. The bool is true when a new key was generated, false when one already existed.

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

func Open() (*Store, error)

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.

func (*Store) Get

func (st *Store) Get(id string) (string, bool)

Get returns the value for id and whether it exists.

func (*Store) List

func (st *Store) List() []string

List returns the sorted secret IDs only — never the values.

func (*Store) Remove

func (st *Store) Remove(id string)

Remove deletes id from the store (in memory). Call Save to persist.

func (*Store) Save

func (st *Store) Save() error

Save encrypts the in-memory map and writes it atomically (temp file + rename) at mode 0600. A fresh random nonce is used on every write, so two saves of the same content produce different ciphertext.

func (*Store) Set

func (st *Store) Set(id, value string)

Set stores value under id in memory. Call Save to persist. The value is never logged.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL