hyperhive/docs/tools/bash.md
atlas 5af1f6a8e5 docs, mcp.nix: an overridable default is not unconditional, and there are four subagent tools
`docs/tools/subagent.md` and `docs/tools/bash.md` both described their MCP
server as injected "unconditionally". Both entries are `lib.mkDefault`, and
the module says why one line above each: "so an agent.nix can still
override/disable the entry", "so the operator's own agent.nix can override
the entry".

The word matters for the subagent one in particular. The same comment block
records the framing that it is default-on for now and should become a real
capability gate later, so "can I turn this off today?" is a question an
operator has — and "unconditionally" answers it as "patch nix/" when the
answer is one override in agent.nix.

Both pages now say default, and say what the default yields to.

The other direction on the same page: `subagentHttpPort`'s option
description and the unit comment beside it both listed three tools,
`start`/`continue`/`interrupt`. The daemon serves four. #4101, which
introduced it, is titled with the three-verb phrasing, so `status` landed
afterwards and never reached either description — while `subagent.md` had
the full set all along. The option description renders into the generated
options doc, so it is the one an operator reads.

Closes #4231.
2026-09-11 16:58:12 +02:00

127 lines
6 KiB
Markdown

# Bash execution tools
Background shell execution via `hive-bash-daemon`. Tools land as
`mcp__bash__<tool>` (the MCP server name is `bash`, not `hyperhive`).
Available on every agent by default — `nix/agent-modules/mcp.nix` injects
bash into `hyperhive.extraMcpServers` via `lib.mkDefault` (with
`allowedTools = ["*"]`), so `mcp__bash__*` is in `--allowedTools` for
every claude invocation regardless of tool groups. Default rather than
unconditional: an `agent.nix` can override or drop the entry.
## Tools
### `run(cmd, timeout_secs?, wait_seconds?, name?)`
Submit a shell command for background execution (runs via `bash`).
Stdout and stderr stream to `harness/bash-tasks/<id>.{out,err}`.
When the task completes (or times out, or the process errors), it
surfaces as a todo in the agent's loose-ends (via `get_loose_ends`),
carrying the exit code and a `Read(<path>)` pointer to the captured
output. Handle it on a future turn — unless `wait_seconds` already
delivered the terminal result inline, in which case no todo is created
(see `status` below).
- `timeout_secs` — kill the task after N seconds and mark it
`timed_out`. Omit for no timeout (runs until natural exit).
- `wait_seconds` — inline poll before returning (capped at 30).
When the task finishes within the window the full status is
returned immediately and no todo is created; when the window expires
the task keeps running and the daemon returns the normal
`task started: id=<id>` response. **Defaults to 3** — pass `wait_seconds: 0`
to disable inline waiting and always get the immediate response.
- `name` — optional caller-chosen task id. When set it replaces the
autogenerated hex id, so it surfaces in `status(<name>)` lookups and
the loose-ends list — a memorable label instead of an opaque id. A name
is **reusable once its previous task has finished**; the daemon
rejects a name whose task is still `pending`/`running`. Allowed
characters: `[a-z0-9-]` (a valid identifier — lowercase, digits,
hyphen; max 63). Omit for the autogenerated id.
Exposed as `mcp__bash__run`.
### `status(id, wait_seconds?)`
Poll the status of a task submitted with `run`. Returns:
- `status``pending` / `running` / `done` / `timed_out` / `interrupted` / `killed`
- `exit_code` — set when done
- run duration
- last 4 KiB of stdout and stderr (full output in the `.out` / `.err` files)
`wait_seconds` — optional inline poll (capped at 30): when the task
finishes within the window the call returns the full status immediately.
Useful to avoid a separate round-trip when the task is expected to
finish soon.
Any `status` call (waited or not) that observes a terminal task clears
that task's completion todo — you already have the result in this
response, so no redundant loose-end follows. Narrow best-effort race: a
`status`/`run` inline wait that resolves in the same instant the task
actually finishes can still occasionally get both.
Tasks marked `interrupted` had their process killed by a harness
restart; a best-effort todo is still surfaced so the agent isn't
silently blocked.
Exposed as `mcp__bash__status`.
### `kill(id, force?)`
Stop a running or pending task by its ID (from `run`). Fire-and-forget:
sends the signal and returns without waiting — the completion surfaces
in the agent's loose-ends; handle it on a future turn.
- `force: false` (default) — SIGINT to the task's **process group**
(graceful; lets the process clean up). The whole process group is
signalled, so children spawned by the shell (cargo, nix, etc.) are
also stopped.
- `force: true` — SIGKILL.
If a SIGINT'd task doesn't exit, call `kill` again with `force: true`.
The daemon cancels a still-pending task before it starts. The task ends as
`killed` and surfaces in the loose-ends like any completion.
Exposed as `mcp__bash__kill`.
## Namespace note
`run` and `status` live in the `bash` MCP server, not `hyperhive`. The
tool names in claude are `mcp__bash__run` and `mcp__bash__status`.
hyperhive blocks the `Bash` built-in tool — all shell execution goes through
this structured path so tasks get task-id tracking and structured output.
## Architecture
`hive-bash-daemon` is a single long-running process (one per agent
container, systemd service in `nix/agent-modules/mcp.nix`) — no stdio
bridge. It owns subprocess management, output file writing, todo delivery
on the harness's in-agent socket, **and** serves the `run`/`status`/`kill`
MCP tools directly over streamable-http on `hyperhive.mcp.bashHttpPort`
(declared in `hyperhive.extraMcpServers.bash` as `{ type = "http"; url =
...; }`). Same shape as the built-in `hyperhive` surface (`hive-mcp-http`)
— claude reconnects to the stable URL every turn instead of respawning a
stdio child, so there's no per-turn MCP re-registration race and no
round-trip socket hop for tool calls.
### Completion as a todo (loose-ends v2)
When a bash task changes state, `hive-bash-daemon` upserts a single keyed
todo (`key = task id`) on the harness's in-agent socket (`HIVE_AGENT_SOCKET`)
— "running" at start, then the completion summary when it finishes. The
summary change signals the harness turn loop directly (in-process, no broker
round-trip), so the harness drives a turn for the agent to handle it via
`get_loose_ends`, then clears the todo with `cancel_loose_end(kind: "todo", id: N)` (dials the
in-container socket directly — no bash task involved, so clearing doesn't
spawn another todo). Same mechanism the matrix daemon uses for
unread rooms. An inline `wait_seconds` / `status` observation that already
delivered the result instead clears the keyed todo, so no redundant
loose-end follows.
## Relationship to the `execution` tool group
`ToolGroup::Execution` exists and appears in `AGENT_DEFAULT`, but its
`tools()` returns `["run", "status"]` which the harness expands to
`mcp__hyperhive__run` / `mcp__hyperhive__status` — tools that don't
exist in the hyperhive MCP server (dead entries). Removing `execution`
from an agent's groups has no effect on bash availability.
`nix/agent-modules/mcp.nix` registers Bash separately via the `extraMcpServers` path described above.