CLAUDE.md
Rama main · réplica de lectura del repositorio original.
# CLAUDE.md
Primary project guidance for agents working in this repository.
Detailed, current design contracts live under `docs/`. This file is intentionally a
short invariant checklist plus pointers; do not let it grow back into a second README.
## Start Here
- Architecture overview: `docs/architecture.md`
- Security model: `docs/security.md`
- Workers compatibility: `docs/compatibility.md`
- Redis key layout: `docs/redis-key-layout.md`
- Source ownership map: `docs/source-map.md`
- Module docs: `docs/modules/README.md`
- Protocol contracts: `docs/protocol-contracts.md`
- Testing contract: `docs/testing.md`
- Contributor reading path: `docs/contributing.md`
- Project-wide standards: `docs/project-standards.md`
- Workerd JS standards: `docs/workerd-js-standards.md`
- Rust sidecar standards: `docs/rust-sidecar-standards.md`
When changing behavior, update the owning module doc in the same change. When changing
cross-module Redis ownership, update `docs/redis-key-layout.md`. When changing source
ownership or permanent directories, update `docs/source-map.md`.
## Project Shape
WDL is a self-hosted multi-tenant Workers platform built on stock Cloudflare workerd.
It dynamically loads immutable worker versions from Valkey/Redis through workerd's
`workerLoader` API and implements the missing platform services around it: control,
auth, KV, R2, D1, Durable Objects, queues, cron, Workflows, ASSETS, service/platform
bindings, AI, observability, and lifecycle cleanup.
Core service families:
- `gateway`: public and admin-host ingress.
- `user-runtime` / `system-runtime`: workerd runtime pools with local redis-proxy
sidecars.
- `d1-runtime` / `do-runtime`: stateful workerd runtimes supervised by Rust PID 1
processes.
- `scheduler`: cron, queue, and workflow tick dispatch.
- `workflows`: database 2 workflow state machine.
- `control` / `auth`: static workers hosted in system-runtime.
Rust crates live under `rust/`; package and binary names match service names except the
shared crate `wdl-rust-common`.
## Core Invariants
- **No workerd fork.** Treat upstream workerd behavior as the base contract; WDL builds
around it.
- **Namespace is the account boundary.** Tenant namespace names follow the current
grammar in `shared/ns-pattern.js`; reserved namespaces are exact literals, not a
broad `__*` convention.
- **Worker identity is immutable version id.** Runtime loader keys are
`<ns>:<worker>:<version>`. Do not route hot requests by mutable active state inside
runtime.
- **Gateway owns route resolution.** Runtime takes `x-worker-id`; it does not keep a
route table or subscribe to route invalidations.
- **Control is the control-plane writer.** Do not add alternate Redis writers for
bundle metadata, routes, lifecycle indexes, secrets, or auth state without a module
doc update and tests.
- **Direct Redis/Valkey writes are repair-only.** They are not a supported consistency
path; readers should fail closed on malformed persisted state instead of adding
WATCH/MULTI protocols for arbitrary manual writes.
- **Redis round trips and atomicity are explicit design work.** Batch related
independent operations through typed, bounded helpers; pipelining does not replace
Lua, WATCH/MULTI, or exact-value CAS. See `docs/project-standards.md`.
- **Control/auth authorization is action-based.** `parseControlRoute()` assigns the
action; auth evaluates that action against `shared/auth-roles.js`. Do not infer
permission from URL prefix in handlers.
- **Admin/control naming split is intentional.** Client-facing literals use "admin"
(`X-Admin-Token`, `ADMIN_TOKEN`, admin host). Service-side code uses "control".
- **Secrets are encrypted at rest.** Secret hash values are `WDL-ENC:` envelopes.
Tenant worker env receives plaintext secret values only in the internal load envelope
after redis-proxy decrypts during `/runtime/load`. AI provider credentials use a
separate host-only path: redis-proxy decrypts the exact provider snapshot during
`/ai/resolve`, and only the runtime host binding receives that plaintext long enough
to attach it to the reviewed provider request. Env materializes in fixed precedence —
vars, then namespace secrets, then worker secrets — so a worker secret shadows a
namespace secret shadows a var on the same key. Control must keep the estimated full
workerLoader env — user vars/secrets plus runtime-injected binding env values such as
required caller secret copies — within WDL's headroomed workerd serialized env budget
before deploy/secret mutation, not let that fail later during cold-load. Workflow
identity stays exclusively in binding-scoped host props and is covered by the env
budget; generated tenant facade code carries only public operation fields.
- **DB split is intentional.** DB 0 is control metadata, DB 1 is data-plane KV/queue/log
streams, DB 2 is active Workflows, and DB 15 is an inactive schema-2 archive retained
until explicitly deleted after verified migration. See `docs/redis-key-layout.md`.
- **D1/DO correctness comes from owner lease + generation fence.** Service DNS only
reaches a router task; it does not prove ownership.
- **Workflows owns DB 2.** Control owns DB 0 workflow definitions (`wf:defs:*`);
Workflows additionally owns only the operator schema-migration state `wf:schema3-migration` in
DB 0 of its configured endpoint. Runtime/control talk to the workflows service instead
of writing DB 2 directly.
- **Queue main streams are not trimmed.** At-least-once delivery beats storage caps.
Diagnostic streams may be bounded.
- **Metrics labels must be bounded.** Namespace, worker, version, token id, raw key,
path, and error text belong in logs, not metric labels.
## Security Boundaries
- user-runtime loaded workers get public-only outbound from workerd config.
- system-runtime loaded `__system__` workers are privileged by design.
- Runtime internal `:8088`, D1 `:8787`, DO `:8788`, workflows `:9120`, and Redis are
private mesh services.
- Private mesh service calls require `x-wdl-internal-auth` with the shared
`WDL_INTERNAL_AUTH_TOKEN`; health and metrics endpoints are the only unauthenticated
service endpoints.
- Privileged runtime endpoints belong on `runtime/internal.js` and `:8088`, not on the
gateway-facing loader socket.
- Hidden backend Fetchers and internal auth tokens for D1/DO/workflows are platform
plumbing and must be stripped before tenant code observes `env`, request headers, or
tenant-realm facade state.
- Tenant-realm ambient state is never a security or correctness owner. Imported env,
nested `withEnv()`, `AsyncLocalStorage`, captured async frames, private Symbols, and
tenant-observable object graphs cannot own authorization, fencing, deduplication, or
invocation attribution. Private identity sets may authenticate one exact object, not
the invocation that later presents it.
- Retry provenance requires the same host-produced Error identity to escape a reviewed
Promise boundary whose current env owns its recorded source capability. Catch/fallback
or detach does not report the current boundary; replacement objects do not inherit
provenance. See `docs/security.md` and `docs/workerd-js-standards.md`.
- Compatibility flags are scoped design tools, not categorical bans. Evaluate them
proactively for platform-owned static workers when they simplify a boundary, but
conservatively for tenant Dynamic Workers; audit and prove the enabled configuration in
real workerd. See `docs/compatibility.md` and `docs/security.md`.
- Tenant-running Fargate task roles must stay least-privilege; tenant code must not
receive broad cloud credentials through task metadata.
## Refactor Discipline
- Pick one deployable boundary per change when possible.
- Keep behavior and docs in the same staged unit.
- Prefer current local patterns over new abstractions. Consolidating a duplicated
cross-cutting primitive into its single `shared/` or `wdl-rust-common` owner is
convergence, not a new abstraction.
- Do not add fake old/new protocol fallback for in-tree tiers that ship together unless
there is a real external rollout requirement.
- Unless an owning contract explicitly says otherwise, WDL software, runtime, config,
schema, and persisted-state evolution is forward-only and greenfield-oriented.
Downgrade instructions are best-effort operational guidance, not exhaustive or
guaranteed compatibility. Do not add backward-compatibility machinery solely to
support downgrade. This does not change product APIs that explicitly promote an
older immutable Worker version.
- Write active repository docs as final-state contracts, not as a history of branch
iterations or rejected approaches. Tests should protect observable behavior and
durable boundaries; do not pin a refactor's implementation shape unless that shape is
itself the contract.
- After authentication, in-tree tiers that ship together may trust ephemeral RPC output
owned by the peer; do not repeat full semantic validation at every hop. Keep strict
validation at tenant inputs, persisted-state readers, credential attachment points,
and external protocol boundaries. Enforce each invariant at one owning layer, and do
not pre-implement a rejection that workerd or the backing platform already performs
unless WDL has an independent product policy or must prevent an earlier side effect.
- If you change a route/body protocol, roll the side that accepts the new shape before
the side that sends it.
- If you change Redis ownership, update writers, readers, docs, and style-contract
tests in the same deployable boundary.
- Cross-language contracts (JS↔Rust grammars, wire literals, metric shapes) are
pinned by `tests/fixtures/*.json` read on both sides plus
`tests/unit/style-contracts.test.js`; change both sides and the fixture together.
- Use the explicit language baselines by default: JavaScript targets ES2025 on Node 24,
and Rust targets Edition 2024 on Rust 1.99.1. Do not reintroduce older spellings when
the repository standard already uses a modern API or syntax. Cross-language rules
live in `docs/project-standards.md`; language-specific rules live in
`docs/workerd-js-standards.md` and `docs/rust-sidecar-standards.md`.
- For docs: active English prose is hard-wrapped; active Chinese prose is not
hard-wrapped.
## Testing And Checks
Use the smallest check that protects the touched behavior, then widen when the blast
radius crosses module boundaries.
Common commands:
```bash
npm run lint
npm run lint:unused
npm run typecheck
npm run typecheck:strict
npm run typecheck:node
npm run compile:workerd
npm run test:unit
npm run test:integration
(cd rust && cargo fmt --all --check)
(cd rust && cargo clippy --locked --workspace --all-targets -- -D warnings)
(cd rust && cargo test --locked --workspace)
(cd rust && cargo deny --manifest-path Cargo.toml check)
```
Test helper and fixture selection, integration runner behavior, sharding, artifacts,
and flags are documented in `docs/testing.md`. When adding tests, prefer the existing
helper matrix and local harnesses over ad hoc response parsing, temp directories,
output capture, or fixture loading.
## Common Gotchas
- Never use `Number(x) || default` or `parseInt(x, 10) || default` when `0` is a valid
user value.
- Node's undici-backed `fetch()` strips the `Host` header; integration helpers use the
plain `http` module when gateway host routing matters.
- `bundleKey(ns, name, version)` stores by integer version:
`worker:<ns>:<name>:v:1`, not `:v:v1`.
- `ctx.props` is the cross-JSRPC caller-identity channel. Service bindings must read
caller identity from host-side props, not caller-controlled arguments.
- Host binding `WorkerEntrypoint` prototype methods are exposed over JSRPC; keep
host-only helpers in module functions or WeakMaps.
- `workerd serve` emits raw console stdout in addition to structured tail events. The
structured JSON log is the platform source of truth.
- Bundled workerd restores async response-body `ReadableStream.cancel()` when an
observed disconnect drops the native response pump. Cleanup is best-effort and can
be skipped during context teardown. Incoming `request.signal` defaults on at
compatibility date `2026-11-17`; earlier dates require `enable_request_signal`.
Do not assume disconnect propagation through every proxy or wrapper. Keep independent
stream deadlines and idle cleanup.
- Forwarding WebSocket `101` responses requires preserving `response.webSocket`; use the
shared response helper instead of wrapping with `new Response(body, init)`.
- workerd/capnp wiring traps apply to every tier's `*.capnp`, not just runtime: `workerd
serve` takes space-separated args; `external` is HTTP/TLS-only so plain TCP (Redis) goes
through a `network` service plus `connect()`; `network` HTTPS fetch needs `tlsOptions =
(trustBrowserCas = true)` plus `ca-certificates`; embedded module names cannot contain
`..` (hence flat names like `shared-redis`). Details live in `docs/modules/runtime.md`.