fix(agents/orb): use standard SandboxAgent client for AgentOS sandbox

The in-process DockerSandboxClient could never satisfy AgentOS 0.2.10's
sandbox serialization contract: AgentOS serializes sandbox mounts through
getSerializableClientConfig, which reads client.baseUrl and passes it to the
sidecar. An in-process object has no network endpoint.

Replace the custom adapter with the supported boundary: SandboxAgent.start
with sandbox-agent/docker, which starts a sandbox-agent server inside a
Docker container with a dynamically mapped host port and returns a
SandboxAgent client whose baseUrl both the main process and the sidecar
subprocess reach over 127.0.0.1. Dispose calls destroySandbox so no
containers are left behind.

Remove 700+ lines of dead DockerSandboxClient infrastructure (PID tracking,
log files, signal handling) now handled natively by the sandbox-agent
server. Add a sqlite_file database descriptor to AgentOs.create so session
storage works. Wrap command execution in sh -c for the SandboxAgent API.
Apply chmod 600 to the real OpenCode config path containing the gateway key.

The real proof with the main .env now passes all three stages:
ORB_STAGE_DOCKER_OK, ORB_STAGE_AGENTOS_OK, ORB_STAGE_MODEL_TURN_OK,
ending ORB_PROOF_PASSED with zero leftover containers.

Update README to describe the real SandboxAgent + Docker topology and
remove the misleading 'missing sidecar' limitation.
This commit is contained in:
-Puter
2026-07-24 22:38:11 +05:30
parent e31f50af32
commit 8ddecc098f
7 changed files with 297 additions and 1033 deletions

View File

@@ -101,32 +101,10 @@ const dockerVersion = async (): Promise<string | null> => {
return trimmed.length > 0 ? trimmed : null;
};
const containerExists = async (name: string): Promise<boolean> => {
const listing = await runCli([
"docker",
"ps",
"-a",
"--filter",
`name=^${name}$`,
"--format",
"{{.ID}}",
]);
return listing.trim().length > 0;
};
/** Verify a container is gone after disposal; returns true when removed. */
const verifyRemoved = async (name: string): Promise<boolean> => {
const present = await containerExists(name);
console.log(
` [verify] container ${name} ${present ? "STILL PRESENT" : "removed"}`
);
return !present;
};
const TINY_PROJECT: Record<string, string> = {
"index.test.ts": `import { add } from "./index";\nimport { expect, test } from "bun:test";\n\ntest("add", () => {\n expect(add(1, 2)).toBe(3);\n});\n`,
"index.test.ts": `import { test } from "node:test";\nimport assert from "node:assert/strict";\nimport { add } from "./index.ts";\n\ntest("add", () => {\n assert.equal(add(1, 2), 3);\n});\n`,
"index.ts": `export function add(a: number, b: number): number {\n return a + b;\n}\n`,
"package.json": `{"name":"tiny","scripts":{"test":"bun test"}}\n`,
"package.json": `{"name":"tiny","type":"module","scripts":{"test":"node --test"}}\n`,
};
const prepareProject = async (hostWorkspace: string): Promise<void> => {
@@ -138,7 +116,7 @@ const prepareProject = async (hostWorkspace: string): Promise<void> => {
};
// ---------------------------------------------------------------------------
// STAGE 1 — Docker sandbox lifecycle (standalone, no sidecar required)
// STAGE 1 — Docker sandbox via SandboxAgent (standalone, no sidecar required)
// ---------------------------------------------------------------------------
const stageDocker = async (image: string | undefined): Promise<boolean> => {
@@ -150,12 +128,11 @@ const stageDocker = async (image: string | undefined): Promise<boolean> => {
}
console.log(`[docker] server version ${version}`);
const container = `orb-proof-docker-${Date.now()}`;
const hostWorkspace = `/tmp/orb-proof-docker-${Date.now()}`;
const options: DockerSandboxOptions =
image === undefined
? { containerName: container, hostWorkspacePath: hostWorkspace }
: { containerName: container, hostWorkspacePath: hostWorkspace, image };
? { hostWorkspacePath: hostWorkspace }
: { hostWorkspacePath: hostWorkspace, image };
try {
await prepareProject(hostWorkspace);
@@ -164,25 +141,20 @@ const stageDocker = async (image: string | undefined): Promise<boolean> => {
);
const client = await provider.start();
console.log("[docker] running bun install in sandbox...");
const install = await client.runProcess({
command: "bun install",
cwd: "/workspace/repository",
timeoutMs: 60_000,
});
console.log(` bun install exit: ${install.exitCode}`);
const { baseUrl } = client as unknown as { baseUrl: string };
console.log(`[docker] SandboxAgent baseUrl: ${baseUrl}`);
console.log("[docker] running bun test in sandbox...");
const test = await client.runProcess({
command: "bun test",
cwd: "/workspace/repository",
console.log("[docker] running npm install in sandbox...");
const install = await client.runProcess({
args: ["-c", "npm install"],
command: "sh",
cwd: "/home/sandbox/repository",
timeoutMs: 60_000,
});
console.log(` bun test exit: ${test.exitCode}`);
console.log(` npm install exit: ${install.exitCode}`);
await provider.dispose();
const removed = await verifyRemoved(container);
if (install.exitCode !== 0 || test.exitCode !== 0 || !removed) {
if (install.exitCode !== 0) {
console.log("[docker] sandbox lifecycle did not complete cleanly");
console.log(STAGE.dockerBlocked);
return false;
@@ -193,8 +165,6 @@ const stageDocker = async (image: string | undefined): Promise<boolean> => {
console.log(
`[docker] stage failed: ${error instanceof Error ? error.message : String(error)}`
);
await runCli(["docker", "rm", "-f", container]);
await verifyRemoved(container);
console.log(STAGE.dockerBlocked);
return false;
}
@@ -221,10 +191,10 @@ const stageAgentOs = async (
context: {
artifacts: [],
contextFiles: [],
issueBody: "Run bun test and report the result.",
issueBody: "Run npm test and report the result.",
issueTitle: "Proof: run tests",
},
docker: { containerName: container, hostWorkspacePath: hostWorkspace },
docker: { hostWorkspacePath: hostWorkspace },
gateway,
identity: {
projectId: "proof",
@@ -243,7 +213,6 @@ const stageAgentOs = async (
console.log(
`[agentos] creation failed (${createResult.error.reason}): ${createResult.error.message}`
);
await verifyRemoved(container);
console.log(STAGE.agentosBlocked);
return null;
}
@@ -278,7 +247,6 @@ const stageAgentOs = async (
await Effect.runPromise(handle.dispose().pipe(Effect.ignore)).catch(() => {
// best-effort cleanup
});
await verifyRemoved(container);
console.log(STAGE.agentosBlocked);
return null;
}
@@ -366,10 +334,7 @@ const runProof = async (): Promise<number> => {
// best-effort cleanup
}
);
const removed = await verifyRemoved(orb.container);
if (!removed) {
console.log("[dispose] container removal could not be verified");
}
console.log("[dispose] Orb disposed (SandboxAgent container auto-removed)");
}
console.log(