Files
paseo/packages/server/src/server/workspace-runtime/workspace-runtime.docker.e2e.test.ts
T
Mohamed Boudra 6ca27a9b12 feat(workspaces): publish external runtime contract
Publish the rootless workspace runtime protocol, enforce module and host-execution boundaries, and prove an independently authored command runtime across lifecycle, PTY, files, Git, providers, and CI.
2026-08-11 22:14:47 +02:00

1247 lines
45 KiB
TypeScript

import { execFile, execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, watch } from "node:fs";
import {
chmod,
copyFile,
mkdir,
mkdtemp,
readFile,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { beforeAll, expect, onTestFinished, test, vi } from "vitest";
import { createWorkspaceRuntimeService } from "./index.js";
import { WorkspaceGitServiceImpl } from "../workspace-git-service.js";
import { createTestLogger } from "../../test-utils/test-logger.js";
import { DaemonClient } from "../test-utils/daemon-client.js";
import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js";
import { allocateWorkspaceServicePort } from "../workspace-service-port-allocator.js";
import {
createPersistedProjectRecord,
createPersistedWorkspaceRecord,
FileBackedProjectRegistry,
FileBackedWorkspaceRegistry,
} from "../workspace-registry.js";
const repositoryRoot = fileURLToPath(new URL("../../../../..", import.meta.url));
const dockerRuntimeRoot = path.join(repositoryRoot, "packages/docker-workspace-runtime");
const runtimeExecutable = path.join(dockerRuntimeRoot, "src/index.ts");
const image = "paseo-workspace-runtime-slice1:test";
const fixtureAgent = fileURLToPath(
new URL("../test-utils/fixtures/workspace-runtime-acp-agent.mjs", import.meta.url),
);
const run = promisify(execFile);
beforeAll(() => {
execFileSync(
"docker",
["build", "-t", image, "-f", path.join(dockerRuntimeRoot, "Dockerfile"), repositoryRoot],
{ stdio: "pipe" },
);
}, 120_000);
async function collect(stream: NodeJS.ReadableStream): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of stream) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
return Buffer.concat(chunks).toString("utf8");
}
test("the command runtime owns Docker workspace materialization, pipes, and lifecycle", async () => {
const fixture = await dockerTestFixture("paseo-runtime-docker-");
const root = fixture.root;
const repo = await createRepository(root);
await writeFile(path.join(repo, ".env"), "must-not-enter-runtime\n");
const runtimeIds = new Map<string, string>();
const options = {
paseoHome: path.join(root, "paseo-home"),
resolveRuntimeId: async (workspaceId: string) => runtimeIds.get(workspaceId) ?? null,
persistRuntimeId: async (workspaceId: string, runtimeId: string) => {
runtimeIds.set(workspaceId, runtimeId);
},
beginWorkspaceDeletion: async () => {},
removeWorkspaceRecord: async (workspaceId: string) => {
runtimeIds.delete(workspaceId);
},
externalRuntimes: {
docker: {
type: "command",
command: [process.execPath, "--import", "tsx", runtimeExecutable] as const,
options: { image },
},
},
};
const service = createWorkspaceRuntimeService(options);
const workspaceId = `docker-${Date.now()}`;
fixture.workspace(workspaceId);
try {
const workspace = await service.create({
workspaceId,
runtimeId: "docker",
project: {
id: "docker-project",
source: { kind: "host-directory", path: repo },
},
placement: { kind: "existing" },
});
expect(workspace).toEqual({ workspaceId, runtimeId: "docker", cwd: "/workspace" });
await expect(service.inspect(workspaceId)).resolves.toEqual({
status: "ready",
cwd: "/workspace",
});
await expect(service.requireHostVisiblePath(workspaceId)).rejects.toThrow(
`Workspace has no host-visible path: ${workspaceId}`,
);
for (const [name, value] of [
["user.email", "test@example.com"],
["user.name", "Paseo Test"],
] as const) {
const configure = await service.run({
workspaceId,
argv: ["git", "config", name, value],
env: { PATH: "/usr/local/bin:/usr/bin:/bin" },
purpose: { kind: "git" },
});
configure.stdin.end();
await expect(configure.exited).resolves.toEqual({ code: 0, signal: null });
}
const workspaceGitService = new WorkspaceGitServiceImpl({
logger: createLogger(),
paseoHome: options.paseoHome,
workspaceRuntime: service,
});
const workspaceGit = workspaceGitService.bindWorkspace({ workspaceId, cwd: repo });
const files = service.files(workspaceId);
const listing = await files.list(".");
expect(listing.entries.map((entry) => entry.name)).toContain("committed.txt");
const initialFile = await files.stat("committed.txt");
expect(initialFile).toMatchObject({ status: "ready", size: 10 });
let resolveFileChange!: () => void;
const fileChanged = new Promise<void>((resolve) => {
resolveFileChange = resolve;
});
const fileSubscription = await files.subscribe({ paths: ["committed.txt"] }, (event) => {
if (event.type === "changed" && event.paths.includes("committed.txt")) resolveFileChange();
});
const terminalFileEdit = await service.run({
workspaceId,
argv: ["/bin/sh", "-c", "printf container-change > committed.txt"],
env: {},
purpose: { kind: "terminal", terminalId: "docker-file-edit" },
});
terminalFileEdit.stdin.end();
await expect(terminalFileEdit.exited).resolves.toEqual({ code: 0, signal: null });
await expect(fileChanged).resolves.toBeUndefined();
const dirtyGit = await workspaceGit.getSnapshot({
force: true,
includeForge: false,
reason: "docker-edit",
});
const dockerDiff = await workspaceGit.getCheckoutDiff(
{ mode: "uncommitted", includeStructured: true },
{ force: true, reason: "docker-edit" },
);
expect(dirtyGit.git).toMatchObject({ isGit: true, isDirty: true, currentBranch: "main" });
expect(JSON.stringify(dockerDiff)).toContain("container-change");
await workspaceGit.commit({ message: "container commit", addAll: true });
await workspaceGit.createBranch({ branch: "container-branch", baseRef: "main" });
await workspaceGit.switchBranch("main");
await workspaceGit.fetch();
const cleanGit = await workspaceGit.getSnapshot();
expect(cleanGit.git).toMatchObject({ isDirty: false, currentBranch: "main" });
expect(await readFileFromHost(repo, "committed.txt")).toBe("committed\n");
const containerFile = await files.read("committed.txt");
await expect(collect(containerFile.chunks)).resolves.toBe("container-change");
const conflict = await files.write({
path: "committed.txt",
contents: Buffer.from("stale"),
...(initialFile.status === "ready" ? { expectedRevision: initialFile.revision } : {}),
});
expect(conflict).toMatchObject({ status: "conflict" });
const large = await files.read("large.bin");
expect(await collectBytes(large.chunks)).toEqual(Buffer.alloc(700_000, 0xa5));
await expect(files.read("outside-link")).rejects.toThrow(
"Access outside of workspace is not allowed",
);
await fileSubscription.unsubscribe();
const write = await service.run({
workspaceId,
argv: ["/bin/sh", "-c", "cat > runtime-owned.txt; printf runtime-stderr >&2; exit 19"],
env: {},
purpose: { kind: "workspace-script", script: "write-contract" },
});
write.stdin.end("docker-state");
const [stderr, writeExit] = await Promise.all([collect(write.stderr), write.exited]);
expect(stderr).toBe("runtime-stderr");
expect(writeExit).toEqual({ code: 19, signal: null });
const env = await service.run({
workspaceId,
argv: ["/usr/bin/env"],
env: { RUNTIME_EXACT_ENV: "docker" },
purpose: { kind: "workspace-script", script: "environment-contract" },
});
env.stdin.end();
await expect(collect(env.stdout)).resolves.toBe("RUNTIME_EXACT_ENV=docker\n");
await expect(env.exited).resolves.toEqual({ code: 0, signal: null });
await service.pause(workspaceId);
await expect(
service.run({
workspaceId,
argv: ["/bin/true"],
env: {},
purpose: { kind: "workspace-script", script: "paused-contract" },
}),
).rejects.toThrow(`Workspace runtime is paused: ${workspaceId}`);
const recovered = createWorkspaceRuntimeService(options);
await recovered.resume(workspaceId);
const escapedCwd = await recovered.run({
workspaceId,
cwd: "../escape",
argv: ["/bin/true"],
env: {},
purpose: { kind: "workspace-script", script: "cwd-contract" },
});
escapedCwd.stdin.end();
await expect(escapedCwd.exited).rejects.toThrow("Workspace cwd escapes its root");
const read = await recovered.run({
workspaceId,
argv: ["/bin/cat", "runtime-owned.txt"],
env: {},
purpose: { kind: "workspace-script", script: "read-contract" },
});
read.stdin.end();
await expect(collect(read.stdout)).resolves.toBe("docker-state");
await expect(read.exited).resolves.toEqual({ code: 0, signal: null });
const terminal = await recovered.openTerminal({
workspaceId,
argv: [
"/usr/local/bin/node",
"-e",
"process.stdin.setEncoding('utf8');process.stdout.write(`${process.cwd()}|${process.stdout.isTTY}|${process.stdout.columns}x${process.stdout.rows}|λ`);process.stdin.once('data',data=>{process.stdout.write(`|${data.trim()}|${process.stdout.columns}x${process.stdout.rows}`);process.exit(6)})",
],
env: { PATH: "/usr/local/bin:/usr/bin:/bin" },
purpose: { kind: "terminal", terminalId: "docker-contract-terminal" },
rows: 24,
cols: 80,
});
let terminalOutput = "";
terminal.onData((data) => {
terminalOutput += data;
});
await Promise.race([
vi.waitFor(() => expect(terminalOutput).toContain("/workspace|true|80x24|λ"), {
timeout: 10_000,
}),
terminal.exited.then((exit) => {
throw new Error(`Docker PTY exited before initial output: ${JSON.stringify(exit)}`);
}),
]);
terminal.resize(103, 39);
terminal.write("héllo\n");
await expect(terminal.exited).resolves.toEqual({ code: 6, signal: null });
expect(terminalOutput).toContain("|héllo|103x39");
const terminalSignal = await recovered.openTerminal({
workspaceId,
argv: ["/bin/sleep", "30"],
env: { PATH: "/usr/bin:/bin" },
purpose: { kind: "terminal", terminalId: "docker-signal-terminal" },
rows: 24,
cols: 80,
});
terminalSignal.kill("SIGTERM");
await expect(terminalSignal.exited).resolves.toEqual({ code: null, signal: "SIGTERM" });
await vi.waitFor(() => expect(dockerProcesses(workspaceId)).not.toContain("sleep 30"));
const terminalForced = await recovered.openTerminal({
workspaceId,
argv: ["/bin/sleep", "30"],
env: { PATH: "/usr/bin:/bin" },
purpose: { kind: "terminal", terminalId: "docker-force-terminal" },
rows: 24,
cols: 80,
});
terminalForced.kill("SIGKILL");
await expect(terminalForced.exited).resolves.toEqual({ code: null, signal: "SIGKILL" });
await vi.waitFor(() => expect(dockerProcesses(workspaceId)).not.toContain("sleep 30"));
const terminalControlFailure = await recovered.openTerminal({
workspaceId,
argv: ["/bin/sleep", "30"],
env: { PATH: "/usr/bin:/bin" },
purpose: { kind: "terminal", terminalId: "docker-control-failure-terminal" },
rows: 24,
cols: 80,
});
terminalControlFailure.kill("SIGUSR1");
await expect(terminalControlFailure.exited).rejects.toThrow("Unsupported signal: SIGUSR1");
expect(dockerProcesses(workspaceId)).not.toContain("sleep 30");
const committedOnly = await recovered.run({
workspaceId,
argv: ["/bin/sh", "-c", "test ! -e .env"],
env: {},
purpose: { kind: "workspace-script", script: "materialization-contract" },
});
committedOnly.stdin.end();
await expect(committedOnly.exited).resolves.toEqual({ code: 0, signal: null });
expect(execFileSync("git", ["status", "--short"], { cwd: repo, encoding: "utf8" })).toBe(
"?? .env\n",
);
const killed = await recovered.run({
workspaceId,
argv: ["/bin/sh", "-c", "sleep 30 & echo ready; wait"],
env: { PATH: "/usr/bin:/bin" },
purpose: { kind: "workspace-script", script: "signal-contract" },
});
killed.stdin.end();
await expect(waitForOutput(killed.stdout, "ready")).resolves.toContain("ready");
const killedAt = Date.now();
killed.kill("SIGTERM");
await expect(killed.exited).resolves.toEqual({ code: null, signal: "SIGTERM" });
expect(Date.now() - killedAt).toBeLessThan(5_000);
await vi.waitFor(() => expect(dockerProcesses(workspaceId)).not.toContain("sleep 30"));
const stubborn = await recovered.run({
workspaceId,
argv: [
"/usr/local/bin/node",
"-e",
"process.on('SIGTERM',()=>{});console.log('stubborn-ready');setInterval(()=>{},1000)",
],
env: {},
purpose: { kind: "workspace-script", script: "escalation-contract" },
});
stubborn.stdin.end();
await expect(waitForOutput(stubborn.stdout, "stubborn-ready")).resolves.toContain(
"stubborn-ready",
);
const pauseStartedAt = Date.now();
await recovered.pause(workspaceId);
expect(Date.now() - pauseStartedAt).toBeLessThan(5_000);
await expect(stubborn.exited).resolves.toEqual({ code: null, signal: "SIGTERM" });
expect(
execFileSync(
"docker",
["inspect", "--format", "{{.State.Running}}", dockerResourceName(workspaceId)],
{
encoding: "utf8",
},
).trim(),
).toBe("false");
await workspaceGitService.dispose();
await recovered.destroy(workspaceId);
expect(dockerResourceCount("ps", workspaceId)).toBe(0);
expect(dockerResourceCount("volume", workspaceId)).toBe(0);
expect(await readFileFromHost(repo, "runtime-owned.txt")).toBeNull();
await expect(
recovered.run({
workspaceId,
argv: ["/bin/true"],
env: {},
purpose: { kind: "workspace-script", script: "missing-contract" },
}),
).rejects.toThrow(`Workspace runtime is not selected: ${workspaceId}`);
} finally {
await service.destroy(workspaceId).catch(() => undefined);
}
}, 180_000);
test("Docker buffers immediate pipe signals until the workload identity is authoritative", async () => {
const fixture = await dockerTestFixture("paseo-runtime-docker-started-");
const root = fixture.root;
const repo = await createRepository(root);
const workspaceId = `docker-started-${Date.now()}`;
fixture.workspace(workspaceId);
const runtimeIds = new Map<string, string>();
const service = createWorkspaceRuntimeService({
paseoHome: path.join(root, "paseo-home"),
resolveRuntimeId: async (id) => runtimeIds.get(id) ?? null,
persistRuntimeId: async (id, runtimeId) => {
runtimeIds.set(id, runtimeId);
},
beginWorkspaceDeletion: async () => {},
removeWorkspaceRecord: async (id) => {
runtimeIds.delete(id);
},
externalRuntimes: {
docker: {
type: "command",
command: [process.execPath, "--import", "tsx", runtimeExecutable],
options: { image },
},
},
});
const originalPath = process.env.PATH;
const originalBarrier = process.env.PASEO_DOCKER_TEST_BARRIER;
try {
await service.create({
workspaceId,
runtimeId: "docker",
project: { id: "started-project", source: { kind: "host-directory", path: repo } },
placement: { kind: "existing" },
});
const shimDirectory = path.join(root, "docker-shim");
await mkdir(shimDirectory);
const realDocker = execFileSync("which", ["docker"], { encoding: "utf8" }).trim();
const shim = path.join(shimDirectory, "docker");
await writeFile(shim, dockerBarrierShim(realDocker));
await chmod(shim, 0o755);
process.env.PATH = `${shimDirectory}:${originalPath ?? ""}`;
for (let attempt = 0; attempt < 6; attempt += 1) {
const barrier = path.join(root, `barrier-${attempt}`);
await mkdir(barrier);
process.env.PASEO_DOCKER_TEST_BARRIER = barrier;
const entered = nextFile(barrier, "entered");
const workload = await service.run({
workspaceId,
argv: ["/bin/sleep", "3"],
env: { PATH: "/usr/bin:/bin" },
purpose: { kind: "workspace-script", script: `immediate-signal-${attempt}` },
});
workload.stdin.end();
await entered;
const killedAt = Date.now();
workload.kill("SIGTERM");
await writeFile(path.join(barrier, "release"), "release");
await expect(workload.exited).resolves.toEqual({ code: null, signal: "SIGTERM" });
expect(Date.now() - killedAt).toBeLessThan(1_500);
expect(dockerProcesses(workspaceId)).not.toContain("sleep 3");
}
const shortBarrier = path.join(root, "barrier-short-exit");
await mkdir(shortBarrier);
process.env.PASEO_DOCKER_TEST_BARRIER = shortBarrier;
const shortEntered = nextFile(shortBarrier, "entered");
const short = await service.run({
workspaceId,
argv: ["/bin/true"],
env: {},
purpose: { kind: "workspace-script", script: "exit-around-started" },
});
short.stdin.end();
await shortEntered;
await writeFile(path.join(shortBarrier, "release"), "release");
await expect(short.exited).resolves.toEqual({ code: 0, signal: null });
delete process.env.PASEO_DOCKER_TEST_BARRIER;
const later = await service.run({
workspaceId,
argv: ["/bin/true"],
env: {},
purpose: { kind: "workspace-script", script: "after-immediate-signals" },
});
later.stdin.end();
await expect(later.exited).resolves.toEqual({ code: 0, signal: null });
} finally {
process.env.PATH = originalPath;
if (originalBarrier === undefined) delete process.env.PASEO_DOCKER_TEST_BARRIER;
else process.env.PASEO_DOCKER_TEST_BARRIER = originalBarrier;
await service.destroy(workspaceId).catch(() => undefined);
}
}, 120_000);
test("a selected Docker service port script executes in the container and leaves the host decoy untouched", async () => {
const fixture = await dockerTestFixture("paseo-runtime-docker-port-script-");
const root = fixture.root;
const repo = await createRepository(root);
const script = path.join(repo, "port-script.sh");
await writeFile(
script,
"#!/bin/sh\nprintf runtime > runtime-port-script.txt\nprintf '41234\\n'\n",
);
await chmod(script, 0o755);
execFileSync("git", ["add", "port-script.sh"], { cwd: repo });
execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "port script"], {
cwd: repo,
});
const workspaceId = `docker-port-script-${Date.now()}`;
fixture.workspace(workspaceId);
const runtimeIds = new Map<string, string>();
const service = createWorkspaceRuntimeService({
paseoHome: path.join(root, "paseo-home"),
resolveRuntimeId: async (id) => runtimeIds.get(id) ?? null,
persistRuntimeId: async (id, runtimeId) => {
runtimeIds.set(id, runtimeId);
},
beginWorkspaceDeletion: async () => {},
removeWorkspaceRecord: async (id) => {
runtimeIds.delete(id);
},
externalRuntimes: {
docker: {
type: "command",
command: [process.execPath, "--import", "tsx", runtimeExecutable],
options: { image },
},
},
});
try {
await service.create({
workspaceId,
runtimeId: "docker",
project: { id: "docker-port-project", source: { kind: "host-directory", path: repo } },
placement: { kind: "existing" },
});
await writeFile(script, "#!/bin/sh\nprintf host > host-port-script.txt\nprintf '49999\\n'\n");
await expect(
allocateWorkspaceServicePort({
allocation: { portScript: "./port-script.sh" },
cwd: repo,
scriptName: "web",
workspaceId,
branchName: null,
runtime: await service.bind(workspaceId),
}),
).resolves.toBe(41234);
await expect(service.files(workspaceId).stat("runtime-port-script.txt")).resolves.toMatchObject(
{
status: "ready",
},
);
await expect(readFile(path.join(repo, "host-port-script.txt"), "utf8")).rejects.toMatchObject({
code: "ENOENT",
});
} finally {
await service.destroy(workspaceId).catch(() => undefined);
}
}, 120_000);
test("public daemon and client keep Docker provider discovery, launch, files, Git, and snapshots in one runtime", async () => {
const fixture = await dockerTestFixture("paseo-runtime-docker-daemon-");
const root = fixture.root;
const repo = await createRepository(root);
await writeFile(path.join(repo, "provider-model.txt"), "fixture-model-v1\n");
execFileSync("git", ["add", "provider-model.txt"], { cwd: repo });
execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "provider model"], {
cwd: repo,
});
const paseoHomeRoot = path.join(root, "daemon-home");
const paseoHome = path.join(paseoHomeRoot, ".paseo");
await mkdir(paseoHome, { recursive: true });
const runtimeConfig = {
type: "command" as const,
command: [process.execPath, "--import", "tsx", runtimeExecutable] as const,
options: { image },
};
const configuredRuntimeConfig = {
...runtimeConfig,
providerEnvironment: { PASEO_DAEMON_ONLY_PROVIDER_ENV: "runtime-configured" },
};
const runtimeConfigs = { docker: runtimeConfig, "docker-configured": configuredRuntimeConfig };
const workspaceIds = [`docker-daemon-a-${Date.now()}`, `docker-daemon-b-${Date.now()}`];
for (const workspaceId of workspaceIds) fixture.workspace(workspaceId);
const hostProbeMarker = path.join(root, "host-provider-probed.txt");
const hostOnlyProvider = path.join(root, "host-only-provider.mjs");
await writeFile(
hostOnlyProvider,
`#!/usr/bin/env node\nawait import("node:fs/promises").then((fs) => fs.appendFile(${JSON.stringify(hostProbeMarker)}, \`${"${process.cwd()}"}\\n\`));\n${(await readFile(fixtureAgent, "utf8")).replace(/^#![^\n]*\n/u, "")}`,
);
await chmod(hostOnlyProvider, 0o755);
const selectedRuntimeIds = new Map([
[workspaceIds[0]!, "docker"],
[workspaceIds[1]!, "docker-configured"],
]);
const seedRuntime = createWorkspaceRuntimeService({
paseoHome,
externalRuntimes: runtimeConfigs,
resolveRuntimeId: async (workspaceId) => selectedRuntimeIds.get(workspaceId) ?? null,
persistRuntimeId: async (workspaceId, runtimeId) => {
selectedRuntimeIds.set(workspaceId, runtimeId);
},
beginWorkspaceDeletion: async () => {},
removeWorkspaceRecord: async (workspaceId) => {
selectedRuntimeIds.delete(workspaceId);
},
});
const projectRegistry = new FileBackedProjectRegistry(
path.join(paseoHome, "projects", "projects.json"),
createTestLogger(),
);
const workspaceRegistry = new FileBackedWorkspaceRegistry(
path.join(paseoHome, "projects", "workspaces.json"),
createTestLogger(),
);
await projectRegistry.initialize();
await workspaceRegistry.initialize();
const now = new Date().toISOString();
await projectRegistry.upsert(
createPersistedProjectRecord({
projectId: "docker-daemon-project",
rootPath: repo,
kind: "git",
displayName: "docker-daemon-project",
createdAt: now,
updatedAt: now,
}),
);
for (const workspaceId of workspaceIds) {
const runtimeId = selectedRuntimeIds.get(workspaceId)!;
await seedRuntime.create({
workspaceId,
runtimeId,
project: { id: "docker-daemon-project", source: { kind: "host-directory", path: repo } },
placement: { kind: "existing" },
});
await workspaceRegistry.upsert(
createPersistedWorkspaceRecord({
workspaceId,
projectId: "docker-daemon-project",
cwd: repo,
kind: "local_checkout",
displayName: workspaceId,
runtime: { runtimeId },
createdAt: now,
updatedAt: now,
}),
);
}
const daemon = await createTestPaseoDaemon({
paseoHomeRoot,
cleanup: false,
mcpEnabled: false,
agentClients: {},
workspaceRuntimes: runtimeConfigs,
providerOverrides: {
"docker-fixture": {
extends: "acp",
label: "Docker Fixture",
command: ["node", "./fixture-agent.mjs"],
params: { supportsMcpServers: false },
enabled: true,
},
"host-only-fixture": {
extends: "acp",
label: "Host Only",
command: [hostOnlyProvider],
params: { supportsMcpServers: false },
enabled: true,
},
},
});
const publicClient = new DaemonClient({
url: `ws://127.0.0.1:${daemon.port}/ws`,
appVersion: "0.3.0-beta.2",
reconnect: { enabled: false },
});
try {
await publicClient.connect();
await publicClient.fetchAgents({ subscribe: { subscriptionId: "docker-runtime-agents" } });
const recoveredRegistry = new FileBackedWorkspaceRegistry(
path.join(paseoHome, "projects", "workspaces.json"),
createTestLogger(),
);
await recoveredRegistry.initialize();
const recoveredRecords = await recoveredRegistry.list();
const publicCwds = new Map(
recoveredRecords
.filter((workspace) => workspaceIds.includes(workspace.workspaceId))
.map((workspace) => [workspace.workspaceId, workspace.cwd]),
);
expect(Array.from(publicCwds.values())).toEqual(["/workspace", "/workspace"]);
expect(
recoveredRecords
.filter((workspace) => workspaceIds.includes(workspace.workspaceId))
.map((workspace) => workspace.hostVisiblePath),
).toEqual([null, null]);
for (const workspaceId of workspaceIds) {
const cwd = publicCwds.get(workspaceId)!;
const snapshot = await publicClient.getProvidersSnapshot({ cwd, workspaceId });
expect(snapshot.entries).toContainEqual(
expect.objectContaining({
provider: "docker-fixture",
status: "ready",
models: [expect.objectContaining({ id: "fixture-model-v1" })],
}),
);
expect(snapshot.entries).toContainEqual(
expect.objectContaining({ provider: "host-only-fixture", status: "unavailable" }),
);
expect(snapshot.entries).toContainEqual(
expect.objectContaining({ provider: "opencode", status: "unavailable" }),
);
const hostProbeCwds = (await readFileFromHost(root, "host-provider-probed.txt"))
?.trim()
.split("\n");
expect(hostProbeCwds).not.toContain(repo);
}
const workspaceId = workspaceIds[0]!;
const cwd = publicCwds.get(workspaceId)!;
const workspaceGit = publicClient.bindWorkspaceGit({ workspaceId, cwd });
await expect(workspaceGit.getStatus()).resolves.toMatchObject({
isGit: true,
isDirty: false,
});
const agent = await publicClient.createAgent({
provider: "docker-fixture",
model: "fixture-model-v1",
cwd,
workspaceId,
title: "Docker public fake provider",
});
await publicClient.sendMessage(agent.id, "public docker edit");
await expect(publicClient.waitForFinish(agent.id, 30_000)).resolves.toMatchObject({
status: "idle",
});
const edited = await publicClient.readFile(
cwd,
"stdio-agent-output.txt",
undefined,
workspaceId,
);
expect(new TextDecoder().decode(edited.bytes)).toBe("public docker edit\n");
const editedTrackedFile = await publicClient.readFile(
cwd,
"committed.txt",
undefined,
workspaceId,
);
expect(new TextDecoder().decode(editedTrackedFile.bytes)).toBe("public docker edit\n");
const dirtyDiff = await workspaceGit.getDiff({ mode: "uncommitted" });
expect(dirtyDiff.files.map((file) => file.path)).toContain("committed.txt");
const isolatedEnvironment = await publicClient.readFile(
cwd,
"stdio-agent-env.txt",
undefined,
workspaceId,
);
expect(new TextDecoder().decode(isolatedEnvironment.bytes)).toBe("<absent>\n");
const configuredAgent = await publicClient.createAgent({
provider: "docker-fixture",
model: "fixture-model-v1",
cwd: publicCwds.get(workspaceIds[1]!)!,
workspaceId: workspaceIds[1],
title: "Configured Docker public fake provider",
});
await publicClient.sendMessage(configuredAgent.id, "configured public docker edit");
await expect(publicClient.waitForFinish(configuredAgent.id, 30_000)).resolves.toMatchObject({
status: "idle",
});
const configuredEnvironment = await publicClient.readFile(
publicCwds.get(workspaceIds[1]!)!,
"stdio-agent-env.txt",
undefined,
workspaceIds[1],
);
expect(new TextDecoder().decode(configuredEnvironment.bytes)).toBe("runtime-configured\n");
await seedRuntime.destroy(workspaceId);
await writeFile(path.join(repo, "provider-model.txt"), "fixture-model-v2\n");
execFileSync("git", ["add", "provider-model.txt"], { cwd: repo });
execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "provider model v2"], {
cwd: repo,
});
await seedRuntime.create({
workspaceId,
runtimeId: "docker",
project: { id: "docker-daemon-project", source: { kind: "host-directory", path: repo } },
placement: { kind: "existing" },
});
const rebuiltA = await publicClient.getProvidersSnapshot({ cwd, workspaceId });
expect(rebuiltA.entries).toContainEqual(
expect.objectContaining({
provider: "docker-fixture",
models: [expect.objectContaining({ id: "fixture-model-v2" })],
}),
);
const unchangedB = await publicClient.getProvidersSnapshot({
cwd: publicCwds.get(workspaceIds[1]!)!,
workspaceId: workspaceIds[1],
});
expect(unchangedB.entries).toContainEqual(
expect.objectContaining({
provider: "docker-fixture",
models: [expect.objectContaining({ id: "fixture-model-v1" })],
}),
);
} finally {
await publicClient.close().catch(() => undefined);
await daemon.close();
await Promise.all(workspaceIds.map((workspaceId) => seedRuntime.destroy(workspaceId)));
}
}, 180_000);
test("Docker creation validates roots and rolls back when runtime selection cannot persist", async () => {
const fixture = await dockerTestFixture("paseo-runtime-docker-rollback-");
const root = fixture.root;
const repo = await createRepository(root);
const workspaceId = `docker-rollback-${Date.now()}`;
fixture.workspace(workspaceId);
fixture.workspace(`${workspaceId}-escape`);
const service = createWorkspaceRuntimeService({
paseoHome: path.join(root, "paseo-home"),
resolveRuntimeId: async () => null,
persistRuntimeId: async () => {
throw new Error("persistence failed");
},
externalRuntimes: {
docker: {
type: "command",
command: [process.execPath, "--import", "tsx", runtimeExecutable],
options: { image },
},
},
});
await expect(
service.create({
workspaceId: `${workspaceId}-escape`,
runtimeId: "docker",
project: { id: "docker-project", source: { kind: "host-directory", path: repo } },
placement: { kind: "existing", relativeCwd: "../escape" },
}),
).rejects.toThrow("Workspace cwd escapes its root");
expect(dockerResourceCount("ps", `${workspaceId}-escape`)).toBe(0);
expect(dockerResourceCount("volume", `${workspaceId}-escape`)).toBe(0);
await expect(
service.create({
workspaceId,
runtimeId: "docker",
project: { id: "docker-project", source: { kind: "host-directory", path: repo } },
placement: { kind: "existing" },
}),
).rejects.toThrow("persistence failed");
expect(dockerResourceCount("ps", workspaceId)).toBe(0);
expect(dockerResourceCount("volume", workspaceId)).toBe(0);
}, 120_000);
test("Docker rejects a committed symlink cwd that resolves outside the selected root", async () => {
const fixture = await dockerTestFixture("paseo-runtime-docker-symlink-");
const root = fixture.root;
const repo = await createRepository(root);
await mkdir(path.join(repo, "selected"));
await symlink("..", path.join(repo, "selected", "escape"));
execFileSync("git", ["add", "."], { cwd: repo });
execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "symlink fixture"], {
cwd: repo,
});
const workspaceId = `docker-symlink-${Date.now()}`;
fixture.workspace(workspaceId);
const runtimeIds = new Map<string, string>();
const service = createWorkspaceRuntimeService({
paseoHome: path.join(root, "paseo-home"),
resolveRuntimeId: async (id) => runtimeIds.get(id) ?? null,
persistRuntimeId: async (id, runtimeId) => {
runtimeIds.set(id, runtimeId);
},
beginWorkspaceDeletion: async () => {},
removeWorkspaceRecord: async (id) => {
runtimeIds.delete(id);
},
externalRuntimes: {
docker: {
type: "command",
command: [process.execPath, "--import", "tsx", runtimeExecutable],
options: { image },
},
},
});
try {
await service.create({
workspaceId,
runtimeId: "docker",
project: { id: "symlink-project", source: { kind: "host-directory", path: repo } },
placement: { kind: "existing", relativeCwd: "selected" },
});
const escaped = await service.run({
workspaceId,
cwd: "escape",
argv: ["/bin/sh", "-c", "touch escaped-marker"],
env: {},
purpose: { kind: "workspace-script", script: "realpath-confinement" },
});
escaped.stdin.end();
const escapedError = collect(escaped.stderr);
await expect(escaped.exited).rejects.toThrow("Workspace cwd escapes its root");
expect(await escapedError).toContain("Workspace cwd escapes its root");
const verify = await service.run({
workspaceId,
argv: ["/bin/sh", "-c", "test ! -e ../escaped-marker"],
env: {},
purpose: { kind: "workspace-script", script: "realpath-confinement-proof" },
});
verify.stdin.end();
await expect(verify.exited).resolves.toEqual({ code: 0, signal: null });
} finally {
await service.destroy(workspaceId).catch(() => undefined);
}
}, 120_000);
test("the Docker runtime never adopts or destroys an unowned deterministic resource", async () => {
const fixture = await dockerTestFixture("paseo-runtime-docker-ownership-");
const root = fixture.root;
const workspaceId = `docker-ownership-${Date.now()}`;
const volume = dockerResourceName(workspaceId);
fixture.workspace(workspaceId);
execFileSync("docker", ["volume", "create", volume]);
const runtimeIds = new Map([[workspaceId, "docker"]]);
const service = createWorkspaceRuntimeService({
paseoHome: path.join(root, "paseo-home"),
resolveRuntimeId: async (id) => runtimeIds.get(id) ?? null,
persistRuntimeId: async (id, runtimeId) => {
runtimeIds.set(id, runtimeId);
},
beginWorkspaceDeletion: async () => {},
removeWorkspaceRecord: async (id) => {
runtimeIds.delete(id);
},
externalRuntimes: {
docker: {
type: "command",
command: [process.execPath, "--import", "tsx", runtimeExecutable],
options: { image },
},
},
});
try {
await expect(
service.create({
workspaceId,
runtimeId: "docker",
project: { id: "ownership-project", source: { kind: "host-directory", path: root } },
placement: { kind: "existing" },
}),
).rejects.toThrow(`Docker volume ownership mismatch: ${volume}`);
await expect(service.destroy(workspaceId)).rejects.toThrow(
`Docker volume ownership mismatch: ${volume}`,
);
expect(
execFileSync("docker", ["volume", "inspect", "--format", "{{.Name}}", volume], {
encoding: "utf8",
}).trim(),
).toBe(volume);
} finally {
execFileSync("docker", ["volume", "rm", volume]);
}
}, 30_000);
test("Docker reconciliation removes only orphaned resources with deterministic names and ownership labels", async () => {
const fixture = await dockerTestFixture("paseo-runtime-docker-reconcile-");
const root = fixture.root;
const orphanWorkspaceId = `docker-orphan-${Date.now()}`;
const foreignWorkspaceId = `${orphanWorkspaceId}-foreign`;
const orphanVolume = dockerResourceName(orphanWorkspaceId);
const foreignVolume = `${dockerResourceName(foreignWorkspaceId)}-foreign`;
fixture.volume(orphanVolume);
fixture.volume(foreignVolume);
execFileSync("docker", [
"volume",
"create",
"--label",
"sh.paseo.runtime=workspace",
"--label",
`sh.paseo.workspace-id=${orphanWorkspaceId}`,
orphanVolume,
]);
execFileSync("docker", [
"volume",
"create",
"--label",
"sh.paseo.runtime=workspace",
"--label",
`sh.paseo.workspace-id=${foreignWorkspaceId}`,
foreignVolume,
]);
const service = createWorkspaceRuntimeService({
paseoHome: path.join(root, "paseo-home"),
resolveRuntimeId: async () => null,
persistRuntimeId: async () => undefined,
beginWorkspaceDeletion: async () => {},
removeWorkspaceRecord: async () => {},
listRuntimeRecords: async () => [],
externalRuntimes: {
docker: {
type: "command",
command: [process.execPath, "--import", "tsx", runtimeExecutable],
options: { image },
},
},
});
try {
await service.reconcile();
expect(dockerResourceCount("volume", orphanWorkspaceId)).toBe(0);
expect(
execFileSync("docker", ["volume", "inspect", "--format", "{{.Name}}", foreignVolume], {
encoding: "utf8",
}).trim(),
).toBe(foreignVolume);
} finally {
execFileSync("docker", ["volume", "rm", "-f", orphanVolume, foreignVolume], {
stdio: "ignore",
});
}
}, 30_000);
async function dockerTestFixture(prefix: string): Promise<{
root: string;
workspace(workspaceId: string): void;
volume(name: string): void;
}> {
const root = await mkdtemp(path.join(tmpdir(), prefix));
const containers = new Set<string>();
const volumes = new Set<string>();
const workspaceIds = new Set<string>();
onTestFinished(async () => {
await Promise.all(
[...containers].map((name) =>
run("docker", ["rm", "-f", name], { timeout: 10_000 }).catch(() => undefined),
),
);
await Promise.all(
[...volumes].map((name) =>
run("docker", ["volume", "rm", "-f", name], { timeout: 10_000 }).catch(() => undefined),
),
);
const leakedContainers = [...containers].filter((name) =>
dockerResourceExists("container", name),
);
const leakedVolumes = [...volumes].filter((name) => dockerResourceExists("volume", name));
const runningProcesses = dockerTestProcessPids(workspaceIds);
if (runningProcesses.length > 0) {
await run("kill", ["-KILL", ...runningProcesses.map(String)], { timeout: 5_000 }).catch(
() => undefined,
);
}
const leakedProcesses = dockerTestProcessPids(workspaceIds);
await rm(root, { recursive: true, force: true });
if (leakedContainers.length || leakedVolumes.length || leakedProcesses.length) {
throw new Error(
`Docker test leaked containers=${leakedContainers.join(",")} volumes=${leakedVolumes.join(",")} processes=${leakedProcesses.join(",")}`,
);
}
}, 30_000);
return {
root,
workspace(workspaceId) {
workspaceIds.add(workspaceId);
const name = dockerResourceName(workspaceId);
containers.add(name);
volumes.add(name);
},
volume(name) {
volumes.add(name);
},
};
}
function dockerResourceExists(kind: "container" | "volume", name: string): boolean {
try {
execFileSync("docker", [kind, "inspect", name], { stdio: "ignore", timeout: 5_000 });
return true;
} catch {
return false;
}
}
function dockerTestProcessPids(workspaceIds: ReadonlySet<string>): number[] {
if (workspaceIds.size === 0) return [];
const output = execFileSync("ps", ["-axo", "pid=,command="], {
encoding: "utf8",
timeout: 5_000,
});
const needles = [...workspaceIds].flatMap((workspaceId) => [
dockerResourceName(workspaceId),
`--workspace-id ${workspaceId}`,
]);
return output
.split("\n")
.map((line) => line.match(/^\s*(\d+)\s+(.*)$/))
.filter(
(match): match is RegExpMatchArray =>
!!match && needles.some((needle) => match[2]!.includes(needle)),
)
.map((match) => Number(match[1]));
}
async function createRepository(root: string): Promise<string> {
const repo = path.join(root, "repo");
await mkdir(repo);
execFileSync("git", ["init", "-b", "main"], { cwd: repo, stdio: "pipe" });
execFileSync("git", ["config", "user.email", "test@getpaseo.local"], { cwd: repo });
execFileSync("git", ["config", "user.name", "Paseo Test"], { cwd: repo });
await writeFile(path.join(repo, "committed.txt"), "committed\n");
await writeFile(path.join(repo, "large.bin"), Buffer.alloc(700_000, 0xa5));
await copyFile(fixtureAgent, path.join(repo, "fixture-agent.mjs"));
await chmod(path.join(repo, "fixture-agent.mjs"), 0o755);
await symlink("/etc/passwd", path.join(repo, "outside-link"));
execFileSync("git", ["add", "."], { cwd: repo });
execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "fixture"], {
cwd: repo,
});
return repo;
}
async function collectBytes(chunks: AsyncIterable<Uint8Array>): Promise<Buffer> {
const buffers: Buffer[] = [];
for await (const chunk of chunks) buffers.push(Buffer.from(chunk));
return Buffer.concat(buffers);
}
async function waitForOutput(stream: NodeJS.ReadableStream, marker: string): Promise<string> {
return new Promise((resolve, reject) => {
let output = "";
const onData = (chunk: Buffer | string) => {
output += chunk.toString();
if (!output.includes(marker)) return;
stream.off("data", onData);
stream.off("error", onError);
stream.off("end", onEnd);
resolve(output);
};
const onError = (error: Error) => {
stream.off("data", onData);
stream.off("end", onEnd);
reject(error);
};
const onEnd = () => {
stream.off("data", onData);
stream.off("error", onError);
reject(new Error(`Stream ended before output marker: ${marker}`));
};
stream.on("data", onData);
stream.once("error", onError);
stream.once("end", onEnd);
});
}
function dockerProcesses(workspaceId: string): string {
const container = dockerResourceName(workspaceId);
return execFileSync("docker", ["top", container, "-eo", "pid,args"], { encoding: "utf8" });
}
function dockerResourceName(workspaceId: string): string {
return `paseo-ws-${createHash("sha256").update(workspaceId).digest("hex").slice(0, 20)}`;
}
function dockerResourceCount(kind: "ps" | "volume", workspaceId: string): number {
const args =
kind === "ps"
? ["ps", "-aq", "--filter", `label=sh.paseo.workspace-id=${workspaceId}`]
: ["volume", "ls", "-q", "--filter", `label=sh.paseo.workspace-id=${workspaceId}`];
return execFileSync("docker", args, { encoding: "utf8" }).trim() ? 1 : 0;
}
function nextFile(directory: string, expectedName: string): Promise<void> {
return new Promise((resolve, reject) => {
const expected = path.join(directory, expectedName);
const watcher = watch(directory, (_event, filename) => {
if (filename?.toString() !== expectedName) return;
finish();
});
watcher.once("error", reject);
if (existsSync(expected)) finish();
function finish(): void {
watcher.close();
resolve();
}
});
}
function dockerBarrierShim(realDocker: string): string {
return `#!/usr/bin/env node
import { spawn } from "node:child_process";
import { existsSync, watch } from "node:fs";
import { writeFile } from "node:fs/promises";
import path from "node:path";
const args = process.argv.slice(2);
const barrier = process.env.PASEO_DOCKER_TEST_BARRIER;
const readiness = barrier && args.includes("paseo-ready");
const signal = barrier && args.includes("paseo-signal");
if (readiness) await writeFile(path.join(barrier, "readiness-waiting"), "waiting");
if (barrier && args[0] === "exec" && args.includes("/opt/paseo-workspace-runtime/workload.mjs")) {
await writeFile(path.join(barrier, "entered"), "entered");
await nextFile(barrier, "release");
await nextOneOf(barrier, ["readiness-waiting", "signal-finished"]);
}
const child = spawn(${JSON.stringify(realDocker)}, args, { stdio: "inherit" });
child.once("error", error => { process.stderr.write(String(error)); process.exitCode = 127; });
child.once("exit", async (code, exitSignal) => {
if (signal) await writeFile(path.join(barrier, "signal-finished"), "finished");
if (exitSignal) { process.removeAllListeners(exitSignal); process.kill(process.pid, exitSignal); }
else process.exitCode = code ?? 1;
});
function nextFile(directory, name) {
const expected = path.join(directory, name);
if (existsSync(expected)) return Promise.resolve();
return new Promise((resolve, reject) => {
const watcher = watch(directory, (_event, filename) => {
if (filename?.toString() !== name) return;
watcher.close();
resolve();
});
watcher.once("error", reject);
if (existsSync(expected)) { watcher.close(); resolve(); }
});
}
function nextOneOf(directory, names) {
if (names.some(name => existsSync(path.join(directory, name)))) return Promise.resolve();
return new Promise((resolve, reject) => {
const watcher = watch(directory, (_event, filename) => {
if (!filename || !names.includes(filename.toString())) return;
watcher.close();
resolve();
});
watcher.once("error", reject);
if (names.some(name => existsSync(path.join(directory, name)))) { watcher.close(); resolve(); }
});
}
`;
}
async function readFileFromHost(root: string, name: string): Promise<string | null> {
try {
return await (await import("node:fs/promises")).readFile(path.join(root, name), "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
throw error;
}
}
function createLogger() {
const logger = {
child: () => logger,
debug: vi.fn(),
info: vi.fn(),
trace: vi.fn(),
warn: vi.fn(),
};
return logger;
}