Conversation
The Compose bundle cannot start. Its api container runs the `api` image, which sets NODE_ENV=production, and docker-compose.yml defaults FACILITY_PREVIEW_URL to http://preview.localhost:4400. Production validation refuses any preview origin that is not HTTPS, so readConfig throws before the API listens: FACILITY_PREVIEW_URL must use HTTPS in production The command in the Compose file's own header comment therefore never brings up an instance. Requiring HTTPS on loopback refuses a configuration that has no transport to protect: there is no name to obtain a certificate for and nothing is reachable off the machine. Exempt an instance whose every origin -- preview included -- is loopback HTTP, the same carve-out the interactive OAuth block already makes for its own URL set. Any origin that leaves the machine puts the whole set back under the HTTPS requirement, and the separate-registered-site rule is untouched: previews stay isolated from the control plane on loopback too. isLoopbackHostname now accepts names under `.localhost`, which RFC 6761 section 6.3 reserves for the loopback interface. Facility needs more than the bare name because the preview origin must be a registered site of its own. This widens two other call sites: FACILITY_INSECURE_DEV may be enabled on a `.localhost` control origin, and MCP_PUBLIC_URL may be HTTP there. Both already accepted `localhost` itself, and neither leaves the host. Regression coverage pins the shipped defaults rather than a paraphrase of them: the Compose environment boots, a published control plane with a loopback preview does not, a loopback control plane with a published preview does not, and the bare control hostname is still refused as a preview origin. The shipped .env.example is also parsed as dotenv delivers it and asserted to boot with only the master key filled in.
docker-compose.yml has carried the whole 0.12 control plane since theam#289, but no page describes it: the quickstart is the from-source path (corepack, pnpm install, pnpm dev) and production mentions the file in one clause. Someone evaluating Facility with Docker has nothing to follow. The new page is the single documentation page issue theam#19 asks for. It covers what the bundle is and is not, the one-shot migrate and runner-image services and why they read as exited, the surface table including the separate preview origin, the two GitHub applications -- OAuth App for sign-in, GitHub App for repository automation -- and binding the first owner with the operator CLI that already ships inside the api image. It also states the limits rather than leaving them to be discovered: GitHub cannot deliver webhooks to localhost, so UI and MCP triggers work untunnelled and repository triggers do not; the bundle runs TLS-free only while every origin stays loopback; and Facility never reclaims workspace volumes by age.
adrian-lorenzo
left a comment
There was a problem hiding this comment.
Thanks.
Please bind the Compose API and web ports to loopback before allowing HTTP. Localhost URLs do not restrict network access. Add a regression check for those bindings.
The HTTP carve-out this PR adds rests on every origin being loopback, and that premise was false. `ports: ["4400:4400"]` and `["3400:3400"]` publish on every interface, so a `localhost` URL constrained nothing: the bundle served control-plane sessions and preview traffic to the whole network segment in plaintext, and the PR description claimed the opposite. Both host ports are now bound to `127.0.0.1`. Container-to-container traffic uses the Compose network, so nothing the stack needs internally was published in the first place. Reaching the instance from another machine now requires changing the binding, which is the moment to put TLS in front of it rather than a property to discover afterwards. The regression lives beside the rule that depends on it, in `config.test.ts`: the premise and the carve-out were in different files, which is how the premise rotted unnoticed. It parses the shipped Compose file, fails on any published port that is not loopback-bound, and asserts which services publish at all so it cannot pass vacuously.
|
Addressed at aa4bf30. You are right, and worse than you put it: my description claimed nothing was reachable off the machine, while |
What changes
docker compose upstarts the bundle. Today it does not: the API exits duringconfig validation before it ever listens.
The
apicontainer runs theapiimage, which setsNODE_ENV=production(
Dockerfile:86), anddocker-compose.ymldefaultsFACILITY_PREVIEW_URLtohttp://preview.localhost:4400. Production validation refuses any previeworigin that is not HTTPS:
So the command in the Compose file's own header comment —
SECRET_MASTER_KEY=$(openssl rand -base64 32) docker compose up -d— cannotbring up an instance.
An instance whose every origin is loopback HTTP is now exempt from that
requirement. This is the carve-out the interactive OAuth block in the same
superRefinealready makes for its own URL set, applied to the preview originthat did not have it.
isLoopbackHostnameadditionally accepts names under.localhost, which RFC 6761 §6.3 reserves for the loopback interface —Facility needs more than the bare name because the preview origin must stay a
registered site of its own.
A second commit adds
self-host/bundle.md, the page that describes the path.Why
Requiring HTTPS on loopback refuses a configuration that has no transport to
protect: there is no name to obtain a certificate for and nothing is reachable
off the machine. Any origin that leaves the machine puts the whole set back
under the requirement, and the separate-registered-site rule is untouched —
previews stay isolated from the control plane on loopback too.
Blast radius, stated rather than buried.
isLoopbackHostnamehas three callsites. Two widen:
FACILITY_INSECURE_DEVmay now be enabled on a.localhostcontrol origin, and
MCP_PUBLIC_URLmay be HTTP there. Both already acceptedlocalhostitself, neither leaves the host, andFACILITY_INSECURE_DEVremainsrefused under
NODE_ENV=productionregardless.docker-compose.ymlhas carried the whole 0.12 control plane since #289, but nopage describes it: the quickstart is the from-source path and
production.mdmentions the file in one clause. The new page is the single documentation page
#19 asks for, and it states the limits rather than leaving them to be
discovered — GitHub cannot deliver webhooks to
localhost, the bundle isTLS-free only while every origin stays loopback, and Facility never reclaims
workspace volumes by age.
Related: #183. Its root cause — a shipped template value the platform's own
validation rejects — was fixed in #159 by the
OptionalNonEmptypreprocess.This is the same failure class one layer up, and the new test pins the
.env.examplehalf so it cannot come back. #183 looks closeable.
Verification
The regression fails without the fix, for the right reason:
Coverage pins the shipped defaults rather than a paraphrase of them: the Compose
environment boots; a published control plane with a loopback preview does not; a
loopback control plane with a published preview does not; the bare control
hostname is still refused as a preview origin; and
.env.exampleis parsed asdotenv delivers it and asserted to boot with only the master key filled in.
pnpm verifypasses locallypnpm verifydoes not pass on this machine and not because of this change:test:devreports 116 tests, 92 pass, 24 fail here, and I measured theidentical 92/24 on a clean
main. The failures are Windows environment noise —the patched
image-sizecases,tarfailing to resolveC:, and the registrypublication tests. Beyond the suite, the defect and the fix were both confirmed
against the real file:
docker compose --env-file .env.example configreproducesthe interpolation path, and the failing validation was reproduced with the exact
environment the Compose file hands the container.