# kv-secrets — Architecture

## The Mental Model

kv-secrets is a vault. Think of a physical safe.

```
LOCKED (default state)
  │
  │  kv unlock (passphrase + optional TOTP)
  ▼
UNLOCKED (secrets in memory, daemon running)
  │
  │  You work. Agent uses kv_api. No prompts.
  │
  │  kv lock (or close terminal, or reboot)
  ▼
LOCKED (secrets gone from memory)
```

Three states. Two transitions. That's it.

---

## Two Ways Agents Use Secrets

### kv_api — Agent makes API calls (RECOMMENDED)

```
Agent calls: kv_api(provider="openai", path="/v1/models", method="GET")

  1. MCP server receives the call
  2. Delegates to daemon via Unix socket
  3. Daemon reads OPENAI_API_KEY from its own memory
  4. Daemon makes HTTPS request with auth header
  5. Daemon returns response to agent
```

**Security guarantee: ABSOLUTE.** The API key never leaves the daemon process. It never enters a subprocess, never touches env vars, never appears in any output. The agent physically cannot access it. There is no attack path.

**Use for:** All HTTP API calls (OpenAI, Anthropic, Google, GitHub, etc.)

### kv_run — Agent runs an app (SECONDARY)

```
Agent calls: kv_run(argv=["python3", "app.py"])

  1. Daemon validates command (blocks shells, inline code)
  2. Daemon creates subprocess with secrets as env vars
  3. Subprocess runs, stdout/stderr captured
  4. Daemon redacts exact secret values from output
  5. Agent sees redacted output
```

**Security guarantee: BEST-EFFORT, NOT ABSOLUTE.** The subprocess has secrets in its environment. The redaction catches exact-match values only. It does NOT catch:
- Partial values (`sk-proj-abc` when full key is `sk-proj-abc123`)
- Case changes (`SK-PROJ-ABC123`)
- Encoded forms (base64, hex, reversed)
- Values written to files (detected and deleted, but race window exists)

**Use for:** Running apps that need secrets as env vars (database connections, test suites, local servers). NOT for API calls — use kv_api instead.

### When to use which

| Need | Use | Why |
|------|-----|-----|
| Call OpenAI API | `kv_api` | Key never leaves daemon |
| Call Anthropic API | `kv_api` | Key never leaves daemon |
| Call any HTTP API | `kv_api` | Key never leaves daemon |
| Run `python app.py` that reads DATABASE_URL | `kv_run` | App needs env var |
| Run `npm start` | `kv_run` | App needs env vars |
| Run `pytest tests/` with real credentials | `kv_run` | Tests need env vars |

**If your workflow is purely API calls (most AI agent use cases), you only need `kv_api`. `kv_run` is unnecessary.**

---

## Commands — What to Use

### Commands you use daily

| Command | What it does |
|---------|-------------|
| `kv unlock` | Open vault (passphrase + TOTP). Runs daemon in background. |
| `kv lock` | Close vault. Daemon stops. Secrets gone from memory. |
| `kv status` | Shows: locked/unlocked, secrets count, security features. |

### Commands you use occasionally

| Command | What it does |
|---------|-------------|
| `kv set KEY` | Store a secret (hidden input) |
| `kv ls` | List secret names (values masked) |
| `kv rm KEY` | Delete a secret |
| `kv env create NAME` | Create new environment (e.g., work, staging) |

### Commands you use once

| Command | What it does |
|---------|-------------|
| `kv init` | Create vault (first time only) |
| `kv setup-2fa` | Add authenticator app (after init) |
| `kv setup claude-code` | Configure MCP server for Claude Code (after init) |

### Commands AI agents use (via MCP)

| Tool | What it does | Agent sees keys? |
|------|-------------|-----------------|
| `kv_api` | Make HTTP API call with injected credentials | **No — impossible** |
| `kv_run` | Run app with secrets as env vars | **Unlikely — best-effort redaction** |
| `kv_list` | List secret names | No (names only) |
| `kv_status` | Check vault status | No |
| `kv_envs` | List environments | No |

### Commands you can ignore

| Command | Same as | Why it exists |
|---------|---------|--------------|
| `kv agent` | `kv unlock` | Old name |
| `kv agent foreground` | `kv unlock --foreground` | Explicit mode |
| `kv agent background` | `kv unlock` | Explicit mode |
| `kv agent stop` | `kv lock` | Old name |
| `kv run CMD` | N/A | For manual app runs — not needed if using kv_api |

---

## File Layout

```
~/.secrets/                     ← your vault (created by kv init)
  key                           ← master key (encrypted with passphrase)
  config.json                   ← settings, environments, security config
  dev.enc                       ← encrypted secrets for dev environment
  .gitignore                    ← prevents key from being committed

~/.kv/                          ← runtime data
  agent.log                     ← daemon log file

/tmp/kv-agent/                  ← daemon socket (exists only while unlocked)
  kv.sock                       ← Unix domain socket (chmod 600)
```

---

## Module Map

```
kv/                             ← core package
  crypto.py                     ← ChaCha20-Poly1305, PBKDF2 key wrapping, TOTP
  store.py                      ← encrypted .enc file I/O, secret CRUD
  config.py                     ← vault init, environments, settings
  agent.py                      ← daemon: socket server, kv_api handler, kv_run handler
  sandbox.py                    ← command validation, network isolation, output redaction
  providers.py                  ← API provider registry (openai, anthropic, google, github, google-cloud)
  hook.py                       ← Claude Code hooks (PreToolUse + PostToolUse)
  cli.py                        ← all CLI commands + UI formatting
  env.py                        ← .env import/export, subprocess injection

kv_mcp/                         ← MCP server for AI agents
  server.py                     ← JSON-RPC lifecycle, daemon delegation
  tools.py                      ← 8 MCP tools across 3 profiles
  protocol.py                   ← JSON-RPC framing
```

---

## Security Model — What's Protected and What's Not

### Absolute protections

| Threat | Protection | Guarantee |
|--------|-----------|-----------|
| Agent reads vault files | Passphrase encryption | Agent can't enter interactive prompts |
| Agent calls HTTP APIs | kv_api credential broker | Key never leaves daemon process |
| Agent runs kv get/export | PreToolUse hook blocks | Hook fires on all agent tool calls |
| Agent creates new vault | PreToolUse hook blocks | kv init blocked from agent Bash |

### Best-effort protections (kv_run only)

| Threat | Protection | Limitation |
|--------|-----------|-----------|
| Secret in subprocess stdout | Exact-string redaction | Doesn't catch partial/encoded values |
| Secret written to file | Post-execution file scan | Exotic encodings may bypass |
| Secret sent over network | unshare network isolation | Linux only; disabled with network=true |
| Secret in new files | File tracking + hook | Base64-encoded paths bypass hook |

### Not protected

| Threat | Why |
|--------|-----|
| Agent constructs path via base64 | Hook inspects text, can't predict runtime |
| Weak passphrase | Blocklist catches common ones, not all |
| User pastes secrets into chat | Operational error, not crypto failure |
| Same-user process memory reads | ptrace_scope=1 blocks on most Linux, not all |

### The honest summary

**kv_api is secure.** The daemon is the only process that touches secrets during API calls.

**kv_run is best-effort.** It catches accidental leaks but not deliberate exfiltration. If your workflow is API calls, don't use kv_run — use kv_api.

**The passphrase is the real barrier.** Everything else is defense-in-depth. A strong passphrase that exists only in your head is what actually stops the agent.

---

## Known Issues (from external code review)

### Fixed

| Issue | Status |
|-------|--------|
| env_names not passed to daemon in kv_run | To fix |
| MCP tools say 7 but code has 8 (kv_api added) | To fix |
| MCP server reports version 1.0.0, pyproject says 0.1.1 | To fix |
| CI only runs test_mcp.py, not full test suite | To fix |

### Parked (team features)

| Issue | Status |
|-------|--------|
| kv team join silently downgrades wrapped vault to plaintext | Parked — team features not active |
| kv export-key doesn't work with wrapped vaults | Parked — team features not active |

### By design

| Issue | Why |
|-------|-----|
| Redaction is exact-match only | Generalized detection would require content analysis, adding latency and false positives. kv_api is the solution for API calls. |
| PostToolUse hook can't undo leaks | Hook fires after the agent already received output. It alerts the user for immediate key rotation. |

---

## Daily Workflow

```
Morning:
  kv unlock → passphrase + TOTP → ✓ vault unlocked
  claude → MCP server connects to daemon → ready

Working:
  Agent: kv_api(provider="openai", ...) → API response (key never visible)
  You: kv set NEW_KEY → store new secret
  You: kv ls → check what's stored

End of day:
  kv lock → secrets cleared from memory
  Or: close terminal → same effect
```

---

## Providers (kv_api)

| Provider | Secret name | Auth method | Base URL |
|----------|------------|-------------|----------|
| `openai` | OPENAI_API_KEY | Bearer token | api.openai.com |
| `anthropic` | ANTHROPIC_API_KEY | x-api-key header | api.anthropic.com |
| `google` | GOOGLE_API_KEY | Query parameter | generativelanguage.googleapis.com |
| `github` | GITHUB_TOKEN | Bearer token | api.github.com |
| `google-cloud` | GOOGLE_CLOUD_API_KEY | Query parameter | www.googleapis.com |
