carlos / contra Public

intent: write down the architecture before writing any code

CONTRA is a password manager, so the one rule has to be stricter than the
family default: CARLOS A1 strict, not A2. Eleven's sharing model is what
makes Eleven an A2 app, so the vault model is Slopbox's instead —
instance-per-account, membership as access control on your own instance
rather than a content-derived identifier at rest.

Records the decisions taken with Paul today, each dated and attributed
with its rejected alternative, including the deliberate reversal of two
settled family rulings: recovery here is a passphrase plus print-once
codes, because a password manager cannot store its own recovery
credential in a password manager.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 9d85b3ebb4525052babd712be90a0442efa4c2f5
5 files changed, +443 −0
  • .claude/settings.json +24 −0
  • AGENTS.md +232 −0
  • CLAUDE.md +2 −0
  • PLAN.md +99 −0
  • README.md +86 −0
diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 0000000..7a1468f
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,24 @@
+{
+ "hooks": {
+ "Stop": [
+ {
+ "hooks": [
+ {
+ "command": "amadan ledger sync-transcript",
+ "type": "command"
+ }
+ ]
+ }
+ ],
+ "UserPromptSubmit": [
+ {
+ "hooks": [
+ {
+ "command": "amadan ledger add-prompt",
+ "type": "command"
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..ab70793
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,232 @@
+# AGENTS.md
+
+🤖 Guidance for everyone — human or agent — changing this repository. This
+is the canonical instructions file; tool-specific files (`CLAUDE.md`) are
+pointers to it, so keep it current *here*.
+
+What CONTRA *is* lives in `README.md`; the protocol in `DESIGN.md`; the
+roadmap and its reasoning in `PLAN.md`. This file is how we build and the
+decisions we have committed to.
+
+CONTRA is a member of the CARLOS app family (`carlosframework/platform`);
+see that family's `carlos:building-carlos-apps` skill for the shape it
+follows. Where this file and the skill disagree, this file wins.
+
+# CONTRA
+
+An end-to-end-encrypted password manager. The server is host-blind: it
+holds ciphertext, public keys, and opaque ids, and nothing else.
+
+## The one rule
+
+**Absolutely nothing is revealed.**
+
+CONTRA is **CARLOS A1** — host-blind, in the strict sense the family
+publishes at carlosframework.com:
+
+> Keys live on the user's own devices; the instance never holds one that
+> opens user content. The only residue is transport shape: who connects
+> when, object sizes, timing. **No content-derived identifiers at rest.**
+
+Not A2. A2 permits an enumerated application-level residue published in
+the open, and it is where Eleven sits — precisely because Eleven's thread
+membership is visible to the server. CONTRA does not get that allowance.
+If a change would put a content-derived identifier in SQLite, it is wrong.
+
+The instance's whole SQLite file is continuously replicated to object
+storage, so **a plaintext column is a plaintext leak**. Metadata — item
+names, URLs, usernames, folder structure, vault names — seals client-side
+exactly like values do.
+
+Enforced, not promised: the end-to-end suite pushes canary strings through
+every write path and greps the raw SQLite bytes *and* the bucket objects
+for them. Keep that test merciless. A new storage surface lands with its
+canary or it does not land.
+
+## Settled decisions (don't relitigate casually)
+
+Each is dated and attributed. Rejected alternatives are recorded beside
+them. These change with new facts, dated and attributed — not in a
+drive-by.
+
+### 1. Trust class: A1 strict, on the Slopbox shape (Paul, 2026-09-01)
+
+Instance-per-account. A shared vault lives on its owner's instance; other
+members are identities on that instance holding the vault's group key
+wrapped to their own public key.
+
+**Rejected: Eleven's sharing model as-is.** It was the starting proposal
+and it is the thing that makes Eleven an A2 app. Membership of a thread on
+a shared multi-tenant instance is a content-derived identifier. Membership
+of a vault on *your own* instance is not — it is the access-control list
+the instance must consult to decide who it serves sealed bytes to, which
+is transport shape. That distinction is load-bearing; if a change erodes
+it, the class claim goes with it.
+
+The consequence we accept: revocation is Slopbox's honest three-part
+answer — credentials die immediately, the group key rotates forward for
+new and changed items, and **nothing already downloaded is un-downloaded**.
+No copy pretends otherwise.
+
+### 2. Recovery: a passphrase and print-once codes (Paul, 2026-09-01)
+
+A passphrase signs in to the web console. Recovery codes are shown
+**exactly once**, at signup. Lose both and the data is gone — no restore,
+no operator escape hatch, ever.
+
+**This reverses two dated family decisions**, deliberately:
+
+- Eleven, *Paul 2026-07-25*: "recovery is a passkey, never a passphrase or
+ a printed code." The paper kit was killed and the passphrase-wrap
+ primitive deleted the same day.
+- Slopbox, *2026-07-26*: "the mandatory paper recovery kit is gone;
+ passkeys hold the keys."
+
+**Why it is right here and nowhere else in the family:** a password
+manager cannot store its own recovery credential in a password manager.
+The family's recovery passkey lives in iCloud Keychain or 1Password —
+which is the exact product CONTRA replaces. The circularity is worst for
+the person migrating off 1Password, who is our first user. Paper is back
+because paper is the only medium that is not the thing we are replacing.
+
+Secondary reason, not the deciding one: the 1Password Emergency Kit means
+this pattern is already understood by the audience.
+
+### 3. Email exists only as a blind index (Paul, 2026-09-01)
+
+Kass's shape: an HMAC under a server-held pepper. It proves identity for
+sign-in and **can never decrypt anything**. There is no stored address
+column and no provider user-id. Email-gated key escrow is the forbidden
+shortcut and stays forbidden — a compromised server plus a passed email
+gate must still yield nothing.
+
+Direction of travel (Paul, 2026-09-01): email + magic link for signup,
+then passkey for quick sign-in thereafter, is intended to become the
+**CARLOS default**. CONTRA is where it gets built first. Build it so it
+can be lifted.
+
+### 4. Passkey + PRF is a fast unlock, never the only door (Paul, 2026-09-01)
+
+A third wrap of the same key material, not a shortcut around it. The
+failure that forced Eleven's rethink — the macOS ceremony sheet reaching
+only iCloud Keychain, and 1Password's macOS provider returning a PRF that
+does not match its own extension's for the same credential — means a
+present passkey can still fail. Every screen that offers it must offer the
+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 —
+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.
+
+### 6. Build order (Paul, 2026-09-01)
+
+1. rastrillo instance + **client-shape** web console — sealing happens
+ where the keys are, so this surface is client-shape by force.
+2. Native Mac app — the *second* implementation of a settled contract.
+3. AutoFill Credential Provider extension (macOS).
+4. iOS.
+
+The protocol is proven once, in one place, before a second client exists.
+
+### 7. Import is 1PUX, and it is client-side (Paul, 2026-09-01)
+
+1Password's 1PUX export for v1: it carries item types, TOTP seeds,
+attachments and vault structure, where CSV loses nearly all of it. The
+file is parsed and sealed **entirely in the browser**. An unsealed export
+touching the server would be an A1 violation on day one.
+
+Stated honestly in the copy: **passkeys stored in 1Password cannot be
+exported by anyone**, us included. That is 1Password's limit, not ours,
+and it needs a manual migration path rather than a pretence.
+
+Bitwarden and Apple Passwords importers are wanted, later — tracked as
+discussions on this repo, not built now.
+
+### 8. Ledgers on, at `prompts` level (Paul, 2026-09-01)
+
+Prompt ledger and carbon ledger both enabled (`amadan ledger init -level
+prompts`). Not `dialogue` or `full`, and the reason is this product: a
+fuller level publishes more of what passes through a session working on a
+password manager.
+
+**The law that goes with it: no real secret ever enters a prompt, a test
+fixture, a commit, or a canary.** Fixtures are generated. If a real value
+is ever typed, `amadan ledger redact` / `amadan ledger withhold` are
+routine tools here, not emergency ones — use them the same day.
+
+### 9. Money: free rung, then €5/year (Paul, 2026-09-01)
+
+Slopbox-inspired: a genuine free rung, paid starting at **€5/year**.
+Cheap on purpose — a password manager people cannot afford to keep is a
+password manager that loses their data. Integer cents; a float never
+touches an amount.
+
+## Build and test
+
+- Go, one static binary, `CGO_ENABLED=0`, thin `main.go` dispatch.
+- **rastrillo** is the framework (`github.com/carlosframework/rastrillo`) —
+ its packages exist to be used, not hand-rolled. Read the repo's own
+ `SKILL.md` first; it postdates most models' training data. Honest note
+ carried from the family: rastrillo does not yet support the E2EE app
+ story at framework level, so the sealed flow is app-side, built on the
+ Keymail/Kass/Slopbox patterns. Don't claim the framework does it.
+- **Stdlib first.** Adding a dependency is a decision, not a default.
+- `modernc.org/sqlite`, WAL (`busy_timeout` set *before* `journal_mode`),
+ `SetMaxOpenConns(1)`, additive-only migrations. New code over an old DB
+ must always be safe. Never delete data to update.
+- `go test ./...`, `go vet ./...`, `gofmt -l .` — clean, always. Nothing
+ lands untested; a bug fix carries its regression test. Never pipe a test
+ run you are gating on: a pipe eats the exit code.
+- Crypto is byte-identical in Go and JS, pinned by shared golden vectors.
+ Change one side and you must change the other, plus the context strings.
+- JS: small self-contained ES modules, no build step, `go:embed`, **300
+ lines per module enforced by test**, caps ratchet down only. Never
+ `innerHTML` untrusted content — build DOM nodes.
+- The merciless greps (above) run in the end-to-end suite.
+
+## Workflow
+
+- **Worktree per session, always.** Never build in the main checkout;
+ reading it is fine, writing is not.
+- **On amadan the branch is the pull request.** Take a worktree, take a
+ branch, push it, and describe it *before* the first edit:
+ `amadan branch describe <branch> -body -`. The branch page is the only
+ view of your work anyone gets — keep the description true, put the plan
+ in Tasks, and put questions and decisions in Discussions.
+- Agents post as their agent account (`claude~paul@keymail.dev`, via
+ `AMADAN_HOME`), never as the human. The 🤖 badge is not optional.
+- Squash-merge to main only when all three hold: the work is done, a human
+ has approved it, and CI is green. Never commit to main.
+- Commit subjects are area-prefixed prose that says why. Every commit
+ carries a `Co-Authored-By:` trailer; the human is the author of record.
+- Review happens on a deployed canary, never on localhost. Report the
+ clickable canary URL in every reply while iterating.
+- **Never echo a secret, a claim link, or a recovery code into a session.**
+
+## AI authorship
+
+Machine-authored text is marked 🤖 where it surfaces. A person marker
+(👨/👤/🧑) certifies human text an LLM must not rewrite, rephrase, condense
+or delete. AI features in the product are opt-in, always disclosed, and
+every path works with the AI switched off.
+
+## Deliberately not built yet
+
+Each with the trigger that would start it.
+
+| Not built | Trigger |
+|---|---|
+| Bitwarden and Apple Passwords importers | 1PUX import is shipped and a real user asks |
+| Passkey migration out of 1Password | A documented manual path exists first; automation only if the export limit ever lifts |
+| iOS app and its AutoFill extension | The Mac app and its extension are shipped and settled |
+| Cross-instance vault sharing (federation) | Never in v1 — the biggest scope trap; revisit only after single-instance sharing is proven |
+| 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 |
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..8e988a8
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,2 @@
+See [AGENTS.md](AGENTS.md) — the canonical instructions for anyone, human
+or agent, working in this repository. Keep guidance there, not here.
diff --git a/PLAN.md b/PLAN.md
new file mode 100644
index 0000000..590855b
--- /dev/null
+++ b/PLAN.md
@@ -0,0 +1,99 @@
+# 🤖 PLAN — why CONTRA is shaped this way, and what lands when
+
+`DESIGN.md` is the spec. This is the reasoning and the order.
+
+## Why build this at all
+
+1Password is genuinely disappointing people. The complaints are not about
+cryptography — they are about a product that grew away from the thing it
+was for. That leaves an audience who already believe in password managers,
+already pay for one, and are actively looking. They arrive with an export
+file and a grievance, which is the easiest migration in software.
+
+The second reason is that the family needs it. The CARLOS apps keep
+reaching for a password manager as the place recovery credentials live —
+and every one of those references is a dependency on a product none of us
+control. A family password manager closes that loop.
+
+## The position
+
+Everything else in the category competes on features. CONTRA competes on a
+sentence that its competitors structurally cannot say: **the operator sees
+nothing at all** — not your vault, not your item names, not your email
+address, not how many vaults you have.
+
+That sentence is only worth saying if it survives audit, which is why the
+class is A1 strict and why the greps are merciless. The moment we need an
+"except…" clause, we are an A2 app competing on features again.
+
+## Build order, and why this order
+
+**1 — Instance + client-shape web console.**
+The crypto and the storage contract get proven exactly once, in the place
+where they can be iterated fastest and reviewed on a canary the same day.
+This surface is client-shape by force: sealing must happen where the keys
+are, so a server-rendered vault is not a thing that can exist. Marketing
+and settings pages stay server-shape beside it.
+
+Done when: signup, passphrase sign-in, recovery-code print, a working
+vault, and the merciless greps green.
+
+**2 — Native Mac app.**
+Fully native, and deliberately *second*. A second client written against a
+settled contract is a port; a second client written against a moving
+contract is two half-designs. Everything the Mac app needs from the server
+already exists by then.
+
+**3 — AutoFill Credential Provider extension (macOS).**
+This is where the product stops being a website. It is also where
+distribution gets decided early rather than late: an
+`ASCredentialProviderExtension` means a paid Apple developer account, code
+signing, notarization, and a Sparkle-versus-App-Store call. That decision
+constrains the Mac app, so it gets made at slice 2, not slice 3.
+
+**4 — iOS.**
+Same contract, third implementation, plus its own AutoFill extension.
+
+## Import
+
+1PUX first, client-side, because that is where the users are coming from
+and because an unsealed export touching the server would break the one
+rule on day one.
+
+Bitwarden and Apple Passwords are wanted and deferred to discussions on
+this repo. Passkeys need a documented manual path: 1Password will not
+export them, so no importer can ever bring them across, and the honest
+move is to tell the user exactly which ones need re-creating rather than
+silently dropping them.
+
+## Money
+
+A real free rung, then **€5/year**.
+
+Deliberately cheap, for a reason specific to this category: a password
+manager somebody stops paying for is a password manager that loses their
+data. Price it so lapsing is never the cheaper option. Shape follows
+Slopbox — free rung that degrades generosity, never availability.
+
+Metering from the first write; integer cents.
+
+## Deferred, with triggers
+
+The full list is in `AGENTS.md` under "Deliberately not built yet". The
+two worth naming here:
+
+- **Cross-instance sharing (federation).** Not in v1, and the family has
+ already called this the biggest scope trap once. Single-instance sharing
+ gets proven first.
+- **Breach and reused-password monitoring.** Every implementation sends
+ something derived from a password off the device. There may be an
+ A1-compatible design; until somebody writes it down, it is not sketched.
+
+## Open questions
+
+- The recovery code's exact strength, alphabet and chunking — under review.
+- Whether the daemon's `use` verb may make outbound requests itself or is
+ limited to injecting into a subprocess environment — under review.
+- Namespace: `contra` is taken on npm and as a GitHub org, and the good
+ short domains are held. No password manager uses the name. See the
+ namespace note in the branch discussion.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b794f64
--- /dev/null
+++ b/README.md
@@ -0,0 +1,86 @@
+# CONTRA
+
+🤖 *This project — code and documentation — was written by an LLM (Claude),
+on the ideas, instruction, and editing of humans, per
+[factor X](https://11factor.org/#x).*
+
+**A password manager where the server knows nothing about you.**
+
+Not "we promise not to look." Not "we encrypt at rest." The server that
+holds your vault cannot open it, cannot tell you apart from anyone else,
+and does not know your email address. If it is stolen tomorrow, the thief
+gets a pile of ciphertext and the times of day you connected.
+
+CONTRA is a [CARLOS](https://carlosframework.com) app, built to the
+strictest class the family publishes — **A1, host-blind**: keys live on
+your devices, the instance never holds one that opens your content, and
+nothing derived from your content is stored in the clear. Item names,
+URLs, usernames, folder structure and vault names are sealed exactly like
+the passwords are, because a name is a secret too.
+
+## Honest trade-offs — read before trusting it
+
+This is the part most password managers put in a support article. It goes
+here instead.
+
+- **If you lose your passphrase *and* your recovery codes, your data is
+ gone.** Permanently. We cannot help you, and neither can anyone running
+ a CONTRA server. There is no reset link, no support override, no
+ "verify your identity and we'll restore it." That is not a gap in the
+ product — it is the product. A server that could give your vault back
+ is a server that could give it to someone else.
+- **Your recovery codes are printed once and never shown again.** Print
+ them, put them somewhere physical. They cannot live in a password
+ manager, because *this* is the password manager.
+- **Removing someone from a shared vault does not un-give what they
+ already have.** Their access dies immediately and the vault's key
+ rotates forward, so they see nothing new. What they already downloaded
+ is already theirs. Nobody's design changes that; ours says so.
+- **The server still learns transport shape** — that someone connected,
+ roughly when, and roughly how big the reply was. That is the residue A1
+ permits, and we are not going to pretend it is zero.
+- **Passkeys stored in 1Password cannot be exported.** Not by us, not by
+ any tool — 1Password does not let them out. Everything else in your
+ vault comes across; those need to be re-created by hand, and we will
+ show you which ones.
+- **It is early.** What is built and what is not is in `PLAN.md`, with
+ triggers for each deferral rather than vague intentions.
+
+## Leaving 1Password
+
+CONTRA imports 1Password's `.1pux` export — items, types, TOTP seeds,
+attachments and vault structure. **The file never reaches our server**: it
+is opened, parsed and sealed in your browser, and only ciphertext is
+uploaded. Bitwarden and Apple Passwords importers are wanted and not built
+yet.
+
+## How you get in
+
+- A **passphrase** signs you in to the web console.
+- **Recovery codes**, printed once at signup, are the way back if the
+ passphrase is lost.
+- A **passkey** on a device you trust is a fast unlock on top — a
+ convenience, never the only door. The passphrase is offered beside it on
+ every screen, because passkey ceremonies fail in ways that are not the
+ user's fault.
+
+Your email address is stored only as a blind index — a keyed hash. There
+is no address column in the database. It proves you are you, and it can
+never decrypt anything.
+
+## Agents
+
+CONTRA can be driven by an AI agent, deliberately and narrowly. An agent
+can **list** what is in a vault (names, sites, usernames), **use** a
+secret without ever receiving it — the local daemon does the authenticated
+thing on the agent's behalf — and **add** new secrets, which needs no
+unlock at all. An agent cannot read a value into its own context. There is
+no setting that turns that on.
+
+## Status
+
+Pre-alpha. Nothing to sign up for yet.
+
+## Licence
+
+See `LICENSE`. Factor VI is not optional, only deferred.