Skip to content
Closed
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
2 changes: 1 addition & 1 deletion services/cloud-agent-next/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { default } from './server.js';
export { Sandbox, Sandbox as SandboxSmall, Sandbox as SandboxDIND } from '@cloudflare/sandbox';
export { Sandbox, SandboxSmall, SandboxDIND, ContainerProxy } from './sandbox-outbound.js';
export { CloudAgentSession } from './persistence/CloudAgentSession.js';
75 changes: 74 additions & 1 deletion services/cloud-agent-next/src/kilo/devcontainer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import {
bringUpDevContainer,
buildRestoreCommand,
buildOverrideConfig,
buildDevContainerTrustedCaBundleSetupCommand,
detectDevContainer,
getDevContainerOverridePath,
getDevContainerTrustedCaBundlePath,
KILO_AGENT_SESSION_LABEL,
KILO_WRAPPER_PORT_LABEL,
mergeDevContainerConfig,
Expand Down Expand Up @@ -197,6 +199,9 @@ describe('bringUpDevContainer', () => {
const commands = execCalls.map(([cmd]) => cmd);
const bootstrapCall = execCalls.find(([cmd]) => cmd.includes('nvm install --lts'));
expect(preflightCount).toBe(2);
expect(commands).toContain(
"source=${GIT_SSL_CAINFO:-/etc/ssl/certs/ca-certificates.crt} && test -f \"$source\" && mkdir -p '/home/agent_xyz/.kilocode/platform' && cp \"$source\" '/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt' && chmod 444 '/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt'"
);
expect(commands.some(cmd => cmd.includes('bun --version'))).toBe(true);
expect(commands.some(cmd => cmd.includes('nvm install --lts'))).toBe(true);
expect(commands.some(cmd => cmd.includes('nvm use --lts'))).toBe(false);
Expand Down Expand Up @@ -328,6 +333,7 @@ describe('buildOverrideConfig', () => {
expect(cfg.mounts).toEqual([
'source=/opt/kilo-cloud,target=/opt/kilo-cloud,type=bind,readonly',
'source=/home/agent_xyz,target=/home/agent_xyz,type=bind',
'source=/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt,target=/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt,type=bind,readonly',
]);
});

Expand All @@ -344,11 +350,21 @@ describe('buildOverrideConfig', () => {
]);
});

it('sets HOME without exposing the outer Docker socket', () => {
it('sets platform-owned nested trust env for lifecycle hooks and remote execution', () => {
const cfg = buildOverrideConfig(baseOpts);
const trustedCaBundle = '/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt';
const platformTrustEnv = {
GIT_SSL_CAINFO: trustedCaBundle,
SSL_CERT_FILE: trustedCaBundle,
CURL_CA_BUNDLE: trustedCaBundle,
REQUESTS_CA_BUNDLE: trustedCaBundle,
NODE_EXTRA_CA_CERTS: trustedCaBundle,
};
expect(cfg.containerEnv).toEqual(platformTrustEnv);
expect(cfg.remoteEnv).toEqual({
HOME: '/home/agent_xyz',
KILO_CLOUD_AGENT: '1',
...platformTrustEnv,
});
});

Expand All @@ -364,6 +380,9 @@ describe('writeMergedOverrideConfig', () => {
expect(cmd).toContain('const outputPath = "/tmp/merged-devcontainer.json"');
expect(cmd).toContain('source=/opt/kilo-cloud,target=/opt/kilo-cloud,type=bind,readonly');
expect(cmd).toContain('source=/home/agent_xyz,target=/home/agent_xyz,type=bind');
expect(cmd).toContain(
'source=/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt,target=/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt,type=bind,readonly'
);
expect(cmd).toContain(`${KILO_AGENT_SESSION_LABEL}=agent_xyz`);
expect(cmd).toContain(`${KILO_WRAPPER_PORT_LABEL}=5050`);
return { exitCode: 0 };
Expand Down Expand Up @@ -414,6 +433,7 @@ describe('mergeDevContainerConfig', () => {
'source=/user,target=/user,type=bind',
'source=/opt/kilo-cloud,target=/opt/kilo-cloud,type=bind,readonly',
'source=/home/agent_xyz,target=/home/agent_xyz,type=bind',
'source=/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt,target=/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt,type=bind,readonly',
]);
expect(merged.runArgs).toEqual([
'--env',
Expand All @@ -426,10 +446,22 @@ describe('mergeDevContainerConfig', () => {
'--label',
`${KILO_WRAPPER_PORT_LABEL}=5050`,
]);
expect(merged.containerEnv).toEqual({
GIT_SSL_CAINFO: '/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt',
SSL_CERT_FILE: '/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt',
CURL_CA_BUNDLE: '/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt',
REQUESTS_CA_BUNDLE: '/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt',
NODE_EXTRA_CA_CERTS: '/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt',
});
expect(merged.remoteEnv).toEqual({
USER_ENV: '1',
HOME: '/home/agent_xyz',
KILO_CLOUD_AGENT: '1',
GIT_SSL_CAINFO: '/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt',
SSL_CERT_FILE: '/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt',
CURL_CA_BUNDLE: '/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt',
REQUESTS_CA_BUNDLE: '/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt',
NODE_EXTRA_CA_CERTS: '/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt',
});
});

Expand All @@ -442,6 +474,26 @@ describe('mergeDevContainerConfig', () => {
expect(merged.remoteUser).toBe('root');
});

it('preserves user env while ensuring platform trust paths win', () => {
const merged = mergeDevContainerConfig(
{
image: 'debian:bookworm',
containerEnv: { USER_CONTAINER_ENV: '1', GIT_SSL_CAINFO: '/user/container-ca.crt' },
remoteEnv: { USER_REMOTE_ENV: '1', GIT_SSL_CAINFO: '/user/remote-ca.crt' },
},
{ sessionHome: '/home/agent_xyz', wrapperPort: 5050, agentSessionId: 'agent_xyz' }
);

expect(merged.containerEnv).toMatchObject({
USER_CONTAINER_ENV: '1',
GIT_SSL_CAINFO: '/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt',
});
expect(merged.remoteEnv).toMatchObject({
USER_REMOTE_ENV: '1',
GIT_SSL_CAINFO: '/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt',
});
});

it('removes host-side initializeCommand while preserving in-container lifecycle hooks', () => {
const merged = mergeDevContainerConfig(
{
Expand Down Expand Up @@ -473,6 +525,7 @@ describe('mergeDevContainerConfig', () => {
'source=/workspace/cache,target=/cache,type=bind',
'source=/opt/kilo-cloud,target=/opt/kilo-cloud,type=bind,readonly',
'source=/home/agent_xyz,target=/home/agent_xyz,type=bind',
'source=/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt,target=/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt,type=bind,readonly',
]);
});

Expand Down Expand Up @@ -651,6 +704,26 @@ describe('buildRestoreCommand', () => {
});
});

describe('nested devcontainer trusted CA bundle', () => {
it('uses a stable path inside the session-home bind mount', () => {
expect(getDevContainerTrustedCaBundlePath('/home/agent_xyz')).toBe(
'/home/agent_xyz/.kilocode/platform/outer-trusted-ca-bundle.crt'
);
});

it('copies the effective outer bundle with safe shell quoting and read-only permissions', () => {
const command = buildDevContainerTrustedCaBundleSetupCommand("/home/agent_'xyz");
expect(command).toContain('source=${GIT_SSL_CAINFO:-/etc/ssl/certs/ca-certificates.crt}');
expect(command).toContain("mkdir -p '/home/agent_'\\''xyz/.kilocode/platform'");
expect(command).toContain(
"cp \"$source\" '/home/agent_'\\''xyz/.kilocode/platform/outer-trusted-ca-bundle.crt'"
);
expect(command).toContain(
"chmod 444 '/home/agent_'\\''xyz/.kilocode/platform/outer-trusted-ca-bundle.crt'"
);
});
});

describe('getDevContainerOverridePath', () => {
it('falls back to the legacy deterministic temp path without config metadata', () => {
expect(getDevContainerOverridePath('agent_xyz')).toBe(
Expand Down
55 changes: 53 additions & 2 deletions services/cloud-agent-next/src/kilo/devcontainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,33 @@ export function getDevContainerOverridePath(
/** Label that records the wrapper HTTP port published by the dev container. */
export const KILO_WRAPPER_PORT_LABEL = 'kilo.wrapperPort';

export function getDevContainerTrustedCaBundlePath(sessionHome: string): string {
return `${sessionHome}/${DEVCONTAINER_TRUSTED_CA_BUNDLE_RELATIVE_PATH}`;
}

export function buildDevContainerTrustedCaBundleSetupCommand(sessionHome: string): string {
const trustedCaBundle = getDevContainerTrustedCaBundlePath(sessionHome);
const trustedCaBundleDir = pathPosix.dirname(trustedCaBundle);
return [
`source=\${GIT_SSL_CAINFO:-${OUTER_TRUSTED_CA_BUNDLE_FALLBACK}}`,
'test -f "$source"',
`mkdir -p ${shellQuote(trustedCaBundleDir)}`,
`cp "$source" ${shellQuote(trustedCaBundle)}`,
`chmod 444 ${shellQuote(trustedCaBundle)}`,
].join(' && ');
}

function buildDevContainerTrustEnv(sessionHome: string): Record<string, string> {
const trustedCaBundle = getDevContainerTrustedCaBundlePath(sessionHome);
return {
GIT_SSL_CAINFO: trustedCaBundle,
SSL_CERT_FILE: trustedCaBundle,
CURL_CA_BUNDLE: trustedCaBundle,
REQUESTS_CA_BUNDLE: trustedCaBundle,
NODE_EXTRA_CA_CERTS: trustedCaBundle,
};
}

/**
* Pinned kilo CLI version installed *inside* the dev container.
*
Expand All @@ -125,6 +152,9 @@ export const KILO_CLI_VERSION = '7.3.12';

const DEVCONTAINER_RUNTIME_BUN_VERSION = '1.3.14';
const DEVCONTAINER_RUNTIME_BOOTSTRAP_TIMEOUT_MS = 10 * 60 * 1000;
const OUTER_TRUSTED_CA_BUNDLE_FALLBACK = '/etc/ssl/certs/ca-certificates.crt';
const DEVCONTAINER_TRUSTED_CA_BUNDLE_RELATIVE_PATH =
'.kilocode/platform/outer-trusted-ca-bundle.crt';

/** `devcontainer up` prints multiple JSON lines on stdout — we look for this final line. */
const UP_OUTCOME_SUCCESS = 'success';
Expand Down Expand Up @@ -334,9 +364,20 @@ export async function bringUpDevContainer(
// `remoteUser: root` in `buildOverrideConfig`), so file ownership lines up
// by construction without any chown/chmod or uid-rewrite trickery.
await session.exec(
`mkdir -p "${sessionHome}/.cache" "${sessionHome}/.local/share/kilo" "${sessionHome}/tmp"`,
`mkdir -p ${shellQuote(`${sessionHome}/.cache`)} ${shellQuote(`${sessionHome}/.local/share/kilo`)} ${shellQuote(`${sessionHome}/tmp`)}`,
{ timeout: 10_000 }
);
const trustedCaSetupResult = await session.exec(
buildDevContainerTrustedCaBundleSetupCommand(sessionHome),
{ timeout: 10_000 }
);
if (trustedCaSetupResult.exitCode !== 0) {
throw new DevContainerUpError(
`Failed to prepare dev container trusted CA bundle (exit ${trustedCaSetupResult.exitCode})`,
trustedCaSetupResult.stdout ?? '',
trustedCaSetupResult.stderr ?? ''
);
}

onProgress?.('Preparing dev container configuration…');

Expand Down Expand Up @@ -473,7 +514,7 @@ export async function bringUpDevContainer(

/**
* Build the override JSON merged on top of the user's `devcontainer.json`.
* Adds Kilo's `mounts`/`runArgs`/`remoteEnv` without changing
* Adds Kilo's `mounts`/`runArgs`/`containerEnv`/`remoteEnv` without changing
* `workspaceMount`/`workspaceFolder`; `remoteUser` is forced to `root` so
* that file ownership across the outer→inner bind mount lines up by
* construction. The user's `"remoteUser": "vscode"` (or similar) is replaced
Expand All @@ -490,6 +531,8 @@ export function buildOverrideConfig(opts: {
agentSessionId: string;
}): Record<string, unknown> {
const { sessionHome, wrapperPort, agentSessionId } = opts;
const trustedCaBundle = getDevContainerTrustedCaBundlePath(sessionHome);
const trustEnv = buildDevContainerTrustEnv(sessionHome);

return {
remoteUser: 'root',
Expand All @@ -498,6 +541,8 @@ export function buildOverrideConfig(opts: {
`source=/opt/kilo-cloud,target=/opt/kilo-cloud,type=bind,readonly`,
// HOME alignment — kilo's xdg-basedir paths must resolve identically inside and out.
`source=${sessionHome},target=${sessionHome},type=bind`,
// Narrow read-only CA propagation for nested HTTPS tooling.
`source=${trustedCaBundle},target=${trustedCaBundle},type=bind,readonly`,
],
runArgs: [
'--network=host',
Expand All @@ -510,9 +555,11 @@ export function buildOverrideConfig(opts: {
'--label',
`${KILO_WRAPPER_PORT_LABEL}=${wrapperPort}`,
],
containerEnv: trustEnv,
remoteEnv: {
HOME: sessionHome,
KILO_CLOUD_AGENT: '1',
...trustEnv,
},
};
}
Expand Down Expand Up @@ -594,6 +641,10 @@ export function mergeDevContainerConfig(
...(typeof override.remoteUser === 'string' ? { remoteUser: override.remoteUser } : {}),
mounts: [...baseMounts, ...overrideMounts],
runArgs: [...sanitizeDevContainerRunArgs(sanitizedBaseConfig.runArgs), ...overrideRunArgs],
containerEnv: {
...(isRecord(sanitizedBaseConfig.containerEnv) ? sanitizedBaseConfig.containerEnv : {}),
...(isRecord(override.containerEnv) ? override.containerEnv : {}),
},
remoteEnv: {
...(isRecord(sanitizedBaseConfig.remoteEnv) ? sanitizedBaseConfig.remoteEnv : {}),
...(isRecord(override.remoteEnv) ? override.remoteEnv : {}),
Expand Down
6 changes: 5 additions & 1 deletion services/cloud-agent-next/src/persistence/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,12 @@ export type OperationResult<T = void> = {
};

export type PersistenceEnv = {
/** Durable Object namespace for Sandbox instances */
/** Durable Object namespace for shared Sandbox instances */
Sandbox: DurableObjectNamespace<Sandbox>;
/** Durable Object namespace for per-session Sandbox instances */
SandboxSmall: DurableObjectNamespace<Sandbox>;
/** Durable Object namespace for Docker-in-Docker Sandbox instances */
SandboxDIND: DurableObjectNamespace<Sandbox>;
/** Durable Object namespace for CloudAgentSession metadata (SQLite-backed) with RPC support */
CLOUD_AGENT_SESSION: DurableObjectNamespace<CloudAgentSession>;
/** Service binding for the session ingest worker */
Expand Down
21 changes: 20 additions & 1 deletion services/cloud-agent-next/src/sandbox-id.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import type { Sandbox } from '@cloudflare/sandbox';
import { generateSandboxId, getSandboxNamespace } from './sandbox-id.js';
import { generateSandboxId, getOutboundContainerId, getSandboxNamespace } from './sandbox-id.js';
import type { Env } from './types.js';

describe('generateSandboxId', () => {
Expand Down Expand Up @@ -291,3 +291,22 @@ describe('getSandboxNamespace', () => {
expect(ns).toBe(mockSandbox);
});
});

describe('getOutboundContainerId', () => {
it.each([
['org-a1b2c3', 'shared-do-id'],
['ses-a1b2c3', 'small-do-id'],
['dind-a1b2c3', 'dind-do-id'],
])('derives %s from the selected sandbox namespace', (sandboxId, expected) => {
const createNamespace = (containerId: string) => ({
idFromName: (name: string) => ({ toString: () => `${containerId}:${name}` }),
});
const env = {
Sandbox: createNamespace('shared-do-id'),
SandboxSmall: createNamespace('small-do-id'),
SandboxDIND: createNamespace('dind-do-id'),
} as unknown as Env;

expect(getOutboundContainerId(env, sandboxId)).toBe(`${expected}:${sandboxId}`);
});
});
11 changes: 10 additions & 1 deletion services/cloud-agent-next/src/sandbox-id.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { SandboxId, Env } from './types.js';
import type { Sandbox } from '@cloudflare/sandbox';

type SandboxNamespaceEnv = Pick<Env, 'Sandbox' | 'SandboxSmall' | 'SandboxDIND'>;

/**
* Parses a comma-separated org ID list into a set.
* Returns an empty set when the value is falsy or blank.
Expand All @@ -21,11 +23,18 @@ function parseOrgIdList(raw: string | undefined): Set<string> {
* - Per-session sandboxes (ses-* prefix) use SandboxSmall
* - All others use Sandbox
*/
export function getSandboxNamespace(env: Env, sandboxId: string): DurableObjectNamespace<Sandbox> {
export function getSandboxNamespace(
env: SandboxNamespaceEnv,
sandboxId: string
): DurableObjectNamespace<Sandbox> {
if (sandboxId.startsWith('dind-')) return env.SandboxDIND;
return sandboxId.startsWith('ses-') ? env.SandboxSmall : env.Sandbox;
}

export function getOutboundContainerId(env: SandboxNamespaceEnv, sandboxId: string): string {
return getSandboxNamespace(env, sandboxId).idFromName(sandboxId).toString();
}

async function hashToSandboxId(input: string, prefix: string): Promise<SandboxId> {
const encoder = new TextEncoder();
const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(input));
Expand Down
Loading
Loading