WORKER_CALLBACK_URL accepts any scheme, so a missing http:// boots green and breaks callbacks #164

Open
opened 2026-09-15 21:24:32 +02:00 by lz · 0 comments
Owner

Surfaced during PR #163 (zod v4) and deliberately kept out of it — correctly, since folding a validation-behaviour change into a dependency bump means a post-merge boot failure cannot be attributed to either. This is not a zod 4 regression: it behaves identically on zod 3.

The trap

z.url() requires only a scheme, not an HTTP one. Verified:

new URL('host.docker.internal:3001')  // VALID, protocol = "host.docker.internal:"
new URL('mailto:x@y.z')               // VALID, protocol = "mailto:"

So WORKER_CALLBACK_URL=host.docker.internal:3001 — a scheme-less value that looks like a host:port — parses as a URL whose scheme is host.docker.internal. Config validation passes, Nexus boots clean, and the value is injected into workers as NEXUS_URL.

Nothing fails until a worker calls notify-preview or notify-artifact against a URL that can never resolve. The operator sees a preview that never registers, with no error at the point of the mistake.

Why this is a likely mistake rather than a theoretical one

AGENTS.md's environment table describes WORKER_CALLBACK_URL as required on Docker Desktop (Windows/macOS) and gives the example http://172.30.0.2:3001. An operator transcribing a host and port without the scheme is an ordinary slip, and the config layer — whose entire job is to catch exactly this at boot — waves it through.

It is also the one env var where a silent failure is most expensive: it exists specifically for the deployment where the bridge-gateway derivation does not work, so there is no fallback behind it.

Fix

Constrain the scheme in config.ts:

WORKER_CALLBACK_URL: z.url({ protocol: /^https?$/ })

Consider whether public_url wants the same treatment — it feeds resolveHosts and the preview advertise chain, and its own validator already rejects wildcards, so scheme-checking is consistent with how that field is already handled.

Watch for

This changes what the container accepts at boot. An existing deployment with a scheme-less WORKER_CALLBACK_URL is currently booting and half-working; after this change it will refuse to start. That is the right outcome — it converts a silent runtime failure into a loud startup one — but it is a breaking change for anyone in that state, and the error message should name the variable and show a correct example rather than emitting a bare zod message.

PR #163 already ships a test pinning the current permissive behaviour, so whoever takes this will see it go red and know they are changing a known-and-recorded decision rather than an accident.

Note

Two related validation gaps were considered in #163 and also left alone, both pre-existing on zod 3 and both design decisions rather than migration ones: COOKIE_NAME= yields an empty cookie name, and DATA_DIR= puts SQLite at ''. Worth deciding on together with this, since the fix is the same shape (.min(1)).

Surfaced during PR #163 (zod v4) and deliberately kept out of it — correctly, since folding a validation-behaviour change into a dependency bump means a post-merge boot failure cannot be attributed to either. **This is not a zod 4 regression: it behaves identically on zod 3.** ## The trap `z.url()` requires only *a* scheme, not an HTTP one. Verified: ```js new URL('host.docker.internal:3001') // VALID, protocol = "host.docker.internal:" new URL('mailto:x@y.z') // VALID, protocol = "mailto:" ``` So `WORKER_CALLBACK_URL=host.docker.internal:3001` — a scheme-less value that *looks* like a host:port — parses as a URL whose scheme is `host.docker.internal`. Config validation passes, Nexus boots clean, and the value is injected into workers as `NEXUS_URL`. Nothing fails until a worker calls `notify-preview` or `notify-artifact` against a URL that can never resolve. The operator sees a preview that never registers, with no error at the point of the mistake. ## Why this is a likely mistake rather than a theoretical one AGENTS.md's environment table describes `WORKER_CALLBACK_URL` as **required on Docker Desktop (Windows/macOS)** and gives the example `http://172.30.0.2:3001`. An operator transcribing a host and port without the scheme is an ordinary slip, and the config layer — whose entire job is to catch exactly this at boot — waves it through. It is also the one env var where a silent failure is most expensive: it exists specifically for the deployment where the bridge-gateway derivation does not work, so there is no fallback behind it. ## Fix Constrain the scheme in `config.ts`: ```ts WORKER_CALLBACK_URL: z.url({ protocol: /^https?$/ }) ``` Consider whether `public_url` wants the same treatment — it feeds `resolveHosts` and the preview advertise chain, and its own validator already rejects wildcards, so scheme-checking is consistent with how that field is already handled. ## Watch for **This changes what the container accepts at boot.** An existing deployment with a scheme-less `WORKER_CALLBACK_URL` is currently booting and half-working; after this change it will refuse to start. That is the right outcome — it converts a silent runtime failure into a loud startup one — but it is a breaking change for anyone in that state, and the error message should name the variable and show a correct example rather than emitting a bare zod message. PR #163 already ships a test pinning the current permissive behaviour, so whoever takes this will see it go red and know they are changing a known-and-recorded decision rather than an accident. ## Note Two related validation gaps were considered in #163 and also left alone, both pre-existing on zod 3 and both design decisions rather than migration ones: `COOKIE_NAME=` yields an empty cookie name, and `DATA_DIR=` puts SQLite at `''`. Worth deciding on together with this, since the fix is the same shape (`.min(1)`).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lz/agent-nexus#164
No description provided.