An operator links an agent's GitHub personal access token in the swarm UI
(LinkGithubAccountForm, "link github account" on /agents). swarm-controller's
PUT /api/hives/{hive}/agents/{agent}/github-account stores it at
swarm/agents/<agent>/github-token (swarm_secret_client::github), a flat leaf
under the agent's prefix that the agent's existing read grant already covers:
no policy change, and no list grant, since there is one token per agent.
In the agent, hive-agent-github-token (oneshot + 2-minute timer, as the agent
user, under its own store certificate, ordered before hive-github-notify)
reads that path and writes <state>/github-token, 0600 and agent-owned, the
file the gh wrapper, git credential helper and hive-github-notify already
read. It replaces the file by rename only when the bytes changed and never
deletes it: a hive-written github-token stays until a token is linked in the
swarm UI. It is installed only with a store address and
services.hyperhive.agent.github.enable.
Removed: the dashboard's CR3D3NTIALS page (credentials.html/js/css, its
build entries and H0M3 tile; GITHUB was its only tab), hive-c0re's
dashboard/matrix_accounts.rs with GET/POST /api/github-account,
priv_client::write_agent_github_token, the host socket's
SetAgentGithubToken and `hivectl github set-token`, and hive-priv's
WriteAgentGithubToken with write_agent_state_file, its only caller gone.
Docs: integrations/github.md and swarm/ui.md describe the swarm path,
swarm/credentials.md gains the store-path row, and the hive UI docs,
hivectl docs and security.md's hive-priv table drop the removed pieces.
Closes#4347
The agents root is 0700 and owned by the daemon's user, so hivectl's
client-side existence guard hit EACCES on traversal for anyone not root.
It reported that as "this command needs root; re-run with sudo", which
turned three verbs' pre-flight check into a permission error about the
wrong thing: `choom`, `subvol upgrade` and `subvol snapshot create` all
failed at the guard rather than at whatever they actually needed.
The daemon runs as the owning user and already answers this question for
its own provisioning paths, so expose it on the host socket as
`AgentExists` and have hivectl ask. Operators reach that socket through
the `hive-admin` group, so the guard now works without sudo.
`choom` still needs root for `machinectl shell` — we ship no polkit rule
granting those actions — so it now checks the effective uid and says so
directly instead of failing later inside systemd's authorisation.
`hivectl open forge` on a host where the daemon is fine and the socket is
fine printed "could not reach the hive-c0re daemon for URLs — is hive-c0re
running?". It was running. The operator was not in `hive-admin` in that
shell, and the connect got EACCES.
The message was a guess, not a diagnosis, because `query_hive_urls`
returned `Option` and threw the cause away with `.ok()`. Three different
failures — not in the group, no socket at all, nobody listening — all
arrived as the same sentence, and only one of the three is fixed by
looking at the daemon.
Classify the connect error in `client::request`, which every
daemon-assisted verb goes through, and keep the io error as the anyhow
cause so the output reads fix-first. EACCES names `hive-admin`,
`services.hyperhive.adminUsers`, and — the part that actually bites — the
re-login, since secondary group membership is only applied at login, so a
shell opened before the grant still cannot connect. ENOENT and
ECONNREFUSED point at the units instead.
Then stop discarding it: `query_hive_urls` returns `Result<Option<_>>`,
`open` and `require_hive_domain` propagate, and `daemon_request` drops its
own "connect to daemon socket" context, which only buried the actionable
line under a vaguer one. `wg init`'s domain lookup stays best-effort by
an explicit `.ok().flatten()` rather than by accident.
Same footgun `agent_exists` was already fixed for: a permission error
collapsed into a value that reads as a different, wrong story.