Skip to content

hive-acp: adapt session/new to the container it is headed for - #5

Merged
unforced merged 20 commits into
Unforced-Dev:mainfrom
unforcedagi:hive-acp-session-new
Jul 30, 2026
Merged

hive-acp: adapt session/new to the container it is headed for#5
unforced merged 20 commits into
Unforced-Dev:mainfrom
unforcedagi:hive-acp-session-new

Conversation

@unforcedagi

Copy link
Copy Markdown
Contributor

Follow-up to #4. Two of the commits below were pushed to that branch after it had already been merged, so they never landed — this reopens them on top of current main, with a third fixing the bug that showed up the moment Buzz drove it for real.

The bug

Creating an agent in Buzz with harness=hive failed at the model picker: "could not load live models for this provider", and no agent could be created.

buzz-acp models spawns the harness, sends initialize, then session/new, and reads the models out of the response. The session/new it sends carries the cwd of the machine Buzz is on. The harness is inside a container, where that path does not exist — claude-agent-acp validates it and fails the session:

Invalid params: `cwd` does not exist on the machine running the agent: /private/tmp

No session, so no models, so no agent. Three steps from the cause, and nothing in the message points at a container boundary.

The fix

hive-acp substitutes the agent's workspace — but only when the container genuinely lacks the path, so a spec that bind-mounts a host directory at the same path still resolves to itself. The workspace is HIVE_ACP_WORKSPACE, else /home/agent/work (which the agent image creates for exactly this), else the image's WORKDIR. Resolving it costs two docker calls, so it is lazy — a session with a usable cwd never pays for it.

cwd and MCP injection are independent reasons to rewrite. Gating the cwd fix on having MCP servers would have left the plainest possible agent — no MCP at all — as the only one unable to open a session.

Verified

Against the live box, through the real buzz-acp binary from Buzz 0.5.0:

$ HIVE_AGENT=uni buzz-acp models --json --agent-command hive-acp --agent-args ""
hive-acp: agent=uni container=hive-uni harness=claude-agent-acp
hive-acp: cwd "/private/tmp" is not in the container; using "/home/agent/work"
hive-acp: attached MCP server parachute -> https://.../vault/unforced/mcp
{ "agent": { "name": "@agentclientprotocol/claude-agent-acp", "version": "0.63.0" },
  "stable": { "configOptions": [ { "id": "model", ... 4 options ... } ] } }

136 workspace tests pass; 12 in hive-acp, including that the workspace is not resolved when the cwd is already good.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM

unforcedagi and others added 20 commits July 29, 2026 10:03
ACP's McpServer is a union — McpServerHttp, McpServerSse, McpServerAcp,
McpServerStdio — and the HTTP variant carries a `headers` array. Buzz
implements only the stdio one. The protocol was never the limitation; its
client is. claude-agent-acp advertises mcpCapabilities {http, sse}.

So session/new is rewritten on the way down: hive appends the agent's own
MCP servers, credentials and all, to whatever the client sent. Doing it
over the protocol rather than by writing harness config files means it
works for any harness advertising mcpCapabilities.http, not only the one
whose config format hive happens to know.

Credentials come from the broker via hive-headers, executed inside the
container. The broker's socket is bind-mounted per-agent and cannot be
reached from the host — on macOS not even in principle, since it is on the
far side of the Docker VM. hive-headers already exists in the image for
this exact conversation, so the fetch stays grant-checked and audited and
there is one path to the broker rather than two.

Its contract is `{"headers": {...}}` — NESTED. Parsing it as a flat map
yields zero headers and an MCP server that 401s with nothing in the logs
to say why. Found by running it rather than by reading it.

Specs are read locally when present and through the daemon container when
not, for the same boundary reason: where hived runs in a container the
spec directory is a volume this process cannot see, and demanding a second
host-side bind mount would create two sources of truth for one file.

Everything that is not a session/new request is still copied verbatim in
both directions, and a line that does not parse as JSON is forwarded as
bytes rather than dropped.

Verified end to end: a client sending mcpServers=[] got an agent that
listed all eleven Parachute tools and called vault-info against the live
vault over HTTPS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
…an agent

Harness mode had no way to work. hive-core hardcoded the container command
to buzz-acp, which is right when the container holds its own relay
connection and wrong when it does not: in harness mode buzz-acp runs
wherever the desktop runs and holds the identity, so a container starting
its own has no nsec, crash-loops, and makes `docker exec` fail
intermittently rather than cleanly.

  [agent]
  mode = "environment"   # default "relay"

`relay` keeps today's behaviour exactly — buzz-acp inside, agent outlives
any desktop. `environment` idles instead and waits for hive-acp to exec
the harness in. The image ENTRYPOINT still runs first either way, so the
state directories an environment container needs are still created.

The nsec requirement follows the mode. Demanding one for an environment
container held the agent forever waiting for a key that by design lives in
the desktop, and the hold read as a missing credential rather than as a
mode mismatch — which is exactly how it presented the first time.

Verified end to end: an environment agent with an [[mcp]] block reconciles
to a stable container, and hive-acp execs claude-agent-acp into it, which
lists every Parachute tool and calls vault-info against the live vault.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
session/new carries the directory the *client* chose, on the client's
machine. The harness is inside a container, where that path means nothing:
claude-agent-acp validates it and fails the session with `cwd does not
exist on the machine running the agent`, so no session opens and Buzz's
model picker reports it could not load models for the provider — three
steps from the actual cause.

hive substitutes the agent's workspace, but only when the container really
lacks the path, so a spec that bind-mounts a host directory at the same
path still resolves to itself. The workspace is HIVE_ACP_WORKSPACE, else
/home/agent/work, else the image's WORKDIR, and it is resolved lazily —
two docker calls that a session with a usable cwd never pays for.

cwd and MCP injection are now independent reasons to rewrite: gating the
cwd fix on having MCP servers would have left the plainest agent, with no
MCP at all, as the only one unable to open a session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
…ntries

The image carries every harness in the catalog, so a container can run any
of them. Pinning the choice to the spec meant one Buzz harness entry per
agent; an override means `hive (claude)`, `hive (codex)` and so on can all
be the same hive-acp, and Buzz's harness picker stays the harness picker.

Model selection needs nothing here. A custom harness definition carries no
model env var, and selecting Opus reaches the harness anyway — Buzz drives
it over ACP's own config options. So each entry reports the models its
harness really supports, and hive never maintains a merged catalog that
would rot on every upstream release.

What the override does NOT move is credentials: hived provisions the
container from the spec, so only the spec harness's credential is present.
Overriding to a harness the container cannot authenticate stays allowed —
refusing would make it impossible to start a container in order to log in —
but it now says so at startup, naming the harness, the missing env vars and
the two ways out. Verified: overriding uni to codex prints that warning and
then fails with exactly the predicted `Authentication required`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
Subscription auth is how these tools are normally used, and for codex that
is auth.json on disk rather than a key in the environment — an API key is
the fallback, not the norm. The catalog could only describe the env form,
so anything reasoning about "is this harness authenticated" was blind to
the common case.

Caught by its own false alarm: overriding uni to codex warned that no
CODEX_API_KEY / OPENAI_API_KEY was set, and codex then authenticated from
auth.json and listed GPT-5.6. A warning that fires on a working container
is worse than no warning, because it teaches the reader to ignore the line
that will one day be true.

`credential_file` records where each harness reads that file, absolute and
under the state volume — a credential written anywhere else is destroyed on
the next recreate, after which the agent silently reverts to
unauthenticated. hive-acp now checks both forms before warning; verified
both ways, with codex silent and goose still reporting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
One key per harness id meant every agent on a box shared one subscription,
which is wrong in both directions. An agent running on someone else's
tokens had no way to say so, and a second subscription — the obvious move
when the first one runs out — could not be expressed at all. Swapping a
credential meant overwriting the one every other agent was using.

`[harness] credential` names the broker key, defaulting to `harness/<id>`
so the common case stays untyped. Rotating is then `hive secret put`
against a different key, and which agent uses which is a spec line rather
than a global.

Auth mode still wins: naming a key does not resurrect the demand that
`auth = "interactive"` removes, since a harness logged in inside the
container holds its credential in the state volume whatever the key is
called.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
HIVE_AGENT named the wrong thing, and the name hid a real bug. It picks a
container — image, state volume, network, credentials, MCP servers — and
says nothing about who the agent is: in environment mode the container has
no Nostr key at all, since buzz-acp holds it on the host and Buzz strips it
from the harness environment before spawning.

Meanwhile "agent" is exactly what Buzz calls the entity that *does* have the
identity. So several Buzz agents pointing at one HIVE_AGENT were silently
one container sharing sessions, skills and credentials, and the variable
read as though it were per-agent.

HIVE_AGENT still works, with a warning rather than a break. It is written
into harness definitions that already exist, and a rename that takes a
running agent offline to make a point is not an improvement.

Also corrects the module header: the provider seam is not "hive competing
with Buzz over location" but a different job entirely. The deploy payload
carries agent_command and env_vars verbatim, so a provider that starts
buzz-acp on another host spawns hive-acp there with HIVE_ENV intact — the
two seams compose rather than conflict, which is the whole argument for
sitting in the harness one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
grok uses GROK_HOME for BOTH its install and its state, and it writes there:
session storage, settings, and auth.json — which it reads from GROK_HOME
rather than from ~/.grok. Pinning it read-only at /opt/grok, to stop every
agent re-bootstrapping its own 127 MB binary, made grok unusable:

  session search bootstrap failed: unable to open database file:
    /opt/grok/sessions/session_search.sqlite
  No credentials found: no login token and no model api_key/env_key
  → session/new fails FS_PERMISSION_DENIED (os error 13)

The credential half is the trap. A correct `grok login` writing
~/.grok/auth.json is invisible when GROK_HOME is set, and the error that
surfaces names a filesystem problem — so the obvious next move is to chase
permissions rather than the credential path. Delivering auth.json as a hive
file credential to ~/.grok looked right and changed nothing.

Now the install stays shared at /opt/grok and GROK_HOME points into the
per-agent state volume, with the binary symlinked in by the entrypoint:
writable state per agent, one 127 MB copy on disk. The install-time ENV is
kept so the postinstall still bootstraps into /opt/grok, plus a `test -x`
so a future npm change that skips it fails the build rather than shipping
an image whose grok is missing.

Verified against the running container: with GROK_HOME writable, session/new
returns a sessionId instead of FS_PERMISSION_DENIED.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
/home/agent/work is where an agent actually writes — hive-acp redirects the
client's cwd there. It was a plain directory baked into the image, so it sat
on the container's ephemeral layer and every recreate deleted it, while the
agent's sessions and credentials survived on the volume next to it. A spec
edit is enough to trigger a recreate.

An empty state/work already existed on the volume, unused: someone knew it
belonged there and wired up the other one.

Now linked into the volume by the entrypoint, and NOT created in the image —
`link_state` refuses to replace a real directory, so leaving the mkdir in
would have silently defeated the fix.

smoke.sh gains a persistence section asserting every path an agent writes to
resolves onto the state volume. Two were wrong at once — this and GROK_HOME —
and both read fine in the Dockerfile; the failure only shows up one recreate
later, with no error anywhere. Checking "does the harness answer ACP" was
never going to catch it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
A non-interactive `ssh host 'hive ...'` — exactly how buzz-backend-hive
deploys — gets PATH=/usr/bin:/bin:/usr/sbin:/sbin, with neither Homebrew nor
/usr/local/bin on it. Bare `docker` then failed from inside the wrapper with

  /Users/uni/.local/bin/hive: line 47: exec: docker: not found

which reads as hive being broken rather than as a login-shell difference,
and only ever appears on the deploy path — never when a human runs the same
command in a terminal.

Same candidate list as hive-acp's find_docker and DockerBackend::discover,
with HIVE_DOCKER as an override and an error naming where it looked.

Found while checking that a laptop can actually deploy to this box: `hive`
itself was also missing from that PATH, which is a host setup problem rather
than a hive one — ~/.zshenv, since `ssh host cmd` sources it and nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
Two blockers, both only visible by running the real Buzz 0.5.0 payload
through the provider rather than reading the code.

1. The spec was installed with `sh -c 'mkdir -p DIR && cat > DIR/x.toml'`,
   which assumes the spec directory is a path on the machine ssh lands on.
   Where hived runs in a container — which on macOS and Windows it must —
   /etc/hive/agents is a VOLUME, and the redirect fails:

     mkdir: /etc/hive: Permission denied

   The asymmetry was the clue: `hive secret put` in the same deploy
   SUCCEEDED, because it goes through the CLI, which knows where the daemon
   lives. So the spec goes through the CLI too. New `hive spec-put <name>`
   reads a spec on stdin, validates it, and installs it write-then-rename —
   hived watches that directory, and a half-written file is a spec it will
   parse and reject. One code path now serves a native daemon, a
   containerized one, and a remote one.

2. Buzz's deploy payload carries `private_key_nsec` but NOT the pubkey, so
   the provider wrote `pubkey = ""`. That is not cosmetic: hived uses
   identity.pubkey to detect two specs deploying one identity to the same
   relay — which answers every mention twice and charges the owner twice —
   so an empty one makes every provider-deployed agent collide with every
   other, and hived holds all of them.

   Derived from the nsec now. bech32 is decoded here rather than added as a
   dependency; the checksum is the part that matters, since a mistyped key
   must fail loudly rather than derive a plausible wrong identity.

Upstream would be better for (2): the desktop has record.pubkey and could
send it, and no provider should need secp256k1 to learn which agent it is
deploying.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
…arly

Every remote failure surfaced to the desktop as a bare "installing the agent
spec". The reason — validation, a missing binary, an ssh refusal — sat one
level down in the anyhow chain, discarded by `e.to_string()`, which prints
only the outermost context. `{:#}` prints the chain. The desktop shows this
string verbatim and there is nowhere else to look, so this was the
difference between a diagnosable failure and a shrug.

With it visible, the first real deploy then reached the container and
crash-looped:

  idle_timeout (900s) must be less than max_turn_duration (600s)

buzz-acp enforces that pair; hive-spec did not, so `hive validate` passed,
hived reconciled, and the reason was only in `docker logs`. Validated now,
so it fails at deploy with the reason attached.

Verified end to end afterwards: a deploy over ssh writes the spec, hived
reconciles it, the container is created and buzz-acp starts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
buzz-backend-hive occupied Buzz's provider seam, which answers "where does
this agent run", and then did hive's job there — resolving harnesses,
writing specs, storing credentials. So selecting hive as the harness AND
hive as the provider produced nonsense, and the two dropdowns used one word
for opposite things.

That asymmetry was ours, not Buzz's. A provider whose only job is location
composes with harness=hive exactly as it composes with harness=claude,
because the deploy payload carries agent_command and env_vars verbatim. The
replacement is a generic host provider, which hive should not be shipping.

Deleted. What makes the harness seam sufficient on its own:

* identity is now OPTIONAL on a spec, required only in mode = "relay". An
  environment has none by design — buzz-acp holds the key outside the
  container — so every generated spec was carrying a pubkey and relay url
  nothing reads. hived skips identity-less specs in duplicate detection;
  treating a missing identity as a shared empty key would have made every
  environment a duplicate of every other and held all of them.

* hive-acp CREATES an environment when HIVE_ENV names one that does not
  exist, so an agent needs no hand-written TOML on the host. The generated
  spec is a harness id and a mode, because there is nothing else to invent:
  identity belongs elsewhere and credentials are named, not stored, here.

* HIVE_ENV is no longer pinned in the harness definition. That pin was the
  reason to point every agent at one spec, which silently put them in ONE
  container sharing sessions, skills and credentials. Set per-agent, each
  gets its own container and its own state volume.

* A held agent now says why. hived holds an agent whose credentials are
  missing and logs the reason where nobody is looking, so the caller waited
  out its patience and reported a timeout naming nothing. hive-acp compares
  requirements against the broker and prints the key to store.

Verified from a clean slate: HIVE_ENV=research with no spec, no container
and no volume creates all three, reports its models, and lands on its own
hive-research-state rather than sharing uni's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
A generated environment defaulted to broker+env, which is right for claude —
a token in CLAUDE_CODE_OAUTH_TOKEN — and wrong for codex and grok, whose
normal auth is a JSON file. Selecting "Codex (hive)" therefore produced a
spec demanding `harness/codex` while what the box held was `codex/auth`, so
hived held the agent for a credential nobody was going to create and it
simply never started.

hive-acp now asks the broker what exists and, when the catalog says the
harness reads a file, emits `auth = "file"` plus the [[file]] block pointing
at it. Both key spellings are tried: `harness/<id>` is what hive asks for by
default, `<id>/auth` is what a file credential gets called when stored by
hand.

Also publishes BUZZ_HOST_UNIT as a fallback for HIVE_ENV, so a deployed
agent needs no environment variable set by hand before it can run. Asking
for one invited the mistake it was meant to prevent: reusing a neighbour's
value and silently sharing that container's sessions and credentials.

Verified from a MacBook against uni, each harness auto-provisioning its own
environment and reporting its own models:

  codex  gpt-5.6-sol[low..ultra], gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, gpt-5.4
  grok   grok-4.5
  claude default, sonnet, opus, haiku

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
… name

Dropping the HIVE_ENV pin from the harness definition fixed isolation and
broke the model list. In the create-agent dialog the desktop probes the
harness for its models — and at that moment nothing has named the agent,
because the agent does not exist yet. hive-acp refused with "HIVE_ENV is not
set" and the dropdown stayed empty, with nothing the user could set to fix
it: pinning a value in the definition is precisely what put every agent in
one shared container.

So discovery falls back to a scratch environment. One PER HARNESS, not one
shared: `session/new` needs that harness's own credentials before it will
answer, so a single probe container would report claude's models for every
entry, or fail outright for the ones it cannot authenticate.

Resolution order is now explicit → supervisor → discovery scratch:

  HIVE_ENV          set by hand, always wins
  BUZZ_HOST_UNIT    per-agent, published by the supervisor
  probe-<harness>   shared, and named so it reads as scratch

Verified from a MacBook with nothing but HIVE_HARNESS set, the way the
desktop calls it:

  claude  default, sonnet, opus, haiku
  codex   gpt-5.6-sol[low..ultra] and 24 more
  grok    grok-4.5

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
Deployed as claude, codex and grok, the first two worked and grok never
answered. It was delivered its credential correctly — 1733 bytes at 0600,
present immediately after create — and had deleted it by the time it
finished starting, leaving only auth.json.lock.

The stored token was 24 minutes past its expires_at, and grok's refresh half
rotates on use: whichever grok refreshes first invalidates every other copy,
so the container's grok could not renew a token the host's grok had already
rolled. It gave up and removed the file.

Injecting a snapshot is therefore worse than injecting nothing. The agent
ends up unauthenticated while its spec records that a credential was
delivered, and the only symptom is an agent that never replies.

`credential_file_rotates` marks a harness that owns and rewrites its own
credential. Auto-provisioning gives those `auth = "interactive"` and prints
the one command that fixes it, rather than `auth = "broker"` — which would
hold the agent forever waiting for a key that by design lives in the state
volume, and hold it so hard the container needed to log in never starts.

codex is deliberately not marked: its auth.json records a last_refresh
rather than a hard expiry, and it is still working in a container hours
after being injected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
A generated environment had no MCP servers at all. Every agent created from
the desktop therefore came up unable to reach the vault, while the one
environment somebody had configured by hand could — so the feature appeared
to work and silently did not for anything new. Three real agents were
created before anyone noticed.

/var/lib/hive/defaults.toml is appended verbatim to every environment
hive-acp creates, and validated by `spec-put`, so a broken defaults file
fails at creation with a reason rather than producing an agent that quietly
lacks its tools.

Deliberately NOT in the spec directory: hived reads every *.toml there as an
agent, so a defaults file next to the specs would be reconciled as one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
The hived runtime is debian:bookworm-slim with a docker binary copied in and
nothing else — no curl, no wget, no openssl, no python. The OAuth flow shells
out to curl, so the subcommand died at its first request with

  Error: probing https://…/mcp
  Caused by: No such file or directory (os error 2)

which names a file and does not name curl. Written and tested against a host
build, where curl is simply always present; it had never once been run where
it actually ships.

curl and ca-certificates are installed now, with `curl --version` in the same
layer so a base image that stops shipping it fails the build rather than the
feature.

Also: omitting --scope now sends NO scope parameter rather than requesting
everything the resource advertises. The consent screen is what should decide
— it knows which scopes exist, which this user may have, and how to ask.
`scopes_supported` describes what the RESOURCE understands, not what the
server will grant, so naming them from it silently caps the token at the
published set and cannot obtain anything outside it. Which is exactly the
case here: the metadata lists vault:read/write/admin and nothing about
account scopes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
The image had git, curl, jq, node, npm and tar. No ripgrep, no python, no
compiler, no ssh, no less, no unzip, no sqlite. An agent in a container
cannot apt-get its way out of that: it has no root, and anything it did
install would be destroyed on the next recreate. So the image has to
anticipate, and not anticipating fails quietly — a harness that shells out
to `rg` falling back to something slower, a native npm module that will not
build, `git log` with no pager.

Added: ripgrep, fd, python3 + pip + venv, build-essential, pkg-config,
openssh-client, rsync, less, vim, nano, tree, file, unzip, zip, sqlite3, and
gh from GitHub's own release since Debian does not package it.

Deliberately NOT added: Go, Rust, and other language toolchains. They are
large, and an agent that needs one wants a specific version rather than
whatever Debian happens to ship.

Each tool is asserted present in the same layer that installs it, so a base
image that stops shipping one fails the build rather than producing agents
that quietly cannot search. smoke.sh passes 9/9 ACP and 9/9 persistence.

Also reverts the scope default. Letting the consent screen decide is right in
principle, but a server that receives no scope may grant nothing — and
Parachute does exactly that, offering no choices and returning a useless
token. Advertised scopes by default; `--scope ""` for a server whose consent
screen should own it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
Typing "uni" into the HIVE_ENV field left behind environments named `u` and
`un` — each with a spec, a container and a volume, all created within the
same second. The desktop re-probes the harness on every keystroke, and
provisioning fired on every probe.

Cheap to create is exactly what made it bad: nothing failed, nothing warned,
and the clutter only surfaced when someone listed containers hours later.

A deployed agent carries BUZZ_HOST_UNIT — its supervisor named it. A probe
does not. So a probe naming an environment that does not exist now falls
back to the shared discovery environment and says so, rather than conjuring
one; creation is reserved for something that has actually been deployed.

Also collapses the spec read into `read_spec`, which was three near-copies of
the same local-then-daemon lookup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gw5rgVt1CQ8EYrLpZtQjdM
@unforced
unforced merged commit 0e37e66 into Unforced-Dev:main Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants