Commit graph hyperhive/hive-c0re/src/workers/credential.rs
Author SHA1 Message Date
atlas
af5dce0648 matrix: scope the daemon's token watcher to this agent's own state dir
`hive-matrix-daemon.path` globbed `/agents/*/state/matrix-token*`. Every
agent's state dir is visible from inside every container, so the
condition is satisfied by a sibling's token.

That is reachable, not cosmetic. The daemon deliberately exits 0 when it
has no token of its own — `Restart = "on-failure"` therefore does not
restart it, and the unit sits inactive, which is the state the path unit
exists for. In that state a sibling's token keeps the glob satisfied:
the path fires, the daemon exits 0, the unit deactivates, the path
re-arms, the condition is still true. systemd.path(5) activates a
`PathExists`-family condition that already holds immediately on arming,
so it repeats until the start limit stops it.

Scoped to this agent, the condition is false exactly when the daemon
would have nothing to do.

The glob is quoted in four other places, all of which would otherwise
name a pattern that no longer exists — a doc, a Rust doc-comment in
hive-c0re, a nix comment, and an assertion message an operator reads.
Each is reworded to the basename (`matrix-token*` in this agent's state
dir), which is what the assertion actually enforces via `baseNameOf`, so
they stay true wherever the directory moves.

Refs #4030.
2026-09-07 13:37:09 +02:00
atlas
6de6bd5d87 hive-c0re: refuse an account name for the disk path, not just the store one
`token_path`'s doc claimed the compiler was the check and that nothing had to
remember to perform one. True of `agent`, which is an `Ident`. Not true of
`account`, a bare `&str` concatenated into the filename — safe only because
`deliver` happened to validate it first, which is the caller-must-remember
pattern the comment denied.

Observably a no-op today: the one call site already rejects a bad account
before reaching here. What changes is that the signature now enforces what the
comment asserted, so a second caller cannot skip it.

The check is `path::checked_segment`, made public rather than reimplemented.
Two copies of a charset are two charsets: they agree until one is edited, and
the day they diverge a name is legal in the store and not on disk.

An account name cannot simply become an `Ident` the way an agent name is:
it is an attribute name in `hyperhive.matrixAccounts`, so uppercase and
underscore are already configurable, and narrowing that is a decision rather
than a refactor. The new test's controls pin both.

Found by argus reviewing the merged PR.
2026-09-03 01:48:39 +02:00
atlas
7a03ce096a hive-c0re: deliver an agent's credential from the store to its state dir
mara on #4015: "not merging code without callers", and on the same PR
"see issue, we decided what the first thing should be". #3726 decided it:
the controller writes a token to the store and tells the hive; the hive
reads it back and writes /agents/<agent>/state/matrix-token-<account> at
0600, where matrix.nix's existing systemd.paths glob re-fires the daemon.
So this is the hive half of that, and the library's first caller.

The notice names a credential and never carries one, and deploy_subject's
own doc is why: the auth-callout responder scopes publish and leaves sub
unrestricted, so a hive that wanted another's messages could subscribe to
them. A secret in that payload would be readable swarm-wide. The value is
read from the store under the reading hive's own certificate, where the
store's policy is what actually scopes it.

Two boundaries guard the two addresses, and they are not the same check.
`path::matrix_account` guards the address in the store. `Ident` guards the
address on disk -- `agent_state_dir` takes one, so an unvalidated name off
the queue cannot reach a directory. I had written the first and assumed it
covered both; the compiler refused the `&str` and was right. `token_path`
now takes the newtype so a call site cannot forget.

The write is atomic because the path-watcher fires on the file appearing:
written in place it would be visible while partial, and the daemon would
read a truncated credential exactly once, which is the hardest possible
failure to reproduce. The temp name is dot-prefixed so it cannot match the
`matrix-token*` glob on its way past.

The publish grant is here because without it the failure is invisible.
policy.rs already says why for its siblings: a refused publish reaches the
client as a timeout, so the symptom is a hive that never receives a
credential with nothing in either log naming a permission. Two tests: the
controller may publish, a hive may not -- its own subject included. A
forged notice leaks nothing, but it would make a hive fetch and overwrite
a token file for a name the forger chose.

Refs #3726
2026-09-03 00:29:52 +02:00