Watch
0
0
Fork
You've already forked hyperhive
0
Commit graph

5,336 commits

Author SHA1 Message Date
atlas
baefb32bd5 Merge forge/main into atlas/5034-s4-remove-hive-model 2026-10-11 03:42:22 +02:00
atlas
3c03d82858 hive-agent: compact tests set the harness dir like the other Bus tests
The two report_compact tests build a Bus, which resolves the events db under
HYPERHIVE_HARNESS_DIR and panics when it is unset, as in the nix test
sandbox. Add a test-only paths::with_harness_dir that points the variable at
a temp dir under a lock, and use it in both the new tests and the existing
swarm_inference Bus test, which set the variable unguarded.
2026-10-11 03:36:34 +02:00
atlas
2c4a3c2fa7 hive-agent: a failed turn-end /compact keeps the turn's own compacted flag
The turn-end path returned the /compact's success as the turn's outcome, so a
turn that had already compacted (reactively or per policy) was recorded as not
compacted when its queued /compact then failed. The outcome now folds the two
with `||`, via `with_turn_end_compact`, which the tests pin.
2026-10-11 03:36:34 +02:00
atlas
7bb1959a94 hive-agent: report a failed mid-turn /compact instead of recording it as compacted
The turn-end /compact (operator button or the agent's `compact` tool) dropped
the compaction error with `let _ =`, emitted no note, and returned
`Ok(true)`, so the turn was recorded `result_kind = "compacted"` although
nothing compacted. The idle path already printed `/compact failed: …`.

Both paths now go through one `report_compact` helper that emits the done /
failed note; the turn-end path returns whether the compact succeeded.
2026-10-11 03:36:34 +02:00
atlas
a200935216 hive-agent: extra web proxies forward every repeated response header
Both proxy paths copied upstream response headers with HeaderMap::insert,
which replaces earlier values of the same name, so an upstream sending two
Set-Cookie (or Link, Vary, ...) headers reached the browser with only the
last one. Use append in both loops. Tests cover the http and unix: paths.
2026-10-11 03:12:18 +02:00
atlas
f3860f3497 hive-claude 0.1.4: the idle watchdog stops claude with SIGINT
hive-claude 0.1.4 (hive-claude #28) sends the idle watchdog's signal as
SIGINT to claude's process group instead of SIGKILL. It then waits for
claude to exit, with no SIGKILL fallback. claude stops the commands its
Bash tool started when it shuts down on SIGINT, and a SIGKILL left them
running. This is the last SIGKILL of a claude process from #4999.

hive-agent's `idle_timeout` -> `Error::IdleTimeout` -> `TurnError::ApiStall`
path is unchanged. The difference is that the error now arrives only
once claude has exited. A claude that ignores the SIGINT keeps the turn
open until it is killed by hand or the unit stops.

The Cargo.lock change is only hive-claude's version and checksum. A plain
`cargo update -p hive-claude --precise 0.1.4` also moved seven crates'
Windows-only `windows-sys` edges from 0.61.2 to 0.52.0, unrelated to this
bump. The minimal lock passes `--locked`.

The turn-loop and web-UI docs, and hive-agent's watchdog docs and stall
note, now say SIGINT instead of "killed". That includes the ACP spots
that the previous commit's `Drop` change made stale.
2026-10-11 02:44:31 +02:00
atlas
43e69d345e subagents, ACP: stop with SIGINT only, so claude ends its own tool commands
claude's Bash tool starts every command in a new session, so a signal to
claude's process group never reaches it. claude stops those commands
itself when SIGINT makes it shut down. A SIGKILL leaves them running,
re-parented to init. Per the ruling on #4999, every stop is SIGINT and
nothing escalates.

- Subagent `interrupt` always sends SIGINT. The `force` argument is gone
  from the MCP tool and from `session::interrupt`. A caller that still
  sends `force: true` gets SIGINT; the argument is ignored.
- `interrupt` used to free the session name before it signalled, so a
  `continue` could start a second claude on the session while the first
  was still exiting. The `running` map now holds a three-state `Slot`
  (starting, running, stopping). An interrupted claude turn is
  `Stopping` until the turn task's own `finish_turn` clears it. `status`
  says "stopping", and `start`/`continue` on that name are refused.
  `interrupt` still returns at once. The ACP arm is unchanged: its name
  is freed when `session/cancel` is sent.
- ACP `Connection` drop sends its process group SIGINT, not SIGKILL, and
  the spawn no longer sets tokio's `kill_on_drop`, which SIGKILLed the
  leader after the `Drop` body. The test fixture's background grandchild
  resets SIGINT to default, since `&` starts it ignored.

Tests: a fake claude that records SIGINT, interrupted, fails with
`cancel(true)` (the old `force: true` path). An interrupted fake claude
that keeps running keeps its name claimed and `continue` is refused,
which fails when the slot is freed before the signal. A fake ACP agent
that records SIGINT, dropped, fails with SIGKILL in `Drop`, and also with
SIGINT in `Drop` but `kill_on_drop(true)` left in place.

Not changed: hive-claude's idle watchdog (still SIGKILL, separate
hive-claude change), the units' `KillSignal`, `machinectl kill`, and the
login helper.
2026-10-11 02:44:31 +02:00
atlas
028c283ff9 claude runtime: interrupting a session kills every process it started
hive-claude 0.1.3 spawns claude as the leader of its own process group,
and its `Cancel::cancel` and idle watchdog signal that group instead of
claude's pid alone. Commands claude started (a `nix build`, a test run)
used to survive a subagent `interrupt` and keep running.

Bump hive-claude to 0.1.3. The subagent `interrupt` call site needs no
code change: `Cancel::cancel(force)` kept its signature.

The harness's `/cancel` and `/logout` did not go through hive-claude:
they ran `kill -INT <pid>` on the claude child they found in /proc. They
now SIGINT the group that child leads, through `libc::kill`. A `claude`
child that leads no group (the `claude plugin` calls) shares the
harness's group and is still signalled alone.

Tests: an interrupted subagent's grandchild dies, for both SIGINT and
`force`, and fails on hive-claude 0.1.2; a harness cancel stops a
turn's grandchild, and fails when only the pid is signalled.
2026-10-11 02:44:31 +02:00
atlas
63ab0bbd21 hive-c0re: clear O_NONBLOCK on the snapshot-store socket before btrfs send gets it
The tokio socket stays non-blocking after into_std, and the flag is
shared with hive-priv through SCM_RIGHTS, so btrfs send wrote into a
non-blocking socket.
2026-10-11 02:42:45 +02:00
atlas
466e32b69f hive-c0re: close build-log rows a dead process left in flight
A build_logs row is opened with finished_at NULL and closed only by the
process that runs the build. If hive-c0re stopped in between, the row stayed
open for good: the dashboard listed it with no status, its log stream never
sent `done`, and the retention sweep, which skips open rows, never deleted it.

Coordinator::open now closes every open row as `fail` and appends a line
saying hive-c0re stopped. It runs before this process starts any build, and
only there: BuildLogs::open itself leaves rows alone so a second opener can't
close another process's live rows.
2026-10-11 02:37:08 +02:00
atlas
a7e640514c Merge forge/main into atlas/5034-s4-remove-hive-model 2026-10-11 02:09:05 +02:00
atlas
4719395b31 docs, comments: drop references to the removed model/effort picker
subagent.md no longer says the web UI's model picker offers
availableModels, and the comments in Dropdown, ComponentsPage and
WantedMenu no longer point at the deleted per-agent Picker.
2026-10-11 01:54:23 +02:00
atlas
ed271e655f agents: remove the hive-level /effort control too
mara on #5034: "effort shares /models fate". The per-agent web UI no
longer sets effort: the `/effort` slash command, the effort picker,
`POST /api/effort`, `Bus::set_effort`, the `hyperhive-effort` writer,
`EFFORT_LEVELS`/`is_valid_effort` and the ACP effort-list payload are
gone, and `lib/modelEffort.ts` with them. The effort chip is
display-only and shows its source (`effort_source` in `/api/state`);
on ACP it stays hidden while the session offers no effort level.

The read side is unchanged: with no swarm value an agent runs an
existing `hyperhive-effort` file, else its agent.nix effort, else
medium. Existing files are not touched.

Refs #5034
2026-10-11 01:28:44 +02:00
atlas
594693f4c7 agents: remove the hive-level /model control; the swarm editor sets the model
The per-agent web UI no longer sets the model: the `/model` slash
command, the model picker, `POST /api/model`, `Bus::set_model` and the
`hyperhive-model` writer are gone. The model chip is display-only and
shows where the model comes from (`model_source` in `/api/state`). The
model is set on the swarm (swarm UI agents page, `swarmctl agent
inference set`).

The read side is unchanged: with no swarm value an agent runs its
agent.nix model, else an existing `hyperhive-model` file, else haiku.
Existing files are not touched.

With the picker gone, its model list goes too: `available_models` in
`/api/state`, the ACP model-list filter, and the harness's global
`HIVE_AVAILABLE_MODELS`. The subagent daemon keeps its own
`HIVE_AVAILABLE_MODELS` from mcp.nix, and the `availableModels`
assertion stays.

`/effort` (command, picker, route, file) is unchanged.

Refs #5034
2026-10-10 23:11:53 +02:00
atlas
0472ed3ab5 hive-agent: test the mark-done handler's 500 through an injected dial
post_mark_todos_done's body moves into mark_ids_done, which takes the
per-id dial as a closure, so tests cover the 500 and 200 responses without
reaching the live in-agent socket.
2026-10-10 21:27:23 +02:00
atlas
9a702d954e hive-agent: web UI "mark done" reports a failed todo store instead of success
post_mark_todos_done dropped every dial failure and Response::Err and
answered 200 {"acked":0}, so the todos panel showed "marked done" and
cleared its checkboxes while the todos stayed. Any failing id now fails
the request with a 500 naming how many ids failed; the panel already
shows non-OK bodies as "failed: ...".
2026-10-10 20:43:20 +02:00
atlas
46e9966e17 docs/review-classifier: cost comes from OTEL labels, not the swarm UI 2026-10-10 19:30:25 +02:00
atlas
00ce00f33d docs: narrow the review-classifier runtime refactor to the per-turn seam 2026-10-10 18:56:30 +02:00
atlas
dcd005623e docs: make review classifier page generic over swarms
Drop the swarm-specific reviewer agent name and the hyperhive-repo
approval count from the architecture page and its index line.
2026-10-10 18:54:02 +02:00
atlas
c0af8d1fd3 docs: review classifier architecture (planned)
Add docs/swarm/review-classifier.md, the high-level architecture of the
planned PR review classifier pipeline, written as the full target shape
under a "planned, not implemented yet" banner. Link it from docs/README.md
and the docs/swarm/README.md piece list.

The page reflects the epic description and the operator's answers in the
epic thread; points still open are listed in its "Still open" section.
The banner names the epic by title rather than by number because the
tracker-tag lint rejects issue references in tracked files.
2026-10-10 18:53:41 +02:00
iris
87aeed32a7 docs: swarm UI inference section in active voice 2026-10-10 18:51:55 +02:00
iris
0d04bd7295 swarm-ui: per-agent inference panel and editor on the agents page
The agent detail panel shows the model and effort an agent runs on with
where each comes from, plus the swarm's subagent model list. An edit
dialog sets the swarm value (PUT) and a clear action removes it (DELETE).
Reads the configured/effective view, and shows the stored values alone
until an agent has reported what it runs on.
2026-10-10 18:51:55 +02:00
atlas
0f85c93848 hive-agent: test the turn-state refresh's re-send decision
Whether a header goes out was split between `run()` (clear `last` on a
refresh) and `publish()` (skip a payload equal to `last`), and only the
refresh cadence was tested: deleting the clear kept every test green.
The decision is now the pure `due(trigger, last, payload)`, which
`publish()` calls, with tests that an unchanged payload is not sent on
an event, is sent on a refresh, a changed one is sent on either, and a
refresh that then fails to send is re-sent by the next event.
Behaviour is unchanged.

Refs #5034
2026-10-10 17:37:23 +02:00
atlas
7364ccdc98 swarm-controller: GET inference answers configured: null without a key
`GET /api/agents/{name}/inference` answered `"configured": {}` both for
an agent with no key in `agent-inference` and for a stored value with no
fields, so a reader could not tell "the swarm sets nothing" from "a
setting with every field unset". It now reads the key as
`Option<AgentInference>`: an absent or deleted key is `null`, a stored
`{}` stays `{}`. The shared `schedules::read` helper is unchanged.

`swarmctl agent inference get` prints "no swarm setting; the agent runs
on its own model and effort" for `null`; `set` merges into an empty
value in that case, as before.

The agent itself still treats no key and `{}` the same.

Refs #5034
2026-10-10 17:37:23 +02:00
atlas
47bd83054f swarm-controller: refuse an inference model name that starts with "-"
`check_model` accepted a value like `--dangerously-skip-permissions`,
and the stored model ends up in the agent's `claude` argv. A PUT whose
`model`, or any `subagent_models` entry (same validator), starts with
`-` is now a 400 with the same problem+json shape as the other model
checks. A dash inside a name (`claude-opus-4-8`) is still accepted.

Raised by argus on #5084.

Refs #5034
2026-10-10 17:37:23 +02:00
atlas
19a045a401 hive-agent: re-send the turn-state header once a minute
The header on `$SWARM.agent-state.<agent>` went out only when a bus event
changed it, and the core subject keeps nothing for a late subscriber. An
idle agent therefore said nothing to a reader that attached after its
last change: swarm-controller after a restart showed `effective: null`
for that agent until its next turn, which can be hours.

The publish loop now also wakes every 60 s and re-sends the current
header through the same `publish` path (clearing the dedup state first).
Transitions still go out as they happen. Same subject, same grant.

An agent whose queue connection is down now logs `swarm agent state:
publish skipped` once a minute even while idle, so the
`hh-swarm-agent-state-publish-skipped` alert also fires for idle
disconnected agents, not only busy ones.

Refs #5034
2026-10-10 17:37:23 +02:00
atlas
d723fac0be swarm: show each agent's effective model and effort, and where they came from
Each agent now reports the model and effort it runs on, and where each
comes from (`swarm`, `agent_nix`, `local_file` or `default`), in its
turn-state header on `$SWARM.agent-state.<agent>`: `effort`,
`model_source` and `effort_source` join `model`. The shared
`swarm_queue_client::agent_inference::{Effective, Source}` types spell
those fields for both ends.

The Bus keeps a source next to the model and effort. `Bus::new` takes it
from the local fallback chain (`harness_state::local_model` /
`local_effort`, which now return the source with the value), a `/model`
or `/effort` pick sets `local_file`, and the swarm poller sets `swarm`
or, on a clear, the local chain's source. A swarm value equal to the one
the agent already runs is now applied once, so the source moves to
`swarm` and the header is republished; before, `next()` skipped it as
unchanged.

swarm-controller subscribes to every agent's header with the reader
grant it already holds for the state relay, and keeps the latest
inference fields per agent in memory (`inference::Reported`).
`GET /api/agents/{name}/inference` now answers
`{configured, effective}`, `effective` being `null` until the agent's
first header with these fields since the controller started.
`swarmctl agent inference get` prints both; `set` reads `configured`.

Refs #5034
2026-10-10 17:37:23 +02:00
atlas
6982b2ec14 swarm-secret-publish: drop RemainAfterExit so the path unit re-runs it
swarm-secret-publish.service was Type=oneshot with RemainAfterExit=true.
After its first successful run it stayed active (exited), so every later
start from swarm-secret-publish.path (PathChanged on authelia's client
secret dir) was a no-op: a secret minted or replaced after that first run
never reached the store, and nothing logged it.

Without RemainAfterExit the unit goes inactive after each run, so each
PathChanged event starts a fresh publish. Same shape as
swarm-bao-forge-publisher in hive-forge/default.nix. No unit requires,
wants, orders after, binds to or is part of swarm-secret-publish.service,
so nothing depended on it staying active.

This also covers the first-boot race where the publisher runs before
authelia's first mint: the mint writes the file, and the path unit now
starts the publisher again.

A module-eval case pins it: the path unit targets the service, the
service is a oneshot, and RemainAfterExit is unset or false.

Closes #5032
2026-10-10 14:08:43 +02:00
atlas
5658361096 docs: describe the forge's OIDC secret as fetched from bao, not copied or hand-placed
sso.md still described a host-side copy unit and an operator-placed file
for a forge elsewhere; secrets.md's client-secret row still named
sso.clientSecretFile as the operator's route. The clientSecretFile
example now shows the path swarm-bao-forge-oidc writes.

Refs #5015
2026-10-10 14:07:31 +02:00
atlas
f6ca5f1433 forge: fetch the OIDC client secret from bao instead of a host-side copy
The forge's login source read its OIDC client secret from a file that
`hive-forge-oidc-secret` copied out of authelia's container tree. That
unit only existed where authelia ran on the forge's host (`ssoLocal`),
so a forge anywhere else needed the file placed by hand.

The publisher on authelia's host now carries the forge's client secret
to `secret/swarm/services/<forge client id>/oidc/client`, like every
other swarm service's. On the forge's host `swarm-bao-forge-oidc`, a copy
of `swarm-bao-matrix-oidc`, reads it under the forge publisher's leaf
(the forge host's one store identity, whose policy gains read on that
one path) and writes the file `forgejo-sso-source` reads, re-running the
login source when the value changes. It retries on the shared
store-retry shape while the store is down or the secret is not yet
published.

`hive-forge-oidc-secret` is removed, `clientSecretFile` defaults to the
in-container path wherever the forge runs, and the `ssoLocal` gate on
the login source's resolver ordering goes with it: the login source
fetches the issuer's discovery document on every forge host.

Refs #5015
2026-10-10 14:07:31 +02:00
atlas
6bf4792d61 docs/integrations: title the forge and matrix pages as swarm-level 2026-10-10 14:06:43 +02:00
atlas
295e8af43a docs: the agent config meta-flake input is the local applied mirror, not forge git+http
config-changes.md § Meta flake, hive-c0re nix comments and the meta.rs/exec.rs
doc comments still described the declared agent-<n> input as a forge-hosted
git+http repo; meta.rs renders git+file://<applied>.
2026-10-10 14:06:43 +02:00
atlas
66c1a564a4 docs/integrations, hive-c0re nix: the agent config input is the local applied mirror
forge.md said the meta flake fetches agent-configs over git+http through a
credential helper. The input is git+file://<applied> (meta.rs), and
hive-c0re's own git reads use forge::reader_token(). Fix the same claim in
the comments above the hive-c0re gitconfig and the credential helper. No
code change.
2026-10-10 14:06:43 +02:00
atlas
0441bca2c9 docs: title the knowledge repo and the forge index entry as swarm-level
The knowledge repo lives on the swarm's forge, shared by every hive; the
forge is the swarm's, not a hive's own.
2026-10-10 14:06:43 +02:00
atlas
81f9aed7a3 docs/integrations: correct what runs at hive vs swarm level
knowledge.md: swarm-controller's site-admin account also pushes; the
clone no longer carries a token, the .git overlay hides history.
forge.md: the dashboard link follows swarm.forge.publicUrl; the poller's
HIVE_FORGE_URL gate is only unmet outside a nix-built hive.
matrix.md: the homeserver is the swarm's, not the hive's.
2026-10-10 14:06:43 +02:00
atlas
f27818340d hive-subagent-mcp: test that start and continue apply the swarm model list
`Reader` gets a test-only fixed source that counts its reads, and `State`
takes the agent's own list as a `fn` (`available_models` outside tests), so
`start` and `continue_` can be driven through `State::allow_model` without
a queue or a process-wide env change. The new tests pin that both calls
refuse a model outside the swarm list ahead of `reserve`, accept a model
the swarm lists but the local list lacks, and read nothing for a call
without a `model`.

Refs #5034 (S5)
2026-10-10 13:58:47 +02:00
atlas
90e6b984eb hive-subagent-mcp: check subagent models against the swarm's subagent_models
A `start` or `continue` naming a `model` now reads this agent's key in the
`agent-inference` bucket and, when `subagent_models` is set, checks the
model against that list instead of HIVE_AVAILABLE_MODELS (agent.nix
`availableModels`). An unset field, an absent key or an absent bucket
keeps today's check. A read that fails or takes over 2 s falls back to
HIVE_AVAILABLE_MODELS with a warning, so a subagent start never waits on
the queue. A call without a `model` is not checked and does not read.

The read uses the agent's own queue credential through swarm_term's
connect, and the one direct-get subject the agent is granted in that
bucket. The refusal names the list and that it came from the swarm.

Refs #5034 (S5)
2026-10-10 13:58:47 +02:00
atlas
971426a1c0 hive-c0re: move the applied mirror before the meta-update cascade relocks it
run_meta_lock relocked each cascade agent's `agent-<name>` input and only
then fast-forwarded `applied/<name>/main` to the forge config main. The
declared input has been the applied mirror (`git+file://<applied>`) since
b9cef950, so the relock pinned the mirror's old head; the cascade's
rebuilds run with `relock = false` and build that lock. An agent whose
mirror was behind forge main was redeployed on its old config, and the
mirror was then moved to the new head, claiming the agent current.

Fast-forward first, then relock. The fast-forward stays best-effort per
agent: a failed one logs a warn and that agent is relocked against
whatever its mirror has. The two steps move into catch_up_cascade_agents,
which takes them as injected effects (the advance_applied_main pattern in
actions.rs) so the order is unit-tested without nix or a forge.

The stale comment claiming the relock ran against "the forge URL" is
replaced by the actual constraint on the helper's doc.

Closes #5090
2026-10-10 10:25:36 +02:00
atlas
e4e62be506 module-eval/hive-otel: pin the fetch reviving a start-limited collector
On a first boot the secret file does not exist until the first fetch
succeeds, so the collector fails `LoadCredential` into its start limit,
which leaves it `failed`. `refresh_unit` already handles that: it sees
`is-failed`, runs `reset-failed` (which clears the start-limit counter)
and starts it. This case pins that arm, the empty-ActiveEnterTimestamp
fallback that makes the file count as newer, and that the collector only
wants the fetch rather than requiring it.

Refs #5015
2026-10-10 01:22:32 +02:00
atlas
9ef4f55113 hive telemetry collector: keep the secret wait for an operator-named file
The previous commit dropped `hive-otel-client-secret` and the collector's
`requires`/`after` on it for every hive. Only the store fetch restarts the
collector when its file lands; an operator-named `otel.clientSecretFile`
has nothing doing that, so a late file failed `LoadCredential` straight into
the collector's start limit with no way back.

The wait unit and the ordering are back for `senderAuth && !fetches`, and
absent only where `swarm-bao-hive-otel-oidc` delivers the file. The
module-eval case for a hand-named secret now also pins the wait unit and
the collector's `requires`/`after` on it, on both of its fixtures.

Refs #5015
2026-10-10 01:16:04 +02:00
atlas
7c50568db1 hive telemetry collector: fetch its OIDC secret from the store under the hive's own leaf
The hive collector's `oauth2client` secret came from a `clientSecretFile`
defaulting to authelia's own minted file on the IdP's host, with a
`hive-otel-client-secret` oneshot that only waited up to 120s for that
file to exist; any other hive had to copy the file across by hand.

`swarm-bao-hive-otel-oidc` now logs in with the hive's leaf
(`deploy.bao.clientCertFile`), reads `swarm/hives/<hive>/oidc/client`
(written by the publisher, granted by the hive leaf's
`swarm/hives/<hive>/*` read), writes it root 0400 when it differs, and
restarts the collector when the file is newer than its last start.
Retries every 30s for 24h (`lib/store-retry.nix`), like the matrix and
forge fetches. `clientSecretFile` defaults to that file wherever the hive
leaf is set and stays as an override; an overridden file renders no fetch.

otelcol-contrib 0.151.0's oauth2client re-reads `client_secret_file` on
every token fetch, but the path is `LoadCredential`'s start-time copy, so a
changed secret still needs a restart. `lib/refresh-consumer.nix` gains
`refresh_unit` for a host unit; `refresh_consumer` now calls it with
`--machine`.

nats-authelia's "consumer of the host client-secret dir" case pointed at
the collector's old default; it now reads the publisher's path unit.

Refs #5015
2026-10-10 01:10:31 +02:00
atlas
a5448c81c4 agent-modules: hand an api-key agent's subagent daemon the backend file
A claude agent with useApiKey gets ANTHROPIC_API_KEY through
backendEnvironmentFile, but only the harness unit loaded that file;
hive-subagent-daemon loaded it for ACP agents alone. The daemon's nested
claude inherits the daemon's environment (hive-claude adds to it, never
clears it), and an api-key agent has no OAuth session in ~/.claude, so
its claude subagents started with no credentials.

hive-subagent-daemon now loads backendEnvironmentFile when the agent is
ACP (unchanged) or has useApiKey set. Module-eval covers the api-key
claude arm; the existing claude arm (file set, useApiKey unset) still
asserts no EnvironmentFile.

Closes #5088
2026-10-10 01:05:50 +02:00
atlas
4edf965f05 module-eval/nats-tls: evaluate the no-URL leaf fixture with hive-c0re on
The case for a hive with a store leaf and no queue URL reads
systemd.services.hive-c0re, which only exists with the hive controller
enabled. Built with `hive`, the fixture had no such unit and
module-eval-nats-tls failed to evaluate:

  error: attribute 'hive-c0re' missing

Refs #5015
2026-10-10 01:05:33 +02:00
atlas
c2c61b6146 hive status publishing: read the hive's OIDC secret from the store, retry until it is there
hive-c0re publishes this hive's status to the swarm queue as the OIDC
client `hive-<name>`. Its secret came from
`deploy.hive-controller.statusPublish.clientSecretFile`, which defaulted
to authelia's minted file only on a host running both authelia and the
queue. Every other hive had no secret and never published, so the
controller showed it `never_reported`.

Now `swarm-secret-publish`, beside authelia, also copies each hive's
`hive-<name>` secret to `swarm/hives/<hive>/oidc/client`. Its policy
already allows `swarm/hives/*`. hive-c0re reads that path under the
hive's own store leaf, which the existing `swarm/hives/<hive>/*` grant
covers, and holds the value in memory.

The shared queue client no longer remembers a failed first connect for
the life of the process. A failed attempt is retried with a backoff
from 5s, doubling to a cap of 5 minutes; the dashboard banner stays up
until an attempt succeeds. The two status publishers wait for the
client instead of giving up on the first `None`, so a secret published
after boot is picked up without a restart.

Removed: the `statusPublish.clientSecretFile` option (both spellings,
now removed options with a message), its same-host default, its
LoadCredential, the assertion tying it to the other two coordinates,
and `swarm_queue_client::QueueConfig::from_env`, whose only caller was
this read. Publishing is now on wherever the queue URL and token
endpoint are set and the hive holds a store leaf.

Refs #5015
2026-10-10 01:05:33 +02:00
atlas
8d206f124f hive-runtime: kill an ACP agent's whole process group, not just its pid
The ACP agent was spawned with only `kill_on_drop`, which SIGKILLs the
agent's own pid. Commands the agent had started (a `nix build`, a test
run) were reparented and kept running after the agent was killed: when a
subagent run ends after `interrupt`, when an agent ignores
`session/cancel` past the grace, and when the harness drops the runtime.

The agent now leads its own process group (`process_group(0)`), and
dropping its `Connection` SIGKILLs that group. The soft path is unchanged:
`interrupt` / `Canceller::cancel` still sends `session/cancel`.

Refs #4999. The claude half needs hive-claude 0.1.3, which is not on
crates.io yet.
2026-10-10 00:06:42 +02:00
atlas
916fdb35b8 swarm: per-agent inference settings (model, effort, subagent_models) in KV
swarm-controller keeps each agent's inference settings in a new
`agent-inference` KV bucket, one key per agent, and is its only writer
(`GET`/`PUT`/`DELETE /api/agents/{name}/inference`; `swarmctl agent
inference get|set|clear`). Every field is optional. Each agent reads its
own key every 10 s through a direct-get grant on that key alone, and a
set `model` or `effort` replaces the Bus value, so the next turn runs on
it and the badge follows.

The swarm value is applied without persisting it: `Bus::set_model` and
`set_effort` are split into a persisting setter and a non-persisting
`apply_*`, and the poller uses the latter. Otherwise the swarm value
would overwrite `hyperhive-model`/`hyperhive-effort`, and clearing it, or
a restart, would no longer find the agent's own value. An unset field
changes nothing; a field cleared after being applied goes back to the
local value (`harness_state::local_model`/`local_effort`, the same
resolution `Bus::new` uses), read again at the clear.

`subagent_models` is validated, stored and shown by `get`; no agent
applies it yet.

Refs #5034
2026-10-09 23:13:13 +02:00
atlas
29d956140e hive-c0re: a deploy request for a rev applied/main already contains is a no-op
advance_applied_main refused every rev that does not descend from
applied/main, including an ancestor of it. Ancestors arrive in normal
operation: swarm-controller queues each merge's TriggerDeploy as an
independent node with no resource or edge between them and spawns each
runnable node on its own task, so two merges of one agent can publish in
either order; the meta-update cascade fast-forwards applied/main to the
forge's main on its own; and the webhook handler does not dedup a
redelivered merge event. Since a refusal now logs at ERROR and fires the
`hh-config-rev-refused` alert, each of those would page as a refusal.

An ancestor is now RevAdvance::Superseded, logged at info, nothing queued.
Only a rev that has diverged (neither descendant nor ancestor) is refused.

Refs #4851
2026-10-09 22:25:39 +02:00
atlas
8c222f8910 hive-c0re: a refused rev is an error log + Grafana alert, not a PR comment
A deploy request whose rev hive-c0re refuses (fetch failed, or the rev
does not descend from applied/main) no longer tries to comment on the PR
that merged it. Off the forge host that attempt always failed with "forge
core token absent". The refusal now logs at ERROR, message unchanged, and
the new `hh-config-rev-refused` Grafana rule fires on it.

Deleted with the posting: actions::post_rev_deploy_failure_to_pr, the
whole forge/pr_comment.rs (config_repo, merged_pr_for_commit,
post_pr_comment), and ReaderToken::value, whose only caller was
merged_pr_for_commit.

Refs #4851
2026-10-09 22:25:39 +02:00
atlas
3d81266483 hive-forge: comments --since --limit returns N, not at most 49
The issue-comments endpoint takes no page or limit, so `comments
--since` cut its window client-side from a whole-list response. The
leftover over-fetch-by-one request (`.page_size(limit + 1)`) was a
no-op and the `--limit` clamp to MAX_LIMIT (49) guarded a truncation
check that no longer needs one.

Drop both: `--since T --limit N` now returns N comments. `comments
--json` still emits `since_limit_clamped`, always `false`. `timeline`
is untouched; its endpoint pages and its clamp stays.
2026-10-09 21:46:14 +02:00
atlas
1d56a74aad hive-forge: fetch issue comments in one request; the endpoint has no paging
The forge's issue-comments endpoint takes no page or limit and always
returns the whole thread. #5055 wrapped view's fetch in all_pages, which
never saw a short page on a thread of 50+ comments and fetched the same
list 50 times: `issue view 3782` printed 3000 comments instead of 60.
`comments --limit K` sent K as a page size the forge ignored, so it
returned the whole thread.

view and pr status's last_comment now send one request. comments fetches
the thread once and cuts the --limit / --tail window client-side; the
X-Total-Count probe and the computed-page tail walk are gone, and the
count of comments outside the window is the length of the list.
--since is unchanged.

tests/comments_no_paging.rs runs the binary against a loopback mock
forge whose comments route ignores page and limit, as Forgejo's does.
2026-10-09 21:19:47 +02:00