Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-secret-client/src/backend_env.rs
atlas 75e9e205e3 fetch the agent's backend env file from the store, seeded from the existing file
the agent's own root unit reads swarm/agents/<agent>/backend-env into /run and
creates it from backendEnvironmentFile with -cas=0 when the store holds nothing.
the shared agent policy gains create (not update) on that one leaf. the harness
and the subagent daemon load the old file first and the fetched one after it.
2026-10-28 01:24:29 +01:00

99 lines
3.4 KiB
Rust

//! The backend environment agreement: where an agent's backend credentials
//! (`ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL`, an ACP provider key) live in
//! the store, and what the object at that path holds.
//!
//! The object holds the text of a systemd `EnvironmentFile`. The agent's own
//! `hive-agent-backend-env` unit (`nix/agent-modules/backend-env.nix`) reads it
//! under the agent's certificate and writes it where the harness's
//! `EnvironmentFile=` points. A leaf no one has written yet is created by that
//! same unit from the agent's existing `backendEnvironmentFile`, which is why
//! [`crate::policy::render_shared_agent`] grants the agent `create` on this one
//! leaf and no other.
use serde::{Deserialize, Serialize};
use crate::{
Error,
path::{Kind, principal_prefix},
};
/// The last segment of the path, shared with the policy stanza that lets an
/// agent create it.
pub(crate) const LEAF: &str = "backend-env";
/// The path holding `agent`'s backend environment.
///
/// A flat leaf under the agent's prefix, like
/// [`crate::github::account_path`], so the agent's own read stanza
/// ([`crate::policy::render_shared_agent`]) already covers it.
///
/// # Errors
/// [`Error::PathSegment`] when `agent` contains anything but `[A-Za-z0-9_-]`,
/// which is what keeps one agent's name from addressing another agent's secret.
pub fn env_path(agent: &str) -> Result<String, Error> {
let prefix = principal_prefix(Kind::Agent, agent)?;
Ok(format!("{prefix}/{LEAF}"))
}
/// What an [`env_path`] holds.
///
/// No `Debug` derive: `env` carries live credentials, and a derived `Debug` is
/// one `{:?}` away from a log line.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BackendEnv {
/// The file text, `KEY=value` per line. `backend-env.nix` writes it with
/// `bao kv put … env=@file` and reads it with `bao kv get -field=env`, so
/// this name is load-bearing for a reader this crate does not control.
pub env: String,
}
impl std::fmt::Debug for BackendEnv {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BackendEnv")
.field("env", &"<redacted>")
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_env_lands_under_the_agent_prefix() {
// Spelled out: `backend-env.nix` spells the same string.
assert_eq!(
env_path("atlas").expect("a plain name is legal"),
"swarm/agents/atlas/backend-env"
);
}
#[test]
fn a_traversal_in_the_agent_name_is_refused() {
for bad in ["../argus", "a/b", "atlas/../argus", "a.b", ""] {
let e = env_path(bad).expect_err("a traversal is not legal");
assert!(matches!(e, Error::PathSegment { kind: "agent", .. }), "{e}");
}
// The control: the legal charset stays reachable.
assert!(env_path("a-b_C9").is_ok());
}
#[test]
fn the_stored_field_is_env() {
let stored: BackendEnv =
serde_json::from_str(r#"{"env":"A=b\n"}"#).expect("the documented shape decodes");
assert_eq!(stored.env, "A=b\n");
}
#[test]
fn debug_hides_the_credentials() {
let shown = format!(
"{:?}",
BackendEnv {
env: "ANTHROPIC_API_KEY=sk-live".to_owned()
}
);
assert!(!shown.contains("sk-live"), "{shown}");
assert!(shown.contains("<redacted>"), "{shown}");
}
}