# Svault 0.9.9 — Findings Register

> **Status: maintainer consolidation of three independent reviews.** 0.9.9 is the
> last feature milestone before 1.0.0 — it adds **conditional access** (time
> windows + required callers, in the encrypted policy) and **seal & escalate**
> (repeated denials seal a medium/high secret for a human to clear). This register
> consolidates the three independent 0.9.9 reviews in `reviews/` and records the
> maintainer disposition for each finding. The reviews were run against the
> `feat/0.9.9-conditional-access-seal` branch (commit `c1696e9`).

- **Version:** 0.9.9
- **Date:** 2026-06-02
- **Maintainer:** Nima Karimi
- **Independent reviews consolidated:**
  - `reviews/0.9.9-claude-opus-4.8.md` — Claude (Opus 4.8), in-place against the working tree.
  - `reviews/0.9.9-grok-biuld.md` — opencode (x-ai/grok-build-0.1), fresh clone.
  - `reviews/0.9.9-openai-gpt-5.5-opencode.md` — OpenAI GPT-5.5 via OpenCode, fresh clone.
- **Tooling pass (all three independently):** `cargo test` (140 pass, 1 ignored stress), `cargo clippy --all-targets --all-features -- -D warnings` (clean), `cargo audit` (no advisories, exit 0), `cargo fmt --all --check` (clean).

## Headline: no Critical/High; the consensus is "1.0-ready as scoped"

All three reviewers independently report **zero Critical/High findings**: no
unauthenticated decryption, no key written world-readable, no agent read path that
returns a classified secret without running the gate, no tier/scope/seal tamper
without the vault key (policy + seals live inside GCM-authenticated `vault.enc`;
`meta.yaml` is HMAC-signed and carries no classification). Overall ratings cluster
at **Low–Moderate on Unix+daemon, Moderate on Windows/CI**. All three endorse a
1.0.0 *as scoped* — cooperative/semi-trusted agents + audit — and all three
explicitly decline to endorse marketing the judge/seal as a boundary against a
hostile **same-UID** process. The new 0.9.9 features were verified sound: the
seal counter (`audit::recent_for_secret`) is **caller-agnostic** (counts denials
per-secret across any caller string), the seal write-on-deny is **bounded** (short-
circuits once sealed → no rewrite storm), and conditional/sealed denials collapse
to the same `GENERIC_DENY` an agent sees for any other denial.

## Medium findings — consolidated

IDs are assigned in this register; the source column maps to each reviewer's own
numbering. Reviewer shorthand: **C** = Claude, **G** = Grok, **O** = GPT-5.5/OpenCode.

| # | Finding | Raised by | Maintainer disposition |
|---|---------|-----------|------------------------|
| **M9-1** | **Self-asserted `--caller` → rate/burst evadable by caller rotation; `require_callers` is a cooperative control, not strong identity.** Rate-limit + burst counts key on the caller string the agent supplies (`policy.rs` rate_and_burst); rotating `--caller`/`$SVAULT_CALLER` evades both and degrades the judge's recent-activity signal. The new 0.9.9 seal counter is **correctly immune** (per-secret across callers). | C-M2, O-M1+M4, G(recs) | **Fixed (burst) in 0.9.9 — caller-rotation backstop landed.** Added a caller-agnostic per-secret burst ceiling (`SECRET_BURST_MAX`, counted across every caller via `allowed_count_for_secret`), so rotation can no longer evade burst detection; regression test `caller_rotation_cannot_evade_the_per_secret_burst_ceiling`. **Residual (accepted, documented):** the per-caller *rate limit* and *scope authorization* still key on the self-asserted caller — inherent to the cooperative same-UID model (carry-forward of **N-1**), and documented in `docs/security.md`. A verified caller identity (peer-UID/token binding) remains a post-1.0 item, not a blocker. |
| **M9-2** | **Seal approval does not require fresh master re-auth.** `svault approve` / TUI `A` clear an encrypted seal via `open_unlocked_or_prompt`, which accepts a cached session — so a same-UID shell with a live session can clear a seal without re-entering the master. "Agent can't clear without master" holds for the MCP/gated path, not for a same-UID process with an existing session. | O-M2 | **Fixed in 0.9.9.** `svault approve` now opens via `open_with_fresh_master` (`ensure_master_unlocked_inner(.., fresh=true)`), which ignores the cached session and re-prompts the master credential (passphrase or YubiKey touch); a non-TTY context fails the prompt and so refuses approval. The TUI `A` path is a present human behind the app login + 6h cap. |
| **M9-3** | **CLI agent `get` can trigger an interactive master prompt that opens the 6h ungated window.** Daemon down + vault locked → `svault get` (agent path) falls through to `open_unlocked_or_prompt`, prompting for the master; if a human types it, the vault is cached for the full 6h and is thereafter readable via the *ungated* human path. MCP correctly never prompts. | C-M4 | **Fixed in 0.9.9.** The agent `get` local fallback now uses `open_unlocked_only` (session-only, never prompts); a locked vault returns "locked — a human must run `svault unlock`", matching `svault mcp`. |
| **M9-4** | **Seal/escalate is an intentional availability lockout (DoS surface).** 5 denials in 5 min on a medium/high secret seals it → forces human approval, which can break a CI run (e.g. a flaky test that trips denials). Bounded: short-circuits once sealed, no rewrite storm. | O-M3, G#4 | **Accept with a follow-up knob.** Behaviour is by design and bounded. Backlog: configurable threshold/cooldown and an "audit-only escalation" mode for CI where lockout would break builds. Not a 1.0 blocker. |
| **M9-5** | **AI judge is a soft, prompt-injectable control + metadata egress to OpenRouter.** Attacker-controlled `reason`/`caller`/`scope` are interpolated into the judge prompt; medium fails open, high fails closed. No secret *value* leaves, but name/scope/caller/reason and user-authored descriptions go to a third-party LLM. | C-M3, G#3, O-L2 | **Addressed in 0.9.9 (doc + tests); soft-control nature accepted.** Carry-forward of **J-1/J-2**. Added prompt-injection regression tests (`judge.rs`): injected `reason` stays a labelled data field, an explicit `deny` is honoured regardless of score, a non-standard decision token degrades to Unavailable (high fails closed). The OpenRouter metadata-egress and self-asserted-caller notes are already prominent in `docs/security.md`. The judge remains friction, not an authorization oracle — by design. |

## Low / Informational — consolidated

| # | Finding | Raised by | Disposition |
|---|---------|-----------|-------------|
| L9-1 | **Windows ACL is best-effort** — `icacls /inheritance:r /grant:r` is fire-and-forget; no daemon, no peer-UID bond on Windows. | C-L3, G#2, O-L3 | **Accepted / de-prioritized** (carry-forward N-7). Unix daemon is the primary path; Windows is documented reduced-assurance. Optional polish: surface `icacls` failures instead of ignoring them. |
| L9-2 | **0.9.9 not yet tagged/merged** — `origin/main` is 0.9.8; latest tag `v0.9.8`. Release state ambiguous for downstream reviewers. | O-L1/P0 | **Resolves on merge/tag.** The reviews targeted the feature branch deliberately. |
| L9-3 | **`serde_yaml` is deprecated** (`v0.9.34+deprecated`). Audit-clean; maintenance signal only. | O-L4, G | **Backlog.** YAML is confined to non-sensitive HMAC-signed `meta.yaml`; plan migration over time. |
| L9-4 | **Session files are key-equivalent** for ≤6h (documented residual). | C-L1, G | **Accepted** — clearly communicated; daemon avoids on-disk keys entirely. |
| L9-5 | **`meta.description` is plaintext** (HMAC-signed) and is fed to the judge as vault purpose. | C-L2 | **Accepted** — user-authored, low impact; docs warn to keep descriptions non-sensitive. |
| L9-6 | **AES-GCM random 96-bit nonces** — safe at Svault's write volumes. | C-L4 | **Accepted** (carry-forward #7). Worth a comment if a vault ever exceeds ~2^32 re-encrypts. |
| L9-7 | **`secure_remove` is best-effort** (zero-then-delete, no guaranteed fsync/multi-pass). | G | **Accepted** — adequate under the at-rest/cooperative model. |
| L9-8 | **Decrypted secret `HashMap` in `load_payload` not per-entry zeroized.** | G | **Accepted** (carry-forward N-6) — consistent with the same-UID cooperative model. |
| L9-9 | **JSON line protocol over AF_UNIX has no length framing.** | G | **Accepted** — clients are first-party; no untrusted network surface. |
| L9-10 | **`.sha256` sidecar is not attested** (the release tarball carries SLSA provenance). | G | **Backlog** — minor; the artifact itself is attested. |
| L9-11 | **Entropy floor is a simple length×log2(pool) estimate.** | G | **Accepted by design** (carry-forward N-13) — conservative; `--force` escape exists. |

## Positive properties — unanimously confirmed

All three reviewers independently verified, against source: everything-encrypted-at-
rest (policy/classification/callers/judge-assignment/**seals** inside GCM-authenticated
`vault.enc`; `meta.yaml` HMAC-signed, no classification); unified master keyslot is
**OR-not-2FA** and fails closed on the GCM tag; **6h hard cap** enforced identically on
file sessions and the daemon (backdated → reads locked + securely removed); the daemon
holds keys in memory behind a `0600` socket under `umask(0077)` with a correct, fail-
closed **peer-UID bond**, connection ceiling, 30s read timeout, poison recovery, and
zeroizing graceful shutdown; the client derives the key so **only a 32-byte key crosses
the socket**; **generic denials** with full rationale only in the peer-UID-stamped audit
log; MCP is a faithful thin frontend that never prompts and is a dead end on a locked
vault; supply chain gated by `cargo audit --deny warnings` + `clippy -D warnings`, with
SHA-256 sums + SLSA build-provenance on release.

## Fixed in 0.9.9 (so 1.0 is test-and-ship)

The actionable should-fix set was landed in this same 0.9.9 release rather than
deferred — the intent is for 1.0 to be QA + release, not a code change:

- **M9-3** — agent `get` is now non-prompting (`open_unlocked_only`); a locked vault is
  a dead end for the agent, exactly like `svault mcp`.
- **M9-2** — `svault approve` requires a fresh master credential (`open_with_fresh_master`),
  ignoring the cached session; non-TTY refuses approval.
- **M9-1 (burst)** — caller-agnostic per-secret burst ceiling (`SECRET_BURST_MAX`) closes
  the caller-rotation evasion; new regression test.
- **M9-5** — judge prompt-injection regression tests added; existing doc notes confirmed.

Tooling after the fixes: `cargo test` 144 pass / 1 ignored, `cargo clippy
--all-targets --all-features -D warnings` clean, `cargo fmt --all --check` clean.

## Remaining before 1.0.0

1. **Run the QA checklist** (`docs/qa-checklist.md`) end-to-end across CLI/TUI/MCP,
   including the new non-prompting agent path and the fresh-master `approve`.
2. **M9-1 residual (post-1.0, not a blocker):** a *verified* caller identity (peer-UID /
   token binding) for the per-caller rate limit and scope authorization. The cooperative
   same-UID model is documented; the burst evasion — the concrete exploit — is closed.
3. **M9-4 (optional, CI ergonomics):** configurable seal threshold/cooldown + audit-only
   escalation mode.
4. **Tag/merge 0.9.9** (clears L9-2).

## Sign-off

Three independent models converged: no Critical/High, the 0.9.9 features (conditional
access + seal/escalate) verified sound and correctly caller-agnostic, and a 1.0.0
endorsed **as scoped** (cooperative agents + audit, not a same-UID sandbox). The
actionable pre-1.0 set — non-prompting agent path (M9-3), fresh-master approve (M9-2),
caller-rotation burst backstop (M9-1), and judge-injection tests (M9-5) — was **fixed in
this 0.9.9 release** so 1.0 is a test-and-ship. The only residuals are the inherent
same-UID caller-identity limitation (documented, post-1.0) and the de-prioritized
Windows item (L9-1).

— Nima Karimi, 2026-06-02
