feat: add raw session args/opts for agent passthrough

This commit is contained in:
Nathan Flurry 2026-02-05 11:32:39 -08:00
parent 375d73e4cb
commit 2f26f76d9b
14 changed files with 365 additions and 37 deletions

View file

@ -237,6 +237,10 @@ impl AgentManager {
}
_ => {}
}
// Apply raw CLI args
for arg in &options.raw_args {
command.arg(arg);
}
if options.streaming_input {
command
.arg("--input-format")
@ -268,6 +272,10 @@ impl AgentManager {
if let Some(session_id) = options.session_id.as_deref() {
command.arg("-s").arg(session_id);
}
// Apply raw CLI args
for arg in &options.raw_args {
command.arg(arg);
}
command.arg(&options.prompt);
}
AgentId::Amp => {
@ -583,6 +591,10 @@ impl AgentManager {
}
_ => {}
}
// Apply raw CLI args
for arg in &options.raw_args {
command.arg(arg);
}
if options.streaming_input {
command
.arg("--input-format")
@ -614,6 +626,10 @@ impl AgentManager {
if let Some(session_id) = options.session_id.as_deref() {
command.arg("-s").arg(session_id);
}
// Apply raw CLI args
for arg in &options.raw_args {
command.arg(arg);
}
command.arg(&options.prompt);
}
AgentId::Amp => {
@ -682,6 +698,8 @@ pub struct SpawnOptions {
pub env: HashMap<String, String>,
/// Use stream-json input via stdin (Claude only).
pub streaming_input: bool,
/// Raw CLI arguments to pass to the agent (for CLI-based agents).
pub raw_args: Vec<String>,
}
impl SpawnOptions {
@ -696,6 +714,7 @@ impl SpawnOptions {
working_dir: None,
env: HashMap::new(),
streaming_input: false,
raw_args: Vec::new(),
}
}
}
@ -1054,7 +1073,12 @@ fn spawn_amp(
if let Some(session_id) = options.session_id.as_deref() {
command.arg("--continue").arg(session_id);
}
command.args(&args).arg(&options.prompt);
command.args(&args);
// Apply raw CLI args
for arg in &options.raw_args {
command.arg(arg);
}
command.arg(&options.prompt);
for (key, value) in &options.env {
command.env(key, value);
}
@ -1095,6 +1119,10 @@ fn build_amp_command(path: &Path, working_dir: &Path, options: &SpawnOptions) ->
if flags.dangerously_skip_permissions && options.permission_mode.as_deref() == Some("bypass") {
command.arg("--dangerously-skip-permissions");
}
// Apply raw CLI args
for arg in &options.raw_args {
command.arg(arg);
}
command.arg(&options.prompt);
for (key, value) in &options.env {
command.env(key, value);
@ -1157,6 +1185,10 @@ fn spawn_amp_fallback(
if !args.is_empty() {
command.args(&args);
}
// Apply raw CLI args
for arg in &options.raw_args {
command.arg(arg);
}
command.arg(&options.prompt);
for (key, value) in &options.env {
command.env(key, value);
@ -1175,6 +1207,10 @@ fn spawn_amp_fallback(
if let Some(session_id) = options.session_id.as_deref() {
command.arg("--continue").arg(session_id);
}
// Apply raw CLI args
for arg in &options.raw_args {
command.arg(arg);
}
command.arg(&options.prompt);
for (key, value) in &options.env {
command.env(key, value);

View file

@ -591,6 +591,8 @@ fn run_sessions(command: &SessionsCommand, cli: &Cli) -> Result<(), CliError> {
model: args.model.clone(),
variant: args.variant.clone(),
agent_version: args.agent_version.clone(),
raw_session_args: None,
raw_session_options: None,
};
let path = format!("{API_PREFIX}/sessions/{}", args.session_id);
let response = ctx.post(&path, &body)?;

View file

@ -379,6 +379,8 @@ async fn ensure_backing_session(
model: None,
variant: None,
agent_version: None,
raw_session_args: None,
raw_session_options: None,
};
match state
.inner

View file

@ -323,6 +323,8 @@ struct SessionState {
model: Option<String>,
variant: Option<String>,
native_session_id: Option<String>,
raw_session_args: Option<Vec<String>>,
raw_session_options: Option<serde_json::Value>,
ended: bool,
ended_exit_code: Option<i32>,
ended_message: Option<String>,
@ -381,6 +383,8 @@ impl SessionState {
model: request.model.clone(),
variant: request.variant.clone(),
native_session_id: None,
raw_session_args: request.raw_session_args.clone(),
raw_session_options: request.raw_session_options.clone(),
ended: false,
ended_exit_code: None,
ended_message: None,
@ -1614,6 +1618,8 @@ impl SessionManager {
model: session.model.clone(),
variant: session.variant.clone(),
native_session_id: None,
raw_session_args: session.raw_session_args.clone(),
raw_session_options: session.raw_session_options.clone(),
};
let thread_id = self.create_codex_thread(&session_id, &snapshot).await?;
session.native_session_id = Some(thread_id);
@ -3079,6 +3085,15 @@ impl SessionManager {
params.sandbox = codex_sandbox_mode(Some(&session.permission_mode));
params.model = session.model.clone();
// Merge raw_session_options into the config field if provided
if let Some(serde_json::Value::Object(raw_options)) = &session.raw_session_options {
let mut config = params.config.take().unwrap_or_default();
for (key, value) in raw_options {
config.insert(key.clone(), value.clone());
}
params.config = Some(config);
}
let request = codex_schema::ClientRequest::ThreadStart {
id: codex_schema::RequestId::from(id),
params,
@ -3488,6 +3503,10 @@ pub struct AgentCapabilities {
pub item_started: bool,
/// Whether this agent uses a shared long-running server process (vs per-turn subprocess)
pub shared_process: bool,
/// Whether this agent supports raw CLI arguments passed at session creation
pub raw_session_args: bool,
/// Whether this agent supports raw options passed at session creation (long-running server agents)
pub raw_session_options: bool,
}
/// Status of a shared server process for an agent
@ -3575,6 +3594,12 @@ pub struct CreateSessionRequest {
pub variant: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_version: Option<String>,
/// Raw CLI arguments to pass to the agent (for CLI-based agents like Claude, OpenCode, Amp)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw_session_args: Option<Vec<String>>,
/// Raw options to pass to the agent (for long-running server agents like Codex)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw_session_options: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
@ -4120,6 +4145,8 @@ fn agent_capabilities_for(agent: AgentId) -> AgentCapabilities {
streaming_deltas: true,
item_started: false,
shared_process: false, // per-turn subprocess with --resume
raw_session_args: true,
raw_session_options: false,
},
AgentId::Codex => AgentCapabilities {
plan_mode: true,
@ -4140,6 +4167,8 @@ fn agent_capabilities_for(agent: AgentId) -> AgentCapabilities {
streaming_deltas: true,
item_started: true,
shared_process: true, // shared app-server via JSON-RPC
raw_session_args: false,
raw_session_options: true,
},
AgentId::Opencode => AgentCapabilities {
plan_mode: false,
@ -4160,6 +4189,8 @@ fn agent_capabilities_for(agent: AgentId) -> AgentCapabilities {
streaming_deltas: true,
item_started: true,
shared_process: true, // shared HTTP server
raw_session_args: true,
raw_session_options: false,
},
AgentId::Amp => AgentCapabilities {
plan_mode: false,
@ -4180,6 +4211,8 @@ fn agent_capabilities_for(agent: AgentId) -> AgentCapabilities {
streaming_deltas: false,
item_started: false,
shared_process: false, // per-turn subprocess with --continue
raw_session_args: true,
raw_session_options: false,
},
AgentId::Mock => AgentCapabilities {
plan_mode: true,
@ -4200,6 +4233,8 @@ fn agent_capabilities_for(agent: AgentId) -> AgentCapabilities {
streaming_deltas: true,
item_started: true,
shared_process: false, // in-memory mock (no subprocess)
raw_session_args: false,
raw_session_options: false,
},
}
}
@ -4434,6 +4469,7 @@ fn build_spawn_options(
None
}
});
options.raw_args = session.raw_session_args.clone().unwrap_or_default();
if let Some(anthropic) = credentials.anthropic {
options
.env
@ -6461,6 +6497,8 @@ struct SessionSnapshot {
model: Option<String>,
variant: Option<String>,
native_session_id: Option<String>,
raw_session_args: Option<Vec<String>>,
raw_session_options: Option<serde_json::Value>,
}
impl From<&SessionState> for SessionSnapshot {
@ -6473,6 +6511,8 @@ impl From<&SessionState> for SessionSnapshot {
model: session.model.clone(),
variant: session.variant.clone(),
native_session_id: session.native_session_id.clone(),
raw_session_args: session.raw_session_args.clone(),
raw_session_options: session.raw_session_options.clone(),
}
}
}

View file

@ -1 +1,2 @@
mod agents;
mod raw_session_args;

View file

@ -0,0 +1,50 @@
use sandbox_agent_agent_management::agents::{AgentId, AgentManager, InstallOptions, SpawnOptions};
/// Tests that raw_args are passed to CLI-based agents.
/// We use `--version` as a raw arg which causes agents to print version info and exit.
#[test]
fn test_raw_args_version_flag() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempfile::tempdir()?;
let manager = AgentManager::new(temp_dir.path().join("bin"))?;
// Test Claude with --version
manager.install(AgentId::Claude, InstallOptions::default())?;
let mut spawn = SpawnOptions::new("test");
spawn.raw_args = vec!["--version".to_string()];
let result = manager.spawn(AgentId::Claude, spawn)?;
let output = format!("{}{}", result.stdout, result.stderr);
assert!(
output.to_lowercase().contains("version")
|| output.contains("claude")
|| result.status.code() == Some(0),
"Claude --version failed: {output}"
);
// Test OpenCode with --version
manager.install(AgentId::Opencode, InstallOptions::default())?;
let mut spawn = SpawnOptions::new("test");
spawn.raw_args = vec!["--version".to_string()];
let result = manager.spawn(AgentId::Opencode, spawn)?;
let output = format!("{}{}", result.stdout, result.stderr);
assert!(
output.to_lowercase().contains("version")
|| output.contains("opencode")
|| result.status.code() == Some(0),
"OpenCode --version failed: {output}"
);
// Test Amp with --version
manager.install(AgentId::Amp, InstallOptions::default())?;
let mut spawn = SpawnOptions::new("test");
spawn.raw_args = vec!["--version".to_string()];
let result = manager.spawn(AgentId::Amp, spawn)?;
let output = format!("{}{}", result.stdout, result.stderr);
assert!(
output.to_lowercase().contains("version")
|| output.contains("amp")
|| result.status.code() == Some(0),
"Amp --version failed: {output}"
);
Ok(())
}