@madarco/agentbox 0.6.0 → 0.7.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 (59) hide show
  1. package/dist/_cloud-attach-DMVH6GWO.js +12 -0
  2. package/dist/chunk-7KOEFGN2.js +1162 -0
  3. package/dist/chunk-7KOEFGN2.js.map +1 -0
  4. package/dist/chunk-I24B6AXR.js +600 -0
  5. package/dist/chunk-I24B6AXR.js.map +1 -0
  6. package/dist/chunk-NAVL4R34.js +7546 -0
  7. package/dist/chunk-NAVL4R34.js.map +1 -0
  8. package/dist/chunk-NW5NYTQM.js +1366 -0
  9. package/dist/chunk-NW5NYTQM.js.map +1 -0
  10. package/dist/chunk-UK72UQ5U.js +237 -0
  11. package/dist/chunk-UK72UQ5U.js.map +1 -0
  12. package/dist/chunk-V5KZGB5V.js +722 -0
  13. package/dist/chunk-V5KZGB5V.js.map +1 -0
  14. package/dist/cloud-poller-ZIWSADJB-JXFRJUEM.js +10 -0
  15. package/dist/dist-ETCFRVPA.js +423 -0
  16. package/dist/dist-QZGJIBT5.js +1339 -0
  17. package/dist/dist-QZGJIBT5.js.map +1 -0
  18. package/dist/dist-R67WMLCF.js +183 -0
  19. package/dist/dist-R67WMLCF.js.map +1 -0
  20. package/dist/index.js +3998 -1569
  21. package/dist/index.js.map +1 -1
  22. package/package.json +8 -3
  23. package/runtime/docker/Dockerfile.box +98 -14
  24. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +15 -8
  25. package/runtime/docker/packages/ctl/dist/bin.cjs +10220 -773
  26. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json +37 -0
  27. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +9 -9
  28. package/runtime/hetzner/agentbox-checkpoint-cleanup +52 -0
  29. package/runtime/hetzner/agentbox-codex-hooks.json +37 -0
  30. package/runtime/hetzner/agentbox-dockerd-start +132 -0
  31. package/runtime/hetzner/agentbox-open +28 -0
  32. package/runtime/hetzner/agentbox-setup-skill.md +196 -0
  33. package/runtime/hetzner/agentbox-vnc-start +77 -0
  34. package/runtime/hetzner/claude-managed-settings.json +54 -0
  35. package/runtime/hetzner/ctl.cjs +22350 -0
  36. package/runtime/hetzner/custom-system-CLAUDE.md +27 -0
  37. package/runtime/hetzner/scripts/install-box.sh +365 -0
  38. package/runtime/relay/bin.cjs +9118 -809
  39. package/share/agentbox-setup/SKILL.md +15 -8
  40. package/dist/chunk-BBZMA2K6.js +0 -238
  41. package/dist/chunk-BBZMA2K6.js.map +0 -1
  42. package/dist/chunk-HHMWQNLF.js +0 -1709
  43. package/dist/chunk-HHMWQNLF.js.map +0 -1
  44. package/dist/chunk-HPZMD5DE.js +0 -106
  45. package/dist/chunk-HPZMD5DE.js.map +0 -1
  46. package/dist/chunk-HTTKML3C.js +0 -2655
  47. package/dist/chunk-HTTKML3C.js.map +0 -1
  48. package/dist/chunk-KJNZP6I3.js +0 -586
  49. package/dist/chunk-KJNZP6I3.js.map +0 -1
  50. package/dist/chunk-M7I247BK.js +0 -525
  51. package/dist/chunk-M7I247BK.js.map +0 -1
  52. package/dist/create-6PWXI6HO-OWAMHBAK.js +0 -15
  53. package/dist/lifecycle-EMXR46DI-DUVBXNTV.js +0 -38
  54. package/dist/state-KD7M46ZP-KHFTHFUS.js +0 -26
  55. package/dist/stats-SZXOJE3D-N7OODCHW.js +0 -19
  56. package/dist/stats-SZXOJE3D-N7OODCHW.js.map +0 -1
  57. /package/dist/{create-6PWXI6HO-OWAMHBAK.js.map → _cloud-attach-DMVH6GWO.js.map} +0 -0
  58. /package/dist/{lifecycle-EMXR46DI-DUVBXNTV.js.map → cloud-poller-ZIWSADJB-JXFRJUEM.js.map} +0 -0
  59. /package/dist/{state-KD7M46ZP-KHFTHFUS.js.map → dist-ETCFRVPA.js.map} +0 -0
@@ -0,0 +1,37 @@
1
+ {
2
+ "SessionStart": [
3
+ {
4
+ "hooks": [
5
+ { "type": "command", "command": "agentbox-ctl codex-state idle >/dev/null 2>&1 &", "timeout": 3 }
6
+ ]
7
+ }
8
+ ],
9
+ "UserPromptSubmit": [
10
+ {
11
+ "hooks": [
12
+ { "type": "command", "command": "agentbox-ctl codex-state working >/dev/null 2>&1 &", "timeout": 3 }
13
+ ]
14
+ }
15
+ ],
16
+ "PreToolUse": [
17
+ {
18
+ "hooks": [
19
+ { "type": "command", "command": "agentbox-ctl codex-state working >/dev/null 2>&1 &", "timeout": 3 }
20
+ ]
21
+ }
22
+ ],
23
+ "PermissionRequest": [
24
+ {
25
+ "hooks": [
26
+ { "type": "command", "command": "agentbox-ctl codex-state waiting >/dev/null 2>&1 &", "timeout": 3 }
27
+ ]
28
+ }
29
+ ],
30
+ "Stop": [
31
+ {
32
+ "hooks": [
33
+ { "type": "command", "command": "agentbox-ctl codex-state idle >/dev/null 2>&1 &", "timeout": 3 }
34
+ ]
35
+ }
36
+ ]
37
+ }
@@ -1,14 +1,14 @@
1
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.
2
+ # Routes in-box URL opens to `agentbox-ctl open`, which opens the link in the
3
+ # box's own Chromium (agent-browser, visible via `agentbox screen`) and asks
4
+ # the host user — in the footer/dashboard whether to also open it on the
5
+ # host. This script is installed at /usr/local/bin (earlier in PATH than
6
+ # xdg-utils' /usr/bin/xdg-open, which it is also symlinked over) and is the
7
+ # box's $BROWSER, so `xdg-open`, Claude Code's OAuth flow, `gh`,
8
+ # `git web--browse`, python's webbrowser, etc. all land here.
8
9
  #
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.
10
+ # Only http(s) URLs are routed. Anything else (a file path, another scheme)
11
+ # falls through to the real xdg-open, which resolves it locally in the box.
12
12
 
13
13
  set -uo pipefail
14
14
 
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ # Pre-`docker commit` cleanup: strip ephemeral / disposable state so the
3
+ # captured checkpoint image is closer to "warm project state, nothing else".
4
+ #
5
+ # Invoked by the host via `docker exec --user root <container>
6
+ # /usr/local/bin/agentbox-checkpoint-cleanup` right before
7
+ # `docker commit`. Best-effort: every step is allowed to fail (a checkpoint
8
+ # capture should never block on cleanup hiccups).
9
+ #
10
+ # What we DELIBERATELY keep:
11
+ # - /workspace the actual point of the checkpoint
12
+ # - /home/vscode/.npm warm npm cache (next install is fast)
13
+ # - /home/vscode/.cache pnpm/yarn/Cargo/etc. caches
14
+ # - /var/lib/docker in-box dockerd's data root
15
+ # - /home/vscode/.claude the named volume is bind-mounted; image
16
+ # layer never sees it anyway
17
+ set +e
18
+
19
+ # apt: drop downloaded .deb cache and the package index. The index is ~50MB
20
+ # and gets refreshed on the next `apt-get update`; the .deb cache is reusable
21
+ # only if we don't change versions, which we usually do.
22
+ apt-get clean 2>/dev/null
23
+ rm -rf /var/lib/apt/lists/* 2>/dev/null
24
+
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
33
+
34
+ # Logs: truncate (don't delete) so the original file modes / ownerships stay
35
+ # intact for the next run. Targets common rotated archives too.
36
+ find /var/log -type f \( -name '*.log' -o -name '*.gz' -o -name '*.1' \) \
37
+ -exec truncate -s0 {} + 2>/dev/null
38
+ find /var/log/agentbox -type f -exec truncate -s0 {} + 2>/dev/null
39
+
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.
43
+ : > /root/.bash_history 2>/dev/null
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
47
+
48
+ # Anthropic's installer writes a transient marker; redundant once the binary
49
+ # is in place. Safe to wipe.
50
+ rm -rf /home/vscode/.claude-installer 2>/dev/null
51
+
52
+ exit 0
@@ -0,0 +1,37 @@
1
+ {
2
+ "SessionStart": [
3
+ {
4
+ "hooks": [
5
+ { "type": "command", "command": "agentbox-ctl codex-state idle >/dev/null 2>&1 &", "timeout": 3 }
6
+ ]
7
+ }
8
+ ],
9
+ "UserPromptSubmit": [
10
+ {
11
+ "hooks": [
12
+ { "type": "command", "command": "agentbox-ctl codex-state working >/dev/null 2>&1 &", "timeout": 3 }
13
+ ]
14
+ }
15
+ ],
16
+ "PreToolUse": [
17
+ {
18
+ "hooks": [
19
+ { "type": "command", "command": "agentbox-ctl codex-state working >/dev/null 2>&1 &", "timeout": 3 }
20
+ ]
21
+ }
22
+ ],
23
+ "PermissionRequest": [
24
+ {
25
+ "hooks": [
26
+ { "type": "command", "command": "agentbox-ctl codex-state waiting >/dev/null 2>&1 &", "timeout": 3 }
27
+ ]
28
+ }
29
+ ],
30
+ "Stop": [
31
+ {
32
+ "hooks": [
33
+ { "type": "command", "command": "agentbox-ctl codex-state idle >/dev/null 2>&1 &", "timeout": 3 }
34
+ ]
35
+ }
36
+ ]
37
+ }
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env bash
2
+ # Start the in-box dockerd. Launched by the host via
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.
8
+
9
+ set -euo pipefail
10
+
11
+ if pgrep -x dockerd >/dev/null; then
12
+ exit 0
13
+ fi
14
+
15
+ mkdir -p /var/lib/docker /var/run /var/log/agentbox
16
+
17
+ # /var/run lives in the container's writable layer (not a volume), so files
18
+ # written by the previous dockerd run survive `docker stop`/`start`. dockerd
19
+ # refuses to start if `/var/run/docker.pid` exists with a PID in /proc — and
20
+ # after restart, that PID number has been reassigned to (probably) the new
21
+ # `sleep infinity`. Wipe both the pidfile and the stale socket here; pgrep
22
+ # above already confirmed no real dockerd is running.
23
+ rm -f /var/run/docker.pid /var/run/docker.sock
24
+
25
+ # Cgroup v2 + unprivileged DinD on OrbStack/Docker Desktop: the outer
26
+ # container has /sys/fs/cgroup and /proc/sys bind-mounted RO from the host
27
+ # (Docker's standard hardening). We need both writable for dockerd to:
28
+ # * mkdir /sys/fs/cgroup/docker (its own cgroup slice for child containers)
29
+ # * write /proc/sys/net/ipv6/conf/<veth>/disable_ipv6 (default bridge setup)
30
+ # Without these, `docker run` fails with EROFS or "failed to disable IPv6".
31
+ # SYS_ADMIN + the private cgroup namespace let us remount these RW; the
32
+ # writes only affect the box's own namespaces (not the host). Failure is
33
+ # tolerable — some hosts already mount these RW.
34
+ mount -o remount,rw /sys/fs/cgroup 2>/dev/null || true
35
+ mount -o remount,rw /proc/sys 2>/dev/null || true
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
+
116
+ # nohup + & + disown lets us survive the `docker exec -d` returning. dockerd
117
+ # reads /etc/docker/daemon.json on its own; no flags here keeps the start path
118
+ # debuggable from inside the container (just edit the file and restart).
119
+ nohup dockerd >>/var/log/agentbox/dockerd.log 2>&1 &
120
+
121
+ # Wait for the socket to become accept()-able. Bound by ~30s — first start has
122
+ # to initialize iptables chains and the storage graphdriver (fuse-overlayfs is
123
+ # noticeably slower to initialize than overlay2).
124
+ for _ in $(seq 1 300); do
125
+ if [ -S /var/run/docker.sock ] \
126
+ && docker -H unix:///var/run/docker.sock info >/dev/null 2>&1; then
127
+ break
128
+ fi
129
+ sleep 0.1
130
+ done
131
+
132
+ disown -a
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bash
2
+ # Routes in-box URL opens to `agentbox-ctl open`, which opens the link in the
3
+ # box's own Chromium (agent-browser, visible via `agentbox screen`) and asks
4
+ # the host user — in the footer/dashboard — whether to also open it on the
5
+ # host. This script is installed at /usr/local/bin (earlier in PATH than
6
+ # xdg-utils' /usr/bin/xdg-open, which it is also symlinked over) and is the
7
+ # box's $BROWSER, so `xdg-open`, Claude Code's OAuth flow, `gh`,
8
+ # `git web--browse`, python's webbrowser, etc. all land here.
9
+ #
10
+ # Only http(s) URLs are routed. Anything else (a file path, another scheme)
11
+ # falls through to the real xdg-open, which resolves it locally in 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
@@ -0,0 +1,196 @@
1
+ ---
2
+ name: agentbox-setup
3
+ description: Generate an agentbox.yaml for the current AgentBox workspace. Invoke when the user opens a sandbox without an agentbox.yaml or asks to (re)configure one.
4
+ ---
5
+
6
+ # /agentbox-setup
7
+
8
+ ## Box layout (what you're configuring against)
9
+
10
+ Your user i `vscode` and you can use `sudo` to run commands as root.
11
+
12
+ `/workspace` is where the user code lives, a per-box git worktree on a fresh `agentbox/<box-name>` branch (or a tar-piped copy of the host workspace for non-git projects).
13
+ Run `agentbox checkpoint --set-default` (similar to `docker commit`) to save any changes make to the system and workspace so that new boxes will start from a warm state. Everything is wiped on `agentbox destroy`.
14
+
15
+ Some special folders:
16
+
17
+ - **Host main repo's `.git/`** — If the box bind-mounted RW at its identical absolute host path. In-box commits land on the host's branch refs (visible to `git log` on the host immediately); the box itself carries no SSH/git creds, so `git push` goes through the host relay (`agentbox-ctl git push`). The host's **working tree is never written to** — only refs/objects under `.git/`.
18
+ - **`~/.claude`** — and similar home folders for coding agents are seeded from the host's `~/.claude` on each create so auth, skills, and plugins persist without leaking the host's home dir.
19
+ - **`agentbox.yaml`** — read by `agentbox-ctl` from `/workspace`. Tasks and services declared here are what the supervisor will run.
20
+
21
+ Exposed ports and services:
22
+ - **portless** - every port with `expose:` setting in agentbox.yaml, will be exposed not only as a local port but also as a special domain name `https://<name>.localhost` (so on https) using `portless` cli and proxy. This will be also mapped to the host where also `portless` proxy is running so users can access the same service on the same looking url.
23
+ - **vnc** - the webVNC server exposed on 6080 will be proxies to the host on a random port.
24
+ - **vscode** - the vscode server is proxied to the host on a random port.
25
+
26
+ ## Goal
27
+
28
+ Produce a `/workspace/agentbox.yaml` that captures this project's services, tasks, and box defaults so the in-box supervisor (`agentbox-ctl`) can boot the workspace deterministically.
29
+
30
+ `agentbox.yaml` is **declarative**. The supervisor reads it on box start, but you don't have to restart the box: after you write the file, `agentbox-ctl reload` (run from inside the box) makes the already-running supervisor re-read it and immediately run the declared tasks and autostart the services. See step 8.
31
+
32
+ ## 1. Discover the project
33
+
34
+ Look at `/workspace`:
35
+
36
+ - Top-level manifests: `package.json`, `pyproject.toml` / `requirements.txt`, `Cargo.toml`, `go.mod`, `Gemfile`, `composer.json`, `mix.exs`, etc. — these tell you the runtime.
37
+ - `docker-compose.yaml` / `docker-compose.yml` — often lists the real services the project expects.
38
+ - `package.json` → `scripts`: look for `dev`, `start`, `build`, `test`, `migrate`, `seed`.
39
+ - `Makefile` / `justfile` / `Taskfile.yaml` — alternative task runners.
40
+ - Listening ports: grep for `listen(`, `PORT=`, framework defaults (3000 for Next.js / Nuxt, 5173 for Vite, 8000 for Django, 8080 for Spring, etc.).
41
+ - Database / cache deps to spin up locally (Postgres, Redis, …) — declare them as services if the project doesn't expect them to be external.
42
+
43
+ ## 2. Pick services and tasks
44
+
45
+ - **Services** = long-running. Web servers, watchers, queue workers, databases. `restart: on-failure` by default.
46
+ - **Tasks** = one-shot. `pnpm install`, DB migrations, codegen, fixture loaders, install apt packages. Wire dependent services with `needs:` so they wait for the task to finish successfully.
47
+ - Names: must match `[A-Za-z0-9_-]+`. Task names and service names share a namespace — no collisions.
48
+ - No cycles in `needs:`.
49
+ - **Always generate a dependency-install task** and make it the root of the `needs:` graph (every service that needs deps gets `needs: [install, …]`). Future boxes start from a snapshot of the final filesystem so they won't need this, but updates or moving to a cloud provider might need to rebuild the container from scratch. The filesystem can be then later captured by `agentbox-ctl checkpoint --set-default`. The task must be **idempotent and self-healing**: `agentbox-ctl` re-runs pending tasks on every box stop/start (the daemon dies with the container and is relaunched), so a plain `rm -rf node_modules && install` would wipe + reinstall on every start. Guard the rebuild with a marker file *inside* `node_modules` (the `.agentbox-installed` convention AgentBox uses internally): rebuild only when the marker is absent (fresh box), and be a fast no-op once it exists. Detect the package manager from the lockfile — never hardcode `pnpm`. See the worked example below.
50
+ - **Add a comment to the beginning** of the file to explain what you did and what issues you encountered, so that future run might use this information in case the project evolves and you need to update the agentbox.yaml file.
51
+ -
52
+
53
+ ## 3. Wire readiness probes (services only)
54
+
55
+ `ready_when:` lets the supervisor decide when a service is "ready" (vs. just "running"). Exactly one of these must be present:
56
+
57
+ - `port: 3000` — TCP connect (default host `127.0.0.1`; override with `host:`).
58
+ - `log_match: "Listening on"` — regex matched against stdout/stderr. First match flips the service to ready.
59
+ - `http: "http://127.0.0.1:3000/health"` — GET probe. Optional `expect_status: 200` (default: any 2xx).
60
+
61
+ Tunables: `interval_ms` (default 500), `initial_delay_ms` (default 0), `timeout_ms` (default 60000), `on_timeout: kill | mark_unhealthy` (default `kill` — re-enters the restart policy).
62
+
63
+ ### Mark the web service with `expose:`
64
+
65
+ The box's primary web app (the dev server / Next.js / API the user opens in a browser) should declare:
66
+
67
+ ```yaml
68
+ expose:
69
+ port: 3000 # the port this service listens on inside the box
70
+ as: 80 # must be 80 — the container port AgentBox publishes
71
+ ```
72
+
73
+ At most **one** service may set `expose:`. AgentBox forwards container `:80` to `127.0.0.1:<port>` and publishes it on the host with `portless` proxy to a <boxname>.localhost url, so `agentbox list`/`status` show it as the box's main URL on every engine (no OrbStack dependency). Set this on the same service whose `ready_when:` you just wrote (a DB or worker should **not** get `expose:`).
74
+
75
+ ## 4. Restart + backoff
76
+
77
+ Per service:
78
+
79
+ - `restart: always | on-failure | never` (default `on-failure`).
80
+ - `backoff:` — `initial_ms` (default 500), `max_ms` (default 30000; must be `>= initial_ms`), `factor` (default 2).
81
+
82
+ ## 5. (Optional) `defaults:` block
83
+
84
+ Sets per-project defaults for `agentbox create`/`claude`/`code`/`shell` — same shape as `~/.agentbox/config.yaml`. CLI flags still override. Common keys:
85
+
86
+ - `box.hostSnapshot` (bool) — APFS-clone the *host* workspace into a per-box scratch dir before seeding `/workspace` (stabilizes the tar-pipe source).
87
+ - `box.defaultCheckpoint` (string) — checkpoint new boxes start from (normally you set this via `agentbox-ctl checkpoint --set-default` at the end of setup — see section 9, not by hand).
88
+ - `box.withPlaywright` (bool) — install `@playwright/cli` globally inside the box.
89
+ - `box.vnc` (bool) — run Xvnc + noVNC on container port 6080.
90
+ - `box.isolateClaudeConfig` (bool) — per-box `~/.claude` volume instead of the shared one.
91
+ - `code.ide` — `vscode | cursor | auto`.
92
+ - `code.autoTerminals` (bool) — auto-generate `.vscode/tasks.json` with per-service tails.
93
+ - `browser.default` — `agent-browser | playwright | both`.
94
+
95
+ Full key list (run on the host): `agentbox config list --keys`.
96
+
97
+ ## 6. Worked example
98
+
99
+ ```yaml
100
+ # yaml-language-server: $schema=https://agentbox.dev/schema/agentbox.schema.json
101
+ # This agentbox.yaml setup this Next.js project, and includes:
102
+ # - a postgres database because it's used in the project
103
+ # - an inngest server for queues
104
+ # - a fix to move .turbo/cache folder to the workspace to avoid a permission error during setup
105
+ # - ...
106
+ defaults:
107
+ box:
108
+ withPlaywright: true
109
+ code:
110
+ ide: cursor
111
+
112
+ tasks:
113
+ # Idempotent install. /workspace is the container's writable filesystem, so
114
+ # node_modules persists across pause/stop/start and is captured by
115
+ # `agentbox checkpoint`. The host's node_modules is macOS-native and is
116
+ # never copied in, so force a clean Linux build the first time — but skip
117
+ # on every subsequent box start (agentbox-ctl re-runs pending tasks after
118
+ # stop/start). Adjust the lockfile detection to the project's package
119
+ # manager.
120
+ install:
121
+ command: |
122
+ set -e
123
+ MARKER=node_modules/.agentbox-installed
124
+ [ -f "$MARKER" ] && { echo "deps installed (marker present) — skip"; exit 0; }
125
+ apt-get update && apt-get install -y postgresql-client
126
+ rm -rf node_modules
127
+ if [ -f pnpm-lock.yaml ]; then
128
+ corepack enable >/dev/null 2>&1 || true
129
+ pnpm install --frozen-lockfile || pnpm install
130
+ fi
131
+ touch "$MARKER"
132
+
133
+ migrate:
134
+ command: pnpm db:migrate
135
+ needs: [install]
136
+
137
+ services:
138
+ postgres:
139
+ command: postgres -D /var/lib/postgresql/data
140
+ ready_when:
141
+ port: 5432
142
+ restart: always
143
+
144
+ dev:
145
+ command: pnpm dev
146
+ needs: [install, migrate, postgres]
147
+ ready_when:
148
+ port: 3000
149
+ timeout_ms: 120000
150
+ expose:
151
+ port: 3000
152
+ as: 80
153
+ restart: on-failure
154
+ backoff:
155
+ initial_ms: 500
156
+ max_ms: 5000
157
+ factor: 2
158
+ ```
159
+
160
+ ## 7. Validate before handing off
161
+
162
+ - check with `agentbox-ctl reload` and then `agentbox-ctl status` that everything is running as expected.
163
+ - Every name in `needs:` must reference an existing task or service.
164
+ - A service with `restart: never` and an autostart dependency will block the dependent forever after one failed run — usually a mistake.
165
+ - `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.
166
+
167
+ ## 8. Checkpoint the warm state (do this at the very end)
168
+
169
+ 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.
170
+ 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.
171
+ This doesn't need to be confirmed by the user.
172
+ It will pause the container for several seconds so warn the user about it and write Done when it's done.
173
+
174
+ ## 9. Hand-off
175
+
176
+ Tell the user (verbatim):
177
+
178
+ ```
179
+ █████╗ ██████╗ ███████╗███╗ ██╗████████╗██████╗ ██████╗ ██╗ ██╗
180
+ ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝██╔══██╗██╔═══██╗╚██╗██╔╝
181
+ ███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ ██████╔╝██║ ██║ ╚███╔╝
182
+ ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ██╔══██╗██║ ██║ ██╔██╗
183
+ ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ██████╔╝╚██████╔╝██╔╝ ██╗
184
+ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
185
+ ```
186
+
187
+ your box is ready, you can start more sessions with `agentbox claude`
188
+ you can access the web app at https://<boxname>.localhost
189
+
190
+ ## 10. Known issues
191
+
192
+ - For Nextjs/Vite/Tasnstack projects, makes sure to forward also websocket for hot reload.
193
+
194
+ - Service like flask, nextjs, BETTER_AUTH_URL, NEXT_PUBLIC_APP_URL should use the <boxname>.localhost url for the local development so that on the host it will use the same url as the box.
195
+
196
+ - The `install` task is intentionally a no-op once `node_modules/.agentbox-installed` exists. Do **not** remove the marker guard to "force a fresh install" — that reinstalls on every box start. To force a one-off rebuild, delete `node_modules` (or just the marker) then run `agentbox-ctl reload`.
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ # Start the per-box VNC stack: Xvnc on :1 (loopback inside container) +
3
+ # websockify on 0.0.0.0:6080 serving noVNC's HTML5 client and proxying RFB.
4
+ # Launched by the host via `docker exec -d --user vscode` after the container
5
+ # is up. Idempotent — re-running while the daemons are alive is a no-op, so
6
+ # `agentbox start` can blindly call us again.
7
+
8
+ set -euo pipefail
9
+
10
+ PASS="${AGENTBOX_VNC_PASSWORD:-}"
11
+ if [[ -z "$PASS" ]]; then
12
+ echo "agentbox-vnc-start: AGENTBOX_VNC_PASSWORD is not set" >&2
13
+ exit 64
14
+ fi
15
+
16
+ if pgrep -u "$(id -u)" -x Xvnc >/dev/null \
17
+ && pgrep -u "$(id -u)" -f "websockify.*6080" >/dev/null; then
18
+ exit 0
19
+ fi
20
+
21
+ mkdir -p "$HOME/.vnc"
22
+ # vncpasswd's -f mode reads plaintext on stdin, writes the DES blob to stdout.
23
+ # VncAuth truncates >8 chars at compare time, which is fine — the host writes
24
+ # an 8-char password. Write to a temp file + rename so a failure (e.g.,
25
+ # vncpasswd missing) doesn't leave an empty file that Xvnc would then reject.
26
+ TMP_PASSWD="$(mktemp "$HOME/.vnc/passwd.XXXXXX")"
27
+ printf '%s\n' "$PASS" | vncpasswd -f > "$TMP_PASSWD"
28
+ chmod 600 "$TMP_PASSWD"
29
+ mv "$TMP_PASSWD" "$HOME/.vnc/passwd"
30
+
31
+ mkdir -p /var/log/agentbox 2>/dev/null || true
32
+
33
+ # Xvnc on display :1, loopback-only (websockify is the only public ingress).
34
+ # 1280x800x24 is a sensible laptop-browser viewport.
35
+ # The clipboard params are on-by-default in TigerVNC 1.13 but pinned here so a
36
+ # base-image bump can't silently break host->box paste, and to document intent:
37
+ # accept cut-text from noVNC, set both the X CLIPBOARD and PRIMARY selections.
38
+ Xvnc :1 \
39
+ -localhost \
40
+ -SecurityTypes VncAuth \
41
+ -PasswordFile "$HOME/.vnc/passwd" \
42
+ -geometry 1280x800 \
43
+ -depth 24 \
44
+ -AlwaysShared \
45
+ -AcceptCutText=1 \
46
+ -SendCutText=1 \
47
+ -SetPrimary=1 \
48
+ -SendPrimary=1 \
49
+ >/var/log/agentbox/xvnc.log 2>&1 &
50
+
51
+ # Wait for Xvnc's RFB socket (5901). bash's /dev/tcp pseudo-device makes the
52
+ # probe a one-liner without needing netcat in the image.
53
+ for _ in $(seq 1 50); do
54
+ if (echo > /dev/tcp/127.0.0.1/5901) 2>/dev/null; then break; fi
55
+ sleep 0.1
56
+ done
57
+
58
+ # With no window manager, nothing owns the X selections, so Xvnc's RFB cut-text
59
+ # isn't reliably handed to Chromium on Ctrl+V. autocutsel (one daemon per
60
+ # selection) keeps CLIPBOARD and PRIMARY populated and synced. Best-effort: a
61
+ # clipboard failure must never abort VNC, and the pgrep guard keeps a stray
62
+ # re-entry from spawning duplicates (Xvnc is up now, so DISPLAY=:1 resolves).
63
+ if ! pgrep -u "$(id -u)" -x autocutsel >/dev/null; then
64
+ DISPLAY=:1 autocutsel -selection CLIPBOARD -fork >/dev/null 2>&1 || true
65
+ DISPLAY=:1 autocutsel -selection PRIMARY -fork >/dev/null 2>&1 || true
66
+ fi
67
+
68
+ # websockify serves noVNC at /vnc.html (--web) and tunnels WS frames to Xvnc's
69
+ # RFB. Bind 0.0.0.0:6080 so both Docker `-p` mappings and OrbStack's
70
+ # <name>.orb.local routing reach it.
71
+ websockify \
72
+ --web=/usr/share/novnc \
73
+ 0.0.0.0:6080 \
74
+ 127.0.0.1:5901 \
75
+ >/var/log/agentbox/websockify.log 2>&1 &
76
+
77
+ disown -a
@@ -0,0 +1,54 @@
1
+ {
2
+ "$comment": "AgentBox enterprise-managed Claude Code settings, baked into the box image at /etc/claude-code/managed-settings.json. Highest precedence and NOT synced from the host ~/.claude, so claude-hooks-filter.ts never touches it; per Claude Code, hook arrays MERGE across settings sources, so the user's own hooks still run. These hooks report Claude's activity to the box supervisor (agentbox-ctl claude-state -> ctl socket -> relay -> ~/.agentbox/boxes/<id>/status.json) so `agentbox status/list/inspect` can show it even when the box is paused. Each command is exit-0 fast and shell-backgrounded so a hook can never block or fail a Claude turn.",
3
+ "hooks": {
4
+ "UserPromptSubmit": [
5
+ {
6
+ "hooks": [
7
+ { "type": "command", "command": "agentbox-ctl claude-state working >/dev/null 2>&1 &", "timeout": 3 }
8
+ ]
9
+ }
10
+ ],
11
+ "PreToolUse": [
12
+ {
13
+ "hooks": [
14
+ { "type": "command", "command": "agentbox-ctl claude-state working >/dev/null 2>&1 &", "timeout": 3 }
15
+ ]
16
+ }
17
+ ],
18
+ "Stop": [
19
+ {
20
+ "hooks": [
21
+ { "type": "command", "command": "agentbox-ctl claude-state idle >/dev/null 2>&1 &", "timeout": 3 }
22
+ ]
23
+ }
24
+ ],
25
+ "Notification": [
26
+ {
27
+ "matcher": "permission_prompt",
28
+ "hooks": [
29
+ { "type": "command", "command": "agentbox-ctl claude-state waiting >/dev/null 2>&1 &", "timeout": 3 }
30
+ ]
31
+ },
32
+ {
33
+ "matcher": "idle_prompt",
34
+ "hooks": [
35
+ { "type": "command", "command": "agentbox-ctl claude-state idle >/dev/null 2>&1 &", "timeout": 3 }
36
+ ]
37
+ }
38
+ ],
39
+ "SessionStart": [
40
+ {
41
+ "hooks": [
42
+ { "type": "command", "command": "agentbox-ctl claude-state idle >/dev/null 2>&1 &", "timeout": 3 }
43
+ ]
44
+ }
45
+ ],
46
+ "SessionEnd": [
47
+ {
48
+ "hooks": [
49
+ { "type": "command", "command": "agentbox-ctl claude-state idle >/dev/null 2>&1 &", "timeout": 3 }
50
+ ]
51
+ }
52
+ ]
53
+ }
54
+ }