Watch
0
0
Fork
You've already forked hyperhive
0

swarm: unrestricted_internet flag on the agent permissions record, unset reads as on, create writes false

This commit is contained in:
atlas 2026-10-27 11:29:06 +01:00 • committed by mara
commit 8699857024
3 changed files with 381 additions and 107 deletions

View file

@ -128,6 +128,7 @@ async fn post_create(
limits: None,
inference: None,
permissions: None,
unrestricted_internet: None,
archetype: None,
agents_md: agents_md.map(str::to_owned),
},

View file

@ -346,7 +346,8 @@ struct AgentsMdApplyArgs {
enum PermissionsVerb {
/// Show the permissions the swarm grants an agent, or that it sets none.
Get(InferenceAgentArgs),
/// Replace the permissions the swarm grants an agent.
/// Replace the permissions the swarm grants an agent; the internet flag
/// stays unless given.
Set(PermissionsSetArgs),
/// Clear the agent's permissions, so the default applies again.
Clear(InferenceAgentArgs),
@ -366,6 +367,10 @@ struct PermissionsSetArgs {
required = true
)]
permissions: Vec<String>,
/// Whether the agent's traffic may leave the host unfiltered. Left out,
/// the stored value stays.
#[arg(long, value_name = "BOOL")]
unrestricted_internet: Option<bool>,
#[command(flatten)]
socket: ControllerSocketArg,
}
@ -765,9 +770,12 @@ fn inference(remote: &RemoteArgs, command: InferenceVerb) -> Result<()> {
fn permissions(remote: &RemoteArgs, command: PermissionsVerb) -> Result<()> {
match command {
PermissionsVerb::Get(a) => permissions::get(&a.socket.resolve(remote)?, &a.agent),
PermissionsVerb::Set(a) => {
permissions::set(&a.socket.resolve(remote)?, &a.agent, &a.permissions)
}
PermissionsVerb::Set(a) => permissions::set(
&a.socket.resolve(remote)?,
&a.agent,
&a.permissions,
a.unrestricted_internet,
),
PermissionsVerb::Clear(a) => permissions::clear(&a.socket.resolve(remote)?, &a.agent),
}
}

View file

@ -45,9 +45,14 @@ pub(crate) fn get(target: &Target, agent: &str) -> Result<()> {
/// Run `swarmctl agent permissions set`: store the given permissions, which
/// replace the agent's record whole.
pub(crate) fn set(target: &Target, agent: &str, permissions: &[String]) -> Result<()> {
pub(crate) fn set(
target: &Target,
agent: &str,
permissions: &[String],
unrestricted_internet: Option<bool>,
) -> Result<()> {
let uri = member(agent)?;
let wanted = with_permissions(permissions)?;
let wanted = with_permissions(permissions, unrestricted_internet)?;
let stored: Permissions = runtime()?.block_on(send(
target,
hyper::Method::PUT,
@ -61,7 +66,10 @@ pub(crate) fn set(target: &Target, agent: &str, permissions: &[String]) -> Resul
/// The record granting `permissions`. An empty name, as `--permissions ''`
/// gives, is dropped, so that spelling grants nothing.
fn with_permissions(permissions: &[String]) -> Result<Permissions> {
fn with_permissions(
permissions: &[String],
unrestricted_internet: Option<bool>,
) -> Result<Permissions> {
let granted = permissions
.iter()
.filter(|name| !name.is_empty())
@ -69,6 +77,7 @@ fn with_permissions(permissions: &[String]) -> Result<Permissions> {
.collect::<Result<Vec<_>>>()?;
Ok(Permissions {
permissions: Some(granted),
unrestricted_internet,
})
}
@ -112,7 +121,12 @@ fn render(p: &Permissions) -> String {
.collect::<Vec<_>>()
.join(", "),
};
format!("permissions: {caps}\n")
let internet = match p.unrestricted_internet {
None => "(not set; unrestricted)",
Some(true) => "unrestricted",
Some(false) => "restricted",
};
format!("permissions: {caps}\nunrestricted_internet: {internet}\n")
}
#[cfg(test)]
@ -122,18 +136,48 @@ mod tests {
#[test]
fn get_tells_unset_from_none() {
let unset: Permissions = serde_json::from_str("{}").unwrap();
assert_eq!(render(&unset), "permissions: (not set; the default)\n");
assert_eq!(
render(&unset),
"permissions: (not set; the default)\nunrestricted_internet: (not set; unrestricted)\n"
);
let none: Permissions = serde_json::from_str(r#"{"permissions":[]}"#).unwrap();
assert_eq!(render(&none), "permissions: (none)\n");
assert_eq!(
render(&none),
"permissions: (none)\nunrestricted_internet: (not set; unrestricted)\n"
);
let ruth: Permissions =
serde_json::from_str(r#"{"permissions":["manage_root_agent"]}"#).unwrap();
assert_eq!(render(&ruth), "permissions: manage_root_agent\n");
assert_eq!(
render(&ruth),
"permissions: manage_root_agent\nunrestricted_internet: (not set; unrestricted)\n"
);
}
/// An explicit flag is shown and sent; left out it is not sent, so a `PUT`
/// keeps the stored one.
#[test]
fn the_internet_flag_is_sent_only_when_given() {
let off = with_permissions(&[], Some(false)).unwrap();
assert_eq!(
serde_json::to_string(&off).unwrap(),
r#"{"permissions":[],"unrestricted_internet":false}"#
);
assert_eq!(
render(&off),
"permissions: (none)\nunrestricted_internet: restricted\n"
);
let left_out = with_permissions(&[], None).unwrap();
assert!(
!serde_json::to_string(&left_out)
.unwrap()
.contains("internet")
);
}
/// `--permissions ''` stores an empty list, not one empty name.
#[test]
fn an_empty_list_flag_grants_nothing() {
let set = with_permissions(&[String::new()]).unwrap();
let set = with_permissions(&[String::new()], None).unwrap();
assert_eq!(
serde_json::to_string(&set).unwrap(),
r#"{"permissions":[]}"#
@ -143,11 +187,11 @@ mod tests {
/// A name that is not a permission fails locally and lists the valid ones.
#[test]
fn an_unknown_permission_is_refused() {
let err = with_permissions(&["messaging".to_owned(), "fly".to_owned()]).unwrap_err();
let err = with_permissions(&["messaging".to_owned(), "fly".to_owned()], None).unwrap_err();
let text = err.to_string();
assert!(text.contains("invalid permission \"fly\""), "{text}");
assert!(text.contains("manage_root_agent"), "{text}");
let ok = with_permissions(&["web_tools".to_owned()]).unwrap();
let ok = with_permissions(&["web_tools".to_owned()], None).unwrap();
assert_eq!(
serde_json::to_string(&ok).unwrap(),
r#"{"permissions":["web_tools"]}"#