Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/adr/0012-agents-store-and-manage-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ The owner settled these details on 2026-11-05 and 2026-11-06, before the build:
- **Where values are typed:** in the app's ordinary editor, the same Add and Edit screen as when you add a key yourself, filled in from the agent's suggestion. Seed Phrases and Private Keys then need the Master Password in today's native panel. "The app's own window" above means the app's window, not the agent's: there is no new native panel for values, and the value takes the same path as any value typed in the editor ([ADR-0003](0003-tauri-with-secrets-kept-in-rust.md)).
- **Edits, field by field.** At once: the name of a Login, API Key or Text, a Login's or API Key's purpose, and an API Key's expiry date. The owner allows first: a Login's website and username, and an API Key's service, key ID, where it is managed and variable name. The owner allows, then types the Master Password: every edit of a Seed Phrase or Private Key, its name included. Never: a Hidden Field (only Replace changes a value) and a Seed Phrase's word count.
- **Delete** needs the owner's confirmation and the Master Password for every Kind.
- **The approved entitlement:** one `temporary-exception.files.absolute-path.read-write` entry for `/Users/Shared/encrypted-note-keys/`, where the app makes its socket. No network entitlement.
- **The approved entitlements:**
- one `temporary-exception.files.absolute-path.read-write` entry for `/Users/Shared/encrypted-note-keys/`, where the app makes its socket;
- approved 2026-11-09, after the [ADR-0013](0013-only-claude-code-and-codex-use-the-agent-routes.md) spike showed that entry alone can't make a socket under the sandbox: one `temporary-exception.sbpl` rule, `(allow network-bind network-inbound (local unix-socket (subpath "/Users/Shared/encrypted-note-keys")))`.

No new network entitlement, no outbound connections and no IP sockets.

Where it lives: the mark and the edit rules in `crates/notes/src/agent.rs`, the grant and the five requests in `src-tauri/src/core/agent.rs`, the socket in `src-tauri/src/agent_route.rs`, the agent's tool in `crates/agent-keys-mcp`, the owner's setup in `scripts/agent-keys-setup.sh`, and the screens in `src/AgentRequest.tsx`, `src/Note.tsx`, `src/Home.tsx` and `src/Settings.tsx`. Steps 2 to 4 of the build order are built and tested with fictional data; steps 5 and 6 are not done.

Expand Down
61 changes: 46 additions & 15 deletions docs/adr/0013-only-claude-code-and-codex-use-the-agent-routes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Only Claude Code and Codex may use the agent routes

Status: Proposed 2026-11-09 from the owner's choices below; it stands once the owner merges it. **Not built.**
Status: Accepted by the owner 2026-11-09 (merged in PR #111). **Spike done 2026-11-09** (see [Spike result](#spike-result-2026-11-09)); the owner then decided the two questions it raised. **The check itself is not built.**

Amends [ADR-0012](0012-agents-store-and-manage-keys.md) ("It accepts connections only from the agent account"). Applies equally to the Chat Wallet requests ([ADR-0011](0011-chat-wallets.md)) and to the Agent Companion's agent entry ([ADR-0014](0014-agent-key-operations-through-the-companion.md)).

Expand All @@ -10,47 +10,78 @@ Amends [ADR-0012](0012-agents-store-and-manage-keys.md) ("It accepts connections

## Decision

- **Three checks on every connection.** All three come from the kernel and macOS, never from anything the caller says about itself:
- **Three checks on every connection.** All three come from the kernel, never from anything the caller says about itself:
1. **The account** is the separate agent account (as built for [ADR-0012](0012-agents-store-and-manage-keys.md)).
2. **The connecting program** is the installed `enote-agent-keys` tool. Its code hash must match the one the owner approved in Settings, which the setup script prints. Once the tool is bundled inside the app and signed with the app's identity, the app checks that signature instead.
3. **The program that started the tool** is Claude Code or Codex, checked by its Apple code signature against the list below.
2. **The connecting program** is the installed `enote-agent-keys` tool. Its code hash must match the one the owner approved in Settings, which the setup script prints. Once the tool is bundled inside the app and signed with the app's identity, the app checks that identity instead.
3. **The program that started the tool** is Claude Code or Codex, from the list below.

A connection that fails any check is closed without a reply, as one from the wrong account is today.
- **The allowed programs**, as found on the owner's Mac on 2026-11-09:
- **The allowed programs**, as seen on the owner's Mac on 2026-11-09:

| Program | Signed by | Team ID | Identifier |
| Program | Identity | Team | Signed by |
|---|---|---|---|
| Claude Code (command line) | Developer ID Application: Anthropic PBC | `Q6L2SF6YDW` | `com.anthropic.claude-code` |
| Codex (command line, its native program) | Developer ID Application: OpenAI OpCo, LLC | `2DC432GLL2` | `codex` |
| Claude Code (command line, and the copy the Claude app's Code tab runs) | `com.anthropic.claude-code` | `Q6L2SF6YDW` | Developer ID Application: Anthropic PBC |
| Codex (command line, its native program) | `codex` | `2DC432GLL2` | Developer ID Application: OpenAI OpCo, LLC |

The list is fixed in code: nothing the agent account can write changes it. The Claude app and the Codex app are added only after a check shows which of their processes starts the tool. ChatGPT and every other program are not on the list.
The list is fixed in code: nothing the agent account can write changes it.
- **Covered:** the Claude app's Code tab runs its own copy of Claude Code, with the same identity and team, and starts tools from it directly. So it is covered.
- **Not yet covered:** the Claude app's chat side and the Codex app are added only after a check shows which of their processes starts the tool.
- **Never on the list:** ChatGPT and every other program.
- **Codex installed with npm.** There the `codex` command is a Node.js script, which can't be signed. It starts Codex's signed native program, which is the program that starts the tool. The build step confirms this on a real run.
- **No fooling it with reused process numbers or injected code.** The connecting program is identified by the connection's audit token. The program that started it is identified by its process number, re-checked after the signature check so a reused number can't pass; the build PR shows how. A program whose signature lets outside code be loaded into it is refused: disabled library validation, allowed DYLD environment variables, or a debuggable signature.
- **How a program is identified.** By the kernel's record of the running program, never by reading program files. The App Sandbox refuses the Security framework's checks for other programs, because those read the program's file (see the spike).
- **The caller:** identified through its connection's audit token.
- **The program that started it:** found by process number through `sysctl`, re-checked afterwards so a reused number can't pass.
- **What must match.** All of these, together:
- **identity and team, exactly:** an identity alone can be faked by anyone (the spike faked one);
- **validation category "Developer ID":** macOS records this only when the signature chains to Apple's Developer ID, so a matching team can't be faked without that company's certificate;
- **a valid signature with hardened runtime;**
- **not debuggable and not being debugged.**
- **Outside code.** A program whose signature allows DYLD environment variables is refused. Neither Claude Code nor Codex has that entitlement today. If the kernel won't give a program's entitlements inside the sandbox, the build PR comes back to the owner.
- **Claude Code is accepted although its signature disables library validation** (owner's decision, 2026-11-09). Both the Homebrew and the Claude app copies carry `disable-library-validation` and `allow-unsigned-executable-memory`; Codex carries neither. See Consequences for what that allows.
- **Where the code lives.** In `src-tauri/src/agent_route.rs`, next to the existing peer checks, under the same placement rule in `scripts/check-network-source.mjs`.
- **A spike comes first.** It tests whether the sandboxed app may read another account's process information and code signatures. If it can't, the check isn't built that way, and this decision goes back to the owner.

## Consequences

- **A speed bump, not a wall.** It stops other programs in the agent account from using the route, whether directly or by running the tool. It doesn't stop a program in that account from starting Claude Code or Codex itself, with its own instructions, settings or MCP configuration. So the agent account must still hold only Claude Code and Codex. The separate account stays the real boundary.
- **Claude Code can load libraries Anthropic didn't sign.** A program in the agent account could plant one where Claude Code loads it, and act as Claude Code. That is the same rule again: the agent account holds only Claude Code and Codex.
- **It doesn't make Claude Code or Codex trustworthy.** A tricked Claude Code or Codex passes the check. Everything [ADR-0011](0011-chat-wallets.md), [ADR-0012](0012-agents-store-and-manage-keys.md) and [ADR-0014](0014-agent-key-operations-through-the-companion.md) say a tricked agent can still do stays true.
- **Updates can lock agents out.** If Anthropic or OpenAI change their signing identity, or the way the tool is started, the route refuses until a new build updates the list. It fails closed.
- **The app reads a little about other programs:** the connecting program's signature and code hash, and its parent's signature. Nothing else, and none of it leaves the app.
- **The app reads a little about other programs:** the caller's and its parent's identity, team, validation category, code hash and signature status, from the kernel. Nothing else, and none of it leaves the app.

## Build order

Each step is its own pull request.

1. These documents.
2. The spike, in a signed, sandboxed test build (a test identifier only, never the owner's Vault): can the app read what the check needs?
2. **Done 2026-11-09:** the spike. A stand-in, signed ad-hoc with hardened runtime and the app's exact entitlements, answered what the check needs. It also found that the socket itself needed one more sandbox rule (see below).
3. The check on the [ADR-0012](0012-agents-store-and-manage-keys.md) route, with tests:
- accepted: the right tool, started by Claude Code or by Codex;
- refused: the tool started by a shell or any other program, a program connecting directly, a wrong code hash, and a program whose signature allows outside code.
- refused: the tool started by a shell or any other program, a program connecting directly, a wrong code hash, a program signed only ad-hoc that claims an allowed identity, and a program that allows DYLD environment variables.
4. The same check on the Agent Companion's agent entry ([ADR-0014](0014-agent-key-operations-through-the-companion.md)).
5. Hand checks from Claude Code and Codex in the agent account, and from a third program that must be refused.
5. Hand checks from Claude Code and Codex in the agent account, and from a third program that must be refused, in a Developer ID-signed build.

## Spike result (2026-11-09)

A throwaway Swift program, signed ad-hoc with this app's exact `Entitlements.plist`, with hardened runtime, and given a bundle identity. Every test also ran unsandboxed as a control. Fake data only, on the owner's Mac (macOS 26).

| What the check needs | Security framework (reads the program file) | Kernel record (`csops_audittoken`, `sysctl`) |
|---|---|---|
| The caller's identity, by its audit token | Refused, except for Apple's own programs | Works |
| Claude Code's and Codex's identity, team and validation category | Refused | Works: Developer ID for both |
| A process owned by another account | Refused | Works |
| The parent's process number | `proc_pidinfo` is refused for another account, even unsandboxed | `sysctl` works, other accounts too |
| A fake signed ad-hoc as `com.anthropic.claude-code` | n/a | Identity matches, but no team and category "none": refused |

**The socket.** The folder entry in `Entitlements.plist` covers files but not Unix sockets.
- **Refused:** `bind()` there, even with `network.server` added; and connecting out to a listener there.
- **Works:** adding one `temporary-exception.sbpl` rule allowing `network-bind` and `network-inbound` on Unix sockets in that folder only. The owner approved that rule on 2026-11-09 ([ADR-0012](0012-agents-store-and-manage-keys.md), As built).
- **Still to confirm:** the rule in a Developer ID-signed, notarized build.

## Considered Options

- **The agent account alone (as built):** the owner asked for more.
- **Asking the caller who it is,** such as a name or token the agent sends: rejected. Any program can claim to be Claude Code.
- **Checking only the tool, not who started it:** rejected. Any program in the agent account can run the tool.
- **Checking the whole chain of parent processes:** not needed. The tool's direct parent must be on the list, so a shell or other program in between is refused.
- **The Security framework's signature checks:** ruled out by the spike. The sandbox refuses them for other programs.
- **Refusing every program that disables library validation:** rejected by the owner, because it would refuse Claude Code.
6 changes: 5 additions & 1 deletion docs/agent-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,11 @@ The code: `src-tauri/src/core/agent.rs` (the grant, the five requests and their
**Still to do by hand, on a real Mac, with fake keys:**

- [ ] `scripts/agent-keys-setup.sh` without `--dry-run`, for a real Standard account: the folder, its mode and its one ACL entry, and that the agent account can reach the socket but not list the folder.
- [ ] The built, sandboxed app can make its socket in `/Users/Shared/encrypted-note-keys` with its one new entitlement, and Start's account check works inside the sandbox (if macOS can't answer the group check there, Start refuses).
- [ ] The built, sandboxed app can make its socket in `/Users/Shared/encrypted-note-keys` with its two entitlements, and Start's account check works inside the sandbox (if macOS can't answer the group check there, Start refuses).
- **Partly answered by a spike on 2026-11-09.** It used a stand-in signed ad-hoc, with hardened runtime and this app's exact entitlements.
- **The folder entry alone isn't enough:** the sandbox refuses to make the socket, and adding `network.server` doesn't help.
- **The added `temporary-exception.sbpl` rule makes it work**, and the stand-in identified each caller.
- **Still to confirm** in a Developer ID-signed, notarized build.
- [ ] From Claude Code, Codex and the Claude app in the agent account: every request, including a Seed Phrase and a Private Key stored, edited, replaced and deleted through the Master Password panel, and the app coming to the front.
- [ ] A connection from your own account, and from a third account, gets no reply.
- [ ] Stop, a lock, sleep, screen lock, user switching, quitting and the end of the hour each end the grant and remove the socket, and a waiting request is cancelled.
Expand Down
Loading
Loading