Watch
0
0
Fork
You've already forked hyperhive
0

swarmctl setup: an existing ruth keeps her permissions

declare_ruth stored the default set plus manage_root_agent over whatever ruth held, so a rerun dropped permissions granted since. It now reads her record and adds manage_root_agent only when it is missing, and writes nothing when she holds it. A new ruth still gets the defaults plus manage_root_agent.
This commit is contained in:
atlas 2026-11-08 14:12:04 +01:00 • committed by mara
commit 526cfbec1b
2 changed files with 137 additions and 8 deletions

View file

@ -43,6 +43,18 @@ pub(crate) fn get(target: &Target, agent: &str) -> Result<()> {
Ok(())
}
/// The stored record of `agent`, printing nothing.
pub(crate) fn fetch(target: &Target, agent: &str) -> Result<Permissions> {
let uri = member(agent)?;
runtime()?.block_on(send(
target,
hyper::Method::GET,
&uri,
None::<&()>,
"permissions-get",
))
}
/// Run `swarmctl agent permissions set`: store the given permissions, which
/// replace the agent's record whole.
pub(crate) fn set(

View file

@ -15,6 +15,7 @@ use std::time::{Duration, Instant};
use anyhow::{Context as _, Result, anyhow, bail};
use hive_types::Permission;
use swarm_controller_api::permissions::Permissions;
use swarm_wire::wanted::AgentState;
use zeroize::Zeroizing;
@ -199,6 +200,44 @@ fn retry<T>(attempts: usize, every: Duration, mut f: impl FnMut() -> Result<T>)
Err(last.unwrap_or_else(|| anyhow!("no attempt was made")))
}
/// Gives ruth `manage_root_agent` without taking anything else from her.
///
/// A new ruth gets the default set plus `manage_root_agent`. An existing one
/// keeps what the swarm stored for her and gains `manage_root_agent` only if
/// it is missing; one that holds it already is not written at all. Both calls
/// retry, since the controller declares a new agent in the background.
fn ensure_manager_permission(
exists: bool,
fetch: impl FnMut() -> Result<Permissions>,
mut store: impl FnMut(&[String]) -> Result<()>,
every: Duration,
) -> Result<()> {
let stored = if exists {
retry(30, every, fetch)?.permissions
} else {
None
};
let Some(granted) = with_manage_root_agent(stored) else {
return Ok(());
};
let names: Vec<String> = granted
.iter()
.map(|p| <&str>::from(*p).to_owned())
.collect();
retry(30, every, || store(&names))
}
/// `stored` plus `manage_root_agent`, or `None` when it holds it already. An
/// unset record means the default set.
fn with_manage_root_agent(stored: Option<Vec<Permission>>) -> Option<Vec<Permission>> {
let mut granted = stored.unwrap_or_else(|| Permission::DEFAULT.to_vec());
if granted.contains(&Permission::ManageRootAgent) {
return None;
}
granted.push(Permission::ManageRootAgent);
Some(granted)
}
/// Whether `program` is an executable file in a `PATH` directory.
fn on_path(program: &str) -> bool {
std::env::var_os("PATH").is_some_and(|path| on_path_in(program, &path))
@ -284,15 +323,13 @@ impl Actions for Live {
if !exists {
agent::request_create(target, RUTH, &hive, None)?;
}
let granted: Vec<String> = Permission::DEFAULT
.iter()
.chain([&Permission::ManageRootAgent])
.map(|p| <&str>::from(*p).to_owned())
.collect();
let every = Duration::from_secs(2);
retry(30, every, || {
permissions::store(target, RUTH, &granted, None)
})?;
ensure_manager_permission(
exists,
|| permissions::fetch(target, RUTH),
|granted| permissions::store(target, RUTH, granted, None).map(drop),
every,
)?;
retry(30, every, || {
agent::request_state(target, RUTH, &hive, AgentState::Up)
})?;
@ -389,6 +426,86 @@ mod tests {
assert_eq!((calls, err.to_string().as_str()), (4, "never"));
}
fn stored(permissions: Option<Vec<Permission>>) -> Permissions {
Permissions {
permissions,
..Permissions::default()
}
}
/// Runs the ruth step against `record`; returns what it stored, if anything.
fn ruth_step(exists: bool, record: &Permissions) -> (Option<Vec<String>>, usize) {
let mut written = None;
let mut fetches = 0;
ensure_manager_permission(
exists,
|| {
fetches += 1;
Ok(record.clone())
},
|granted| {
written = Some(granted.to_vec());
Ok(())
},
Duration::ZERO,
)
.expect("the step succeeds");
(written, fetches)
}
#[test]
fn an_existing_ruth_keeps_her_permissions_and_gains_manage_root_agent() {
let held = vec![
Permission::Messaging,
Permission::Logs,
Permission::WebTools,
];
let (written, _) = ruth_step(true, &stored(Some(held)));
assert_eq!(
written.expect("manage_root_agent was missing, so it is written"),
["messaging", "logs", "web_tools", "manage_root_agent"]
);
}
#[test]
fn an_existing_ruth_that_holds_manage_root_agent_is_not_written() {
let held = vec![Permission::Logs, Permission::ManageRootAgent];
let (written, fetches) = ruth_step(true, &stored(Some(held)));
assert_eq!(written, None);
assert_eq!(fetches, 1, "her record was read, not assumed");
}
#[test]
fn a_new_ruth_gets_the_defaults_and_manage_root_agent_without_a_read() {
let (written, fetches) = ruth_step(false, &stored(None));
assert_eq!(
written.expect("written"),
[
"messaging",
"meta",
"inbox",
"execution",
"manage_root_agent"
]
);
assert_eq!(fetches, 0);
}
#[test]
fn an_existing_ruth_on_the_default_set_gets_the_defaults_and_manage_root_agent() {
let (written, _) = ruth_step(true, &stored(None));
assert_eq!(
written.expect("written"),
[
"messaging",
"meta",
"inbox",
"execution",
"manage_root_agent"
]
);
}
#[test]
fn a_program_on_path_is_found_only_when_it_is_executable() {
let dir = std::env::temp_dir().join(format!("swarmctl-onpath-{}", std::process::id()));