-
Notifications
You must be signed in to change notification settings - Fork 37
feat(kiloclaw): mint per-instance <label>.kiloclaw.ai URLs (PR3) #3029
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
bb8a36f
refactor(kiloclaw): consolidate catch-all proxy blocks
pandemicsyn 410191e
refactor(worker-utils): host label + sandbox-id helpers for cross-pac…
pandemicsyn d1b29ec
feat(kiloclaw): route users to per-instance `<label>.kiloclaw.ai` URLs
pandemicsyn a550665
fix(kiloclaw): address PR review — normalize WS no-upgrade, reserve '…
pandemicsyn 7719fe7
Merge remote-tracking branch 'origin/main' into florian/feat/namebase…
pandemicsyn e0d8699
fix(worker-utils): drop .js extensions from sibling imports for Turbo…
pandemicsyn fa6ee8d
feat(web): default KILOCLAW_INSTANCE_URL_TEMPLATE to prod on NODE_ENV…
pandemicsyn 858742a
feat(web): default per-instance URLs on in dev too, derived from KILO…
pandemicsyn 7772d4c
feat(kiloclaw): 301 redirect www.kiloclaw.ai -> apex
pandemicsyn 787b9d2
fix(kiloclaw,web): address PR review — drop www redirect, harden kill…
pandemicsyn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| import { describe, it, expect } from '@jest/globals'; | ||
| import { resolveInstanceUrlTemplate } from './config.server'; | ||
|
|
||
| describe('resolveInstanceUrlTemplate', () => { | ||
| describe('kill switch (KILOCLAW_INSTANCE_URL_TEMPLATE=legacy)', () => { | ||
| it('returns empty (legacy routing) when set to the kill-switch sentinel in production', () => { | ||
| expect(resolveInstanceUrlTemplate('legacy', 'production', 'https://claw.kilo.ai')).toBe(''); | ||
| }); | ||
|
|
||
| it('matches the sentinel case-insensitively', () => { | ||
| expect(resolveInstanceUrlTemplate('Legacy', 'production', 'https://claw.kilo.ai')).toBe(''); | ||
| expect(resolveInstanceUrlTemplate('LEGACY', 'production', 'https://claw.kilo.ai')).toBe(''); | ||
| }); | ||
|
|
||
| it('also disables per-instance URLs in dev when set', () => { | ||
| expect(resolveInstanceUrlTemplate('legacy', 'development', 'http://localhost:8795')).toBe(''); | ||
| }); | ||
|
|
||
| it('treats an explicit empty string as "unset" (falls through to defaults), not as a kill switch', () => { | ||
| // Platform env pipelines often coerce empty values to "unset", so | ||
| // empty string must not be the rollback signal. | ||
| expect(resolveInstanceUrlTemplate('', 'production', 'https://claw.kilo.ai')).toBe( | ||
| 'https://{label}.kiloclaw.ai' | ||
| ); | ||
| expect(resolveInstanceUrlTemplate('', 'development', 'http://localhost:8795')).toBe( | ||
| 'http://{label}.kiloclaw.localhost:8795' | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('production defaults', () => { | ||
| it('defaults to the canonical prod template when no override is set', () => { | ||
| expect(resolveInstanceUrlTemplate(undefined, 'production', 'https://claw.kilo.ai')).toBe( | ||
| 'https://{label}.kiloclaw.ai' | ||
| ); | ||
| }); | ||
|
|
||
| it('honors an explicit override in production', () => { | ||
| expect( | ||
| resolveInstanceUrlTemplate( | ||
| 'https://{label}.preview.kiloclaw.ai', | ||
| 'production', | ||
| 'https://claw.kilo.ai' | ||
| ) | ||
| ).toBe('https://{label}.preview.kiloclaw.ai'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('development / test defaults', () => { | ||
| it('derives a loopback-parity template from a localhost KILOCLAW_API_URL', () => { | ||
| expect(resolveInstanceUrlTemplate(undefined, 'development', 'http://localhost:8795')).toBe( | ||
| 'http://{label}.kiloclaw.localhost:8795' | ||
| ); | ||
| }); | ||
|
|
||
| it('derives a loopback-parity template from a 127.0.0.1 KILOCLAW_API_URL', () => { | ||
| expect(resolveInstanceUrlTemplate(undefined, 'development', 'http://127.0.0.1:8795')).toBe( | ||
| 'http://{label}.kiloclaw.localhost:8795' | ||
| ); | ||
| }); | ||
|
|
||
| it('preserves the port from KILOCLAW_API_URL when non-default', () => { | ||
| expect(resolveInstanceUrlTemplate(undefined, 'development', 'http://localhost:9999')).toBe( | ||
| 'http://{label}.kiloclaw.localhost:9999' | ||
| ); | ||
| }); | ||
|
|
||
| it('preserves the scheme from KILOCLAW_API_URL', () => { | ||
| expect(resolveInstanceUrlTemplate(undefined, 'development', 'https://localhost:8795')).toBe( | ||
| 'https://{label}.kiloclaw.localhost:8795' | ||
| ); | ||
| }); | ||
|
|
||
| it('falls back to the wrangler dev port when KILOCLAW_API_URL is missing', () => { | ||
| expect(resolveInstanceUrlTemplate(undefined, 'development', undefined)).toBe( | ||
| 'http://{label}.kiloclaw.localhost:8795' | ||
| ); | ||
| }); | ||
|
|
||
| it('falls back when KILOCLAW_API_URL is unparsable', () => { | ||
| expect(resolveInstanceUrlTemplate(undefined, 'development', 'not a url')).toBe( | ||
| 'http://{label}.kiloclaw.localhost:8795' | ||
| ); | ||
| }); | ||
|
|
||
| it('uses the fallback template when KILOCLAW_API_URL points at a non-loopback host', () => { | ||
| // Remote staging — dev mode with a non-local worker. We don't try | ||
| // to derive a wildcard host for it; fall back to the loopback | ||
| // template. Operators who want a real per-instance URL on remote | ||
| // staging set KILOCLAW_INSTANCE_URL_TEMPLATE explicitly. | ||
| expect(resolveInstanceUrlTemplate(undefined, 'development', 'https://staging.kilo.ai')).toBe( | ||
| 'http://{label}.kiloclaw.localhost:8795' | ||
| ); | ||
| }); | ||
|
|
||
| it('defaults loopback-parity in test mode too', () => { | ||
| expect(resolveInstanceUrlTemplate(undefined, 'test', 'http://localhost:8795')).toBe( | ||
| 'http://{label}.kiloclaw.localhost:8795' | ||
| ); | ||
| }); | ||
|
|
||
| it('honors a dev-parity override', () => { | ||
| expect( | ||
| resolveInstanceUrlTemplate( | ||
| 'http://{label}.kiloclaw.localhost:8795', | ||
| 'development', | ||
| 'http://localhost:8795' | ||
| ) | ||
| ).toBe('http://{label}.kiloclaw.localhost:8795'); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import { describe, it, expect, jest } from '@jest/globals'; | ||
| import { workerUrlForInstance } from './instance-url'; | ||
| import { sandboxIdFromUserId, sandboxIdFromInstanceId } from '@kilocode/worker-utils/sandbox-id'; | ||
|
|
||
| const LEGACY = 'https://claw.kilo.ai'; | ||
| const TEMPLATE = 'https://{label}.kiloclaw.ai'; | ||
|
|
||
| describe('workerUrlForInstance', () => { | ||
| it('falls back to the legacy URL when the template is unset', () => { | ||
| const sandboxId = sandboxIdFromInstanceId('550e8400-e29b-41d4-a716-446655440000'); | ||
| expect( | ||
| workerUrlForInstance({ | ||
| sandboxId, | ||
| controllerCapabilitiesVersion: 2, | ||
| template: '', | ||
| fallback: LEGACY, | ||
| }) | ||
| ).toBe(LEGACY); | ||
| }); | ||
|
|
||
| it('falls back to the legacy URL and warns once when the template has no {label} placeholder', () => { | ||
| const sandboxId = sandboxIdFromInstanceId('550e8400-e29b-41d4-a716-446655440000'); | ||
| const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); | ||
| try { | ||
| expect( | ||
| workerUrlForInstance({ | ||
| sandboxId, | ||
| controllerCapabilitiesVersion: 2, | ||
| template: 'https://claw.kiloclaw.ai', | ||
| fallback: LEGACY, | ||
| }) | ||
| ).toBe(LEGACY); | ||
| // Subsequent calls with the same misconfiguration must not spam logs. | ||
| workerUrlForInstance({ | ||
| sandboxId, | ||
| controllerCapabilitiesVersion: 2, | ||
| template: 'https://claw.kiloclaw.ai', | ||
| fallback: LEGACY, | ||
| }); | ||
| expect(warn).toHaveBeenCalledTimes(1); | ||
| expect(warn.mock.calls[0][0]).toMatch(/missing the \{label\} placeholder/); | ||
| } finally { | ||
| warn.mockRestore(); | ||
| } | ||
| }); | ||
|
|
||
| it('falls back to the legacy URL for pre-v2 instances', () => { | ||
| const sandboxId = sandboxIdFromInstanceId('550e8400-e29b-41d4-a716-446655440000'); | ||
| expect( | ||
| workerUrlForInstance({ | ||
| sandboxId, | ||
| controllerCapabilitiesVersion: null, | ||
| template: TEMPLATE, | ||
| fallback: LEGACY, | ||
| }) | ||
| ).toBe(LEGACY); | ||
| expect( | ||
| workerUrlForInstance({ | ||
| sandboxId, | ||
| controllerCapabilitiesVersion: 1, | ||
| template: TEMPLATE, | ||
| fallback: LEGACY, | ||
| }) | ||
| ).toBe(LEGACY); | ||
| }); | ||
|
|
||
| it('falls back to the legacy URL when sandboxId is null (no-instance sentinel)', () => { | ||
| expect( | ||
| workerUrlForInstance({ | ||
| sandboxId: null, | ||
| controllerCapabilitiesVersion: 2, | ||
| template: TEMPLATE, | ||
| fallback: LEGACY, | ||
| }) | ||
| ).toBe(LEGACY); | ||
| }); | ||
|
|
||
| it('expands the template for instance-keyed sandboxIds on v2+', () => { | ||
| const sandboxId = sandboxIdFromInstanceId('550e8400-e29b-41d4-a716-446655440000'); | ||
| expect( | ||
| workerUrlForInstance({ | ||
| sandboxId, | ||
| controllerCapabilitiesVersion: 2, | ||
| template: TEMPLATE, | ||
| fallback: LEGACY, | ||
| }) | ||
| ).toBe('https://i-550e8400e29b41d4a716446655440000.kiloclaw.ai'); | ||
| }); | ||
|
|
||
| it('expands the template for legacy userId sandboxes on v2+', () => { | ||
| const sandboxId = sandboxIdFromUserId('oauth/google:118234567890'); | ||
| expect( | ||
| workerUrlForInstance({ | ||
| sandboxId, | ||
| controllerCapabilitiesVersion: 2, | ||
| template: TEMPLATE, | ||
| fallback: LEGACY, | ||
| }) | ||
| ).toMatch(/^https:\/\/u-[0-9a-v]+\.kiloclaw\.ai$/); | ||
| }); | ||
|
|
||
| it('falls back to the legacy URL when the sandboxId cannot be safely labelled', () => { | ||
| const overlongSandboxId = sandboxIdFromUserId('a'.repeat(39)); | ||
| expect( | ||
| workerUrlForInstance({ | ||
| sandboxId: overlongSandboxId, | ||
| controllerCapabilitiesVersion: 2, | ||
| template: TEMPLATE, | ||
| fallback: LEGACY, | ||
| }) | ||
| ).toBe(LEGACY); | ||
| }); | ||
|
|
||
| it('uses the hardcoded default when fallback is empty', () => { | ||
| expect( | ||
| workerUrlForInstance({ | ||
| sandboxId: null, | ||
| controllerCapabilitiesVersion: 2, | ||
| template: '', | ||
| fallback: '', | ||
| }) | ||
| ).toBe('https://claw.kilo.ai'); | ||
| }); | ||
|
|
||
| it('works with dev-parity templates (http + port)', () => { | ||
| const sandboxId = sandboxIdFromInstanceId('550e8400-e29b-41d4-a716-446655440000'); | ||
| expect( | ||
| workerUrlForInstance({ | ||
| sandboxId, | ||
| controllerCapabilitiesVersion: 2, | ||
| template: 'http://{label}.kiloclaw.localhost:8795', | ||
| fallback: 'http://localhost:8795', | ||
| }) | ||
| ).toBe('http://i-550e8400e29b41d4a716446655440000.kiloclaw.localhost:8795'); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.