fix: start Docker daemon on sandbox startup, not just after install (#3129)

The sandbox mode now starts the Docker daemon whenever it's not running,
not only after a fresh install. This handles the common case where
OrbStack/Docker is installed but the daemon isn't started yet.

Flow: check daemon → if down, check binary → if missing, install →
start daemon (open -a OrbStack / systemctl start docker) → poll up to 30s

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
A 2026-03-31 17:50:57 -07:00 committed by GitHub
parent c1d8acb73e
commit 426ebc9b76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 140 additions and 36 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.30.0",
"version": "0.30.1",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -72,7 +72,7 @@ describe("ensureDocker", () => {
spy.mockRestore();
});
it("attempts brew install on macOS when docker unavailable", async () => {
it("attempts brew install on macOS when docker not installed", async () => {
const origPlatform = Object.getOwnPropertyDescriptor(process, "platform");
Object.defineProperty(process, "platform", {
value: "darwin",
@ -82,19 +82,7 @@ describe("ensureDocker", () => {
let callCount = 0;
const spy = spyOn(Bun, "spawnSync").mockImplementation((..._args: unknown[]) => {
callCount++;
// First call: docker info → fail, second: brew install → succeed, third: docker info → succeed
if (callCount === 1) {
return {
exitCode: 1,
stdout: new Uint8Array(),
stderr: new Uint8Array(),
success: false,
signalCode: null,
resourceUsage: undefined,
pid: 1234,
} satisfies ReturnType<typeof Bun.spawnSync>;
}
return {
const ok = {
exitCode: 0,
stdout: new Uint8Array(),
stderr: new Uint8Array(),
@ -103,16 +91,37 @@ describe("ensureDocker", () => {
resourceUsage: undefined,
pid: 1234,
} satisfies ReturnType<typeof Bun.spawnSync>;
const fail = {
exitCode: 1,
stdout: new Uint8Array(),
stderr: new Uint8Array(),
success: false,
signalCode: null,
resourceUsage: undefined,
pid: 1234,
} satisfies ReturnType<typeof Bun.spawnSync>;
// 1: docker info → fail, 2: which docker → fail (not installed),
// 3: brew install → ok, 4: open -a OrbStack → ok, 5: docker info → ok
if (callCount <= 2) {
return fail;
}
return ok;
});
await ensureDocker();
// Second call should be brew install orbstack
expect(spy.mock.calls[1][0]).toEqual([
// Call 1: docker info, 2: which docker, 3: brew install orbstack
expect(spy.mock.calls[2][0]).toEqual([
"brew",
"install",
"orbstack",
]);
// Call 4: open -a OrbStack (starts daemon)
expect(spy.mock.calls[3][0]).toEqual([
"open",
"-a",
"OrbStack",
]);
spy.mockRestore();
if (origPlatform) {

View file

@ -68,31 +68,129 @@ export async function interactiveSession(cmd: string): Promise<number> {
// ─── Docker Sandbox ─────────────────────────────────────────────────────────
/** Check whether Docker (or OrbStack) is available on the host. */
/** Check whether the Docker daemon is running and responsive. */
export function isDockerAvailable(): boolean {
const result = Bun.spawnSync(
[
"docker",
"info",
],
{
stdio: [
"ignore",
"ignore",
"ignore",
return (
Bun.spawnSync(
[
"docker",
"info",
],
},
{
stdio: [
"ignore",
"ignore",
"ignore",
],
},
).exitCode === 0
);
return result.exitCode === 0;
}
/** Install Docker if not present, or exit with guidance if install fails. */
/** Check whether the docker binary exists (installed but daemon may be stopped). */
function isDockerInstalled(): boolean {
return (
Bun.spawnSync(
[
"which",
"docker",
],
{
stdio: [
"ignore",
"ignore",
"ignore",
],
},
).exitCode === 0
);
}
/** Try to start the Docker daemon and wait up to 30s for it to respond. */
function startAndWaitForDocker(isMac: boolean): void {
if (isMac) {
logStep("Starting OrbStack...");
Bun.spawnSync(
[
"open",
"-a",
"OrbStack",
],
{
stdio: [
"ignore",
"ignore",
"ignore",
],
},
);
} else {
logStep("Starting Docker daemon...");
const hasSudo =
Bun.spawnSync(
[
"which",
"sudo",
],
{
stdio: [
"ignore",
"ignore",
"ignore",
],
},
).exitCode === 0;
if (hasSudo) {
Bun.spawnSync(
[
"sudo",
"systemctl",
"start",
"docker",
],
{
stdio: [
"ignore",
"inherit",
"inherit",
],
},
);
}
}
// Wait up to 30s for the daemon to be ready
logStep("Waiting for Docker daemon...");
for (let i = 0; i < 30; i++) {
if (isDockerAvailable()) {
logInfo("Docker is ready");
return;
}
Bun.sleepSync(1000);
}
logInfo("Docker daemon did not start within 30s.");
if (isMac) {
logInfo("Open OrbStack.app manually, then retry.");
}
process.exit(1);
}
/** Ensure Docker is installed and the daemon is running. Installs and starts if needed. */
export async function ensureDocker(): Promise<void> {
// Fast path: daemon already running
if (isDockerAvailable()) {
return;
}
const isMac = process.platform === "darwin";
// Docker binary exists but daemon not running — just start it
if (isDockerInstalled()) {
startAndWaitForDocker(isMac);
return;
}
// Not installed at all — install first
if (isMac) {
logStep("Docker not found — installing OrbStack...");
const result = Bun.spawnSync(
@ -150,11 +248,8 @@ export async function ensureDocker(): Promise<void> {
}
}
// Verify Docker works after install
if (!isDockerAvailable()) {
logInfo("Docker installed but not responding. You may need to start the Docker daemon.");
process.exit(1);
}
// Start the daemon after fresh install
startAndWaitForDocker(isMac);
}
/** Pull the agent Docker image and start a container. */