Watch
0
0
Fork
You've already forked hyperhive
0

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.
This commit is contained in:
atlas 2026-10-27 21:14:44 +01:00
commit 75e9e205e3

View file

@ -30,7 +30,7 @@
//! Rendering stays separate from writing so the text can be asserted with no store to talk to.
use crate::{
Error,
Error, backend_env,
path::{Kind, MOUNT, ROOT, checked_segment},
};
@ -111,9 +111,9 @@ pub fn agents_from_role_names(names: &[String]) -> Vec<String> {
.collect()
}
/// One read stanza. With [`list_stanza`], the only shapes this module emits, so
/// "writes nothing" is a property of the renderers rather than of each call
/// site.
/// One read stanza. With [`list_stanza`] and [`create_stanza`], the only shapes
/// this module emits, so what an agent may write is a property of the renderers
/// rather than of each call site.
fn read_stanza(path: &str) -> String {
format!("path \"{path}\" {{\n capabilities = [\"read\"]\n}}\n")
}
@ -124,6 +124,13 @@ fn list_stanza(path: &str) -> String {
format!("path \"{path}\" {{\n capabilities = [\"list\"]\n}}\n")
}
/// One create stanza: `create` and `read`, never `update`. A leaf that holds
/// nothing takes a first write and the holder cannot replace it afterwards,
/// which is what a `-cas=0` write needs and all it gets.
fn create_stanza(path: &str) -> String {
format!("path \"{path}\" {{\n capabilities = [\"create\", \"read\"]\n}}\n")
}
/// The one policy every hive's cert-auth role attaches.
///
/// Inside [`HIVE_PREFIX`] so that the controller may write it. It is the object
@ -196,10 +203,12 @@ const ENTITY_NAME_TEMPLATE: &str = "{{identity.entity.name}}";
/// each agent's entity after the agent, and a login that no entity of that
/// name owns reads `swarm/agents/<generated name>/`, where nothing is stored.
///
/// Nothing here writes: an agent that could write its own credentials could
/// hand itself an identity it was never issued. An agent has no business with
/// another agent's credentials, a hive's, a service's, or the controller's, so
/// none of [`render_shared_hives`]'s breadth applies. A hive still reads this
/// The one write is `create` on the agent's own [`backend_env`] leaf, without
/// `update`: the agent seeds it from the file it has today, once, and cannot
/// replace it. Any other write would let an agent hand itself an identity it
/// was never issued. An agent has no business with another agent's
/// credentials, a hive's, a service's, or the controller's, so none of
/// [`render_shared_hives`]'s breadth applies. A hive still reads this
/// agent's secrets, and closing that is its own decision.
#[must_use]
pub fn render_shared_agent() -> String {
@ -210,7 +219,11 @@ pub fn render_shared_agent() -> String {
let list = list_stanza(&format!(
"{MOUNT}/metadata/{ROOT}/{kind}/{ENTITY_NAME_TEMPLATE}/*"
));
format!("{read}{list}")
let seed = create_stanza(&format!(
"{MOUNT}/data/{ROOT}/{kind}/{ENTITY_NAME_TEMPLATE}/{}",
backend_env::LEAF
));
format!("{read}{list}{seed}")
}
/// What an agent's token grants under [`SHARED_AGENT_POLICY`]: the document
@ -241,7 +254,8 @@ mod tests {
assert_eq!(
render_shared_agent(),
"path \"secret/data/swarm/agents/{{identity.entity.name}}/*\" {\n capabilities = [\"read\"]\n}\n\
path \"secret/metadata/swarm/agents/{{identity.entity.name}}/*\" {\n capabilities = [\"list\"]\n}\n"
path \"secret/metadata/swarm/agents/{{identity.entity.name}}/*\" {\n capabilities = [\"list\"]\n}\n\
path \"secret/data/swarm/agents/{{identity.entity.name}}/backend-env\" {\n capabilities = [\"create\", \"read\"]\n}\n"
);
}
@ -250,7 +264,6 @@ mod tests {
let shared = render_shared_agent();
assert!(!shared.contains("atlas"));
assert!(!shared.contains("agents/*"), "{shared}");
assert!(!shared.contains("create"));
assert!(!shared.contains("update"));
assert!(!shared.contains("delete"));
}
@ -456,10 +469,11 @@ mod tests {
/// What `effective_agent("atlas")` must be, byte for byte.
const ATLAS_DOCUMENT: &str = "path \"secret/data/swarm/agents/atlas/*\" {\n capabilities = [\"read\"]\n}\n\
path \"secret/metadata/swarm/agents/atlas/*\" {\n capabilities = [\"list\"]\n}\n";
path \"secret/metadata/swarm/agents/atlas/*\" {\n capabilities = [\"list\"]\n}\n\
path \"secret/data/swarm/agents/atlas/backend-env\" {\n capabilities = [\"create\", \"read\"]\n}\n";
#[test]
fn an_agents_document_is_two_stanzas_naming_that_agent() {
fn an_agents_document_is_its_stanzas_naming_that_agent() {
assert_eq!(effective_agent("atlas"), ATLAS_DOCUMENT);
}
@ -495,13 +509,13 @@ mod tests {
assert!(!p.contains("argus"), "no other principal is named");
assert_eq!(
p.matches("path \"").count(),
2,
"two stanzas, or the document grants something unaccounted for: {p}"
3,
"three stanzas, or the document grants something unaccounted for: {p}"
);
}
#[test]
fn an_agents_document_is_exactly_its_own_two_stanzas() {
fn an_agents_document_is_exactly_its_own_stanzas() {
// Pinned byte for byte: an added stanza (a hive's queue credential, a
// second agent) is exactly what a presence check misses.
assert_eq!(effective_agent("atlas"), ATLAS_DOCUMENT);
@ -509,18 +523,49 @@ mod tests {
assert!(!effective_agent("other").contains("agents/atlas/"));
}
/// The paths of the stanzas in `document` that grant `capability`.
fn paths_granting(document: &str, capability: &str) -> Vec<String> {
let mut paths = Vec::new();
let mut current = "";
for line in document.lines() {
if let Some(rest) = line.strip_prefix("path \"") {
current = rest.split_once('"').map_or("", |(path, _)| path);
} else if line.contains(&format!("\"{capability}\"")) {
paths.push(current.to_owned());
}
}
paths
}
#[test]
fn an_agents_grant_writes_nothing() {
fn an_agents_only_write_is_creating_its_own_backend_env() {
// An agent that could write its own credentials could hand itself an
// identity it was never issued — and `create`/`update` on that path is
// exactly what the controller holds, so the wall is the capability.
// identity it was never issued. The one leaf it may create is the one
// it seeds from its backend environment file, and `update` on it would
// let the agent replace a value the operator set.
let p = effective_agent("atlas");
for capability in ["create", "update", "delete", "sudo", "patch"] {
let leaf = format!(
"secret/data/{}",
crate::backend_env::env_path("atlas").expect("legal")
);
// The control: the walk finds the stanzas that are there.
assert_eq!(
paths_granting(&p, "read"),
["secret/data/swarm/agents/atlas/*", leaf.as_str()]
);
assert_eq!(paths_granting(&p, "create"), [leaf.as_str()]);
for capability in ["update", "delete", "sudo", "patch"] {
assert!(
!p.contains(capability),
"an agent's document must not grant {capability}: {p}"
);
}
// The exact stanza overrides the glob for its path, so it carries
// `read` itself.
assert!(
p.contains("backend-env\" {\n capabilities = [\"create\", \"read\"]"),
"{p}"
);
}
#[test]
@ -530,8 +575,9 @@ mod tests {
// granted, nor is `list` anywhere but `metadata/`.
let p = effective_agent("atlas");
let stanzas: Vec<&str> = p.split_inclusive("}\n").collect();
assert_eq!(stanzas.len(), 2, "{p}");
for stanza in stanzas {
assert_eq!(stanzas.len(), 3, "{p}");
// The third is the create stanza, pinned by the test above.
for stanza in &stanzas[..2] {
let on_data = stanza.starts_with("path \"secret/data/");
let on_metadata = stanza.starts_with("path \"secret/metadata/");
assert!(on_data != on_metadata, "{stanza}");