feat(cli): host-mediated agentbox git <op> <box> + --from-branch#8
Conversation
The reverse direction (box → host shims for `gh` / `git`) was already wired in #6 / #7. This adds host → box: run git / PR operations against a specific box from the host without attaching. New commands: agentbox git push <box> push the box's branch agentbox git fetch <box> fetch refs into the box's worktree agentbox git pull <box> [<branch>] fetch+merge; with <branch>, checkout first (reuse a box for a new task) agentbox git checkout <box> <br> change the box's working branch agentbox git status <box> read-only status agentbox git pr [create|view|list|comment|review|merge|checkout|close|reopen] <box> PR operations; defaults to `create`, which auto-injects `--head <box-branch>` Plus `--from-branch <ref>` on `create` / `claude` / `codex` / `opencode`: base the box's `agentbox/<name>` branch on `<ref>` instead of HEAD. Host fetches + validates the ref before any provider work, so a bad ref fails fast. Security (untrusted box must not bypass host prompts): - The host CLI mints a one-time token via a loopback-only admin endpoint (`POST /admin/host-initiated/mint`) scoped to (boxId, method, paramsHash). A box cannot reach loopback, so it cannot mint tokens. - The token is delivered to `agentbox-ctl` via a hidden CLI arg (not env, which would propagate to git hooks). - The relay re-hashes incoming params and rejects on mismatch — a token harvested from /proc/<pid>/cmdline can't be replayed with mutated args (no `--force` on push, no tampered `--title`/`--body` on PR). - `params.hostInitiated` present but invalid → hard reject (exit 10), no prompt. Falling through to the prompt would leak that the attacker holds a token and bait the user into approving a hijacked call. - `params.hostInitiated` absent → falls through to the existing `askPrompt` (normal agent-initiated path; unchanged behavior). - `gh pr merge` / `checkout` env opt-ins (AGENTBOX_GH_FORCE, AGENTBOX_GH_PR_CHECKOUT) still apply — host-initiated does not weaken destructive-op guards.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9767f66. Configure here.
| r.branch, | ||
| wt, | ||
| 'HEAD', | ||
| baseRef, |
There was a problem hiding this comment.
Docker path applies fromBranch to nested repos too
Medium Severity
baseRef = opts.fromBranch ?? 'HEAD' is applied to every repo in the loop — both root and nested. The cloud path in workspace-seed.ts correctly passes fromBranch only to the root repo's seedFromGitClone call, omitting it for nested repos. The PR description confirms: "Nested repos keep their own default branch — fromBranch is applied to the root only." A nested repo that doesn't have the specified ref will fail the git worktree add and throw a GitWorktreeError, aborting box creation.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 9767f66. Configure here.
`baseRef` was applied uniformly to every entry in `opts.repos`, but `--from-branch <ref>` is a host-level option resolved against the user's primary repo. A monorepo with nested sub-repos almost never has the same ref in each repo, so `git worktree add ... <ref>` for the nested entries would fail with `unknown revision`, aborting box creation. Mirror the cloud path (workspace-seed.ts already only forwards `fromBranch` to the root `seedFromGitClone` call): use `opts.fromBranch` when `r.repo.kind === 'root'`, fall back to `HEAD` for nested repos.
…tems The 9 actionable items are done. Write concrete implementation plans (from this session's investigation) into the backlog for the heavier/interactive remainder so they can be picked up on the host: - #4 relay round-trip: why nested doesn't work + the host runbook (E2E_RELAY=1) - #8 ttyd attach: bake ttyd + 4th port + ws client rewrite (needs a re-bake) - #14 per-project snapshot tier: prepared-state projects[<hash>] mirroring daytona/hetzner - #16 Sandbox.fork: SDK is ready; needs a finalizeCloudBox refactor of the shared create() + an `agentbox fork` command + a branch-semantics decision


Summary
agentbox git <op> <box>group on the host:push,fetch,pull [<branch>],checkout <branch>,status,pr {create|view|list|comment|review|merge|checkout|close|reopen}. Each subcommand resolves the box, attaches the provider, and runs the matchingagentbox-ctl git/agentbox-ctl gh prinside/workspace.git prdefaults tocreateand auto-injects--head <box-branch>so the PR is opened for the box's branch, not whatever the host happens to have checked out.--from-branch <ref>oncreate/claude/codex/opencode: base the box'sagentbox/<name>branch on<ref>(branch / tag / SHA) instead ofHEAD. Host fetches non-SHA refs from origin, validates viagit rev-parse, fails fast before any provider work.ghandgitshims (Claude Code PR badge) #7; this completes the loop.Security
agentbox git <op> <box>ultimately triggers credentialed RPCs (git.push,gh.pr.<op>, …) on the host-side relay. Those RPCs normally surface a y/N prompt because the box is untrusted. For host-initiated calls re-prompting is just friction, but "host-initiated" must not be something a malicious agent inside the box can claim:POST /admin/host-initiated/mint), scoped to(boxId, method, paramsHash). A box cannot reach loopback → cannot mint tokens.--host-initiated-token <tok>toagentbox-ctl(not env — envs propagate to git hooks).hostInitiated) and rejects on mismatch — a token harvested from/proc/<pid>/cmdlinecan't be replayed with mutated args (--forceon push, fake--title/--bodyon PR).params.hostInitiatedpresent but invalid → hard reject (exit 10), no prompt. Falling through would leak that an attacker holds a token and bait the user into approving a hijacked call.params.hostInitiatedabsent → falls through to existingaskPrompt(normal agent-initiated path, unchanged).gh pr merge/checkoutenv opt-ins (AGENTBOX_GH_FORCE,AGENTBOX_GH_PR_CHECKOUT) still run before the host-initiated check — destructive guards do not weaken.Files of note
packages/relay/src/host-initiated.ts(new) — token store,hashRpcParamscanonical JSON hasher,consume()with(boxId, method, paramsHash)validation.packages/relay/src/server.ts—POST /admin/host-initiated/mintadmin route; reject-on-mismatch wired intogit.pushandhandleGhPrRpc.packages/relay/src/host-actions.ts— same wiring for the cloud (daytona / hetzner) execution path.packages/ctl/src/commands/{git,pr-subcommands}.ts— hidden--host-initiated-token <token>per leaf, forwarded asparams.hostInitiated.apps/cli/src/commands/git.ts(new) — host CLI group; mints a paramsHash-bound token before each credentialed call, auto-injects--headonpr create.apps/cli/src/lib/from-branch.ts(new) — host-side--from-branchfetch +git rev-parse --verifyvalidation.packages/core/src/provider.ts,sandbox-docker/src/{create,in-box-git,docker-provider}.ts,sandbox-cloud/src/{cloud-provider,workspace-seed}.ts) — threadfromBranchthroughCreateBoxRequestintogit worktree add -b <branch> <wt> <ref>(docker) andgit clone --branch <ref>(cloud).Test plan
pnpm build,pnpm -w typecheck,pnpm -w lint,pnpm -w test— all greenagentbox git status / push / push --dry-run / pull / checkout / pr create / pr viewall workagentbox claude --from-branch some-feature -n smoke— box'sgit log -1shows the requested ref as the fork pointagentbox create --from-branch this-does-not-exist— fails fast with a clear error, no half-built boxagentbox-ctl gh pr create --host-initiated-token <random>) → rejected, exit 10paramsHash(P1), in-box call with different params → rejected, exit 10Note
High Risk
Changes authentication/authorization for git push and
gh pron the relay plus new host-mediated credentialed paths; mistakes could weaken prompts or allow token replay.Overview
Adds
agentbox giton the host so you can run git andgh pragainst a box (push,fetch,pull,checkout,status, and a fullprgroup). Commands resolve the box, execagentbox-ctlin/workspace, and for credentialed relay RPCs mint a one-time, params-hash-bound token (loopback/admin/host-initiated/mint) so the relay can skip the confirm prompt without letting the box forge “host-initiated.” Invalid tokens fail hard (exit 10); missing tokens keep the existing prompt path.pr createdefaults tocreateand auto-injects--headfrom the box’s root worktree branch.Adds
--from-branch <ref>oncreateand agent entrypoints (claude,codex,opencode, etc.): host fetch +rev-parsebefore any provider work, then threadsfromBranchintoCreateBoxRequestso the per-box branch forks from that ref (git worktree add … <ref>on Docker,git clone --branchon cloud seed).Wires
hostInitiatedthroughagentbox-ctl, relay types,server, and cloudhost-actions; exportsmintHostInitiatedTokenfrom sandbox-docker for the CLI.Reviewed by Cursor Bugbot for commit 9767f66. Configure here.