Commit 9e80deb
authored
feat(kiloclaw): mint per-instance <label>.kiloclaw.ai URLs (PR3) (#3029)
* refactor(kiloclaw): consolidate catch-all proxy blocks
All four catch-all paths (/i/:instanceId/*, host-based, cookie-routed,
default personal) now share a single `proxyThroughTarget` helper.
Previously the host branch used the helper while the other three
inlined the same ~130-line HTTP + WebSocket relay; this commit
collapses them onto the helper.
The helper gains optional `unreachableHint` / `startingUpHint`
parameters so the default-personal branch can keep its user-facing
hint strings (these are test-asserted). All other behavior is
preserved — same status codes, same JSON shapes, identical
WebSocket relay semantics.
One pre-existing inconsistency is unified rather than preserved: the
cookie branch used to return `{ error: 'WebSocket upgrade failed' }`
with status 502 when the upstream returned no webSocket. The helper
(and the /i and default branches) return the raw containerResponse in
that case, which is strictly more informative. No test asserted the
cookie-branch-specific response.
src/index.ts: +48 / -393 (net -345).
* refactor(worker-utils): host label + sandbox-id helpers for cross-package reuse
Move the pure sandboxId <-> hostname-label logic (plus sandboxId <-> userId
encoding) from `services/kiloclaw/src/auth/` into
`@kilocode/worker-utils` so `apps/web` can use it to mint per-instance
URLs in PR3 without duplicating the base64url / base32hex encoding.
- New subpath exports: `@kilocode/worker-utils/hostname-label` and
`@kilocode/worker-utils/sandbox-id`.
- The existing `services/kiloclaw/src/auth/hostname-label.ts` and
`sandbox-id.ts` become thin re-export shims so the many existing
`./auth/hostname-label` / `./auth/sandbox-id` imports inside the
worker don't have to migrate all at once.
- Tests move with the implementation.
No behaviour change.
* feat(kiloclaw): route users to per-instance `<label>.kiloclaw.ai` URLs
Wires the dashboard to emit per-instance hostnames as `workerUrl` so
users of v2+ instances open their instance directly on its virtual host
instead of the single shared `claw.kilo.ai` / `claw.kilosessions.ai`
endpoint. Completes the PR3 step of the name-based routing rollout;
PR1 built the host space, PR2 taught the worker to route by Host.
- New env var `KILOCLAW_INSTANCE_URL_TEMPLATE` (e.g.
`https://{label}.kiloclaw.ai`). Unset → legacy single-host behaviour
(dev default, no change).
- `workerUrlForInstance` helper expands the template only when the
instance is on `controllerCapabilitiesVersion >= 2`. Pre-v2 instances
don't have their per-instance origin in
`OPENCLAW_ALLOWED_ORIGINS`, so WebSocket upgrades from the new host
would fail openclaw's exact-match origin check; keep them on the
legacy host until they restart onto v2.
- `getStatus` tRPC procedures (personal + org) thread the new field
through and compute `workerUrl` via the helper. No-instance sentinel
stays on the legacy URL (no sandboxId yet to label).
- `PlatformStatusResponse` type gains `controllerCapabilitiesVersion`;
worker DO was already emitting it, this just exposes it to callers.
- Worker `KILOCLAW_CHECKIN_URL` flipped from
`claw.kilosessions.ai` to `claw.kiloclaw.ai`. Only affects
newly-provisioned / restarted machines; running machines continue
hitting the legacy URL (still live via the existing custom domain).
- Test fixtures (state tests, walkthrough) updated for the new field.
- New helper covered by 9 unit tests in `instance-url.test.ts`.
* fix(kiloclaw): address PR review — normalize WS no-upgrade, reserve 'claw' label, warn on misconfigured URL template
Three PR review findings on the PR2/PR3 routing work.
1. proxyThroughTarget: on a WebSocket request where the upstream returns
a non-upgrade response, return a normalized 502 JSON
`{ error: 'WebSocket upgrade failed' }` instead of the raw upstream
response. The previous helper passed `containerResponse` straight
through (matching the pre-refactor /i/ and default branches but
changing the cookie-routed branch's contract, which was 502 JSON).
Raw upstream bodies on this edge path can leak provider/controller
error detail to the Control UI; normalize to a minimal error body
and log the upstream status for operators. Unified across all four
call sites.
2. Host-based routing: add an explicit `claw` reserved-label guard.
With PR3 flipping KILOCLAW_CHECKIN_URL to `claw.kiloclaw.ai`, that
hostname now enters the `*.kiloclaw.ai/*` wildcard route. The
controller check-in path is registered before the catch-all so it
works, but any other path on that host was hitting
handleHostBasedRoute → `claw` fails label parsing → 404 "Instance
not found" — a confusing error for a reserved operational hostname.
Short-circuit the host branch for reserved labels so requests fall
through to cookie/default routing and produce the normal catch-all
responses instead. Introduces RESERVED_INSTANCE_HOST_LABELS as an
explicit set so future reserved hostnames (`api`, `www`, etc.) are
trivial to add.
3. workerUrlForInstance: log a one-time `console.warn` when
KILOCLAW_INSTANCE_URL_TEMPLATE is set but missing the `{label}`
placeholder. Silently falling back to the legacy URL hides the
misconfiguration. Guarded by a module-level flag so the warning
doesn't spam logs on every getStatus call.
* fix(worker-utils): drop .js extensions from sibling imports for Turbopack
Next.js / Turbopack can't resolve `./instance-id.js` when apps/web
imports @kilocode/worker-utils/hostname-label through the subpath
export — the .js rewrite convention only works in resolvers that do
the TS→JS extension mapping (Vitest, tsgo). Turbopack treats the
literal .js filename and fails.
Drop the .js suffix on the three sibling imports that crossed the
package boundary. worker-utils uses `moduleResolution: bundler`, which
accepts extensionless imports, so the typecheck and Vitest runs stay
green.
* feat(web): default KILOCLAW_INSTANCE_URL_TEMPLATE to prod on NODE_ENV=production
So the per-instance URL rollout goes live automatically on merge, without
needing a Vercel env var edit.
- New `resolveInstanceUrlTemplate(envVar, nodeEnv)` pure function with
three-level resolution: explicit override wins (including empty string
as a kill switch), then NODE_ENV=production defaults to the canonical
`https://{label}.kiloclaw.ai` template, otherwise empty (dev/test).
- Operators can roll back without a code deploy by setting
`KILOCLAW_INSTANCE_URL_TEMPLATE=` (empty) in Vercel.
- Dev/test stay on legacy localhost unless a dev opts in by setting the
dev-parity template (`http://{label}.kiloclaw.localhost:8795`)
explicitly.
- Factored out of the config-module scope so it's testable without
forcing a re-import of config.server.ts, which runs production-only
validation on unrelated secrets at module load time.
* feat(web): default per-instance URLs on in dev too, derived from KILOCLAW_API_URL
Previously `resolveInstanceUrlTemplate` only defaulted on in
production; dev/test returned empty so the dashboard kept emitting the
legacy `KILOCLAW_API_URL` (usually `http://localhost:8795`) as
`workerUrl` until a developer manually added
`KILOCLAW_INSTANCE_URL_TEMPLATE` to `apps/web/.env.local`. That hoop
defeats the point of merging the feature — local repro of the
per-instance flow is exactly what devs need to verify changes.
Make the new pattern default in dev too, derived from
`KILOCLAW_API_URL`:
- `http://localhost:8795` -> `http://{label}.kiloclaw.localhost:8795`
- `http://127.0.0.1:9000` -> `http://{label}.kiloclaw.localhost:9000`
- Non-loopback / missing / unparsable `KILOCLAW_API_URL` falls back to
`http://{label}.kiloclaw.localhost:8795` (the wrangler dev default).
Scheme and port are preserved from `KILOCLAW_API_URL` so a dev
running wrangler on a non-default port still gets a working template.
Opt-out is unchanged: `KILOCLAW_INSTANCE_URL_TEMPLATE=` (empty) in
env returns empty and falls back to legacy routing. Tests exercise
prod default, dev defaults across loopback/non-loopback URLs, explicit
overrides, and the kill-switch opt-out.
* feat(kiloclaw): 301 redirect www.kiloclaw.ai -> apex
The `*.kiloclaw.ai/*` wildcard route catches `www.kiloclaw.ai`; without
an explicit handler it would surface as "Instance not found" 404 because
`www` fails hostname-label parsing. Add a canonical-redirect set
(currently just `www`) that 301s to the apex host derived from
`KILOCLAW_INSTANCE_HOST_SUFFIX` + `KILOCLAW_INSTANCE_URL_SCHEME`, so
dev parity works automatically (`www.kiloclaw.localhost:8795` ->
`kiloclaw.localhost:8795`) without hardcoding the apex.
Redirect target is built via URL setters (pathname/search), not string
concatenation, to sidestep the scheme-relative `//` open-redirect class
PR2 had to patch out of the capability-gate path.
Two new tests cover the prod and dev-parity cases.
DNS side: the existing proxied wildcard `AAAA * -> 100::` record on
`kiloclaw.ai` already covers `www`, and the wildcard cert SAN matches
one-label subdomains. No extra DNS / cert work needed.
* fix(kiloclaw,web): address PR review — drop www redirect, harden kill switch
Two review findings on the latest PR3 commits.
1. **www redirect removed.** `CANONICAL_APEX_REDIRECT_LABELS` and
`buildApexRedirectUrl` lived inside `handleHostBasedRoute`, which
runs from the catch-all route. The catch-all is behind the global
`authGuard` middleware, so unauthenticated `www.kiloclaw.ai`
requests would 401 before the redirect could fire — exactly the
traffic the redirect was meant to serve. Tests passed because the
auth middleware is mocked to always succeed in the worker test
harness. Rather than rearrange the middleware chain to make it
work, drop the worker-side redirect entirely: the `www` → apex
redirect is handled by Cloudflare DNS/edge routes, which is the
right layer for this anyway (no worker invocation cost, runs
before any auth, always correct).
2. **Kill switch now uses an explicit `legacy` sentinel.** The
previous `KILOCLAW_INSTANCE_URL_TEMPLATE=` (empty string) rollback
was brittle: Vercel / Node env pipelines frequently coerce empty
entries into "unset", making an empty-string rollback
indistinguishable from the default-on path (fails open). Switch to
a non-empty word sentinel: `KILOCLAW_INSTANCE_URL_TEMPLATE=legacy`
(case-insensitive) disables per-instance URLs. Empty string now
falls through to the production/dev defaults, matching "unset"
semantics across all env pipelines.
Tests updated to cover the new sentinel behavior and to assert that
empty string no longer disables the feature.1 parent acb2ac0 commit 9e80deb
20 files changed
Lines changed: 908 additions & 685 deletions
File tree
- apps/web/src
- app/(app)/claw
- components
- new
- lib
- kiloclaw
- routers
- organizations
- packages/worker-utils
- src
- services/kiloclaw
- src
- auth
Lines changed: 1 addition & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
67 | 67 | | |
68 | 68 | | |
69 | 69 | | |
| 70 | + | |
70 | 71 | | |
71 | 72 | | |
72 | 73 | | |
| |||
Lines changed: 1 addition & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
| 51 | + | |
51 | 52 | | |
52 | 53 | | |
53 | 54 | | |
| |||
Lines changed: 1 addition & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
| 44 | + | |
44 | 45 | | |
45 | 46 | | |
46 | 47 | | |
| |||
Lines changed: 1 addition & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
| 42 | + | |
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
209 | 209 | | |
210 | 210 | | |
211 | 211 | | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
212 | 302 | | |
213 | 303 | | |
214 | 304 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
0 commit comments