From d96060e2469df8c57af0c341089f99e4c15e1947 Mon Sep 17 00:00:00 2001 From: syn Date: Tue, 7 Apr 2026 16:36:10 -0500 Subject: [PATCH 1/4] try 2026.3.13 --- services/kiloclaw/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/kiloclaw/Dockerfile b/services/kiloclaw/Dockerfile index 3c24559ac5..0ef22f2b22 100644 --- a/services/kiloclaw/Dockerfile +++ b/services/kiloclaw/Dockerfile @@ -42,7 +42,7 @@ RUN npm install -g pnpm # Install OpenClaw # Pin to specific version for reproducible builds -RUN npm install -g openclaw@2026.3.28 \ +RUN npm install -g openclaw@2026.3.13 \ && openclaw --version # Install ClawHub CLI From 3ec60bfd84a63a99bed79e4f58cfc7764b192d9b Mon Sep 17 00:00:00 2001 From: syn Date: Tue, 7 Apr 2026 17:06:55 -0500 Subject: [PATCH 2/4] maybe? --- services/kiloclaw/Dockerfile | 2 +- services/kiloclaw/controller/src/proxy.test.ts | 8 +++++--- services/kiloclaw/controller/src/proxy.ts | 4 +++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/services/kiloclaw/Dockerfile b/services/kiloclaw/Dockerfile index 0ef22f2b22..3c24559ac5 100644 --- a/services/kiloclaw/Dockerfile +++ b/services/kiloclaw/Dockerfile @@ -42,7 +42,7 @@ RUN npm install -g pnpm # Install OpenClaw # Pin to specific version for reproducible builds -RUN npm install -g openclaw@2026.3.13 \ +RUN npm install -g openclaw@2026.3.28 \ && openclaw --version # Install ClawHub CLI diff --git a/services/kiloclaw/controller/src/proxy.test.ts b/services/kiloclaw/controller/src/proxy.test.ts index c94fb2f892..451f93b0f1 100644 --- a/services/kiloclaw/controller/src/proxy.test.ts +++ b/services/kiloclaw/controller/src/proxy.test.ts @@ -265,9 +265,11 @@ describe('WebSocket proxy', () => { expect(forwarded?.['host']).toBe('127.0.0.1:3001'); // Upstream proxy headers must be stripped so the gateway's // isLocalDirectRequest check doesn't treat the request as proxied/remote. - expect(forwarded?.['x-forwarded-for']).toBeUndefined(); - expect(forwarded?.['x-real-ip']).toBeUndefined(); - expect(forwarded?.['x-forwarded-host']).toBeUndefined(); + expect(forwarded?.['forwarded']).toBeUndefined(); + expect(forwarded?.['x-forwarded-for']).toBeUndefined(); + expect(forwarded?.['x-forwarded-proto']).toBeUndefined(); + expect(forwarded?.['x-real-ip']).toBeUndefined(); + expect(forwarded?.['x-forwarded-host']).toBeUndefined(); expect((clientSocket as unknown as FakeSocket).pipe).toHaveBeenCalledWith(backendSocket); expect(backendSocket.pipe).toHaveBeenCalledWith(clientSocket); }); diff --git a/services/kiloclaw/controller/src/proxy.ts b/services/kiloclaw/controller/src/proxy.ts index b9acb6897e..85e904720e 100644 --- a/services/kiloclaw/controller/src/proxy.ts +++ b/services/kiloclaw/controller/src/proxy.ts @@ -152,10 +152,12 @@ export function handleWebSocketUpgrade( const forwardedHeaders = { ...req.headers }; delete forwardedHeaders['x-kiloclaw-proxy-token']; // Rewrite Host so the gateway sees a loopback origin (matching the HTTP proxy path). - // Strip forwarded-* headers injected by upstream proxies (Fly, CF) so the gateway's + // Strip proxy/forwarding headers injected by upstream proxies (Fly, CF) so the gateway's // isLocalDirectRequest check doesn't conclude the request came from a remote client. forwardedHeaders['host'] = `${backendHost}:${backendPort}`; + delete forwardedHeaders['forwarded']; delete forwardedHeaders['x-forwarded-for']; + delete forwardedHeaders['x-forwarded-proto']; delete forwardedHeaders['x-real-ip']; delete forwardedHeaders['x-forwarded-host']; From 42ee31b450f9b3554847f74732b311a54f26e05a Mon Sep 17 00:00:00 2001 From: syn Date: Tue, 7 Apr 2026 19:34:55 -0500 Subject: [PATCH 3/4] fix(kiloclaw): stop leaking /i/{instanceId} prefix in access gateway redirect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildRedirectUrl included /i/{instanceId}/ in the redirect path for instance-keyed instances. This leaked the proxy prefix to the OpenClaw SPA, which derived its WebSocket URL from window.location and persisted it to localStorage — bypassing cookie-based instance routing entirely. Always redirect to /#token={token} and let the kiloclaw-active-instance cookie (set before the redirect) handle catch-all proxy routing. --- .../src/app/(app)/claw/hooks/useGatewayUrl.ts | 2 +- services/kiloclaw/src/routes/access-gateway.ts | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/(app)/claw/hooks/useGatewayUrl.ts b/apps/web/src/app/(app)/claw/hooks/useGatewayUrl.ts index c192e97d5c..17f5a6bccc 100644 --- a/apps/web/src/app/(app)/claw/hooks/useGatewayUrl.ts +++ b/apps/web/src/app/(app)/claw/hooks/useGatewayUrl.ts @@ -7,7 +7,7 @@ export function useGatewayUrl(status: KiloClawDashboardStatus | undefined) { if (!status?.userId) return baseUrl; const params = new URLSearchParams({ userId: status.userId }); // Instance-keyed instances need the instanceId so the access gateway - // can resolve the correct sandboxId and redirect to /i/{instanceId}/. + // can resolve the correct sandboxId and set the active-instance cookie. if (status.instanceId) { params.set('instanceId', status.instanceId); } diff --git a/services/kiloclaw/src/routes/access-gateway.ts b/services/kiloclaw/src/routes/access-gateway.ts index 7507fd15b4..6c39b48a16 100644 --- a/services/kiloclaw/src/routes/access-gateway.ts +++ b/services/kiloclaw/src/routes/access-gateway.ts @@ -117,10 +117,11 @@ async function resolveSandboxId( /** * Build the redirect URL after successful auth. * - * For instance-keyed instances: /i/{instanceId}/#token={token} - * → subsequent requests go through the /i/:instanceId/* proxy route - * For legacy: /#token={token} - * → subsequent requests go through the catch-all proxy route + * Always redirects to /#token={token} — the catch-all proxy uses the + * kiloclaw-active-instance cookie (set before this redirect) to route + * requests to the correct instance. The /i/{instanceId} prefix must + * never appear in the redirect URL because the OpenClaw SPA would use + * it as the WebSocket base path, bypassing cookie-based routing. */ async function buildRedirectUrl( userId: string, @@ -130,7 +131,12 @@ async function buildRedirectUrl( if (!env.GATEWAY_TOKEN_SECRET) return '/'; const sandboxId = await resolveSandboxId(userId, env, instanceId); const token = await deriveGatewayToken(sandboxId, env.GATEWAY_TOKEN_SECRET); - const basePath = instanceId && isValidInstanceId(instanceId) ? `/i/${instanceId}/` : '/'; + // Always redirect to '/' — never include the /i/{instanceId} prefix. + // The kiloclaw-active-instance cookie (set before this redirect) tells + // the catch-all proxy which instance to route to. Exposing the prefix + // in the URL would leak it to the OpenClaw SPA, which would then use + // it as the WebSocket target — bypassing cookie-based routing entirely. + const basePath = '/'; return `${basePath}#token=${token}`; } From 9c8caacb0dd1c08a346e9812c176e7ad9a85c372 Mon Sep 17 00:00:00 2001 From: syn Date: Tue, 7 Apr 2026 19:49:53 -0500 Subject: [PATCH 4/4] test(kiloclaw): set forwarded/x-forwarded-proto in proxy test fixture The assertions for stripping these headers were passing trivially because the incoming request never set them. Add them to the fixture so the test actually exercises the delete logic. Fix indentation. --- services/kiloclaw/controller/src/proxy.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/services/kiloclaw/controller/src/proxy.test.ts b/services/kiloclaw/controller/src/proxy.test.ts index 451f93b0f1..e3e22d5b9b 100644 --- a/services/kiloclaw/controller/src/proxy.test.ts +++ b/services/kiloclaw/controller/src/proxy.test.ts @@ -212,7 +212,9 @@ describe('WebSocket proxy', () => { const req = createIncomingMessage({ 'x-kiloclaw-proxy-token': 'token-1', host: 'acct-xxxx.fly.dev', + forwarded: 'for=1.2.3.4;proto=https', 'x-forwarded-for': '1.2.3.4', + 'x-forwarded-proto': 'https', 'x-real-ip': '1.2.3.4', 'x-forwarded-host': 'claw.kilo.ai', }); @@ -265,11 +267,11 @@ describe('WebSocket proxy', () => { expect(forwarded?.['host']).toBe('127.0.0.1:3001'); // Upstream proxy headers must be stripped so the gateway's // isLocalDirectRequest check doesn't treat the request as proxied/remote. - expect(forwarded?.['forwarded']).toBeUndefined(); - expect(forwarded?.['x-forwarded-for']).toBeUndefined(); - expect(forwarded?.['x-forwarded-proto']).toBeUndefined(); - expect(forwarded?.['x-real-ip']).toBeUndefined(); - expect(forwarded?.['x-forwarded-host']).toBeUndefined(); + expect(forwarded?.['forwarded']).toBeUndefined(); + expect(forwarded?.['x-forwarded-for']).toBeUndefined(); + expect(forwarded?.['x-forwarded-proto']).toBeUndefined(); + expect(forwarded?.['x-real-ip']).toBeUndefined(); + expect(forwarded?.['x-forwarded-host']).toBeUndefined(); expect((clientSocket as unknown as FakeSocket).pipe).toHaveBeenCalledWith(backendSocket); expect(backendSocket.pipe).toHaveBeenCalledWith(clientSocket); });