Files
hack-house/docs/spec-goose-harness.md
T
leetcrypt ad13ec14ba docs(sandbox): specs for goose harness + GUI distros, command ref, themes
Add the goose-harness and sandbox-distros-GUI design specs, a consolidated
command reference and demo-reels plan, the AI-harness planning note, and
two new client themes (blue-orange, pink-red-gray).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 10:38:36 -07:00

8.2 KiB

hack-house → Podman backend + Goose harness — Spec

Status: Draft v1 · Date: 2026-06-07 Scope: Add Podman as an additional sandbox backend (Docker stays), and make Goose (block/goose) the default agentic harness for the /ai !task path — strictly contained to spawned /sbx sessions, output rendered to chat. Baseline reviewed: cmd_chat/agent/bridge.py, hh/src/sbx.rs, hh/src/app.rs, hh/src/net.rs @ main. Builds on: spec-collaborative-sandbox.md, spec-agent-bridge.md. See also: spec-sandbox-distros-gui.md (Podman default = Kali, Docker default = Parrot, the /sbx <type> <option> grammar, and the container GUI). Grammar note: examples below use /sbx <backend> (e.g. /sbx podman); the older /sbx launch <backend> is kept as a transitional alias.


0. Decisions locked (from product owner)

# Decision Choice
A Podman vs Docker Both. Podman is an additional Backend, not a replacement. /sbx podman and /sbx docker both work.
B Why Podman Daemonless + rootless ⇒ no root daemon, no sudo modal, stronger userns isolation for untrusted shared code.
C Default harness Goose for the !task (sandbox-action) path. Opt-out to the legacy one-shot injector (simple).
D Goose containment Goose is never a host-side process. It runs only inside a spawned /sbx session. Container/VM backends fully isolate it; the only host path is /sbx local (host shell) — the explicit, warned exception.
E Capability gate /grant is the switch. Granted → full Goose shell inside the opened container. Not granted → advisory, chat-only, never touches the sandbox.
F Output Goose output is rendered to chat, not the terminal pane.
G Tools Goose may use confined tool calls (web search/fetch, approved scripts) — all inside the sandbox namespace. No host fs (no bind mounts).

1. Podman backend

1.1 Model

Backend gains a Podman variant alongside {Local, Docker, Multipass}. Docker and Podman share the same OCI CLI grammar (run/exec/commit/save/rm/images), so the two collapse onto one container code path parameterised by an engine binary (engine_bin(backend) -> "docker" | "podman"). Only two things differ:

Concern Docker Podman
Daemon needs docker info up; may need sudo to start daemonless — skip the daemon check + sudo modal entirely
Host gateway (for in-container Ollama) add --add-host=host.docker.internal:host-gateway at run host.containers.internal is native (rootless pasta/slirp)

Everything else — provisioning per-member unix accounts, the sandbox-bootstrap toolchain install, commit/save snapshots, push (file bridge), exec shell — is identical and reuses the existing logic via the engine binary.

1.2 Snapshots

podman commithh-snap:<label> and podman save -o …tar mirror Docker 1:1 (rootless-friendly). SnapKind gains a Podman arm so /sbx load <label> restores through the engine that holds the image. CRIU checkpoint/restore is out of scope (rootless Podman generally can't); filesystem snapshots via commit remain the save/load mechanism.

1.3 Install

ensure-podman.sh (apt) + a rootless preflight note (subuid/subgid). No daemon to start. /sbx podman install opts into installing it.


2. Goose harness

2.1 Containment invariant

The bridge already only ever emits keystroke/chat frames — it never executes on the host. Goose preserves this: it runs as a command inside the spawned sandbox, located via the engine + container name the broker advertises. Goose therefore inherits exactly the backend's isolation:

Backend Goose runs in Host access
podman / docker container namespace (rootless userns ideal) none — no host fs (no -v), separate net ns
multipass guest VM none
local (/sbx local) host shell yes — the one explicit, warned exception

2.2 Two tiers, gated by /grant

/ai <agent> !<task>
   ├─ granted (drive ACL)  → run Goose IN the sandbox  → stream stdout → chat
   └─ not granted          → advisory answer only       → chat (no sandbox touch)
  • Granted: the bridge runs, per backend:
    • podman|docker exec -i <container> goose run -t "<task>" --no-session -q --max-turns N
    • multipass exec <name> -- goose run …
    • localgoose run … on the host with a loud warning Goose's full agentic loop (read output → edit files → run approved scripts → web search → self-correct) executes inside that sandbox. stdout/stderr is captured, throttled, capped, and posted to chat as the agent.
  • Not granted: the task is answered by the lightweight provider as plain chat — explicitly told it has no drive and must advise, not assume execution.

2.3 Harness selection & opt-out

  • Default harness = goose. /ai start … plain (or --harness simple) selects the legacy one-shot injector. models.toml may carry harness = "goose"|"simple".
  • Graceful degrade: if the harness is goose but the binary isn't runnable in the sandbox, the bridge says so once and falls back to the simple injector — Goose is default but never load-bearing. This is also the "remove Goose" path.

2.4 Tool surface (confined)

Configured in a container-side ~/.config/goose/config.yaml baked in by sandbox-bootstrap.sh (so the policy travels with the isolation):

  • GOOSE_PROVIDER=ollama, GOOSE_MODEL=…, OLLAMA_HOST → host gateway.
  • Enabled extensions: developer/shell (scoped to the container), a web fetch/search extension, and a curated approved-scripts dir (/opt/hh/bin).
  • No host bind mounts, ever. The only host-ward connection is Goose → host Ollama (a model API call, not shell reach).

3. Wire conventions

The broker advertises where the sandbox lives so the (co-located) agent can run Goose in it. The existing _sbx:status frame gains two fields:

{"_sbx":"status","state":"ready","backend":"podman","engine":"podman","name":"hh-sandbox","rows":40,"cols":120}
  • engine{docker, podman, multipass, local} — the exec family.
  • name — the container/instance handle (empty for local).

The bridge reads these from the frame (extending _handle_control) and uses them to build the goose run invocation. Grant state continues to ride the existing _perm:acl frame (drivers).


4. Trust model honesty

  • Containment is hard: Goose's reach == the sandbox namespace. Container/VM backends give real isolation; local is host by explicit user choice (warned).
  • Grant tier is app-layer for the Goose path (the co-located bridge runs the exec), the same trust class as the operator ACL in spec-agent-bridge.md §3. A well-behaved bridge honours self.granted; a hostile process with the room password is out of scope (it already has the password). The keystroke-injection path remains hard-gated by the broker.
  • Prompt injection: room text is untrusted; the sandbox is the blast radius; output is capped + throttled.

5. Bootstrap touch points

Script Change
bootstrap.sh add podman, goose to the optional prereq probe
bootstrap-ai.sh install Goose on the host (default on; --no-goose to skip); write host ~/.config/goose/config.yaml
sandbox-bootstrap.sh install the Goose binary into the container (it's a release binary, not apt — can't ride sandbox-tools.json); write the container-side goose config pointing OLLAMA_HOST at the host gateway
ensure-podman.sh new detect-then-install helper (apt)

6. Phasing

Phase Scope
1 Podman backend (launch/exec/provision/teardown/save/load parity); _sbx:status carries engine+name.
2 Goose harness in the bridge: grant-gated two tiers, container exec, stdout→chat, simple-injector fallback; --harness flag + /ai start … plain.
3 Bootstrap: host + in-container Goose install, ensure-podman, confined tool config.
4 Polish: structured Goose event rendering, approved-script allowlist UI, web-egress allowlist.