Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions dev/local/env-sync/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ function toPkcs8IfNeeded(pem: string): string {

type ResolvedValueSource = 'env-local' | 'generated' | 'exec' | 'default' | 'missing';

const WORKER_LOCALHOST_URL_KEYS = new Set(['KILOCODE_BACKEND_BASE_URL', 'KILO_OPENROUTER_BASE']);

function resolveAnnotatedValue(
key: string,
entry: ExampleEntry,
Expand All @@ -224,14 +226,21 @@ function resolveAnnotatedValue(
const isHostname = key.includes('HOSTNAME') && !key.includes('URL');
const isWs = key.includes('_WS_');
const defaultUsesDockerHost = entry.defaultValue.includes('host.docker.internal');
const defaultUsesWorkerLocalhost =
WORKER_LOCALHOST_URL_KEYS.has(key) &&
(entry.defaultValue.includes('localhost') || entry.defaultValue.includes('127.0.0.1'));
// LAN IP for container services, but never for ORIGINS keys.
// Preserve host.docker.internal when the example default uses it
// (sandbox containers need it to reach the host from inside Docker).
// Preserve localhost for worker-side URLs that are translated separately
// before being sent into sandbox containers.
const host = defaultUsesDockerHost
? 'host.docker.internal'
: serviceUsesLanIp && !isOrigins && lanIp
? lanIp
: 'localhost';
: defaultUsesWorkerLocalhost
? 'localhost'
: serviceUsesLanIp && !isOrigins && lanIp
? lanIp
: 'localhost';
const protocol = isWs ? 'ws' : 'http';

const resolvedParts: string[] = [];
Expand Down
35 changes: 35 additions & 0 deletions dev/local/env-sync/plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,3 +384,38 @@ test('preserves host.docker.internal in @url defaults for useLanIp services', ()
repo.cleanup();
}
});

test('preserves localhost in worker-side @url defaults for useLanIp services', () => {
const repo = createRepo({
'.env.local': '',
[`${workerDir}/package.json`]: JSON.stringify(
{ scripts: { dev: "wrangler dev --env 'dev'" } },
null,
2
),
[`${workerDir}/wrangler.jsonc`]: '{ "dev": { "port": 8794 } }',
[`${workerDir}/.dev.vars.example`]: [
'# @url nextjs',
'KILOCODE_BACKEND_BASE_URL=http://localhost:3000',
'# @url nextjs/api',
'KILO_OPENROUTER_BASE=http://localhost:3000/api',
'# @url cloud-agent-next',
'WORKER_URL=http://host.docker.internal:8794',
'',
].join('\n'),
});
try {
const plan = computePlan(repo.root, new Set(['cloud-agent-next']));
assert.equal(plan.missingEnvLocal, false);
assert.equal(plan.devVarsChanges.length, 1);
const [change] = plan.devVarsChanges;
assert.ok(change);
assert.equal(change.isNew, true);
const content = change.newFileContent ?? '';
assert.ok(content.includes('KILOCODE_BACKEND_BASE_URL=http://localhost:3000'));
assert.ok(content.includes('KILO_OPENROUTER_BASE=http://localhost:3000/api'));
assert.ok(content.includes('WORKER_URL=http://host.docker.internal:8794'));
} finally {
repo.cleanup();
}
});
12 changes: 6 additions & 6 deletions services/cloud-agent-next/.dev.vars.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ INTERNAL_API_SECRET=your-internal-api-secret-here
#KILOCODE_TOKEN_OVERRIDE=your-override-token-here
#KILOCODE_ORG_ID_OVERRIDE=your-override-org-id-here

# Kilocode backend base URL and KILO_OPENROUTER_BASE for API calls and session environment variables
# For local development, point to your local kilocode-backend.
# Sandbox containers run under Docker Desktop, so `host.docker.internal`
# resolves to the host where Next.js is listening.
# Kilocode backend URLs used by the worker for local server-side fetches and
# CLI provider config. Use a hostname reachable from the worker process. Before
# these values are sent into sandbox containers, the worker translates
# localhost/127.0.0.1 to host.docker.internal automatically.
# @url nextjs
KILOCODE_BACKEND_BASE_URL=http://host.docker.internal:3000
KILOCODE_BACKEND_BASE_URL=http://localhost:3000
# @url nextjs/api
KILO_OPENROUTER_BASE=http://host.docker.internal:3000/api
KILO_OPENROUTER_BASE=http://localhost:3000/api

# Worker base URL used by the wrapper to connect to /ingest.
# Sandbox containers reach the host via `host.docker.internal`.
Expand Down
62 changes: 62 additions & 0 deletions services/cloud-agent-next/src/session-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import {
cleanupWorkspace as mockCleanupWorkspace,
} from './workspace.js';
import {
backendUrlForSandbox,
buildAgentEntryFromRuntimeAgent,
InvalidSessionMetadataError,
SessionService,
Expand Down Expand Up @@ -1353,6 +1354,55 @@ describe('SessionService', () => {
});
});

it('translates worker-local backend URLs before injecting sandbox environment', async () => {
const fakeSession = {
exec: vi.fn().mockResolvedValue({ success: true, exitCode: 0 }),
gitCheckout: vi.fn().mockResolvedValue({ success: true, exitCode: 0 }),
writeFile: vi.fn().mockResolvedValue(undefined),
deleteFile: vi.fn().mockResolvedValue(undefined),
};
const sandboxCreateSession = vi.fn().mockResolvedValue(fakeSession);
const sandbox = {
createSession: sandboxCreateSession,
mkdir: vi.fn().mockResolvedValue(undefined),
exec: vi.fn().mockResolvedValue({ exitCode: 0 }),
writeFile: vi.fn().mockResolvedValue(undefined),
} as unknown as SandboxInstance;
const sessionId: SessionId = 'agent_local_backend_url';
mockedSetupWorkspace.mockResolvedValue({
workspacePath: `/workspace/org/user/sessions/${sessionId}`,
sessionHome: `/home/${sessionId}`,
});

const service = new SessionService();
await service.initiate({
sandbox,
sandboxId: 'org__user',
orgId: 'org',
userId: 'user',
sessionId,
kilocodeToken: 'token',
kilocodeModel: 'test-model',
githubRepo: 'acme/repo',
env: {
...mockEnv,
KILOCODE_BACKEND_BASE_URL: 'http://localhost:3000',
KILO_OPENROUTER_BASE: 'http://localhost:3000/api',
},
});

const callArgs = sandboxCreateSession.mock.calls[0]?.[0] as { env: Record<string, string> };
expect(callArgs.env.KILOCODE_BACKEND_BASE_URL).toBe('http://host.docker.internal:3000');
expect(callArgs.env.KILO_API_URL).toBe('http://host.docker.internal:3000');

const configContent = JSON.parse(callArgs.env.KILO_CONFIG_CONTENT) as {
provider: { kilo: { options: { baseURL?: string } } };
};
expect(configContent.provider.kilo.options.baseURL).toBe(
'http://host.docker.internal:3000/api'
);
});

it('should handle special characters in env var values', async () => {
const fakeSession = {
exec: vi.fn().mockResolvedValue({ success: true, exitCode: 0 }),
Expand Down Expand Up @@ -3981,3 +4031,15 @@ describe('buildAgentEntryFromRuntimeAgent', () => {
expect(result.mode).toBe('subagent');
});
});

describe('backendUrlForSandbox', () => {
it.each([
['http://localhost:3000', 'http://host.docker.internal:3000'],
['http://127.0.0.1:3000', 'http://host.docker.internal:3000'],
['http://localhost:3000/api', 'http://host.docker.internal:3000/api'],
['https://api.kilo.ai', 'https://api.kilo.ai'],
['not-a-url', 'not-a-url'],
])('maps %s to %s', (input, expected) => {
expect(backendUrlForSandbox(input)).toBe(expected);
});
});
20 changes: 17 additions & 3 deletions services/cloud-agent-next/src/session-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,19 @@ export function determineBranchName(sessionId: string, upstreamBranch?: string):
return upstreamBranch ?? `session/${sessionId}`;
}

export function backendUrlForSandbox(workerBackendUrl: string): string {
try {
const url = new URL(workerBackendUrl);
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
url.hostname = 'host.docker.internal';
return url.toString().replace(/\/$/, '');
}
} catch {
// Non-URL value: leave untouched.
}
return workerBackendUrl;
}

type SandboxRetryConfig = {
maxAttempts: number;
baseBackoffMs: number;
Expand Down Expand Up @@ -798,7 +811,7 @@ export class SessionService {
providerOptions.kilocodeOrganizationId = kilocodeOrganizationId;
}
if (env.KILO_OPENROUTER_BASE) {
providerOptions.baseURL = env.KILO_OPENROUTER_BASE;
providerOptions.baseURL = backendUrlForSandbox(env.KILO_OPENROUTER_BASE);
}
const isInteractive = createdOnPlatform == 'cloud-agent-web';
const commandGuardPolicy = getCommandGuardPolicy(createdOnPlatform);
Expand Down Expand Up @@ -963,9 +976,10 @@ export class SessionService {
}

if (env.KILOCODE_BACKEND_BASE_URL) {
envVars.KILOCODE_BACKEND_BASE_URL = env.KILOCODE_BACKEND_BASE_URL;
const sandboxUrl = backendUrlForSandbox(env.KILOCODE_BACKEND_BASE_URL);
envVars.KILOCODE_BACKEND_BASE_URL = sandboxUrl;
// Used by kilo server to check user auth to send to ingest
envVars.KILO_API_URL = env.KILOCODE_BACKEND_BASE_URL;
envVars.KILO_API_URL = sandboxUrl;
}

if (env.KILO_SESSION_INGEST_URL) {
Expand Down