@madarco/agentbox 0.13.0 → 0.15.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 (74) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.md +11 -8
  3. package/dist/{_cloud-attach-HJC672UR.js → _cloud-attach-R6TRWG5L.js} +4 -4
  4. package/dist/{chunk-QYRK5H6Q.js → chunk-43Q5GWP6.js} +108 -56
  5. package/dist/chunk-43Q5GWP6.js.map +1 -0
  6. package/dist/{chunk-ECLLV5JH.js → chunk-72CJTXN6.js} +156 -5
  7. package/dist/chunk-72CJTXN6.js.map +1 -0
  8. package/dist/{chunk-R5XIDQFR.js → chunk-BKU34KYY.js} +170 -6
  9. package/dist/chunk-BKU34KYY.js.map +1 -0
  10. package/dist/{chunk-4NQXNQ53.js → chunk-E7CHS7ZR.js} +168 -58
  11. package/dist/chunk-E7CHS7ZR.js.map +1 -0
  12. package/dist/chunk-MCOU6CZS.js +346 -0
  13. package/dist/chunk-MCOU6CZS.js.map +1 -0
  14. package/dist/{chunk-B4QG2MCW.js → chunk-MLMFNN4T.js} +762 -483
  15. package/dist/chunk-MLMFNN4T.js.map +1 -0
  16. package/dist/{chunk-2LF5YILI.js → chunk-RSKG7AFU.js} +80 -6
  17. package/dist/chunk-RSKG7AFU.js.map +1 -0
  18. package/dist/{chunk-SNTHHWKY.js → chunk-XKH7NTT7.js} +80 -22
  19. package/dist/chunk-XKH7NTT7.js.map +1 -0
  20. package/dist/{dist-7KVUIKJX.js → dist-AGTIA7AD.js} +37 -226
  21. package/dist/dist-AGTIA7AD.js.map +1 -0
  22. package/dist/{dist-OPIBZ7XM.js → dist-FIFEFKJ7.js} +14 -69
  23. package/dist/dist-FIFEFKJ7.js.map +1 -0
  24. package/dist/dist-JZ3XO6EB.js +662 -0
  25. package/dist/dist-JZ3XO6EB.js.map +1 -0
  26. package/dist/{dist-OG6NW6SM.js → dist-OGJGZETZ.js} +5 -3
  27. package/dist/{dist-JAN5VABY.js → dist-S4XR4ACV.js} +25 -177
  28. package/dist/dist-S4XR4ACV.js.map +1 -0
  29. package/dist/index.js +2229 -1314
  30. package/dist/index.js.map +1 -1
  31. package/dist/{prepared-state-MQHD3M5F-KE4DT3GX.js → prepared-state-MQHD3M5F-Q27AZU53.js} +2 -2
  32. package/package.json +6 -4
  33. package/runtime/docker/Dockerfile.box +21 -26
  34. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +67 -1
  35. package/runtime/docker/packages/ctl/dist/bin.cjs +361 -43
  36. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-vnc-start +17 -6
  37. package/runtime/docker/packages/sandbox-docker/scripts/chromium-resolver +57 -0
  38. package/runtime/docker/packages/sandbox-docker/scripts/claude-managed-settings.json +2 -1
  39. package/runtime/e2b/agentbox-checkpoint-cleanup +52 -0
  40. package/runtime/e2b/agentbox-codex-hooks.json +68 -0
  41. package/runtime/e2b/agentbox-open +28 -0
  42. package/runtime/e2b/agentbox-setup-skill.md +263 -0
  43. package/runtime/e2b/agentbox-vnc-start +102 -0
  44. package/runtime/e2b/attach-helper.cjs +167 -0
  45. package/runtime/e2b/claude-managed-settings.json +116 -0
  46. package/runtime/e2b/ctl.cjs +24158 -0
  47. package/runtime/e2b/custom-system-CLAUDE.md +46 -0
  48. package/runtime/e2b/gh-shim +344 -0
  49. package/runtime/e2b/git-shim +131 -0
  50. package/runtime/e2b/scripts/build-template.sh +295 -0
  51. package/runtime/hetzner/agentbox-setup-skill.md +67 -1
  52. package/runtime/hetzner/agentbox-vnc-start +17 -6
  53. package/runtime/hetzner/claude-managed-settings.json +2 -1
  54. package/runtime/hetzner/ctl.cjs +361 -43
  55. package/runtime/relay/bin.cjs +380 -233
  56. package/runtime/vercel/agentbox-setup-skill.md +67 -1
  57. package/runtime/vercel/agentbox-vnc-start +17 -6
  58. package/runtime/vercel/claude-managed-settings.json +2 -1
  59. package/runtime/vercel/ctl.cjs +361 -43
  60. package/share/agentbox-setup/SKILL.md +67 -1
  61. package/share/host-skills/agentbox-info/SKILL.md +47 -35
  62. package/dist/chunk-2LF5YILI.js.map +0 -1
  63. package/dist/chunk-4NQXNQ53.js.map +0 -1
  64. package/dist/chunk-B4QG2MCW.js.map +0 -1
  65. package/dist/chunk-ECLLV5JH.js.map +0 -1
  66. package/dist/chunk-QYRK5H6Q.js.map +0 -1
  67. package/dist/chunk-R5XIDQFR.js.map +0 -1
  68. package/dist/chunk-SNTHHWKY.js.map +0 -1
  69. package/dist/dist-7KVUIKJX.js.map +0 -1
  70. package/dist/dist-JAN5VABY.js.map +0 -1
  71. package/dist/dist-OPIBZ7XM.js.map +0 -1
  72. /package/dist/{_cloud-attach-HJC672UR.js.map → _cloud-attach-R6TRWG5L.js.map} +0 -0
  73. /package/dist/{dist-OG6NW6SM.js.map → dist-OGJGZETZ.js.map} +0 -0
  74. /package/dist/{prepared-state-MQHD3M5F-KE4DT3GX.js.map → prepared-state-MQHD3M5F-Q27AZU53.js.map} +0 -0
@@ -0,0 +1,46 @@
1
+ # AgentBox sandbox (e2b provider)
2
+
3
+ You are running inside an AgentBox sandbox: an E2B microVM (Firecracker on
4
+ Debian) provisioned just for this box. Your user is `vscode` and you can use
5
+ passwordless **sudo** to run commands as root. The whole microVM is yours — the
6
+ user's host filesystem is not visible from here and nothing is bind-mounted.
7
+
8
+ **No containers.** E2B microVMs can't run nested containers, so `docker` /
9
+ `podman` cannot run here — not even rootless. Don't try to start a container
10
+ engine; run build/test/dev processes directly on the microVM instead.
11
+
12
+ This box is **persistent**: stopping it pauses the microVM (cold-store) and
13
+ resuming wakes it back up, so the filesystem survives a pause. You can also save
14
+ the current filesystem state for future boxes with
15
+ `agentbox-ctl checkpoint --set-default` — that calls E2B's `createSnapshot` to
16
+ mint a reusable template snapshot. A checkpoint/snapshot PAUSES the box while
17
+ it's captured, so use it only at the end of the setup wizard.
18
+
19
+ `/workspace` is a normal git checkout seeded from the host repo at create time.
20
+ Because there is no host bind-mount, plain `git` inside the box only affects
21
+ this box-local repo — commits do **not** appear in the user's host `git log`
22
+ until you hand them off. For any operation that must reach the host repo or its
23
+ remotes (push, fetch, pull, picking up host-side changes), use
24
+ `agentbox-ctl git push|fetch|pull -- <args>` — it RPCs to the host, which runs
25
+ git with the real SSH agent and writes back into the host's worktree state. The
26
+ wrapper already builds `git push <remote> <branch>` host-side from the
27
+ registered worktree; the `-- <args>` slot is for extra flags only (e.g.
28
+ `--force-with-lease`, `--tags`). Re-passing the remote or branch makes git treat
29
+ them as refspecs and fails with `refs/remotes/origin/HEAD cannot be resolved to
30
+ branch`.
31
+
32
+ For GitHub PR work, use `agentbox-ctl git pr <op> [args...]` — same model, relay
33
+ shells to host `gh`. Ops: `create`, `view`, `list`, `comment`, `review`,
34
+ `merge`, `close`, `reopen`, `checkout`. `view` / `list` are read-only and run
35
+ silently; everything else asks the user to confirm in the host wrapper (deny →
36
+ exit 10).
37
+
38
+ For ad-hoc file transfers between this box and the host, use
39
+ `agentbox-ctl cp toHost <boxPath> <hostPath>` and
40
+ `agentbox-ctl cp fromHost <hostPath> <boxPath>` or `agentbox-ctl download claude` /
41
+ `download env` / `download config`. They RPC to the host and ask the user for
42
+ confirmation on the wrapper that runs `agentbox claude`; deny returns exit 10
43
+ (`denied by user`). Don't put any timeout on the command, it will run forever
44
+ and the user will be notified through multiple channels.
45
+
46
+ Box identity: /etc/agentbox/box.env and the AGENTBOX_* env vars.
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env bash
2
+ # agentbox `gh` shim — translates a strict subset of `gh` subcommands into
3
+ # `agentbox-ctl gh ...` so the host's authenticated `gh` runs the operation
4
+ # and only the result crosses back into the box. The in-box agent never sees
5
+ # a GitHub token.
6
+ #
7
+ # This shim ships only what Claude Code's PR badge and our documented agent
8
+ # flows need. Anything outside the subset below is rejected with a clear
9
+ # error — better safe than compatible. Add ops deliberately, not by default.
10
+
11
+ set -euo pipefail
12
+
13
+ # Paths are constants in production; env overrides exist purely to let unit
14
+ # tests substitute a stub `agentbox-ctl` on PATH without rewriting the shim.
15
+ CTL="${AGENTBOX_CTL_PATH:-/usr/local/bin/agentbox-ctl}"
16
+ REAL_GIT="${AGENTBOX_REAL_GIT_PATH:-/usr/bin/git}"
17
+
18
+ die() {
19
+ printf 'agentbox gh shim: %s\n' "$*" >&2
20
+ exit 2
21
+ }
22
+
23
+ # Resolve the in-box current branch once. Used to inject the right ref into
24
+ # `gh pr` commands so the host's `gh` (which runs in the host main repo, not
25
+ # the box's worktree) doesn't fall back to the host's HEAD.
26
+ box_branch() {
27
+ "$REAL_GIT" -C "$PWD" rev-parse --abbrev-ref HEAD 2>/dev/null || true
28
+ }
29
+
30
+ # Returns 0 if any element of "$@" equals "$1" (the needle).
31
+ needle_present() {
32
+ local needle="$1"; shift
33
+ local arg
34
+ for arg in "$@"; do
35
+ if [ "$arg" = "$needle" ]; then return 0; fi
36
+ done
37
+ return 1
38
+ }
39
+
40
+ # Walk argv: if any arg starts with `-` and isn't in the allowed set, die.
41
+ # Doesn't try to validate flag _values_ (e.g. `--json number,url`); that's
42
+ # real gh's job — we only block flags we don't expect to see.
43
+ strict_flags() {
44
+ local subcmd="$1"; shift
45
+ local allowed="$1"; shift
46
+ local re="^(${allowed})$"
47
+ local arg
48
+ for arg in "$@"; do
49
+ case "$arg" in
50
+ --) ;;
51
+ -*)
52
+ if ! [[ "$arg" =~ $re ]]; then
53
+ die "unsupported flag '$arg' for '$subcmd'. Allowed: ${allowed//|/, }"
54
+ fi
55
+ ;;
56
+ esac
57
+ done
58
+ }
59
+
60
+ # Returns the first true positional arg of "$@" (skipping flags AND their
61
+ # values), or '' if none. $1 is a `|`-separated list of value-taking flags
62
+ # for the current subcommand — e.g. "--json|--title|--body|--base" means the
63
+ # token after any of those flags is a value, not a positional. Without this
64
+ # we'd treat `--json number,url`'s field list as the positional and miss
65
+ # branch injection (real bug: PR-badge lookups returned "no PR for main"
66
+ # because the JSON field list looked positional).
67
+ first_positional() {
68
+ local value_taking="$1"; shift
69
+ local re="^(${value_taking})$"
70
+ local arg
71
+ local skip_value=0
72
+ for arg in "$@"; do
73
+ if [ "$skip_value" = "1" ]; then
74
+ skip_value=0
75
+ continue
76
+ fi
77
+ case "$arg" in
78
+ --) ;;
79
+ -*)
80
+ if [[ -n "$value_taking" && "$arg" =~ $re ]]; then
81
+ skip_value=1
82
+ fi
83
+ ;;
84
+ *) printf '%s\n' "$arg"; return 0 ;;
85
+ esac
86
+ done
87
+ }
88
+
89
+ handle_pr() {
90
+ local op="${1-}"; shift || true
91
+ if [ -z "$op" ]; then
92
+ die "missing subcommand for 'gh pr'. Supported: view, list, diff, checks, create, comment, review, merge, checkout, close, reopen"
93
+ fi
94
+ local branch
95
+ branch="$(box_branch)"
96
+
97
+ case "$op" in
98
+ view)
99
+ strict_flags "gh pr view" "--json" "$@"
100
+ if [ -z "$(first_positional "--json" "$@")" ] && [ -n "$branch" ]; then
101
+ set -- "$branch" "$@"
102
+ fi
103
+ exec "$CTL" gh pr view -- "$@"
104
+ ;;
105
+ list)
106
+ strict_flags "gh pr list" "--json|--state" "$@"
107
+ if ! needle_present "--head" "$@" && [ -n "$branch" ]; then
108
+ set -- "$@" "--head" "$branch"
109
+ fi
110
+ exec "$CTL" gh pr list -- "$@"
111
+ ;;
112
+ diff)
113
+ strict_flags "gh pr diff" "--color|--name-only|--patch" "$@"
114
+ if [ -z "$(first_positional "--color" "$@")" ] && [ -n "$branch" ]; then
115
+ set -- "$branch" "$@"
116
+ fi
117
+ exec "$CTL" gh pr diff -- "$@"
118
+ ;;
119
+ checks)
120
+ strict_flags "gh pr checks" "--json|--required" "$@"
121
+ if [ -z "$(first_positional "--json" "$@")" ] && [ -n "$branch" ]; then
122
+ set -- "$branch" "$@"
123
+ fi
124
+ exec "$CTL" gh pr checks -- "$@"
125
+ ;;
126
+ create)
127
+ strict_flags "gh pr create" "--fill|--draft|--title|--body|--base" "$@"
128
+ if ! needle_present "--head" "$@" && [ -n "$branch" ]; then
129
+ set -- "$@" "--head" "$branch"
130
+ fi
131
+ exec "$CTL" gh pr create -- "$@"
132
+ ;;
133
+ comment)
134
+ strict_flags "gh pr comment" "--body" "$@"
135
+ if [ -z "$(first_positional "--body" "$@")" ] && [ -n "$branch" ]; then
136
+ set -- "$branch" "$@"
137
+ fi
138
+ exec "$CTL" gh pr comment -- "$@"
139
+ ;;
140
+ review)
141
+ strict_flags "gh pr review" "--approve|--request-changes|--comment|--body" "$@"
142
+ if [ -z "$(first_positional "--body" "$@")" ] && [ -n "$branch" ]; then
143
+ set -- "$branch" "$@"
144
+ fi
145
+ exec "$CTL" gh pr review -- "$@"
146
+ ;;
147
+ merge)
148
+ strict_flags "gh pr merge" "--squash|--merge|--rebase|--delete-branch" "$@"
149
+ if [ -z "$(first_positional "" "$@")" ] && [ -n "$branch" ]; then
150
+ set -- "$branch" "$@"
151
+ fi
152
+ exec "$CTL" gh pr merge -- "$@"
153
+ ;;
154
+ close)
155
+ strict_flags "gh pr close" "--delete-branch" "$@"
156
+ if [ -z "$(first_positional "" "$@")" ] && [ -n "$branch" ]; then
157
+ set -- "$branch" "$@"
158
+ fi
159
+ exec "$CTL" gh pr close -- "$@"
160
+ ;;
161
+ reopen)
162
+ strict_flags "gh pr reopen" "" "$@"
163
+ if [ -z "$(first_positional "" "$@")" ] && [ -n "$branch" ]; then
164
+ set -- "$branch" "$@"
165
+ fi
166
+ exec "$CTL" gh pr reopen -- "$@"
167
+ ;;
168
+ checkout)
169
+ # gh pr checkout takes a required ref (number / URL / branch). No
170
+ # auto-inject — the relay also refuses checkout by default
171
+ # (AGENTBOX_GH_PR_CHECKOUT=allow opt-in).
172
+ strict_flags "gh pr checkout" "" "$@"
173
+ if [ -z "$(first_positional "" "$@")" ]; then
174
+ die "'gh pr checkout' requires a positional ref (PR number, URL, or branch)"
175
+ fi
176
+ exec "$CTL" gh pr checkout -- "$@"
177
+ ;;
178
+ *)
179
+ die "'gh pr $op' is not proxied (supported: view, list, diff, checks, create, comment, review, merge, checkout, close, reopen)"
180
+ ;;
181
+ esac
182
+ }
183
+
184
+ handle_run() {
185
+ local op="${1-}"; shift || true
186
+ if [ -z "$op" ]; then
187
+ die "missing subcommand for 'gh run'. Supported: list, view, rerun"
188
+ fi
189
+ case "$op" in
190
+ list)
191
+ strict_flags "gh run list" "--json|--limit|-L|--workflow|-w|--branch|-b|--status|--user" "$@"
192
+ exec "$CTL" gh run list -- "$@"
193
+ ;;
194
+ view)
195
+ strict_flags "gh run view" "--json|--log|--log-failed|--job" "$@"
196
+ # gh run view needs a run-id OR a --job <id>; refuse when neither is
197
+ # present (host-side gh would otherwise try an interactive picker and
198
+ # fail without a TTY).
199
+ if [ -z "$(first_positional "--json|--job" "$@")" ] && ! needle_present "--job" "$@"; then
200
+ die "'gh run view' requires a positional <run-id> (or --job <job-id>)"
201
+ fi
202
+ exec "$CTL" gh run view -- "$@"
203
+ ;;
204
+ rerun)
205
+ strict_flags "gh run rerun" "--failed|--job" "$@"
206
+ if [ -z "$(first_positional "--job" "$@")" ] && ! needle_present "--job" "$@"; then
207
+ die "'gh run rerun' requires a positional <run-id> (or --job <job-id>)"
208
+ fi
209
+ exec "$CTL" gh run rerun -- "$@"
210
+ ;;
211
+ watch)
212
+ die "'gh run watch' is not proxied (it blocks until CI finishes). Poll status with 'gh run view <run-id>' instead."
213
+ ;;
214
+ *)
215
+ die "'gh run $op' is not proxied (supported: list, view, rerun)"
216
+ ;;
217
+ esac
218
+ }
219
+
220
+ handle_api() {
221
+ local endpoint="${1-}"
222
+ if [ -z "$endpoint" ] || [ "${endpoint:0:1}" = "-" ]; then
223
+ die "'gh api' requires a positional <endpoint> (e.g. repos/:owner/:repo/pulls/:number/comments)"
224
+ fi
225
+ shift
226
+ # GET reads are proxied for any allowlisted endpoint; POST is proxied only to
227
+ # the PR review-comment endpoints (the relay enforces the endpoint + method
228
+ # policy). `--input` (stdin/file body) can't cross the relay — the host `gh`
229
+ # runs with stdin ignored — so reject it locally and point at -f/-F fields.
230
+ local arg
231
+ for arg in "$@"; do
232
+ case "$arg" in
233
+ --input|--input=*)
234
+ die "'gh api --input' (stdin/file body) isn't supported through the relay; use -f/-F fields"
235
+ ;;
236
+ esac
237
+ done
238
+ strict_flags "gh api" \
239
+ "--method|-X|-f|-F|--field|--raw-field|--jq|-q|--paginate|--cache|--hostname|-H|--header" "$@"
240
+ exec "$CTL" gh api "$endpoint" -- "$@"
241
+ }
242
+
243
+ handle_repo() {
244
+ local op="${1-}"; shift || true
245
+ if [ "$op" != "clone" ]; then
246
+ die "'gh repo $op' is not proxied (supported: clone)"
247
+ fi
248
+ # gh repo clone <repo> [<dir>] [--branch <n>] [--depth <n>]
249
+ strict_flags "gh repo clone" "--branch|--depth" "$@"
250
+ # Split argv into (repo, dir, flags). Pass positionals BEFORE options to the
251
+ # ctl so commander parses --branch/--depth as real options; using a `--`
252
+ # separator would force commander to treat them as extra positionals (the
253
+ # ctl `gh repo clone` command doesn't allowExcessArguments).
254
+ local repo='' dir=''
255
+ local -a flags=()
256
+ local pos=0
257
+ local skip_value=0
258
+ local arg
259
+ for arg in "$@"; do
260
+ if [ "$skip_value" = "1" ]; then
261
+ flags+=("$arg")
262
+ skip_value=0
263
+ continue
264
+ fi
265
+ case "$arg" in
266
+ --branch|--depth)
267
+ flags+=("$arg")
268
+ skip_value=1
269
+ ;;
270
+ -*)
271
+ flags+=("$arg")
272
+ ;;
273
+ *)
274
+ if [ $pos -eq 0 ]; then repo="$arg"
275
+ elif [ $pos -eq 1 ]; then dir="$arg"
276
+ else die "too many positionals for 'gh repo clone' (got '$arg' after repo + dir)"
277
+ fi
278
+ pos=$((pos+1))
279
+ ;;
280
+ esac
281
+ done
282
+ if [ -z "$repo" ]; then
283
+ die "'gh repo clone' requires a positional <repo> (owner/name or full URL)"
284
+ fi
285
+ if [ -n "$dir" ]; then
286
+ exec "$CTL" gh repo clone "$repo" "$dir" ${flags[@]+"${flags[@]}"}
287
+ else
288
+ exec "$CTL" gh repo clone "$repo" ${flags[@]+"${flags[@]}"}
289
+ fi
290
+ }
291
+
292
+ # Top-level dispatch.
293
+ if [ $# -eq 0 ]; then
294
+ die "no subcommand. Supported: pr {view,list,diff,checks,create,comment,review,merge,checkout,close,reopen}, run {list,view,rerun}, api <endpoint>, repo clone, auth status, --version"
295
+ fi
296
+
297
+ case "$1" in
298
+ --version|-v)
299
+ # Real gh prints something like:
300
+ # gh version 2.40.0 (2023-10-26)
301
+ # https://github.com/cli/cli/releases/tag/v2.40.0
302
+ # Tools that sniff "gh version" succeed with our shim line too.
303
+ printf 'gh version 2.0.0 (agentbox-shim)\n'
304
+ printf 'https://github.com/cli/cli\n'
305
+ ;;
306
+ --help|-h)
307
+ printf 'agentbox gh shim — strict subset.\n' >&2
308
+ printf 'Supported: pr {view,list,diff,checks,create,comment,review,merge,checkout,close,reopen}, run {list,view,rerun}, api <endpoint>, repo clone, auth status, --version\n' >&2
309
+ printf 'Anything else is rejected. Run host `gh --help` for full upstream docs.\n' >&2
310
+ ;;
311
+ auth)
312
+ shift
313
+ case "${1-}" in
314
+ status)
315
+ # Real auth state is verified host-side on the next real RPC (relay's
316
+ # assertGhReady returns exit 4 if the host is logged out). We don't
317
+ # round-trip on every refresh-driven poll Claude Code does.
318
+ printf 'agentbox gh shim: logged in to github.com (via agentbox host relay)\n' >&2
319
+ ;;
320
+ *)
321
+ die "'gh auth ${1-}' is not proxied (supported: status)"
322
+ ;;
323
+ esac
324
+ ;;
325
+ pr)
326
+ shift
327
+ handle_pr "$@"
328
+ ;;
329
+ run)
330
+ shift
331
+ handle_run "$@"
332
+ ;;
333
+ api)
334
+ shift
335
+ handle_api "$@"
336
+ ;;
337
+ repo)
338
+ shift
339
+ handle_repo "$@"
340
+ ;;
341
+ *)
342
+ die "'gh $1' is not proxied (supported: pr {…}, run {…}, api <endpoint>, repo clone, auth status, --version)"
343
+ ;;
344
+ esac
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env bash
2
+ # agentbox `git` shim — intercepts the four network ops (`push`, `pull`,
3
+ # `fetch`, `clone`) and routes them through `agentbox-ctl git ...` so the
4
+ # host runs the credential-touching part. Everything else (commit, status,
5
+ # log, diff, add, checkout, branch, ...) falls through to the real
6
+ # `/usr/bin/git` with zero overhead and zero shim output.
7
+ #
8
+ # Strict per-op flag whitelist — better safe than compatible. Adding a flag
9
+ # is a deliberate decision, not a default.
10
+
11
+ set -euo pipefail
12
+
13
+ # Paths are constants in production; env overrides exist purely to let unit
14
+ # tests substitute a stub `agentbox-ctl` on PATH without rewriting the shim.
15
+ # NEVER `exec git` — would loop on this shim. Always exec the resolved $REAL_GIT.
16
+ CTL="${AGENTBOX_CTL_PATH:-/usr/local/bin/agentbox-ctl}"
17
+ REAL_GIT="${AGENTBOX_REAL_GIT_PATH:-/usr/bin/git}"
18
+
19
+ die() {
20
+ printf 'agentbox git shim: %s\n' "$*" >&2
21
+ exit 2
22
+ }
23
+
24
+ # Walk argv: any arg matching `-*` must be in $allowed (regex alternation).
25
+ # Doesn't validate flag _values_ (e.g. `--branch main`); that's git's job.
26
+ strict_flags() {
27
+ local subcmd="$1"; shift
28
+ local allowed="$1"; shift
29
+ local re="^(${allowed})$"
30
+ local arg
31
+ for arg in "$@"; do
32
+ case "$arg" in
33
+ --) ;;
34
+ -*)
35
+ if ! [[ "$arg" =~ $re ]]; then
36
+ die "unsupported flag '$arg' for '$subcmd'. Allowed: ${allowed//|/, }"
37
+ fi
38
+ ;;
39
+ esac
40
+ done
41
+ }
42
+
43
+ # Network ops (push/pull/fetch): refuse positional remote/branch. The ctl
44
+ # rebuilds them from the registered worktree; re-passing them as positionals
45
+ # makes git treat them as refspecs and fail with
46
+ # `refs/remotes/origin/HEAD cannot be resolved to branch` (documented at
47
+ # packages/ctl/src/commands/git.ts:131).
48
+ refuse_positionals() {
49
+ local subcmd="$1"; shift
50
+ local arg
51
+ for arg in "$@"; do
52
+ case "$arg" in
53
+ --) ;;
54
+ -*) ;;
55
+ *)
56
+ die "positional '$arg' not allowed for '$subcmd' (the box's remote and branch are taken from the registered worktree)"
57
+ ;;
58
+ esac
59
+ done
60
+ }
61
+
62
+ if [ $# -eq 0 ]; then
63
+ exec "$REAL_GIT"
64
+ fi
65
+
66
+ case "$1" in
67
+ push)
68
+ shift
69
+ strict_flags "git push" "--force-with-lease" "$@"
70
+ refuse_positionals "git push" "$@"
71
+ exec "$CTL" git push -- "$@"
72
+ ;;
73
+ pull)
74
+ shift
75
+ strict_flags "git pull" "--ff-only" "$@"
76
+ refuse_positionals "git pull" "$@"
77
+ exec "$CTL" git pull -- "$@"
78
+ ;;
79
+ fetch)
80
+ shift
81
+ strict_flags "git fetch" "--prune" "$@"
82
+ refuse_positionals "git fetch" "$@"
83
+ exec "$CTL" git fetch -- "$@"
84
+ ;;
85
+ clone)
86
+ shift
87
+ strict_flags "git clone" "--branch|--depth" "$@"
88
+ # Walk argv: split into (url, dir, flags) so we can hand them to ctl
89
+ # in commander's expected shape (positionals first, then options).
90
+ url=''
91
+ dir=''
92
+ flags=()
93
+ pos=0
94
+ skip_value=0
95
+ for arg in "$@"; do
96
+ if [ "$skip_value" = "1" ]; then
97
+ flags+=("$arg")
98
+ skip_value=0
99
+ continue
100
+ fi
101
+ case "$arg" in
102
+ --branch|--depth)
103
+ flags+=("$arg")
104
+ skip_value=1
105
+ ;;
106
+ -*)
107
+ flags+=("$arg")
108
+ ;;
109
+ *)
110
+ if [ $pos -eq 0 ]; then url="$arg"
111
+ elif [ $pos -eq 1 ]; then dir="$arg"
112
+ else die "too many positionals for 'git clone' (got '$arg' after url + dir)"
113
+ fi
114
+ pos=$((pos+1))
115
+ ;;
116
+ esac
117
+ done
118
+ if [ -z "$url" ]; then
119
+ die "'git clone' requires a positional <url> (github URL or owner/name shorthand)"
120
+ fi
121
+ # `set -u` + empty array: use the ${arr[@]+"${arr[@]}"} guard.
122
+ if [ -n "$dir" ]; then
123
+ exec "$CTL" git clone "$url" "$dir" ${flags[@]+"${flags[@]}"}
124
+ else
125
+ exec "$CTL" git clone "$url" ${flags[@]+"${flags[@]}"}
126
+ fi
127
+ ;;
128
+ *)
129
+ exec "$REAL_GIT" "$@"
130
+ ;;
131
+ esac