@madarco/agentbox 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/{chunk-7J5AJLWG.js → chunk-BBZMA2K6.js} +3 -3
  2. package/dist/{chunk-RFC5F5HR.js → chunk-HHMWQNLF.js} +8 -8
  3. package/dist/chunk-HHMWQNLF.js.map +1 -0
  4. package/dist/{chunk-PXUBE5KS.js → chunk-HTTKML3C.js} +351 -42
  5. package/dist/chunk-HTTKML3C.js.map +1 -0
  6. package/dist/{chunk-6VTAPD4H.js → chunk-KJNZP6I3.js} +100 -21
  7. package/dist/chunk-KJNZP6I3.js.map +1 -0
  8. package/dist/{chunk-FJNIFTWK.js → chunk-M7I247BK.js} +6 -4
  9. package/dist/chunk-M7I247BK.js.map +1 -0
  10. package/dist/{create-AHZ3GVEZ-TGEDL7UX.js → create-6PWXI6HO-OWAMHBAK.js} +4 -4
  11. package/dist/index.js +310 -102
  12. package/dist/index.js.map +1 -1
  13. package/dist/{lifecycle-LFOL6YFM-TCHDX3J5.js → lifecycle-EMXR46DI-DUVBXNTV.js} +4 -4
  14. package/dist/{stats-Z4BVJODD-HEC4TMUZ.js → stats-SZXOJE3D-N7OODCHW.js} +3 -3
  15. package/package.json +4 -4
  16. package/runtime/docker/Dockerfile.box +23 -11
  17. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +19 -11
  18. package/runtime/docker/packages/ctl/dist/bin.cjs +56 -15
  19. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +13 -3
  20. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-dockerd-start +87 -7
  21. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +28 -0
  22. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +4 -9
  23. package/runtime/relay/bin.cjs +121 -2
  24. package/share/agentbox-setup/SKILL.md +19 -11
  25. package/dist/chunk-6VTAPD4H.js.map +0 -1
  26. package/dist/chunk-FJNIFTWK.js.map +0 -1
  27. package/dist/chunk-PXUBE5KS.js.map +0 -1
  28. package/dist/chunk-RFC5F5HR.js.map +0 -1
  29. /package/dist/{chunk-7J5AJLWG.js.map → chunk-BBZMA2K6.js.map} +0 -0
  30. /package/dist/{create-AHZ3GVEZ-TGEDL7UX.js.map → create-6PWXI6HO-OWAMHBAK.js.map} +0 -0
  31. /package/dist/{lifecycle-LFOL6YFM-TCHDX3J5.js.map → lifecycle-EMXR46DI-DUVBXNTV.js.map} +0 -0
  32. /package/dist/{stats-Z4BVJODD-HEC4TMUZ.js.map → stats-SZXOJE3D-N7OODCHW.js.map} +0 -0
@@ -13,12 +13,12 @@ import {
13
13
  startBox,
14
14
  stopBox,
15
15
  unpauseBox
16
- } from "./chunk-FJNIFTWK.js";
16
+ } from "./chunk-M7I247BK.js";
17
17
  import {
18
18
  SNAPSHOTS_ROOT
19
- } from "./chunk-PXUBE5KS.js";
19
+ } from "./chunk-HTTKML3C.js";
20
20
  import "./chunk-HPZMD5DE.js";
21
- import "./chunk-RFC5F5HR.js";
21
+ import "./chunk-HHMWQNLF.js";
22
22
  export {
23
23
  AmbiguousBoxError,
24
24
  BoxNotFoundError,
@@ -35,4 +35,4 @@ export {
35
35
  stopBox,
36
36
  unpauseBox
37
37
  };
38
- //# sourceMappingURL=lifecycle-LFOL6YFM-TCHDX3J5.js.map
38
+ //# sourceMappingURL=lifecycle-EMXR46DI-DUVBXNTV.js.map
@@ -6,8 +6,8 @@ import {
6
6
  parseDockerSize,
7
7
  projectCheckpointImageBytes,
8
8
  volumeSizeBytes
9
- } from "./chunk-7J5AJLWG.js";
10
- import "./chunk-RFC5F5HR.js";
9
+ } from "./chunk-BBZMA2K6.js";
10
+ import "./chunk-HHMWQNLF.js";
11
11
  export {
12
12
  agentboxHomeBytes,
13
13
  allCheckpointImagesBytes,
@@ -16,4 +16,4 @@ export {
16
16
  projectCheckpointImageBytes,
17
17
  volumeSizeBytes
18
18
  };
19
- //# sourceMappingURL=stats-Z4BVJODD-HEC4TMUZ.js.map
19
+ //# sourceMappingURL=stats-SZXOJE3D-N7OODCHW.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@madarco/agentbox",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Launch Claude Code, Codex, and other coding agents in isolated sandboxes",
5
5
  "license": "MIT",
6
6
  "author": "Marco D'Alia",
@@ -55,11 +55,11 @@
55
55
  "tsup": "^8.3.5",
56
56
  "typescript": "^5.7.2",
57
57
  "vitest": "^2.1.8",
58
+ "@agentbox/config": "0.0.0",
58
59
  "@agentbox/core": "0.0.0",
59
- "@agentbox/relay": "0.0.0",
60
60
  "@agentbox/ctl": "0.0.0",
61
- "@agentbox/config": "0.0.0",
62
- "@agentbox/sandbox-docker": "0.0.0"
61
+ "@agentbox/sandbox-docker": "0.0.0",
62
+ "@agentbox/relay": "0.0.0"
63
63
  },
64
64
  "scripts": {
65
65
  "build": "tsup",
@@ -5,8 +5,8 @@
5
5
  # Silicon hosts). /workspace is a plain dir in the image's writable layer:
6
6
  # `agentbox create` populates it via in-container `git worktree add` (or a
7
7
  # tar pipe for the no-git case). The old FUSE overlay over /host-src+/upper
8
- # is gone — but fuse3 + fuse-overlayfs stay because the in-box dockerd uses
9
- # fuse-overlayfs as its storage driver. Plus the "universal-ish" set of
8
+ # is gone — but fuse3 + fuse-overlayfs stay as the in-box dockerd's fallback
9
+ # storage driver (it prefers the kernel-native overlay2). Plus the "universal-ish" set of
10
10
  # language runtimes (Node.js 22 from NodeSource, Python 3 from apt). Heavier
11
11
  # tooling (Go, Java, Ruby, .NET, more browser tooling, vscode-server) goes in
12
12
  # a later iteration.
@@ -105,14 +105,15 @@ RUN npm install -g corepack@latest \
105
105
  # bind-mount of ~/.gitconfig from the host.
106
106
  RUN git config --system --add safe.directory '*'
107
107
 
108
- # Docker-in-Docker. dockerd inside the box (storage-driver=fuse-overlayfs, set
109
- # in /etc/docker/daemon.json) lets the agent run `docker build`/`docker run`
110
- # in its own namespace without exposing the host daemon. fuse-overlayfs is
111
- # required because the kernel `overlay` driver isn't usable from an
112
- # unprivileged container fuse3 + fuse-overlayfs are installed above
113
- # specifically for this (the outer /workspace overlay is gone, but the inner
114
- # dockerd still needs them). iptables is needed
115
- # for inner-container bridge networking. Adding `vscode` to the `docker` group
108
+ # Docker-in-Docker. dockerd inside the box lets the agent run
109
+ # `docker build`/`docker run` in its own namespace without exposing the host
110
+ # daemon. The storage driver is NOT pinned here: agentbox-dockerd-start picks
111
+ # it at runtime — the kernel-native overlay2 when a probe proves it works on
112
+ # the data-root filesystem, else the fuse-overlayfs fallback (fuse3 +
113
+ # fuse-overlayfs are installed above for that fallback). The baked daemon.json
114
+ # only carries `iptables: true`; the script rewrites it with the resolved
115
+ # `storage-driver` before launching dockerd. iptables is needed for
116
+ # inner-container bridge networking. Adding `vscode` to the `docker` group
116
117
  # lets the agent invoke `docker` without sudo once dockerd creates the socket
117
118
  # at /var/run/docker.sock at runtime. The matching launch script is COPY'd in
118
119
  # below; the daemon is started by the host via `docker exec -d --user root`
@@ -123,7 +124,7 @@ RUN apt-get update \
123
124
  iptables \
124
125
  && rm -rf /var/lib/apt/lists/* \
125
126
  && mkdir -p /etc/docker \
126
- && printf '%s\n' '{ "storage-driver": "fuse-overlayfs", "iptables": true }' > /etc/docker/daemon.json \
127
+ && printf '%s\n' '{ "iptables": true }' > /etc/docker/daemon.json \
127
128
  && usermod -aG docker vscode
128
129
 
129
130
  # agentbox-ctl: the in-container supervisor + CLI. Build context is the
@@ -284,6 +285,17 @@ RUN chmod +x /usr/local/bin/agentbox-dockerd-start
284
285
  COPY packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup /usr/local/bin/agentbox-checkpoint-cleanup
285
286
  RUN chmod +x /usr/local/bin/agentbox-checkpoint-cleanup
286
287
 
288
+ # Host-routed link opener. The box has no real browser; this wrapper forwards
289
+ # http(s) URLs to the host's default browser via the relay (`agentbox-ctl
290
+ # open`). It shadows xdg-utils' /usr/bin/xdg-open (the symlink lands earlier
291
+ # in PATH) and is set as $BROWSER so any tool that opens a link — Claude
292
+ # Code's OAuth flow, `gh`, `git web--browse`, python's webbrowser — routes
293
+ # to the host.
294
+ COPY packages/sandbox-docker/scripts/agentbox-open /usr/local/bin/agentbox-open
295
+ RUN chmod +x /usr/local/bin/agentbox-open \
296
+ && ln -sf /usr/local/bin/agentbox-open /usr/local/bin/xdg-open
297
+ ENV BROWSER=/usr/local/bin/agentbox-open
298
+
287
299
  # tmux config so Claude's true-color output and OSC 8 hyperlinks survive the
288
300
  # in-container tmux. `terminal-features` is a no-op on tmux < 3.4. Without
289
301
  # this, claude renders without 24-bit color (logo invisible) and hyperlinks
@@ -158,21 +158,29 @@ services:
158
158
  - A service with `restart: never` and an autostart dependency will block the dependent forever after one failed run — usually a mistake.
159
159
  - `command:` is either a shell string (run via `bash -c`) or an argv array. Use the argv form if you need to avoid shell quoting.
160
160
 
161
- ## 8. Hand-off
161
+ ## 8. Checkpoint the warm state (do this at the very end)
162
162
 
163
- 1. Write the file to `/workspace/agentbox.yaml`.
164
- 2. **Apply it live**: from inside the box run `agentbox-ctl reload`. The already-running supervisor re-reads the config and immediately runs the declared tasks and autostarts the services no box restart needed. It prints the `added` / `removed` / `changed` diff. If it errors because the daemon isn't running, the config is still valid: the next `agentbox start` (or `agentbox create` in this workspace) picks it up automatically.
165
- 3. Confirm with `agentbox-ctl status`: tasks should be `running` or `done`, autostart services `starting` or `ready`. If something failed, tail it with `agentbox-ctl logs <service>` and fix the config, then `agentbox-ctl reload` again.
166
- 4. Checkpoint (snapshot) this box writable layer: once the box is warmed up (deps installed, services ready), checkpoint it with `agentbox-ctl checkpoint --set-default` so future boxes start ready.
163
+ Checkpoint (snapshot) this box writable layer: once the box is warmed up (deps installed, services ready), checkpoint it with `agentbox-ctl checkpoint --name setup --replace --set-default` so future boxes start ready.
164
+ Run this command exactly once. The `--name setup --replace` makes it idempotentif it ever needs to run again it overwrites the existing `setup` checkpoint instead of stacking duplicates.
165
+ This doesn't need to be confirmed by the user.
166
+ It will pause the container for several seconds so warn the user about it and write Done when it's done.
167
167
 
168
- 5. Tell the user:
168
+ ## 9. Hand-off
169
169
 
170
- > I wrote `/workspace/agentbox.yaml` and ran `agentbox-ctl reload` so the supervisor is already running the declared tasks/services. To land the file on the host:
171
- > - I've created a checkpoint of the warm box state so future boxes start ready in seconds, no reinstall.
172
- > - commit it inside the box (`git add agentbox.yaml && git commit -m 'add agentbox config'`) — the box's `.git/` is bind-mounted, so the commit shows up on the host immediately; or
173
- > - on the host, tell the user to run `agentbox download config` to update their original host workspace.
170
+ Tell the user (verbatim):
174
171
 
175
- ## 9. Known issues
172
+ ```
173
+ █████╗ ██████╗ ███████╗███╗ ██╗████████╗██████╗ ██████╗ ██╗ ██╗
174
+ ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝██╔══██╗██╔═══██╗╚██╗██╔╝
175
+ ███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ ██████╔╝██║ ██║ ╚███╔╝
176
+ ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ██╔══██╗██║ ██║ ██╔██╗
177
+ ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ██████╔╝╚██████╔╝██╔╝ ██╗
178
+ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
179
+ ```
180
+
181
+ your box is ready, you can start more sessions with `agentbox claude`
182
+
183
+ ## 10. Known issues
176
184
 
177
185
  - For Nextjs/Vite/Tasnstack projects, makes sure to forward also websocket for hot reload.
178
186
 
@@ -10371,8 +10371,27 @@ var {
10371
10371
  } = import_index.default;
10372
10372
 
10373
10373
  // src/client.ts
10374
+ var import_node_child_process = require("child_process");
10375
+ var import_node_fs = require("fs");
10374
10376
  var import_node_net = require("net");
10375
- async function connect(opts) {
10377
+ async function tryReviveDaemon(socketPath) {
10378
+ if (process.env.AGENTBOX !== "1") return false;
10379
+ try {
10380
+ const child = (0, import_node_child_process.spawn)("agentbox-ctl", ["daemon"], {
10381
+ detached: true,
10382
+ stdio: "ignore"
10383
+ });
10384
+ child.unref();
10385
+ } catch {
10386
+ return false;
10387
+ }
10388
+ for (let i = 0; i < 50; i++) {
10389
+ await new Promise((r) => setTimeout(r, 100));
10390
+ if ((0, import_node_fs.existsSync)(socketPath)) return true;
10391
+ }
10392
+ return false;
10393
+ }
10394
+ async function connectOnce(opts) {
10376
10395
  const sock = (0, import_node_net.createConnection)(opts.socketPath);
10377
10396
  await new Promise((resolve, reject) => {
10378
10397
  const timer = setTimeout(() => {
@@ -10390,6 +10409,17 @@ async function connect(opts) {
10390
10409
  });
10391
10410
  return sock;
10392
10411
  }
10412
+ async function connect(opts) {
10413
+ try {
10414
+ return await connectOnce(opts);
10415
+ } catch (err) {
10416
+ const code = err.code;
10417
+ if (code !== "ECONNREFUSED" && code !== "ENOENT") throw err;
10418
+ const revived = await tryReviveDaemon(opts.socketPath);
10419
+ if (!revived) throw err;
10420
+ return await connectOnce(opts);
10421
+ }
10422
+ }
10393
10423
  async function sendOneShot(opts, req) {
10394
10424
  const sock = await connect(opts);
10395
10425
  sock.write(`${JSON.stringify(req)}
@@ -11077,9 +11107,9 @@ function describeCommand(cmd) {
11077
11107
  }
11078
11108
 
11079
11109
  // src/supervisor.ts
11080
- var import_node_child_process = require("child_process");
11110
+ var import_node_child_process2 = require("child_process");
11081
11111
  var import_node_events = require("events");
11082
- var import_node_fs = require("fs");
11112
+ var import_node_fs2 = require("fs");
11083
11113
  var import_promises3 = require("fs/promises");
11084
11114
  var import_node_path = require("path");
11085
11115
 
@@ -11343,7 +11373,7 @@ var cachedLoginPath;
11343
11373
  function loginShellPath() {
11344
11374
  if (cachedLoginPath !== void 0) return cachedLoginPath;
11345
11375
  try {
11346
- const out = (0, import_node_child_process.execFileSync)("bash", ["-lc", 'printf %s "$PATH"'], {
11376
+ const out = (0, import_node_child_process2.execFileSync)("bash", ["-lc", 'printf %s "$PATH"'], {
11347
11377
  encoding: "utf8",
11348
11378
  timeout: 5e3
11349
11379
  }).trim();
@@ -11358,7 +11388,7 @@ var ServiceRunner = class extends import_node_events.EventEmitter {
11358
11388
  super();
11359
11389
  this.spec = spec;
11360
11390
  this.opts = opts;
11361
- this.spawnFn = opts.spawn ?? import_node_child_process.spawn;
11391
+ this.spawnFn = opts.spawn ?? import_node_child_process2.spawn;
11362
11392
  this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
11363
11393
  this.clearTimer = opts.clearTimer ?? ((h) => {
11364
11394
  clearTimeout(h);
@@ -11455,7 +11485,7 @@ var ServiceRunner = class extends import_node_events.EventEmitter {
11455
11485
  const spec = this.spec;
11456
11486
  const cwd = resolveCwd(spec.cwd, this.opts.cwd);
11457
11487
  if (!this.logStream) {
11458
- this.logStream = (0, import_node_fs.createWriteStream)((0, import_node_path.join)(this.opts.logDir, `${spec.name}.log`), {
11488
+ this.logStream = (0, import_node_fs2.createWriteStream)((0, import_node_path.join)(this.opts.logDir, `${spec.name}.log`), {
11459
11489
  flags: "a"
11460
11490
  });
11461
11491
  this.logStream.on("error", (err) => {
@@ -11589,7 +11619,7 @@ var TaskRunner = class extends import_node_events.EventEmitter {
11589
11619
  super();
11590
11620
  this.spec = spec;
11591
11621
  this.opts = opts;
11592
- this.spawnFn = opts.spawn ?? import_node_child_process.spawn;
11622
+ this.spawnFn = opts.spawn ?? import_node_child_process2.spawn;
11593
11623
  }
11594
11624
  spec;
11595
11625
  opts;
@@ -11653,7 +11683,7 @@ var TaskRunner = class extends import_node_events.EventEmitter {
11653
11683
  const spec = this.spec;
11654
11684
  const cwd = resolveCwd(spec.cwd, this.opts.cwd);
11655
11685
  if (!this.logStream) {
11656
- this.logStream = (0, import_node_fs.createWriteStream)((0, import_node_path.join)(this.opts.logDir, `${spec.name}.log`), {
11686
+ this.logStream = (0, import_node_fs2.createWriteStream)((0, import_node_path.join)(this.opts.logDir, `${spec.name}.log`), {
11657
11687
  flags: "a"
11658
11688
  });
11659
11689
  this.logStream.on("error", (err) => {
@@ -12137,10 +12167,10 @@ var import_promises4 = require("fs/promises");
12137
12167
  var import_node_path2 = require("path");
12138
12168
 
12139
12169
  // src/status-reporter.ts
12140
- var import_node_child_process3 = require("child_process");
12170
+ var import_node_child_process4 = require("child_process");
12141
12171
 
12142
12172
  // src/tmux.ts
12143
- var import_node_child_process2 = require("child_process");
12173
+ var import_node_child_process3 = require("child_process");
12144
12174
  var import_node_os = require("os");
12145
12175
  var MAX_TITLE_LEN = 120;
12146
12176
  function sanitizePaneTitle(raw, ctx) {
@@ -12154,7 +12184,7 @@ function sanitizePaneTitle(raw, ctx) {
12154
12184
  }
12155
12185
  function runTool(cmd, args) {
12156
12186
  return new Promise((resolve) => {
12157
- const child = (0, import_node_child_process2.spawn)(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
12187
+ const child = (0, import_node_child_process3.spawn)(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
12158
12188
  let stdout = "";
12159
12189
  let stderr = "";
12160
12190
  child.stdout.on("data", (b) => stdout += b.toString("utf8"));
@@ -12298,7 +12328,7 @@ async function collectPorts(supervisor) {
12298
12328
  }
12299
12329
  function run(cmd, args) {
12300
12330
  return new Promise((resolve) => {
12301
- const child = (0, import_node_child_process3.spawn)(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
12331
+ const child = (0, import_node_child_process4.spawn)(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
12302
12332
  let stdout = "";
12303
12333
  child.stdout.on("data", (b) => stdout += b.toString("utf8"));
12304
12334
  child.on("error", () => resolve({ exitCode: 127, stdout }));
@@ -12345,7 +12375,8 @@ async function startServer(opts) {
12345
12375
  resolve();
12346
12376
  });
12347
12377
  });
12348
- await (0, import_promises4.chmod)(opts.socketPath, 432);
12378
+ await (0, import_promises4.chmod)(opts.socketPath, 432).catch(() => {
12379
+ });
12349
12380
  return server;
12350
12381
  }
12351
12382
  async function handleConnection(sock, opts) {
@@ -12592,7 +12623,7 @@ var checkpointCommand = new Command("checkpoint").description("Capture this box
12592
12623
  );
12593
12624
 
12594
12625
  // src/commands/git.ts
12595
- var import_node_child_process4 = require("child_process");
12626
+ var import_node_child_process5 = require("child_process");
12596
12627
  function buildParams(opts, extra) {
12597
12628
  const params = { path: opts.cwd ?? process.cwd() };
12598
12629
  if (opts.remote) params.remote = opts.remote;
@@ -12601,7 +12632,7 @@ function buildParams(opts, extra) {
12601
12632
  }
12602
12633
  function runLocalGit(args, cwd) {
12603
12634
  return new Promise((resolve) => {
12604
- const child = (0, import_node_child_process4.spawn)("git", args, { cwd, stdio: "inherit" });
12635
+ const child = (0, import_node_child_process5.spawn)("git", args, { cwd, stdio: "inherit" });
12605
12636
  child.on("close", (code) => resolve(code ?? 1));
12606
12637
  child.on("error", (err) => {
12607
12638
  process.stderr.write(`agentbox-ctl git: ${String(err.message ?? err)}
@@ -12663,6 +12694,15 @@ var notifyCommand = new Command("notify").description(
12663
12694
  })
12664
12695
  );
12665
12696
 
12697
+ // src/commands/open.ts
12698
+ var openCommand = new Command("open").description("Open a URL in the host's default browser (via the agentbox relay)").argument("<url>", "http(s) URL to open on the host").action(async (url) => {
12699
+ const params = { url };
12700
+ const code = await postRpcAndExit("browser.open", params, {
12701
+ errorPrefix: "agentbox-ctl open"
12702
+ });
12703
+ process.exit(code);
12704
+ });
12705
+
12666
12706
  // src/render.ts
12667
12707
  function renderStatusTable(rows) {
12668
12708
  if (rows.length === 0) return "(no services configured)";
@@ -12854,6 +12894,7 @@ program2.addCommand(checkpointCommand);
12854
12894
  program2.addCommand(cpCommand);
12855
12895
  program2.addCommand(downloadCommand);
12856
12896
  program2.addCommand(notifyCommand);
12897
+ program2.addCommand(openCommand);
12857
12898
  program2.parseAsync(process.argv).catch((err) => {
12858
12899
  const msg = err instanceof Error ? err.message : String(err);
12859
12900
  process.stderr.write(`agentbox-ctl: ${msg}
@@ -22,8 +22,14 @@ set +e
22
22
  apt-get clean 2>/dev/null
23
23
  rm -rf /var/lib/apt/lists/* 2>/dev/null
24
24
 
25
- # Throwaway scratch dirs.
26
- rm -rf /tmp/* /tmp/.[!.]* /var/tmp/* /var/tmp/.[!.]* 2>/dev/null
25
+ # Throwaway scratch dirs. Preserve /tmp/claude-* — that is the live in-box
26
+ # Claude Code session's working tree (its per-task stdout/stderr files). The
27
+ # agent that triggered this checkpoint *is* that session; deleting its task
28
+ # output mid-run makes its harness see ENOENT, treat the command as failed,
29
+ # and retry the checkpoint (observed: 5 duplicate auto-named checkpoints).
30
+ # Stale claude-* dirs baked into the image are tiny and Claude Code prunes
31
+ # them itself on the next session start.
32
+ find /tmp /var/tmp -mindepth 1 -maxdepth 1 ! -name 'claude-*' -exec rm -rf {} + 2>/dev/null
27
33
 
28
34
  # Logs: truncate (don't delete) so the original file modes / ownerships stay
29
35
  # intact for the next run. Targets common rotated archives too.
@@ -31,9 +37,13 @@ find /var/log -type f \( -name '*.log' -o -name '*.gz' -o -name '*.1' \) \
31
37
  -exec truncate -s0 {} + 2>/dev/null
32
38
  find /var/log/agentbox -type f -exec truncate -s0 {} + 2>/dev/null
33
39
 
34
- # Bash history (root + vscode).
40
+ # Bash history (root + vscode). Re-assert vscode ownership: `: >` run as root
41
+ # (re)creates the file root-owned 0644 when it didn't exist, which the uid-1000
42
+ # vscode user cannot append to, silently dropping all shell history.
35
43
  : > /root/.bash_history 2>/dev/null
36
44
  : > /home/vscode/.bash_history 2>/dev/null
45
+ chown vscode:vscode /home/vscode/.bash_history 2>/dev/null
46
+ chmod 600 /home/vscode/.bash_history 2>/dev/null
37
47
 
38
48
  # Anthropic's installer writes a transient marker; redundant once the binary
39
49
  # is in place. Safe to wipe.
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env bash
2
2
  # Start the in-box dockerd. Launched by the host via
3
- # `docker exec -d --user root` after the FUSE overlay is up. Idempotent — safe
4
- # to call again on `agentbox start`. Storage driver is fuse-overlayfs (set in
5
- # /etc/docker/daemon.json baked into the image) so the inner daemon doesn't
6
- # need the kernel `overlay` driver, which an unprivileged container can't load.
3
+ # `docker exec -d --user root`. Idempotent safe to call again on
4
+ # `agentbox start`. The storage driver is selected at runtime (see
5
+ # select_storage_driver below): the kernel-native `overlay2` when a probe
6
+ # proves it works on the data-root filesystem, otherwise `fuse-overlayfs`.
7
+ # The chosen driver is written to /etc/docker/daemon.json before launch.
7
8
 
8
9
  set -euo pipefail
9
10
 
@@ -33,14 +34,93 @@ rm -f /var/run/docker.pid /var/run/docker.sock
33
34
  mount -o remount,rw /sys/fs/cgroup 2>/dev/null || true
34
35
  mount -o remount,rw /proc/sys 2>/dev/null || true
35
36
 
37
+ # --- Storage-driver selection -------------------------------------------------
38
+ # The inner dockerd's data root (/var/lib/docker, a Docker named volume) used
39
+ # to be pinned to fuse-overlayfs. fuse-overlayfs is broken on recent kernels
40
+ # (e.g. Docker Desktop's 6.x linuxkit kernel): inner `docker run` fails at
41
+ # execve() with "exec ...: invalid argument". The kernel-native overlay2
42
+ # driver works when the data-root filesystem can carry an overlay mount, which
43
+ # the ext4 named volume can. We pick overlay2 when a probe proves it works,
44
+ # else fall back to fuse-overlayfs.
45
+ #
46
+ # dockerd refuses to switch drivers once its data root is populated, so if the
47
+ # data root is already initialized under one driver we reuse that driver and
48
+ # skip the probe — a box created under one driver never switches.
49
+ DOCKER_DATA_ROOT=/var/lib/docker
50
+ DAEMON_JSON=/etc/docker/daemon.json
51
+
52
+ probe_overlay2() {
53
+ # The kernel overlay filesystem has to exist at all.
54
+ grep -qw overlay /proc/filesystems 2>/dev/null || return 1
55
+
56
+ local probe lower upper work merged ok=1
57
+ # The probe dir MUST live inside the data root so the test overlay is mounted
58
+ # on the SAME filesystem the real graph will use. A probe under /tmp would
59
+ # test the container's overlayfs writable layer — the wrong filesystem.
60
+ probe="$(mktemp -d "$DOCKER_DATA_ROOT/.overlay2-probe.XXXXXX" 2>/dev/null)" || return 1
61
+ lower="$probe/lower"; upper="$probe/upper"; work="$probe/work"; merged="$probe/merged"
62
+ mkdir -p "$lower" "$upper" "$work" "$merged" || { rm -rf "$probe"; return 1; }
63
+
64
+ # Stage a known-good executable so the merged view exposes it.
65
+ cp /bin/true "$lower/probe-bin" 2>/dev/null || { rm -rf "$probe"; return 1; }
66
+ chmod 0755 "$lower/probe-bin" 2>/dev/null || true
67
+
68
+ if mount -t overlay overlay \
69
+ -o "lowerdir=$lower,upperdir=$upper,workdir=$work" "$merged" 2>/dev/null; then
70
+ # The actual fuse-overlayfs failure mode: execve from the merged dir. A
71
+ # successful mount is not enough — fuse-overlayfs mounts fine and only
72
+ # fails here.
73
+ "$merged/probe-bin" >/dev/null 2>&1 || ok=0
74
+ umount "$merged" 2>/dev/null || umount -l "$merged" 2>/dev/null || true
75
+ else
76
+ ok=0
77
+ fi
78
+ rm -rf "$probe"
79
+ [ "$ok" = 1 ]
80
+ }
81
+
82
+ select_storage_driver() {
83
+ # 1. Reuse an already-initialized data root's driver — dockerd cannot switch
84
+ # a populated data root, and this script reruns on every `agentbox start`.
85
+ local has_overlay2=0 has_fuse=0
86
+ [ -d "$DOCKER_DATA_ROOT/overlay2" ] \
87
+ && [ -n "$(ls -A "$DOCKER_DATA_ROOT/overlay2" 2>/dev/null)" ] && has_overlay2=1
88
+ [ -d "$DOCKER_DATA_ROOT/fuse-overlayfs" ] \
89
+ && [ -n "$(ls -A "$DOCKER_DATA_ROOT/fuse-overlayfs" 2>/dev/null)" ] && has_fuse=1
90
+ if [ "$has_overlay2" = 1 ]; then echo "overlay2"; return 0; fi
91
+ if [ "$has_fuse" = 1 ]; then echo "fuse-overlayfs"; return 0; fi
92
+
93
+ # 2. Fresh data root: probe overlay2 against the data-root filesystem.
94
+ if probe_overlay2; then echo "overlay2"; return 0; fi
95
+ echo "fuse-overlayfs"
96
+ }
97
+
98
+ # Sweep any leaked probe dir from a hard-killed previous run (cosmetic; the
99
+ # driver subdir checks above ignore it, and dockerd ignores non-driver dirs).
100
+ rm -rf "$DOCKER_DATA_ROOT"/.overlay2-probe.* 2>/dev/null || true
101
+
102
+ STORAGE_DRIVER="$(select_storage_driver)"
103
+
104
+ # Write daemon.json with the resolved driver. `iptables: true` stays for inner
105
+ # bridge networking. Rewritten every start, but the driver is stable (step 1
106
+ # above), so this never causes a mid-life driver switch.
107
+ mkdir -p /etc/docker
108
+ printf '%s\n' \
109
+ "{ \"storage-driver\": \"$STORAGE_DRIVER\", \"iptables\": true }" \
110
+ > "$DAEMON_JSON"
111
+ # Truncate dockerd.log fresh for this start, marker line first; dockerd appends.
112
+ echo "agentbox-dockerd-start: storage-driver=$STORAGE_DRIVER" \
113
+ > /var/log/agentbox/dockerd.log
114
+ # --- end storage-driver selection --------------------------------------------
115
+
36
116
  # nohup + & + disown lets us survive the `docker exec -d` returning. dockerd
37
117
  # reads /etc/docker/daemon.json on its own; no flags here keeps the start path
38
118
  # debuggable from inside the container (just edit the file and restart).
39
- nohup dockerd >/var/log/agentbox/dockerd.log 2>&1 &
119
+ nohup dockerd >>/var/log/agentbox/dockerd.log 2>&1 &
40
120
 
41
121
  # Wait for the socket to become accept()-able. Bound by ~30s — first start has
42
- # to initialize iptables chains and the fuse-overlayfs graphdriver, which is
43
- # noticeably slower than overlay2.
122
+ # to initialize iptables chains and the storage graphdriver (fuse-overlayfs is
123
+ # noticeably slower to initialize than overlay2).
44
124
  for _ in $(seq 1 300); do
45
125
  if [ -S /var/run/docker.sock ] \
46
126
  && docker -H unix:///var/run/docker.sock info >/dev/null 2>&1; then
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bash
2
+ # Routes in-box URL opens to the *host's* default browser via the agentbox
3
+ # relay. The box has no real browser of its own. This script is installed at
4
+ # /usr/local/bin (earlier in PATH than xdg-utils' /usr/bin/xdg-open, which it
5
+ # is also symlinked over) and is the box's $BROWSER, so `xdg-open`, Claude
6
+ # Code's OAuth flow, `gh`, `git web--browse`, python's webbrowser, etc. all
7
+ # land here.
8
+ #
9
+ # Only http(s) URLs are forwarded to the host. Anything else (a file path,
10
+ # another scheme) falls through to the real xdg-open, which resolves it
11
+ # locally inside the box.
12
+
13
+ set -uo pipefail
14
+
15
+ target="${1:-}"
16
+
17
+ case "$target" in
18
+ http://* | https://*)
19
+ exec agentbox-ctl open "$target"
20
+ ;;
21
+ *)
22
+ if [[ -x /usr/bin/xdg-open ]]; then
23
+ exec /usr/bin/xdg-open "$@"
24
+ fi
25
+ echo "agentbox-open: not an http(s) URL: $target" >&2
26
+ exit 1
27
+ ;;
28
+ esac
@@ -2,13 +2,13 @@
2
2
 
3
3
  You are running inside an AgentBox sandbox: a Linux Docker container with
4
4
  docker-in-docker (run `docker` directly, no sudo).
5
- Your user is `vscode` and you can use passwordless sudo to run commands as root.
5
+ Your user is `vscode` and you can use passwordless **sudo** to run commands as root.
6
6
  `/workspace` is your own per-box git worktree on branch `agentbox/<box-name>`:
7
7
  writes there stay in the container's writable layer and don't touch the host's working
8
8
  tree.
9
9
 
10
10
  You can save the current filesystem state to be reused by future boxes by
11
- running `agentbox-ctl checkpoint --set-default`.
11
+ running `agentbox-ctl checkpoint --set-default`. This doesn't need to be confirmed by the user.
12
12
 
13
13
  The main `.git/` is bind-mounted at the same absolute path as on
14
14
  the host, so local commits show up in the host's `git log` immediately.
@@ -18,15 +18,10 @@ No SSH creds, no host gitconfig identity. For ops that need the user
18
18
 
19
19
  For ad-hoc file transfers between this box and the host, use
20
20
  `agentbox-ctl cp toHost <boxPath> <hostPath>` and
21
- `agentbox-ctl cp fromHost <hostPath> <boxPath>`. They RPC to the host and
21
+ `agentbox-ctl cp fromHost <hostPath> <boxPath>` or `agentbox-ctl download claude` / `download env` /
22
+ `download config`. They RPC to the host and
22
23
  ask the user for confirmation on the wrapper that runs `agentbox claude`;
23
24
  deny returns exit 10 (`denied by user`).
24
25
  Don't put any timeout on the command, it will run forever and the user will be notified through multiple channels.
25
26
 
26
- If you install a skill/plugin, change `~/.claude`, or write
27
- `.env`/`.envrc`/secrets/`agentbox.yaml`, you can pull those onto the host
28
- yourself with `agentbox-ctl download claude` / `download env` /
29
- `download config` (also user-confirmed; additive; never overwrites host
30
- files, don't put any timeout on the command).
31
-
32
27
  Box identity: /etc/agentbox/box.env and the AGENTBOX_* env vars.