Host services on slug subdomains, with the Chat preview as the first customer #5
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Today the only way to see a UI change running is to apply it to the host, which
moves the live URL and restarts the daemon. That makes the main site the test
environment, and it means a change reaches
feat/hosted-endo-managementonfaith rather than on evidence.
Proposal
Add a preview step between "built" and "approved":
building it before approval anyway). The build already lands a complete tree
at
/var/lib/endo/releases/<rev>/.mounts a filesystem under a freshly generated, unguessable capability path
and returns a revoker.
feat/hosted-endo-management, or revoke the mount and drop the revision.No second hostname, no daemon restart, and the live UI never moves. The preview
dies when its capability is revoked.
Open question
The chat bundle has to reach the daemon gateway. Served from a capability path
rather than the main origin, that may hit CORS or a hardcoded origin assumption —
worth testing before committing to this shape.
Scope
This covers front-end changes only. Anything touching the daemon itself still
needs a real apply to prove out, so the preview step should be optional rather
than a gate on every run.
Branch policy
Implied by the above: agent stays the proposal branch, and
feat/hosted-endo-managementonly takes a change that has been previewed orapplied successfully — so it stays the known-good line.
Looked into feasibility. The open question resolves, but it takes the proposal
somewhere different.
The connection is not the problem
The Chat app takes its gateway from a query parameter (
?gateway=…&agent=…,packages/chat/main.js), not from its own origin, andws-gateway.jsperformsno
Origincheck. So a bundle served from anywhere can open the WebSocket andreach the daemon. No CORS obstacle.
The obstacle is an isolation decision we should not undo
modules/reverse-proxy.nixsays it plainly:Serving a candidate Chat build from the asset origin means putting the
agentnonce — a live capability — into a URL on the origin specifically chosen to be
the one that never sees it, and sharing
localStoragethere with every otherserved mount. That defeats the reason the split exists.
Proposal instead: a preview ORIGIN, not a preview path
Give a preview its own subdomain, e.g.
preview.<domain>, served exactly likethe main Chat vhost but rooted at a symlink the machine-admin caplet can point
at any BUILT release:
Then previewing revision X is: prebuild X (which floot/endo#4 already does
before approval), point the preview symlink at
releases/X, and openpreview.<domain>with the usual params. Each preview is its own origin,isolated from both the live Chat origin and the asset origin, and it goes away
by pointing the symlink elsewhere.
Costs more than the original sketch — a vhost, a symlink, a caplet verb — but
it does not spend an isolation property to buy convenience. It also composes
with the prebuild work: the release being previewed is the same artifact the
apply would activate, byte for byte.
The asset-server route stays right for previewing content that does NOT need
the agent capability.
Revised plan
Broader than the original sketch, and blocked on one decision.
Shape
A general slug-subdomain host, not a chat-specific preview:
${slug}.<domain>serves a filesystem the site-host caplet has mounted under that slug. The Chat
preview becomes its first customer rather than the feature itself.
The slug is a ROUTING KEY, not authority. Holding the caplet is control;
knowing a slug gets you the content and nothing more. One controller manages
every mount — no per-site controller secret.
Why this needs a wildcard certificate
A subdomain is not secret. Caddy takes a Let's Encrypt certificate per name,
and every issued certificate is published to Certificate Transparency within
minutes — so a per-name certificate makes every slug publicly enumerable, and a
"random-named cap" would be a random name that anyone can look up. This is the
difference from the asset server, whose secret is in the PATH; paths never
appear in CT.
A wildcard for
*.<domain>puts only the wildcard in CT. Individual slugsnever appear.
The DNS question
Wildcards can only be validated over DNS-01 — no HTTP challenge issues one — so
something must write a TXT record on renewal, roughly every 60 days. Options,
in order of machinery:
token is limited to editing DNS on the one zone. Needs Caddy rebuilt with the
provider plugin — the vendor hash is discoverable by proposing once and
reading the mismatch out of
apply.log, which now captures build output.pointing the challenge name at a small delegated zone; automation then only
ever writes there. A compromise of this host cannot rewrite real DNS, only
challenge records. Costs another service and an NS delegation.
possible (manual DNS-01 is manual by definition), and a missed renewal takes
every slug site down at once. A timer could only remind, and a reminder is
what gets ignored until previews break.
Blast radius
Leave
<domain>,git.<domain>andassets.<domain>on the per-name automaticcertificates they use today. The wildcard covers ONLY slug hosts, so a lapsed
wildcard breaks previews rather than the machine's front door.
Status
Held pending the DNS decision — provider, and whether this host may hold a
scoped credential. Everything downstream (the vhost, the slug table, the caplet
verb, pointing a preview at a prebuilt release) is unaffected by that choice;
only the TLS line differs.
Preview a candidate revision at a capability URL before it lands on hosted-endoto Host services on slug subdomains, with the Chat preview as the first customer