@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.
- package/dist/{chunk-7J5AJLWG.js → chunk-BBZMA2K6.js} +3 -3
- package/dist/{chunk-RFC5F5HR.js → chunk-HHMWQNLF.js} +8 -8
- package/dist/chunk-HHMWQNLF.js.map +1 -0
- package/dist/{chunk-PXUBE5KS.js → chunk-HTTKML3C.js} +351 -42
- package/dist/chunk-HTTKML3C.js.map +1 -0
- package/dist/{chunk-6VTAPD4H.js → chunk-KJNZP6I3.js} +100 -21
- package/dist/chunk-KJNZP6I3.js.map +1 -0
- package/dist/{chunk-FJNIFTWK.js → chunk-M7I247BK.js} +6 -4
- package/dist/chunk-M7I247BK.js.map +1 -0
- package/dist/{create-AHZ3GVEZ-TGEDL7UX.js → create-6PWXI6HO-OWAMHBAK.js} +4 -4
- package/dist/index.js +310 -102
- package/dist/index.js.map +1 -1
- package/dist/{lifecycle-LFOL6YFM-TCHDX3J5.js → lifecycle-EMXR46DI-DUVBXNTV.js} +4 -4
- package/dist/{stats-Z4BVJODD-HEC4TMUZ.js → stats-SZXOJE3D-N7OODCHW.js} +3 -3
- package/package.json +4 -4
- package/runtime/docker/Dockerfile.box +23 -11
- package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +19 -11
- package/runtime/docker/packages/ctl/dist/bin.cjs +56 -15
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +13 -3
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-dockerd-start +87 -7
- package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +28 -0
- package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +4 -9
- package/runtime/relay/bin.cjs +121 -2
- package/share/agentbox-setup/SKILL.md +19 -11
- package/dist/chunk-6VTAPD4H.js.map +0 -1
- package/dist/chunk-FJNIFTWK.js.map +0 -1
- package/dist/chunk-PXUBE5KS.js.map +0 -1
- package/dist/chunk-RFC5F5HR.js.map +0 -1
- /package/dist/{chunk-7J5AJLWG.js.map → chunk-BBZMA2K6.js.map} +0 -0
- /package/dist/{create-AHZ3GVEZ-TGEDL7UX.js.map → create-6PWXI6HO-OWAMHBAK.js.map} +0 -0
- /package/dist/{lifecycle-LFOL6YFM-TCHDX3J5.js.map → lifecycle-EMXR46DI-DUVBXNTV.js.map} +0 -0
- /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-
|
|
16
|
+
} from "./chunk-M7I247BK.js";
|
|
17
17
|
import {
|
|
18
18
|
SNAPSHOTS_ROOT
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-HTTKML3C.js";
|
|
20
20
|
import "./chunk-HPZMD5DE.js";
|
|
21
|
-
import "./chunk-
|
|
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-
|
|
38
|
+
//# sourceMappingURL=lifecycle-EMXR46DI-DUVBXNTV.js.map
|
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
parseDockerSize,
|
|
7
7
|
projectCheckpointImageBytes,
|
|
8
8
|
volumeSizeBytes
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
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-
|
|
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.
|
|
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/
|
|
62
|
-
"@agentbox/
|
|
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
|
|
9
|
-
#
|
|
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
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
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' '{ "
|
|
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.
|
|
161
|
+
## 8. Checkpoint the warm state (do this at the very end)
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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 idempotent — if 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
|
-
|
|
168
|
+
## 9. Hand-off
|
|
169
169
|
|
|
170
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
11110
|
+
var import_node_child_process2 = require("child_process");
|
|
11081
11111
|
var import_node_events = require("events");
|
|
11082
|
-
var
|
|
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,
|
|
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 ??
|
|
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,
|
|
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 ??
|
|
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,
|
|
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
|
|
12170
|
+
var import_node_child_process4 = require("child_process");
|
|
12141
12171
|
|
|
12142
12172
|
// src/tmux.ts
|
|
12143
|
-
var
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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.
|