mod apply_patch;
mod prompt;
mod shell_command;
use std::sync::Arc;
use locode_host::Host;
use locode_protocol::{ContentBlock, Message, Role};
use locode_tools::Registry;
use apply_patch::CodexApplyPatch;
use shell_command::CodexShellCommand;
use crate::pack::{Pack, PackContext};
const APPLY_PATCH_INSTRUCTIONS: &str = include_str!("templates/apply_patch_tool_instructions.md");
#[derive(Debug, Default, Clone, Copy)]
pub struct CodexPack;
impl Pack for CodexPack {
fn name(&self) -> &'static str {
"codex"
}
fn register(&self, host: &Arc<Host>, registry: &mut Registry) {
registry.register("shell_command", CodexShellCommand::new(Arc::clone(host)));
registry.register("apply_patch", CodexApplyPatch::new(Arc::clone(host)));
}
fn required_api_schemas(&self) -> Option<&'static [&'static str]> {
Some(&["openai-responses"])
}
fn preamble(&self, ctx: &PackContext) -> Vec<Message> {
let system = format!("{}\n{}", prompt::render(ctx), APPLY_PATCH_INSTRUCTIONS);
vec![
Message {
role: Role::System,
content: vec![ContentBlock::Text { text: system }],
},
Message {
role: Role::User,
content: vec![ContentBlock::Text {
text: prompt::environment_context(ctx),
}],
},
]
}
}
#[cfg(test)]
mod tests {
use super::*;
use locode_host::HostConfig;
use locode_protocol::ResultChunk;
use locode_tools::ToolCtx;
use serde_json::json;
use std::path::Path;
use tokio_util::sync::CancellationToken;
fn setup() -> (tempfile::TempDir, Registry, std::path::PathBuf) {
let dir = tempfile::tempdir().unwrap();
let mut config = HostConfig::new(dir.path());
config.login_shell = false;
let host = Arc::new(Host::new(config).unwrap());
let root = host.workspace_root().to_path_buf();
let registry = CodexPack.build_registry(&host);
(dir, registry, root)
}
fn ctx(dir: &Path) -> ToolCtx {
ToolCtx::new(
dir.to_path_buf(),
"c1".into(),
dir.to_path_buf(),
CancellationToken::new(),
)
}
fn result_text(block: &ContentBlock) -> String {
match block {
ContentBlock::ToolResult { content, .. } => content
.iter()
.filter_map(|chunk| match chunk {
ResultChunk::Text { text } => Some(text.clone()),
ResultChunk::Image { .. } => None,
})
.collect(),
_ => panic!("expected a tool_result"),
}
}
#[test]
fn pack_registers_shell_command_and_apply_patch() {
let (_dir, registry, _root) = setup();
let mut names: Vec<&str> = registry.names().collect();
names.sort_unstable();
assert_eq!(names, vec!["apply_patch", "shell_command"]);
assert_eq!(
registry.kind_of("shell_command"),
Some(locode_tools::ToolKind::Shell)
);
assert_eq!(
registry.kind_of("apply_patch"),
Some(locode_tools::ToolKind::Edit)
);
}
#[test]
fn shell_command_schema_is_faithful() {
let (_dir, registry, _root) = setup();
let specs = registry.specs();
let spec = specs
.iter()
.find(|s| s.name == "shell_command")
.expect("shell_command spec");
let params = match &spec.input {
locode_protocol::ToolInputFormat::JsonSchema { parameters } => parameters,
locode_protocol::ToolInputFormat::Freeform { .. } => panic!("JSON-schema tool"),
};
assert_eq!(params["additionalProperties"], json!(false));
let props = params["properties"].as_object().unwrap();
assert_eq!(
props["command"]["description"],
json!("Shell script to run in the user's default shell.")
);
for key in ["command", "workdir", "timeout_ms", "login"] {
assert!(props.contains_key(key), "missing field {key}");
}
for absent in ["sandbox_permissions", "justification", "prefix_rule"] {
assert!(!props.contains_key(absent), "{absent} must be absent");
}
let required: Vec<&str> = params["required"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert_eq!(required, vec!["command"]);
}
#[cfg(unix)]
#[tokio::test]
async fn shell_command_echo_frames_output() {
let (_dir, registry, root) = setup();
let out = registry
.dispatch(
"shell_command",
json!({ "command": "echo hi" }),
&ctx(&root),
)
.await;
assert!(out.record.ok);
let text = result_text(&out.tool_result);
assert!(text.starts_with("Exit code: 0\n"), "{text}");
assert!(text.contains("\nWall time: "), "{text}");
assert!(text.contains("\nOutput:\nhi"), "{text}");
assert_eq!(out.record.output["exit_code"], json!(0));
}
#[cfg(unix)]
#[tokio::test]
async fn shell_command_nonzero_exit_is_soft_ok() {
let (_dir, registry, root) = setup();
let out = registry
.dispatch(
"shell_command",
json!({ "command": "echo oops; exit 3" }),
&ctx(&root),
)
.await;
assert!(out.record.ok);
assert!(result_text(&out.tool_result).contains("Exit code: 3"));
assert_eq!(out.record.output["exit_code"], json!(3));
}
#[cfg(unix)]
#[tokio::test]
async fn shell_command_timeout_is_exit_124() {
let (_dir, registry, root) = setup();
let out = registry
.dispatch(
"shell_command",
json!({ "command": "sleep 5", "timeout_ms": 50 }),
&ctx(&root),
)
.await;
assert!(out.record.ok);
let text = result_text(&out.tool_result);
assert!(text.contains("Exit code: 124"), "{text}");
assert!(text.contains("command timed out after"), "{text}");
assert_eq!(out.record.output["timed_out"], json!(true));
}
#[tokio::test]
async fn shell_command_rejects_unknown_field() {
let (_dir, registry, root) = setup();
let out = registry
.dispatch(
"shell_command",
json!({ "command": "echo hi", "sandbox_permissions": "use_default" }),
&ctx(&root),
)
.await;
assert!(!out.record.ok);
}
#[test]
fn preamble_is_system_prompt_then_user_environment_context() {
let dir = tempfile::tempdir().unwrap();
let pc = PackContext {
cwd: dir.path().to_path_buf(),
os: "macos".into(),
shell: "/bin/zsh".into(),
date: "2026-07-24".into(),
headless: true,
is_git_repo: false,
model: Some("gpt-5.6-sol".into()),
os_version: None,
timezone: Some("America/Los_Angeles".into()),
strip_identity: false,
};
let msgs = CodexPack.preamble(&pc);
assert_eq!(msgs.len(), 2);
assert_eq!(msgs[0].role, Role::System);
assert_eq!(msgs[1].role, Role::User);
let ContentBlock::Text { text: system } = &msgs[0].content[0] else {
panic!("expected text");
};
assert!(system.starts_with("You are Codex"), "{system}");
assert!(system.contains("# Personality"), "full prompt present");
assert!(system.contains("## `apply_patch`"), "instructions appended");
assert!(
system.contains("*** Begin Patch"),
"instructions body present"
);
let ContentBlock::Text { text: env } = &msgs[1].content[0] else {
panic!("expected text");
};
assert!(env.starts_with("<environment_context>"), "{env}");
assert!(env.contains("<shell>zsh</shell>"), "{env}");
assert!(
env.contains("<timezone>America/Los_Angeles</timezone>"),
"{env}"
);
}
#[test]
fn codex_requires_the_responses_wire() {
assert_eq!(
CodexPack.required_api_schemas(),
Some(&["openai-responses"][..])
);
}
}