use gate4agent_types::{
AdapterId, LaunchSpec, SessionOptionSelection, SessionOptionValue,
PROVIDER_EVENT_TEXT_MAX_BYTES,
};
use thiserror::Error;
pub const ONE_SHOT_REVISION: &str = "gate4agent-one-shot/orca-d8629c4/v2";
pub const CLAUDE_CODE_INLINE_REVISION: &str = "gate4agent-inline/claude-code-2.1/v1";
pub const CODEX_CLI_INLINE_REVISION: &str = "gate4agent-inline/codex-cli-0.144/v1";
pub const KIMI_CODE_INLINE_REVISION: &str = "gate4agent-inline/kimi-code-0.31/v1";
pub const ONE_SHOT_OUTPUT_MAX_BYTES: usize = 4 * 1024 * 1024;
pub const ONE_SHOT_TIMEOUT_SECONDS: u64 = 60;
pub const ONE_SHOT_THINKING_OPTION_ID: &str = "thinking-level";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OneShotPromptDelivery {
StdinClose,
Positional,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OneShotModelSource {
Static,
Dynamic,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum OneShotSessionPersistence {
#[default]
Ephemeral,
Persist,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OneShotThinkingLevel {
pub id: String,
pub label: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OneShotModelSpec {
pub id: String,
pub label: String,
pub thinking_levels: Vec<OneShotThinkingLevel>,
pub default_thinking_level: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OneShotAdapterSpec {
pub adapter_id: AdapterId,
pub label: String,
pub prompt_delivery: OneShotPromptDelivery,
pub model_source: OneShotModelSource,
pub models: Vec<OneShotModelSpec>,
pub default_model_id: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OneShotPlan {
pub program: String,
pub args: Vec<String>,
pub stdin_payload: Option<String>,
pub label: String,
pub applied: SessionOptionSelection,
}
pub fn one_shot_specs() -> Vec<OneShotAdapterSpec> {
["claude", "codex", "kimi"]
.into_iter()
.map(|id| spec_for(id).expect("hardcoded one-shot adapter"))
.collect()
}
pub fn one_shot_spec(adapter_id: &AdapterId) -> Result<OneShotAdapterSpec, OneShotAdapterError> {
spec_for(adapter_id.as_str())
}
pub fn resolve_one_shot_plan(
adapter_id: &AdapterId,
launch: &LaunchSpec,
prompt: &str,
selection: Option<&SessionOptionSelection>,
) -> Result<OneShotPlan, OneShotAdapterError> {
resolve_one_shot_plan_with_persistence(
adapter_id,
launch,
prompt,
selection,
OneShotSessionPersistence::Ephemeral,
)
}
pub fn resolve_one_shot_plan_with_persistence(
adapter_id: &AdapterId,
launch: &LaunchSpec,
prompt: &str,
selection: Option<&SessionOptionSelection>,
persistence: OneShotSessionPersistence,
) -> Result<OneShotPlan, OneShotAdapterError> {
if prompt.trim().is_empty()
|| prompt.len() > PROVIDER_EVENT_TEXT_MAX_BYTES
|| prompt.contains('\0')
{
return Err(OneShotAdapterError::InvalidPrompt);
}
if launch.program.trim().is_empty() || launch.program.contains('\0') {
return Err(OneShotAdapterError::InvalidLaunch);
}
if launch.fixed_args.iter().any(|arg| arg.contains('\0')) {
return Err(OneShotAdapterError::InvalidLaunch);
}
let spec = one_shot_spec(adapter_id)?;
if persistence == OneShotSessionPersistence::Persist && adapter_id.as_str() != "codex" {
return Err(OneShotAdapterError::PersistentSessionsUnsupported(
adapter_id.as_str().to_owned(),
));
}
let (model, thinking) = resolve_selection(&spec, selection)?;
let mut args = launch.fixed_args.clone();
match adapter_id.as_str() {
"claude" => {
args.extend(strings(&[
"-p",
"--output-format",
"text",
"--model",
&model,
"--permission-mode",
"plan",
]));
if let Some(thinking) = &thinking {
args.extend(["--effort".to_owned(), thinking.clone()]);
}
}
"codex" => {
args.push("exec".to_owned());
if persistence == OneShotSessionPersistence::Ephemeral {
args.push("--ephemeral".to_owned());
}
args.extend(strings(&[
"--skip-git-repo-check", "-s", "read-only", "--model", &model,
]));
if let Some(thinking) = &thinking {
args.extend([
"-c".to_owned(),
format!("model_reasoning_effort={thinking}"),
]);
}
}
"kimi" => {
args.extend(strings(&["-p", prompt, "--output-format", "text"]));
}
id => return Err(OneShotAdapterError::UnsupportedAdapter(id.to_owned())),
}
let mut applied = SessionOptionSelection::new(model);
if let Some(thinking) = thinking {
applied
.values
.insert(ONE_SHOT_THINKING_OPTION_ID.to_owned(), thinking.into());
}
Ok(OneShotPlan {
program: launch.program.clone(),
args,
stdin_payload: (spec.prompt_delivery == OneShotPromptDelivery::StdinClose)
.then(|| prompt.to_owned()),
label: spec.label,
applied,
})
}
fn resolve_selection(
spec: &OneShotAdapterSpec,
selection: Option<&SessionOptionSelection>,
) -> Result<(String, Option<String>), OneShotAdapterError> {
if let Some(selection) = selection {
selection
.validate()
.map_err(|_| OneShotAdapterError::InvalidSelection)?;
if selection
.values
.keys()
.any(|key| key != ONE_SHOT_THINKING_OPTION_ID)
{
return Err(OneShotAdapterError::UnknownOption);
}
}
let model_id = selection
.map(|selection| selection.model.clone())
.unwrap_or_else(|| spec.default_model_id.clone());
let known = spec.models.iter().find(|model| model.id == model_id);
if known.is_none() && spec.model_source == OneShotModelSource::Static {
return Err(OneShotAdapterError::UnknownModel(model_id));
}
let requested_thinking = selection
.and_then(|selection| selection.values.get(ONE_SHOT_THINKING_OPTION_ID))
.map(|value| match value {
SessionOptionValue::String(value) => Ok(value.clone()),
SessionOptionValue::Boolean(_) => Err(OneShotAdapterError::InvalidSelection),
})
.transpose()?;
let default_thinking = known
.and_then(|model| model.default_thinking_level.clone())
.or_else(|| {
(spec.model_source == OneShotModelSource::Dynamic
&& supports_openai_thinking(&model_id))
.then(|| "low".to_owned())
});
let thinking = requested_thinking.or(default_thinking);
if let (Some(model), Some(thinking)) = (known, &thinking) {
if !model.thinking_levels.is_empty()
&& !model
.thinking_levels
.iter()
.any(|level| &level.id == thinking)
{
return Err(OneShotAdapterError::UnknownThinkingLevel(thinking.clone()));
}
if model.thinking_levels.is_empty() && spec.model_source == OneShotModelSource::Static {
return Err(OneShotAdapterError::ThinkingUnsupported(model.id.clone()));
}
}
Ok((model_id, thinking))
}
fn spec_for(id: &str) -> Result<OneShotAdapterSpec, OneShotAdapterError> {
let (label, delivery, source, models, default_model_id) = match id {
"claude" => (
"Claude",
OneShotPromptDelivery::StdinClose,
OneShotModelSource::Static,
vec![
model("haiku", "Haiku", &[], None),
model("sonnet", "Sonnet", &claude_thinking(), Some("low")),
model("opus", "Opus", &claude_thinking(), Some("low")),
],
"sonnet",
),
"codex" => (
"Codex",
OneShotPromptDelivery::StdinClose,
OneShotModelSource::Dynamic,
[
"gpt-5.5",
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.3-codex",
"gpt-5.3-codex-spark",
"gpt-5.2",
]
.into_iter()
.map(|id| model(id, id, &openai_thinking(), Some("low")))
.collect(),
"gpt-5.5",
),
"kimi" => (
"Kimi Code",
OneShotPromptDelivery::Positional,
OneShotModelSource::Static,
vec![model("default", "Provider default", &[], None)],
"default",
),
id => return Err(OneShotAdapterError::UnsupportedAdapter(id.to_owned())),
};
Ok(OneShotAdapterSpec {
adapter_id: AdapterId::new(id).expect("hardcoded adapter ID"),
label: label.to_owned(),
prompt_delivery: delivery,
model_source: source,
models,
default_model_id: default_model_id.to_owned(),
})
}
fn model(
id: &str,
label: &str,
thinking: &[(&str, &str)],
default_thinking: Option<&str>,
) -> OneShotModelSpec {
OneShotModelSpec {
id: id.to_owned(),
label: label.to_owned(),
thinking_levels: thinking
.iter()
.map(|(id, label)| OneShotThinkingLevel {
id: (*id).to_owned(),
label: (*label).to_owned(),
})
.collect(),
default_thinking_level: default_thinking.map(str::to_owned),
}
}
fn openai_thinking() -> [(&'static str, &'static str); 4] {
[
("low", "Low"),
("medium", "Medium"),
("high", "High"),
("xhigh", "Extra High"),
]
}
fn claude_thinking() -> [(&'static str, &'static str); 5] {
[
("low", "Low"),
("medium", "Medium"),
("high", "High"),
("xhigh", "Extra High"),
("max", "Max"),
]
}
fn supports_openai_thinking(id: &str) -> bool {
let id = id.to_ascii_lowercase();
id.contains("gpt-5") || id.contains("codex")
}
fn strings(values: &[&str]) -> Vec<String> {
values.iter().map(|value| (*value).to_owned()).collect()
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum OneShotAdapterError {
#[error("unsupported one-shot adapter '{0}'")]
UnsupportedAdapter(String),
#[error("one-shot prompt is empty, contains NUL, or exceeds its bound")]
InvalidPrompt,
#[error("one-shot launch contract is invalid")]
InvalidLaunch,
#[error("one-shot session-option selection is invalid")]
InvalidSelection,
#[error("one-shot selection contains an unknown option")]
UnknownOption,
#[error("one-shot model '{0}' is not declared by the provider adapter")]
UnknownModel(String),
#[error("one-shot model '{0}' does not support thinking effort")]
ThinkingUnsupported(String),
#[error("one-shot thinking level '{0}' is not declared by the model")]
UnknownThinkingLevel(String),
#[error("one-shot adapter '{0}' does not support persistent sessions")]
PersistentSessionsUnsupported(String),
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
fn launch(program: &str) -> LaunchSpec {
LaunchSpec {
program: program.to_owned(),
fixed_args: vec!["fixed".to_owned()],
}
}
#[test]
fn one_shot_inventory_includes_current_target_contracts() {
let specs = one_shot_specs();
let actual = specs
.iter()
.map(|spec| spec.adapter_id.as_str())
.collect::<BTreeSet<_>>();
assert_eq!(actual, ["claude", "codex", "kimi"].into_iter().collect());
assert_eq!(
specs
.iter()
.find(|spec| spec.adapter_id.as_str() == "codex")
.unwrap()
.default_model_id,
"gpt-5.5"
);
}
#[test]
fn exact_provider_plans_preserve_prompt_delivery_and_safety_flags() {
let claude = resolve_one_shot_plan(
&AdapterId::new("claude").unwrap(),
&launch("claude-custom"),
"large prompt",
None,
)
.unwrap();
assert_eq!(claude.program, "claude-custom");
assert_eq!(claude.stdin_payload.as_deref(), Some("large prompt"));
assert!(claude
.args
.windows(2)
.any(|pair| pair == ["--permission-mode", "plan"]));
assert!(claude
.args
.windows(2)
.any(|pair| pair == ["--effort", "low"]));
let codex = resolve_one_shot_plan(
&AdapterId::new("codex").unwrap(),
&launch("codex"),
"prompt",
Some(
&SessionOptionSelection::new("gpt-5.4")
.with_value(ONE_SHOT_THINKING_OPTION_ID, "xhigh"),
),
)
.unwrap();
assert!(codex.args.contains(&"--ephemeral".to_owned()));
assert!(codex.args.contains(&"read-only".to_owned()));
assert!(codex
.args
.contains(&"model_reasoning_effort=xhigh".to_owned()));
let kimi = resolve_one_shot_plan(
&AdapterId::new("kimi").unwrap(),
&launch("kimi"),
"prompt in argv",
None,
)
.unwrap();
assert_eq!(kimi.stdin_payload, None);
assert_eq!(
kimi.args,
["fixed", "-p", "prompt in argv", "--output-format", "text"]
);
}
#[test]
fn codex_persistence_is_explicit_and_preserves_the_exact_safe_plan() {
assert_eq!(
OneShotSessionPersistence::default(),
OneShotSessionPersistence::Ephemeral
);
let adapter_id = AdapterId::new("codex").unwrap();
let selection = SessionOptionSelection::new("gpt-5.4")
.with_value(ONE_SHOT_THINKING_OPTION_ID, "xhigh");
let legacy = resolve_one_shot_plan(
&adapter_id,
&launch("codex"),
"prompt",
Some(&selection),
)
.unwrap();
let ephemeral = resolve_one_shot_plan_with_persistence(
&adapter_id,
&launch("codex"),
"prompt",
Some(&selection),
OneShotSessionPersistence::Ephemeral,
)
.unwrap();
assert_eq!(legacy, ephemeral);
let persistent = resolve_one_shot_plan_with_persistence(
&adapter_id,
&launch("codex"),
"prompt",
Some(&selection),
OneShotSessionPersistence::Persist,
)
.unwrap();
assert_eq!(
persistent.args,
[
"fixed",
"exec",
"--skip-git-repo-check",
"-s",
"read-only",
"--model",
"gpt-5.4",
"-c",
"model_reasoning_effort=xhigh",
]
);
assert_eq!(persistent.stdin_payload.as_deref(), Some("prompt"));
}
#[test]
fn persistent_sessions_fail_closed_for_non_codex_adapters() {
assert!(matches!(
resolve_one_shot_plan_with_persistence(
&AdapterId::new("claude").unwrap(),
&launch("claude"),
"prompt",
None,
OneShotSessionPersistence::Persist,
),
Err(OneShotAdapterError::PersistentSessionsUnsupported(id)) if id == "claude"
));
}
#[test]
fn every_registered_provider_builds_its_default_execution_path() {
let expected_flag = [
("claude", "--permission-mode"),
("codex", "--ephemeral"),
("kimi", "-p"),
];
for (id, flag) in expected_flag {
let plan = resolve_one_shot_plan(
&AdapterId::new(id).unwrap(),
&launch(&format!("{id}-binary")),
"provider prompt",
None,
)
.unwrap();
assert_eq!(plan.program, format!("{id}-binary"));
assert_eq!(plan.args.first().map(String::as_str), Some("fixed"));
assert!(plan.args.iter().any(|arg| arg == flag), "{id}: {flag}");
let delivery = one_shot_spec(&AdapterId::new(id).unwrap())
.unwrap()
.prompt_delivery;
assert_eq!(
plan.stdin_payload.is_some(),
delivery == OneShotPromptDelivery::StdinClose,
"{id}"
);
}
}
#[test]
fn static_models_fail_closed_while_dynamic_models_remain_discovery_compatible() {
assert!(matches!(
resolve_one_shot_plan(
&AdapterId::new("claude").unwrap(),
&launch("claude"),
"prompt",
Some(&SessionOptionSelection::new("unknown")),
),
Err(OneShotAdapterError::UnknownModel(_))
));
let dynamic = resolve_one_shot_plan(
&AdapterId::new("codex").unwrap(),
&launch("codex"),
"prompt",
Some(&SessionOptionSelection::new("vendor/new-model")),
)
.unwrap();
assert!(dynamic.args.contains(&"vendor/new-model".to_owned()));
}
}