@madarco/agentbox 0.14.0 → 0.16.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 (66) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/dist/{_cloud-attach-GUBB5RH2.js → _cloud-attach-5KJWOASL.js} +4 -4
  3. package/dist/{chunk-RSKG7AFU.js → chunk-3WCEB6RE.js} +2 -2
  4. package/dist/{chunk-XKH7NTT7.js → chunk-DBBUDKKB.js} +248 -5
  5. package/dist/chunk-DBBUDKKB.js.map +1 -0
  6. package/dist/{chunk-TCS5HXJX.js → chunk-GXJNJUEV.js} +1090 -527
  7. package/dist/chunk-GXJNJUEV.js.map +1 -0
  8. package/dist/{chunk-LDMYHWUS.js → chunk-NW2UZQV6.js} +10 -6
  9. package/dist/chunk-NW2UZQV6.js.map +1 -0
  10. package/dist/{chunk-TBSIJVSN.js → chunk-PIK47622.js} +37 -17
  11. package/dist/chunk-PIK47622.js.map +1 -0
  12. package/dist/{chunk-BKU34KYY.js → chunk-QXFNLKJJ.js} +9 -3
  13. package/dist/{chunk-BKU34KYY.js.map → chunk-QXFNLKJJ.js.map} +1 -1
  14. package/dist/{chunk-BYCLD6D6.js → chunk-SB4QTF2T.js} +98 -54
  15. package/dist/chunk-SB4QTF2T.js.map +1 -0
  16. package/dist/{chunk-VATTS2MR.js → chunk-SENASAU4.js} +10 -6
  17. package/dist/{chunk-VATTS2MR.js.map → chunk-SENASAU4.js.map} +1 -1
  18. package/dist/{dist-34RKQ74M.js → dist-4IQFJJQI.js} +5 -5
  19. package/dist/{dist-4DPOL5A7.js → dist-7YB7BMNG.js} +5 -5
  20. package/dist/{dist-3IMQNTTV.js → dist-SL2QSMBE.js} +5 -5
  21. package/dist/{dist-J2IHD5T7.js → dist-VHI5QOSQ.js} +6 -6
  22. package/dist/{dist-57M6ZA7H.js → dist-XC47DSCR.js} +5 -5
  23. package/dist/index.js +1043 -333
  24. package/dist/index.js.map +1 -1
  25. package/dist/{prepared-state-MQHD3M5F-Q27AZU53.js → prepared-state-MQHD3M5F-2LANTRL7.js} +2 -2
  26. package/package.json +6 -5
  27. package/runtime/docker/Dockerfile.box +21 -2
  28. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +112 -29
  29. package/runtime/docker/packages/ctl/dist/bin.cjs +10353 -8575
  30. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +5 -2
  31. package/runtime/docker/packages/sandbox-docker/scripts/linear-shim +181 -0
  32. package/runtime/docker/packages/sandbox-docker/scripts/ntn-shim +95 -0
  33. package/runtime/e2b/agentbox-checkpoint-cleanup +5 -2
  34. package/runtime/e2b/agentbox-setup-skill.md +112 -29
  35. package/runtime/e2b/ctl.cjs +10353 -8575
  36. package/runtime/e2b/linear-shim +181 -0
  37. package/runtime/e2b/ntn-shim +95 -0
  38. package/runtime/e2b/scripts/build-template.sh +13 -7
  39. package/runtime/hetzner/agentbox-checkpoint-cleanup +5 -2
  40. package/runtime/hetzner/agentbox-setup-skill.md +112 -29
  41. package/runtime/hetzner/ctl.cjs +10353 -8575
  42. package/runtime/hetzner/linear-shim +181 -0
  43. package/runtime/hetzner/ntn-shim +95 -0
  44. package/runtime/hetzner/scripts/install-box.sh +19 -9
  45. package/runtime/relay/bin.cjs +3707 -2828
  46. package/runtime/vercel/agentbox-checkpoint-cleanup +5 -2
  47. package/runtime/vercel/agentbox-setup-skill.md +112 -29
  48. package/runtime/vercel/ctl.cjs +10353 -8575
  49. package/runtime/vercel/linear-shim +181 -0
  50. package/runtime/vercel/ntn-shim +95 -0
  51. package/runtime/vercel/scripts/provision.sh +13 -7
  52. package/share/agentbox-setup/SKILL.md +112 -29
  53. package/share/host-skills/agentbox-info/SKILL.md +22 -2
  54. package/dist/chunk-BYCLD6D6.js.map +0 -1
  55. package/dist/chunk-LDMYHWUS.js.map +0 -1
  56. package/dist/chunk-TBSIJVSN.js.map +0 -1
  57. package/dist/chunk-TCS5HXJX.js.map +0 -1
  58. package/dist/chunk-XKH7NTT7.js.map +0 -1
  59. /package/dist/{_cloud-attach-GUBB5RH2.js.map → _cloud-attach-5KJWOASL.js.map} +0 -0
  60. /package/dist/{chunk-RSKG7AFU.js.map → chunk-3WCEB6RE.js.map} +0 -0
  61. /package/dist/{dist-34RKQ74M.js.map → dist-4IQFJJQI.js.map} +0 -0
  62. /package/dist/{dist-4DPOL5A7.js.map → dist-7YB7BMNG.js.map} +0 -0
  63. /package/dist/{dist-3IMQNTTV.js.map → dist-SL2QSMBE.js.map} +0 -0
  64. /package/dist/{dist-J2IHD5T7.js.map → dist-VHI5QOSQ.js.map} +0 -0
  65. /package/dist/{dist-57M6ZA7H.js.map → dist-XC47DSCR.js.map} +0 -0
  66. /package/dist/{prepared-state-MQHD3M5F-Q27AZU53.js.map → prepared-state-MQHD3M5F-2LANTRL7.js.map} +0 -0
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env bash
2
+ # agentbox `linear` shim — translates a strict subset of `linear`
3
+ # (@schpet/linear-cli, v2) subcommands into `agentbox-ctl integration
4
+ # linear <op>` so the host's authenticated `linear` runs the operation and
5
+ # only the result crosses back into the box. The in-box agent never sees a
6
+ # Linear API token.
7
+ #
8
+ # Installed at /usr/local/bin/linear (real `linear` is not in the box).
9
+ #
10
+ # This shim ships only what documented agent flows need; anything outside
11
+ # the subset below is rejected with a clear error. Add ops deliberately —
12
+ # the relay is gated by `integrations.linear.enabled` and an explicit op
13
+ # allowlist in @agentbox/integrations.
14
+ #
15
+ # Three classes of upstream subcommand are EXPLICITLY rejected even though
16
+ # they exist on the host CLI, because proxying them would defeat the
17
+ # security model:
18
+ # - `auth token` PRINTS the raw API token to stdout — proxying it would
19
+ # hand the box the host's Linear credential. The only auth-family op
20
+ # we proxy is `auth whoami` (identity only), via `linear whoami`.
21
+ # - `auth login/logout/migrate/default` would mutate host auth state.
22
+ # - `issue delete` / `team delete` / `team create` are destructive and
23
+ # off-list (widen deliberately, as gated writes, only if needed).
24
+
25
+ set -euo pipefail
26
+
27
+ # Path is a constant in production; the env override exists purely to let
28
+ # unit tests substitute a stub `agentbox-ctl` on PATH without rewriting the
29
+ # shim. Mirrors gh-shim / git-shim / ntn-shim.
30
+ CTL="${AGENTBOX_CTL_PATH:-/usr/local/bin/agentbox-ctl}"
31
+
32
+ die() {
33
+ printf 'agentbox linear shim: %s\n' "$*" >&2
34
+ exit 2
35
+ }
36
+
37
+ handle_auth() {
38
+ local sub="${1-}"; shift || true
39
+ case "$sub" in
40
+ whoami)
41
+ exec "$CTL" integration linear whoami -- "$@"
42
+ ;;
43
+ token)
44
+ die "'auth token' leaks the raw API key — refused. Use 'linear whoami' for identity."
45
+ ;;
46
+ login|logout|migrate|default)
47
+ die "'auth $sub' is not proxied (the host owns auth; run it on the host)."
48
+ ;;
49
+ '')
50
+ die "missing subcommand for 'auth'. Supported: whoami"
51
+ ;;
52
+ *)
53
+ die "unsupported 'auth $sub' (allowed: whoami)"
54
+ ;;
55
+ esac
56
+ }
57
+
58
+ handle_issue_comment() {
59
+ local sub="${1-}"; shift || true
60
+ case "$sub" in
61
+ add)
62
+ exec "$CTL" integration linear issue.comment -- "$@"
63
+ ;;
64
+ '')
65
+ die "missing subcommand for 'issue comment'. Supported: add"
66
+ ;;
67
+ *)
68
+ die "unsupported 'issue comment $sub' (allowed: add)"
69
+ ;;
70
+ esac
71
+ }
72
+
73
+ handle_issue() {
74
+ local sub="${1-}"; shift || true
75
+ case "$sub" in
76
+ list)
77
+ exec "$CTL" integration linear issue.list -- "$@"
78
+ ;;
79
+ mine)
80
+ exec "$CTL" integration linear issue.mine -- "$@"
81
+ ;;
82
+ view)
83
+ exec "$CTL" integration linear issue.view -- "$@"
84
+ ;;
85
+ query)
86
+ exec "$CTL" integration linear issue.query -- "$@"
87
+ ;;
88
+ create)
89
+ exec "$CTL" integration linear issue.create -- "$@"
90
+ ;;
91
+ update)
92
+ exec "$CTL" integration linear issue.update -- "$@"
93
+ ;;
94
+ comment)
95
+ handle_issue_comment "$@"
96
+ ;;
97
+ delete)
98
+ die "'issue delete' is not proxied (destructive; off-list by default)."
99
+ ;;
100
+ '')
101
+ die "missing subcommand for 'issue'. Supported: list, mine, view, query, create, update, comment add"
102
+ ;;
103
+ *)
104
+ die "unsupported 'issue $sub' (allowed: list, mine, view, query, create, update, comment add)"
105
+ ;;
106
+ esac
107
+ }
108
+
109
+ handle_team() {
110
+ local sub="${1-}"; shift || true
111
+ case "$sub" in
112
+ list)
113
+ exec "$CTL" integration linear team.list -- "$@"
114
+ ;;
115
+ create|delete)
116
+ die "'team $sub' is not proxied (destructive; off-list by default)."
117
+ ;;
118
+ '')
119
+ die "missing subcommand for 'team'. Supported: list"
120
+ ;;
121
+ *)
122
+ die "unsupported 'team $sub' (allowed: list)"
123
+ ;;
124
+ esac
125
+ }
126
+
127
+ # Top-level dispatch. `linear`'s real subcommands are
128
+ # `auth issue team project cycle milestone initiative label document api schema`;
129
+ # we expose only the read-safe ones plus a few gated writes (no destructive
130
+ # ops, no auth token).
131
+ if [ $# -eq 0 ]; then
132
+ die "no subcommand. Supported: whoami, auth whoami, issue {list,mine,view,query,create,update,comment add}, team list, api <query>, --version"
133
+ fi
134
+
135
+ case "$1" in
136
+ --version|-v)
137
+ # Tools that sniff "linear --version" succeed with our shim line. The
138
+ # real version lives host-side and is reported by the relay's
139
+ # readiness probe (`assertIntegrationReady`).
140
+ printf 'linear version 0.0.0 (agentbox-shim)\n'
141
+ ;;
142
+ --help|-h)
143
+ printf 'agentbox linear shim — strict subset.\n' >&2
144
+ printf 'Supported: whoami, auth whoami, issue {list,mine,view,query,create,update,comment add}, team list, api <query>, --version\n' >&2
145
+ printf 'Anything else is rejected. Run host `linear --help` for full upstream docs.\n' >&2
146
+ ;;
147
+ whoami)
148
+ shift
149
+ exec "$CTL" integration linear whoami -- "$@"
150
+ ;;
151
+ auth)
152
+ shift
153
+ handle_auth "$@"
154
+ ;;
155
+ issue)
156
+ shift
157
+ handle_issue "$@"
158
+ ;;
159
+ team)
160
+ shift
161
+ handle_team "$@"
162
+ ;;
163
+ api)
164
+ shift
165
+ # `linear api` accepts pre-positional flags (`--variable`,
166
+ # `--variables-json`, `--paginate`, `--silent`) before the GraphQL
167
+ # query, so we don't require the FIRST arg to be a non-flag — only
168
+ # that some arg is present. The relay's refuseGraphqlNonQuery
169
+ # enforces query-only by rejecting any positional whose first
170
+ # keyword is `mutation`/`subscription` (and any `--variable
171
+ # key=@<path>` host-file load), so we don't duplicate that check
172
+ # here. Writes go through the dedicated issue.* ops.
173
+ if [ $# -eq 0 ]; then
174
+ die "'api' requires a positional <query> (e.g. '{ teams { id } }')"
175
+ fi
176
+ exec "$CTL" integration linear api -- "$@"
177
+ ;;
178
+ *)
179
+ die "'$1' is not proxied (supported: whoami, issue {list,mine,view,query,create,update,comment add}, team list, api <query>, --version)"
180
+ ;;
181
+ esac
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env bash
2
+ # agentbox `ntn` shim — translates a strict subset of `ntn` (the official
3
+ # Notion CLI) subcommands into `agentbox-ctl integration notion <op>` so the
4
+ # host's authenticated `ntn` runs the operation and only the result crosses
5
+ # back into the box. The in-box agent never sees a Notion token.
6
+ #
7
+ # Installed at /usr/local/bin/ntn (real `ntn` is not in the box). The same
8
+ # shim is symlinked as /usr/local/bin/notion — the per-service surface name
9
+ # from docs/integrations_backlog.md — both invocations behave identically.
10
+ #
11
+ # This shim ships only what documented agent flows need; anything outside
12
+ # the subset below is rejected with a clear error. Add ops deliberately —
13
+ # the relay is gated by `integrations.notion.enabled` and an explicit op
14
+ # allowlist in @agentbox/integrations.
15
+
16
+ set -euo pipefail
17
+
18
+ # Path is a constant in production; the env override exists purely to let
19
+ # unit tests substitute a stub `agentbox-ctl` on PATH without rewriting the
20
+ # shim. Mirrors gh-shim / git-shim.
21
+ CTL="${AGENTBOX_CTL_PATH:-/usr/local/bin/agentbox-ctl}"
22
+
23
+ die() {
24
+ printf 'agentbox notion shim: %s\n' "$*" >&2
25
+ exit 2
26
+ }
27
+
28
+ handle_pages() {
29
+ local op="${1-}"; shift || true
30
+ case "$op" in
31
+ create)
32
+ exec "$CTL" integration notion page.create -- "$@"
33
+ ;;
34
+ update)
35
+ exec "$CTL" integration notion page.update -- "$@"
36
+ ;;
37
+ '')
38
+ die "missing subcommand for 'pages'. Supported: create, update"
39
+ ;;
40
+ *)
41
+ die "unsupported 'pages $op' (allowed: create, update)"
42
+ ;;
43
+ esac
44
+ }
45
+
46
+ # Top-level dispatch. `ntn`'s real subcommands are
47
+ # `api datasources files pages login logout whoami workers`; we expose only
48
+ # the read-safe ones plus `pages {create,update}`.
49
+ if [ $# -eq 0 ]; then
50
+ die "no subcommand. Supported: whoami, api <endpoint>, pages {create,update}, --version"
51
+ fi
52
+
53
+ case "$1" in
54
+ --version|-v)
55
+ # Tools that sniff "ntn version" succeed with our shim line. The real
56
+ # version lives host-side and is reported by the relay's readiness probe
57
+ # (`assertIntegrationReady`).
58
+ printf 'ntn version 0.0.0 (agentbox-shim)\n'
59
+ ;;
60
+ --help|-h)
61
+ printf 'agentbox notion shim — strict subset.\n' >&2
62
+ printf 'Supported: whoami, api <path> [inputs] [-d JSON], pages {create, update}, --version\n' >&2
63
+ printf 'api is read-only: GET to any endpoint; POST only to v1/search and\n' >&2
64
+ printf 'v1/{databases,data_sources}/{id}/query. Writes go through `pages`.\n' >&2
65
+ printf 'Anything else is rejected. Run host `ntn --help` for full upstream docs.\n' >&2
66
+ ;;
67
+ whoami)
68
+ shift
69
+ exec "$CTL" integration notion whoami -- "$@"
70
+ ;;
71
+ api)
72
+ shift
73
+ # Forward verbatim to mirror real `ntn api` (options may precede the path;
74
+ # `ls`/`help`/`--spec`/`--docs` and `-d <JSON>` bodies are all valid). The
75
+ # relay's refuseUnsafeApiCall is the security boundary: GET to any endpoint,
76
+ # POST only to read endpoints (v1/search, v1/databases/{id}/query,
77
+ # v1/data_sources/{id}/query); every other method/endpoint is refused.
78
+ # Writes go through the dedicated `pages create/update` ops.
79
+ exec "$CTL" integration notion api -- "$@"
80
+ ;;
81
+ pages)
82
+ shift
83
+ handle_pages "$@"
84
+ ;;
85
+ comment|comments)
86
+ # The T1 connector intentionally has no comment op — `ntn` exposes no
87
+ # top-level `comment` subcommand and Notion's REST POST /v1/comments
88
+ # takes a structured JSON body that doesn't trivially map from CLI
89
+ # flags. Tracked as a focused follow-up in docs/notion_backlog.md.
90
+ die "comment ops not supported yet (deferred from T2; see docs/notion_backlog.md)"
91
+ ;;
92
+ *)
93
+ die "'$1' is not proxied (supported: whoami, api <endpoint>, pages {create,update}, --version)"
94
+ ;;
95
+ esac
@@ -22,6 +22,8 @@
22
22
  # /tmp/agentbox-open -- in-box xdg-open shim
23
23
  # /tmp/agentbox-gh-shim -- in-box `gh` shim (routes to host gh)
24
24
  # /tmp/agentbox-git-shim -- in-box `git` shim (routes via relay)
25
+ # /tmp/agentbox-ntn-shim -- in-box `ntn`/`notion` shim (routes to host ntn)
26
+ # /tmp/agentbox-linear-shim -- in-box `linear` shim (routes to host linear; rejects `auth token`)
25
27
  # /tmp/agentbox-custom-CLAUDE.md -- /etc/claude-code/CLAUDE.md content
26
28
  # /tmp/agentbox-managed-settings.json -- /etc/claude-code/managed-settings.json
27
29
  # /tmp/agentbox-codex-hooks.json -- /usr/local/share/agentbox/codex-hooks.json
@@ -96,10 +98,10 @@ visudo -cf /etc/sudoers >/dev/null
96
98
  done_ "vscode user + sudoers"
97
99
 
98
100
  step "agentbox base dirs + /workspace ownership"
99
- mkdir -p /workspace /run/agentbox /var/log/agentbox /etc/agentbox /etc/claude-code \
101
+ mkdir -p /workspace /run/agentbox /var/log/agentbox /var/lib/agentbox /etc/agentbox /etc/claude-code \
100
102
  /usr/local/share/agentbox
101
103
  chmod 755 /workspace
102
- chown vscode:vscode /workspace /run/agentbox /var/log/agentbox
104
+ chown vscode:vscode /workspace /run/agentbox /var/log/agentbox /var/lib/agentbox
103
105
  done_ "agentbox base dirs + /workspace ownership"
104
106
 
105
107
  step "node setcap (bind <1024 without root)"
@@ -317,15 +319,19 @@ done_ "dnf cleanup"
317
319
  # the bake there is no relay, so they must not shadow the real binaries until
318
320
  # provisioning is done. Installed from /tmp just before the trim step removes the
319
321
  # sources.
320
- step "relay shims (gh + git)"
321
- install -m 0755 /tmp/agentbox-gh-shim /usr/local/bin/gh
322
- install -m 0755 /tmp/agentbox-git-shim /usr/local/bin/git
323
- done_ "relay shims (gh + git)"
322
+ step "relay shims (gh + git + ntn + linear)"
323
+ install -m 0755 /tmp/agentbox-gh-shim /usr/local/bin/gh
324
+ install -m 0755 /tmp/agentbox-git-shim /usr/local/bin/git
325
+ install -m 0755 /tmp/agentbox-ntn-shim /usr/local/bin/ntn
326
+ ln -sf /usr/local/bin/ntn /usr/local/bin/notion
327
+ install -m 0755 /tmp/agentbox-linear-shim /usr/local/bin/linear
328
+ done_ "relay shims (gh + git + ntn + linear)"
324
329
 
325
330
  step "trim /tmp/agentbox-*"
326
331
  rm -f /tmp/agentbox-ctl /tmp/agentbox-vnc-start \
327
332
  /tmp/agentbox-checkpoint-cleanup /tmp/agentbox-open \
328
- /tmp/agentbox-gh-shim /tmp/agentbox-git-shim \
333
+ /tmp/agentbox-gh-shim /tmp/agentbox-git-shim /tmp/agentbox-ntn-shim \
334
+ /tmp/agentbox-linear-shim \
329
335
  /tmp/agentbox-custom-CLAUDE.md /tmp/agentbox-managed-settings.json \
330
336
  /tmp/agentbox-codex-hooks.json /tmp/agentbox-setup-skill.md
331
337
  mv /tmp/agentbox-provision.sh /var/log/agentbox/provision.sh 2>/dev/null || true
@@ -46,35 +46,56 @@ Look at `/workspace`:
46
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
47
  - Names: must match `[A-Za-z0-9_-]+`. Task names and service names share a namespace — no collisions.
48
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.
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**: `agentbox-ctl` re-runs pending tasks on every box stop/start (the daemon dies with the container and is relaunched), so an unguarded install would reinstall on every start. The clean way is the **`run_once: true`** field — the supervisor stores a marker keyed by a hash of the command and skips warm boots automatically (the marker lives at `/var/lib/agentbox/tasks/<name>`, on the box rootfs, captured by checkpoints, never polluting `/workspace`). Editing the command re-runs it. Detect the package manager from the lockfile — never hardcode `pnpm`. See the worked example below.
50
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
51
 
52
52
  ### Stateful services: data persistence & re-seeding (read this for databases)
53
53
 
54
+ **Declare a containerized dependency with the `image:` service form** — AgentBox
55
+ generates the `docker start`-or-`run` shell (no hand-written `docker run … || docker
56
+ start …`). The container runs in the box's dockerd; a published port is reachable
57
+ from other in-box services at `127.0.0.1:<host port>`:
58
+
59
+ ```yaml
60
+ services:
61
+ postgres:
62
+ image: # bare string (image: postgres:17-alpine) or a mapping:
63
+ name: postgres:17-alpine
64
+ ports: ["5432:5432"]
65
+ env:
66
+ POSTGRES_PASSWORD: postgres
67
+ POSTGRES_DB: app
68
+ args: "-c max_connections=200" # string or ["-c","max_connections=200"]
69
+ container_name: app_db # optional; default = service name
70
+ ready_when: { port: 5432 }
71
+ restart: always
72
+ ```
73
+
74
+ The container is reused by name across box stop/start. (Changing `image`/`env`
75
+ reuses the existing container as-is; `docker rm <container_name>` + `agentbox-ctl
76
+ reload` to apply.) Install the DB client the migrate/seed tasks need (e.g.
77
+ `postgresql-client`) in the `install` task and reach the DB over TCP — don't
78
+ `docker exec` the container (nested exec fails with a `setns` error in a box).
79
+
54
80
  **A checkpoint does NOT capture docker-in-docker data.** `agentbox checkpoint` is a `docker commit` of the box's writable filesystem (the system + `/workspace`). The in-box `dockerd` keeps its storage in a *separate* per-box volume (`/var/lib/docker`), which is **not** part of that image — it's fresh on every new box and wiped on `agentbox destroy`. So a database or cache you run as a **docker container** (e.g. `docker run … postgres`) starts **empty on every new box** created from a checkpoint (every `agentbox claude` / `agentbox create`), even though `/workspace` and any marker files you wrote were restored. (A DB run as a **native process** with its data dir on the box filesystem — e.g. `postgres -D /var/lib/postgresql/data` — *is* captured by the checkpoint, since it lives in the writable layer.)
55
81
 
56
- **Consequence for migrate/seed tasks of a containerized DB: do not gate them on a filesystem marker.** A marker like `node_modules/.agentbox-installed` is correct for deps (they live in `/workspace`, which the checkpoint captures), but **wrong** for DB data living in a docker volume: the marker is restored from the checkpoint while the DB is empty, so a marker-guarded seed wrongly skips and the app boots against an empty database. Instead, **gate on the actual data** connect to the DB and check whether a sentinel table/row exists, and seed only when it's missing:
82
+ **Consequence for migrate/seed tasks of a containerized DB: do NOT use `run_once: true` (the marker form).** A command-hash marker is correct for deps (they live in `/workspace`, which the checkpoint captures), but **wrong** for DB data living in a docker volume: the marker is restored from the checkpoint while the DB is empty, so a marker-guarded seed wrongly skips and the app boots against an empty database. Instead use the **`run_once: { check: <cmd> }`** form — the probe runs first and the seed runs unless the probe exits 0, and **no marker is written** (the DB is the source of truth). Gate on the actual data:
57
83
 
58
84
  ```yaml
59
85
  seed:
60
- # Re-seed when the DB is empty. The postgres data lives in the in-box
61
- # docker volume, which is NOT captured by `agentbox checkpoint` — so a box
62
- # started from a checkpoint has the workspace warm but an empty DB. We can't
63
- # use a filesystem marker here (it would be restored while the DB is blank);
64
- # instead probe the DB and seed only if the data is absent. Fast no-op once
86
+ # Re-seed when the DB is empty. The postgres data lives in the in-box docker
87
+ # volume, which is NOT captured by `agentbox checkpoint` — so a box started
88
+ # from a checkpoint has the workspace warm but an empty DB. The marker form
89
+ # would be restored while the DB is blank and wrongly skip; the `check` probe
90
+ # gates on the data itself. Exit 0 = already seeded, skip. Fast no-op once
65
91
  # the data is present.
66
- command: |
67
- set -e
68
- export PGPASSWORD=postgres
69
- # Probe for existing data. If the table is missing the query errors,
70
- # stderr is suppressed, stdout is empty, the grep fails — so we seed.
71
- if psql -h 127.0.0.1 -p 5432 -U postgres -d app -tAc \
72
- "SELECT EXISTS (SELECT 1 FROM users LIMIT 1)" 2>/dev/null | grep -q t; then
73
- echo "data present — skip seed"
74
- exit 0
75
- fi
76
- pnpm db:seed
92
+ command: pnpm db:seed
77
93
  needs: [install, migrate]
94
+ run_once:
95
+ check: |
96
+ export PGPASSWORD=postgres
97
+ psql -h 127.0.0.1 -p 5432 -U postgres -d app -tAc \
98
+ "SELECT EXISTS (SELECT 1 FROM users LIMIT 1)" 2>/dev/null | grep -q t
78
99
  ```
79
100
 
80
101
  **Lifecycle nuance (this is why the data check, not a marker, is right):**
@@ -148,22 +169,19 @@ tasks:
148
169
  # Idempotent install. /workspace is the container's writable filesystem, so
149
170
  # node_modules persists across pause/stop/start and is captured by
150
171
  # `agentbox checkpoint`. The host's node_modules is macOS-native and is
151
- # never copied in, so force a clean Linux build the first time — but skip
152
- # on every subsequent box start (agentbox-ctl re-runs pending tasks after
153
- # stop/start). Adjust the lockfile detection to the project's package
154
- # manager.
172
+ # never copied in, so the first Linux install runs; `run_once: true` then
173
+ # skips it on every subsequent box start (the supervisor stores a marker
174
+ # keyed by a hash of the command). Adjust the lockfile detection to the
175
+ # project's package manager.
155
176
  install:
156
177
  command: |
157
178
  set -e
158
- MARKER=node_modules/.agentbox-installed
159
- [ -f "$MARKER" ] && { echo "deps installed (marker present) — skip"; exit 0; }
160
- apt-get update && apt-get install -y postgresql-client
161
- rm -rf node_modules
179
+ sudo apt-get update && sudo apt-get install -y postgresql-client
162
180
  if [ -f pnpm-lock.yaml ]; then
163
181
  corepack enable >/dev/null 2>&1 || true
164
182
  pnpm install --frozen-lockfile || pnpm install
165
183
  fi
166
- touch "$MARKER"
184
+ run_once: true
167
185
 
168
186
  migrate:
169
187
  command: pnpm db:migrate
@@ -192,6 +210,36 @@ services:
192
210
  factor: 2
193
211
  ```
194
212
 
213
+ ## 6b. Bringing extra host files/folders into the box
214
+
215
+ Two ways to copy host files in (both COPY — never a live mount, so the box can't
216
+ write back to the host):
217
+
218
+ - **`carry:` block** (declarative, in `agentbox.yaml`) — for files/dirs every box
219
+ should get at create time. Each entry is `{ src, dest }` with optional `mode`,
220
+ `user`, `optional`, and `exclude:` (a list of tar globs / bare dir names to drop
221
+ when copying a directory). Heavy regenerable dirs (`.git`, `node_modules`, `bin`,
222
+ `obj`, `packages`, `dist`, `.next`, `target`) are dropped by default; `exclude:`
223
+ is additive. Each carry entry is capped at `box.cpMaxBytes` (default 100 MiB
224
+ after excludes) — the same limit `agentbox cp` enforces.
225
+ - **`agentbox-ctl cp fromHost <hostPath> <boxPath>`** (ad-hoc, from inside the box)
226
+ — for a one-off copy. Prompts the user on the host to approve.
227
+
228
+ **The per-copy size limit (important for large/legacy folders).** A single copy is
229
+ blocked above `box.cpMaxBytes` (default **100 MB**) *after* default excludes, so it
230
+ fails loud instead of silently hanging. When blocked you get a `du`-style tree of
231
+ the biggest remaining folders/subfolders. To get under the limit, EITHER:
232
+
233
+ - **drop what the box can regenerate** (the default excludes already remove
234
+ `node_modules`/`.git`/build output; add more with `--exclude=<glob-or-name>`), OR
235
+ - **copy the heavy folders one at a time** so each copy is under the limit, OR
236
+ - pass `--yes` to copy the whole thing anyway (only when you really need it all).
237
+
238
+ Example: a 2.4 GB legacy folder is mostly `packages/` (NuGet) + `.git`; those are
239
+ excluded by default, and what's left can be split:
240
+ `agentbox-ctl cp fromHost ../legacy/src /workspace/legacy/src` then
241
+ `... cp fromHost ../legacy/Database /workspace/legacy/Database`.
242
+
195
243
  ## 7. Validate before handing off
196
244
 
197
245
  - check with `agentbox-ctl reload` and then `agentbox-ctl status` that everything is running as expected.
@@ -228,6 +276,41 @@ On Vercel: this actually STOPS the sandbox, so warn the user about it. Also the
228
276
 
229
277
  - For Nextjs/Vite/Tasnstack projects, makes sure to forward also websocket for hot reload.
230
278
 
231
- - 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.
279
+ - 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. Render this automatically instead of hand-writing `sed` — see section 6c.
280
+
281
+ - The `install` task above uses `run_once: true`, so it is a no-op on warm boots. Do **not** wrap it in a manual marker check too. To force a one-off rebuild, run `agentbox-ctl run-task install --force` (which bypasses the run_once marker), or edit the command (a changed command invalidates the hash and re-runs).
282
+
283
+ ## 11. Pin URLs / render config files (env, secrets)
284
+
285
+ Many apps hard-code a hostname (e.g. `optima.localhost`) or read a gitignored `.env`. Instead of long `sed` commands in a task, use the built-ins:
286
+
287
+ - **`agentbox-ctl render <src>`** — a declarative `sed` for files already in the workspace. `--env` substitutes `{{AGENTBOX_*}}` placeholders; `--rules <name>` applies a named rule-set from the top-level `replacements:` block; `--rule 'from=>to'` / `--rule-regex 'pat=>repl'` are inline. Write to `--out <path>` (or `--in-place`). The whitelist placeholders are `{{AGENTBOX_BOX_NAME}}`, `{{AGENTBOX_BOX_HOST}}` (= `<boxname>.localhost`), `{{AGENTBOX_BOX_ID}}`, `{{AGENTBOX_BOX_KIND}}`, `{{AGENTBOX_HOST_WORKSPACE}}`, `{{AGENTBOX_PROJECT_ROOT}}`.
288
+
289
+ Render a gitignored `.env` from a committed `env.example` on every boot, pinning the URLs to this box:
290
+
291
+ ```yaml
292
+ replacements:
293
+ box-host:
294
+ - { from: 'optima\.localhost', to: '{{AGENTBOX_BOX_HOST}}', regex: true } # {{AGENTBOX_BOX_HOST}} = <box>.localhost
295
+
296
+ tasks:
297
+ env:
298
+ # The render is idempotent (the rules re-pin the same lines every boot), so
299
+ # no `run_once:` guard is needed — it self-corrects on a checkpoint-started
300
+ # box that carries a different box's host in .env.
301
+ command: agentbox-ctl render apps/saas/env.example --out apps/saas/.env --env --rules box-host
302
+ ```
303
+
304
+ Note: an `run_once: { check: <cmd> }` probe runs verbatim via `bash -c` with the box env — use shell vars like `$AGENTBOX_BOX_NAME`, NOT `{{…}}` placeholders (those are only expanded by `render`/carry, never by the supervisor).
305
+
306
+ **Generated secrets:** put `{{AGENTBOX_AUTO_SECRET}}` in the template for a value like `BETTER_AUTH_SECRET` instead of shelling out to `openssl rand`. Unnamed → a fresh 32-byte base64url secret each render (stable when you render the template→`.env` once). `{{AGENTBOX_AUTO_SECRET:better-auth}}` → generated once, persisted at `/var/lib/agentbox/secrets/<name>`, reused on every render (stable even if you render every boot). Example `env.example` line: `BETTER_AUTH_SECRET="{{AGENTBOX_AUTO_SECRET:better-auth}}"`.
307
+
308
+ - **`carry:` + `replaceEnvs`/`replace`/`rules`** — for a host-only file (e.g. a real `.env` with secrets that never lives in the repo), carry it in and render it host-side in one step (file entries only):
232
309
 
233
- - 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`.
310
+ ```yaml
311
+ carry:
312
+ - src: ~/secrets/optima.env
313
+ dest: /workspace/apps/saas/.env
314
+ replaceEnvs: true
315
+ rules: [box-host]
316
+ ```
@@ -174,10 +174,29 @@ Wrap step 2 in a loop to babysit a box across many turns. Use the narrow `wait-f
174
174
  Implications for you, the host-side agent:
175
175
 
176
176
  - Inside the box you can `git commit … && git push` exactly as normal. No setup needed.
177
- - Pushes are gated host-side: the relay can require a confirm prompt for destructive operations (the user sees it in the dashboard footer, ~25 s TTL). If a push appears to hang, tell the user to check the dashboard.
177
+ - Pushes are gated host-side: the relay can require a confirm prompt for destructive operations (the user sees it in the dashboard footer, ~25 s TTL). If a push appears to hang, it's waiting on this approval see "Answering host-action approvals" below.
178
178
  - The relay process is started lazily by the first `agentbox create` / `agentbox claude` and persists across runs (PID at `~/.agentbox/relay.pid`, log at `~/.agentbox/relay.log`). You normally don't need to manage it.
179
179
  - For HTTPS origins (`https://github.com/...`), pushing usually needs a credential — recommend the user run `gh auth login` and `gh auth setup-git` once on the host. After that, host `git push` uses gh's OAuth token automatically. SSH origins (`git@github.com:...`) keep using the host's SSH agent as before.
180
180
 
181
+ ## Answering approvals (orchestrator path)
182
+
183
+ When you are **orchestrating boxes unattended** (no human watching the dashboard footer), a box blocks on two kinds of approval and `agent approvals` / `agent approve` cover **both**:
184
+
185
+ - **Relay host-action approvals** — `git push` / `cp` / `gh pr` write / checkpoint. You answer them yourself; you're a host process that already holds the user's git/file credentials, so approving grants nothing you don't already have.
186
+ - **In-TUI agent prompts** — Claude plan-mode approval, `AskUserQuestion`, a tool-permission dialog. Previously you had to craft `drive keypress` sends by hand; now `approve` enacts the right keystrokes for you.
187
+
188
+ ```bash
189
+ agentbox agent approvals 1 --json # list everything box 1 is blocked on: each row has an id + kind
190
+ agentbox agent approve <id> # answer that exact prompt (default = approve / first option)
191
+ agentbox agent approve <id> --option 2 # in-TUI question/plan: pick option 2 (or --option "Risk first")
192
+ agentbox agent approve <id> --deny # reject (relay: deny; in-TUI: Escape)
193
+ agentbox agent approvals 1 --wait 600000 # block until something is pending, then act
194
+ ```
195
+
196
+ `kind` is `host-action` (relay), or `plan` / `question` / `permission` (in-TUI). Relay rows carry `command`/`argv`; `question` rows carry the option labels; `plan` rows the plan body.
197
+
198
+ **The id is a safety token — inspect, then approve that exact id.** `approve <id>` answers the specific prompt you listed; if a *different* prompt has since taken its place, the recomputed id won't match and the approve is **refused** (it never answers the wrong thing). So always `approvals` → read the `command`/`argv`/options → `approve <id>`, one at a time. Do not blanket-approve whatever a box asks (that defeats the gate against a prompt-injected box laundering a malicious push), and never hand-`curl` `/admin/prompts/answer` — these commands are the supported surface. In-TUI keystroke mapping is best-effort and TUI-version-sensitive; if an approve doesn't take, fall back to `drive snapshot` + `drive keypress`.
199
+
181
200
  ## PRs through the host relay (`agentbox-ctl git pr …`)
182
201
 
183
202
  In-box agents can drive GitHub PRs from inside a box via the host's `gh` CLI. Same model as `git push`: the box has no GitHub token; the relay shells out to `gh` on the host with the user's authenticated gh identity. Requires `gh` installed on the host and `gh auth login` run once.
@@ -208,6 +227,7 @@ If a PR op appears to hang, tell the user to check the dashboard footer for the
208
227
  | `agentbox code [n\|name]` | Open VS Code / Cursor pointed at the box. |
209
228
  | `agentbox prepare --provider <name>` | One-time base image / snapshot build for `daytona` or `hetzner` or `vercel`. With no `--provider`, prints status across all providers. |
210
229
  | `agentbox prune --provider <name>` | Clean up orphan boxes / images / snapshots for a provider (docker + daytona supported; hetzner pending). |
230
+ | `agentbox cp <src> <dst>` | Copy a file/dir host↔box (`box:/path` prefix picks direction). Heavy dirs (`.git`, `node_modules`, build output) are dropped by default; add `--exclude=<glob\|name>` or `--no-default-excludes`. Uploads over `box.cpMaxBytes` (100 MB, post-exclude) are **blocked** with a size breakdown — trim with `--exclude`, copy heavy folders one at a time, or pass `--yes`. |
211
231
 
212
232
  Per-project numeric index (`1`, `2`, …) and friendly name (`review`, `smoke`) both work wherever `<box>` is accepted. Index `1` is the first box created in the current workspace.
213
233
 
@@ -217,7 +237,7 @@ Per-project numeric index (`1`, `2`, …) and friendly name (`review`, `smoke`)
217
237
  2. **Use `-i` whenever the user asks for parallel agent work** rather than spawning multiple foreground sessions. Then point them at `agentbox dashboard` to watch progress.
218
238
  3. **Pick the provider deliberately.** `docker` is the fast default. `--provider hetzner` gives a real VPS (heavier, isolated, requires `agentbox prepare --provider hetzner` once). `--provider vercel` is the managed cloud option.
219
239
  4. **Cross-check before recommending a command.** If a flag isn't listed here, run `agentbox <command> --help` (it's safe and read-only) before suggesting it to the user.
220
- 5. **`/agentbox-setup` is a different skill.** It runs *inside* a box to generate `/workspace/agentbox.yaml`. Don't conflate it with `/agentbox` (host-side fork) or this reference skill.
240
+ 5. **`/agentbox-setup` is a different skill.** It runs *inside* a box to generate `/workspace/agentbox.yaml`. Don't conflate it with `/agentbox` (host-side fork) or this reference skill. When authoring `agentbox.yaml`, prefer the declarative `run_once: true` / `run_once: { check }` task field over hand-rolled marker/probe guards, and `agentbox-ctl render` / carry `replaceEnvs` over `sed` for pinning env URLs to `{{AGENTBOX_BOX_HOST}}`.
221
241
 
222
242
  ## Reference
223
243