Skip to content

router: one MCP server over local and remote box hosts, with compact output - #55

Merged
chxmxii merged 1 commit into
chxperiments:mainfrom
KoLDXr00T:feat/router
Oct 23, 2026
Merged

chxmxii merged 1 commit into
chxperiments:mainfrom
KoLDXr00T:feat/router

Conversation

@KoLDXr00T

@KoLDXr00T KoLDXr00T commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

box mcp can now front other machines running box, and its tools use fewer of the agent's tokens.

Remote hosts

List them in ~/.box/hosts.yaml and box mcp routes to them alongside the local box. A sandbox there is host/name. Without the file, nothing changes.

hosts:
  - name: gpu1
    ssh: me@gpu1.example.com
    max_up: 8          # optional placement cap
  • Transport: ssh only, no new network listener. The router runs box __ensure-serve on the host. That starts the host's own box serve (idle limit 30m) and prints its socket. One ssh -L per host then forwards that owner-only socket into a 0700 directory here.
  • Names. A bare name means the local sandbox, else the only host that has it. When several remote hosts have it, up and run are placed: where it is already up or warm, then the least loaded host under max_up. diff, fork, apply and discard must name the host, since each copy has its own state.
  • Failures. An unreachable host is reported (/v1/hosts, box hosts) rather than failing the list, and is retried after a pause. After a quiet spell a host is pinged before use, because its box serve exits when idle or upgraded.
  • Hardening. The host list is validated before ssh sees it: no destination that ssh could read as an option (-oProxyCommand=…), and no shell metacharacters in the remote binary path. ssh runs in BatchMode, and host-key checking is left to the user's ssh config. SECURITY.md has a new section on this.

Fewer tokens

  • Command output has ANSI escapes and \r progress redraws removed and repeated lines collapsed. It is then cut to its first and last lines: 8 KiB per stream by default (--output-budget), replacing the 64 KiB head-only clip. The cut is marked with an id, and the new output tool returns any slice by line range or regex. Full outputs are kept in memory only, capped at 64 MiB per session.
  • "unchanged" reads. read_file, diff and list_sandboxes answer unchanged when the fresh result is identical to what the agent already has, including a file it just wrote. Every read still reaches the sandbox, so the answer is never stale, and full: true shows it again.
  • The sandbox list is one line per sandbox instead of indented JSON.

Code

  • internal/router: the router is an http.Handler speaking the same /v1 API, so mcp.New takes it in place of server.NewHandler. hosts.go has the config and the ssh transport.
  • internal/mcp: compact.go (cleaning, clipping, the spool, the output tool) and seen.go. mcp.New now takes Options.
  • internal/cli: new box hosts, the hidden box __ensure-serve, and new box mcp flags --hosts, --local-only, --output-budget.

Checked

  • CI on the fork (check, vulncheck, three fuzz targets): https://github.com/KoLDXr00T/bluebox/actions/runs/39481149641
  • Router and transport tests use fake hosts and a fake ssh (the test binary forwarding the socket in Go), under -race. They cover name resolution, placement and max_up, forks staying on their host, a down host, a server that exits and comes back, and config validation.
  • End to end with the real binary, through a stand-in ssh and a second box home: box hosts, remote box serve autostart and reuse, qualified listing, bare-name resolution to the remote host, an "unchanged" re-list, and remote error passthrough.
  • Escape suite, firecracker backend, strict and standard: no escapes, run on a GitHub-hosted runner, because no self-hosted kvm runner is registered upstream (A self-hosted KVM runner for the nightly escape suite #31): https://github.com/KoLDXr00T/bluebox/actions/runs/39482571791. podman/krun not covered (stock Ubuntu crun lacks libkrun).

Not checked: real exec output through the new compaction (unit tests only; no KVM where this was written) and a real ssh login (stand-in only). Trying ssh: localhost on a host with sshd and KVM would close both.

Not in this PR

  • Creating or building sandboxes on a remote host. Placement only picks among existing copies.
  • SDK access to the router.
  • Found while running the escape suite, to be fixed separately: RuntimeIdentity requires krun on PATH whatever the backend, so box up fails on a firecracker-only host. The isolation-proof cache is also shared across backends, so one backend's proof lets another skip its check. The hosted run above worked around the first with a krun -> crun symlink.

…output

box mcp fronts the hosts in ~/.box/hosts.yaml besides the local box. A host
is reached only over ssh: box __ensure-serve starts its own box serve, and
one ssh forward per host brings that owner-only socket here, into a 0700
directory. No network listener is opened. A sandbox there is host/name; a
bare name is the local one, else the only host with it. Where several
remote hosts have it, up and run are placed (where it is up or warm, then
the least loaded under max_up); anything acting on one copy's state must
name the host. An unreachable host is reported, not fatal, and retried
after a pause; after a quiet spell a host is checked before use, since its
box serve exits when idle or upgraded. box hosts shows them.

The MCP tools spend fewer of the agent's tokens:
- command output loses escapes and progress redraws, collapses repeated
  lines, and is cut to its first and last lines (8 KiB a stream by
  default, --output-budget); the rest is kept in memory for a new output
  tool, by line range or regex
- read_file, diff and list_sandboxes answer "unchanged" when the fresh
  result is identical to what the agent already holds; every read still
  reaches the sandbox, so it is never stale, and full: true shows it again
- the sandbox list is one line per sandbox instead of indented JSON

Host lists are validated before ssh sees them: no destination ssh could
read as an option, no shell metacharacters in the remote binary path.
@chxmxii
chxmxii merged commit e89ba2e into chxperiments:main Oct 23, 2026
5 checks passed
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.

2 participants