Commit graph

3,465 commits

Author SHA1 Message Date
iris
90cd602d4e hive-bash-mcp: drop the command from the running-task todo entirely
Per mara: just show the task name, don't show any command — if an
agent doesn't name a task itself it still gets the id back in the
run tool's own result, so the todo doesn't need to repeat it. Drops
short_cmd_label (and its tests) added in the previous commit; the
summary is now just "bash task `<id>` running".
2026-08-14 02:24:42 +02:00
iris
31c34939ef hive-bash-mcp: shorten the running-task todo label instead of embedding the whole command
The todo pushed at task start embedded the entire raw shell command
(task.cmd) as its summary — for a multi-line heredoc script (a common
agent pattern), that balloons every UI that renders todo summaries to
the command's full line count (hyperhive#3248).

short_cmd_label keeps only the first non-blank line, char-truncated to
100 chars, with a trailing ellipsis whenever either the line itself
was cut or more lines follow — so a short-looking first line ahead of
a long heredoc body still reads as truncated, not as the complete
command. The full command is still on disk in the task file for
status/view; this only shortens the todo label.
2026-08-14 02:24:42 +02:00
damocles
36b2e2a16c job_queue: remove the now-duplicated forge sweep + webhook tokio::spawn blocks in main.rs 2026-08-14 02:23:05 +02:00
damocles
f367fa518e job_queue: submit boot-time forge/matrix/webhook/knowledge sweeps as DAG nodes 2026-08-14 02:23:05 +02:00
atlas
e15c499a31 fix(#3245): resolve the remaining intra-doc links in hive-c0re
Takes the crate from 26 rustdoc warnings to 1, on top of the ten in the
previous commit.

argus's review findings:
- agent_sockets.rs: [`write`] was still ambiguous (function vs macro).
  The previous change narrowed the qualifier and left the ambiguity;
  [`write()`] is what resolves it.
- forge/users.rs <hex> and stats/container_stats.rs <name>: unclosed
  HTML tags in prose, now backticked.

The rest of the crate, so the count actually reaches zero:
- job_queue/mod.rs: Queue::graph_snapshot -> JobQueue::graph_snapshot
  (there is no Queue type), and super::scheduler -> scheduler (mod.rs
  *is* job_queue, so super:: pointed outside it)
- job_queue/resource.rs: NodeKind -> super::model::NodeKind
- matrix.rs: password_path(name) -> password_path; and
  forge::provision_user_token -> crate::forge::provision_user_token.
  Note the path has no `users` segment: forge/mod.rs declares `mod
  users` private and re-exports it, so the canonical path comes from the
  re-export rather than the directory tree.
- socket_server/lifecycle_handlers.rs: InfraContainer ->
  hive_priv_sock::InfraContainer
- stats/otel_metrics.rs: crate::meta::otel_config is a private fn no
  path can name from another module, so it becomes prose
- main.rs: redundant explicit link target dropped

coordinator.rs:405 (CrashWatchGuard) is deliberately untouched: #3244
deletes that doc block, so fixing it here would conflict with an open PR
and repair a symbol that is about to stop existing.
2026-08-14 00:25:35 +02:00
bitburner
517acc9f33 fix(#3245): resolve broken intra-doc links in hive-c0re
Remove or fix broken documentation links that accumulate silently:
- container_view.rs: HiveEnv reference
- forge/mod.rs: READY_TIMEOUT and webhook handler links
- workers/knowledge.rs: webhook handler link
- job_queue/model.rs: Claim::deps and WireNode::data references
- stats/hive_stats.rs: read_skill_breakdown reference
- stores/audit_log.rs: global() reference
- workers/agent_sockets.rs: ambiguous agent_sockets::write reference
- coordinator.rs: systemd.services.<harness> formatting
- resource_limits.rs: ambiguous write/read references

Some broken links were to deleted functions/types; these are replaced
with prose descriptions. Others referenced items outside this crate or
were private; these are replaced with plain text references or qualified
paths as appropriate.

Fixes: #3245
2026-08-14 00:25:35 +02:00
atlas
40403fc6d5 docs(#2916): drop two more doc-comments left dangling by the deletion
argus caught these on review. `recent_transient`'s doc still pointed at
NO_NODE_LABEL, and `running_transients`' still described destroy as
having no queue node behind it and linked suppress_crash_watch -- all
three deleted in the parent commit, so the first two were broken
intra-doc links and the third was simply false.

Migration is now the only operation in that sentence.
2026-08-14 00:24:45 +02:00
atlas
6338939657 refactor(#2916): destroy submits a DAG instead of an imperative teardown
Destroy was a straight-line async fn with no queue node behind it, so
nothing in the graph could answer "is this container going down on
purpose?". That gap is why an imperative crash-watch suppression guard
existed: an RAII handle held for the operation's duration, a second way
to say what every other lifecycle op already says through its node.

Reuse the existing Stop node rather than teaching a new node to stop
things:

    Stop -> DestroyContainer -> (PurgeState) -> DestroyBookkeeping

Stop already declares takes_container_down honestly, so the suppression
is now derived from the graph like every other op's. It also turns the
precondition into an edge: DestroyContainer runs only under a completed
Stop, so it operates on an already-stopped container and carries
takes_container_down = false permanently. A container still alive at
that point is a real bug and stays loud instead of being absorbed by a
flag -- which matters because a wrong true silently swallows a crash
while a wrong false only costs a spurious event.

Removes suppress_crash_watch, CrashWatchSuppression, crash_suppressed,
crash_watch_suppressed and NO_NODE_LABEL. The migration call sites went
with the obsolete startup migrations, so destroy was the last caller and
intent now has exactly one home.

destroy() becomes a submit-and-return, matching every sibling endpoint
(rebuild, kill, restart, start, pause, resume) -- it was the only
lifecycle op that awaited its work. The container rescan moves into the
bookkeeping tail, so ContainerRemoved now arrives after the 200 rather
than before it.

Also drops an orphaned doc-comment in coordinator.rs: two stacked blocks
where only the second described crash_suppressed, the first documenting
a field that no longer exists. Removing the field would have re-pointed
it at recent_transient.
2026-08-14 00:24:45 +02:00
damocles
2c7872841a hive-agent: guarantee a wake after a self-requested /compact 2026-08-13 23:17:10 +02:00
damocles
fa658567db hive-priv: replace json! with typed structs for account sidecar files 2026-08-13 23:16:17 +02:00
atlas
9f26c416c0 hide .git on the agent config mounts, like /knowledge already does
An agent's config mount is a git repo, so it could read every branch and
the full history of a config whose currently deployed value is the only
thing it may act on -- and an abandoned branch looks no different from a
live one.

The knowledge bind already solved this with an empty tmpfs overlaid on
its .git. Same rule, extended: an agent is handed a working tree, never
a repository. Folds both cases into git_overlay_flags so the reason is
stated once instead of hardcoded per mount.

Config mounts are matched by shape rather than a name list because the
set grows at runtime with each child bound into a parent.
2026-08-13 21:01:08 +02:00
atlas
c32a9367e4 gateway: reject unmatched Host instead of serving the dashboard
The `_` vhost was serving the hive's own surface, so every dashboard and
agent-UI request matched the default server rather than a named vhost --
and so did a request for any name at all, including a raw IP.

Split it: `_` keeps only `return 444`, and the hive surface moves to a
vhost named for the hive domain. `_` is `mkDefault` so an operator can
claim default_server themselves, plus an assertion for the case where
they add one without turning ours off -- nginx refuses to start on a
duplicate default_server and nixpkgs asserts nothing, so that would
otherwise surface as a gateway outage at rebuild time.
2026-08-13 19:57:27 +02:00
damocles
786e4610f0 hive-agent: replace json! with typed structs in web_ui handlers 2026-08-13 19:43:58 +02:00
atlas
127846ef1b mount the applied config repo, not the proposed one
/agents/<name>/config bound the working clone a config change is staged
in, so an agent could see a proposal that was never approved -- a config
that does not govern its container. Both objects already exist; this
repoints the bind at the deployed one.

Both mounts (own + child) now resolve through config_bind_source() so
they cannot drift, and agent_proposed_dir's doc-comment is corrected:
it claimed to be manager-editable and bind-mounted, and neither is true.
2026-08-13 19:31:17 +02:00
damocles
351341e87c hive-c0re: replace json! with typed structs in dashboard handlers 2026-08-13 18:12:35 +02:00
atlas
fbffccbbb2 feat(nix): a flake check that actually covers nix
Every other check in nix/checks.nix is a Rust derivation, so a
.nix-only diff moves no hash, the whole set is cache hits, and
`nix flake check` reports green without evaluating what changed.

`checks.module-eval` is one derivation holding a table of cases, each
named by the PROPERTY it defends. Its builder text embeds the evaluated
results, so the derivation's hash is a function of them: a nix change
that flips a property rebuilds the check and fails in the builder,
naming that property.

PROVEN, not assumed — the mechanism was executed before the cases were
written. Same expression with one property true vs false:

  drvPath true  -> 5v11mnbv…-module-eval.drv
  drvPath false -> ivm3dvv8…-module-eval.drv        (differs)
  build false   -> FAILS, stderr names the property

and the table itself was mutation-tested: inverting one case's
expectation gives `FAILED: a hive that has not opted into all-local
runs no swarm controller / module-eval: 1 of 5 properties broke`. A
check that cannot go red on a broken tree is not evidence.

Cases are named by property and never by ticket: a case named after the
ticket that prompted it has that ticket's lifetime; one named after the
property lives as long as the property does.

⚠️ It evaluates, it does not execute. Where the artifact is a command
line, a request or a certificate, a value assertion cannot stand in —
that is written into the file's header, because the gap is exactly what
made two earlier outages evaluable-but-broken.
2026-08-13 17:26:58 +02:00
atlas
f80facbbe0 refactor(3202): all-local asserts the host's own /etc/hosts entries
Clause 2 of #3202, reading 1 (mara: "the all local stuff and swarm
services auto conf belong in those mods, not spread all over").

`gateway.localHostsEntry` is the gateway's only local-deployment knob —
`openFirewall` is about EXTERNAL exposure, `tls.acme` needs a public DNS
name, `hsts` is a hardening choice. It is now asserted by the mode in
local-defaults.nix, beside the three swarm toggles, instead of being the
one all-local implication an operator still had to know about.

`mkDefault`, so "all local except this" still needs no new option.

⚠️ The non-obvious half: this does NOT change what CONTAINERS resolve.
dnsmasq sets `no-hosts = true` unconditionally, so agents keep getting
the bridge IP from the authoritative `address=` rules rather than the
host's 127.0.0.1 — which would point every agent at its own netns. That
guard already existing is what makes this safe to default on; without it
this one line would break every agent's access to the forge.
2026-08-13 17:26:08 +02:00
atlas
030eef0948 refactor(3202): the swarm UI declares its own vhost and dns name
Last of the four. The vhost, its `auth_request` block and the swarm
apex's dns record move into swarm-ui.nix; vhosts.nix drops `uiCfg`,
`controllerCfg` and `autheliaCfg` and is now 259 lines of hive surface
with no swarm service in it.

Also collapses a THIRD copy of the per-service list. `networking.hosts`
restated every service's name with its own copy of that service's guard,
after the vhosts and the dnsmasq records had each done the same. It asks
the same question — which names does this host answer for — so it now
reads the same answer: a service added later lands in /etc/hosts with no
edit, and cannot land there under a different condition than it used for
DNS.

The `forceSSL`-not-`addSSL` comment travels intact: it records that
authelia answers an http auth subrequest with 400 and nginx's
auth_request only understands 2xx/401/403, so the scheme is load-bearing
for this vhost and no other.
2026-08-13 16:19:08 +02:00
atlas
6caf177416 refactor(3202): matrix declares its own vhost, dns name and SPA map
Moves the matrix sub-domain vhost out of the gateway's vhosts.nix, its
`address=` rule out of dnsmasq.nix, and the Accept-header
`$matrix_spa_target` map out of the gateway's appendHttpConfig — all
three into hive-matrix.nix.

The map is the one that had no business being where it was: it exists
solely for the SPA fallback in the vhost's `/` location, and
`appendHttpConfig` is a `lines` option, so a module can contribute to
it without the gateway assembling it.

The `.well-known/matrix/*` delegation deliberately stays on the hive's
own vhost. The spec requires it at the SERVER NAME, which is the hive
domain: that is the hive answering "where is my homeserver", not the
homeserver answering for itself. Moving it would have been the obvious
symmetric thing and it would have been wrong.
2026-08-13 16:14:37 +02:00
atlas
56ab6d26c1 refactor(3202): authelia declares its own vhost and dns name
Moves the authelia vhost out of the gateway's vhosts.nix and its
`address=` rule out of dnsmasq.nix, into swarm-authelia.nix.

Both land inside that module's existing `cfg.enable` guard, which is
the load-bearing part: every hive in a swarm knows `authelia.url`, but
only the host that RUNS the container may claim the name. A client hive
declaring the vhost would answer for a service it does not run, and
publishing the DNS record would point every agent on its bridge at that
wrong answer.

The kit grows a fourth member, `errorPages`, because the vhost aims its
502/503/504 at the gateway's styled sso-unavailable page. Republished
rather than imported per module: a service rendering its own would drift
from the rest of the gateway the first time the theme changed.
2026-08-13 16:14:37 +02:00
atlas
d60a0585d6 refactor(3202): the forge declares its own vhost and dns name
Moves `forgeVhost` out of the gateway's vhosts.nix and the forge's
`address=` rule out of dnsmasq.nix, into nix/host-modules/hive-forge —
the module that already owns everything else about the forge.

The gateway keeps what is gateway knowledge (the listen set, which
issuer covers a name, the header block) and loses the last reason it
had to read `swarm.forge` at all: `forgeCfg` is gone from both files
and from the module's `let`.

Both halves stay gated on `behindGateway` — with it off the operator
fronts forgejo themselves, so this hive must neither claim the vhost nor
answer DNS for the name.
2026-08-13 16:14:36 +02:00
atlas
991cd24fc8 refactor(3202): the gateway takes contributed dns names
Adds `services.hyperhive.gateway.localNames` (internal): hostnames the
hive resolver answers with the bridge IP, contributed by the modules
that own them. The service says which name, the gateway says where it
points — the same split `lib.tlsFor` already makes.

No behaviour change yet: the list is empty until the service modules
contribute in the following commits.

The assertion is not defensive padding. Duplicate `address=` rules do
not make dnsmasq complain; it resolves them by precedence, so a name
claimed twice silently stops being served by one of its claimants. That
failure mode only becomes reachable because contribution is now open, so
it gets closed in the same commit that opens it.
2026-08-13 16:14:36 +02:00
damocles
b4e56b964f pr-merge: don't tell the caller to just un-draft a PR that isn't theirs 2026-08-13 15:21:12 +02:00
damocles
dd5ccb5ce7 hive-forge: don't assert 'conflicts' from a bare mergeable=false 2026-08-13 15:21:12 +02:00
damocles
145a20eae6 fix the other 3 UpsertTodo construction sites CI caught 2026-08-13 15:01:44 +02:00
damocles
1b72ed56ff todos: reopen an acked keyed row when the caller says so 2026-08-13 12:55:20 +02:00
atlas
d5782965db refactor(3202): the gateway publishes its vhost construction kit
Slice 1 of #3202. The listen set, the per-name TLS attrs and the
security headers move out of vhosts.nix into ./vhost-lib.nix and are
published as `services.hyperhive.gateway.lib` (internal, readOnly).

No behaviour change: vhosts.nix consumes the published value, so the
rendered vhost tree is identical.

The point is the next slice. Today a swarm service's vhost lives in
the gateway because only the gateway knows the port pair, the issuer
for a name, and the header block. Publishing those three is what lets
a service module declare its own vhost without the gateway having to
know that service by name.
2026-08-13 12:46:50 +02:00
atlas
272944b98f docs: delete two false cost claims about the boot sweep's drains
Both said a whole-hive graceful stop costs ONE `GRACEFUL_STOP_TIMEOUT`
in total because drains overlap. That is only true for a power op. In a
rebuild subtree the brace holds the build slot across the whole subtree,
drain included, so the boot sweep's per-agent drains serialise and the
sweep costs one timeout per wave of `buildSlots`.

Deleted rather than corrected. The right cost statement depends on an
operator knob and belongs in docs/coordinator.md if it belongs anywhere;
a comment that has to hedge about a config value is the kind that goes
stale silently. A comment saying nothing beats one that lies.
2026-08-13 12:46:06 +02:00
iris
e7f4a19939 swarm-ui: real hive-roster overview page
Fixes hyperhive#3223.

swarm-controller: GET /api/hives (utoipa-annotated same as /health),
serving the swarm's hive directory (name + domain) loaded once at
startup from a new SWARM_CONTROLLER_HIVES env var. The controller's
NixOS module sets it from services.hyperhive.swarm.hives, JSON-encoded
the same way hive-c0re already builds HYPERHIVE_PEERS for its own peer
list (environment.nix) — the full directory here rather than
peers-minus-self, since a swarm-level daemon has no 'self' hive to
exclude. Unset/malformed both fall back to an empty list with a
warning rather than failing startup, so /health stays answerable even
if this one env var is wrong.

swarm-ui: App.tsx's Home route fetches /api/hives and renders it
through the already-merged <Table>/<StatusChip>/<Panel> primitives —
name, domain (linking out to that hive's own gateway-routed
dashboard), and a static "configured" status chip until a real
online/stale/offline rollup exists server-side. Also gave swarm-ui a
base <a> color (theme's --blue) — base.css covers body/typography but
not links, and this is genuinely page-level rather than any one
component's concern.

Verified end to end, not just source-reading: ran the real
swarm-controller binary with SWARM_CONTROLLER_HIVES set, curled
/api/hives + /health over its actual unix socket; separately served
the real swarm-ui dist against a mock /api/hives and screenshotted the
rendered table. Also re-verified the nginx wiring evaluates (same
throwaway nixosSystem eval technique as #3212) — SWARM_CONTROLLER_HIVES
resolves to the expected JSON shape.

cargo test/clippy -p swarm-controller clean (2 tests, including a new
load_hives one covering missing/malformed/valid env var states). npm
run build + typecheck clean.
2026-08-13 11:21:30 +02:00
damocles
5300c242f3 skills(forge-issue-filing): soften labels section per review 2026-08-12 22:03:41 +02:00
damocles
ef40b03c87 skills: add forge-issue-filing skill 2026-08-12 21:56:00 +02:00
iris
4828c96957 swarm-controller: OpenAPI spec + gateway swagger UI wiring
Rust half mirrors hive-c0re/src/dashboard/mod.rs's utoipa pattern
exactly: an ApiDoc root, #[utoipa::path(...)] on /health (the one
existing route), and a raw JSON route at /api/openapi.json served
via OpenApiRouter::split_for_parts(). Only annotated routes appear
in the spec.

Gateway wiring extends the swarm-UI vhost (the only vhost swarm-
controller is reachable from) with:
- /api/ — proxied to the controller's unix socket untouched (no URI
  segment after the socket path), so a route swarm-controller
  registers is the path nginx forwards, no prefix-stripping to keep
  in sync by hand.
- /api/docs/ (+ the bare /api/docs redirect) — the same
  swagger-ui-theme dist the per-hive dashboard already serves at its
  own /api/docs/, reused as-is since it's generic.

Both new locations reuse the same auth_request block the vhost's own
'/' already applies, factored into a shared swarmAuthRequest string —
auth_request does not inherit across sibling nginx locations, so
without this the page itself would be gated while its own API and
API docs sat open.

cargo test -p swarm-controller + cargo clippy --all-targets both
clean. Verified the new nginx wiring evaluates correctly with a
throwaway nixosSystem eval (services.hyperhive.swarm.{controller,ui}
enabled): /api/ proxies to the socket, /api/docs redirects, and both
require auth_request the same as the vhost root.

Fixes hyperhive#3212
2026-08-12 21:36:01 +02:00
iris
8642d4acf6 swarm-ui: colocate component CSS as JS-side-effect imports
Per mara's review question on PR#3219 ('shouldnt the jsx files import
their css?'): each component now does its own import ('./Shell.css',
'./Panel.css', ...) instead of swarm-ui.css centrally @import-ing
every component's stylesheet. esbuild folds every .css reachable from
main.tsx's import graph into one main.css companion output next to
main.js — no separate build step, this is bundle:true's existing
behavior, just not exercised until now.

swarm-ui.css keeps only the shared base reset (@hive/shared/base.css)
since that isn't any one component's concern. Added src/css.d.ts
(ambient '*.css' module) since tsc otherwise rejects a side-effect
import of a non-JS/TS specifier.

Side benefit: a component nothing imports (yet) no longer ships its
CSS either — StatusChip/Table aren't referenced from App.tsx today,
and main.css correctly only carries Shell.css + Panel.css. The old
central-import approach shipped all four unconditionally.

npm run build + typecheck clean. Re-screenshotted the real dist —
pixel-identical to before this change.
2026-08-12 21:33:44 +02:00
iris
55f9e6c15f swarm-ui: one subdirectory per ui/ component
Per mara's PR review request: Panel/StatusChip/Table each move into
their own subdir (ui/panel/, ui/status-chip/, ui/table/) colocating
the component with its stylesheet, matching shell/ (Shell.tsx +
Shell.css already lived this way). Import paths in App.tsx and
swarm-ui.css updated to match; no behavior change.

npm run build + typecheck both clean.
2026-08-12 21:33:44 +02:00
iris
67eb0c660d swarm-ui: page shell + layout primitives
Structural foundation split out of hyperhive#3118 per mara's steer:
structure first so the real overview page (hive roster) and later
routes (swarm-wide agent roster) land as content changes rather than
each reinventing chrome + nav + a table/panel/chip shape.

- <Shell>: header bar (branding) + nav row, wraps every route. Route
  list lives in Shell itself (one small SPA, one place to know its
  own nav).
- ui/Panel, ui/StatusChip, ui/Table: the three primitives the
  overview page's actual scope (hive roster: name/domain/status,
  linking out to each hive's own dashboard) calls for, nothing
  speculative beyond that.

Preact-native styling (plain CSS files imported via swarm-ui.css, no
shadow DOM — this package renders into light DOM) — not
@hive/shared's chrome.css, which is the old MPA dashboard's visual
language. Same base16/Catppuccin color tokens via theme.css/colors.css
so it still reads as hyperhive.

npm run build (whole frontend workspace) + npm run typecheck both
clean. Verified with a real headless-chromium screenshot against the
built dist, not just source-reading.

Fixes hyperhive#3211
2026-08-12 21:33:44 +02:00
atlas
75f99ecafb feat(3216): swarmctl shell completions
Mirrors hivectl exactly: a `completions <shell>` verb that walks the
live clap tree, and a package that pipes it into installShellCompletion
for bash/zsh/fish. Generating from the command tree rather than writing a
script by hand is what keeps completions from drifting away from the
verbs they complete — the same reason `markdown-docs` renders the docs
from that tree.

Dispatched before PathArgs::resolve() for the same reason markdown-docs
is: emitting a completion script needs none of the SWARMCTL_AUTHELIA_*
deployment env vars, and requiring them would make the package's own
build-time invocation fail — exactly where it runs.

swarmctl leaves mkBinPackage for its own derivation, since the extractor
installs a binary and nothing else.
2026-08-12 21:09:29 +02:00
atlas
0b1b08dfe6 fix(3149): the host asks the hive's resolver, at the bridge IP
Per mara: a general fix, not one name in one container.

Every container inherits a COPY of the host's /etc/resolv.conf at start
(nixos-containers.nix: cp --remove-destination, one shot, not a
bind-mount), so the address written there is the address every container
tries - in its own netns. That makes the value load-bearing:

  value       host  host-netns containers  bridged containers
  127.0.0.1    ok           ok             THEIR OWN loopback
  bridge IP    ok           ok                    ok

dnsmasq binds lo and the bridge, so the bridge IP works for the host
too. It is the only value correct on both sides of a netns boundary.

resolveLocalQueries goes ON for its plumbing, not its address: it points
dnsmasq's own upstreams at a separate resolv-file, without which
dnsmasq reads /etc/resolv.conf and every non-hive query loops the moment
the host is pointed at dnsmasq. Its two loopback-publishing effects
(networking.nameservers and resolvconf.useLocalResolver) are overridden.

Cost: the host's DNS now depends on dnsmasq being up. Every container
already did.

The forge container keeps its hosts entry from the previous commit -
not redundancy, a fallback in a different failure domain: it works with
no DNS at all, so SSO does not ride on a host-wide resolver change.
2026-08-12 21:09:21 +02:00
atlas
25d5b4b69c fix(3149): the forge container can resolve the authelia name
The login source still failed after the argv fix, with

  dial tcp: lookup auth.constellation.darkest.space: no such host

The hive's dnsmasq is authoritative for the swarm service names, but
only containers whose resolv.conf points at the bridge ask it — agent
containers do, via an explicit unit written for that reason
(nix/agent-modules/network.nix). hive-forge resolves through the host's
resolvers instead, and the swarm domain has no public records, so
discovery fails for a name that resolves fine one container over.

Publish it in the container's own hosts file, mapped to 127.0.0.1:
sharing the host netns, loopback is the host, where nginx already serves
that vhost. TLS still validates - the CA trust bundle is bind-mounted
and the leaf covers the name. Gated on authelia being local, since a
remote provider's name belongs to another machine.

Gate (state/eval-3149-hosts.sh) asserts the RENDERED /etc/hosts rather
than the option, plus an absence probe with SSO off and a check that the
discovery URL names the same host the entry publishes.
2026-08-12 21:09:21 +02:00
iris
861a1f8f26 job_queue: filter jobq graph snapshot by per-node state
graph_snapshot previously filtered which whole roots got projected
based on the root node's own state, so a group root that was still
Running but had already-Done internal steps couldn't be filtered
down to just its live nodes, and a filtered-out root hid its entire
subtree even when a descendant still matched.

Apply the states filter after GraphWire::wire_snapshot instead, over
every node in the flattened tree, not just roots. The jobq-graph
client already handles an orphaned node (parent filtered out) by
promoting it to a rendered root, so this is safe on the client side
with no changes needed there.

Fixes hyperhive#3210
2026-08-12 20:55:25 +02:00
atlas
0b6b3b755d fix(3044): a parent's mount of a child's config is read-only
The parent's copy is for reading a child's config; a change to it is a PR
on the child's repo, made from a clone and merged after review. A
writable mount is a second path to the same file that skips that review,
which makes the boundary a convention rather than a permission.

Confirmed with ruth before flipping: it clones from the forge and opens a
PR, including for a brand-new child's first config.

The prose was the larger half. docs/approvals.md did not merely describe
the old mount, it *instructed* agents to use it ("can therefore edit,
commit, and submit changes for any of its direct children directly inside
its container"), and the doc comment in host_config.rs asserted a
dependency that never existed: the InitConfig seed runs as hive-c0re
against the host path, and read_only on a bind constrains writers inside
the container only. That comment is what produced issue #3206, now closed
as invalid.
2026-08-12 20:33:47 +02:00
atlas
d03549c40f fix(3213): the swarm UI vhost forces https
authelia refuses to authorize a non-https target: the auth subrequest for
`X-Original-URL: http://…` answers 400, and nginx's auth_request only
understands 2xx/401/403, so a plain-http visit died as "auth request
unexpected status: 400" — a bare 400 in the browser with no hint that a
login exists.

The shared vhostListen binds :80 as well as :443 and every vhost used
addSSL, so the door was open on a port the lock cannot work on. That is
harmless for forge and matrix, where http is merely insecure rather than
broken, so the asymmetry stays local to the one vhost whose correctness
depends on the scheme.

removeAttrs because nixos asserts on a vhost declaring both addSSL and
forceSSL.

Measured against the pinned binary rather than reasoned about:
  X-Original-URL: http://constellation…/   -> 400 Bad Request
  X-Original-URL: https://constellation…/  -> 401 + Location: auth…?rd=…
2026-08-12 20:12:45 +02:00
iris
4b71a76b12 agent: drop dead .header-pill-loose CSS
Styled variant with zero HTML/JS reference anywhere in the frontend
tree, found while surveying pill/chip/badge sites for the "extract
pill component" issue. The extraction it was originally paired with
(PR#3197) got closed as not worth the churn for 2 instances, but this
one finding stands on its own — no reason to carry dead CSS forward.

npm run build clean.
2026-08-12 19:33:07 +02:00
iris
8dd6d05d6c frontend: unify pill/chip/badge shape into shared CSS
Per mara's steer on #3053 ("chip/pill/badge is the same if you squint
... theme unification is part of the goal" then "make it common css
instead of component, thats fine. but make them look unified (not as
much per usage css)"): shared CSS, not a JS component.

New @hive/shared/pill.css defines two classes, `.hive-pill` (primary
state chips) and `.hive-pill-sm` (secondary meta chips) — border/
border-radius/padding/font-size/letter-spacing (colour stays per-site,
the meaningful semantic part). Every render call site across dashboard
(swarm.js/core.js/builds.js) and agent (index.html/app.js) now carries
one of the two shared classes directly, alongside its own existing
semantic-colour modifier class.

Second cut of this PR, per argus's approve + mara's follow-up review
comment on the first cut: the first version instead enumerated every
legacy classname (`.badge`, `.status-badge`, `.header-pill`, etc.)
straight into pill.css's own selector groups so no call sites needed
touching. Mara's correction: that just relocates the duplication
rather than removing it, and the shared CSS shouldn't have to keep
naming every consumer. This version does the real rename instead.

Most visible consequence, unchanged from the first cut: dashboard's
`.badge` family moves off its own shape (2px square corners, uppercase,
tighter padding) onto the shared rounded-pill shape + agent's "sm" tier
sizing. `npm run build` clean across all three packages; verified the
compiled bundles carry the new classnames at every call site (dashboard
JS, agent index.html + app.js), not just the source tree.

Fixes #3053
2026-08-12 19:25:45 +02:00
iris
92b9e67327 frontend: unify pill/chip/badge shape into shared CSS
Per mara's steer on #3053 ("chip/pill/badge is the same if you squint
... theme unification is part of the goal ... make it common css
instead of component, thats fine. but make them look unified"):
shared CSS, not a JS component.

New @hive/shared/pill.css defines the shape (border/border-radius/
padding/font-size/letter-spacing — color stays per-site, that's the
meaningful semantic part) in two tiers matching what was already
organically in use: `.hive-pill` (primary state chips) and
`.hive-pill-sm` (secondary meta chips). Every pre-existing classname
that drew its own copy of this shape (dashboard's `.badge` family,
agent's `.status-badge`/`.state-badge`/`.header-pill`/`.ctx-badge`/
`.model-chip`/`.effort-chip`) is folded straight into the same
selector groups, so no markup or JS changes were needed anywhere —
every render call site keeps constructing the exact same classnames
it always did.

Most visible consequence: dashboard's `.badge` family moves from its
own shape (2px square corners, uppercase, tighter padding) onto the
shared rounded-pill shape + agent's "sm" tier sizing, matching the
"look unified" ask directly. `npm run build` clean across all three
packages.
2026-08-12 19:25:45 +02:00
atlas
ae05289e8b docs(3201): setup guide — the group flag is not one-shot any more
The step-3 warning said to keep `--group admins` and left the impression
that a later re-run would fix a missing one. It would not: `user add`
refuses on an existing name, so until `user update` existed the only
repair was hand-editing two files as root. mara asked the question the
warning should have answered — "will the cmd just add the group or do i
get a whole new user?".

Deliberately in this PR rather than #3199's: the sentence only becomes
true once the verb it names exists.
2026-08-12 19:23:31 +02:00
atlas
24ee0990a2 feat(3201): swarmctl user update — change an existing subject's attributes
`user add` refuses on an existing name, so the `--group` flag it takes at
creation time could not be added afterwards at all: repairing an account
meant hand-editing both users.json and the rendered users.yml as root.
mara, on #3167: "i will not edit those files by hand, we will have the
same issues elsewhere".

The merge rules live in users.rs as a pure function over a UserUpdate, so
they are testable without a command line, a container or a running
authelia — main.rs's arm only loads, applies, publishes and prints.

Removals are strict and everything else is idempotent, which is the one
asymmetry here and is deliberate: a --remove-group naming a group the
user does not have fails, because a revocation that reports success
without revoking is the outcome nobody re-checks; while refusing an
already-satisfied set would make the multi-attribute call this verb
exists for break whenever one of the values was already right.

A command that changes nothing at all still fails — it would otherwise
rewrite both files and restart the SSO provider to no effect.

Passwords are out of scope: regenerating a credential is a different
intent from editing an attribute, and folded together an attribute edit
can invalidate a login by accident.

Extracts publish() from user_add so both verbs share the
render -> store -> users.yml -> restart ordering and the comment that
explains why that order, rather than the second verb copying it.
2026-08-12 19:23:31 +02:00
iris
086f3f43d6 claude-plugins: add headless-screenshot skill to base
Captures a lesson from this session: "no browser in this container"
usually means no browser pre-installed, not unreachable. nixpkgs#chromium
is a `nix shell` away and one headless invocation renders a page +
writes a PNG, no puppeteer/playwright needed for a static screenshot.

Every agent hits "I have no browser" sooner or later when reviewing a
frontend change; this has been rediscovered independently across
several sessions rather than remembered. Belongs in base (every agent
needs it, not role-specific) alongside the sibling skills already
there.
2026-08-12 18:31:55 +02:00
atlas
4dd5093c6f docs(3167): the setup guide gains a swarm UI step, and one group name
Per mara on the PR: the guide someone follows on a fresh deploy is a
different page from the one they read while debugging, and only the
second existed.

Also fixes a contradiction the request surfaced: setup.md and
swarm/sso.md have told every operator to create '--group admins' since
the bootstrap step existed, while the new rule required 'operators'.
This is the first rule that CONSUMES a group name, so it takes the one
the guide already creates - inventing a second would have meant every
account made by following the guide silently failing the check it was
supposed to pass, and for mara specifically a migration step that now
may not be needed at all.

setup.md's step 3 says why --group is not decoration; the new step 4
says what decides whether the page opens (the group, and the name
resolving) rather than listing a command, because there is nothing to
run. Steps renumbered, including the matrix block's internal 4a-4e.
2026-08-12 17:57:52 +02:00
atlas
40e1ed2967 fix(3167): wire the swarm-ui package from the flake, not a pkgs attr
The option defaulted to pkgs.swarm-ui, which does not exist: this
project has no overlay - flake.nix's nixosModules.default wires package
options with mkDefault from its own package set, and swarm.controller
does exactly that. The default would have failed to evaluate on any real
deployment, not just in a test harness.

Found by the gate forcing .package: the earlier probes passed because
they only read option values that never touched it.
2026-08-12 17:49:21 +02:00
atlas
470d2ad845 docs(3167): the swarm UI page, and the group step that gates it
New docs/swarm/ui.md (split-page shape, per the docs rule), linked from
the swarm README and added to the gateway's vhost map.

Leads with the step that separates 'protected' from 'locked out':
swarmctl user add <you> --group operators. auth_request asks whether
there is a session; the access_control rule is what makes it mean
operator, and an account created before the rule existed has no groups.

Also records the four wiring sites a swarm service name needs, with the
certificate one called out - serviceDomains is both the sub-CA's
nameConstraints set and the leaf's SANs, and the apex is a sibling of
the other three rather than a parent, so nothing issues for it
implicitly.
2026-08-12 17:46:15 +02:00