# Changelog

All notable changes to Svault are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - UNRELEASED (release candidate)

Svault's first **stable** release. The agent-ready secret layer is feature-complete
and independently security-reviewed; 1.0.0 is the consolidation, not new scope.

What 1.0.0 stands on (built across the 0.9.x line):
- **Enforced policy gate** — every agent read runs reason → classification → scope →
  caller → conditions → rate/burst → tier → AI judge, evaluated where the key lives
  (the daemon), audited with the connecting process's real peer UID. No unguarded
  read path; denials are generic to the caller.
- **Everything encrypted at rest** — secret values *and* the whole policy surface
  (classification, caller rules, judge assignment, seals) live AES-256-GCM inside
  `vault.enc`; `meta.yaml` leaks no classification or secret names.
- **Unified unlock** — one master passphrase opens every vault and the keyring, via
  the keyslot model; YubiKey (FIDO2) and a recovery code are alternative OR-slots; a
  6-hour re-auth cap on every path.
- **Conditional access + seal/escalate**, the **MCP server** as the agent door, and
  the honest same-UID trust model stated up front.
- **Independently security-reviewed** — three external model reviews of the 0.9.9
  surface (no Critical/High), with the actionable findings fixed before this release
  (`docs/security-review/`).

Changes since 0.9.9:

### Changed
- **`svault get` is deprecated.** The agent door is now the **MCP server**
  (`svault mcp`); `svault get` runs the identical gate but prints a deprecation note
  to stderr and will be removed in a later release. Docs reframed so agents use MCP
  and humans use `svault secret get` + the management commands.
- **`-v/--vault` works on every vault-scoped command.** Added the `-v/--vault` flag to
  `policy` (previously it could not target a vault — `svault policy check <caller> -v
  <vault>` errored) and as an accepted alias on the commands that only took a
  positional vault (`settings`, `unlock`, `lock`, `pending`, `recover`, `export`); the
  positional form still works.

### Fixed
- `svault judge test` printed its judge/model/threshold summary line twice.
- `svault keyring` help listed a `rekey` action with "change its passphrase"; the
  keyring has no separate passphrase (it's opened by the master). Help now points to
  `svault master rekey`, and the unknown-action hint mentions it.
- `svault create` no longer prints a `git add ~/.svault/<name>/` hint for the default
  home store (it only makes sense for a project-scoped `SVAULT_HOME`).
- `svault install` is now clearly labelled "not yet implemented" and points to the
  manual MCP setup, instead of looking like a shipped command.
- `svault policy init` now spells out that the seeded callers are deny-by-default
  (`default` holds no scopes, `claude-code` only `misc`) and that you must grant each
  caller the scopes its secrets use — previously the documented `policy init` → `get`
  flow denied every scoped read with no hint why.
- Docs drift: `policy-engine.md` no longer says the CLI "runs the gate locally before
  unlocking" (the agent path never prompts); `daemon.md` no longer calls `secret list`
  a "mutation". QA checklist scratch notes cleaned up; added a check that `meta.yaml`
  exposes no secret names at rest.

## [0.9.9] - 2026-06-03

The **conditional access + seal/escalate** release — the last feature milestone
before 1.0. Secrets can now be gated by *when* and *by whom*, and sustained abuse
seals a secret and hands it to a human instead of letting an agent grind against it.

### Added
- **Conditional access** — a secret can carry conditions in its encrypted policy:
  allowed **time windows** (local machine time, e.g. `mon-fri 09:00-18:00`,
  `fri 10:00-12:00`, or `09:00-17:00`; 24h, start inclusive / end exclusive,
  same-day) and **required callers**. Outside a window, or for a non-required
  caller, the request is denied with the same generic message — an agent can't read
  the window to wait for it or learn the required-caller list. Set with
  `svault secret add --window … --require-caller …` (both repeatable) or the TUI
  classify screen (`c`), which gains *Windows* and *Required callers* fields.
- **Seal & escalate** — repeated denials on a *medium/high* secret (5 within 5
  minutes, counted across any caller) **seal** it in the encrypted policy. While
  sealed, every gated agent `get` is denied — even an otherwise-valid one — until a
  **human** clears it. A seal blocks the agent path only: a human still reads the
  secret via `svault secret get` and clears the seal. Agents can never unseal or
  unlock — human-only by design.
- **`svault pending [VAULT]`** — list sealed secrets awaiting approval (one vault or
  all). **`svault approve <secret> -v <vault>`** — clear a seal. In the TUI, sealed
  secrets are marked in the secret browser and `A` clears the selected one.
- `svault policy check` now shows each secret's windows / required callers and any
  active seals.
- **The store now lives at `~/.svault` by default.** The `svault` binary resolves
  its store under the user's home directory regardless of the working directory it's
  launched from — so the CLI/TUI, the daemon, and especially the `svault mcp` server
  (whose CWD the MCP host picks) all agree on one store without configuration. This
  fixes `svault mcp` returning an empty vault list when the host launched it outside
  the project that held the vaults.
- **`SVAULT_HOME` env override.** Set `SVAULT_HOME` to a base directory to use
  `$SVAULT_HOME/.svault` instead of home (e.g. a project-scoped store, or to point an
  MCP server at a non-home location). It governs the whole store — vaults, master
  keyslots, keyring, sessions, daemon socket — so every surface stays consistent.

### Changed
- The MCP capability descriptor notes that some secrets are restricted by
  caller/time or may be temporarily sealed, and that a denial can be final and
  require a human — without revealing the window or seal criteria, so a well-behaved
  agent stops rather than retrying in a loop.

### Hardened
Acting on the three independent 0.9.9 security reviews (`docs/security-review/`), the
following were fixed in this release so 1.0 is a test-and-ship, not a fix:
- **Agent `get` never prompts.** When no daemon holds the vault and it is locked, the
  agent path now returns "locked — a human must run `svault unlock`" instead of
  prompting for the master (mirroring `svault mcp`). This closes the path where an
  agent could induce a human master entry that then cached the vault for 6h and was
  readable via the ungated human `secret get`.
- **Clearing a seal requires a fresh master.** `svault approve` now re-prompts the
  master credential (passphrase or YubiKey touch) and ignores any cached session, so
  a same-UID process can't ride a lingering unlock to clear a seal unattended; a
  non-TTY context refuses approval. (The TUI `A` path is already a present human
  behind the app login.)
- **Caller-agnostic burst ceiling.** Beyond the per-caller rate/burst limits (keyed
  on the self-asserted `--caller`), a secret now also has a hard ceiling on *allowed*
  reads within the burst window counted **across every caller** — so rotating caller
  names can no longer evade burst detection. (The seal detector was already
  caller-agnostic.)
- Added prompt-injection regression tests for the AI judge: an injected `reason`
  stays a labelled data field, an explicit `deny` is honoured regardless of score,
  and a non-standard decision token degrades to "unavailable" (high then fails
  closed) rather than being read as allow.

### Notes
- Policy format is extended additively (`serde` defaults), so existing `.svault/`
  vaults load unchanged — no migration needed.

## [0.9.8] - 2026-06-02

The **hardware-key + hardening** release: YubiKey (FIDO2) unlock, a 6-hour re-auth
cap on every unlock path, a first-run onboarding flow with an app-level sign-in /
logout in the TUI, and an honest repositioning of the docs. Storage is local-only
(the never-wired cloud placeholders are gone).

### Added
- **TUI login gate + logout (`o`)** — the TUI now has an app-level sign-in. On
  launch, if a master is set but its session isn't active (never signed in this
  run, or expired past the 6h cap), it opens a **login screen** (master passphrase,
  or `Ctrl+Y` for an enrolled YubiKey) before the vault list. `o` on the list
  **logs out** — clears the master login session and returns to the login screen.
  Logout signs out only: it leaves the vaults' own locked/unlocked state, the
  keyring, the daemon, the judge, and all data unchanged.
- **First-run onboarding (TUI)** — opening the TUI with no master set now walks
  through a short flow: an honest disclaimer/boundary screen you accept, then
  setting the master passphrase, then the one-time recovery code, then an optional
  YubiKey enrollment — instead of burying master setup inside the first vault
  create. Blocking YubiKey touches show an in-app "Touch your YubiKey" modal (the
  library's own touch chatter is silenced) so the dashboard stays intact.
- **YubiKey unlock (FIDO2 hmac-secret)** — enroll a YubiKey as an alternative
  keyslot over the master key with `svault master yubikey enroll` (touch, plus the
  YubiKey PIN if one is set). Afterwards `svault unlock` offers the key and the TUI
  unlock screen takes `Ctrl+Y`; the master passphrase stays available as a
  fallback. It's *passphrase **or** touch*, never a two-step 2FA — additive over
  the same master key, no data re-encrypted, stored in `master.yubikey.enc` (+ a
  non-secret `.meta`). Manage with `svault master yubikey enroll | remove | status`.
  Ships as an **opt-in Cargo feature** (`yubikey`, off by default) so the base
  crate keeps building with no system dependencies; the prebuilt release binaries
  include it, and from source it's `cargo install svault-ai --features yubikey`
  (Linux also needs `libudev-dev`).

### Changed
- **6-hour re-auth hard cap on every unlock path.** File sessions (CLI/TUI) now
  carry an unlock timestamp and expire 6 hours after unlock — they previously never
  expired. The master and keyring sessions honor the same cap, and the daemon's
  in-memory hard cap (which backs the MCP path) dropped from 8h to 6h. This bounds
  the window in which an already-unlocked vault — including one an agent was
  prompted into at the CLI — can be read before a human must re-authenticate.
- **Repositioned the framing for honesty.** The README now leads with "the
  principled way to give cooperative AI agents secret access" and states the
  boundary up front (Svault gates and audits cooperative/semi-trusted agents and
  encrypts at rest; it is **not** a sandbox against a hostile same-UID process).
  The previous "knows the difference" / "the secret manager that knows an AI is
  asking" headline is gone. No behavior change.
- **Folded the storage docs.** `docs/storage-backends.md` (thin after the cloud
  removal) merged into `docs/architecture.md` under "Storage and vault naming".

### Removed
- **Non-functional storage placeholders** — the `cloud`, `self-hosted`, and `s3`
  storage backends are removed from the documentation. They were never wired;
  `local` is and always was the only real backend. The `storage: local` field and
  the `local:` prefix on vault listings stay.
- **Cloud roadmap** — the 3.0.0+ "Remote / cloud" direction (remote MCP with
  OAuth, a hosted cloud anomaly-scoring tier via Claude Haiku, remote sync) is
  removed from the roadmap (README, PLAN, roadmap.md). Svault stays a local-first,
  CLI-first secret manager.

## [0.9.7] - 2026-06-01

The **MCP** release. `svault mcp` runs a local [Model Context Protocol] server
that exposes Svault's gated secret access to MCP-aware agents (Claude Code,
Cursor, VS Code, …) over stdio. It is a thin frontend over the existing gate — the
human unlocks once, and every agent request runs through the same policy + AI-judge
enforcement as `svault get`, audited with `source = mcp`. The server never sees the
master passphrase; a locked vault tells the agent a human must unlock it, and
high-sensitivity secrets stay human-only. Denials are generic, so an agent can't
probe its way in.

[Model Context Protocol]: https://modelcontextprotocol.io

### Added
- **`svault mcp`** — a stdio JSON-RPC 2.0 MCP server (`src/mcp/`). Tools:
  `svault_get_secret` (the gated agent path — `name` / `scope` / `reason`, plus
  optional `vault` / `caller`) and `svault_list_vaults` (names + lock state). The
  `initialize` response carries a **capability descriptor** that tells agents how
  to request a secret without revealing the decision criteria (tiers, thresholds,
  and judge prompts stay server-side and encrypted).
- New [docs/mcp.md](docs/mcp.md): security model, tools, wiring into Claude Code /
  Cursor, and a raw transcript.
- **TUI MCP screen** (`m` on the vault list) — shows readiness (daemon + unlocked
  vaults), the `.mcp.json` config snippet, and `w` to write/merge that entry into
  `./.mcp.json` (preserving any other servers). The stdio server is still launched
  by the agent platform; this screen wires and arms it.

### Changed
- The gated agent path is now one shared core function, `core::gate::gated_get`
  (authorize → audit → usage → fetch, no prompting/printing), used by both the CLI
  local fallback and the MCP server so they enforce identically.

## [0.9.6] - 2026-06-01

The **layered source** release. Everything used to live flat in `src/` with all
20 modules and the entire CLI (2391 lines) declared in `main.rs`, as a
binary-only crate. 0.9.6 reshapes the source into a library crate with explicit
layers — a reusable, frontend-agnostic `core` plus the frontends that drive it —
so future frontends (an MCP server, a GUI) can reuse `core` without touching the
CLI or TUI. The restructure is **purely structural** — no behavior, CLI surface,
or on-disk format changed, and there is nothing to migrate — and the release also
bundles a few TUI UX fixes alongside it.

### Added
- **Per-vault judge assignment (CLI + TUI).** You can now choose *which* keyring
  judge gates a vault, not just whether the judge is on. The TUI create/settings
  forms gained an **Assigned judge** picker (space / ←→ cycles `default` → each
  judge in the keyring), and `svault create` / `svault settings` gained the
  matching **Assigned judge** prompt. `svault settings` now also toggles the
  per-vault AI judge on/off (previously it managed neither). All wired to the
  vault's encrypted `judge` field; `default` falls back to the keyring's default
  judge, and the vault's current judge stays selectable even when the keyring is
  locked or the judge was renamed.

### Changed
- **Clearer judge API-key state in the TUI.** The judge manager now labels each
  judge's key as `key set` (stored, green), `env key` (`$SVAULT_OPENROUTER_KEY`,
  amber), or `no key` (red), with a one-line legend. When the judge is **on** but
  the active judge has no usable key, a warning calls it out (medium/high requests
  would otherwise fail silently). The judge detail view and add/edit hints spell
  out the stored-vs-env options.
- **More readable TUI colors.** Body text is now white and muted text a readable
  mid-grey (was `DarkGray`, near-invisible on many terminals).
- **Source restructured into layers.** `src/` is now a library crate (`src/lib.rs`)
  with the bin (`src/main.rs`) reduced to a thin wrapper over `cli::run()`:
  - `src/core/` — crypto, vault storage, the policy engine, the AI judge, gate,
    metadata, keyring/master-key management, recovery, audit, usage, session, and
    the supporting file/passphrase/config primitives. Self-contained; no frontend
    dependencies.
  - `src/daemon/` — the Unix unlock daemon (`mod.rs`) and its client (`client.rs`),
    moved from `src/daemon.rs` and `src/client.rs`.
  - `src/tui/` — the interactive terminal UI (unchanged location).
  - `src/cli/` — the `svault` command-line frontend (the former body of `main.rs`),
    exposing `cli::run()`.
  - `src/mcp/`, `src/gui/` — placeholders for future MCP and GUI frontends.
- Cross-module references now path through the layer (`crate::core::…`). The
  `core` module deliberately shadows the std `core` crate; the source uses `std`
  throughout, so this is safe.

## [0.9.5] - 2026-05-31

The **keyring-under-master** release. 0.9.4 unified every vault under one master
passphrase but left one exception: the keyring (the encrypted store for the AI
judges, their API keys, criteria, and operational config) still had its own
separate passphrase. 0.9.5 removes that last extra secret — the keyring is now a
keyslot-backed store exactly like a vault, opened by the **same master
passphrase**. One secret now opens everything.

**No migration (pre-release):** the keyring's on-disk model changed (it is now
encrypted under a random data key wrapped under the master, not under a keyring
passphrase). Delete any old `.svault/` and recreate: `svault master init`, then
`svault keyring init` and `svault create`.

### Added
- **Keyring keyslot** (`src/master.rs`): the keyring gets a random data key (DEK)
  that encrypts `keyring.enc`; the DEK is wrapped under the master key in
  `.svault/keyring.keyslot.enc` (the same wrap/unwrap the vaults use).
- **`svault keyring init`** now creates the keyring under your master passphrase
  (reusing the master session if unlocked, prompting it otherwise, or setting one
  on first ever use) — there is no separate keyring passphrase.
- **`svault unlock`** (no vault argument) now also unlocks the keyring under the
  same master, so the AI judge is live without a second prompt. It works even
  with no vaults yet (a keyring-only setup).
- **Master recovery code** — setting the master passphrase (via `svault master
  init`, the first `svault create`, or the TUI set-master step in create / the
  judge screen) now generates a one-time recovery code, wrapped around the master
  key in `.svault/master.recovery.enc` and shown once. **`svault master recover`**
  resets a forgotten master passphrase with it; because it wraps the master key
  directly, this single code reopens every store (all vaults **and** the keyring).
  The TUI shows it on the same one-time screen as the vault recovery code.

### Changed
- **The keyring has no passphrase of its own.** `svault keyring unlock` opens it
  via the master; `svault keyring rekey` is gone — change the secret with
  `svault master rekey` (it covers vaults and the keyring at once).
- **`svault lock --all`** now also clears the keyring session.
- **TUI judge screen**: unlocking or creating the keyring goes through the master
  passphrase (or the live master session) instead of a keyring passphrase.

### Removed
- `Keyring::init/open/rekey` (the passphrase-based keyring path) and the
  `svault keyring rekey` command.

## [0.9.4] - 2026-05-31

The **unified-unlock** release. Until now every vault had its own passphrase — up
to N secrets to type. 0.9.4 replaces the per-vault passphrases with a single
**master passphrase** using a keyslot model: each vault gets a random data key
(DEK) that encrypts it, and that DEK is wrapped under the master. Unlock once and
every vault opens. This is the foundation for additional unlock methods (a YubiKey
touch, the recovery code) — each is just another keyslot over the same key, so any
one of them opens everything.

The keyring (the optional store for the AI judge's config and API keys) still has
its own passphrase in this release; bringing it under the master is the next step
in the unified-unlock milestone.

**No migration (pre-release):** the on-disk model changed. Delete any old
`.svault/` and recreate your vaults — `svault master init`, then `svault create`.

### Added
- **Master passphrase** (`svault master init | rekey | status`): one secret that
  unlocks every vault. `init` sets it (and, on first `svault create`, you are
  prompted to set it inline); `rekey` changes it without re-encrypting any vault
  (only the wrapped master key is rewritten); `status` shows whether it is set,
  unlocked, and how many vaults are wrapped under it.
- **Keyslot model** (`src/master.rs`): a random 32-byte master key (MK) is wrapped
  under a passphrase-derived KEK in `.svault/master.enc`; each vault's random data
  key is wrapped under MK in `<vault>/keyslot.enc`. Generalises the existing
  `recovery.enc` wrap/unwrap.
- **TUI master flow**: the create form sets the master passphrase on first run
  (passphrase + confirm), prompts the existing master when one is set but locked,
  or skips straight through when it is already unlocked; the unlock prompt is now
  a master-passphrase prompt that unwraps the selected vault's key.

### Changed
- **`svault create` no longer asks for a per-vault passphrase.** A new vault gets
  a random data key wrapped under the master; if no master exists yet, you set
  one first.
- **`svault unlock` (no vault) opens every vault at once** by unwrapping each
  vault's key with the master — into the daemon's memory if it is running, else a
  `0600` file session. With a vault name it unlocks just that one, still via the
  master. `svault lock --all` now also clears the master session.
- **`svault recover`** re-attaches a vault to the master: the recovery code
  unwraps the (unchanged) data key, which is then wrapped under the current master
  — no per-vault passphrase reset, no re-encryption.

### Security
- The master passphrase is the only thing a passphrase wraps; vault data keys are
  random and never derived from it. Changing the master rewraps one small slot and
  never touches vault ciphertext.
- Same-UID trust model is unchanged: keyslots close the read-the-files path; they
  are not a sandbox against a hostile same-UID process reading the unlocked
  daemon's memory or a `0600` session.

## [0.9.3] - 2026-05-31

The **encrypted-keyring and multiple-judges** release. 0.9.2 encrypted the policy,
but two plaintext artifacts still leaked abusable material at rest: the judge
config in `.svault/config.yaml` (model and thresholds — read them, tune a passing
request) and the OpenRouter API key in `~/.config/svault/openrouter.key` (`0600`,
but plaintext and directly stealable). Both are gone. All global config now lives
in a single AES-256-GCM-encrypted **keyring** (`.svault/keyring.enc`) under its own
passphrase. The judge is no longer single and global: you can define **multiple
named judges** — each with its own model, thresholds, free-text **criteria**
injected into its prompt, and **API key** — pick a **default**, and **assign one
per vault**.

**Honest scope:** the keyring is exactly as protected as a vault — it closes the
read-the-files path, but is not a sandbox against a hostile same-UID process
reading the unlocked daemon's memory or the `0600` session.

### Added
- **Encrypted keyring** (`svault keyring init | unlock | lock | rekey | status`): the single store for judges, their API keys, and operational config, AES-256-GCM encrypted (Argon2id) under its own passphrase. Unlock once per session (cached like a vault); until unlocked the judge is off and the static tier rules apply.
- **Multiple named judges** (`svault judge add | edit | remove | list | set-default | set-key`): each judge has a model, base URL, timeout, allow/high thresholds, free-text **criteria**, and its own key. `criteria` are appended to that judge's prompt so different vaults can be judged by different rules.
- **Per-vault judge assignment**: a vault's encrypted policy stores the *name* of the judge that gates it (or falls back to the keyring's default), so which judge — and which criteria — a vault uses is not readable at rest.
- `svault judge test [--judge <name>]` dry-runs a chosen judge (criteria included).

### Changed
- **No plaintext config or key files.** `.svault/config.yaml` and `~/.config/svault/openrouter.key` are removed; their contents (lock timers, daemon max-connections, backend, judge settings, API keys) live encrypted in the keyring. The daemon starts on built-in defaults and adopts the keyring's operational settings once it is unlocked (lock/connection/backend changes take effect on the next daemon start; the judge activates as soon as the keyring is unlocked).
- The TUI `shift-J` screen is now a full **judge manager** backed by the keyring: create the keyring (passphrase + confirm) when none exists, unlock it, toggle the global switch, **add** and **edit** judges with a multi-field form (name, model, base URL, timeout, allow/high thresholds, criteria), **view** a judge's detail, set the default, set/clear a judge's key, test, and remove. Equivalent to the `svault keyring` / `svault judge` commands, without leaving the TUI.
- `$SVAULT_OPENROUTER_KEY` remains as an explicit, opt-in per-judge fallback when a judge has no stored key (an env var, never a file).

### Security
- The OpenRouter API key is no longer a plaintext file — it is AES-256-GCM encrypted in the keyring, decryptable only with the keyring key held in memory after unlock.
- Judge models, thresholds, and **criteria** are encrypted at rest, so an agent can't read them to tune a request that passes.

### Migration
- No migration (pre-release): delete any old `.svault/config.yaml` and `~/.config/svault/openrouter.key`, then `svault keyring init`, `svault judge add <name>`, `svault judge enable`.

## [0.9.2] - 2026-05-31

The **encrypt-the-policy-at-rest** release. Through 0.9.1 only secret *values*
were encrypted; the entire policy surface lived in the plaintext (HMAC-signed)
`meta.yaml` and the committable `svault.policy.yaml`. Signing stopped *tampering*
but did nothing against *reading* — a same-UID agent could read each secret's
tier, scope, purpose description, rate limit, and which callers hold which scopes,
then craft a request designed to pass. This release closes that reconnaissance
path: the whole policy now lives **AES-256-GCM encrypted inside `vault.enc`**, no
easier to read than the secrets themselves.

**Honest scope:** this defeats reading the files to plan a bypass. It is **not** a
sandbox against a hostile same-UID process that reads the unlocked daemon's memory
directly — that remains inherent to the documented same-UID trust model.

### Changed
- **Policy is encrypted at rest.** Per-secret classification (scope/tier/`require_reason`/description), the vault's `allow_agent` + `rate_limit`, the per-vault judge override, the default tier, and **caller rules** all moved out of the plaintext `meta.yaml` (and the former `svault.policy.yaml`) into a versioned, AES-256-GCM-encrypted payload inside `vault.enc` (`policy::VaultPolicyData`). The public `meta.yaml` now carries only non-sensitive metadata (name, description, storage, created-at, behavioural settings). Reading any file at rest reveals nothing an agent could use to plan a passing request.
- **Caller rules are now per-vault and encrypted.** The committable project-level `svault.policy.yaml` is gone; `svault policy init` seeds caller rules into a vault's encrypted policy, and `svault policy check <caller>` unlocks the vault to read them. (Team policy-as-code sharing will return via an explicit export path.)
- **Denials are generic to the caller.** Every denied `svault get` now returns a single opaque message (`denied: request not authorized for this secret`); the real reason — judge score + rationale, scope/caller mismatch, rate limit — is recorded only in the audit log, for the human. A caller can no longer learn what to change to make a denied request pass. (A future escalation hook can notify a channel on denials.)
- Policy view/edit now requires an unlocked vault (you can't read or change policy on a locked vault) — `svault settings`, `svault secret add`, the TUI classification table, and `svault policy check`/`init` all open the vault first.
- The vault list (`svault vaults`) no longer shows allow-agent / rate-limit columns (they're encrypted now) — use `svault policy check` to see who may access what.
- TUI: the judge-management shortcut is labelled **`shift-J`** (it always required Shift; `j` is list-down).

### Security
- Closes the policy-reconnaissance path: classification, caller scopes, rate limits, and judge thresholds-of-effect are unreadable at rest.
- Removes the score/rationale leak to the caller on a denial (no hill-climbing toward a passing request).

### Migration
- The on-disk vault payload format is bumped to v2. There is no migration — the project has no released users; create vaults fresh.

## [0.9.1] - 2026-05-30

Brings the **policy engine + AI judge to the TUI**. Everything 0.9.0 added on the
command line — key management, the global judge switch, per-secret
classification — is now drivable from the keyboard, and the change is reflected
in the audit timeline.

### Added
- **TUI judge management** — press `J` on the vault list to open the AI-judge screen: toggle the judge on/off globally, edit the model / allow- and high-thresholds / timeout (saved to `.svault/config.yaml`), **set** the OpenRouter key (masked entry, stored `0600`), **remove** it (`Del`), and run a live **test** that dry-runs a sample request and shows the verdict + score inline. This is the TUI equivalent of the `svault judge` commands, plus the global enable switch that previously needed a hand-edited config.
- **Per-secret classification in the secret browser** — the browser is now a table showing each secret's **TIER · SCOPE · REASON? · DESCRIPTION** (unclassified secrets read `unset`). Press `c` to **reclassify** a secret — edit its scope/tier/require-reason/description and re-sign `meta.yaml`; the secret value is never touched.
- **`svault judge enable` / `disable`** — flip the global judge switch from the CLI too (CLI/TUI parity). The judge acts only when this is on **and** a key is configured; a per-vault `meta.judge.enabled = false` still opts a vault out.
- **First-run guidance** — after creating a vault with the judge turned on, `svault create` now tells you exactly what's still needed for the judge to act (`svault judge set-key` and/or `svault judge enable`).

### Changed
- **Audit reflects judge/policy changes.** Reclassifying a secret records `secret.classify`, and global judge changes (`judge.config`, `judge.key.set`, `judge.key.remove`) are recorded to a value-free `.svault/usage.log`. The activity timeline (`v`) folds these global events in alongside a vault's own, sorted newest-first, so a policy/judge change made from the `J` screen is visible in the audit trail.
- `SvaultConfig` can now be persisted (owner-only `.svault/config.yaml`) so the judge config is editable without hand-editing YAML.

## [0.9.0] - 2026-05-30

The **enforced policy engine** release — closes the advisory-policy gap that all
three 0.7.0 reviews named as the 1.0.0 blocker (#2/#5/#22, review N-1/N-2/N-5) —
plus the **AI judge**, Svault's behavioural gate.

### Added
- **AI judge (OpenRouter)** — for medium/high-tier secrets (and any secret flagged `require_reason`), the daemon asks a cheap, fast LLM whether the caller's stated *reason* plausibly justifies the request, given the secret's scope/tier and the caller's recent activity. Configurable model (default `google/gemini-2.5-flash`), thresholds, and timeout in `.svault/config.yaml` `[judge]`. The key comes from `$SVAULT_OPENROUTER_KEY` or a `0600` key file — never committable config. **Off until a key is configured**, so upgrading never silently calls an external API. Synchronous (`ureq`, bundled rustls — no async runtime). Manage the key with `svault judge set-key` (hidden prompt or stdin → `0600` file), `svault judge status` (resolves the source without printing the key), and `svault judge remove-key`; `svault judge test [--vault --vault-description --description ...]` dry-runs a sample request against the live model without touching a secret.
- **Per-secret classification in the signed `meta.yaml`** — each secret carries a `scope`, `tier` (low/medium/high), `require_reason`, and an optional `description`, set via `svault secret add [--scope --tier --require-reason --description]` or interactively (and in the TUI add form). The **description** records what the secret is for and is given to the AI judge as context, so a request whose reason doesn't fit the secret's purpose is denied; the vault's own description is included too. Because it's HMAC-signed, a same-UID attacker can't downgrade a tier without the vault key (#5/#22). Vault creation now also sets a `default_tier` and a per-vault judge toggle.

### Changed
- **Policy is now enforced inside the daemon.** The agent path (`svault get`) sends a structured `GetGated` request; the daemon evaluates policy, consults the judge per tier, audits the decision, and only then returns a value — the socket is the single choke point (#2). The CLI runs the identical gate locally when no daemon is up. There is no longer an unguarded read path.
- **Tiers, with the judge on:** low = auto-allow; medium = judge-gated, **fail-open** + audit-flag if the judge is unavailable; high = judge-gated, **fail-closed**. With the judge **off**, behaviour is the pre-0.9.0 rule (high = human-only), so nothing regresses.
- **Audit trail** now records every daemon read (human and agent) stamped with the connecting process's **peer UID** — unforgeable, unlike the self-asserted caller string (N-1) — closing the unaudited-daemon-read gap (N-5).
- The policy file (`svault.policy.yaml`) now holds **caller definitions only**; secret classification moved to the signed meta. Discovery is **anchored to the project root** (no unbounded upward walk, #5) and a present-but-unparseable policy now **fails closed** (N-2).

## [0.8.0] - 2026-05-30

A security-review-response + hardening release. Acts on the three independent
0.7.0 model reviews (`docs/security-review/reviews/0.7.0-*.md`), consolidated with
maintainer dispositions in `docs/security-review/findings/0.7.0.md`. All three
re-confirmed the advisory-policy gap (#2/#5/#22) as the 1.0.0 blocker; that policy
work is **deferred to 0.9.0** (not dropped) so this stays a clean hardening
release. Everything else the review surfaced is addressed here.

### Fixed
- **Owner-only TUI export** (review N-3) — the TUI export wrote the bundle (which wraps the vault key) with the default umask, leaving it potentially world-readable; it now uses `secfile::write_owner_only` like the CLI path.
- **Owner-only import directory** (review N-4) — importing a bundle created the vault directory with the default `0755`; it is now `0700` (`secfile::create_dir_owner_only`), matching `Vault::init`. Regression test added.
- **`daemon.log` rotated file is `0600`** (review N-10) — the log is opened with mode `0600` so a rotated/recreated file is never group/other-readable (it already sat inside the `0700` `.svault/`).
- **Daemon transport zeroization** (review N-6) — the serialized reply buffer and the in-memory `Response::Secret` value are now wiped (`zeroize`) after the secret is written to the socket, instead of lingering in freed heap.
- **`sigaction` for shutdown signals** (review N-9) — SIGTERM/SIGINT handlers are installed via `libc::sigaction` (zeroed mask, `SA_RESTART`) rather than the legacy `signal()`, for well-defined semantics across Unix variants.

### Deferred (gates 1.0.0)
- Policy engine as an enforced control: evaluate policy + write the audit record inside the daemon `Get` path (#2/#5, review N-1/N-2/N-5/#22), authenticate the caller, sign/pin `svault.policy.yaml`, and fail closed on an unparseable policy. This is the last substantive gap before a 1.0.0 "stable CLI" label and is scheduled for 0.9.0.

## [0.7.0] - 2026-05-29

Continued security hardening on the road to a stable 1.0.0 CLI (GUI is planned
for 2.0.0, Claude/AI-platform access for 3.0.0). Acts on the 0.6.0 review
carry-forward (`docs/security-review/findings/0.6.0.md`).

### Added
- **Supply-chain CI gate** (#9) — a `cargo audit` job in `lint.yml` fails CI on any RustSec advisory (vulnerability, unsound, or unmaintained) across the dependency tree.
- **Passphrase entropy floor** (#12) — `create` and `recover` now require a passphrase to clear a ~50-bit entropy estimate (`passphrase::entropy_bits`), re-prompting until it does; a `--force` flag overrides for non-interactive use. The TUI create/recover forms enforce the same floor.
- **Release-artifact integrity + provenance** (#11) — each release archive ships a matching `<archive>.sha256`, plus a signed SLSA build-provenance attestation (`actions/attest-build-provenance`, verifiable with `gh attestation verify`).
- **Daemon peer-UID bond** (#1) — the daemon checks `getpeereid` on each connection and refuses any peer whose UID isn't our own, on top of the `0600` socket.

### Changed
- **`ratatui` 0.29 → 0.30** (#10, crossterm 0.28 → 0.29) — pulls a fixed `lru` and drops the unmaintained `paste` crate, so `cargo audit` is now clean. No source changes were needed.
- **Daemon key handling — passphrase never crosses the socket** (#3) — `Unlock` now carries the hex-encoded 32-byte derived key. The client derives + validates the key locally (a wrong passphrase fails before any socket traffic); the daemon re-validates it with `open_with_key` before caching.
- **Owner-only at-rest files** (#14, #16) — `recovery.enc`, export bundles, and the `.session` key are written owner-only (mode `0600` on Unix, an `icacls` owner-only ACL on Windows — the latter also closes the Windows half of #4). `.svault/` and vault dirs are created `0700`, and the daemon socket is bound under a `0077` umask so it's born `0600` (no TOCTOU window).

### Security
- **Zeroized secrets in memory** (#6) — passphrase / recovery-code / secret-value prompts and `get_secret`'s return are `Zeroizing<String>`, and the TUI reveal modal holds the secret in `Zeroizing`, so these heap copies are wiped on drop (the bulk decrypted store was already zeroized via `SecretStore`).
- **Graceful daemon shutdown** (#17) — `SIGTERM`/`SIGINT` now trigger an orderly shutdown that zeroizes keys and cleans up the socket/pid files (instead of an abrupt terminate); `daemon.log` rotates past ~5 MB.

### Internal
- New `secfile` module (owner-only file/dir writes). Suite now 84 (+ 1 ignored benchmark); `cargo audit`, `cargo fmt`, and `cargo clippy -D warnings` all clean across Linux/macOS/Windows/Fedora CI.

> **Not in this release:** policy enforcement in the daemon + a signed/pinned policy file (#2/#5) — the policy layer remains advisory/audit-only for now and is the next iteration's focus.

## [0.6.0] - 2026-05-29

Security-hardening release. Acts on the consolidated 0.5.0 review register
(`docs/security-review/findings/0.5.0.md`) and adds a logged concurrency stress
simulation. Carry-forward status for every 0.5.0 finding is recorded in
`docs/security-review/findings/0.6.0.md`.

### Added
- **Security review process** — `docs/security-review/` documents a release-gated security workflow: every `0.x.0` release gets one or more independent, model-agnostic security reviews (prompt in `docs/security-review/PROMPT.md`) plus a tooling/bulletproofing pass. For 0.5.0, five independent reviews (Grok 4.3, GLM-5-1, Gemini 3.5 Flash, DeepSeek-V4-Pro, Claude Opus 4.8) are recorded under `docs/security-review/reviews/`, with all findings de-duplicated into a consolidated decision register at `docs/security-review/findings/0.5.0.md`. Maintainer dispositions for all 22 findings are recorded and signed off in that register.
- **Configurable daemon connection ceiling** (finding #8) — `daemon.max_connections` in `.svault/config.yaml` (default 512) caps simultaneously-served connections so the thread-per-connection model can't be driven to spawn unbounded handler threads. Each connection also gets a 30 s read timeout so a stalled client can't pin a handler. A connection refused at the ceiling gets a `too many connections` error and the client falls back.
- **Concurrency stress simulation** — an `#[ignore]`d benchmark (`daemon_stress_simulation`) drives the real daemon under sustained parallel reads plus a connection flood, classifying every outcome (correct / busy-refused / connect-error / wrong) and logging latency percentiles + throughput. Methodology and a recorded run live in `docs/security-review/stress/0.6.0.md`. Run: `cargo test --release daemon_stress_simulation -- --ignored --nocapture`.

### Changed
- **TUI footer survives narrow windows.** The key-hint footer used to clip from the right on small terminals, hiding the "help" and "quit" hints entirely. It now falls back to a compact hint that always keeps `h/? help` visible, and the help overlay opens with **`h`** as well as `?`.
- **Import no longer errors when the name already exists.** Re-importing a bundle onto a machine that already has that vault now picks a free name by appending a suffix (`TUI-Vault` → `TUI-Vault-2`), or you can choose one with `svault import <file> --name <NEW>`. Since the vault name is part of the HMAC-signed `meta.yaml`, importing under a different name re-signs it and asks for the passphrase once to finish (a clean import under the original name still needs no passphrase). The TUI import flow does the same.
- **Session caches the derived key, not the passphrase** (finding #4) — the no-daemon `.session` fallback now stores the vault's 32-byte derived key (hex, mode 0600 on Unix) instead of the raw passphrase, on every platform including the TUI. A stolen `.session` still opens that one vault, but no longer leaks the reusable passphrase that may protect other vaults or services. The daemon (keys in memory, nothing on disk) remains the preferred path. Pre-0.6 sessions that cached a passphrase are treated as locked and require a re-unlock.

### Fixed
- **Daemon survives a poisoned mutex** (finding #13) — the key-store lock is now taken with poison recovery, so a panicking connection handler can no longer take down the whole daemon (and every key it holds) on the next lock.
- **Truncated `vault.enc` returns an error instead of panicking** (finding #20) — `save_secrets` length-checks the salt slice rather than `unwrap()`-ing it.
- **Daemon connect resilience** — `daemon::send` retries the socket connect a few times with short backoff, so a momentary OS listener-backlog drop under connect-churn surfaces as a served request rather than a hard error.

### Internal
- New `DaemonConfig` in `config.rs`; the connection ceiling is threaded through `serve`. Stress-classification and poison-recovery covered by new tests. Suite now 82 (+ 1 ignored benchmark).

## [0.5.0] - 2026-05-29

### Added
- **Background daemon (Unix)** — `svault daemon start | stop | status | doctor | run`. An opt-in local process that holds unlocked vault keys **in memory** and serves secret reads over a `0600` Unix socket (`.svault/daemon.sock`), replacing the on-disk `.session` while it's running. Keys are zeroized on lock, on auto-lock, and on shutdown. See [docs/daemon.md](docs/daemon.md).
  - **Auto-lock** — idle timeout (default 15 min, reset on each read) and a hard-max cap (default 8 h), both configurable in `.svault/config.yaml`. A background ticker evicts and zeroizes expired keys.
  - **`daemon doctor`** — health check: daemon liveness + pid, socket presence and `0600` permissions, effective timeouts, and detection of stale socket / pid files left by a crash. `--fix` cleans them up; exits non-zero when unhealthy.
  - **Daemon-aware commands** — when a daemon is running, `unlock` caches the key in it (no `.session` file written), and `get` / `secret get` are served from memory with no prompt; `status` shows `unlocked (daemon)`; `lock` / `lock --all` drop keys from both the daemon and any file session.
  - **TUI integration** — the interactive UI shows a `daemon running` / `daemon off` indicator in the header, and `d` on the vault list starts / stops the daemon.
  - **Concurrency** — one thread per connection; a `Get` holds the shared lock only long enough to copy the key and bump the last-used timestamp, decrypting outside the lock so parallel reads don't serialize.
- **Source/surface tracking in the logs** — `usage.log` and `audit.log` now record a `source` field (`cli` / `tui` / `gui` / `mcp`) alongside the actor (human/agent). Together they distinguish a human at the CLI, a human in the TUI, an agent via the CLI, and (later) a GUI or MCP caller. The TUI activity view (`v`) gains a **VIA** column; events from before this change show `-`.

### Changed
- The daemon is **Unix-only**. On Windows, `svault daemon <...>` reports that it's Unix-only and all other commands fall back to the file session unchanged.
- `secret add` / `secret list` / `secret remove` still prompt for the passphrase even when the daemon is up — the daemon deliberately holds only the key (not the passphrase) and exposes no write operations over the socket.

### Internal
- New `daemon` and `client` modules; `libc` added as a Unix-only dependency (`setsid` to detach, `kill` for liveness / stop).
- 7 new tests (protocol JSON roundtrips, idle / hard-max / active auto-lock decisions, a unix unlock→get→lock→shutdown integration test, and a 16-thread concurrent-reads stress test) — suite now 72.

## [0.4.0] - 2026-05-29

### Added
- **Usage log + activity view** — every vault now keeps a per-vault `usage.log` (JSON lines, owner-only, gitignored) recording human and agent activity (unlock, lock, secret add/get/reveal/remove, create, export, import, recover, settings, and agent `get.allow`/`get.deny`). It never stores secret values. Press `v` on a vault in the TUI for a read-only timeline (WHEN / ACTOR / ACTION / TARGET, agents highlighted). This is groundwork for later usage analysis.
- **CREATED column** in the TUI vault table, from each vault's `created_at`.
- **TUI help overlay** — press `?` on the vault list or in the secret browser for an on-screen keybinding cheat sheet.
- **Quit confirmation** — `q` / `Esc` on the vault list (or `q` in the secret browser) now asks before exiting: `enter` quits, any other key stays.
- **Paste support** — bracketed paste works in every TUI text field (passphrases, recovery codes, bundle paths); newlines are stripped so a multi-line paste can't break the layout.
- **Block cursor** — the focused text field shows a solid cursor block (even when empty) so it's obvious where typed/pasted text lands.

### Changed
- **Vault list is now a real table** with STORAGE / VAULT / STATUS / DESCRIPTION columns and a clean selection highlight, replacing the single packed line whose per-cell reverse-video highlight looked garbled.
- **TUI status line** — transient `ok` / `warning` / `error` / `note` messages moved out of the title bar into a dedicated line below the body.
- **Readable empty states** — the "No vaults yet" / "No secrets yet" placeholders are now a high-contrast heading plus a readable hint instead of hard-to-read dark gray.
- **Clearer field label** — the create/recover forms label the second passphrase field "Confirm passphrase" (was just "Confirm").
- **Honest pickers** — the create and settings forms no longer offer non-functional Storage (cloud / self-hosted / S3) and Login (yubikey / google) choices that were silently coerced to `local` / `passphrase`. Storage and login are shown as a static note instead; the roadmap items live in the docs.
- **Recovery code is shown as typed** (not masked) on the TUI recover form, so a mistype while copying it from paper or a password manager is visible.
- Centralized TUI colors into a single theme module for a consistent look.

### Fixed
- Pressing `space` while editing the **Rate limit** field in the TUI create form no longer secretly toggles auto-lock (an off-by-one in the focus handling). Form focus now uses named field enums so the draw order and key logic can't drift apart.
- **TUI export** now reports the full absolute path of the written bundle instead of a bare filename, so the file is easy to find.

### Internal
- Added unit tests for TUI key dispatch and field logic (no terminal required), covering the bug fix and navigation/paste/help behavior.

## [0.3.0] - 2026-05-29

### Added
- **Recovery code** — `svault create` now generates a one-time 160-bit recovery code and stores the vault key wrapped under it in `recovery.enc`. `svault recover` uses the code to reset a lost passphrase (re-keys the vault; the code stays valid). Part of Step 3.
- **Export / import** — `svault export` writes a portable, checksummed JSON bundle of an encrypted vault (`meta.yaml` + `vault.enc` + `recovery.enc`); `svault import` verifies the checksum and restores it on another machine, refusing to overwrite an existing vault name.
- **TUI support** — the recovery code is shown once on its own screen after create and requires an explicit `y` confirmation that it was saved; the vault list gains `e` export, `i` import, and `r` recover, mirroring the CLI.

### Security
- The recovery code is displayed once and never stored in plaintext; both the CLI and TUI now require an explicit acknowledgment that it was saved before continuing.

## [0.2.1] - 2026-05-29

### Added
- **Storage backends** — pick where a vault lives at create time: `local` (default, the only one wired today) plus `cloud`, `self-hosted`, and `s3` placeholders for upcoming remote sync. Selectable in both `svault create` and the TUI create form.
- `storage:name` prefix shown everywhere a vault is listed (`svault vaults`, `svault status`, the TUI) so vault identity is unambiguous per backend.
- `CHANGELOG.md` (this file).

### Changed
- **Documentation revamp** — the README is now a lean landing page (badges, doc index, collapsible sections, Mermaid diagrams) and the long-form content moved into a dedicated `docs/` folder (`installation`, `tui`, `commands`, `policy-engine`, `storage-backends`, `security`, `architecture`, `roadmap`).
- `storage` is now a required field on `meta.yaml` — vault names must be unique, and creating a duplicate name is rejected.

### Removed
- Backward compatibility for vaults created before the `storage` field existed (Svault is in beta — re-create affected vaults).

## [0.2.0] - 2026-05-29

### Added
- **Policy engine (Step 2)** — `svault get` is the agent path: a structured request an AI must justify, run through a pipeline (caller identity → required reason → scope capability check → sensitivity tier → rate limit + burst detection → audit log).
- Committable `svault.policy.yaml` at the project root defining callers (scopes + rate limit) and per-vault secret scope/tier; `svault policy init` scaffolds it.
- Sensitivity tiers — `low` (auto-allow), `medium` (allow + flagged in audit), `high` (denied for agents).
- `svault policy check <caller>` — show a caller's scopes, accessible secrets, rate limit, and recent activity.
- Append-only, gitignored audit log at `.svault/<vault>/audit.log`; falls back to `meta.yaml` `allow_agent` / `rate_limit` when no policy file is present.

## [0.1.1] - 2026-05-29

### Added
- **Interactive TUI (Ratatui)** — run `svault` with no subcommand for a full-screen dashboard: vault list with live lock state, form-based create, lock-aware unlock/lock, settings editor, and a secret browser (add / view / delete) once a vault is unlocked.
- `svault create` (with `init` kept as an alias) and `svault settings` commands; per-vault targeting via positional name or `-v/--vault`.
- CI across Ubuntu, Fedora, macOS, and Windows, plus a release workflow; status and crates.io badges in the README.

## [0.1.0] - 2026-05-28

### Added
- **Local encrypted vault (Step 1)** — AES-256-GCM encryption with Argon2id key derivation (GPU-resistant).
- HMAC-SHA256 signed `meta.yaml` — tampering is detectable.
- `ZeroizeOnDrop` on the vault key and secret store — secrets wiped from memory on drop.
- `svault secret add | get | list | remove`, session-based lock/unlock, and `svault status`.
- Per-vault `.gitignore` written at init so `.session` is never committed; `vault.enc` + `meta.yaml` are safe to commit.
- Published to crates.io as [`svault-ai`](https://crates.io/crates/svault-ai).
