Commit graph hyperhive/nix/agent-modules
Author SHA1 Message Date
iris
e525dcb6d4 agent icon: 404 when unconfigured, client-side fallback
hive_sh4re::assets::branding_svg() resolved a server-side default
icon at runtime from HIVE_ASSETS_DIR — the only consumer was
serve_icon(), which fell back to it whenever the agent had no
`hyperhive.icon` override. Removed both the fallback and the
function: serve_icon() now 404s when /etc/hyperhive/icon.svg is
absent, and the per-agent web UI (app.js) picks up the existing
dashboard swarm.js pattern — swap the <img> src to the
frontend-bundled /favicon.svg on load failure, guarded against
looping if the fallback itself 404s.

Updated the doc/comment claims that said the server always returns
an image (docs/web-ui/agent.md, nix/agent-modules/default.nix, the
hive-c0re/forge/users.rs comment referencing the old shared-asset
set). forge-avatar-sync and the matrix avatar sync are unaffected —
both are gated on hyperhive.icon != null and never depended on the
removed fallback.
2026-08-10 20:59:59 +02:00
damocles
7ba492b965 move hive-sh4re's harness_dir() into hive-agent-sock, drop the dev-fallback derivation 2026-08-09 19:54:13 +02:00
atlas
1a0cb0fb44 docs: name the swarm display name by its new path
Two sites spelled it as a brace group, services.hyperhive.{hiveName,
swarmName}, which no anchored rewrite can handle correctly now that only
one of the two moves; both are written out separately. One of them is an
MCP tool description, so it is rendered into every agent's system prompt.
2026-08-05 11:15:41 +02:00
atlas
0e9b1c563d fix(#2860): no loopback default for the matrix homeserver
Third and last of #2860's agent-facing URL fallbacks. The operator's
ruling was "any special casing is done on the nix side - same binaries,
no hard coded fallback", so the default is deleted rather than replaced.

Every layer guessed the same wrong thing, and each guess was only ever
correct for a process sharing the host netns:

- nix/agent-modules/matrix.nix: matrixUrlDefault = localhost:8008, both
  as the option's default and as a sentinel the daemon unit compared
  against to decide whether to write HIVE_MATRIX_URL. Now nullOr str,
  default null, the guard is != null, and the doc says what forge.url's
  already says: null means "no matrix", not "guess one".
- nix/host-modules/hive-c0re/environment.nix: forwarded
  http://127.0.0.1:<port> when no gatewayHost was set. hive-c0re shares
  the host netns so it reads as harmless, but the value is handed to
  agents, which do not -- there it names the agent itself. Now forwarded
  only when there is a gateway vhost to name, matching the guard
  HIVE_MATRIX_PUBLIC_URL already uses twelve lines below.
- hive-matrix-mcp: paths::DEFAULT_HOMESERVER was the same address
  compiled in, so dropping the nix defaults alone would have left the
  daemon dialling loopback inside the agent's own netns -- the very bug,
  one layer down. homeserver_url() is now Option, and an account with no
  homeserver is skipped with a log, exactly as one with no token is.
  discover_token_accounts already refused to guess for the same reason.

Two comments taught the assumption back to the next reader ("shared host
netns means every agent container resolves localhost to the same
machine"); both now say which side of the netns boundary they describe.
MATRIX_HTTP keeps its value -- hive-c0re really does share the host
netns -- but no longer claims agents do.

Gated with nix eval against the extended agent-base config, as a pair:
with no url set the daemon unit carries no HIVE_MATRIX_URL, and with one
set it carries exactly that. Either check alone passes on a broken guard.
2026-08-03 20:34:36 +02:00
damocles
3617578341 web_ui: scope /cancel and /logout's SIGINT to the harness's own claude child 2026-08-02 13:10:34 +02:00
atlas
772482a52a fix(#2898): carry hive/swarm display names as build-time options
The OTEL resource attributes are baked into every agent's
managed-settings.json at evaluation time, but claude-settings.nix read
the names from the container's environment.variables - where they are
never set. meta.rs forwards them as runtime env only, so the reader hit
its "unknown" fallback and every agent shipped

    service.name=hyperhive-agent,agent=<a>,hive=unknown,swarm=unknown

on every metric, while the same process's env held pr1ma/constellation.
Measured in this container's /etc/claude-code/managed-settings.json.

The map from forwarded env var to agent option already existed for the
service URLs, with a doc comment naming this exact hazard: "setting only
one leaves the other on its default". The names were simply never added
to it. They are now, and the constant is renamed FORWARDED_VAR_OPTIONS
since it no longer holds only URLs.

hyperhive.hiveName / hyperhive.swarmName follow the forge.url shape:
nullOr str defaulting to null, where null means the hive did not name
itself and "unknown" is an honest label rather than a guess baked at
eval time.

Also fixes, unasked: CLAUDE_REMOTE_CONTROL_SESSION_NAME_PREFIX reads the
same value, so remote-control sessions were named "unknown-<agent>".

Gated with nix, not just cargo - the blast radius here is module eval,
which fmt/clippy/test cannot see:

  agent-base assertions            -> [] (no failures)
  extendModules with both options  -> hive=pr1ma,swarm=constellation

Note the value is baked, so every agent needs a rebuild before the new
label appears on its metrics.
2026-08-01 12:35:50 +02:00
atlas
bcb9e837f7 fix(#2860): make hyperhive.forge.url nullable instead of guessing a URL
The option had a `http://localhost:3000` default, which is only ever
correct when the forge shares the caller's network namespace — inside an
agent's netns `localhost` is the agent, and the forge may well be on
another host. Making it *required* instead was worse: the flake's own
container configs are what hive-c0re extends per agent, so the value
they needed in order to evaluate became a second definition on every
agent and collided with the real one.

`null` resolves both. It is not a URL, so nothing can quietly talk to
the wrong machine, and it needs no placeholder anywhere: the bases
evaluate as they are, so nothing deployment-shaped sits on the config
agents inherit from. The units that would consume the URL — tea-login
and forge-avatar-sync — are simply not generated without one, making an
absent forge an absent integration rather than a misdirected one.

hive-forge-notify is unaffected: it reads HIVE_FORGE_URL from the
forwarded global environment, not from this option.

Verified:
  agent-base/ruth evaluate with forge.url = null, zero failing assertions
  bare base:      tea_login_present = false, avatar_present = false,
                  notify_present = true
  extended with a rendered URL: FORGE_URL=http://forge.real.test

Refs #2860
2026-08-01 00:36:09 +02:00
atlas
7ef9905e9f fix(#2860): make hyperhive.forge.url required, drop the loopback default
The default `http://localhost:3000` was a guess, and a guess that
evaluates is worse than one that doesn't: the forge may run on a
different host from the agents, and inside an agent's network namespace
`localhost` reaches the agent itself, not the forge. hive-c0re renders
this option into every agent's config from the host's `HIVE_FORGE_URL`,
which `hive-c0re.nix` sets unconditionally, so nothing legitimate was
relying on the default to be there.

Also tightens the URL assertion, which accepted `""`. That escape hatch
only existed because the default made the empty case unreachable; with
the default gone, `""` is exactly what a caller supplies when they have
nothing, so it now fails naming the option instead of silently building
a config that talks to nowhere.

Refs #2860
2026-07-31 22:53:51 +02:00
atlas
0db83c40a0 feat(#2642): a github.com notification poller alongside the forge one
hive-forge-notify grows a second binary, hive-github-notify. The two
share the notification half of the job — tolerant parse, classification,
formatting, dedupe, todo delivery — and nothing else: each binary owns
its host's protocol outright.

Two binaries rather than one multi-source daemon, and rather than a
cargo feature. A feature would unify across the workspace and cost every
crate its build cache. Two binaries keep the decision in nix: forge.nix
installs the forge unit, github.nix installs the github one under
hyperhive.github.enable, so a hive built without that module has no
github poller in its closure at all — GitHub access is separable (a
tier, a policy boundary), not merely switched off. Both binaries ship
from the existing derivation, so packages.nix is untouched.

The split is real at the code level too, not just at the unit level.
source.rs is a trait; the impls live in the binaries that use them, so
neither binary links the other's protocol code and the library names no
host at all. The forge-only assigned-issue rollup moves into the forge
binary for the same reason: it asks the forge what is assigned to this
agent, which is not a notification-protocol concern.

At runtime the github unit needs a PAT at <state>/github-token, the same
dashboard-provisioned token the gh wrapper and the git credential helper
already use. No PAT: it logs why and exits 0, which is why the unit is
Restart=on-failure and not always.

Forgejo's notifications API is modelled on GitHub's, so one tolerant
parse serves both — the differences (string thread ids, PullRequest vs
Pull) are absorbed by lenient deserializers rather than a second parse
path. Thread ids normalise to String at the parse boundary; they are
only ever opaque keys. Todo keys gain a per-source prefix so the two
hosts cannot collide, and the forge's is deliberately empty to keep
existing forge todo keys stable across the deploy that lands this.

The github loop honours the server's X-Poll-Interval, re-arming only
when the server asks for a slower cadence than ours; the hint is read
before the status check, because it arrives on error and empty pages too
and that is exactly when it matters. Reading the notification stream
needs the notifications scope on the PAT, which a token minted for push
access typically lacks; the failure mode is silence, so docs/github.md
says so explicitly.
2026-07-31 17:23:18 +02:00
damocles
03afbd1316 hivectl: rename hivectl agents to hivectl agent <name> <verb> 2026-07-27 19:07:18 +02:00
damocles
1d3f22805f claude-plugins: generalize state-hygiene into a shared base plugin for all agents 2026-07-27 13:56:29 +02:00
damocles
d207299d0f claude-plugins: fix docs build by adding defaultText to claudeMarketplaces 2026-07-27 13:56:29 +02:00
damocles
2bd1b0a3d5 claude-plugins: rename notes-hygiene to state-hygiene 2026-07-27 13:56:29 +02:00
damocles
bc83fde4ad claude-plugins: ship a hyperhive-authored notes-hygiene skill by default 2026-07-27 13:56:29 +02:00
atlas
2ad4b43118 refactor(#2693): null, not "", for the unpinned claude-code
mara on PR #2769: "make the default null instead of special casing """.

`claude_code_path` was a `String` whose empty value meant "no host-level
pin". That is a sentinel doing an `Option`'s job — the same shape argus
and mara already rejected on #2755's weights, and the same
empty-field cruft mara called out on #2756.

So it is `Option<String>` end to end:

- host module: `claudeCodePath` evaluates to `null` when
  `claudeCodePackage` is unset, so `serve.json` carries JSON `null`
  rather than `""`.
- `Coordinator` + `HiveEnv`: `Option<String>`, defaulting to `None`.
- `render_flake`/`render_flake_with_lookup`: `Option<&str>`, and the
  emission is an `if let Some(path)` instead of an `is_empty()` guard.
- agent module: `hyperhive.claudeCodePath` is `nullOr str`, default
  `null`.

Behaviour is unchanged in both directions; only the way "unset" is
spelled moves. The `builtins.hasContext` assertion still guards the
pinned case (short-circuited by the null check, so an unpinned hive
never evaluates it).

16/16 `meta::` tests, clippy clean, `nix fmt` no-op, `nix build .#docs`
green.
2026-07-27 13:56:28 +02:00
atlas
b08176f089 feat(#2693): let the operator pin the claude-code every agent runs
Agents run whatever `claude-code` the meta flake's `nixpkgs` resolves
to, and that is normally a release channel. This one package moves fast
enough that stable trails unstable by weeks — 26.05 is on 2.1.187 while
unstable carries 2.1.220 — and an agent cannot fix it for itself: it
only ever sees the single nixpkgs hive-c0re injects, so an `agent.nix`
has no other tree to reach for.

New host option `services.hyperhive.c0re.claudeCodePackage` takes the
package directly and rides the existing `hyperhiveDocs` threading path —
serveConfigJson -> HiveEnv -> render_flake — to reach each agent as
`hyperhive.claudeCodePath`. Null (the default) is today's behaviour.

What travels is the store *path*, as a plain string literal, not a flake
input: containers share the host's `/nix/store`, so the build is already
reachable inside them with its whole closure and has nothing to travel.
An input would be worse than useless — a `path:/nix/store/<pkg>` input
is re-copied as a reference-less `-source`, which strips exactly the
closure the binary needs.

The catch is that a path written into a generated flake is text, so
nothing in the container's closure keeps the binary alive. The host does
that instead, and gets it for free: the package is interpolated into
`/etc/hyperhive/serve.json`, `builtins.toJSON` preserves string context,
so the /etc entry references it and the system closure gc-roots it for
as long as that generation is the one the agents were rendered from. An
assertion pins that property, because losing the context is invisible at
eval and at deploy — it would surface only as every agent failing to
spawn `claude` whenever the next gc ran.

Container side wraps the path in a symlink farm rather than putting it
on PATH directly: `systemd.services.<name>.path` and
`environment.systemPackages` both coerce a store-path *string* through
`lib.toDerivation`, i.e. `builtins.storePath`, which pure evaluation
rejects. Interpolating the path into a builder is just text and
evaluates anywhere. `claude-code` drops out of systemPackages when a
pin is set, so there is exactly one claude in the container.

Refs #2693
2026-07-27 13:56:28 +02:00
damocles
bd14cc5c46 feat: derive BUN_JSC_forceRAMSize from effective per-agent MemoryMax= 2026-07-26 21:56:25 +02:00
atlas
246c9471b1 refactor(hive-agent): split the forge notification poller into its own crate
The poller was a `tokio::spawn` inside the `hive-agent` serve loop. It
never needed anything from that loop except a socket path, so being
in-process bought nothing and cost two things: a harness restart took
forge notifications down with it, and the whole forge/HTTP dependency
tree was linked into the serve-loop binary.

It is now `hive-forge-notify`, a per-agent daemon with its own systemd
unit, a sibling of `hive-bash-daemon` and `hive-matrix-daemon`. Same
contract as those two: it reaches the harness only by upserting todos on
the in-agent socket, and nowhere else.

The module moves verbatim (`notify.rs`) — the formatters, the activation
gates, the dedupe map and all 33 tests are unchanged. Only the socket
call sites are rewritten, onto a small local `todo_client` rather than
the harness's. That mirrors what both sibling daemons already do, and
the etiquette differs on purpose: the harness's client carries a 60s
backoff schedule sized to ride out a hive-c0re restart, which its
callers need because they have no retry of their own. This poller's two
call sites both sit inside the 30s poll loop and both treat a failure as
"leave the thread unread, try next tick", so the poll interval already
is the retry; a second backoff would only stack sleeps and delay the
rest of the batch.

The unit is `Restart=on-failure`, not `always`. An agent with no forge
account is a supported configuration and the poller reports it by
logging why and exiting 0 — under `always` that clean exit would be a
restart loop on every forge-less agent.

`forgejo-api`, `url` and `time` drop out of `hive-agent`'s dependencies
with the module.

Also corrects docs that outlived the code they described: the persisted
`forge_cursor` field is long gone (forge's own read-state is the durable
record of what has been delivered), but `docs/persistence.md` and the
`harness_state` module docs still documented it as live.
2026-07-26 21:30:29 +02:00
atlas
14f141625a feat(agent): ship Anthropic's skill-creator plugin by default
Agents had the official marketplace configured out of the box but an
empty plugin list, so nothing was installed from it unless an agent's
own config asked. skill-creator is the one plugin that pays for itself
generically: it teaches an agent to write, refine, and evaluate its own
skills, which is exactly the capability an agent can't bootstrap by
being told about it once in a prompt.

Defaulting the option keeps this consistent with claudeMarketplaces,
which already ships the official marketplace the same way. Documented
the list-option semantics next to both: a per-agent definition replaces
the default rather than extending it, so an agent that sets its own
plugin list has to name skill-creator alongside its entries.
2026-07-26 20:28:51 +02:00
damocles
2316287327 remove hive-agent-wake — no shipped consumer 2026-07-25 20:05:32 +02:00
atlas
65a0686297 fix(#2673): set nix fallback in agent + CI containers
Container nix invocations hard-failed whenever the remote builder
muede-pc2 was unreachable, while the identical build on the host
succeeded. Both go through the same host nix-daemon, so the difference
looked impossible.

The cause is that `fallback` is a client-side option: the nix client
transmits it to the daemon per connection (`tryFallback` in setOptions),
so the caller decides whether a failed remote dispatch may degrade to a
local build — even when the build itself runs on the host daemon under
NIX_REMOTE=daemon. Only genuinely daemon-side settings such as
`builders` are inherited from the host. The containers never set
`fallback`, so they took nix's default of false.

Set it in the agent-container base module and in the CI container, and
correct the hive-ci comment that claimed fallback was inherited from
the host daemon along with buildMachines and max-jobs.

Verified in an agent container: `nix fmt` fails outright on the remote
store's connection reset, while the same command with fallback enabled
reports the same connection error and then builds locally and succeeds.
2026-07-25 19:36:31 +02:00
damocles
a66b7ab298 feat(#2659): serve hive-matrix-mcp over persistent streamable-http, drop stdio bridge 2026-07-24 12:44:39 +02:00
damocles
63b1a6fe39 docs(#2659): note bash extraMcpServers example is illustrative, tracks bashHttpPort 2026-07-23 18:01:20 +02:00
damocles
c4fcf7fbf1 feat(#2659): serve hive-bash-mcp over persistent streamable-http, drop stdio bridge 2026-07-23 18:01:20 +02:00
damocles
17a9a156c2 feat(#2628): migrate bash producer to the in-agent todo socket (keyed active + keyless done, not wakes) 2026-07-21 23:51:35 +02:00
damocles
ade7be46c2 feat(#2569): wire HIVE_AGENT_SOCKET for the harness, mcp-http, and matrix services 2026-07-20 23:29:26 +02:00
iris
228a5bacca feat(#2618): add mouse_move + mouse_click via RFB PointerEvent
Implements mouse input by speaking the RFB protocol directly to Weston's
neatvnc server (localhost:HIVE_GUI_VNC_PORT, default 5900) — the VNC
backend's native remote-input path. No /dev/uinput, no kernel bypass;
the compositor mediates all input just as it does for the browser VNC viewer.

Changes:
- rfb_handshake(): RFB 3.8 handshake with security type None (auth-method=none
  in weston.ini); shared-session ClientInit keeps the browser viewer connected
- rfb_pointer_event(): encodes a 6-byte RFB PointerEvent (type=5, button-mask,
  x/y big-endian)
- rfb_send_pointer_events(): connects, handshakes, sends an event slice,
  flushes — all in one TCP connection
- mouse_move(x, y): sends a single PointerEvent(mask=0, x, y)
- mouse_click(x, y, button): sends move → button-down → button-up sequence
  (left/middle/right via RFB button-mask bits 0/1/2)
- vnc_port(): reads HIVE_GUI_VNC_PORT from env, falls back to 5900

No new packages or nix options — HIVE_GUI_VNC_PORT is already set by the
harness when gui.enable = true; grim/wtype are the only runtime deps.

Closes #2618.
2026-07-20 21:00:09 +02:00
iris
7fa7e2bdfd refactor(#2305): drop mouse tools + ydotool, switch key_press to wtype
- remove mouse_move and mouse_click (no Wayland-native alternative on Weston
  without /dev/uinput; follow-up filed for future investigation)
- replace key_press from 'ydotool key' to 'wtype -k': parses mod1+mod2+key
  into -M mod1 ... -k key ... -m mod1 sequence via virtual-keyboard protocol
- remove dest_path parameter from screenshot: always writes to /tmp/ (fixes
  arbitrary write-path concern from security review)
- simplify screen.nix: drop screenInput option, ydotoold systemd unit, ydotool
  package; only grim + wtype remain (both compositor-mediated, no /dev/uinput)
- update module header comment to reflect three-tool surface

Addresses mara's /dev/uinput veto (PR #2617 comment #40524).
2026-07-20 20:55:27 +02:00
iris
584dfed0c9 fix(#2305): run_cmd returns Result, nix fmt, collapse nested if
- run_cmd now returns Result<String, String> — callers pattern-match
  instead of comparing against an "ok" sentinel string
- Add cmd_result() helper to format run_cmd results as tool strings
- mouse_click: collapse nested if-let into let-chain (clippy collapsible_if)
- nix fmt: reformat screen.nix package list
2026-07-20 20:55:27 +02:00
iris
0b3268feae feat(#2305): hive-screen-mcp — screenshot + input MCP for GUI agents
New crate hive-screen-mcp: a stdio MCP bridge activated automatically
when an agent has hyperhive.gui.enable = true. Provides five tools:

- screenshot   — grim → saves PNG, returns path for Read tool
- type_text    — wtype → Unicode text input (no daemon)
- key_press    — ydotool key → combos like ctrl+c, super+l
- mouse_move   — ydotool mousemove --absolute
- mouse_click  — ydotool click, optionally with prior move

New nix/agent-modules/screen.nix: wires the MCP bridge into
extraMcpServers.screen; adds grim + wtype to systemPackages. Adds
hyperhive.gui.screenInput option (default false) which enables the
ydotoold daemon + ydotool for mouse/keyboard injection via /dev/uinput.

screenshot and type_text work without screenInput. key_press,
mouse_move, and mouse_click return a ydotool error until ydotoold is
running and /dev/uinput is accessible in the container.
2026-07-20 20:55:27 +02:00
damocles
3f1643c594 refactor(#2464): rename hive-ag3nt crate to hive-agent, collapse lib into main 2026-07-15 16:09:39 +02:00
müde
79fec131d6 docs: sweep archaeology comments to present state 2026-07-13 22:16:02 +02:00
müde
e0cfed7fe8 refactor: jq for tea-login, build-time avatar png, shared leaf-sign script 2026-07-13 22:14:25 +02:00
müde
4a48ce5024 refactor: nix/host-modules + nix/agent-modules layout, update doc paths 2026-07-13 22:05:49 +02:00