From 47bd83054f6eed28463bb17ba5ec0df12f41d22b Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 10 Oct 2026 00:29:08 +0200 Subject: [PATCH] swarm-controller: refuse an inference model name that starts with "-" `check_model` accepted a value like `--dangerously-skip-permissions`, and the stored model ends up in the agent's `claude` argv. A PUT whose `model`, or any `subagent_models` entry (same validator), starts with `-` is now a 400 with the same problem+json shape as the other model checks. A dash inside a name (`claude-opus-4-8`) is still accepted. Raised by argus on #5084. Refs #5034 --- docs/swarm/inference.md | 10 +++++----- swarm-controller/src/inference.rs | 19 +++++++++++++++++-- swarm-controller/src/main.rs | 21 ++++++++++----------- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/docs/swarm/inference.md b/docs/swarm/inference.md index eeb3ef30..631f1e71 100644 --- a/docs/swarm/inference.md +++ b/docs/swarm/inference.md @@ -82,11 +82,11 @@ which swarm-controller alone writes: Every field is optional, and `{}` reads the same as no key. -| method | path | body | -| -------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| `GET` | `/api/agents//inference` | —; answers `{"configured": , "effective": }` | -| `PUT` | `/api/agents//inference` | the whole value, replacing the stored one; the controller refuses unknown fields, an unknown effort, or a model name with whitespace | -| `DELETE` | `/api/agents//inference` | —; answers 204 | +| method | path | body | +| -------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GET` | `/api/agents//inference` | —; answers `{"configured": , "effective": }` | +| `PUT` | `/api/agents//inference` | the whole value, replacing the stored one; the controller refuses unknown fields, an unknown effort, or a model name with whitespace or a leading `-` | +| `DELETE` | `/api/agents//inference` | —; answers 204 | An agent's own queue credential grants it `$JS.API.DIRECT.GET.KV_agent-inference.$KV.agent-inference.`, the diff --git a/swarm-controller/src/inference.rs b/swarm-controller/src/inference.rs index 343e5bb0..1f747d12 100644 --- a/swarm-controller/src/inference.rs +++ b/swarm-controller/src/inference.rs @@ -59,7 +59,8 @@ impl From for InferenceSettings { } } -/// A model name as the agent passes it to claude: one non-empty word. +/// A model name as the agent passes it to claude: one non-empty word that +/// cannot be read as a command-line flag. fn check_model(name: &str, what: &str) -> Result<(), String> { if name.is_empty() { return Err(format!("the {what} is empty")); @@ -67,6 +68,9 @@ fn check_model(name: &str, what: &str) -> Result<(), String> { if name.chars().any(|c| c.is_whitespace() || c.is_control()) { return Err(format!("the {what} {name:?} contains whitespace")); } + if name.starts_with('-') { + return Err(format!("the {what} {name:?} starts with \"-\"")); + } Ok(()) } @@ -259,7 +263,8 @@ pub(crate) async fn get_agent_inference( (status = 200, description = "the settings as stored; they replace the previous \ ones whole", body = InferenceSettings), (status = 400, description = "not an identifier, not declared on any hive, or a \ - model name is empty, has whitespace, or is listed twice (problem+json)", + model name is empty, has whitespace, starts with `-`, or is listed twice \ + (problem+json)", body = String), (status = 422, description = "an unknown field, or an effort that is not one of \ the five levels"), @@ -465,8 +470,18 @@ mod tests { r#"{"subagent_models":[]}"#, r#"{"subagent_models":["haiku",""]}"#, r#"{"subagent_models":["haiku","haiku"]}"#, + r#"{"model":"--dangerously-skip-permissions"}"#, + r#"{"model":"-x"}"#, + r#"{"subagent_models":["haiku","--print"]}"#, ] { assert!(settings(json).unwrap().into_stored().is_err(), "{json}"); } } + + /// Control for the leading-dash refusal: full model ids have dashes inside. + #[test] + fn a_dash_inside_a_model_name_is_accepted() { + let json = r#"{"model":"claude-opus-4-8","subagent_models":["claude-haiku-4-5"]}"#; + assert!(settings(json).unwrap().into_stored().is_ok()); + } } diff --git a/swarm-controller/src/main.rs b/swarm-controller/src/main.rs index 975a5ffa..6971c207 100644 --- a/swarm-controller/src/main.rs +++ b/swarm-controller/src/main.rs @@ -5544,17 +5544,16 @@ mod tests { "DELETE {name}" ); } - assert_eq!( - status( - put_agent_inference( - State(state), - Path("iris".to_owned()), - body(r#"{"model":"opus 4"}"#) - ) - .await - ), - Some(StatusCode::BAD_REQUEST) - ); + for bad in [r#"{"model":"opus 4"}"#, r#"{"model":"--print"}"#] { + assert_eq!( + status( + put_agent_inference(State(state.clone()), Path("iris".to_owned()), body(bad)) + .await + ), + Some(StatusCode::BAD_REQUEST), + "{bad}" + ); + } } #[tokio::test]