Skip to content

feat(mcp): cap + TTL the NonceStore#27

Merged
radiosilence merged 1 commit intomainfrom
feat/nonce-store-bounds
Apr 18, 2026
Merged

feat(mcp): cap + TTL the NonceStore#27
radiosilence merged 1 commit intomainfrom
feat/nonce-store-bounds

Conversation

@radiosilence
Copy link
Copy Markdown
Owner

Summary

Closes #25. The `NonceStore` introduced in #24 was unbounded — PREVIEW without CONFIRM would grow the map forever. Low impact (trusted local MCP caller) but should still be bounded.

Changes

  • `Nonce` is now a struct carrying `fingerprint` + `issued_at: Instant`. The map type becomes `HashMap<String, Nonce>`.
  • 15-minute TTL per nonce. Swept from the map on every `issue_nonce` call; rejected with "expired" error on `consume_nonce`.
  • Hard cap of 256 outstanding nonces. When full, the oldest entry (by `issued_at`) is evicted before insertion.
  • 7 new unit tests: happy path, missing nonce, reused nonce, tampered params, expired nonce, cap eviction, TTL sweep.

Why these numbers

  • TTL = 15 min — long enough for a human to write a draft between PREVIEW and CONFIRM, short enough to bound steady-state memory to minutes rather than process-lifetime.
  • Cap = 256 — a single MCP session drafting 256 concurrent emails is already pathological. Well under any memory concern.

Test plan

  • `cargo test` — 89 passing (82 previous + 7 new)
  • `cargo clippy --all-targets -- -D warnings` — clean
  • `cargo fmt -- --check` — clean
  • CI green

🤖 Generated with Claude Code

The NonceStore added in #24 was an unbounded HashMap. A misbehaving MCP
client that PREVIEW'd repeatedly without ever CONFIRM'ing could grow it
without limit for the life of the process. Not exploitable (local,
trusted caller) but not tidy either.

Now bounded:
- 15-minute TTL per nonce, swept on every issue and rejected on consume
- Hard cap of 256 outstanding nonces, oldest evicted when full
- Seven tests: roundtrip, missing, reuse, tampered params, expired,
  cap eviction, expired sweep

Entries now carry an `issued_at: Instant` alongside the fingerprint;
`Nonce` became a struct so the map type reflects what it holds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@radiosilence radiosilence merged commit a5e221a into main Apr 18, 2026
3 checks passed
@radiosilence radiosilence deleted the feat/nonce-store-bounds branch April 18, 2026 10:30
@radiosilence radiosilence mentioned this pull request Apr 18, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mcp: cap or TTL the NonceStore

1 participant