Skip to content

feat: agents store and manage keys without seeing them (ADR-0012) - #110

Merged
nniiovoo merged 2 commits into
mainfrom
feat/agent-key-storage
Nov 9, 2026
Merged

nniiovoo merged 2 commits into
mainfrom
feat/agent-key-storage

Conversation

@nniiovoo

@nniiovoo nniiovoo commented Nov 9, 2026

Copy link
Copy Markdown
Owner

Builds ADR-0012: an agent such as Claude Code, ChatGPT or Codex, running in the separate agent Mac account, can ask the app to store keys and manage the keys it added, without ever seeing a value.

What the agent can do

  • Store: the app comes to the front with the editor already filled in. You type the value and choose Save. The agent gets back only "saved" plus the key's id, or "cancelled".
  • List: only the keys it added, with names and visible details, never a value.
  • Edit:
    • Renames, purposes and expiry dates apply at once.
    • A Login's website or username, or an API Key's service, needs your Allow.
    • Seed Phrase and Private Key edits need your Allow plus your Master Password.
    • Replacing a value always means you type it.
  • Delete: always needs your Master Password, and the key moves to Trash.

Limits

  • Only the agent account's uid can connect, checked with the kernel's peer uid. Programs in your own account are refused.
  • Access needs a grant you start, lasting at most an hour. Lock, sleep or quit ends it.
  • Replies are fixed shapes and error codes, never free text. No request or reply carries a value, whether a value is set, a Note not added by an agent, a Note in Trash, the Master Password or the Recovery Key.
  • Nothing marks an existing Note as agent-added, nothing goes over the network, and nothing signs (ADR-0011 stays deferred).

Where it lives

  • crates/notes/src/agent.rs: the "added by an agent" mark and agent_edit_rule.
  • src-tauri/src/core/agent.rs: the grant and the five requests.
  • src-tauri/src/agent_route.rs: the Unix socket in /Users/Shared/encrypted-note-keys, the app's only listener.
  • crates/agent-keys-mcp: enote-agent-keys, the agent's MCP tool. It uses no Vault crate and no network crate.
  • src/AgentRequest.tsx and Settings: the request windows and the grant.
  • Entitlements.plist: read-write access to that one shared folder only, with scripts/pinned-hashes.txt updated.
  • scripts/check-network-source.mjs: keeps sockets and peer-uid checks inside the route and the tool, keeps Vault crates out of the tool, and keeps companion/ out of all of it.
  • scripts/agent-keys-setup.sh and docs/agent-keys.md, plus updates to the guide, security model, FORMAT, ADR-0004/0007/0012 and the Emergency Reader, which shows " (added by an agent)" after the title.

Dependencies

No new packages in Cargo.lock. agent-keys-mcp uses serde_json and tempfile from the workspace, and libc = "=0.2.189" (exact pin, already in the lockfile) for getpeereid. That call sits in one reviewed module, peer.rs, the only place in the crate allowed to use unsafe code.

Choices made where the ADR didn't say

  • The app says "An agent asked…" rather than naming the program, because it can't tell which program in the agent account sent the request. It's one constant in src/strings.ts.
  • enote-agent-keys isn't bundled inside the app yet. The docs say to install it in /usr/local/bin.

Checks

./scripts/check.sh steps, run on 2026-11-09 with fictional data only:

  • 816 Rust tests passed, 0 failed.
  • cargo fmt, clippy with -D warnings and cargo deny are clean.
  • The secret scan passed, including the new files.
  • The network-source check, npm run typecheck and the production WebView build passed.
  • All 98 companion tests passed.

Not done yet (owner only)

  • Create the Standard agent account and run scripts/agent-keys-setup.sh for real.
  • On a signed build, confirm the sandboxed app can open its socket in /Users/Shared/encrypted-note-keys.
  • Click through the new windows by hand.
  • Do the drill with fake keys.
  • Security review before storing any real key this way.

🤖 Generated with Claude Code

An agent running in the separate agent Mac account can now ask the app
to store a key, list the keys it added, edit them and move them to
Trash. The owner types every value into the app's own editor; the agent
only ever gets back fixed replies and error codes.

- crates/notes/src/agent.rs: the "added by an agent" mark and
  agent_edit_rule (which edits apply at once, which need the owner's
  Allow, which also need the Master Password).
- src-tauri/src/core/agent.rs: the owner's grant (at most an hour,
  ended by lock, sleep or quit) and the five requests.
- src-tauri/src/agent_route.rs: the Unix socket in
  /Users/Shared/encrypted-note-keys, accepting only the agent
  account's uid.
- crates/agent-keys-mcp: enote-agent-keys, the agent's MCP tool. No
  Vault crate, no network crate.
- src/AgentRequest.tsx and Settings: the request windows and the grant.
- Entitlements: read-write access to the one shared folder, with the
  pinned hash updated.
- scripts/agent-keys-setup.sh, docs/agent-keys.md and the docs, ADRs
  and Emergency Reader note for agent-added keys.
- check-network-source.mjs: sockets and peer-uid checks stay in the
  route and the tool, the tool imports no Vault crate, and none of it
  uses companion/.

Never signs (ADR-0011 stays deferred). Built and tested with fictional
data only; not yet checked by hand or security reviewed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nniiovoo
nniiovoo enabled auto-merge November 9, 2026 16:36
On Windows the route has no listener, and its stand-in AgentRoute was a
unit struct, so AgentRoute::default() failed clippy's
default_constructed_unit_structs lint. Give it a private field, like the
Mac route. The route tests imported the agent_route module at the top
but used it only in the Mac-only listener tests, an unused import on
Windows; import it there instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nniiovoo
nniiovoo merged commit 2366c6f into main Nov 9, 2026
2 checks passed
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.

1 participant