Watch
0
0
Fork
You've already forked hyperhive
0

bind a namespaced agent's socket dir off /run so its owner maps to the agent user

This commit is contained in:
atlas 2026-11-02 18:24:33 +01:00 • committed by mara
commit 6847ed865a
4 changed files with 53 additions and 7 deletions

View file

@ -745,6 +745,10 @@ container lifetime:
on the host before every start; the harness binds its sockets
there as the agent user. Why the mode matters:
[`boundary.md`](../trust-boundary/boundary.md#the-per-agent-socket-dir).
A namespaced agent has the dir bound at `/agents/<name>/sockets` and
linked from `/run/hive-agent/<name>`; the activation chowns nothing, because
a mount point under `/run` would belong to root and the id mapping would
give the agent user no ids there.
## Matrix per-agent daemon + token-arrival trigger

View file

@ -465,11 +465,7 @@ async fn set_nspawn_flags(
// to the agent user. Don't chown or chmod it here.
let socket_dir = crate::agent_sockets::agent_dir_for(agent_name);
crate::priv_client::ensure_agent_socket_dir(agent_name, private_users).await?;
binds.push(BindMount {
host_path: socket_dir.to_string_lossy().into_owned(),
container_path: socket_dir.to_string_lossy().into_owned(),
read_only: false,
});
binds.push(socket_bind(agent_name, &socket_dir, private_users));
// Every container runs in a private network namespace with a veth
// pair attached to the host bridge — including the manager (all
@ -495,6 +491,29 @@ async fn set_nspawn_flags(
.await
}
/// The bind of the agent's socket dir.
///
/// A namespaced agent's binds are mounted `owneridmap`, which maps the owner of
/// the mount point inside the container to the host owner. nspawn mounts a
/// fresh tmpfs on `/run` before the binds, so a mount point under `/run` is
/// created by nspawn owned by root, and the agent user would have no mapping
/// (EOVERFLOW). A namespaced agent gets the dir at `/agents/<name>/sockets`,
/// whose mount point hive-priv owns to the agent; the agent module links
/// `/run/hive-agent/<name>` to it.
fn socket_bind(agent_name: &str, socket_dir: &Path, private_users: Option<bool>) -> BindMount {
let host_path = socket_dir.to_string_lossy().into_owned();
let container_path = if private_users == Some(true) {
format!("/agents/{agent_name}/sockets")
} else {
host_path.clone()
};
BindMount {
host_path,
container_path,
read_only: false,
}
}
/// What hive-priv is told about the agent's user namespace: the record's flag,
/// or `None` (leave the agent as it is) when there is no record or it cannot be
/// read, so a swarm outage never turns a namespaced agent off or on.
@ -555,6 +574,21 @@ mod tests {
/// The flag in the record is what hive-priv is told, both ways. No record
/// and an unreadable one say nothing, so a swarm outage changes no agent's
/// namespace; a record from before the field reads false.
/// Only a namespaced agent's socket dir moves off `/run`.
#[test]
fn the_socket_dir_leaves_run_only_for_a_namespaced_agent() {
let dir = std::path::Path::new("/run/hive-agent/iris");
for private_users in [None, Some(false)] {
let b = super::socket_bind("iris", dir, private_users);
assert_eq!(b.container_path, "/run/hive-agent/iris");
assert_eq!(b.host_path, "/run/hive-agent/iris");
}
let b = super::socket_bind("iris", dir, Some(true));
assert_eq!(b.container_path, "/agents/iris/sockets");
assert_eq!(b.host_path, "/run/hive-agent/iris");
assert!(!b.read_only);
}
#[test]
fn the_records_flag_is_what_the_start_path_passes() {
assert_eq!(

View file

@ -199,7 +199,12 @@ in
"d ${homeDir}/.local/state 0755 ${owner} - -"
"L ${homeDir}/.local/state/nix - - - - ${persisted}"
"L ${homeDir}/.nix-profile - - - - ${homeDir}/.local/state/nix/profiles/profile"
];
]
# A namespaced agent's socket dir is bound at /agents/<name>/sockets, not
# under /run, whose tmpfs nspawn mounts before the binds and so owns the
# mount point to root (hive-c0re `socket_bind`). The harness keeps the
# /run/hive-agent path. A dir already bound there is left alone.
++ lib.optional config.services.hyperhive.agent.user.privateUsers "L /run/hive-agent/${userName} - - - - /agents/${userName}/sockets";
# `NOPASSWD: ALL` for the agent user. Lets claude's Bash tool
# keep working with anything that expected root (systemctl,

View file

@ -54,7 +54,7 @@ let
++ [
{
host_path = nsSocketDir;
container_path = nsSocketDir;
container_path = "/agents/${nsAgent}/sockets";
read_only = false;
}
];
@ -368,6 +368,9 @@ pkgs.testers.runNixOSTest {
assert as_ns(f"cat /agents/{NS_AGENT}/state/pre.txt") == "before"
assert as_ns(f"stat -c %u:%g /agents/{NS_AGENT}/state/pre.txt") == "1000:1000"
as_ns(f"echo after > /agents/{NS_AGENT}/state/post.txt")
# The socket dir is bound off /run (an owneridmap mount point under the
# container's /run tmpfs would belong to root) and linked back.
assert as_ns(f"readlink /run/hive-agent/{NS_AGENT}") == f"/agents/{NS_AGENT}/sockets"
as_ns(f"touch /run/hive-agent/{NS_AGENT}/web.sock")
assert as_ns(f"cat /agents/{NS_AGENT}/config/cfg") == "from-config"
host.fail(f"nixos-container run {NS} -- runuser -l {NS_AGENT} -c 'touch /agents/{NS_AGENT}/config/new'")