c0re: chown per-agent state writes to agent uid:gid (#673)
This commit is contained in:
parent
d23fd8847d
commit
0cf703a939
3 changed files with 70 additions and 0 deletions
|
|
@ -162,6 +162,24 @@ fn extract_token(output: &str) -> Option<String> {
|
|||
.map(str::to_owned)
|
||||
}
|
||||
|
||||
/// Best-effort chown `path` to the agent's container-local uid/gid.
|
||||
/// Closes the gap where c0re (running as root on the host) writes
|
||||
/// per-agent state files that the agent's non-root unix user then
|
||||
/// can't read until the next container activation runs the chown
|
||||
/// fixup in `harness-base.nix` (#673).
|
||||
///
|
||||
/// Silently no-ops when the container hasn't been built yet (passwd
|
||||
/// file absent) or when the chown syscall fails — the activation
|
||||
/// script remains the safety net.
|
||||
fn chown_to_agent(name: &str, path: &Path) {
|
||||
let Some((uid, gid)) = crate::lifecycle::agent_uid_gid(name) else {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = std::os::unix::fs::chown(path, Some(uid), Some(gid)) {
|
||||
tracing::debug!(%name, path = %path.display(), error = %e, "forge: chown to agent failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// Canonical email address for a hive agent's Forgejo account.
|
||||
/// Must match the `user.email` set by `meta::render_flake` so commits
|
||||
/// by the agent link back to their Forgejo profile page.
|
||||
|
|
@ -335,6 +353,7 @@ async fn mint_and_persist_token(name: &str, path: &Path, scopes: &str) -> Result
|
|||
std::fs::write(path, format!("{token}\n"))
|
||||
.with_context(|| format!("write token to {}", path.display()))?;
|
||||
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
|
||||
chown_to_agent(name, path);
|
||||
tracing::info!(%name, path = %path.display(), "forge: persisted access token");
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,6 +92,42 @@ pub fn is_manager(name: &str) -> bool {
|
|||
name == MANAGER_NAME
|
||||
}
|
||||
|
||||
/// Read the agent user's `(uid, gid)` from the container's nixos-managed
|
||||
/// `/etc/passwd`. Returns `None` when the container hasn't been built
|
||||
/// yet, the passwd file is unparseable, or the agent user is missing
|
||||
/// (e.g. legacy pre-#658 container that still runs as root).
|
||||
///
|
||||
/// Used by `forge` + `matrix` after writing per-agent state files so
|
||||
/// the bind-mounted host file ends up readable by the agent user
|
||||
/// without waiting for the next container activation to run the chown
|
||||
/// fixup (#673).
|
||||
///
|
||||
/// Notes:
|
||||
/// - Reads the *container-local* passwd at
|
||||
/// `/var/lib/nixos-containers/<container>/etc/passwd`, not the host's.
|
||||
/// The container's user-namespace shares uids with the host (no
|
||||
/// `PrivateUsers`), so the uid is directly usable in host-side
|
||||
/// `chown(2)`.
|
||||
/// - Best-effort: caller treats `None` as "skip the chown".
|
||||
#[must_use]
|
||||
pub fn agent_uid_gid(agent_name: &str) -> Option<(u32, u32)> {
|
||||
let container = container_name(agent_name);
|
||||
let passwd_path = format!("/var/lib/nixos-containers/{container}/etc/passwd");
|
||||
let content = std::fs::read_to_string(&passwd_path).ok()?;
|
||||
for line in content.lines() {
|
||||
let mut parts = line.split(':');
|
||||
let user = parts.next()?;
|
||||
if user != agent_name {
|
||||
continue;
|
||||
}
|
||||
let _ = parts.next()?; // x (password placeholder)
|
||||
let uid: u32 = parts.next()?.parse().ok()?;
|
||||
let gid: u32 = parts.next()?.parse().ok()?;
|
||||
return Some((uid, gid));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn validate(name: &str) -> Result<()> {
|
||||
if name.is_empty() {
|
||||
bail!("agent name must not be empty");
|
||||
|
|
|
|||
|
|
@ -274,10 +274,25 @@ pub async fn ensure_user_for(
|
|||
std::fs::write(&path, format!("{access_token}\n"))
|
||||
.with_context(|| format!("matrix: write token to {}", path.display()))?;
|
||||
let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
|
||||
chown_to_agent(name, &path);
|
||||
tracing::info!(%name, path = %path.display(), "matrix: registered user + persisted access token");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Best-effort chown `path` to the agent's container-local uid/gid.
|
||||
/// Mirrors `forge::chown_to_agent` for the matrix access-token write —
|
||||
/// closes the gap where c0re (root on host) writes a file the agent's
|
||||
/// non-root unix user then can't read until the next activation runs
|
||||
/// the harness-base.nix chown fixup (#673).
|
||||
fn chown_to_agent(name: &str, path: &Path) {
|
||||
let Some((uid, gid)) = crate::lifecycle::agent_uid_gid(name) else {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = std::os::unix::fs::chown(path, Some(uid), Some(gid)) {
|
||||
tracing::debug!(%name, path = %path.display(), error = %e, "matrix: chown to agent failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a matrix account for `name` with the supplied `password`
|
||||
/// and return the freshly-minted access token. Unlike [`ensure_user_for`],
|
||||
/// the token is **not** persisted to disk — the caller is responsible
|
||||
|
|
|
|||
Loading…
Reference in a new issue