diff --git a/Cargo.lock b/Cargo.lock index 3e1d5ad1..ef0bc59d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4590,6 +4590,7 @@ dependencies = [ "futures-util", "hive-jobq", "hive-jobq-wire", + "hive-types", "reqwest 0.13.1", "serde", "serde_json", diff --git a/swarm-controller/Cargo.toml b/swarm-controller/Cargo.toml index 69e94710..aad95ae4 100644 --- a/swarm-controller/Cargo.toml +++ b/swarm-controller/Cargo.toml @@ -34,6 +34,10 @@ futures-util.workspace = true # same shape `hive-c0re/src/job_queue/scheduler.rs` uses over its own graph. hive-jobq.workspace = true hive-jobq-wire.workspace = true +# Validates `POST /api/agents`' `name` before it becomes `agent`/`repo` +# everywhere downstream — see `create_agent`'s doc comment for why this is +# defense-in-depth, not the only gate (per an argus review finding). +hive-types.workspace = true # `auth`'s bridge client — same crate the bridge itself uses to define the # request/response shape, so the two ends cannot drift. `forge.rs` also # uses this directly for `StatusCode` in its error-classification helpers. diff --git a/swarm-controller/src/main.rs b/swarm-controller/src/main.rs index 5903da83..26aa08fd 100644 --- a/swarm-controller/src/main.rs +++ b/swarm-controller/src/main.rs @@ -508,12 +508,21 @@ struct CreateAgentResponse { /// `SwarmNodeKind` variants have: each node kind's `dead_code` bound is /// the whole reason the executor arm and this endpoint had to land in the /// same change as the variant, not as a follow-up. +/// +/// `name` is validated with [`hive_types::Ident::parse`] before it becomes +/// `agent`/`repo` anywhere downstream — not the *only* gate (`CreateIdentity` +/// runs first and validates server-side too), but the upstream check is a +/// few hops removed from where an unvalidated name would do damage +/// (`forge::seed_agent_config` interpolates `agent` into a nix comment +/// line). Flagged in review as safe today but fragile if a second caller +/// of these nodes ever appears; validating here closes it locally. #[utoipa::path( post, path = "/api/agents", request_body = CreateAgentRequest, responses( (status = 200, description = "job chain queued", body = CreateAgentResponse), + (status = 400, description = "`name` is not a valid identifier", body = String), (status = 500, description = "the job chain could not be queued", body = String), ), tag = "agents" @@ -522,12 +531,15 @@ async fn create_agent( State(state): State, Json(req): Json, ) -> Result, (axum::http::StatusCode, String)> { + let agent = hive_types::Ident::parse(&req.name) + .map_err(|reason| (axum::http::StatusCode::BAD_REQUEST, reason.to_owned()))? + .into_string(); + let repo = agent.clone(); + let mut sched = state .jobq .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - let agent = req.name; - let repo = agent.clone(); let ids = sched .insert_job(None, |b| { let create_identity = b.node(SwarmNodeKind::CreateIdentity {