kcode-codex-terra-protocol 0.1.1

Deterministic Codex Terra command, JSON, and notification protocol helpers.
Documentation
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(),
    }
}