use std::{fmt, path::Path};
use serde_json::{Value, json};
use tokio::process::Command;
pub const MODEL: &str = "gpt-5.6-terra";
const DEVELOPER_INSTRUCTION: &str =
"Call the supplied dynamic function exactly once. Do not produce assistant prose.";
const CODEX_CONFIG: &str = "web_search=\"disabled\"|mcp_servers={}|features.shell_tool=false|features.apps=false|features.browser_use=false|features.computer_use=false|features.goals=false|features.hooks=false|features.image_generation=false|features.multi_agent=false|features.plugins=false|features.tool_suggest=false|features.remote_plugin=false|model_auto_compact_token_limit=9223372036854775807";
const DISABLED_EVENTS: &str = "commandExecution|fileChange|mcpToolCall|webSearch|imageView|imageGeneration|collabAgentToolCall|subAgentActivity";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NotificationKind {
Continue,
Usage,
TurnCompleted,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Error {
message: String,
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)
}
}
impl std::error::Error for Error {}
pub fn app_server_command(executable: &Path, working_directory: &Path) -> Command {
let mut command = Command::new(executable);
for value in CODEX_CONFIG.split('|') {
command.arg("-c").arg(value);
}
command
.args(["app-server", "--stdio"])
.current_dir(working_directory)
.env_remove("OPENAI_API_KEY")
.env_remove("CODEX_API_KEY");
command
}
pub fn initialize_params(version: &str) -> Value {
json!({
"clientInfo":{"name":"kcode-codex-terra","version":version},
"capabilities":{"experimentalApi":true}
})
}
pub fn thread_start_params(
working_directory: &Path,
tool_name: String,
tool_description: String,
input_schema: Value,
) -> Value {
json!({
"model":MODEL,
"cwd":working_directory,
"approvalPolicy":"never",
"sandbox":"readOnly",
"baseInstructions":"",
"developerInstructions":DEVELOPER_INSTRUCTION,
"dynamicTools":[{
"type":"function",
"name":tool_name,
"description":tool_description,
"inputSchema":input_schema
}],
"ephemeral":true,
"environments":[]
})
}
pub fn turn_start_params(thread_id: &str, input: String) -> Value {
json!({
"threadId":thread_id,
"input":[{"type":"text","text":input}],
"approvalPolicy":"never"
})
}
pub fn tool_success_result() -> Value {
json!({"success":true,"contentItems":[{"type":"inputText","text":"ok"}]})
}
pub fn classify_notification(
method: &str,
params: &Value,
thread_id: &str,
turn_id: &str,
) -> std::result::Result<NotificationKind, Error> {
match method {
"thread/started" => {
require_id(params, "/thread/id", thread_id)?;
Ok(NotificationKind::Continue)
}
"thread/tokenUsage/updated" => {
require_scope(params, thread_id, turn_id)?;
params
.get("tokenUsage")
.ok_or_else(|| protocol("Codex omitted token usage"))?;
Ok(NotificationKind::Usage)
}
"turn/started" => {
require_turn_object(params, thread_id, turn_id)?;
Ok(NotificationKind::Continue)
}
"item/started" | "item/completed" => {
require_scope(params, thread_id, turn_id)?;
validate_item(params)?;
Ok(NotificationKind::Continue)
}
"turn/completed" => {
require_turn_object(params, thread_id, turn_id)?;
if params.pointer("/turn/status").and_then(Value::as_str) != Some("completed") {
return Err(protocol("Codex turn failed"));
}
Ok(NotificationKind::TurnCompleted)
}
_ => {
validate_notification(method, params, thread_id, turn_id)?;
Ok(NotificationKind::Continue)
}
}
}
type Result<T> = std::result::Result<T, Error>;
fn validate_notification(method: &str, params: &Value, thread: &str, turn: &str) -> Result<()> {
if method.contains("rerout") || DISABLED_EVENTS.split('|').any(|kind| method.contains(kind)) {
return Err(protocol("Codex attempted a disabled capability"));
}
if matches!(
method,
"model/safetyBuffering/updated" | "model/verification"
) {
return require_scope(params, thread, turn);
}
if method.starts_with("thread/") {
return require_id(params, "/threadId", thread);
}
if method.starts_with("turn/") || method.starts_with("item/") {
require_scope(params, thread, turn)?;
if params.get("item").is_some() {
validate_item(params)?;
}
return Ok(());
}
Err(protocol("Codex emitted an unexpected event"))
}
fn validate_item(params: &Value) -> Result<()> {
match params.pointer("/item/type").and_then(Value::as_str) {
Some("userMessage" | "agentMessage" | "reasoning" | "dynamicToolCall") => Ok(()),
Some(_) => Err(protocol("Codex attempted a disabled built-in tool")),
None => Err(protocol("Codex item event omitted its item type")),
}
}
fn require_id(value: &Value, pointer: &str, expected: &str) -> Result<()> {
(value.pointer(pointer).and_then(Value::as_str) == Some(expected))
.then_some(())
.ok_or_else(|| protocol("Codex used a mismatched identifier"))
}
fn require_scope(value: &Value, thread: &str, turn: &str) -> Result<()> {
require_id(value, "/threadId", thread)?;
require_id(value, "/turnId", turn)
}
fn require_turn_object(value: &Value, thread: &str, turn: &str) -> Result<()> {
require_id(value, "/threadId", thread)?;
require_id(value, "/turn/id", turn)
}
fn protocol(message: impl Into<String>) -> Error {
Error {
message: message.into(),
}
}