| 1 | # AGENTS.md |
| 2 | |
| 3 | π€ Guidance for everyone β human or agent β changing this repository. This |
| 4 | is the canonical instructions file; tool-specific files (`CLAUDE.md`) are |
| 5 | pointers to it, so keep it current *here*. |
| 6 | |
| 7 | What CONTRA *is* lives in `README.md`; who it is for in `PRODUCT.md`; the |
| 8 | cryptographic protocol in `PROTOCOL.md`; the visual system in `DESIGN.md`; |
| 9 | the roadmap and its reasoning in `PLAN.md`. This file is how we build and |
| 10 | the decisions we have committed to. |
| 11 | |
| 12 | `PROTOCOL.md` was called `DESIGN.md` until 2026-09-04. If you are reading a |
| 13 | discussion, a commit message or a code comment older than that, the two |
| 14 | names mean the same file. |
| 15 | |
| 16 | CONTRA is a member of the CARLOS app family (`carlosframework/platform`); |
| 17 | see that family's `carlos:building-carlos-apps` skill for the shape it |
| 18 | follows. Where this file and the skill disagree, this file wins. |
| 19 | |
| 20 | # CONTRA |
| 21 | |
| 22 | An end-to-end-encrypted password manager. The server is host-blind: it |
| 23 | holds ciphertext, public keys, and opaque ids, and nothing else. |
| 24 | |
| 25 | ## The one rule |
| 26 | |
| 27 | **Absolutely nothing is revealed.** |
| 28 | |
| 29 | CONTRA is **CARLOS A1** β host-blind, in the strict sense the family |
| 30 | publishes at carlosframework.com: |
| 31 | |
| 32 | > Keys live on the user's own devices; the instance never holds one that |
| 33 | > opens user content. The only residue is transport shape: who connects |
| 34 | > when, object sizes, timing. **No content-derived identifiers at rest.** |
| 35 | |
| 36 | Not A2. A2 permits an enumerated application-level residue published in |
| 37 | the open, and it is where Eleven sits β precisely because Eleven's thread |
| 38 | membership is visible to the server. CONTRA does not get that allowance. |
| 39 | If a change would put a content-derived identifier in SQLite, it is wrong. |
| 40 | |
| 41 | The instance's whole SQLite file is continuously replicated to object |
| 42 | storage, so **a plaintext column is a plaintext leak**. Metadata β item |
| 43 | names, URLs, usernames, folder structure, vault names β seals client-side |
| 44 | exactly like values do. |
| 45 | |
| 46 | Enforced, not promised: the end-to-end suite pushes canary strings through |
| 47 | every write path and greps the raw SQLite bytes *and* the bucket objects |
| 48 | for them. Keep that test merciless. A new storage surface lands with its |
| 49 | canary or it does not land. |
| 50 | |
| 51 | ## Settled decisions (don't relitigate casually) |
| 52 | |
| 53 | Each is dated and attributed. Rejected alternatives are recorded beside |
| 54 | them. These change with new facts, dated and attributed β not in a |
| 55 | drive-by. |
| 56 | |
| 57 | ### 1. Trust class: A1 strict, on the Slopbox shape (Paul, 2026-09-01) |
| 58 | |
| 59 | Instance-per-account. A shared vault lives on its owner's instance; other |
| 60 | members are identities on that instance holding the vault's group key |
| 61 | wrapped to their own public key. |
| 62 | |
| 63 | **Rejected: Eleven's sharing model as-is.** It was the starting proposal |
| 64 | and it is the thing that makes Eleven an A2 app. Membership of a thread on |
| 65 | a shared multi-tenant instance is a content-derived identifier. Membership |
| 66 | of a vault on *your own* instance is not β it is the access-control list |
| 67 | the instance must consult to decide who it serves sealed bytes to, which |
| 68 | is transport shape. That distinction is load-bearing; if a change erodes |
| 69 | it, the class claim goes with it. |
| 70 | |
| 71 | The consequence we accept: revocation is Slopbox's honest three-part |
| 72 | answer β credentials die immediately, the group key rotates forward for |
| 73 | new and changed items, and **nothing already downloaded is un-downloaded**. |
| 74 | No copy pretends otherwise. |
| 75 | |
| 76 | ### 2. Recovery: a passphrase and print-once codes (Paul, 2026-09-01) |
| 77 | |
| 78 | A passphrase signs in to the web console. Recovery codes are shown |
| 79 | **exactly once**, at signup. Lose both and the data is gone β no restore, |
| 80 | no operator escape hatch, ever. |
| 81 | |
| 82 | **This reverses two dated family decisions**, deliberately: |
| 83 | |
| 84 | - Eleven, *Paul 2026-07-25*: "recovery is a passkey, never a passphrase or |
| 85 | a printed code." The paper kit was killed and the passphrase-wrap |
| 86 | primitive deleted the same day. |
| 87 | - Slopbox, *2026-07-26*: "the mandatory paper recovery kit is gone; |
| 88 | passkeys hold the keys." |
| 89 | |
| 90 | **Why it is right here and nowhere else in the family:** a password |
| 91 | manager cannot store its own recovery credential in a password manager. |
| 92 | The family's recovery passkey lives in iCloud Keychain or 1Password β |
| 93 | which is the exact product CONTRA replaces. The circularity is worst for |
| 94 | the person migrating off 1Password, who is our first user. Paper is back |
| 95 | because paper is the only medium that is not the thing we are replacing. |
| 96 | |
| 97 | Secondary reason, not the deciding one: the 1Password Emergency Kit means |
| 98 | this pattern is already understood by the audience. |
| 99 | |
| 100 | **Settled the same day (Paul, 2026-09-01): the kit prints an Account Key |
| 101 | too.** A 128-bit Account Key is mixed into the passphrase's KEK branch and |
| 102 | printed on the same client-generated PDF as the recovery code, because |
| 103 | passphrase-only unlock would have made the security of the replicated |
| 104 | bucket equal to the strength of whatever the user typed. Rejected: Option |
| 105 | B, passphrase-only plus compensating controls β a control that depends on |
| 106 | user behaviour is a hope with a progress bar. |
| 107 | |
| 108 | A **fresh** device needs the kit, or a handoff from an enrolled one, so |
| 109 | "passphrase known, kit lost, every device gone" is unrecoverable. **That |
| 110 | is not a cost we are paying β it is the one rule applied without |
| 111 | exception** (Paul, 2026-09-01), and the copy states it without |
| 112 | apologising. The everyday case is unchanged: a device holding a cached |
| 113 | Account Key unlocks with the passphrase alone. |
| 114 | |
| 115 | What it retires is a promise that was never quite true β "your passphrase |
| 116 | *or* your codes get you back in". The honest model is **the kit is the |
| 117 | root; the passphrase is the everyday convenience.** |
| 118 | |
| 119 | The obligation is therefore not to soften the rule but to ensure nobody |
| 120 | meets it by accident: a three-rung ladder that never dead-ends, a |
| 121 | prominent reprint flow, and a nag to enrol a second device. |
| 122 | |
| 123 | **The kit is two separate PDFs, not one sheet** β because the two values |
| 124 | have different blast radii. The Account Key alone is nearly harmless (one |
| 125 | input to the passphrase branch); the recovery code alone is full |
| 126 | takeover. One is a setup aid to keep handy, the other a bearer credential |
| 127 | to archive, and the risk we are managing is *storage* β people save PDFs, |
| 128 | and one file puts both in the same folder forever. `PROTOCOL.md` carries |
| 129 | both documents, the ladder, the reprint semantics and the loss table. |
| 130 | |
| 131 | ### 3. Email exists only as a blind index (Paul, 2026-09-01) |
| 132 | |
| 133 | Kass's shape: an HMAC under a server-held pepper. It proves identity for |
| 134 | sign-in and **can never decrypt anything**. There is no stored address |
| 135 | column and no provider user-id. Email-gated key escrow is the forbidden |
| 136 | shortcut and stays forbidden β a compromised server plus a passed email |
| 137 | gate must still yield nothing. |
| 138 | |
| 139 | Direction of travel (Paul, 2026-09-01): email + magic link for signup, |
| 140 | then passkey for quick sign-in thereafter, is intended to become the |
| 141 | **CARLOS default**. CONTRA is where it gets built first. Build it so it |
| 142 | can be lifted. |
| 143 | |
| 144 | ### 4. Passkey + PRF is a fast unlock, never the only door (Paul, 2026-09-01) |
| 145 | |
| 146 | A third wrap of the same key material, not a shortcut around it. The |
| 147 | failure that forced Eleven's rethink β the macOS ceremony sheet reaching |
| 148 | only iCloud Keychain, and 1Password's macOS provider returning a PRF that |
| 149 | does not match its own extension's for the same credential β means a |
| 150 | present passkey can still fail. Every screen that offers it must offer the |
| 151 | passphrase beside it, always, with no dead ends. |
| 152 | |
| 153 | ### 5. Agents may use a secret; they may not see one (Paul, 2026-09-01) |
| 154 | |
| 155 | An agent gets a scoped, per-agent credential that can **list** metadata, |
| 156 | **use** a value without receiving it, and **create** new values (free β |
| 157 | sealing to a public key needs no unlock). Reading a value into a model's |
| 158 | context is **not a path that exists in v1**. |
| 159 | |
| 160 | Sharpened by a design review, 2026-09-01. The review's findings are |
| 161 | binding, not advisory: |
| 162 | |
| 163 | **Say which threat each control actually stops.** Two adversaries, and |
| 164 | they must never be conflated in our docs or our UI: |
| 165 | |
| 166 | - **T1 β the honest but leaky agent.** Doing its job, about to spill a |
| 167 | value into a transcript and, through the prompt ledger, into public. |
| 168 | - **T2 β the adversarial agent.** Prompt-injected through a README, a web |
| 169 | page, an issue comment; actively trying to exfiltrate. |
| 170 | |
| 171 | Use-without-seeing defeats T1. It **does not** defeat T2 β an agent that |
| 172 | chooses the command can make that command print its own environment. We |
| 173 | ship the honest claim: *values never enter transcripts by default; a |
| 174 | hostile agent is contained by scoping, staging and audit, not by the |
| 175 | injection trick.* Marketing this as a T2 control would be a lie. |
| 176 | |
| 177 | **Same-UID is not a boundary.** The agent's shell runs as the same user as |
| 178 | the daemon. Minimum posture: macOS hardened runtime, non-dumpable and no |
| 179 | core dumps on Linux, `mlock`ed key pages, and a written plan to move the |
| 180 | daemon to its own UID with peer-credential checks on the socket. The |
| 181 | threat model says out loud that a determined same-UID process wins; our |
| 182 | job is to make winning **noisy**. |
| 183 | |
| 184 | **The three v1-mandatory controls**, built before anything else in the |
| 185 | agent surface: |
| 186 | |
| 187 | 1. **The daemon-side redaction choke point.** One serialization boundary |
| 188 | through which every byte returning from daemon to agent passes β |
| 189 | stdout, stderr, error strings β scanned for each injected value and its |
| 190 | cheap encodings (base64, hex, URL- and JSON-escaped) and replaced. |
| 191 | 2. **Redaction-hit alerting.** A spike in redactor firings is the best |
| 192 | intrusion signal we will ever own. Alert on it; do not merely log it. |
| 193 | 3. **The ledger publish-time screen.** Every prompt is checked against |
| 194 | local vault-value hashes *before* it is published. Values are local, so |
| 195 | the check is local and exact. Given that this repo publishes a prompt |
| 196 | ledger automatically, this is the last gate between "leaked into a |
| 197 | transcript" and "published to the internet". It is not optional. |
| 198 | |
| 199 | **Agents create; humans promote; nothing mutates.** The attack that forced |
| 200 | this: an agent rewrites the *URL* of an existing item to a lookalike host, |
| 201 | the human later autofills, and the client pours the real password β which |
| 202 | the agent could never read β into the attacker's page. Write access to |
| 203 | metadata plus autofill is credential theft through the human's own hands. |
| 204 | So agent writes are **create-only into a quarantined staging state**: |
| 205 | never autofilled, never usable by `use`, visibly badged, kept out of the |
| 206 | main list, forbidden from colliding with an existing item's name or host. |
| 207 | A human promotes after a diff-style review. **No agent overwrite of an |
| 208 | existing item in v1, period.** Rotation is stage-a-new-version plus |
| 209 | promote. This costs one human click per provisioned credential; that is a |
| 210 | good trade. |
| 211 | |
| 212 | **Two `use` verbs that fail in opposite directions.** `use-env` leaks the |
| 213 | *value* easily but wields only the command's authority; `use-http` |
| 214 | protects the value nearly perfectly but hands over the credential's |
| 215 | *entire authority* at that host β and for most API tokens the authority is |
| 216 | worth more than the string. Therefore: |
| 217 | |
| 218 | - `use-env` grants name a **human-registered command binding**, not a |
| 219 | free-form command. Prefer stdin/fd injection over the environment where |
| 220 | the tool supports it. |
| 221 | - `use-http` is **default-deny with per-grant method and path |
| 222 | allowlists**, on a human-confirmed origin pin (scheme+host+port, HTTPS |
| 223 | only): no redirects followed, proxy env ignored, TLS validated against |
| 224 | the pinned name, response bodies redacted, size caps and timeouts. The |
| 225 | **daemon** owns credential placement β the agent supplies method, path |
| 226 | and body, and never composes the authenticated part of a request. An |
| 227 | agent may never set an origin pin; pins are trusted only once promoted. |
| 228 | |
| 229 | **Grant scope** is (agent identity, item, verb, verb-specific narrowing, |
| 230 | TTL). Per-item for anything value-touching β vault-wide grants are how you |
| 231 | lose everything to one injection; vault-level is fine for `list`. Use |
| 232 | counts are theatre except `n=1` on `read`. Revocation is a local delete, |
| 233 | checked atomically per use: nothing new starts, anything already launched |
| 234 | completes β so the console must list processes currently holding secrets, |
| 235 | with a kill button. |
| 236 | |
| 237 | **Approval** attaches to an (agent, item, verb, binding) tuple, not to |
| 238 | each use, with a session TTL as the default and the concrete action shown. |
| 239 | Long unattended runs get their grants at kickoff, while the human is |
| 240 | present. A grant not anticipated mid-run **blocks the run** β that is |
| 241 | correct behaviour, not a bug. Per-use prompts are worse than useless: they |
| 242 | train reflexive clicking. |
| 243 | |
| 244 | **Audit** is local, append-only, hash-chained and signed, with a sealed |
| 245 | copy synced to the server for durability (the operator cannot read it, |
| 246 | which is fine β the server was never going to enforce anything). |
| 247 | **Audit-before-result**: no operation's result is released to the agent |
| 248 | until its log entry is durably queued. Review is a screen β "what did |
| 249 | agents do last night", anomalies first β not a log file. Audit without |
| 250 | review is a diary, not a control. |
| 251 | |
| 252 | **The failure mode we expect to actually bite**, recorded so nobody is |
| 253 | surprised by it: an honest agent debugging a failing job at 3am under a |
| 254 | standing `use-env` grant runs `env`, and the secret rides the transcript |
| 255 | into the public ledger. Boring, likely, and uniquely bad here because |
| 256 | publication is automated. The three mandatory controls above exist for |
| 257 | exactly this. |
| 258 | |
| 259 | ### 6. Build order (Paul, 2026-09-01) |
| 260 | |
| 261 | 1. rastrillo instance + **client-shape** web console β sealing happens |
| 262 | where the keys are, so this surface is client-shape by force. |
| 263 | 2. Native Mac app β the *second* implementation of a settled contract. |
| 264 | 3. AutoFill Credential Provider extension (macOS). |
| 265 | 4. iOS. |
| 266 | |
| 267 | The protocol is proven once, in one place, before a second client exists. |
| 268 | |
| 269 | ### 7. Import is 1PUX, and it is client-side (Paul, 2026-09-01) |
| 270 | |
| 271 | 1Password's 1PUX export for v1: it carries item types, TOTP seeds, |
| 272 | attachments and vault structure, where CSV loses nearly all of it. The |
| 273 | file is parsed and sealed **entirely in the browser**. An unsealed export |
| 274 | touching the server would be an A1 violation on day one. |
| 275 | |
| 276 | Stated honestly in the copy: **passkeys stored in 1Password cannot be |
| 277 | exported by anyone**, us included. That is 1Password's limit, not ours, |
| 278 | and it needs a manual migration path rather than a pretence. |
| 279 | |
| 280 | Bitwarden and Apple Passwords importers are wanted, later β tracked as |
| 281 | discussions on this repo, not built now. |
| 282 | |
| 283 | ### 8. Ledgers on, at `prompts` level (Paul, 2026-09-01) |
| 284 | |
| 285 | Prompt ledger and carbon ledger both enabled (`amadan ledger init -level |
| 286 | prompts`). Not `dialogue` or `full`, and the reason is this product: a |
| 287 | fuller level publishes more of what passes through a session working on a |
| 288 | password manager. |
| 289 | |
| 290 | **The law that goes with it: no real secret ever enters a prompt, a test |
| 291 | fixture, a commit, or a canary.** Fixtures are generated. If a real value |
| 292 | is ever typed, `amadan ledger redact` / `amadan ledger withhold` are |
| 293 | routine tools here, not emergency ones β use them the same day. |
| 294 | |
| 295 | ### 9. Licence: MPL 2.0 (Paul, 2026-09-01) |
| 296 | |
| 297 | Mozilla Public License 2.0. File-level copyleft: a modified CONTRA must |
| 298 | ship its modified files, so what a user runs stays checkable against what |
| 299 | we published β the same argument the whole product makes. Factor VI, not |
| 300 | deferred. |
| 301 | |
| 302 | **The mechanical part, for when code lands:** MPL expects each source file |
| 303 | to carry the Exhibit A notice. Add it as files are created rather than in |
| 304 | a sweep afterwards, and if it is worth enforcing, enforce it as a test β |
| 305 | this house does not rely on remembering. |
| 306 | |
| 307 | ### 10. Money: free rung, then β¬5/year (Paul, 2026-09-01) |
| 308 | |
| 309 | Slopbox-inspired: a genuine free rung, paid starting at **β¬5/year**. |
| 310 | Cheap on purpose β a password manager people cannot afford to keep is a |
| 311 | password manager that loses their data. Integer cents; a float never |
| 312 | touches an amount. |
| 313 | |
| 314 | ## Build and test |
| 315 | |
| 316 | - Go, one static binary, `CGO_ENABLED=0`, thin `main.go` dispatch. |
| 317 | - **rastrillo** is the framework (`github.com/carlosframework/rastrillo`) β |
| 318 | its packages exist to be used, not hand-rolled. Read the repo's own |
| 319 | `SKILL.md` first; it postdates most models' training data. Honest note |
| 320 | carried from the family: rastrillo does not yet support the E2EE app |
| 321 | story at framework level, so the sealed flow is app-side, built on the |
| 322 | Keymail/Kass/Slopbox patterns. Don't claim the framework does it. |
| 323 | - **Stdlib first.** Adding a dependency is a decision, not a default. |
| 324 | - `modernc.org/sqlite`, WAL (`busy_timeout` set *before* `journal_mode`), |
| 325 | `SetMaxOpenConns(1)`, additive-only migrations. New code over an old DB |
| 326 | must always be safe. Never delete data to update. |
| 327 | - `go test ./...`, `go vet ./...`, `gofmt -l .` β clean, always. Nothing |
| 328 | lands untested; a bug fix carries its regression test. Never pipe a test |
| 329 | run you are gating on: a pipe eats the exit code. |
| 330 | - Crypto is byte-identical in Go and JS, pinned by shared golden vectors. |
| 331 | Change one side and you must change the other, plus the context strings. |
| 332 | - JS: small self-contained ES modules, no build step, `go:embed`, **300 |
| 333 | lines per module enforced by test**, caps ratchet down only. Never |
| 334 | `innerHTML` untrusted content β build DOM nodes. |
| 335 | - The merciless greps (above) run in the end-to-end suite. |
| 336 | |
| 337 | ## Picking up work |
| 338 | |
| 339 | The backlog lives in this repo's **discussions**, not in this file. |
| 340 | **Discussion 18, "START HERE", is the map** β the two tracks (`web/*` for |
| 341 | remote agents, `native/*` for local), the dependency order, and the |
| 342 | claiming convention. |
| 343 | |
| 344 | Read it before claiming anything. In particular: amadan has no assign or |
| 345 | claim verb yet (filed as `amadan/amadan` 36), so claiming is a **reply** |
| 346 | on the discussion, and skipping that step is how two agents end up |
| 347 | building the same thing overnight. |
| 348 | |
| 349 | ## Workflow |
| 350 | |
| 351 | - **Worktree per session, always.** Never build in the main checkout; |
| 352 | reading it is fine, writing is not. |
| 353 | - **On amadan the branch is the pull request.** Take a worktree, take a |
| 354 | branch, push it, and describe it *before* the first edit: |
| 355 | `amadan branch describe <branch> -body -`. The branch page is the only |
| 356 | view of your work anyone gets β keep the description true, put the plan |
| 357 | in Tasks, and put questions and decisions in Discussions. |
| 358 | - Agents post as their agent account (`claude~paul@keymail.dev`, via |
| 359 | `AMADAN_HOME`), never as the human. The π€ badge is not optional. |
| 360 | - Squash-merge to main only when all three hold: the work is done, a human |
| 361 | has approved it, and CI is green. Never commit to main. |
| 362 | - **The bootstrap exception, once.** "CI green" binds where CI exists. This |
| 363 | repo has no CI script until `web/1` lands, so there is no green to wait |
| 364 | for and the condition cannot be met by anyone. amadan agrees: its merge |
| 365 | gate defaults to off β *"a gate nobody chose is not a gate"* β and |
| 366 | reports such a repo's CI as `skipped`, "this repo has no CI script, so |
| 367 | nothing can be green". So the first merges land on **done + approved**, |
| 368 | and **adding CI is itself the first work item**, not a prerequisite that |
| 369 | cannot be satisfied. The moment `.amadan/ci` exists the three-condition |
| 370 | rule binds absolutely, and this paragraph is spent. |
| 371 | - Commit subjects are area-prefixed prose that says why. Every commit |
| 372 | carries a `Co-Authored-By:` trailer; the human is the author of record. |
| 373 | - Review happens on a deployed canary, never on localhost. Report the |
| 374 | clickable canary URL in every reply while iterating. |
| 375 | - **Never echo a secret, a claim link, or a recovery code into a session.** |
| 376 | |
| 377 | ## AI authorship |
| 378 | |
| 379 | Machine-authored text is marked π€ where it surfaces. A person marker |
| 380 | (π¨/π€/π§) certifies human text an LLM must not rewrite, rephrase, condense |
| 381 | or delete. AI features in the product are opt-in, always disclosed, and |
| 382 | every path works with the AI switched off. |
| 383 | |
| 384 | ## Deliberately not built yet |
| 385 | |
| 386 | Each with the trigger that would start it. |
| 387 | |
| 388 | | Not built | Trigger | |
| 389 | |---|---| |
| 390 | | Bitwarden and Apple Passwords importers | 1PUX import is shipped and a real user asks | |
| 391 | | Passkey migration out of 1Password | A documented manual path exists first; automation only if the export limit ever lifts | |
| 392 | | iOS app and its AutoFill extension | The Mac app and its extension are shipped and settled | |
| 393 | | Cross-instance vault sharing (federation) | Never in v1 β the biggest scope trap; revisit only after single-instance sharing is proven | |
| 394 | | 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 | |
| 395 | | Breach / reused-password monitoring | Requires sending something derived from a value off-device; needs an A1-compatible design before it is even sketched | |
| 396 | | Secure sharing with non-members (one-off links) | After vault sharing ships | |
| 397 | | Agent mutation of an existing item | A user-demanded rotation workflow β and then as staged-version-plus-promote, never an in-place write | |
| 398 | | 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 | |
| 399 | | Remote or mobile approval of an agent grant | The audit pipeline proven in the field, plus a dedicated design review of the control channel | |
| 400 | | Free-form `use-http` (no path allowlist) | **No trigger. This stays denied.** | |
| 401 | | Agent-settable origin pins; autofill of agent-created items | **Never** β both are the lookalike-URL attack | |
| 402 | |