# 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`; who it is for in `PRODUCT.md`; the
cryptographic protocol in `PROTOCOL.md`; the visual system in `DESIGN.md`;
the roadmap and its reasoning in `PLAN.md`. This file is how we build and
the decisions we have committed to.

`PROTOCOL.md` was called `DESIGN.md` until 2026-09-04. If you are reading a
discussion, a commit message or a code comment older than that, the two
names mean the same file.

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.

**Settled the same day (Paul, 2026-09-01): the kit prints an Account Key
too.** A 128-bit Account Key is mixed into the passphrase's KEK branch and
printed on the same client-generated PDF as the recovery code, because
passphrase-only unlock would have made the security of the replicated
bucket equal to the strength of whatever the user typed. Rejected: Option
B, passphrase-only plus compensating controls — a control that depends on
user behaviour is a hope with a progress bar.

A **fresh** device needs the kit, or a handoff from an enrolled one, so
"passphrase known, kit lost, every device gone" is unrecoverable. **That
is not a cost we are paying — it is the one rule applied without
exception** (Paul, 2026-09-01), and the copy states it without
apologising. The everyday case is unchanged: a device holding a cached
Account Key unlocks with the passphrase alone.

What it retires is a promise that was never quite true — "your passphrase
*or* your codes get you back in". The honest model is **the kit is the
root; the passphrase is the everyday convenience.**

The obligation is therefore not to soften the rule but to ensure nobody
meets it by accident: a three-rung ladder that never dead-ends, a
prominent reprint flow, and a nag to enrol a second device.

**The kit is two separate PDFs, not one sheet** — because the two values
have different blast radii. The Account Key alone is nearly harmless (one
input to the passphrase branch); the recovery code alone is full
takeover. One is a setup aid to keep handy, the other a bearer credential
to archive, and the risk we are managing is *storage* — people save PDFs,
and one file puts both in the same folder forever. `PROTOCOL.md` carries
both documents, the ladder, the reprint semantics and the loss table.

### 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 **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**.

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)

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. Licence: MPL 2.0 (Paul, 2026-09-01)

Mozilla Public License 2.0. File-level copyleft: a modified CONTRA must
ship its modified files, so what a user runs stays checkable against what
we published — the same argument the whole product makes. Factor VI, not
deferred.

**The mechanical part, for when code lands:** MPL expects each source file
to carry the Exhibit A notice. Add it as files are created rather than in
a sweep afterwards, and if it is worth enforcing, enforce it as a test —
this house does not rely on remembering.

### 10. 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.

## Picking up work

The backlog lives in this repo's **discussions**, not in this file.
**Discussion 18, "START HERE", is the map** — the two tracks (`web/*` for
remote agents, `native/*` for local), the dependency order, and the
claiming convention.

Read it before claiming anything. In particular: amadan has no assign or
claim verb yet (filed as `amadan/amadan` 36), so claiming is a **reply**
on the discussion, and skipping that step is how two agents end up
building the same thing overnight.

## 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.
- **The bootstrap exception, once.** "CI green" binds where CI exists. This
  repo has no CI script until `web/1` lands, so there is no green to wait
  for and the condition cannot be met by anyone. amadan agrees: its merge
  gate defaults to off — *"a gate nobody chose is not a gate"* — and
  reports such a repo's CI as `skipped`, "this repo has no CI script, so
  nothing can be green". So the first merges land on **done + approved**,
  and **adding CI is itself the first work item**, not a prerequisite that
  cannot be satisfied. The moment `.amadan/ci` exists the three-condition
  rule binds absolutely, and this paragraph is spent.
- 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 |
| 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 |
