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

5,320 commits

Author SHA1 Message Date
atlas
4e6c3b4a2f hive-forge: pr create --draft: extract draft_warning so the warning decision is tested 2026-10-09 12:51:22 +02:00
atlas
4d164819b3 hive-forge: drop the subscription --ignore change (covered by #5053) 2026-10-09 12:51:22 +02:00
atlas
6e32b432ec hive-forge: subscription --ignore errors instead of subscribing; warn on --draft; always print agit PR URL 2026-10-09 12:51:22 +02:00
atlas
0e5c61d12c hive-forge: page past the forge's 50-row clamp; show list --json truncation
The forge clamps page_size to 50. branches, pr reviews, the review
bodies merged into `comments <pr>`, and view's comment list each sent
one request, so anything past the first page was dropped silently.
labels::repo_labels did loop, but asked for 100/page and stopped on
`len < 100`, so a clamped full page of 50 ended the walk.

Add verbs::all_pages: requests 50/page until a short page, capped at
50 pages with a stderr warning when the cap is hit. Use it in all of
the above; repo-labels now calls labels::repo_labels instead of its
own copy of the loop.

list --json now prints the same "N of M shown" stderr trailer as the
pretty output; its paging and stdout shape are unchanged.

Closes #5051
2026-10-09 12:35:51 +02:00
atlas
52f589780f agent: order bao/github/forge-accounts logins after the isolated-DNS rewrite 2026-10-09 12:34:30 +02:00
atlas
252b893bbb module-eval: agent-user test asserts the heal_own_dir form 2026-10-09 11:30:30 +02:00
atlas
6e1d72bfdb user.nix: log failed ownership heals of the agent's own dirs 2026-10-09 11:30:30 +02:00
atlas
6bb13f964d hive-c0re: validate forge env at startup; char-safe short revs
HIVE_FORGE_URL (set + parseable) and HIVE_ASSETS_DIR are checked once in
main before workers start. Short revs in container_view and pr_status
take 12 chars instead of byte-slicing.
2026-10-09 10:51:39 +02:00
atlas
9b711d5ffa hive-forge: mirror actions/upload-artifact alongside actions/checkout
With actionsFromSelf, Forgejo resolves bare `uses: actions/<name>` against
the forge itself, so every action a workflow names needs a mirror there.
Only actions/checkout was seeded, and coverage.yml's
`actions/upload-artifact@v3` has failed at job setup with "repository not
found" since the CI runner moved to swarm-ci-runner (e2da5509).

The single hardcoded checkout mirror becomes a list of action names both
share, mirrored from github.com/actions/* like checkout already was. v3
points at the same commit (ff15f03) on github.com and code.forgejo.org.

The controller's forge-objects pass creates any missing mirror on every
pass, and the changed SWARM_CONTROLLER_FORGE_MIRRORS env restarts the unit
on switch, so an existing forge picks the mirror up on deploy.

Closes #5040
2026-10-09 10:40:28 +02:00
atlas
b874d21cda hive-c0re/swarm-*: drop clippy allows and dedupe unix_now + KV open_or_create
No behaviour change. Casts replaced by try_from / Duration math; schedules.rs
reuses agent_renewal::unix_now; the five swarm-queue-client KV open-or-create
copies share one private kv::open_or_create.
2026-10-09 10:25:48 +02:00
atlas
f53ccb2740 hive-c0re: stop silently dropping errors on credential/marker writes
Add secret_file::write_secret_0600: create the parent dir, write a
sibling temp file opened create_new + mode 0600, sync_all, rename over
the target. Use it for the matrix sender token, the space/chat room-id
files and the forge core token, which previously wrote at the process
umask, then chmod-ed with the error discarded, and dropped the
create_dir_all error. agent_identity::write_identity uses it too
instead of its own open-0600 loop; that loop opened with create +
truncate, so a pre-existing file kept its old mode.

Also log instead of discarding:
- the manager's applied-rev marker write in auto_update
- the knowledge `git remote set-url` scrub (spawn error and non-zero
  exit; the exit status only, since stderr may echo the old URL)
- the parent create_dir_all in paths::move_if_legacy

Closes #5037
2026-10-09 10:24:15 +02:00
atlas
54226e027d swarm-bao: build the granter policy units from one helper
The six hand-written granter policy units (secret-publisher, matrix-ctl,
services-issuer, nats-tls, hive-leaf, operator-viewer) repeated the same
skeleton readerPolicyUnit already carried: ordering after the controller's
unit, pki requirement, path, granter environment, unseal retry window and
oneshot serviceConfig. They differed only in description and the script
after the granter login.

granterPolicyUnit now owns that skeleton; the six units and readerPolicyUnit
pass a description and a script body. Every rendered unit is unchanged
byte-for-byte.
2026-10-09 09:37:03 +02:00
flake-bot
4fa1cee620 nix flake update 2026-10-09 05:00:55 +02:00
damocles
8053462e2a tool_groups: add test for the case-insensitive is_known match 2026-10-09 00:50:27 +02:00
damocles
67fd164dfc tool_groups: match names case-insensitively, like capabilities::is_known 2026-10-09 00:50:27 +02:00
damocles
1d120289d1 hive-c0re: extract read_from so the prune_unknown test exercises the real wiring 2026-10-09 00:50:27 +02:00
damocles
e24259dde1 hive-c0re: add prune_unknown table tests + read() coverage, mirroring capabilities.rs 2026-10-09 00:50:27 +02:00
damocles
06951ddc30 hive-c0re: drop unrecognised tool-group names on read, matching capabilities' prune_unknown 2026-10-09 00:50:27 +02:00
atlas
597fdebaf3 swarm-bao-forge-publisher starts on the deploy that adds it
The publisher was wanted only by container@hive-forge.service. A switch
starts a new unit only through the targets that want it, and the container
is already running, so the deploy that introduced the publisher left it dead
until the next container restart or boot; the path unit cannot help, because
the mint's only-once guard never rewrites the file.

- The publisher is also wanted by multi-user.target, like swarm-secret-publish.
- A token file that is not there yet exits 0 instead of waiting 120s and
  failing: the path unit runs the publisher when the mint writes it, so a
  switch with the forge container down does not fail. TimeoutStartSec is 60.
- swarm-controller loses its After= on the publisher: a switch issues the two
  jobs separately, so it ordered nothing there, and the background re-read
  covers the gap.
- The controller's background re-read logs at info once the backoff is
  capped, one line per five minutes, so a forge host that never got its
  publisher leaf stays visible; earlier retries stay at debug.

Refs #4998
2026-10-08 13:59:26 +02:00
atlas
2d2d7529fc swarm-bao-forge-publisher re-runs on container start and token writes
The publisher was a oneshot with RemainAfterExit=true, started only through
`wantedBy = container@hive-forge.service`. Once it had run it stayed
`active (exited)`, so a later container start made its start a no-op: a token
re-minted without a host reboot or a unit change never reached the store.

- RemainAfterExit is dropped, so each start runs the script again.
- systemd.paths.swarm-bao-forge-publisher watches the token file in the
  container (PathChanged) and starts the publisher when the mint writes it,
  the same shape as swarm-secret-publish's path unit.
- The code comment, secrets.md and credentials.md now name both triggers;
  forge-placement pins the missing RemainAfterExit and the path unit.

Refs #4998
2026-10-08 13:45:56 +02:00
atlas
c45f7a5330 swarm-controller gains forge access when the token arrives, without a restart
A controller that started before the forge's host had published its token
ran without forge access until someone restarted it: on a split-host first
deploy that is a manual step. Now the forge client lives in a slot that is
filled once (`Late<T>`, an `Arc<OnceLock<T>>`):

- `forge::enable` tries the store once at start. On failure it logs once and
  re-reads in the background with backoff (30s doubling to a 5-minute cap,
  no log per retry), fills the slot and logs once when the token is there.
- The forge passes (#5014's per-hive token mint among them), the config-PR
  poll and the webhook registration start from that moment, through
  `start_forge`'s callback.
- Job nodes and HTTP handlers read the slot when they run, so they answer
  "no forge" before and work after.

A token replaced in the store after the slot is filled still reaches the
controller with its next restart. The round-1 change that shared
`queue_identity::fetch_with_retry` is reverted; nothing else uses it.

Refs #4998
2026-10-08 13:40:24 +02:00
atlas
121c4678aa swarm-controller reads its forge token from the store, not a host file
The controller's forge token never entered bao: the forge minted it in its
container, hive-forge-swarm-controller-token copied it to a host file, and the
controller LoadCredential'ed that file. forgeTokenFile defaulted to that path
only where the forge ran, so a controller on any other host had no forge
access unless an operator copied the file by hand.

Now the forge's host publishes it and the controller reads it:

- swarm-bao-forge-publisher (replaces hive-forge-swarm-controller-token)
  waits for the in-container token, logs in with its own forge-publisher leaf
  and writes it to swarm/controller/swarm-controller/forge/token. It runs on
  every forge container start, the old unit's trigger.
- New principal swarm-forge-publisher: leaf minted by glue-bao-tls.nix,
  policy create/update on that one path, written by
  swarm-bao-forge-publisher-policy, CN reserved as a hive name.
- The controller policy gains read on that path. swarm-controller reads it
  at start under its own certificate with the OIDC secret's retry, and runs
  without forge access if it still cannot.
- forgeTokenFile, its LoadCredential and the forgeEnv gate are gone;
  SWARM_CONTROLLER_FORGE_URL is always set. hostSwarmControllerTokenFile is
  removed, and a tmpfiles rule deletes the old host copy on the forge host.

The in-container file stays: the mint unit's only-once guard reads it, and
it is the publisher's source.

Refs #4998
2026-10-08 13:25:47 +02:00
damocles
c433bdef92 hive-runtime: log the acp permission decision at the point it is made 2026-10-08 10:29:13 +02:00
flake-bot
90c7600c8a nix flake update 2026-10-08 08:38:11 +02:00
damocles
d071c8df00 hive-subagent-mcp: cover the non-goal interrupted-turn case for #4337 2026-10-08 08:19:10 +02:00
damocles
04a8e2b328 hive-subagent-mcp: write the fake-claude test script via write_executable 2026-10-08 02:01:04 +02:00
damocles
70644cb004 hive-c0re: widen nix stderr predicate to cover error:/evaluation warning:, add tests 2026-10-08 00:46:23 +02:00
damocles
25893d0f95 hive-c0re, hive-priv: demote three routine, non-actionable log sites 2026-10-08 00:46:23 +02:00
damocles
77034a7c9c hive-matrix-mcp: fix matrix.nix comment to match the current diff 2026-10-08 00:28:03 +02:00
damocles
7eb338a8c1 hive-matrix-mcp: drop the main.rs fallback edit, keep only the nix RUST_LOG change 2026-10-08 00:28:03 +02:00
damocles
a28163a008 hive-matrix-mcp: demote backup-key WARN where it actually runs, not just the fallback 2026-10-08 00:28:03 +02:00
damocles
2835c08cb0 hive-matrix-mcp: demote matrix-sdk-crypto's benign backup-key WARN by default 2026-10-08 00:28:03 +02:00
iris
8a7aacec13 swarm-grafana: drop stale windows from the subscription usage line, match sibling panel interval 2026-10-07 23:54:53 +02:00
iris
5b8d1dd54d swarm-grafana: add a subscription usage line to the claude usage dashboard 2026-10-07 23:52:43 +02:00
damocles
e1df50ee07 hive-forge-notify: fix assigned-issues poll hitting the wrong search path 2026-10-07 23:21:58 +02:00
atlas
1ca758daa7 swarm-controller grants internal/docs read to agents; hives stop doing it
Agent creation gains a GrantDocsRead node (after CreateForgeUser) that adds
the agent as a read collaborator on internal/docs, and forge::docs_grant
runs a pass over the store's hive-agent-* roster at start and every five
minutes that queues the same job for any agent that is not one. The pass only
adds; an unreadable roster or forge read changes nothing.

hive-c0re drops shared_docs_access, its call in sync_agent, and the
SHARED_ORG / SHARED_DOCS_REPO constants only it used.
2026-10-07 21:39:55 +02:00
atlas
4e7142ae53 swarm-controller: refuse new agents named hive-<anything>
`hive-<hive>` is each hive's read-only forge account, and an agent's forge
account takes the agent's name, so an agent called `hive-<x>` would claim
hive `<x>`'s account. `hive_types::reserved_agent_prefix_violation` names
the rule, and `broken_name_rules` applies it, so both creation paths that
go through it (`POST /api/agents` and placing an undeclared agent on a
hive) refuse such a name for a new agent. An agent already in the roster
keeps its name with a warning, as for every other naming rule.

The email check between hive and agent accounts stays as the second
guard, for agents created before this rule.

Refs #4851
2026-10-07 21:07:17 +02:00
atlas
21e3f626f4 hives read the forge through their own swarm-minted read account
swarm-controller now gives every hive in its directory a forge account,
`hive-<hive>`, in a new `hives` team of `agent-configs` (read on
`repo.code` + `repo.pulls`, every repo including later ones), and mints
one `read:repository` token named `swarm-hive` into the store at
`swarm/hives/<hive>/forge/token`. A five-minute pass queues a
`MintHiveForgeToken` node for a hive whose token is missing or no longer
matches the forge, whose account is missing, or whose team membership or
team settings drifted. A hive whose store or forge read failed is never
queued, so an outage cannot rotate every hive's token. The token decision
reuses `agent_token`'s classifier and replace step, now parameterised by
name and scopes.

An agent may be called `hive-<x>`. The hive account is created with
`hive-<hive>@hives.hyperhive.local` and the job refuses an account by that
name with any other email before it locks it down, adds it to the team or
mints for it; agent creation already refuses the hive's account by the
same email check.

The controller's bao policy gains the `swarm/hives/+/forge/token` leaf
(create/read/update), pinned by the bao-grants module-eval assertion. The
hive's own policy already reads `swarm/hives/<hive>/*`.

hive-c0re reads that token through `forge::reader_token()`, falling back
to the core token file when the store holds none, and uses it for:
fetching a merged config rev (swarm deploy, `hivectl forge
reconcile-config`), reseeding a purged `applied`, the meta-update
cascade's fast-forward, seeding `proposed` on first deploy, and finding
the PR that merged a refused rev. Those reads no longer require a local
`hive-forge` container. The `/knowledge` clone moves from the forge
host's boot sweep into the knowledge pull, so every hive clones it.

Posting the deploy-failure comment on that PR stays on the core token.

Refs #4851
2026-10-07 20:52:32 +02:00
atlas
c8472966e9 hive-c0re: drop the remaining "mirror" wording for the removed push
The reseed bail text, the `ensure_all` doc, the forge module doc and a
rebuild comment still described hive-c0re mirroring the applied config to
`agent-configs/<name>`. State what the code does now; scope the
config-changes doc to swarm-created agents and say what a legacy agent
whose repo has no `main` gets.

Refs #4851
2026-10-07 20:04:07 +02:00
atlas
456a4511ab hive-c0re: stop mirroring deploy tags to the forge (drop push_config)
Per the operator's ruling on #4851 (option (a)): the hive holds no
write identity on the forge, so the `push_config` mirror of an applied
repo's tags and `main` to `agent-configs/<name>` is removed, along with
its `run_config_push` helper and its call in `sync_agent`.

Docs and comments that described the mirror now say that forge `main`
advances only by an operator's merge.

Refs #4851
2026-10-07 19:40:16 +02:00
atlas
c1672218b1 hive-bash-daemon: keep sibling tasks alive when one is OOM-killed
systemd's default OOMPolicy=stop ends the whole unit when the kernel
kills any process in it; set continue, as hive-subagent-daemon does.
Module-eval case asserts it.
2026-10-07 18:18:15 +02:00
atlas
1b061fc9d5 swarm-nats-auth: take the subscribe subjects from the shared definitions
The reader's subscribe allow-list spelled `$SWARM.term.*`,
`$SWARM.term.*.sub.*` and `$SWARM.agent-state.*` as literals, copies of
private prefixes in hive-agent and swarm-controller. A rename on either side
left the policy behind and the stream stopped with only a connection-level
server error.

swarm-queue-client now owns `term_subject`, `agent_state_subject` and their
`*_wildcard` forms, and `subagent_term::subject_wildcard`; hive-agent's
publishers, swarm-controller's relays and the policy all build from them.
Subjects are byte-identical.
2026-10-07 16:51:53 +02:00
atlas
d0c2b85d45 swarm-nats-auth: limit hive and swarm-controller subscribe to the subjects they use
A hive's queue credential and the swarm controller's could subscribe to
every subject in the swarm account, so a hive could read every other
hive's deploy requests and every agent's terminal, state, input and
request replies.

Their grants now carry a subscribe allow-list:

- a hive `hive-<h>`: `$SWARM.knowledge`, `$SWARM.deploy.<h>` and its
  reply inbox `_INBOX.client.hive-<h>.>`
- the reader (swarm-controller): `$SWARM.term.*`, `$SWARM.term.*.sub.*`,
  `$SWARM.agent-state.*` and its reply inbox
  `_INBOX.client.<client-id>.>`

`swarm_queue_client::connect`, which hive-c0re and swarm-controller both
use, now connects with `_INBOX.client.<client-id>` as its inbox prefix,
the same shape agents got for their own credential. In async-nats 0.50
every request reply and every ordered consumer (the hive's wanted-state
watch, the controller's KV key listings) delivers under that prefix, so
the one inbox grant covers them all.

A hive or controller still on a build without the prefix has every
JetStream reply refused once the responder runs this policy, so the
clients need to be rebuilt before, or with, swarm-nats-auth.

Closes #4945
2026-10-07 16:05:36 +02:00
atlas
22ce8b815a hive-subagent-mcp: a refused continue leaves the session's records alone
`continue_` cleared the stop reason, reset the turn budget and remembered
the caller's `dir` before the session lookup ran, and nothing restored
them when the lookup refused. With the strict lookup a refusal is an
expected outcome (the caller omitted `dir`), so every such retry erased
why the session stopped from `status`, gave a capped goal session its
budget back, and made the wrong `dir` the remembered one for the next
bare `continue`.

The lookup now runs first, against the dir peeked rather than committed;
only a session that is found gets `State::begin_continue`, which does
all three. Same for the ACP path's recorded-session check.
2026-10-07 13:18:07 +02:00
atlas
0e015197b6 hive-subagent-mcp: refuse a continue whose session is not in the dir searched
resume_target no longer falls back to the bare name when the project dir
holds no session titled it: continue (and a goal run's next turn) refuses
before anything spawns, naming the directory searched. A store that cannot
be resolved refuses too, with the io error. The fallback notice is gone.
2026-10-07 12:28:25 +02:00
atlas
d03f10822c hive-subagent-mcp: say so when a resume falls back to the bare name
resume_target logs the fallback and returns a notice naming the project
dir searched; continue appends it to its reply and to a failed resume's
error, so a caller hitting claude's 'matches 2 sessions' knows to pass dir.
The test's final assertion now pins the whole attach list so the continue
site is covered on its own, and the fallback branch has a test.
2026-10-07 12:28:25 +02:00
atlas
8f2069486d hive-subagent-mcp: resume a subagent by session id, not by name
`continue`, and every later turn of a goal run, passed `--resume <name>`.
claude 2.1.287 resolves a title across the project dirs of every git
worktree of the repo and refuses when two sessions match, while `start`
archives a prior session only in its own cwd's project dir. A name first
started under one worktree and then started again under a sibling stayed
live in both, and every resume of it failed with `--resume "<name>"
matches 2 sessions`. That is the #5000 incident: build-4921a/b were first
run under /home/atlas/wt/4901-build, then under their own worktrees.

Both resume sites now look up the live session titled `name` in the
config's own project dir (the same `SessionStore::find_by_title` lookup
`start` archives with) and pass its id. The id comes off disk at each
resume, so it survives a daemon restart. When that dir holds no session
under the name, the name is passed as before and claude's answer stands.

The test drives a fake claude that keeps sessions on disk and resolves
titles across project dirs the way claude does. Its script is written by
a child process, so no write fd on it exists in the test binary for a
concurrent fork to inherit (the ETXTBSY of #4898).
2026-10-07 12:28:25 +02:00
atlas
40687ab4a5 swarm-nats-auth: a failed re-read no longer logs that the stored secret was refused
When the store re-read failed or timed out, introspect fell through to the
unchanged-secret branch and logged "the stored OIDC client secret is the one
the IdP refused" after the re-read's own failure line. The re-read now says
whether it read the store, and a failed one denies without the second line.
The verdict is unchanged.
2026-10-06 16:01:53 +02:00
atlas
f83030281e nats-callout-removed: remove the container's OIDC secret copy here, ungated
The tmpfiles `r` rule for the stale `oidc-client.secret` in the
swarm-nats container sat in swarm-nats.nix under `deploy.nats.enable`,
so a host that turned the queue off kept the copy. It now sits in the
ungated removal shim next to the seed copies' rules, which is where the
S2 plan put it and the one file to delete when the deprecation window
closes. The module-eval case asserts the rule on a host with the queue
off as well.
2026-10-06 15:25:29 +02:00
atlas
32e28e6096 swarm-nats-auth: bound the OIDC secret re-read, and test the 401 that starts it
The re-read of the responder's own OIDC client secret had no timeout:
neither swarm-secret-client nor vaultrs nor reqwest sets one by default,
and while a re-read runs every later refusal joins it rather than
starting another. A store connection whose peer stays alive but silent
therefore held off rotation pickup indefinitely. The spawned re-read
now gives up after 10 s, logs that it did (without the secret), keeps
the secret in use, and frees the slot for the next refusal's re-read.

The link from a non-2xx introspection (identify_caller's
`Answer::Refused`) to that re-read had no test: every existing test
handed `OidcSecret::introspect` an `Answer` directly. A new test runs
the real `identify_caller` against a loopback IdP that answers 401 to
the old secret and admits the new one, so mapping a non-2xx to
anything but `Refused` turns it red. axum is a dev-dependency for that
stub, as in swarm-controller's forge tests.
2026-10-06 15:25:29 +02:00