The previous round (88f0923) threaded a `backend_id` from `BACKEND_ID`/`HOSTNAME`
env vars into `AppState`. That required the operator to manually align each
instance's `BACKEND_ID` with the Cloudflare Worker's `BACKEND_ORIGINS_JSON`, which
is exactly the kind of manual cross-config sync that drifts.
The request's own `Host` header already carries the answer: the Worker load
balancer (`ARCH-LB-WORKER`) preserves the inbound `Host` while rewriting the target
to a backend origin (`joinUrl` builds `origin + path + query`), so `Host` is the
public address the LB routed this instance as. `backend_label_from_headers` reads
it per-request in `setup_status`, trims the default https/http port
(`example.com:443` -> `example.com`) so the label mirrors a bare origin, and returns
`None` when the request carries no usable `Host`.
This removes the startup env entirely — `backend_id_from_env`, `AppState::backend_id`,
all five router-builder `backend_id` parameters, the `.env.example` `BACKEND_ID`
entry, and the `BACKEND_ID` env documentation are all gone. Handlers still never
touch the environment: `Host` is request-scoped, not process-global, so there is no
test race (the `debug_surface_requested` rationale was about env, not headers).
Tests: `setup_status_reports_backend_label` now sends `Host: edge-1.example.com:443`
and asserts the label is the bare origin `edge-1.example.com` (port trimmed). A new
`setup_status_reports_null_backend_without_host` asserts `null` when no Host is sent.
GATE-P0 106 passed / 0 failed / 4 ignored; GATE-DOCS 6/6 clean (49 line-refs).
`/api/status` now echoes a `backend` field alongside `version`. The baked
`BUILD_VERSION` is identical across every instance built from the same image bath,
so it answers "did this deploy land?" but not "which origin answered?" when a
probe goes through the Cloudflare Worker load balancer (`ARCH-LB-WORKER`) — and the
Worker's `x-lb-backend` header is unreliable (it is silently dropped when the
response headers are immutable, e.g. cross-origin without CORS). Having the backend
self-report in the body removes that dependency entirely.
The value is a per-instance observability label read once at startup from
`BACKEND_ID` (falling back to `HOSTNAME`, which container runtimes set their
selves). It is not a connection secret and is never persisted (`C-REDIS-ONLY-STATE`).
`null` means neither env var was set — two such instances are indistinguishable by
this field by design.
Wiring keeps handlers off the environment (matching the `debug_surface_requested`
rationale: env is process-global and reading it inside a handler races tests):
`main` reads it once and threads `backend_id` through the router builders into
`AppState::backend_id`.
Also replaces the drift-prone `src/main.rs:<line>` anchors in design.md /
design.zh-CN.md with symbol references (`std::env::var` key names,
`LlmClient::with_runtime`), per the symbol-not-line rule; the insert had already
shifted them.
Tests: `setup_status_reports_backend_label` asserts the label round-trips into the
body. GATE-P0 105 passed / 0 failed / 4 ignored; GATE-DOCS 6/6 clean.
Code (src/config.rs 521->394, src/main.rs 232->200):
- Config::from_env() + required_secret / required_nonblank / env_bool deleted.
config.rs now performs no std::env read; all business fields come from Redis
business config (BusinessConfigWire -> validate_business_wire -> validate_nonblank).
- RUN_MODE deleted: variable, Config.run_mode field and the main.rs validation
block. The two modes never changed runtime behaviour -- webhook and reconcile
share one router, POST /reconcile is a standalone endpoint.
- jmap_password() accessor deleted; the field name is kept in
BusinessConfig / BusinessConfigWire because it is real data, not a stub.
- Config.port and Config.redis_url deleted: both had zero readers, and port was
hardcoded to 8080 in both constructors while the listener binds straight from
the PORT env var, so the field could silently disagree with the real port.
Config is now 6 fields: telegram / jmap / account_id / llm / auth / worker_token.
- redis_only() and from_business(value) signatures simplified accordingly.
- #[expect(dead_code)] count in src/ is now 0.
Test coverage: the from_env()-driven startup_rejects_blank_auth_secret case is
gone with the path it tested. The non-leak assertion it carried was reattached
to the real validation route (BusinessConfigWire -> TryFrom -> validate_nonblank)
with a stronger third assertion that the rejected value is not echoed either.
Docs: design.md 7.1 re-scoped to the Redis business config table; reference.md
5.1/5.3 rewritten (no legacy env path); deployment.md 5 and the runtime-mode
table rewritten; retired.md gains section 4 registering every deletion;
README x2, .env.example, charter.md updated.
GATE-P0: fmt / check / clippy -D warnings green, 61 passed / 0 failed / 1
ignored (was 62; the deleted case). GATE-DOCS: PASS 5/5, ANCHOR 247 line-refs /
0, SECTION-REF 216 / 0.
Code (2 sites):
- main.rs logged bootstrap_config.port, which is always the hardcoded 8080 in
the two-variable production config; the listener actually binds the port
parsed at main.rs:31-34. Log the bound port.
- That exposed Config.port as genuinely unread (clippy -D warnings), so mark it
with #[expect(dead_code, reason=...)] instead of silently deleting a
documented env-parsing site.
Docs, phantom identifiers (names that do not exist in src/):
- RuntimeConfig x3 -> OutboundConfig via RuntimeConfigProvider (state.rs:34/41)
- LlmClient::noop() x2 -> None in Option<Arc<LlmClient>> (main.rs:125)
- main.rs::validate_env_or_exit x2 -> config.rs:338 read, main.rs:75 validation
- TelegramChannel::send_message -> TelegramClient::send_text (channel.rs:134)
Docs, stale facts:
- LLM table: ENABLED and ALLOW_NET defaulted true,true -> false,false;
API_KEY, BASE_URL, MODE were "required" -> only required when enabled. This
contradicted design.md:385-387, which already had the correct values.
- design.md:560 said RUN_MODE "is actually consumed"; it is only read and
validated and never affects behaviour (main.rs:73-74).
- RUN_MODE operator docs (README, reference, deployment, .env.example) now say
both accepted values produce identical behaviour.
- retired.md: 5 intents -> 6 (/search shipped, worker.rs:517); 4 LLM env vars
-> 6; main.rs tracing events 6 -> 7.
- design.md:494 tracing count 6 -> 7, debug-gate warning added to the list.
- design.md:350 code-gap count 2 -> 1 (multi-instance window done in 6c99ce5).
- design.md:554 marks the 45-paused stage-0 baseline explicitly as historical.
- .env.example said the Redis ACL password was the bootstrap trust root; it is
CONFIG_ENCRYPTION_KEY (main.rs:88-91).
Docs, re-anchored after the main.rs shrink (6 refs shifted by -2).
GATE-P0 62 passed / 0 failed / 1 ignored; GATE-DOCS 4/4 clean
(228 line anchors, 211 section refs, 0 errors).