docs: move the agent tool docs to 4-implementation/agent-tools
This commit is contained in:
parent
df8a82bc33
commit
aa227a95c1
1 changed files with 94 additions and 83 deletions
|
|
@ -1,463 +0,0 @@
|
|||
# Subagent daemon
|
||||
|
||||
`hive-subagent-daemon` (crate `hive-subagent-mcp`) spawns nested headless
|
||||
`claude` sessions on request. Own process, own systemd unit, own MCP
|
||||
server (`subagent`, not `hyperhive`) — independent of `hive-bash-daemon`:
|
||||
a subagent is a full nested claude process, a materially heavier
|
||||
capability than a background shell command, so it gets its own
|
||||
deployable/restartable unit rather than living inside the bash daemon.
|
||||
|
||||
Shipped default-on for every agent — `nix/agent-modules/mcp.nix` injects
|
||||
`subagent` into `services.hyperhive.agent.extraMcpServers` via `lib.mkDefault`
|
||||
(`allowedTools = ["*"]`), same as `bash`. Default-on rather than
|
||||
unconditional: an `agent.nix` can override or drop the entry, which is
|
||||
what `mkDefault` is there for.
|
||||
|
||||
For what the tools do and when an agent should reach for them, see the
|
||||
`subagent` MCP server's own tool descriptions and the
|
||||
`base:claude-subagents` skill — this page covers the daemon as deployed
|
||||
infrastructure, not the agent-facing API.
|
||||
|
||||
## Tools
|
||||
|
||||
Served under the `subagent` MCP server (`mcp__subagent__<tool>`): `start`,
|
||||
`continue`, `status`, `interrupt`.
|
||||
|
||||
A second route on the same port serves the two tools a **subagent** calls
|
||||
about its own run — `goal_reached` and `need_help`. It isn't part of the
|
||||
`subagent` server an agent's own config points at; the daemon writes it
|
||||
into each subagent's `--mcp-config` itself, under `subagent_control`. Two
|
||||
routes rather than six tools on one, so that being able to report on a run
|
||||
never carries the ability to start one: there's no route a subagent holds
|
||||
that `start` is reachable from.
|
||||
|
||||
## A subagent can't name a session, not even its own
|
||||
|
||||
Neither signal tool takes a session name. **The URL is the identity.** At
|
||||
each spawn the daemon mints that run an unguessable token, serves it at
|
||||
`/signal/mcp/<token>`, and writes that one URL into that one subagent's own
|
||||
`--mcp-config` — a file per session, not a shared one. The daemon resolves
|
||||
a request to a session before dispatching it, and the tools read the
|
||||
session off the resolution.
|
||||
|
||||
A subagent therefore has no field in which to name a sibling, and knowing
|
||||
a sibling's name buys nothing: a name isn't a token. Two subagents running
|
||||
concurrently can't signal each other — which the earlier shape, a shared
|
||||
route plus a `name` argument guarded by a liveness check, allowed.
|
||||
|
||||
A token that resolves to nothing — never minted, or revoked when its run
|
||||
ended — gets a bare **404**, the same answer for every token, so nothing
|
||||
about the refusal says whether some other session exists.
|
||||
|
||||
## State
|
||||
|
||||
In-memory only: what's running now, where each name's session lives, how
|
||||
each name's last turn ended, when each running turn last produced output,
|
||||
what each session is working toward, how far through its turn budget it
|
||||
is, why its run stopped, where it writes its report, and which signal
|
||||
token belongs to it. All of it lives only as long as the daemon process
|
||||
does — so a daemon restart invalidates every signal URL it had issued,
|
||||
which is the same thing as it having stopped the runs those URLs belonged
|
||||
to. A daemon
|
||||
restart stops whatever was running rather than adopting it. The durable
|
||||
record of a subagent's existence is claude's own on-disk session
|
||||
(`hive_claude::SessionStore`), which `continue` reattaches to independent
|
||||
of the daemon's own lifetime — a restart loses the _in-flight turn_, not
|
||||
the subagent's history.
|
||||
|
||||
`continue`, and each later turn of a goal run, hands claude a session id,
|
||||
not the name: the id of the live session carrying that name in `dir`'s
|
||||
project directory, read off disk at every resume. claude looks a bare name
|
||||
up across every git worktree of the repo and refuses when two sessions
|
||||
match, and `start` archives a prior session only in its own directory, so
|
||||
a name reused under a sibling worktree would otherwise stop resuming. When
|
||||
`dir` holds no session under the name, the daemon refuses and never hands
|
||||
claude the bare name — see below.
|
||||
|
||||
Because the remembered directory goes with the rest of it, a `continue`
|
||||
after a restart has to re-supply `dir` when the session lives anywhere
|
||||
other than the agent's work dir (the default when it exists) — and a restart is the
|
||||
situation you reach for `continue` in most often.
|
||||
|
||||
## Roles
|
||||
|
||||
A `start` may name a **role**: `role: "reviewer"` runs that subagent under
|
||||
the spawning agent's own `reviewer.md`. Role files live in one directory
|
||||
per agent, `subagent_roles/` inside that agent's state directory, one
|
||||
markdown file per role, and the `role` argument is the filename without
|
||||
its extension.
|
||||
|
||||
One convention directory of named files, because a named set enumerates.
|
||||
Anything that watches roles — a check for a safety clause that has gone
|
||||
stale, an operator asking what this agent can dispatch — lists the
|
||||
directory and has the answer. Per-agent rather than hive-wide: the clauses
|
||||
a role carries come out of that one agent's own prompt files, and the
|
||||
daemon only ever spawns on that agent's behalf.
|
||||
|
||||
**The task is the turn, never a standing system prompt — with or without a
|
||||
role.** A task baked into the system prompt re-asserts itself as an
|
||||
instruction on every later turn of the session (`continue`, `goal`), not
|
||||
just the one the caller wrote it for — the system prompt is the
|
||||
subagent's standing identity, not a one-shot channel, so the daemon always
|
||||
reads the task instructions (`prompt_file`) and folds them ahead of the
|
||||
turn's own prompt instead, whether or not the session has a role. The
|
||||
caller's own task file stays untouched either way.
|
||||
|
||||
A role additionally names a standing identity: its text, alone, becomes a
|
||||
per-session file under the harness directory, and `--append-system-prompt-file`
|
||||
points at that — nothing about the task ever reaches it. Without a role
|
||||
there is no such identity to hold a file open for, so the daemon omits
|
||||
`--append-system-prompt-file` altogether rather than pointing it at
|
||||
anything task-shaped.
|
||||
|
||||
⚠️ This applies to every dispatch, not just role-bearing ones.
|
||||
|
||||
### A role with no file refuses the call
|
||||
|
||||
A named role with no file **fails the `start`** and names the roles the
|
||||
directory does hold. No fallback, no spawn, no reservation — the refusal
|
||||
lands before the daemon claims the session name, so a caller that fixes
|
||||
the name may retry it immediately.
|
||||
|
||||
The reason is the state every agent starts in. The directory starts
|
||||
empty: hyperhive ships no role files, and nothing creates one until an
|
||||
agent writes its own, so a name with no file behind it counts as the
|
||||
ordinary first-run answer rather than a rare corruption. Falling back to a spawn
|
||||
without the role would hand that subagent a prompt missing every clause
|
||||
the role existed to carry, and would do it quietly. An empty role file
|
||||
draws the same refusal, for the same reason.
|
||||
|
||||
## Goals, and turns toward them
|
||||
|
||||
`start` takes an optional `goal`. Without one a session is a single turn,
|
||||
exactly as it always was. With one, the daemon keeps the session going:
|
||||
when a turn ends and nothing has said to stop, it starts another turn
|
||||
re-prompting the subagent toward that goal, quoting it verbatim and saying
|
||||
which turn of the budget this is. `max_turns` caps that, defaulting to
|
||||
**5**.
|
||||
|
||||
`status` reports `Turn N of M` for such a session in every state it
|
||||
reaches. Read alongside the last-event age below, it's what separates a
|
||||
subagent that's working from one that's wedged from one that's out of
|
||||
turns — without `ps` and without opening a file.
|
||||
|
||||
Five things stop a run; the daemon records each distinctly, reports it via
|
||||
`status`, and appends it to the one todo it pushes when the run ends:
|
||||
|
||||
- **the turn ended and there was no goal** — the single-turn case;
|
||||
- **`goal_reached`**, which the subagent calls itself;
|
||||
- **`need_help`**, likewise;
|
||||
- **the turn cap**, which says so rather than stopping quietly: the todo
|
||||
states that the run hit the harness limit and the goal was never
|
||||
reported reached, so the work stopped where it had got to;
|
||||
- **`interrupt`**, which cancels the whole run and not merely the turn
|
||||
that was in flight.
|
||||
|
||||
A killed or failed turn ends the run too, and keeps the records it already
|
||||
had — see [A killed turn](#a-killed-turn).
|
||||
|
||||
`interrupt` being on that list is load-bearing, not bookkeeping. Killing
|
||||
the process isn't by itself a cancel: `interrupt` sends SIGINT by
|
||||
default, and `claude --print` handles SIGINT by writing its terminal
|
||||
result event and exiting **zero**, which is indistinguishable from a turn
|
||||
that finished. `interrupt` therefore records the cancel before it
|
||||
signals, and the continuation loop reads that record rather than guessing
|
||||
from how the child exited. `continue` clears it and hands the session a
|
||||
fresh turn allowance, which is what makes an interrupt a pause you can
|
||||
undo rather than a session you have to abandon.
|
||||
|
||||
On `claude`, the signal is always SIGINT, and it goes to claude's whole
|
||||
process group. The signal doesn't reach a command that moved itself out of
|
||||
the group (`setsid`), and claude's Bash tool starts every command that way:
|
||||
claude stops those commands itself when it shuts down on SIGINT. A SIGKILL
|
||||
would leave them running, so nothing escalates to one. `interrupt` returns
|
||||
at once, and the name stays claimed until claude has exited: `status` says
|
||||
**stopping**, and the daemon refuses `start` and `continue` on that name
|
||||
until then. A claude that ignores the SIGINT stays stopping until someone kills
|
||||
it by hand or the unit stops.
|
||||
|
||||
When the session knows where its report goes — `start`'s `report_file`,
|
||||
or the path the subagent names when it signals — the daemon appends the
|
||||
stop reason to that file as well, so the artifact you were going to read
|
||||
anyway also says how the run ended. The daemon infers nothing: with no
|
||||
path given, it touches no file.
|
||||
|
||||
## `goal_reached` is a label, not a gate
|
||||
|
||||
Both signals stop the continuation and **extend** the done message. Extend,
|
||||
not replace: the turn's observed end and the reason the run stopped are
|
||||
different facts, and the second never stands in for the first.
|
||||
|
||||
`goal_reached` is **self-reported**, by a subagent that has just been
|
||||
re-prompted with "you haven't reached the goal" — which is precisely the
|
||||
incentive to claim it. It's the same failure class as a build report
|
||||
asserting "done, tests pass": a claim about an artifact, not the artifact.
|
||||
Nothing in this daemon treats it as verification, and every surface that
|
||||
renders it says so. Read the diff and the gate output regardless.
|
||||
|
||||
`need_help` is the blocking signal. It stops the run and shows up in
|
||||
`status` as its own state — blocked, with the subagent's reason — so a
|
||||
parent that polls `status` sees the block without reading anything else.
|
||||
`continue` is how you answer it.
|
||||
|
||||
## Telling working from wedged
|
||||
|
||||
`status` reporting **running** says the daemon tracks a process, which a
|
||||
wedged subagent satisfies as fully as a busy one. A running answer therefore
|
||||
carries the age of that turn's last event too: seconds means it's working,
|
||||
an age climbing into the minutes means it's stuck. That one number
|
||||
replaces inferring the same thing from `ps` output and CPU-time deltas.
|
||||
It resets at each turn's spawn, so on a goal run it describes the turn in
|
||||
flight rather than the run — which is what you want, since a run that's
|
||||
making progress spends a few perfectly healthy minutes.
|
||||
|
||||
Every line the subagent's `claude` process writes bumps the timestamp —
|
||||
stream-json events, plain stdout chatter and stderr alike — and what the
|
||||
subagent actually said is never read. The record says the child is alive,
|
||||
not what it's doing. The clock starts at the spawn, so a subagent that wedged
|
||||
before it ever emitted anything still reports a climbing age rather than
|
||||
no age at all. It's dropped when the turn ends, since a finished turn has
|
||||
no progress left to describe.
|
||||
|
||||
## A `continue` that finds no session
|
||||
|
||||
`continue` looks for the session before it spawns anything. The lookup is
|
||||
the live session titled the name in the project directory of the `dir` in
|
||||
effect: the one passed, or else the one the daemon remembers for the name.
|
||||
No such session refuses the call — nothing spawns, the name stays free
|
||||
for a retry, and the session keeps its `status` (stop reason and turn
|
||||
budget) and its remembered `dir` — and the error names the directory
|
||||
searched:
|
||||
|
||||
```
|
||||
continue error: no session titled `n` under /home/agent/.claude/projects/-home-agent-work; pass the `dir` the session was started in
|
||||
```
|
||||
|
||||
A name is never searched for in any other directory, because claude would
|
||||
resolve a bare title across every worktree of the repo and could pick, or
|
||||
refuse, the wrong session. A session started in another directory
|
||||
needs that directory passed as `dir` — and after a daemon restart, which
|
||||
forgets the remembered one, a session outside the agent's work dir
|
||||
needs it again. A store location the daemon can't resolve
|
||||
refuses the same way, with the io error.
|
||||
|
||||
A goal run's next turn does the same lookup; a miss there ends the run as
|
||||
a failed turn, with the same message in its end-of-turn todo.
|
||||
|
||||
The refusal covers a session missing from the directory searched.
|
||||
It can't cover one that vanishes between the lookup and the spawn, so
|
||||
`continue` still waits for claude's answer instead of returning the
|
||||
instant the process exists. Where `start` returns at the spawn — it
|
||||
creates its session, so the spawn succeeding is the whole story — a
|
||||
resumed turn can fail a moment _after_ a pid exists. `continue` holds the
|
||||
tool call open until the turn is underway or the resume has come back
|
||||
missed, and reports a miss as the call's own error, carrying claude's
|
||||
message plus the location this daemon searched:
|
||||
|
||||
```
|
||||
continue error: claude error: no session matched the requested id or title (searched /home/agent/.claude for cwd /home/agent/work; if it was started elsewhere, pass the `dir` it was started in)
|
||||
```
|
||||
|
||||
The directory is the part claude's own message never names, and the part
|
||||
that resolves the confusion: pass `dir` to point `continue` at the
|
||||
directory where the session began.
|
||||
|
||||
Nothing here is a fixed delay on the way to a successful turn. The wait
|
||||
ends on whichever comes first — the turn's first stream event or its
|
||||
early exit — and on this box both land inside a second, so a `continue`
|
||||
that works answers about as fast as it did before. A five-second cap
|
||||
bounds the one case neither covers: a child that neither speaks nor
|
||||
exits, reported as started, with the end-of-turn todo left to say how it
|
||||
goes. That todo carries every failure that happens after the one
|
||||
`continue` already returned to the caller.
|
||||
|
||||
## A killed turn
|
||||
|
||||
A subagent whose `claude` process dies on a signal — the kernel's OOM
|
||||
killer under memory pressure, a stopped unit, an `interrupt` from the
|
||||
owning agent — didn't finish its turn, and the daemon says so rather
|
||||
than letting it settle back into `idle`:
|
||||
|
||||
- `status` reports the session **killed**, naming the signal, instead of
|
||||
the `idle` it reports for a turn that ended on its own;
|
||||
- the end-of-turn todo the daemon pushes unprompted says something killed
|
||||
the subagent mid-turn, not that it finished;
|
||||
- `continue` still resumes such a session — often what you want — but
|
||||
its reply says something killed the previous turn, so nobody carries on
|
||||
from cut-off work believing it was complete.
|
||||
|
||||
The record is per-name, in memory with the rest of this daemon's state,
|
||||
and the next confirmed spawn under that name clears it. A daemon restart
|
||||
loses it along with everything else — the daemon has to outlive the kill
|
||||
to report it, which is what `OOMPolicy=continue` on the unit is for.
|
||||
|
||||
## Compaction trade-off
|
||||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
|
||||
Built on `hive_claude::Claude::spawn` + `RunningClaude::wait` directly
|
||||
rather than `InfiniteSession::run`, since only the low-level driver
|
||||
exposes a cancel handle to stop a turn mid-flight — that's what makes
|
||||
`interrupt` genuinely stop a running turn rather than only cancelling a
|
||||
still-pending one. The cost: a turn that overflows the context window
|
||||
surfaces as an error rather than self-healing via reactive compaction.
|
||||
Subagents are meant to be bounded, single-batch work, not sessions
|
||||
long-lived enough to need in-place compaction — a real follow-up if that
|
||||
assumption stops holding.
|
||||
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
||||
## Configuration
|
||||
|
||||
`services.hyperhive.agent.mcp.subagentHttpPort` — the daemon's streamable-http listen
|
||||
port. Same pattern as `bashHttpPort`/`matrixHttpPort`: a per-agent default
|
||||
assigned by `nix/agent-modules/mcp.nix`, only worth overriding for an
|
||||
agent that needs a stable or non-default port.
|
||||
|
||||
Own systemd unit, defined alongside the other per-agent MCP daemons in
|
||||
`nix/agent-modules/mcp.nix`.
|
||||
|
||||
## Which models a subagent may run on
|
||||
|
||||
The daemon refuses a `start` or `continue` that names a `model` outside the
|
||||
agent's list before anything spawns, and the error names the list and where
|
||||
it came from. One without a `model` runs on claude's own default, which the
|
||||
daemon doesn't check. The list is:
|
||||
|
||||
- the swarm's `subagent_models` for this agent, when it sets one
|
||||
([`inference.md`](../../4-implementation/swarm/inference.md)). It replaces the agent's own
|
||||
list, so it can name models that list leaves out.
|
||||
- otherwise `services.hyperhive.agent.availableModels`, which reaches the
|
||||
daemon as `HIVE_AVAILABLE_MODELS`. An empty list restricts nothing.
|
||||
|
||||
The daemon reads the swarm's value on each such call, so a change applies
|
||||
to the next one. When the read fails, or doesn't answer within 2 seconds,
|
||||
the daemon logs a warning and checks against `availableModels`. The same
|
||||
happens until the daemon's first connection to the swarm queue succeeds.
|
||||
The match ignores case. On an ACP agent the names are the agent's own, such
|
||||
as `<provider>/<model>` on `opencode`.
|
||||
|
||||
## How many subagents may run at once
|
||||
|
||||
When the swarm sets `max_subagents` for the agent
|
||||
([`inference.md`](../../4-implementation/swarm/inference.md)), the daemon refuses a `start`, or a
|
||||
`continue` of a session that isn't running, once that many subagents are
|
||||
running, before anything spawns:
|
||||
|
||||
```text
|
||||
3 subagents running, cap is 3 (set in swarm-ui → this agent → inference) — wait for one to finish, or `interrupt` one
|
||||
```
|
||||
|
||||
Every subagent the daemon tracks counts, whatever its role: one running a
|
||||
turn, one starting, a goal run between turns, and an interrupted one whose
|
||||
process hasn't exited yet. A goal run's next turn isn't a new start, and the
|
||||
cap never stops it. Lowering the cap stops nothing already running.
|
||||
|
||||
The daemon reads the cap with the model list, on every `start` and
|
||||
`continue`, so a change applies to the next call. A cap of 0 refuses every
|
||||
`start` and `continue`, and the message says the cap is 0. Without a swarm value, or when the read fails, no cap applies. A failed read logs a warning naming the agent and the error, and the start proceeds: an unreachable swarm must not block subagent work.
|
||||
|
||||
## Runtime
|
||||
|
||||
A subagent runs on its parent agent's runtime
|
||||
(`services.hyperhive.agent.runtime`). The unit gets the harness's
|
||||
`HIVE_RUNTIME`/`HIVE_ACP_*` variables, and the daemon reads them once at
|
||||
startup (`hive_runtime::RuntimeSpec`). On `claude`, everything else on this
|
||||
page applies as written.
|
||||
|
||||
On `acp`:
|
||||
|
||||
- each run spawns its own copy of the parent's ACP agent. The end of the
|
||||
run sends SIGINT to it and every command in its process group. The unit loads
|
||||
`backendEnvironmentFile` for it. On the `opencode` preset, the daemon
|
||||
reads a provider key that file leaves unset from the swarm secret store
|
||||
at `swarm/agents/<agent>/acp-provider`, as the harness does.
|
||||
- the session id lives in `subagent-acp-session-<name>` under the harness
|
||||
dir. `start` moves an old one aside, `continue` loads it into a fresh
|
||||
agent process, and a `continue` with none recorded fails straight away.
|
||||
- a role goes in front of the session's first prompt, since ACP has no
|
||||
system prompt.
|
||||
- `interrupt` sends `session/cancel`. The runtime sends SIGINT to the
|
||||
process group of an agent that ignores the cancel for 10s, and when the
|
||||
run ends. Nothing escalates past that SIGINT. The name is free as soon as
|
||||
`interrupt` sends the cancel.
|
||||
- the runtime sets `model` and `effort` on the session when the agent
|
||||
offers that value for its `model` or `thought_level` config option. Any
|
||||
other value does nothing (`hive_runtime::AcpRuntime`).
|
||||
- the agent's permission requests get the same answers a claude subagent's
|
||||
tool list gives: MCP tools and file tools, `fetch` with `web_tools`,
|
||||
`execute` with `execution` (`session::acp_permits`).
|
||||
|
||||
## The tool surface a subagent gets
|
||||
|
||||
Two flags, each covering one half, and neither covering the other:
|
||||
`--tools` governs claude's built-in tools, `--strict-mcp-config` governs
|
||||
the MCP ones. Dropping either brings that half back in full; in
|
||||
particular `--tools` does **not** filter `mcp__*` tools.
|
||||
|
||||
### Built-in tools (`--tools`)
|
||||
|
||||
**A subagent gets exactly the built-ins its parent agent has, plus one
|
||||
deliberate exception** — the base resolution
|
||||
(`hive_sh4re::permissions::subagent_builtin_tools_for`, wrapping the
|
||||
harness's own `builtin_tools_for`) over the agent's permissions, which the
|
||||
daemon reads from the swarm on each `start`/`continue`:
|
||||
`Edit`, `Glob`, `Grep`, `Read`, `Skill`, `Write`, plus `WebFetch`/
|
||||
`WebSearch` for an agent granted `web_tools` and not otherwise. A running
|
||||
subagent keeps the tools it started with. See
|
||||
[the harness's own allowlist](../../4-implementation/turn-loop/mcp.md#tool-allowlist-hive_sh4repermissionsallowed_builtin_tools)
|
||||
for what that list contains and why.
|
||||
|
||||
The exception: **a subagent gets Claude's built-in `Bash` iff its parent
|
||||
holds the `execution` permission** — the permission that already grants the
|
||||
parent shell execution, via the out-of-process `bash` MCP server
|
||||
(`mcp__bash__run`/`status`/`kill`). A subagent has no MCP server of its
|
||||
own by default (see [MCP servers](#mcp-servers---strict-mcp-config)
|
||||
below), so there is no `mcp__bash__*` for it to inherit that capability
|
||||
through; it gets Claude's own tool instead. The rule is the capability
|
||||
transfers, not the mechanism — the parent itself never gains built-in
|
||||
`Bash` no matter which permissions it holds, only a subagent spawned by an
|
||||
`execution`-holding parent does.
|
||||
|
||||
Inheriting rather than listing is the point: a hardcoded subagent list
|
||||
would hand web egress (or `Bash`) to the subagent of an agent that isn't
|
||||
allowed it, and would diverge from the parent's on the first tool anyone
|
||||
adds to either.
|
||||
|
||||
Everything else in claude's built-in set is absent, in particular the
|
||||
tools that let a session act outside the run for which it began: peer and
|
||||
operator messaging, nested agents (including the stop verb, which takes
|
||||
an _agent_ id rather than a session), schedule and webhook creation, and
|
||||
worktree switching.
|
||||
|
||||
One rule worth knowing before editing any of this: **the daemon never
|
||||
emits an empty `--tools` value**, and asserts rather than doing so. Not
|
||||
because of what an empty value does — that's exactly the point. The hive's own
|
||||
measurement and the installed `claude --help` disagree about whether
|
||||
`--tools ""` means "no tools" or reads as the flag being absent, and the
|
||||
answer belongs to whichever claude release the system has installed
|
||||
rather than to this repo. Refusing the case is correct under either reading, so the code
|
||||
never has to know which holds. Nothing here wants a subagent with no
|
||||
built-in tools anyway.
|
||||
|
||||
### MCP servers (`--strict-mcp-config`)
|
||||
|
||||
A subagent runs with `--strict-mcp-config` and, by default, exactly one
|
||||
MCP server: the two-tool `subagent_control` route above — not the
|
||||
parent's `mcp__bash__*` / `mcp__hyperhive__*` surface. Those two signal
|
||||
tools are deliberately unnamed in `--tools`, which doesn't govern them;
|
||||
they survive on this flag alone. Nothing implicit reaches it: the built-in
|
||||
hyperhive surface (todos/messaging) isn't an `extraMcpServers` entry at
|
||||
all, and the automatically injected `bash`/`subagent` entries default to excluded
|
||||
too (a subagent can't spawn hive-bash tasks or its own nested subagents
|
||||
unless an operator opts them in explicitly, same as anything else).
|
||||
|
||||
Set `services.hyperhive.agent.extraMcpServers.<name>.availableToSubagents = true` on a
|
||||
specific entry to hand that one server to subagents as well — useful for,
|
||||
say, a read-only lookup or scraper MCP a subagent's bounded, single-batch
|
||||
task might need. `hive-subagent-mcp`'s `mcp_config` module renders the
|
||||
opted-in subset into a `--mcp-config` file per session, rewritten each
|
||||
turn; an entry left at the default `false` never appears there. Per
|
||||
session rather than one shared file, because the `subagent_control` entry
|
||||
in it carries that session's own signal URL — one file for everyone would
|
||||
be a race over whose identity each subagent reads at startup.
|
||||
Loading…
Reference in a new issue