intent: agents create, humans promote — nothing mutates
A design review of the agent surface found the attack the original shape missed: an agent cannot read a value, but it could rewrite an existing item's URL to a lookalike host and let the human's own autofill pour the real password into the attacker's page. Write access to metadata plus autofill is credential theft through the human's hands. So agent writes become create-only into quarantined staging, promoted by a human after a diff review, with no overwrite path at all in v1. Also records what "use without seeing" actually defends against. It stops an honest agent leaking a value into a transcript; it does not stop a prompt-injected one, because an agent that picks the command can make the command print its own environment. Claiming otherwise would be a lie, so the threat model names both adversaries and says which control covers which. Three controls become v1-mandatory, the third specific to this repo: a daemon-side redaction choke point on everything returned to an agent, alerting on redaction hits as an intrusion signal, and a publish-time screen of every prompt against local vault-value hashes — the last gate before a leaked secret becomes a published one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+104
−4
AGENTS.md+104 −4
Intent
diff --git a/AGENTS.md b/AGENTS.md| index ab70793..9b96db6 100644 |
| --- a/AGENTS.md |
| +++ b/AGENTS.md |
| @@ -117,13 +117,108 @@ passphrase beside it, always, with no dead ends. |
| ### 5. Agents may use a secret; they may not see one (Paul, 2026-09-01) |
| An agent gets a scoped, per-agent credential that can **list** metadata, |
| -**use** a value without receiving it, and **write** new values (free — |
| +**use** a value without receiving it, and **create** new values (free — |
| sealing to a public key needs no unlock). Reading a value into a model's |
| context is **not a path that exists in v1**. |
| -This is sharper here than elsewhere because this repo publishes a prompt |
| -ledger: a secret that reaches a model's context is close to a secret that |
| -gets published. |
| +Sharpened by a design review, 2026-09-01. The review's findings are |
| +binding, not advisory: |
| + |
| +**Say which threat each control actually stops.** Two adversaries, and |
| +they must never be conflated in our docs or our UI: |
| + |
| +- **T1 — the honest but leaky agent.** Doing its job, about to spill a |
| + value into a transcript and, through the prompt ledger, into public. |
| +- **T2 — the adversarial agent.** Prompt-injected through a README, a web |
| + page, an issue comment; actively trying to exfiltrate. |
| + |
| +Use-without-seeing defeats T1. It **does not** defeat T2 — an agent that |
| +chooses the command can make that command print its own environment. We |
| +ship the honest claim: *values never enter transcripts by default; a |
| +hostile agent is contained by scoping, staging and audit, not by the |
| +injection trick.* Marketing this as a T2 control would be a lie. |
| + |
| +**Same-UID is not a boundary.** The agent's shell runs as the same user as |
| +the daemon. Minimum posture: macOS hardened runtime, non-dumpable and no |
| +core dumps on Linux, `mlock`ed key pages, and a written plan to move the |
| +daemon to its own UID with peer-credential checks on the socket. The |
| +threat model says out loud that a determined same-UID process wins; our |
| +job is to make winning **noisy**. |
| + |
| +**The three v1-mandatory controls**, built before anything else in the |
| +agent surface: |
| + |
| +1. **The daemon-side redaction choke point.** One serialization boundary |
| + through which every byte returning from daemon to agent passes — |
| + stdout, stderr, error strings — scanned for each injected value and its |
| + cheap encodings (base64, hex, URL- and JSON-escaped) and replaced. |
| +2. **Redaction-hit alerting.** A spike in redactor firings is the best |
| + intrusion signal we will ever own. Alert on it; do not merely log it. |
| +3. **The ledger publish-time screen.** Every prompt is checked against |
| + local vault-value hashes *before* it is published. Values are local, so |
| + the check is local and exact. Given that this repo publishes a prompt |
| + ledger automatically, this is the last gate between "leaked into a |
| + transcript" and "published to the internet". It is not optional. |
| + |
| +**Agents create; humans promote; nothing mutates.** The attack that forced |
| +this: an agent rewrites the *URL* of an existing item to a lookalike host, |
| +the human later autofills, and the client pours the real password — which |
| +the agent could never read — into the attacker's page. Write access to |
| +metadata plus autofill is credential theft through the human's own hands. |
| +So agent writes are **create-only into a quarantined staging state**: |
| +never autofilled, never usable by `use`, visibly badged, kept out of the |
| +main list, forbidden from colliding with an existing item's name or host. |
| +A human promotes after a diff-style review. **No agent overwrite of an |
| +existing item in v1, period.** Rotation is stage-a-new-version plus |
| +promote. This costs one human click per provisioned credential; that is a |
| +good trade. |
| + |
| +**Two `use` verbs that fail in opposite directions.** `use-env` leaks the |
| +*value* easily but wields only the command's authority; `use-http` |
| +protects the value nearly perfectly but hands over the credential's |
| +*entire authority* at that host — and for most API tokens the authority is |
| +worth more than the string. Therefore: |
| + |
| +- `use-env` grants name a **human-registered command binding**, not a |
| + free-form command. Prefer stdin/fd injection over the environment where |
| + the tool supports it. |
| +- `use-http` is **default-deny with per-grant method and path |
| + allowlists**, on a human-confirmed origin pin (scheme+host+port, HTTPS |
| + only): no redirects followed, proxy env ignored, TLS validated against |
| + the pinned name, response bodies redacted, size caps and timeouts. The |
| + **daemon** owns credential placement — the agent supplies method, path |
| + and body, and never composes the authenticated part of a request. An |
| + agent may never set an origin pin; pins are trusted only once promoted. |
| + |
| +**Grant scope** is (agent identity, item, verb, verb-specific narrowing, |
| +TTL). Per-item for anything value-touching — vault-wide grants are how you |
| +lose everything to one injection; vault-level is fine for `list`. Use |
| +counts are theatre except `n=1` on `read`. Revocation is a local delete, |
| +checked atomically per use: nothing new starts, anything already launched |
| +completes — so the console must list processes currently holding secrets, |
| +with a kill button. |
| + |
| +**Approval** attaches to an (agent, item, verb, binding) tuple, not to |
| +each use, with a session TTL as the default and the concrete action shown. |
| +Long unattended runs get their grants at kickoff, while the human is |
| +present. A grant not anticipated mid-run **blocks the run** — that is |
| +correct behaviour, not a bug. Per-use prompts are worse than useless: they |
| +train reflexive clicking. |
| + |
| +**Audit** is local, append-only, hash-chained and signed, with a sealed |
| +copy synced to the server for durability (the operator cannot read it, |
| +which is fine — the server was never going to enforce anything). |
| +**Audit-before-result**: no operation's result is released to the agent |
| +until its log entry is durably queued. Review is a screen — "what did |
| +agents do last night", anomalies first — not a log file. Audit without |
| +review is a diary, not a control. |
| + |
| +**The failure mode we expect to actually bite**, recorded so nobody is |
| +surprised by it: an honest agent debugging a failing job at 3am under a |
| +standing `use-env` grant runs `env`, and the secret rides the transcript |
| +into the public ledger. Boring, likely, and uniquely bad here because |
| +publication is automated. The three mandatory controls above exist for |
| +exactly this. |
| ### 6. Build order (Paul, 2026-09-01) |
| @@ -230,3 +325,8 @@ Each with the trigger that would start it. |
| | Agent *read* of a secret value into context | Not planned. Would need a written argument that use-without-seeing genuinely cannot serve a real case | |
| | Breach / reused-password monitoring | Requires sending something derived from a value off-device; needs an A1-compatible design before it is even sketched | |
| | Secure sharing with non-members (one-off links) | After vault sharing ships | |
| +| Agent mutation of an existing item | A user-demanded rotation workflow — and then as staged-version-plus-promote, never an in-place write | |
| +| Vault-wide grants for value-touching verbs | Sustained user reports that per-item granting is the top friction. We are host-blind, so this can only come from users, never telemetry | |
| +| Remote or mobile approval of an agent grant | The audit pipeline proven in the field, plus a dedicated design review of the control channel | |
| +| Free-form `use-http` (no path allowlist) | **No trigger. This stays denied.** | |
| +| Agent-settable origin pins; autofill of agent-created items | **Never** — both are the lookalike-URL attack | |