carlos / contra Public

Clone
git clone https://amadan.net/carlos/contra

Plain git — no account needed to clone.

Download

Download this file

1# AGENTS.md
2
3πŸ€– Guidance for everyone β€” human or agent β€” changing this repository. This
4is the canonical instructions file; tool-specific files (`CLAUDE.md`) are
5pointers to it, so keep it current *here*.
6
7What CONTRA *is* lives in `README.md`; who it is for in `PRODUCT.md`; the
8cryptographic protocol in `PROTOCOL.md`; the visual system in `DESIGN.md`;
9the roadmap and its reasoning in `PLAN.md`. This file is how we build and
10the decisions we have committed to.
11
12`PROTOCOL.md` was called `DESIGN.md` until 2026-09-04. If you are reading a
13discussion, a commit message or a code comment older than that, the two
14names mean the same file.
15
16CONTRA is a member of the CARLOS app family (`carlosframework/platform`);
17see that family's `carlos:building-carlos-apps` skill for the shape it
18follows. Where this file and the skill disagree, this file wins.
19
20# CONTRA
21
22An end-to-end-encrypted password manager. The server is host-blind: it
23holds ciphertext, public keys, and opaque ids, and nothing else.
24
25## The one rule
26
27**Absolutely nothing is revealed.**
28
29CONTRA is **CARLOS A1** β€” host-blind, in the strict sense the family
30publishes 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
36Not A2. A2 permits an enumerated application-level residue published in
37the open, and it is where Eleven sits β€” precisely because Eleven's thread
38membership is visible to the server. CONTRA does not get that allowance.
39If a change would put a content-derived identifier in SQLite, it is wrong.
40
41The instance's whole SQLite file is continuously replicated to object
42storage, so **a plaintext column is a plaintext leak**. Metadata β€” item
43names, URLs, usernames, folder structure, vault names β€” seals client-side
44exactly like values do.
45
46Enforced, not promised: the end-to-end suite pushes canary strings through
47every write path and greps the raw SQLite bytes *and* the bucket objects
48for them. Keep that test merciless. A new storage surface lands with its
49canary or it does not land.
50
51## Settled decisions (don't relitigate casually)
52
53Each is dated and attributed. Rejected alternatives are recorded beside
54them. These change with new facts, dated and attributed β€” not in a
55drive-by.
56
57### 1. Trust class: A1 strict, on the Slopbox shape (Paul, 2026-09-01)
58
59Instance-per-account. A shared vault lives on its owner's instance; other
60members are identities on that instance holding the vault's group key
61wrapped to their own public key.
62
63**Rejected: Eleven's sharing model as-is.** It was the starting proposal
64and it is the thing that makes Eleven an A2 app. Membership of a thread on
65a shared multi-tenant instance is a content-derived identifier. Membership
66of a vault on *your own* instance is not β€” it is the access-control list
67the instance must consult to decide who it serves sealed bytes to, which
68is transport shape. That distinction is load-bearing; if a change erodes
69it, the class claim goes with it.
70
71The consequence we accept: revocation is Slopbox's honest three-part
72answer β€” credentials die immediately, the group key rotates forward for
73new and changed items, and **nothing already downloaded is un-downloaded**.
74No copy pretends otherwise.
75
76### 2. Recovery: a passphrase and print-once codes (Paul, 2026-09-01)
77
78A 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,
80no 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
91manager cannot store its own recovery credential in a password manager.
92The family's recovery passkey lives in iCloud Keychain or 1Password β€”
93which is the exact product CONTRA replaces. The circularity is worst for
94the person migrating off 1Password, who is our first user. Paper is back
95because paper is the only medium that is not the thing we are replacing.
96
97Secondary reason, not the deciding one: the 1Password Emergency Kit means
98this pattern is already understood by the audience.
99
100**Settled the same day (Paul, 2026-09-01): the kit prints an Account Key
101too.** A 128-bit Account Key is mixed into the passphrase's KEK branch and
102printed on the same client-generated PDF as the recovery code, because
103passphrase-only unlock would have made the security of the replicated
104bucket equal to the strength of whatever the user typed. Rejected: Option
105B, passphrase-only plus compensating controls β€” a control that depends on
106user behaviour is a hope with a progress bar.
107
108A **fresh** device needs the kit, or a handoff from an enrolled one, so
109"passphrase known, kit lost, every device gone" is unrecoverable. **That
110is not a cost we are paying β€” it is the one rule applied without
111exception** (Paul, 2026-09-01), and the copy states it without
112apologising. The everyday case is unchanged: a device holding a cached
113Account Key unlocks with the passphrase alone.
114
115What 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
117root; the passphrase is the everyday convenience.**
118
119The obligation is therefore not to soften the rule but to ensure nobody
120meets it by accident: a three-rung ladder that never dead-ends, a
121prominent 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
124have different blast radii. The Account Key alone is nearly harmless (one
125input to the passphrase branch); the recovery code alone is full
126takeover. One is a setup aid to keep handy, the other a bearer credential
127to archive, and the risk we are managing is *storage* β€” people save PDFs,
128and one file puts both in the same folder forever. `PROTOCOL.md` carries
129both 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
133Kass's shape: an HMAC under a server-held pepper. It proves identity for
134sign-in and **can never decrypt anything**. There is no stored address
135column and no provider user-id. Email-gated key escrow is the forbidden
136shortcut and stays forbidden β€” a compromised server plus a passed email
137gate must still yield nothing.
138
139Direction of travel (Paul, 2026-09-01): email + magic link for signup,
140then 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
142can be lifted.
143
144### 4. Passkey + PRF is a fast unlock, never the only door (Paul, 2026-09-01)
145
146A third wrap of the same key material, not a shortcut around it. The
147failure that forced Eleven's rethink β€” the macOS ceremony sheet reaching
148only iCloud Keychain, and 1Password's macOS provider returning a PRF that
149does not match its own extension's for the same credential β€” means a
150present passkey can still fail. Every screen that offers it must offer the
151passphrase 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
155An agent gets a scoped, per-agent credential that can **list** metadata,
156**use** a value without receiving it, and **create** new values (free β€”
157sealing to a public key needs no unlock). Reading a value into a model's
158context is **not a path that exists in v1**.
159
160Sharpened by a design review, 2026-09-01. The review's findings are
161binding, not advisory:
162
163**Say which threat each control actually stops.** Two adversaries, and
164they 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
171Use-without-seeing defeats T1. It **does not** defeat T2 β€” an agent that
172chooses the command can make that command print its own environment. We
173ship the honest claim: *values never enter transcripts by default; a
174hostile agent is contained by scoping, staging and audit, not by the
175injection 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
178the daemon. Minimum posture: macOS hardened runtime, non-dumpable and no
179core dumps on Linux, `mlock`ed key pages, and a written plan to move the
180daemon to its own UID with peer-credential checks on the socket. The
181threat model says out loud that a determined same-UID process wins; our
182job is to make winning **noisy**.
183
184**The three v1-mandatory controls**, built before anything else in the
185agent surface:
186
1871. **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.
1912. **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.
1933. **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
200this: an agent rewrites the *URL* of an existing item to a lookalike host,
201the human later autofills, and the client pours the real password β€” which
202the agent could never read β€” into the attacker's page. Write access to
203metadata plus autofill is credential theft through the human's own hands.
204So agent writes are **create-only into a quarantined staging state**:
205never autofilled, never usable by `use`, visibly badged, kept out of the
206main list, forbidden from colliding with an existing item's name or host.
207A human promotes after a diff-style review. **No agent overwrite of an
208existing item in v1, period.** Rotation is stage-a-new-version plus
209promote. This costs one human click per provisioned credential; that is a
210good 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`
214protects the value nearly perfectly but hands over the credential's
215*entire authority* at that host β€” and for most API tokens the authority is
216worth 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,
230TTL). Per-item for anything value-touching β€” vault-wide grants are how you
231lose everything to one injection; vault-level is fine for `list`. Use
232counts are theatre except `n=1` on `read`. Revocation is a local delete,
233checked atomically per use: nothing new starts, anything already launched
234completes β€” so the console must list processes currently holding secrets,
235with a kill button.
236
237**Approval** attaches to an (agent, item, verb, binding) tuple, not to
238each use, with a session TTL as the default and the concrete action shown.
239Long unattended runs get their grants at kickoff, while the human is
240present. A grant not anticipated mid-run **blocks the run** β€” that is
241correct behaviour, not a bug. Per-use prompts are worse than useless: they
242train reflexive clicking.
243
244**Audit** is local, append-only, hash-chained and signed, with a sealed
245copy synced to the server for durability (the operator cannot read it,
246which is fine β€” the server was never going to enforce anything).
247**Audit-before-result**: no operation's result is released to the agent
248until its log entry is durably queued. Review is a screen β€” "what did
249agents do last night", anomalies first β€” not a log file. Audit without
250review is a diary, not a control.
251
252**The failure mode we expect to actually bite**, recorded so nobody is
253surprised by it: an honest agent debugging a failing job at 3am under a
254standing `use-env` grant runs `env`, and the secret rides the transcript
255into the public ledger. Boring, likely, and uniquely bad here because
256publication is automated. The three mandatory controls above exist for
257exactly this.
258
259### 6. Build order (Paul, 2026-09-01)
260
2611. rastrillo instance + **client-shape** web console β€” sealing happens
262 where the keys are, so this surface is client-shape by force.
2632. Native Mac app β€” the *second* implementation of a settled contract.
2643. AutoFill Credential Provider extension (macOS).
2654. iOS.
266
267The 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
2711Password's 1PUX export for v1: it carries item types, TOTP seeds,
272attachments and vault structure, where CSV loses nearly all of it. The
273file is parsed and sealed **entirely in the browser**. An unsealed export
274touching the server would be an A1 violation on day one.
275
276Stated honestly in the copy: **passkeys stored in 1Password cannot be
277exported by anyone**, us included. That is 1Password's limit, not ours,
278and it needs a manual migration path rather than a pretence.
279
280Bitwarden and Apple Passwords importers are wanted, later β€” tracked as
281discussions on this repo, not built now.
282
283### 8. Ledgers on, at `prompts` level (Paul, 2026-09-01)
284
285Prompt ledger and carbon ledger both enabled (`amadan ledger init -level
286prompts`). Not `dialogue` or `full`, and the reason is this product: a
287fuller level publishes more of what passes through a session working on a
288password manager.
289
290**The law that goes with it: no real secret ever enters a prompt, a test
291fixture, a commit, or a canary.** Fixtures are generated. If a real value
292is ever typed, `amadan ledger redact` / `amadan ledger withhold` are
293routine tools here, not emergency ones β€” use them the same day.
294
295### 9. Licence: MPL 2.0 (Paul, 2026-09-01)
296
297Mozilla Public License 2.0. File-level copyleft: a modified CONTRA must
298ship its modified files, so what a user runs stays checkable against what
299we published β€” the same argument the whole product makes. Factor VI, not
300deferred.
301
302**The mechanical part, for when code lands:** MPL expects each source file
303to carry the Exhibit A notice. Add it as files are created rather than in
304a sweep afterwards, and if it is worth enforcing, enforce it as a test β€”
305this house does not rely on remembering.
306
307### 10. Money: free rung, then €5/year (Paul, 2026-09-01)
308
309Slopbox-inspired: a genuine free rung, paid starting at **€5/year**.
310Cheap on purpose β€” a password manager people cannot afford to keep is a
311password manager that loses their data. Integer cents; a float never
312touches 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
339The backlog lives in this repo's **discussions**, not in this file.
340**Discussion 18, "START HERE", is the map** β€” the two tracks (`web/*` for
341remote agents, `native/*` for local), the dependency order, and the
342claiming convention.
343
344Read it before claiming anything. In particular: amadan has no assign or
345claim verb yet (filed as `amadan/amadan` 36), so claiming is a **reply**
346on the discussion, and skipping that step is how two agents end up
347building 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
379Machine-authored text is marked πŸ€– where it surfaces. A person marker
380(πŸ‘¨/πŸ‘€/πŸ§‘) certifies human text an LLM must not rewrite, rephrase, condense
381or delete. AI features in the product are opt-in, always disclosed, and
382every path works with the AI switched off.
383
384## Deliberately not built yet
385
386Each 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