AGENTS.md
Rama main · réplica de lectura del repositorio original.
# Agent Operating Guide
This is the operating guide for AI coding agents working **in** this repository. For
self-hosting Reccado into a Cloudflare account (deployment, not code changes), see
[`SKILL.md`](SKILL.md). For human contributor setup and PR expectations, see
[`CONTRIBUTING.md`](CONTRIBUTING.md). Start with [`README.md`](README.md) for the product
overview.
This repository was implemented from `docs/IMPLEMENTATION.md` under senior-gated phases — do not
spend context rediscovering facts already validated in `docs/validation/PHASE0_VALIDATION.md` and
`docs/validation/PHASE1_VALIDATION.md`. Those two files are a **historical build-validation log**, not
current operating docs; treat their concrete IDs/URLs as examples from a past run, not live
state.
## Durable Invariants
These hold regardless of which task or phase you're on. Do not weaken them without an explicit,
informed decision from the user/supervisor.
### Data ownership and architecture
- The **mailbox Durable Object is the only component allowed to decide canonical mailbox state**.
D1 is a rebuildable cross-mailbox/control-plane index, never the source of truth — see
`docs/ARCHITECTURE.md` ("Why Not D1 as Trunk").
- Queue payloads are **metadata only**. Never put raw MIME, parsed HTML/text bodies, or attachment
content into a Queue message.
- Raw MIME and attachments live in **R2**, never as Durable Object SQLite blobs.
- MIME parsing happens in the Durable Object (or its ingest path), never in the `email()` handler
itself — the handler's only job is validate → write raw bytes to R2 → enqueue metadata.
- Ingest must be idempotent across Email Routing retries, Queue retries, and DLQ replay. A
duplicate delivery (same Message-ID or same raw SHA-256) must never create a second message row.
A reused Message-ID with a *different* raw body is a conflict to flag, not a silent overwrite.
- Parser failures must never drop the email: keep a message row with `parse_status='failed'` and
the raw R2 key preserved, and surface an ops event.
### Outbound sending
- **An agent (or any automated caller) may draft and summarize mail, but must never send without
explicit human confirmation.** The `request-send` → `confirm-send` flow with an idempotency key
is the only sending path; there is no shortcut. This is a security boundary, not a UX
preference — see `docs/ARCHITECTURE.md` ("Authentication and Security Model") and `SKILL.md`
("Hard invariant"). Repeated `confirm-send` calls with the same idempotency key must send at
most once.
### Legal / provenance
- Do not copy source, schema, text, or file structure from `mailflare`, `agentic-inbox`, or any
other external inbox implementation into this product. External repos may be read only for
behavior/pattern understanding and attribution notes — reimplement from first principles.
- If evaluating an external reference implementation (clone it to verify behavior, deploy it to
compare), clone only to a disposable path (e.g. `/tmp`), never import its code, deploy only
under a disposable Worker name, and treat real edge-auth enforcement (an
unauthenticated request must `302` to the provider's login page) as the only
acceptable evidence that auth actually works — an app-level "not configured" message is
not sufficient.
### Cloudflare resource safety
- Do not modify production Cloudflare resources. Use only dev/disposable resources for
exploration and validation.
- Document every Cloudflare resource you create (name, ID, URL) in your validation evidence.
- Delete disposable Cloudflare resources before finishing a task, unless the validation evidence
needs to persist and the supervisor explicitly approves keeping it.
### Tooling
- The Workers Vitest harness is configured in `vitest.config.ts` (via
`@cloudflare/vitest-pool-workers`, reading `wrangler.jsonc` `env.dev`). Do not remove it or fall
back to loading `vite.config.ts` for tests.
- Use `rg` for search. Avoid broad full-repo reads unless the task genuinely requires it.
- Use normal editor tooling (`apply_patch` or equivalent) for source edits; do not generate
repository files via ad hoc shell heredocs, except for temporary evidence files kept outside
the repo.
- Do not commit unless the user/supervisor explicitly asks, or the current instruction says to
create a checkpoint commit.
### Evidence and reporting discipline
- Never write "works" or "seems fine" without evidence. If something cannot be validated, stop
and report `FAIL` with the exact blocker — don't guess.
- Every validation/gate result should include: explicit `PASS`/`FAIL`, the commands run and
meaningful output snippets, files changed, Cloudflare resource names/IDs touched, URLs tested,
example payloads (for email/queue/API/WebSocket/scheduled-handler work), logs or screenshots
when relevant, the duplicate/idempotency result when relevant, and known failures with whether
they're blocking.
- End an agent run that did implementation work with a handoff in this shape:
```text
RESULT: PASS|FAIL
SCOPE: <phase/milestone or task>
FILES CHANGED:
- path
COMMANDS:
- command -> key output
RESOURCES:
- name/id/url
EVIDENCE:
- concise bullets
BLOCKERS:
- none, or exact blocker
NEXT:
- recommended next step, not implementation beyond scope
```
## Build/Process Notes
This section is current-state choreography — how to start a session and what's true *right now*.
Unlike the invariants above, these are expected to go stale; if you notice a mismatch with the
actual repo state, fix this section rather than trusting it blindly.
### Current baseline
- Phase 1 (Tier A inbox) is senior-validated; see `docs/validation/PHASE1_VALIDATION.md` for the historical
record. The transactional API (Phases 2–4 of `docs/plans/transactional-api.md` — HMAC/pepper API
keys, auth + owner-gated key/template admin, `POST/GET /v1/.../transactional/...` with Bearer
auth, mandatory `Idempotency-Key`, scopes, quotas, limits, test-key rejection for sending,
`unknown` → no auto-retry, redacted logs, non-authoritative D1 projections) is implemented in
`src/lib/transactional-*`, `src/do/transactional-*`, D1 migrations `0006`/`0007`, and covered by
`tests/unit/transactional-*.test.ts` + `tests/integration/transactional-*.test.ts`. The
MCP endpoint (`/mcp`, Better Auth OAuth + owner registry, read/search/draft only) is implemented in
`src/mcp/*` (no send tool). Tier B proper (Workflows, EmailAgent drafting, RAG/Vectorize, AI
Gateway) has not started — do not claim it. Cloudflare Email Sending lifecycle events are
consumed by the `inbox-mcp-email-events` Queue and hard-bounce/complaint suppressions are
enforced in the mailbox DO. The per-domain Email Sending event subscription is now created and
verified by `pnpm setup:sending` (wrangler ≥ 4.127.1 exposes `--source email.sending`), checked
against the live provider lists by `pnpm doctor --cloud`, and its actual liveness is reported
per sending domain in `/api/health` → `dependencies.sendingFeedback` and on each transactional
status response as `deliveryFeedback`. A domain with no subscription produces no events at all,
so **never read the absence of a delivery event as evidence about a message without checking
that domain's feedback liveness first** — see `src/lib/feedback-liveness.ts`.
- Deploys go through `scripts/deploy.ts` (`pnpm run deploy:dev` / `pnpm run deploy`, `--dry-run`
to preview): build, then overlay the gitignored `wrangler.generated.<env>.json` onto
`dist/server/wrangler.json`. The generated file wins only for the fields it owns (vars, D1 id,
sender allow-list, a `setup:domain` route) — `scripts/lib/deploy-config.ts`; `setup:domain`
and `setup:cloud` use the same overlay. Never deploy with raw `wrangler deploy --env <env>`.
- Transactional API current gap: no simulated delivery sink for test keys; test keys are rejected
by the production send path. `reconcileStaleTransactionalRequests` is wired into the hourly
cron and an auth-protected operator endpoint, and unknown outcomes remain manual-review-only.
- A security-hardening pass on top of Phase 1 is current/recent work: debug endpoints fail closed
by default, attachment/raw downloads get hardened response headers, dev-data seeding requires
explicit opt-in, an optional `OWNER_BOOTSTRAP_EMAILS` owner bootstrap exists, inbound size is
capped, and mutating `/api/*` routes get an Origin-check CSRF defense. See `SECURITY.md` for the
current posture and `CHANGELOG.md` for what's landed.
- The web perimeter is **Better Auth inside the worker** (Better Auth 1.7.2): `/login` with e-mail
OTP (registration closed to the owner registry ∪ `OWNER_BOOTSTRAP_EMAILS`), pairing-code rescue
via `wrangler d1 execute`, `/mcp` over OAuth + `jwt()`, session cookie cache (5 min) with a
confirm-send bypass, and migrations `0016`–`0018`. `BETTER_AUTH_SECRET` is set for the
maintainer's dev environment; auth fails closed outside `localhost` when it is unset.
`pnpm setup:auth` machine-generates and uploads the secret and offers the WAF rate-limit rule
for `/api/auth/*`; `scripts/setup-access.ts` and `scripts/smoke-access.ts` are deleted.
- D1 **is** in the inbound/outbound hot path as the cross-mailbox index (`message_index`,
`ingest_events`, `outbound_sends`, `ops_events` are written on every ingest/send; transactional
key/request projections are written on admin ops and send outcomes) — it is not authoritative
(the mailbox Durable Object is), but it is no longer merely "bound and unused."
- For control-plane `/api/*` access from the terminal (creating domains, mailboxes, aliases,
templates, API keys), use `pnpm operator login --env <env> --host <host> --email <owner>` and
the session helpers in `scripts/lib/operator-session.ts` — not a browser, and not hand-minted
pairing codes plus `curl`. Run `pnpm operator logout --host <host>` when done.
- To onboard a whole product (sending names, routing rules, domain/mailbox/aliases, templates,
keys into 1Password), use `pnpm onboard --env <env> --manifest <file>` — dry-run by default,
`--apply` to perform; see `docs/OPERATIONS.md` ("Onboarding a product"). It never deploys.
- For the Telegram bridge (bot identity, rebinding the chat, forum topics per mailbox, delivery
checks) use `pnpm operator telegram status|rebind|topic|topics` and `pnpm smoke:telegram` —
never raw D1 on `runtime_config` / `telegram_topics`; see `docs/OPERATIONS.md` ("Moving the
Telegram bridge to a forum").
- Do not re-run a full Cloudflare-resource preflight unless the assigned task actually touches
Cloudflare resources or auth. Do not advance beyond the assigned spike/milestone/task —
validation gates are blocking.
### Known dev Cloudflare resources (maintainer's environment — placeholders, fill in your own)
- Account: `<your-cloudflare-account-id>`.
- Dev Worker: `reccado-dev`.
- Dev URL: `https://reccado-dev.<your-subdomain>.workers.dev`.
- R2 bucket: `inbox-mcp-raw-dev-eu` (created with `--jurisdiction eu`).
- Queue: `inbox-mcp-inbound-dev`.
- DLQ: `inbox-mcp-inbound-dlq-dev`.
- D1 database: `inbox-mcp-index-dev-eu` (created with `--jurisdiction eu`), id `<your-d1-database-id>`.
- Email Routing rule: `test@example.com -> reccado-dev`.
- Dev test mailbox: `test@example.com` resolves to the fixed dev-fixture id
`DEV_TEST_MAILBOX_ID` (`src/db/seed-dev.ts`, seeded by `pnpm seed:dev-id`), so the dev server,
the debug endpoint and the smoke scripts all agree without talking to each other. Real mailbox
IDs are **random**, assigned by the `INSERT` that creates the row, with D1 as the only source of
truth (`src/lib/mailbox-id.ts`) — nothing derives them from a secret. The literal `mbx_test`
you'll see in some smoke commands (e.g. `pnpm smoke:ws .../mailboxes/mbx_test/ws`) is just an
arbitrary Durable Object name used for WebSocket echo testing and is unrelated to D1-routed
mailbox identity.
### Fast preflight
Run only this baseline preflight at the start of a task:
```sh
git status --short --untracked-files=all
git log -1 --oneline
node -v
pnpm -v
pnpm wrangler --version
```
If the task touches Cloudflare resources, add:
```sh
pnpm wrangler whoami
pnpm wrangler deployments list --name reccado-dev
pnpm wrangler r2 bucket list
pnpm wrangler queues list
pnpm wrangler d1 list
```
Do not list every Cloudflare service on every task. Do not browse Cloudflare docs unless the
assigned gate explicitly requires verifying a current limit or API shape.
### Current verified commands
These are expected to pass before checkpointing foundation changes:
```sh
pnpm wrangler types --env dev
pnpm run build
pnpm exec tsc --noEmit
pnpm test -- --run
pnpm smoke:ws wss://reccado-dev.<your-subdomain>.workers.dev/api/mailboxes/mbx_test/ws
pnpm smoke:email:local http://localhost:3001 fixtures/mime/simple-text.eml
```
The local smoke commands require a running dev server. If port `3000` is occupied, Vite may bind
to `3001` (or another port); use the port printed by `pnpm dev`.
### Implementation reading discipline
- Read only the assigned section of `docs/IMPLEMENTATION.md` plus nearby definitions needed for
that section, and `docs/ARCHITECTURE.md` only for the architecture decisions relevant to the
assigned work.
- Prefer existing local patterns in `src/server.ts`, `src/do/mailbox-do.ts`, `src/cloudflare/*`,
and `src/lib/*` over inventing new conventions.