# The Ledger — Operation Tracker

> This file is the SINGLE SOURCE OF TRUTH for kv-secrets development.
> Updated before every step, after every step. If it's not here, it didn't happen.

**Current branch**: `main` (v0.2.0)
**Last session**: 2026-03-26 (Audit + hardening + features — 3 commits pushed)
**Repo**: Clean. ~6,400 LOC across 21 modules. 143 tests passing, 0 errors.

---

## What kv-secrets Is

A product. On PyPI. For any developer who uses AI coding agents.

**The problem no one has solved:**
AI coding agents (Claude Code, Cursor, Copilot) need API keys to do real work —
make API calls, run apps, deploy. Today, developers paste keys into `.env` files,
into chat, into environment variables. The agent sees the key. It's in the context
window, in logs, in tool outputs, everywhere. One prompt injection, one confused
agent, one careless moment — the key is compromised.

dotenvx encrypts at rest but doesn't address agents. Doppler is cloud-only at
$21/user. Vault is overkill for small teams. Nobody builds for this problem because
nobody has framed it correctly: **the agent should be able to USE the key without
POSSESSING it.**

**The kv answer:**
A daemon holds secrets in memory. The agent never sees them. Two controlled channels:

- `kv_api` — daemon makes the HTTP call, injects the credential, returns the
  response. The key never leaves the daemon's address space.
- `kv_run` — daemon injects env vars into a subprocess, runs the command, redacts
  output before the agent sees it.

The encryption, passphrase wrapping, TOTP — that's supporting infrastructure.
The core product is the daemon + MCP bridge that lets AI agents use secrets
without possessing them.

**The security promise:**
The ONLY way a user loses their keys is by giving them away themselves.
Not through our product. Not through a clever agent. Not through subprocess
tricks, file reads, encoding games, or socket manipulation.

**The monetization path:**
Free local CLI. Paid team sync ($15/team/month) — encrypted blobs pushed to
a zero-knowledge server. Undercuts Doppler with per-team pricing.

---

## The Hard Question: Have We Actually Achieved the Promise?

This is the question that matters more than any bug list.

**The claim:** No agent can extract keys through kv-secrets. Only the user can leak them.

**Where we stand today — honest assessment:**

### What DOES work

1. **Secrets never touch disk in plaintext.** ChaCha20-Poly1305 encryption,
   per-environment keys derived via BLAKE2b. .enc blobs are safe to commit.
2. **The daemon architecture is sound.** Secrets live only in the daemon's memory.
   PR_SET_DUMPABLE=0 prevents ptrace/proc reads (same as ssh-agent, gpg-agent).
3. **kv_api is the strongest channel.** The daemon makes the HTTP call itself.
   The API key genuinely never enters any other process. This is the real
   innovation — not "redact the output" but "the key never leaves."
4. **Output redaction catches the obvious.** Secret values replaced with
   [REDACTED] in stdout/stderr before the agent sees it.
5. **File scanning catches encoded exfil.** Base64, hex, reversed variants
   detected and deleted post-execution.
6. **Claude Code hooks block direct access.** PreToolUse hooks prevent reading
   .secrets/key, .enc files, and kv reveal commands.

### What DOESN'T work (gaps in the promise)

**GAP-A: The daemon socket has NO authentication.**
Any process running as the same user can connect to the Unix socket and issue
commands. An agent with Bash access can write a Python script that connects
directly to `/tmp/kv-agent/kv.sock` and sends:
```python
{"cmd": "run", "argv": ["python3", "exfil.py"]}
```
The hooks might block the Bash command that creates the script, but the agent
could use Write tool, or craft the script through a chain of safe-looking steps.
The socket itself has zero defense against same-user callers.

**GAP-B: Hooks only protect Claude Code.**
The PreToolUse/PostToolUse hook system is Claude Code specific. If a developer
uses kv with Cursor, Windsurf, Copilot, or a custom MCP client — no hooks run.
The MCP server has profile gating (safe/mutate/reveal) but no equivalent of
the hook protection against filesystem tricks.

**GAP-C: `cmd_run` CLI bypasses all sandbox protections.**
When no daemon is running, `kv run` falls back to raw `subprocess.run` with
secrets injected — no command validation, no redaction, no network isolation,
no file scanning. All the sandbox work is bypassed entirely.

**GAP-D: The sandbox is pattern-based, not a real sandbox.**
Command validation uses blocklists and string matching. A determined agent can:
- Write a script that looks innocent but exfiltrates via creative encoding
- Use language features not in the blocklist (Go, Rust, Zig — only Python/Node/Ruby/Perl checked)
- Use piped commands or command substitution to evade `_extract_all_paths`
- If `unshare` fails (common on non-root Linux), network isolation silently degrades

**GAP-E: `check_leak` is a secret oracle.**
The daemon command `check_leak` takes arbitrary text and returns which secret
names are found in it. An agent could probe character-by-character, though this
is slow for long secrets. Low risk in practice, but architecturally wrong —
it turns the daemon into a side-channel.

**GAP-F: kv_run with `network=True` bypasses network isolation.**
The MCP tool accepts a `network` boolean. If an agent asks for it, the script
gets full network access with secrets in env vars. The agent itself decides
whether to request network access.

### The fundamental tension (informed by engram LRN-012, LRN-015, MST-011)

Same-user security is advisory, not containment. We proved this through live
testing (2026-03-20): the agent extracted ANTHROPIC_API_KEY via `kv ls --reveal`
in one Bash command. We also proved subprocess file exfil bypasses socket redaction.

Three tiers of defense exist at same-user level:
1. **Containment** (OS sandbox, separate user) — enforced by kernel, not us
2. **Interface barrier** (passphrase prompt) — works because agents can't enter
   interactive input. This is kv's strongest non-crypto defense. Same model as ssh-agent.
3. **Advisory** (hooks, redaction, sandbox) — catches the 99% accidental case.
   Not proof against deliberate exploitation. Still valuable.

**Our achievable promise (calibrated):**
kv makes it so that an agent CAN'T possess your keys through normal operation,
and SHOULDN'T be able to extract them without multi-step exploitation that any
reasonable user review would catch. `kv_api` achieves true isolation — the key
never enters any other process. `kv_run` provides practical defense-in-depth.
The passphrase barrier prevents unauthorized unlock.

**What we DON'T claim:** Resistance to a deliberately adversarial agent with
unlimited Bash access and a user who approves everything blindly. That requires
OS-level containment, which is outside our scope as a Python package.

---

## Industry Research (2026-03-26)

Full analysis: [`docs/competitive-landscape.md`](docs/competitive-landscape.md)

**Key findings:**
- Three models exist: SDK Retrieval (1Password — agent gets plaintext), Proxy/Daemon
  (kv, AgentSecrets, OneCLI — agent never possesses key), Signing-only (ssh-agent).
- **kv_api IS the ssh-agent model applied to API keys.** Industry validated independently.
- Two direct competitors: AgentSecrets (Go), OneCLI (Rust). Both proxy-only — no kv_run equivalent.
- 1Password Unified Access launched March 2026 with Anthropic partnership, but their
  SDK model is weaker — agent gets plaintext.
- **kv's moat:** MCP-native, passphrase+TOTP interface barrier, kv_run for non-HTTP protocols.
- **kv's gaps:** No SSRF protection, no audit logging, hardcoded providers (5), no cloud sync shipped.

**Why kv_run exists (competitors don't have it):**
- kv_api handles HTTP API calls — key never leaves daemon. Primary path.
- kv_run is for processes that genuinely need the credential: databases (Postgres, Redis),
  cloud SDKs (AWS boto3), non-HTTP protocols (SMTP, AMQP), legacy apps.
- These can't go through an HTTP proxy — the client library needs the credential in memory.
- Product hierarchy: kv_api = primary (true isolation), kv_run = fallback (sandboxed compromise).

---

## Threat Model

Before touching code, define precisely what we defend against.

### Attacker profile

**Primary:** An AI coding agent with Bash/file access (Claude Code, Cursor, etc.)
that is either confused (prompt injection, hallucination) or subtly adversarial
(compromised model, malicious tool output). The user is present and reviewing
tool calls but may approve things that look reasonable.

**NOT in scope:** Remote attacker, root attacker, different OS user, kernel exploit,
hardware attack. Also not: a user who pastes keys into chat or disables all
protections deliberately.

### What the attacker can do

- Execute Bash commands (user approves each one)
- Read/write files via tools (user approves)
- Call MCP tools (approved by profile)
- Chain multiple operations (write script, then run it)
- Reason about how to bypass defenses

### Security invariants (must ALWAYS hold)

| # | Invariant | How it's enforced |
|---|-----------|-------------------|
| I-1 | Secret values never appear in MCP tool responses | kv_api: key stays in daemon. kv_run: output redacted. kv_list: names only. |
| I-2 | Vault cannot be unlocked without user's passphrase | Interface barrier — interactive prompt or daemon already running. |
| I-3 | Key file cannot be read by agent in normal operation | Hooks (Claude Code) + passphrase wrapping (all editors). |
| I-4 | kv_api calls cannot target attacker-controlled servers | Fixed provider registry. No custom base URLs. |
| I-5 | kv_run commands are validated before secrets are injected | Sandbox checks: blocklist/allowlist, shell blocking, network tool blocking. |
| I-6 | Files created during kv_run are tracked and access-blocked | Daemon tracks new files, hooks block reads. |

### Where invariants are VIOLATED today

| Invariant | Violation | Severity |
|-----------|-----------|----------|
| I-1 | `cmd_run` CLI direct mode: no redaction at all | **Critical** — BUG-2 |
| I-5 | `cmd_run` CLI direct mode: no command validation | **Critical** — BUG-2 |
| I-3 | `cmd_export_key` / `_team_key`: bypass wrapped key protection | **Medium** — BUG-5+6 |
| I-5 | MCP `handle_kv_run` direct mode: `network` flag ignored | **Medium** — BUG-3 |
| I-6 | Hooks only run in Claude Code, not Cursor/other editors | **Architectural** — GAP-B |

---

## The Plan

### Process rules (binding — from Satish, 2026-03-26)

1. **Plan first.** No code until the plan is written and aligned on.
2. **Chunk by chunk.** One item. Fix. Test. Update LEDGER.md. Commit. Next.
3. **Find the seed.** Don't patch symptoms. Trace to root cause first.
4. **Websearch when needed.** Don't guess at security properties or competitor approaches.
5. **Proactively recommend next priority and why.** Don't wait to be asked.
6. **Once written here, follow it.** No exceptions. LEDGER.md is binding.

### Feature work (post-Phase 3)

| # | Item | Status |
|---|------|--------|
| 1 | Dynamic provider registry — config-based, not hardcoded | **DONE** |
| 2 | Audit logging — tamper-evident JSONL with CLI query tool | **DONE** |
| 3 | SSRF protection — block private IPs, localhost, non-HTTPS in kv_api | **DONE** |
| 4 | Daemon hot-reload secrets — no restart after kv set | **DONE** |
| 5 | Per-project agent sockets — multi-vault support | **DONE** |
| 6 | Agent status shows which project/vault it serves | **DONE** |
| 7 | THINK.md review — 6 hidden issues found and fixed | **DONE** |
| 8 | Agent orphan fix — stop old agent before starting new one | **DONE** |
| 9 | BrokenPipeError handled — client disconnect doesn't log errors | **DONE** |
| 10 | Deep audit — send/recv data loss, agent startup race condition | **DONE** |

### CRITICAL — Agent socket disappearing (unresolved)

The agent socket file (`~/.kv/sockets/kv-<hash>.sock`) is being deleted while
the agent process is alive. The process keeps running but is unreachable.
No signal is logged. No "agent stopping" in logs. The agent doesn't know
its socket is gone.

**What we've ruled out:**
- NOT our cleanup code — PID ownership check prevents old agent from deleting new socket
- NOT `/tmp` cleanup — sockets moved to `~/.kv/sockets/`
- NOT BrokenPipeError — handled silently
- NOT orphaned processes — PID file cleanup works
- NOT signals — no SIGTERM/SIGINT/SIGHUP logged

**Correlation found but unproven:**
- `guardian.timer` (crypto watchdog) runs every 5 min
- Socket disappeared at 14:14:16, guardian last ran at 14:14:16
- But guardian.py has no socket/unlink/cleanup code
- Second disappearance at 14:24 — timing unclear

**What we need:**
1. `inotifywait` — install and monitor `~/.kv/sockets/` to catch the exact
   process that deletes the socket file
2. Or: `strace -p <pid> -e unlink` on the agent to catch if it unlinks itself
3. Or: agent self-monitors its own socket file (watchdog thread)

**Priority: P0 — RESOLVED. Root cause: SIGPIPE.**

**Fixes applied but insufficient:**
- Moved socket from /tmp to ~/.kv/sockets/
- PID file for reliable agent cleanup
- PID ownership check on shutdown (don't delete if not owner)
- Signal logging (SIGINT, SIGTERM, SIGHUP)
- atexit handler for unexpected exits
- Polling parent wait (10s instead of 1s)

**Resolution: replaced Unix sockets with TCP localhost.**

Multiple causes found and fixed along the way:
- SIGPIPE killing agent silently → fixed (SIG_IGN)
- Old agent cleanup deleting new agent's socket → fixed (PID ownership check)
- /tmp cleanup deleting socket files → fixed (moved to ~/.kv/)

But the socket kept disappearing from unknown causes. Spent hours on PID files,
ownership checks, self-healing, strace, inotifywait — never caught the deleter.

Final fix: remove the file. Agent now listens on TCP 127.0.0.1:<port> where
port is derived from project root hash (range 19200-19999). No file = nothing
to delete. Code got simpler (-151 lines, +44 lines). Problem structurally eliminated.

| 11 | Named global vaults (personal, work) — `kv vault create/switch` | Future |
| 12 | Rust daemon rewrite — memory-level secret zeroing, single binary | Future (after features) |

### Next priorities (feature gap — closing competitive distance)

| # | Feature | Why | Effort |
|---|---------|-----|--------|
| 1 | Cloud sync server | AgentSecrets has it shipped. kv client code ready, needs server. | Large |
| 2 | Domain allowlist for kv_api | AgentSecrets has it. Restrict which URLs a provider can call. | Small |
| 3 | `kv provider` auth style: JSON body + form fields | AgentSecrets supports 6 styles, kv has 5. | Small |
| 4 | Website: docs page | Single-page docs (getting started + CLI reference) | Medium |

### Strategic insight: kv_api is the crown jewel

The product has two channels. They are NOT equal:

- **kv_api**: TRUE key isolation. The daemon makes the HTTP call. The key never
  enters any other process, period. The provider registry is fixed — agent can't
  redirect to an attacker-controlled server. Even with full socket access,
  the key is safe. This is the real innovation.

- **kv_run**: PRACTICAL defense. The key IS in the subprocess env. Sandbox layers
  (validation, isolation, redaction, scanning) raise the bar significantly, but
  a sufficiently clever script can theoretically exfiltrate (MST-011 in engram).

**Implication:** Long-term, the product should push users toward kv_api and
treat kv_run as the reluctant fallback for legacy apps that expect env vars.
Short-term, tighten kv_run's defenses.

### Phase 0 — Threat model + research (THIS SESSION, before any code)

- [x] Define attacker profile
- [x] Define security invariants
- [x] Map invariant violations to bugs/gaps
- [x] **Research: industry landscape for AI agent credential security.**
      ssh-agent, gpg-agent, 1Password, AgentSecrets, OneCLI. See "Industry Research" section above.
      **Key finding:** kv_api implements the ssh-agent security model for API keys.
      The proxy/daemon model is what the industry is converging on. kv was here first.

### Phase 1 — Fix invariant violations (code changes)

Order by: most critical invariant violation first.

| # | Item | Seed | Fix | Status |
|---|------|------|-----|--------|
| 1 | BUG-2 | `cmd_run` direct path has NO sandbox | Refuse to run without daemon. Removed entire unsafe path. | **DONE** |
| 2 | BUG-1 | `cmd_agent` menu falls through | Indented unlock menu into `else:` block of `if is_agent_running()`. | **DONE** |
| 3 | BUG-5+6 | `_team_key` and `cmd_export_key` use `load_key()` on wrapped keys | Both now use `_get_store()` for proper passphrase flow. Removed unused `load_key` import. | **DONE** |
| 4 | BUG-3 | MCP direct mode ignores `network` flag | Added `network=args.get("network", False)` to `wrap_argv()` call. | **DONE** |
| 5 | BUG-7 | Test functions named `test()` collide with pytest | Renamed to `check()` in both test_e2e.py and test_mcp.py. | **DONE** |
| 6 | BUG-4 | Timeout mismatch (30s vs 60s) | Changed `RUN_TIMEOUT = 30` to `60` in tools.py. | **DONE** |

### Phase 2 — Strengthen advisory layers

After Phase 1, no invariant is violated. Phase 2 raises the bar on the
advisory defenses.

| # | Item | Seed | Approach | Why this order |
|---|------|------|----------|----------------|
| 1 | GAP-C | CLI `kv run` without daemon has no sandbox | Already fixed by BUG-2 (refuse without daemon). Mark complete. | — |
| 2 | GAP-B | Hooks only protect Claude Code | **RESOLVED by analysis.** With passphrase-protected vault, kv IS already client-agnostic. Passphrase = interface barrier. Daemon = controlled gateway. Hooks = defense-in-depth. Strengthened warnings for unprotected vaults in `kv init` and `kv doctor`. | The seed wasn't "hooks need to work everywhere" — it was "passphrase is the foundational layer." |
| 3 | GAP-F | `network=True` lets agent bypass isolation | **DONE.** Removed `network` param from MCP schema. Network access now controlled by `network_commands` list in config.json — user decides, not agent. Added `is_network_allowed()` and `add_network_command()` to config.py. Daemon and MCP direct mode both use config. | Agent no longer controls this. |
| 4 | GAP-E | `check_leak` is a secret oracle | **DONE.** Returns `{"leaked": bool, "count": int}` — no names. Daemon logs leaked names to `~/.kv/agent.log` (user-accessible, not agent-accessible). Hook updated to show count + rotation instructions. | No more oracle. |
| 5 | GAP-A | Socket has no auth | **Deprioritized.** After reviewing engram LRN-012: socket auth adds a token that the agent could read if it bypasses hooks. The REAL defense is the constrained channels (kv_api is safe by design, kv_run has sandbox). Socket auth is defense-in-depth, not the security boundary. Implement only after the constrained channels are tight. | Same-user auth is advisory by definition. |
| 6 | GAP-D | Sandbox is pattern-based | Longer term. Consider: allowlist-only by default for new projects, with a migration path for existing blocklist users. The allowlist model is fundamentally stronger — deny everything, approve explicitly. | Biggest structural improvement but also biggest change. |

### Phase 3 — Code quality and docs

| # | Item | Approach | Status |
|---|------|----------|--------|
| 1 | DUP-1 | Deleted `_redact_secrets` from tools.py (was unused). Replaced inline redaction loops in `handle_kv_api` with `sandbox.redact()`. | **DONE** |
| 2 | DUP-2 | Extracted `make_api_call()` into providers.py. Both agent.py and tools.py now use it. ~50 lines removed from each caller. | **DONE** |
| 3 | DOC-1 | Rewrite DESIGN.md — full rewrite reflecting current architecture: daemon, MCP, hooks, providers, security model, 30 commands, competitive positioning. kv_server marked as "not yet deployed." | **DONE** |
| 4 | GAP-1 | kv_server: first pass built on another machine (data recovery uncertain, was first pass). Client-side code (auth.py, remote.py, sync.py, cli_remote.py) is solid — API contract defined. kvsecure.com landing page live. Decision: rebuild server fresh when free tier has traction. Not a priority now. | **Parked** |

---

### What NOT to do

- Don't add features until Phase 1 is complete
- Don't touch the crypto layer — it's clean
- Don't pursue "absolute containment" — we proved it's impossible at same-user level (LRN-012)
- Don't add socket auth before the constrained channels are tight (premature complexity)
- Don't remove remote commands — the server is the monetization path
- Don't skip LEDGER.md updates — EVER

### How we work

- **Plan first.** No code without a plan written here.
- **Chunk by chunk.** One item. Fix. Test. Update LEDGER.md. Repeat.
- **Find the seed.** Every bug is a symptom. Find what caused it before patching.
- **Websearch when needed.** Security assumptions must be verified, not guessed.
- **Proactive recommendations.** After each chunk, recommend next priority and why.
- **Once written, binding.** LEDGER.md is law. Don't ignore what's written here.
- **Think like an attacker.** Before shipping a fix, try to bypass it.

### Key reference files

| File | What it contains |
|------|-----------------|
| `LEDGER.md` | This file. The single source of truth. |
| `REVIEW.md` | Previous structured review (2026-03-21). |
| `DESIGN.md` | Original design doc. **Stale** — needs update. |
| `AGENT.md` | Guide for AI agents using kv-secrets. |
| `kv/agent.py` | Daemon server — the security centerpiece. |
| `kv/sandbox.py` | Command validation, redaction, isolation. |
| `kv/hook.py` | Claude Code hook protection. |
| `kv/cli.py` | 1,881 lines. 30 commands. Most bugs live here. |
| `kv_mcp/tools.py` | MCP tool handlers. Code duplication lives here. |
| `kv_mcp/server.py` | MCP lifecycle, profile enforcement. |

### Architecture

```
User's terminal                    AI Agent (Claude Code / Cursor / etc.)
      │                                       │
      │ kv unlock                              │ MCP: kv_api, kv_run
      │ (passphrase + TOTP)                    │
      ▼                                        ▼
┌──────────────────────────────────────────────────────┐
│                   kv Agent Daemon                     │
│                                                       │
│  Secrets in memory    Unix socket (chmod 600)         │
│  PR_SET_DUMPABLE=0                                    │
│                                                       │
│  Two channels (NOT equal):                            │
│    kv_api → TRUE isolation. Key never leaves daemon.  │
│             Dynamic provider registry. Crown jewel.   │
│    kv_run → PRACTICAL defense. Key in subprocess env. │
│             Sandbox: validate → isolate → redact →    │
│             scan. Advisory, not containment.          │
│                                                       │
│  Metadata only (no secret values):                    │
│    list → names only                                  │
│    status/envs → counts and state                     │
│                                                       │
│  Defense layers (kv_run):                             │
│    1. Command validation (allowlist/blocklist)         │
│    2. Network isolation (unshare --user --net)         │
│    3. Output redaction (values → [REDACTED])           │
│    4. File scanning (base64, hex, reversed)            │
│    5. Process memory protection (PR_SET_DUMPABLE=0)    │
│    6. Claude Code hooks (defense-in-depth, not gate)   │
│    7. Tamper-evident audit log (JSONL + hash chain)    │
└──────────────────────────────────────────────────────┘
         │
         ▼
┌─────────────────────────┐       ┌─────────────────────┐
│     Encrypted Vault      │       │  Interface Barrier   │
│  .secrets/key (wrapped)  │◄──────│  Passphrase + TOTP   │
│  .secrets/dev.enc        │       │  Agent can't enter   │
│  ChaCha20-Poly1305       │       │  interactive input   │
│  BLAKE2b key derivation  │       └─────────────────────┘
└─────────────────────────┘
```

### Current test status

```
pytest:             195 passed, 1 skipped, 0 errors ✓
```

### Engram cross-references

| Entry | Key insight | How it shapes our plan |
|-------|-------------|----------------------|
| LRN-012 | Same-user security = advisory, not containment | Don't pursue absolute containment. Focus on interface barrier + strong advisory layers. |
| LRN-015 | Agent IS the credential consumer, no platform solves this | kv_api is the right answer — daemon makes the call, not the agent. |
| MST-011 | Subprocess + filesystem = can always exfiltrate | kv_run's file scanning is defense-in-depth. kv_api avoids the problem entirely. |

---

## Action Item Prefixes

| Prefix | Category | Example |
|--------|----------|---------|
| **BUG-** | Bug fix | BUG-1: cmd_agent double menu |
| **GAP-** | Security gap | GAP-A: socket has no auth |
| **DUP-** | Code duplication | DUP-1: redaction logic |
| **DOC-** | Documentation | DOC-1: DESIGN.md update |
| **SEC-** | Security hardening | (reserved) |
| **FEA-** | New feature | (reserved) |
| **REF-** | Refactor | (reserved) |

---

## Session Log

### 2026-03-26 — Full Codebase Audit + Security Model + Plan

**What:** Read all 20 source files, all docs, all tests. Ran test suite.
Checked engram (LRN-012, LRN-015, MST-011) for cross-project security learnings.
Analyzed the security model against the product promise. Defined threat model
and security invariants. Wrote the plan. No code changes.

**Key realizations:**
1. kv_api is the crown jewel — true key isolation. kv_run is the compromise channel.
   Product strategy should push users toward kv_api.
2. Socket auth (GAP-A) is less critical than initially thought — the constrained
   channels ARE the defense, not the socket boundary.
3. BUG-2 (`cmd_run` direct mode) is the most dangerous issue — it bypasses ALL
   sandbox protections. Fix is simple: refuse to run without daemon.
4. The security promise should be calibrated: not "impossible to break" but
   "hard enough that exploitation requires obvious multi-step attacks."

**Decisions:**
- Phase 0 (threat model): done.
- Phase 1 (fix invariant violations): 6 items, starting with BUG-2.
- Phase 2 (strengthen advisory layers): 6 items, starting with GAP-B.
- Phase 3 (code quality): 4 items.
- GAP-A deprioritized — socket auth is advisory-on-advisory, not high value.

**Research findings (websearch):**
- The industry has three models: SDK retrieval (1Password, Vault — agent gets plaintext),
  proxy/daemon (kv, AgentSecrets, OneCLI — agent never possesses key), signing-only (ssh-agent).
- kv_api implements the ssh-agent model for API keys. This is the correct architecture.
- Two direct competitors: AgentSecrets (Go, HTTP proxy) and OneCLI (Rust, HTTP gateway).
  Both launched on HN in March 2026. kv was first but needs to catch up on: SSRF protection,
  audit logging, dynamic provider registry.
- 1Password launched "Unified Access" with Anthropic partnership. Different market (enterprise,
  cloud). Their SDK model is actually WEAKER — agent gets plaintext secrets.
- kv's moat: MCP-native, passphrase+TOTP interface barrier, kv_run for legacy apps, zero deps.

**Phase 1 completed (all 6 items):**
- BUG-2: `cmd_run` now refuses without daemon — removed entire unsafe direct path
- BUG-1: `cmd_agent` menu fixed — unlock menu inside `else:` block
- BUG-5+6: `cmd_export_key` and `_team_key` now use `_get_store()` for wrapped keys
- BUG-3: MCP `handle_kv_run` direct mode now passes `network` flag
- BUG-7: Test helpers renamed `test()` → `check()` — 0 pytest errors
- BUG-4: Timeout aligned to 60s in tools.py

**Phase 2 completed (4 of 6 — 2 deprioritized):**
- GAP-B: Resolved by analysis — passphrase IS the client-agnostic security layer. Strengthened warnings.
- GAP-F: `network` flag removed from MCP schema. Network access now config-based (`network_commands`).
- GAP-E: `check_leak` returns boolean+count only. Names logged to `~/.kv/agent.log`, not returned.
- GAP-A: Deprioritized — advisory-on-advisory, constrained channels are the real defense.
- GAP-D: Deprioritized — longer-term, allowlist-only default for new projects.

**Phase 3 completed (all 4 items):**
- DUP-1: Deleted `_redact_secrets`, use `sandbox.redact()` everywhere.
- DUP-2: Extracted `make_api_call()` into providers.py, used by daemon + MCP.
- DOC-1: DESIGN.md fully rewritten — architecture, security model, 30 commands, competitive positioning.
- GAP-1: kv_server clarified — first pass on another machine, rebuild fresh when ready. Parked.

**Feature work:**
- Dynamic provider registry: `load_custom_providers()` from config.json. `kv provider add/list/rm`.
  5 auth types (bearer, x-api-key, query, basic, header). Loaded at daemon/MCP startup.
- Tamper-evident audit logging: `kv/audit.py`, JSONL at `~/.kv/audit.jsonl`. Hash chain.
  7 event types. `kv audit` CLI with --security, --provider, --summary, --verify filters.
  Daemon instrumented at every operation point. No competitor has tamper-evident chains.

**Commits pushed to GitHub:**
- `7767f83` Security hardening, code cleanup, and architecture docs
- `78cc43f` Feature: dynamic provider registry for kv_api
- `3f3f5b2` Feature: tamper-evident audit logging with CLI query tool

**Tests: 143 passed, 1 skipped, 0 errors throughout.**
