subagent: give each run its own signal URL, and drop the name argument

`goal_reached`/`need_help` took the session name as a tool argument, so
identity was an assertion by the caller and the only guard on it was
`occupancy()` — "does that name have a turn in flight", which two
concurrently running siblings both satisfy for each other. A subagent
could stop its sibling's run by naming it.

Identity moves into the URL. Each spawned run is minted an unguessable
token (`Uuid::new_v4`, the OS CSPRNG), the URL carrying it goes into that
one subagent's own `--mcp-config`, and the route resolves it back to a
session before dispatching to a handler bound to that session. Neither
tool takes a `name` any more: a subagent has no field in which to name a
sibling, and a sibling's name — which a brief may well mention — is not a
token.

One route with a path parameter, not a route per session: the `Router` is
built once at startup and subagents come and go for the daemon's whole
life. An unminted or revoked token gets a bare 404, the same answer either
way, so nothing enumerates. A run's token is revoked when the run ends
(`finish_turn`) or when a call never reached a spawn.

Two things fall out of that:

- the config file becomes one per session. A single shared path was
  already a race between two `start`s; with a per-session URL in it, the
  loser would read the winner's identity.
- `occupancy()` stops being the identity guard and is gone from the signal
  path entirely rather than kept "just in case" — a revoked token can't
  reach it, and it never answered the question it was standing in for.
  It still backs `status`, which is what it was always actually for.

Refs #4403
Refs #4413
This commit is contained in:
atlas 2026-09-14 22:24:51 +02:00
commit 34129d776c
11 changed files with 575 additions and 180 deletions

View file

@ -350,9 +350,12 @@ in
# crate, own process): spawns nested claude sessions on request, serves
# the `start`/`continue`/`status`/`interrupt` MCP tools directly over
# streamable-http on `hyperhive.mcp.subagentHttpPort`. The same port also
# serves a second, subagent-facing route (`/signal/mcp`:
# `goal_reached`/`need_help`) that the daemon hands each subagent it
# spawns — not something an agent's own config points at. No task files —
# serves a second, subagent-facing route (`/signal/mcp/<token>`:
# `goal_reached`/`need_help`) — not something an agent's own config points
# at: the daemon mints each subagent it spawns its own token and writes
# that one URL into that subagent's own `--mcp-config`, which is how a
# signal's identity comes from the endpoint instead of from a `name` the
# caller could have filled in with a sibling's. No task files —
# this daemon's only state is in-memory, live only as long as the process
# is (see `hive-subagent-mcp/src/session.rs`'s module doc); a restart
# stops whatever's running, the actual claude session survives