Watch
0
0
Fork
You've already forked hyperhive
0

swarmctl shares the create-agent, inference and permissions shapes with the controller

This commit is contained in:
atlas 2026-10-19 17:57:21 +02:00
commit 0831b85b73
26 changed files with 675 additions and 434 deletions

View file

@ -19,7 +19,8 @@ use backon::{ConstantBuilder, Retryable as _};
use futures_util::StreamExt as _;
use hive_types::Permission;
use problem_details::ProblemDetails;
use serde::{Deserialize, Serialize};
use serde::Serialize;
use swarm_controller_api::permissions::Permissions;
use swarm_queue_client::agent_permissions;
use swarm_wire::agent_permissions::{
AgentPermissions, Import, ImportReply, hive_from_import_subject, import_subject_wildcard,
@ -34,28 +35,14 @@ use crate::wanted::WantedWriter;
/// `wanted::MAX_ATTEMPTS`.
const MAX_ATTEMPTS: usize = 5;
/// One agent's permissions. A field left out is not set by the swarm, and the
/// compiled-in default applies; an empty list grants nothing.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(deny_unknown_fields)]
pub struct Permissions {
/// Permission names, `snake_case`: `messaging`, `meta`, `inbox`,
/// `execution`, `web_tools`, `manage_root_agent`.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<Vec<String>>, example = json!(["messaging", "meta", "inbox", "execution"]))]
permissions: Option<Vec<Permission>>,
}
impl Permissions {
/// The value to store, or why it is refused.
fn into_stored(self) -> Result<AgentPermissions, String> {
if let Some(perms) = &self.permissions {
unique(perms)?;
}
Ok(AgentPermissions {
permissions: self.permissions,
})
/// The permissions to store, or why they are refused.
fn into_stored(req: Permissions) -> Result<AgentPermissions, String> {
if let Some(perms) = &req.permissions {
unique(perms)?;
}
Ok(AgentPermissions {
permissions: req.permissions,
})
}
/// Refuse a list that names a permission twice.
@ -70,11 +57,10 @@ pub(crate) fn unique(perms: &[Permission]) -> Result<(), String> {
Ok(())
}
impl From<AgentPermissions> for Permissions {
fn from(v: AgentPermissions) -> Self {
Self {
permissions: v.permissions,
}
/// The stored permissions as the API shows them.
fn view(v: AgentPermissions) -> Permissions {
Permissions {
permissions: v.permissions,
}
}
@ -122,7 +108,7 @@ pub(crate) async fn get_agent_permissions(
let (current, _, _) = read_or_default::<AgentPermissions>(&store, &agent)
.await
.map_err(|e| queue_problem(&e))?;
Ok(Json(current.into()))
Ok(Json(view(current)))
}
/// Set the permissions the swarm grants one agent.
@ -153,9 +139,7 @@ pub(crate) async fn put_agent_permissions(
Json(req): Json<Permissions>,
) -> Result<Json<Permissions>, ProblemDetails> {
let agent = agent_name(&name).map_err(refused)?;
let value = req
.into_stored()
.map_err(|why| error_problem(StatusCode::BAD_REQUEST, &why))?;
let value = into_stored(req).map_err(|why| error_problem(StatusCode::BAD_REQUEST, &why))?;
let client = queue(&state).map_err(refused)?;
if !live(&state).await?.contains(&agent) {
return Err(error_problem(
@ -174,7 +158,7 @@ pub(crate) async fn put_agent_permissions(
.map_err(|e| queue_problem(&e))?
{
tracing::info!(%agent, ?value, "set agent permissions");
return Ok(Json(value.into()));
return Ok(Json(view(value)));
}
}
Err(queue_problem(&anyhow::anyhow!(
@ -808,7 +792,7 @@ mod tests {
let create = |name: &str, permissions| {
crate::routes::agents::create_agent(
State(state.clone()),
axum::Json(crate::routes::agents::CreateAgentRequest {
axum::Json(swarm_controller_api::agents::CreateAgentRequest {
name: name.to_owned(),
hive: "pr1ma".to_owned(),
limits: None,
@ -870,7 +854,7 @@ mod tests {
.expect("destroyed");
let _ = crate::routes::agents::create_agent(
State(state.clone()),
axum::Json(crate::routes::agents::CreateAgentRequest {
axum::Json(swarm_controller_api::agents::CreateAgentRequest {
name: "atlas".to_owned(),
hive: "pr1ma".to_owned(),
limits: None,