# 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 `::`. 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:::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`.