Skip to content

Commit 426ebc9

Browse files
la14-1spawn-botclaude
authored
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>
1 parent c1d8acb commit 426ebc9

3 files changed

Lines changed: 140 additions & 36 deletions

File tree

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openrouter/spawn",
3-
"version": "0.30.0",
3+
"version": "0.30.1",
44
"type": "module",
55
"bin": {
66
"spawn": "cli.js"

packages/cli/src/__tests__/sandbox.test.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ describe("ensureDocker", () => {
7272
spy.mockRestore();
7373
});
7474

75-
it("attempts brew install on macOS when docker unavailable", async () => {
75+
it("attempts brew install on macOS when docker not installed", async () => {
7676
const origPlatform = Object.getOwnPropertyDescriptor(process, "platform");
7777
Object.defineProperty(process, "platform", {
7878
value: "darwin",
@@ -82,19 +82,7 @@ describe("ensureDocker", () => {
8282
let callCount = 0;
8383
const spy = spyOn(Bun, "spawnSync").mockImplementation((..._args: unknown[]) => {
8484
callCount++;
85-
// First call: docker info → fail, second: brew install → succeed, third: docker info → succeed
86-
if (callCount === 1) {
87-
return {
88-
exitCode: 1,
89-
stdout: new Uint8Array(),
90-
stderr: new Uint8Array(),
91-
success: false,
92-
signalCode: null,
93-
resourceUsage: undefined,
94-
pid: 1234,
95-
} satisfies ReturnType<typeof Bun.spawnSync>;
96-
}
97-
return {
85+
const ok = {
9886
exitCode: 0,
9987
stdout: new Uint8Array(),
10088
stderr: new Uint8Array(),
@@ -103,16 +91,37 @@ describe("ensureDocker", () => {
10391
resourceUsage: undefined,
10492
pid: 1234,
10593
} satisfies ReturnType<typeof Bun.spawnSync>;
94+
const fail = {
95+
exitCode: 1,
96+
stdout: new Uint8Array(),
97+
stderr: new Uint8Array(),
98+
success: false,
99+
signalCode: null,
100+
resourceUsage: undefined,
101+
pid: 1234,
102+
} satisfies ReturnType<typeof Bun.spawnSync>;
103+
// 1: docker info → fail, 2: which docker → fail (not installed),
104+
// 3: brew install → ok, 4: open -a OrbStack → ok, 5: docker info → ok
105+
if (callCount <= 2) {
106+
return fail;
107+
}
108+
return ok;
106109
});
107110

108111
await ensureDocker();
109112

110-
// Second call should be brew install orbstack
111-
expect(spy.mock.calls[1][0]).toEqual([
113+
// Call 1: docker info, 2: which docker, 3: brew install orbstack
114+
expect(spy.mock.calls[2][0]).toEqual([
112115
"brew",
113116
"install",
114117
"orbstack",
115118
]);
119+
// Call 4: open -a OrbStack (starts daemon)
120+
expect(spy.mock.calls[3][0]).toEqual([
121+
"open",
122+
"-a",
123+
"OrbStack",
124+
]);
116125

117126
spy.mockRestore();
118127
if (origPlatform) {

packages/cli/src/local/local.ts

Lines changed: 114 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -68,31 +68,129 @@ export async function interactiveSession(cmd: string): Promise<number> {
6868

6969
// ─── Docker Sandbox ─────────────────────────────────────────────────────────
7070

71-
/** Check whether Docker (or OrbStack) is available on the host. */
71+
/** Check whether the Docker daemon is running and responsive. */
7272
export function isDockerAvailable(): boolean {
73-
const result = Bun.spawnSync(
74-
[
75-
"docker",
76-
"info",
77-
],
78-
{
79-
stdio: [
80-
"ignore",
81-
"ignore",
82-
"ignore",
73+
return (
74+
Bun.spawnSync(
75+
[
76+
"docker",
77+
"info",
8378
],
84-
},
79+
{
80+
stdio: [
81+
"ignore",
82+
"ignore",
83+
"ignore",
84+
],
85+
},
86+
).exitCode === 0
8587
);
86-
return result.exitCode === 0;
8788
}
8889

89-
/** Install Docker if not present, or exit with guidance if install fails. */
90+
/** Check whether the docker binary exists (installed but daemon may be stopped). */
91+
function isDockerInstalled(): boolean {
92+
return (
93+
Bun.spawnSync(
94+
[
95+
"which",
96+
"docker",
97+
],
98+
{
99+
stdio: [
100+
"ignore",
101+
"ignore",
102+
"ignore",
103+
],
104+
},
105+
).exitCode === 0
106+
);
107+
}
108+
109+
/** Try to start the Docker daemon and wait up to 30s for it to respond. */
110+
function startAndWaitForDocker(isMac: boolean): void {
111+
if (isMac) {
112+
logStep("Starting OrbStack...");
113+
Bun.spawnSync(
114+
[
115+
"open",
116+
"-a",
117+
"OrbStack",
118+
],
119+
{
120+
stdio: [
121+
"ignore",
122+
"ignore",
123+
"ignore",
124+
],
125+
},
126+
);
127+
} else {
128+
logStep("Starting Docker daemon...");
129+
const hasSudo =
130+
Bun.spawnSync(
131+
[
132+
"which",
133+
"sudo",
134+
],
135+
{
136+
stdio: [
137+
"ignore",
138+
"ignore",
139+
"ignore",
140+
],
141+
},
142+
).exitCode === 0;
143+
if (hasSudo) {
144+
Bun.spawnSync(
145+
[
146+
"sudo",
147+
"systemctl",
148+
"start",
149+
"docker",
150+
],
151+
{
152+
stdio: [
153+
"ignore",
154+
"inherit",
155+
"inherit",
156+
],
157+
},
158+
);
159+
}
160+
}
161+
162+
// Wait up to 30s for the daemon to be ready
163+
logStep("Waiting for Docker daemon...");
164+
for (let i = 0; i < 30; i++) {
165+
if (isDockerAvailable()) {
166+
logInfo("Docker is ready");
167+
return;
168+
}
169+
Bun.sleepSync(1000);
170+
}
171+
logInfo("Docker daemon did not start within 30s.");
172+
if (isMac) {
173+
logInfo("Open OrbStack.app manually, then retry.");
174+
}
175+
process.exit(1);
176+
}
177+
178+
/** Ensure Docker is installed and the daemon is running. Installs and starts if needed. */
90179
export async function ensureDocker(): Promise<void> {
180+
// Fast path: daemon already running
91181
if (isDockerAvailable()) {
92182
return;
93183
}
94184

95185
const isMac = process.platform === "darwin";
186+
187+
// Docker binary exists but daemon not running — just start it
188+
if (isDockerInstalled()) {
189+
startAndWaitForDocker(isMac);
190+
return;
191+
}
192+
193+
// Not installed at all — install first
96194
if (isMac) {
97195
logStep("Docker not found — installing OrbStack...");
98196
const result = Bun.spawnSync(
@@ -150,11 +248,8 @@ export async function ensureDocker(): Promise<void> {
150248
}
151249
}
152250

153-
// Verify Docker works after install
154-
if (!isDockerAvailable()) {
155-
logInfo("Docker installed but not responding. You may need to start the Docker daemon.");
156-
process.exit(1);
157-
}
251+
// Start the daemon after fresh install
252+
startAndWaitForDocker(isMac);
158253
}
159254

160255
/** Pull the agent Docker image and start a container. */

0 commit comments

Comments
 (0)