Skip to content

feat(cli): host-mediated agentbox git <op> <box> + --from-branch#8

Merged
madarco merged 3 commits into
mainfrom
feat/agentbox-git
May 27, 2026
Merged

feat(cli): host-mediated agentbox git <op> <box> + --from-branch#8
madarco merged 3 commits into
mainfrom
feat/agentbox-git

Conversation

@madarco
Copy link
Copy Markdown
Owner

@madarco madarco commented May 27, 2026

Summary

  • New 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 matching agentbox-ctl git / agentbox-ctl gh pr inside /workspace. git pr defaults to create and auto-injects --head <box-branch> so the PR is opened for the box's branch, not whatever the host happens to have checked out.
  • New --from-branch <ref> on create / claude / codex / opencode: base the box's agentbox/<name> branch on <ref> (branch / tag / SHA) instead of HEAD. Host fetches non-SHA refs from origin, validates via git rev-parse, fails fast before any provider work.
  • The reverse direction (box → host shims) was already wired in feat(sandbox-cloud): cap cloud-seed history via shallow git clone + tar #6 / feat(box): host-mediated gh and git shims (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:

  • 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 → cannot mint tokens.
  • Token is delivered via a hidden CLI arg --host-initiated-token <tok> to agentbox-ctl (not env — envs propagate to git hooks).
  • The relay re-hashes incoming params (excluding hostInitiated) and rejects on mismatch — a token harvested from /proc/<pid>/cmdline can't be replayed with mutated args (--force on push, fake --title/--body on PR).
  • params.hostInitiated present 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.hostInitiated absent → falls through to existing askPrompt (normal agent-initiated path, unchanged).
  • gh pr merge / checkout env 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, hashRpcParams canonical JSON hasher, consume() with (boxId, method, paramsHash) validation.
  • packages/relay/src/server.tsPOST /admin/host-initiated/mint admin route; reject-on-mismatch wired into git.push and handleGhPrRpc.
  • 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 as params.hostInitiated.
  • apps/cli/src/commands/git.ts (new) — host CLI group; mints a paramsHash-bound token before each credentialed call, auto-injects --head on pr create.
  • apps/cli/src/lib/from-branch.ts (new) — host-side --from-branch fetch + git rev-parse --verify validation.
  • Provider plumbing (packages/core/src/provider.ts, sandbox-docker/src/{create,in-box-git,docker-provider}.ts, sandbox-cloud/src/{cloud-provider,workspace-seed}.ts) — thread fromBranch through CreateBoxRequest into git worktree add -b <branch> <wt> <ref> (docker) and git clone --branch <ref> (cloud).

Test plan

  • pnpm build, pnpm -w typecheck, pnpm -w lint, pnpm -w test — all green
  • Fresh docker box: agentbox git status / push / push --dry-run / pull / checkout / pr create / pr view all work
  • agentbox claude --from-branch some-feature -n smoke — box's git log -1 shows the requested ref as the fork point
  • agentbox create --from-branch this-does-not-exist — fails fast with a clear error, no half-built box
  • Security live-fire against a fresh box:
    • Legitimate host CLI push / pr → succeeds, no prompt
    • Forged token from inside the box (agentbox-ctl gh pr create --host-initiated-token <random>) → rejected, exit 10
    • Mint token bound to paramsHash(P1), in-box call with different params → rejected, exit 10
    • Same token + matching params → accepted (prompt skipped, token consumed once)
  • Cloud (daytona / hetzner) end-to-end — code paths mirrored but not smoked in this PR

Note

High Risk
Changes authentication/authorization for git push and gh pr on the relay plus new host-mediated credentialed paths; mistakes could weaken prompts or allow token replay.

Overview
Adds agentbox git on the host so you can run git and gh pr against a box (push, fetch, pull, checkout, status, and a full pr group). Commands resolve the box, exec agentbox-ctl in /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 create defaults to create and auto-injects --head from the box’s root worktree branch.

Adds --from-branch <ref> on create and agent entrypoints (claude, codex, opencode, etc.): host fetch + rev-parse before any provider work, then threads fromBranch into CreateBoxRequest so the per-box branch forks from that ref (git worktree add … <ref> on Docker, git clone --branch on cloud seed).

Wires hostInitiated through agentbox-ctl, relay types, server, and cloud host-actions; exports mintHostInitiatedToken from sandbox-docker for the CLI.

Reviewed by Cursor Bugbot for commit 9767f66. Configure here.

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.
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9767f66. Configure here.

madarco added 2 commits May 27, 2026 20:48
`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.
@madarco madarco merged commit 6af4824 into main May 27, 2026
1 check passed
madarco added a commit that referenced this pull request May 29, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant