refactor(ai): strip Goose harness (Phase 1) — native/simple host-side only
CI / rust client (hh) (macos-latest) (push) Waiting to run
CI / rust client (hh) (ubuntu-latest) (push) Waiting to run
CI / rust coverage (push) Waiting to run
CI / python server (3.10) (push) Waiting to run
CI / python server (3.11) (push) Waiting to run
CI / python server (3.12) (push) Waiting to run
CI / headless e2e smoke (push) Waiting to run
CI / dependency audit (push) Waiting to run
CI / secret scanning (push) Waiting to run

Remove the Goose agentic harness across the codebase per
docs/spec-native-harness.md §3. Goose made N sequential model calls inside the
sandbox (slow on CPU-only hardware) and forced an in-container→host Ollama
gateway that tripped the rootless-Podman slirp4netns loopback bug.

- bridge.py: delete _run_goose/_goose_argv/_goose_present + GOOSE_* consts and
  the present-cache; __init__ now takes harness="simple"/max_turns=5; granted
  !task runs _run_simple until the native loop lands (Phase 2).
- __main__.py: --harness {native,simple} (was {goose,simple}); drop
  --goose-max-turns, add --max-turns; default harness simple.
- app.rs: /ai start accepts native|simple (plain aliases simple) instead of a
  bare plain flag; refresh harness comments.
- sbx.rs: remove the in-container Ollama gateway (Docker host-gateway / Podman
  slirp4netns host-loopback) and the dk_bootstrap OLLAMA_HOST env — kills the
  slirp4netns loopback bug; drop Goose comments.
- bootstrap.sh: drop goose from the prereq probe.
- bootstrap-ai.sh: remove the entire Goose install block, --no-goose flag,
  GOOSE_INSTALLER_URL, host config writer, and goose_bin helper.
- sandbox-bootstrap.sh: remove the in-sandbox Goose binary install + config.
- spec-goose-harness.md: banner — harness portion superseded; Podman stays.

cargo check + py_compile clean. No Goose refs remain (headroom/ untouched).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-06-08 13:03:41 -07:00
parent cf6b0b5b73
commit e49dbca451
8 changed files with 67 additions and 310 deletions
+5 -68
View File
@@ -8,18 +8,16 @@
#
# The baseline is untouched: ./bootstrap.sh alone never installs or enables AI.
#
# It also installs Goose (block/goose) by default — the agentic harness the /ai
# agent uses for the sandbox `!task` path — and writes a host Goose config that
# points it at the local Ollama. Skip it with --no-goose (the agent still works:
# the bridge falls back to its built-in one-shot injector).
# The /ai agent's sandbox `!task` harness runs host-side (native tool-calling /
# one-shot injector) and needs no extra binary, so this script installs nothing
# beyond Ollama + the model.
#
# usage:
# ./bootstrap-ai.sh # baseline setup + Ollama + default model + Goose
# ./bootstrap-ai.sh # baseline setup + Ollama + default model
# ./bootstrap-ai.sh --release # ...and build the client in release mode
# ./bootstrap-ai.sh --check # report only; install/pull nothing
# ./bootstrap-ai.sh --yes # don't prompt before installing Ollama / pulling the model
# ./bootstrap-ai.sh --ai-only # only the AI layer; skip the baseline ./bootstrap.sh
# ./bootstrap-ai.sh --no-goose # skip the Goose harness install (one-shot injector only)
# ./bootstrap-ai.sh -h | --help # this help
#
# environment:
@@ -31,22 +29,19 @@ ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
MODEL="${HH_AI_MODEL:-qwen2.5:3b}"
OLLAMA_HOST="${OLLAMA_HOST:-http://localhost:11434}"
INSTALLER_URL="https://ollama.com/install.sh"
GOOSE_INSTALLER_URL="https://github.com/block/goose/releases/download/stable/download_cli.sh"
RELEASE_ARGS=()
CHECK_ONLY=0
ASSUME_YES=0
AI_ONLY=0
DO_GOOSE=1
for arg in "$@"; do
case "$arg" in
--release) RELEASE_ARGS+=(--release) ;;
--check) CHECK_ONLY=1 ;;
--yes|-y) ASSUME_YES=1 ;;
--ai-only) AI_ONLY=1 ;;
--no-goose) DO_GOOSE=0 ;;
-h|--help|-help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "✖ unknown arg: $arg (try --release / --check / --yes / --ai-only / --no-goose / --help)" >&2; exit 2 ;;
*) echo "✖ unknown arg: $arg (try --release / --check / --yes / --ai-only / --help)" >&2; exit 2 ;;
esac
done
@@ -74,14 +69,6 @@ if have ollama; then echo " ✓ ollama ($(ollama --version 2>&1 | head -1))"
else echo " · ollama not installed"; fi
if ollama_up; then echo " ✓ ollama daemon reachable at $OLLAMA_HOST"
else echo " · ollama daemon not reachable at $OLLAMA_HOST"; fi
# Goose may install to ~/.local/bin, which isn't always on PATH in this shell.
goose_bin() { command -v goose 2>/dev/null || { [[ -x "$HOME/.local/bin/goose" ]] && echo "$HOME/.local/bin/goose"; }; }
if [[ $DO_GOOSE -eq 1 ]]; then
if [[ -n "$(goose_bin)" ]]; then echo " ✓ goose ($("$(goose_bin)" --version 2>&1 | head -1))"
else echo " · goose not installed (will install — the agent's default !task harness)"; fi
else
echo " · goose skipped (--no-goose; agent uses its one-shot injector)"
fi
if [[ $CHECK_ONLY -eq 1 ]]; then
echo "--check: no changes made"
@@ -143,56 +130,6 @@ else
echo " ✓ model '$MODEL' ready"
fi
# 5. Goose harness (default on; --no-goose skips). Goose is the agentic harness
# the /ai agent runs for the sandbox `!task` path. It ships as a release binary
# (not apt/pip), so we use its official installer with CONFIGURE=false to skip
# the interactive setup — we write a minimal host config ourselves instead.
# Failure here is non-fatal: the bridge falls back to its built-in one-shot
# injector, so the agent still works without Goose.
if [[ $DO_GOOSE -eq 1 ]]; then
echo
echo "── Goose harness ──"
if [[ -z "$(goose_bin)" ]]; then
if [[ "$(uname -s)" != "Linux" && "$(uname -s)" != "Darwin" ]]; then
echo " ✖ automatic Goose install supports Linux/macOS only — see https://block.github.io/goose/ ; the agent will use its one-shot injector." >&2
else
echo " Goose is not installed. The official installer will run:"
echo " curl -fsSL $GOOSE_INSTALLER_URL | CONFIGURE=false bash"
proceed=1
if [[ $ASSUME_YES -ne 1 && -t 0 ]]; then
read -r -p " install Goose now? [Y/n] " ans
[[ -z "$ans" || "$ans" == [yY]* ]] || proceed=0
fi
if [[ $proceed -eq 1 ]]; then
if curl -fsSL "$GOOSE_INSTALLER_URL" | CONFIGURE=false bash; then
echo " ✓ goose installed ($([ -n "$(goose_bin)" ] && "$(goose_bin)" --version 2>&1 | head -1))"
else
echo " ⚠ Goose install failed — the agent will fall back to its one-shot injector" >&2
fi
else
echo " · skipped Goose install — the agent will use its one-shot injector"
fi
fi
fi
# Write a minimal host Goose config pointing at the local Ollama. Goose reads
# ~/.config/goose/config.yaml; we only set it if absent so we never clobber a
# config the user has tuned themselves.
GOOSE_CFG="${XDG_CONFIG_HOME:-$HOME/.config}/goose/config.yaml"
if [[ -f "$GOOSE_CFG" ]]; then
echo " ✓ host Goose config already exists ($GOOSE_CFG) — leaving it untouched"
else
mkdir -p "$(dirname "$GOOSE_CFG")"
cat > "$GOOSE_CFG" <<EOF
# Written by hh bootstrap-ai.sh — local-first Goose defaults (host).
GOOSE_PROVIDER: ollama
GOOSE_MODEL: $MODEL
OLLAMA_HOST: $OLLAMA_HOST
EOF
echo " ✓ wrote host Goose config → $GOOSE_CFG (ollama/$MODEL)"
fi
echo " ⓘ in-sandbox Goose (containers/VMs) is installed separately by scripts/sandbox-bootstrap.sh"
fi
echo
echo "AI ready. start a local agent against a running room with:"
echo " .venv/bin/python -m cmd_chat.agent <host> <port> \\"
+1 -1
View File
@@ -49,7 +49,7 @@ for bin in python3 cargo; do
if have "$bin"; then echo "$bin ($($bin --version 2>&1 | head -1))"
else echo "$bin — REQUIRED"; missing=1; fi
done
for bin in tmux docker podman multipass direnv goose; do
for bin in tmux docker podman multipass direnv; do
if have "$bin"; then echo "$bin (optional)"
else echo " · $bin not found (optional)"; fi
done
+3 -30
View File
@@ -50,36 +50,9 @@ if ! apt-get install -y --no-install-recommends $PKGS; then
done
fi
# ---- Goose harness (the /ai agent's sandbox `!task` path) --------------------
# Goose ships as a release binary (not apt), so install it via its official
# installer into a system path so every container user can run it. Best-effort:
# a failure here just means the agent falls back to its built-in one-shot
# injector, so it never blocks provisioning. Skip entirely with HH_SBX_GOOSE=0.
if [[ "${HH_SBX_GOOSE:-1}" != "0" ]] && ! command -v goose >/dev/null 2>&1; then
if command -v curl >/dev/null 2>&1; then
GOOSE_BIN_DIR=/usr/local/bin CONFIGURE=false \
bash -c 'curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | bash' \
|| true
fi
fi
# Container-side Goose config: point it at the host's Ollama via the engine's
# host gateway (passed in as $HH_OLLAMA_HOST by the launcher). The shared shell
# runs as root here, so write it under /root — only if absent, so a tuned config
# survives a re-provision.
if command -v goose >/dev/null 2>&1; then
GOOSE_CFG=/root/.config/goose/config.yaml
if [[ ! -f "$GOOSE_CFG" ]]; then
mkdir -p "$(dirname "$GOOSE_CFG")"
cat > "$GOOSE_CFG" <<EOF
# Written by hh sandbox-bootstrap.sh — Goose runs INSIDE this sandbox and reaches
# the host Ollama over the engine's host gateway. No host filesystem is mounted.
GOOSE_PROVIDER: ollama
GOOSE_MODEL: ${HH_GOOSE_MODEL:-qwen2.5:3b}
OLLAMA_HOST: ${HH_OLLAMA_HOST:-http://host.containers.internal:11434}
EOF
fi
fi
# No in-sandbox agentic harness is installed here: the native `!task` harness
# runs the model host-side and only execs commands into this sandbox, so nothing
# extra needs to live in the container.
mkdir -p "$(dirname "$SENTINEL")"
date -u +%FT%TZ > "$SENTINEL" # mark done; skip on the next provision pass
+18 -11
View File
@@ -2624,11 +2624,12 @@ fn handle_command(
app.sys("an AI agent is already running from this client — /ai stop first");
} else {
// Trailing flag words (any order): `allow` auto-grants the agent
// sandbox drive on launch; `plain` selects the legacy one-shot
// injector instead of the default Goose harness.
// sandbox drive on launch; the harness word `native` (default) or
// `simple` picks the granted-`!task` harness. `plain` is a back-compat
// alias for `simple`.
let mut raw = rest.trim();
let mut grant_sbx = false;
let mut plain = false;
let mut harness: Option<&str> = None;
loop {
if let Some(head) = raw
.strip_suffix("allow")
@@ -2638,17 +2639,20 @@ fn handle_command(
raw = head.trim();
continue;
}
if let Some(head) = raw
.strip_suffix("plain")
.filter(|h| h.is_empty() || h.ends_with(' '))
if let Some((word, head)) = ["native", "simple", "plain"]
.iter()
.find_map(|w| {
raw.strip_suffix(w)
.filter(|h| h.is_empty() || h.ends_with(' '))
.map(|h| (*w, h))
})
{
plain = true;
harness = Some(if word == "native" { "native" } else { "simple" });
raw = head.trim();
continue;
}
break;
}
let harness = if plain { Some("simple") } else { None };
// A bare name (no ':' tag, no '/' path) is a models.toml profile;
// anything else is treated as a literal Ollama model tag.
let (profile, model): (Option<&str>, &str) = if raw.is_empty() {
@@ -2671,7 +2675,10 @@ fn handle_command(
Some(p) => format!("profile {p}"),
None => format!("ollama/{model}"),
};
let hdesc = if plain { ", simple harness" } else { "" };
let hdesc = match harness {
Some(h) => format!(", {h} harness"),
None => String::new(),
};
app.sys(format!(
"⛧ summoning {name} ({desc}{hdesc})… it will announce when online"
));
@@ -3245,8 +3252,8 @@ fn spawn_agent(
.arg(model);
}
}
// Override the agent's default `!task` harness (goose) when the launcher
// asked for the legacy one-shot injector via `/ai start … plain`.
// Override the agent's default `!task` harness when the launcher asked for a
// specific one via `/ai start … native|simple` (plain aliases simple).
if let Some(h) = harness {
cmd.arg("--harness").arg(h);
}
+7 -27
View File
@@ -685,7 +685,7 @@ impl Backend {
/// sandbox — `docker`/`podman exec`, `multipass exec`, or host `local`.
/// Advertised in the `_sbx:status` frame (distinct from `label`, whose Local
/// value is the cosmetic "local-shell") so the bridge can build the right
/// `goose run` invocation for the backend that holds the sandbox.
/// `<engine> exec` invocation for the backend that holds the sandbox.
pub fn engine(self) -> &'static str {
match self {
Backend::Local => "local",
@@ -777,19 +777,11 @@ pub fn prepare(
// surfaced through the returned error (shown in the error popup).
let mut run = Command::new(engine);
run.args(["run", "-d", "--name", name, "--hostname", name, "-w", "/root"]);
// Goose (and any in-container tool) reaches the host Ollama via a
// gateway. Docker maps `host.docker.internal` to the host gateway IP.
// For rootless Podman the native `host.containers.internal` resolves to
// the host's *LAN* interface, which can't reach an Ollama bound to
// 127.0.0.1 (the safe default) — so request slirp4netns host-loopback
// forwarding and point the in-container OLLAMA_HOST at the slirp gateway
// 10.0.2.2 (see dk_bootstrap). Without this the granted `!task` path dies
// with "Could not connect to host.containers.internal:11434".
if backend == Backend::Docker {
run.arg("--add-host=host.docker.internal:host-gateway");
} else if backend == Backend::Podman {
run.arg("--network=slirp4netns:allow_host_loopback=true");
}
// The native harness runs the model host-side and only execs commands
// into the container, so the sandbox no longer needs to reach host
// Ollama. The old in-container Ollama gateway (Docker host-gateway /
// Podman slirp4netns host-loopback) is therefore gone — and with it the
// rootless-Podman loopback bug it used to work around.
run.args([image, "sleep", "infinity"]);
let out = run
.output()
@@ -1246,21 +1238,9 @@ fn dk_bootstrap(engine: &str, name: &str) {
"apt-get update -qq && apt-get install -y --no-install-recommends $HH_SBX_PKGS".into()
});
let pkgs_env = format!("HH_SBX_PKGS={}", sandbox_pkgs());
// The in-container Goose config points OLLAMA_HOST at the host gateway, whose
// address depends on the engine. Docker resolves `host.docker.internal` via the
// `--add-host` we add at run time. Rootless Podman's `host.containers.internal`
// resolves to the host LAN IP and can't reach a loopback-bound Ollama, so we
// launch the container with slirp4netns:allow_host_loopback=true (see launch())
// and target the slirp host-loopback gateway 10.0.2.2 instead. Pass it through
// so sandbox-bootstrap.sh can bake the right URL.
let gateway = if engine == "podman" {
"HH_OLLAMA_HOST=http://10.0.2.2:11434"
} else {
"HH_OLLAMA_HOST=http://host.docker.internal:11434"
};
let child = Command::new(engine)
.args([
"exec", "-i", "-e", &pkgs_env, "-e", gateway, name, "bash", "-s",
"exec", "-i", "-e", &pkgs_env, name, "bash", "-s",
])
.stdin(Stdio::piped())
.stdout(Stdio::null())