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
2 changes: 1 addition & 1 deletion apps/web/src/routers/cloud-agent-next-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ describe('cloudAgentNextRouter.prepareSession', () => {
});

expect(mockPrepareSession).toHaveBeenCalledWith(
expect.objectContaining({ attachments: images })
expect.objectContaining({ attachments: images, createdOnPlatform: 'cloud-agent-web' })
);
expect(mockPrepareSession).not.toHaveBeenCalledWith(expect.objectContaining({ images }));
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ describe('organizationCloudAgentNextRouter.prepareSession', () => {
});

expect(mockPrepareSession).toHaveBeenCalledWith(
expect.objectContaining({ attachments: images })
expect.objectContaining({ attachments: images, createdOnPlatform: 'cloud-agent-web' })
);
expect(mockPrepareSession).not.toHaveBeenCalledWith(expect.objectContaining({ images }));
});
Expand Down
25 changes: 13 additions & 12 deletions dev/local/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,7 @@ function doShowGroup(
): void {
if (runningServiceNames.length === 0) return;
const current = viewedRef.current;
const currentIsGroup = isGroupView(current);
const result = showGroupInTmux(
sessionName,
runningServiceNames,
currentViewedEncoded(current),
currentIsGroup
);
const result = showGroupInTmux(sessionName, runningServiceNames, currentViewedEncoded(current));
if (result !== currentViewedEncoded(current)) {
viewedRef.current = { kind: 'group', groupId, serviceNames: runningServiceNames };
}
Expand Down Expand Up @@ -373,7 +367,9 @@ function Dashboard({
return new Map(entries);
});
};
refresh();
void refresh().catch(error => {
console.error('Failed to refresh service statuses:', error);
});
const timer = setInterval(refresh, REFRESH_MS);
return () => clearInterval(timer);
}, [runningServices]);
Expand Down Expand Up @@ -691,7 +687,7 @@ function Dashboard({

const handleStdin = (data: Buffer) => {
const str = data.toString('utf-8');
const re = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
const re = new RegExp(`${String.fromCharCode(27)}\\[<(\\d+);(\\d+);(\\d+)([Mm])`, 'g');
let m;
while ((m = re.exec(str)) !== null) {
const button = parseInt(m[1], 10);
Expand Down Expand Up @@ -894,6 +890,11 @@ const { waitUntilExit } = render(
/>
);

waitUntilExit().then(() => {
process.exit(0);
});
waitUntilExit()
.then(() => {
process.exit(0);
})
.catch(error => {
console.error('Dashboard exited with an error:', error);
process.exit(1);
});
38 changes: 17 additions & 21 deletions dev/local/runner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { execFileSync, execSync } from 'node:child_process';
import { execFileSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as net from 'node:net';
import * as path from 'node:path';
Expand Down Expand Up @@ -134,28 +134,25 @@ function getPnpmCommand(): string {

export function startServiceInTmux(sessionName: string, serviceName: string): void {
const svc = getService(serviceName);
const winIndex = createWindow(sessionName, serviceName);
if (svc.type === 'infra') {
// Profile-gated services need --profile on every compose subcommand,
// including `logs`. Without it, Compose v2 filters the service out of
// the graph and the tmux pane is silent or errors. Shell-quote the
// profile and service names — they are safe identifiers today but the
// quoting keeps the command robust if a future maintainer adds a name
// containing whitespace or metacharacters.
const profile = getInfraProfile(serviceName);
const profileArg = profile ? `--profile ${shellQuote(profile)} ` : '';
sendKeys(
sessionName,
serviceName,
`docker compose ${profileArg}-f dev/docker-compose.yml logs -f ${shellQuote(serviceName)}`
);
} else {
sendKeys(sessionName, serviceName, buildStartCommand(serviceName));
}
const startupCommand =
svc.type === 'infra' ? buildInfraLogCommand(serviceName) : buildStartCommand(serviceName);
const winIndex = createWindow(sessionName, serviceName, startupCommand);
const logPath = path.join(findRepoRoot(), 'dev', 'logs', `${serviceName}.log`);
pipePane(sessionName, winIndex, 0, buildLogPipeCommand(logPath));
}

function buildInfraLogCommand(serviceName: string): string {
// Profile-gated services need --profile on every compose subcommand,
// including `logs`. Without it, Compose v2 filters the service out of
// the graph and the tmux pane is silent or errors. Shell-quote the
// profile and service names — they are safe identifiers today but the
// quoting keeps the command robust if a future maintainer adds a name
// containing whitespace or metacharacters.
const profile = getInfraProfile(serviceName);
const profileArg = profile ? `--profile ${shellQuote(profile)} ` : '';
return `docker compose ${profileArg}-f dev/docker-compose.yml logs -f ${shellQuote(serviceName)}`;
}

function buildLogPipeCommand(logPath: string): string {
const filterPath = path.join(findRepoRoot(), 'dev', 'local', 'log-filter.ts');
return `tsx ${shellQuote(filterPath)} >> ${shellQuote(logPath)}`;
Expand Down Expand Up @@ -286,8 +283,7 @@ export function showServiceInTmux(
export function showGroupInTmux(
sessionName: string,
serviceNames: string[],
currentPaneNames: string,
currentViewedIsGroup: boolean
currentPaneNames: string
): string {
if (serviceNames.length === 0) return currentPaneNames;
try {
Expand Down
17 changes: 17 additions & 0 deletions dev/local/tmux.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import test from 'node:test';

import { buildInteractiveShellCommand } from './tmux';

test('buildInteractiveShellCommand wraps quoted startup commands in parseable shell syntax', () => {
const startupCommand =
"PATH='/tmp/with spaces:/bin' PNPM_HOME='/tmp/pnpm home' node '/tmp/runner with spaces.js' --flag";

const wrapped = buildInteractiveShellCommand(startupCommand, '/bin/sh');

assert.match(wrapped, /^'\/bin\/sh' -lc /);
assert.match(wrapped, /exec/);
assert.match(wrapped, /PATH/);
execFileSync('/bin/sh', ['-n', '-c', wrapped]);
});
34 changes: 29 additions & 5 deletions dev/local/tmux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,37 @@ function attachSession(sessionName: string): void {
// Window management
// ---------------------------------------------------------------------------

function createWindow(sessionName: string, windowName: string): number {
const output = execSync(
`tmux new-window -d -t ${sessionName} -n ${windowName} -P -F "#{window_index}"`,
{ encoding: 'utf-8' }
).trim();
function createWindow(sessionName: string, windowName: string, startupCommand?: string): number {
const args = [
'new-window',
'-d',
'-t',
sessionName,
'-n',
windowName,
'-c',
getWorktreeRoot(),
'-P',
'-F',
'#{window_index}',
];
if (startupCommand) {
args.push(buildInteractiveShellCommand(startupCommand));
}

const output = execFileSync('tmux', args, { encoding: 'utf-8' }).trim();
return parseInt(output, 10);
}

function buildInteractiveShellCommand(
startupCommand: string,
shell = process.env.SHELL || '/bin/sh'
): string {
return `${escapeForShell(shell)} -lc ${escapeForShell(
`${startupCommand}; exec ${escapeForShell(shell)} -l`
)}`;
}

function paneTarget(sessionName: string, windowTarget: string | number, pane?: number): string {
return pane !== undefined
? `${sessionName}:${windowTarget}.${pane}`
Expand Down Expand Up @@ -442,6 +465,7 @@ export {
killSession,
attachSession,
createWindow,
buildInteractiveShellCommand,
sendKeys,
sendInterrupt,
selectWindow,
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ minimumReleaseAgeExclude:
- '@typescript/native-preview-linux-x64'
- '@typescript/native-preview-win32-arm64'
- '@typescript/native-preview-win32-x64'
# Kilo SDK releases are owned internally and adopted with pinned runtime validation.
- '@kilocode/sdk'
# KiloClaw pins and live-smoke-validates OpenClaw image upgrades before rollout.
- openclaw
overrides:
Expand Down
2 changes: 1 addition & 1 deletion services/cloud-agent-next/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ FROM docker.io/cloudflare/sandbox:0.10.3
# Build arguments for metadata (all optional with defaults)
ARG BUILD_DATE=""
ARG VCS_REF=""
ARG KILOCODE_CLI_VERSION="7.3.12"
ARG KILOCODE_CLI_VERSION="7.3.21"

# Install latest stable git + git-lfs from the git-core PPA, GitHub CLI, and supporting tools.
# The default Ubuntu git (2.34.1 on 22.04) is outdated; the git-core PPA ships the latest
Expand Down
2 changes: 1 addition & 1 deletion services/cloud-agent-next/Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ FROM docker.io/cloudflare/sandbox:0.10.3
# Build arguments for metadata (all optional with defaults)
ARG BUILD_DATE=""
ARG VCS_REF=""
ARG KILOCODE_CLI_VERSION="7.3.12"
ARG KILOCODE_CLI_VERSION="7.3.21"

# Build the kilo binary:
# cd ~/projects/kilocode-backend/cloud-agent
Expand Down
2 changes: 1 addition & 1 deletion services/cloud-agent-next/Dockerfile.dind
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ USER root
# Build arguments for metadata (all optional with defaults)
ARG BUILD_DATE=""
ARG VCS_REF=""
ARG KILOCODE_CLI_VERSION="7.3.12"
ARG KILOCODE_CLI_VERSION="7.3.21"

# Cloudflare Containers run without root privileges, so Docker must run in
# rootless mode. The Sandbox SDK server is copied into this image so the
Expand Down
2 changes: 1 addition & 1 deletion services/cloud-agent-next/scripts/dev-with-docker-proxy.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/sh
# Run `wrangler dev` with a local Docker socket proxy that injects
# HostConfig.Privileged=true for SandboxSmall (Docker-in-Docker).
# HostConfig.Privileged=true for SandboxDIND containers only.
#
# See scripts/docker-privileged-proxy.mjs for context.
# Args after `--` are forwarded to wrangler dev.
Expand Down
31 changes: 21 additions & 10 deletions services/cloud-agent-next/scripts/docker-privileged-proxy.mjs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// Docker socket proxy that injects HostConfig.Privileged=true into
// `POST /containers/create` requests.
// `SandboxDIND` container create requests.
//
// Why this exists
// ---------------
// Cloudflare Containers run our `SandboxSmall` image (Docker-in-Docker)
// privileged in production, but local `wrangler dev` has no supported way
// to set Docker container create options like `HostConfig.Privileged=true`.
// Without that, rootless dockerd inside the Sandbox container fails to set
// up its mounts and `/var/run/docker.sock` never appears.
// Cloudflare Containers run our `SandboxDIND` image privileged in production,
// but local `wrangler dev` has no supported way to set Docker container create
// options like `HostConfig.Privileged=true`. Without that, rootless dockerd
// inside the SandboxDIND container fails to set up its mounts and
// `/var/run/docker.sock` never appears.
//
// Workaround: run a small Unix-socket proxy on the developer machine that
// forwards Docker API calls to the host's real Docker socket and rewrites
// `POST /containers/create` bodies to set `HostConfig.Privileged=true`.
// forwards Docker API calls to the host's real Docker socket and rewrites only
// `SandboxDIND` create bodies to set `HostConfig.Privileged=true`.
// `pnpm dev` then runs Wrangler with `DOCKER_HOST` pointed at this proxy.
//
// This matches the workaround documented in cloudflare/sandbox-sdk#662 and
Expand Down Expand Up @@ -83,10 +83,10 @@ const server = net.createServer(client => {

const header = buffered.slice(0, headerEnd).toString('utf8');
const bodyStart = headerEnd + 4;
const match = header.match(/^POST\s+\S*\/containers\/create(?:\?|\s)/);
const createRequest = header.match(/^POST\s+(\S*\/containers\/create(?:\?\S*)?)\s/);
const contentLength = header.match(/\r\nContent-Length:\s*(\d+)/i);

if (!match || !contentLength) {
if (!createRequest || !contentLength) {
patched = true;
upstream.write(buffered);
return;
Expand All @@ -107,6 +107,17 @@ const server = net.createServer(client => {
return;
}

const containerName = new URL(createRequest[1], 'http://docker').searchParams.get('name');
const isSandboxDind =
containerName?.includes('-SandboxDIND-') &&
typeof payload.Image === 'string' &&
payload.Image.startsWith('cloudflare-dev/sandboxdind:');
if (!isSandboxDind) {
patched = true;
upstream.write(buffered);
return;
}

payload.HostConfig = { ...payload.HostConfig, Privileged: true };
const nextBody = Buffer.from(JSON.stringify(payload));
const nextHeader = header.replace(/(\r\nContent-Length:\s*)\d+/i, `$1${nextBody.length}`);
Expand Down
Loading