Skip to main content

kcode_codex_terra_protocol/
lib.rs

1use std::{fmt, path::Path};
2
3use serde_json::{Value, json};
4use tokio::process::Command;
5
6pub const MODEL: &str = "gpt-5.6-terra";
7
8const DEVELOPER_INSTRUCTION: &str =
9    "Call the supplied dynamic function exactly once. Do not produce assistant prose.";
10const 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";
11const DISABLED_EVENTS: &str = "commandExecution|fileChange|mcpToolCall|webSearch|imageView|imageGeneration|collabAgentToolCall|subAgentActivity";
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum NotificationKind {
15    Continue,
16    Usage,
17    TurnCompleted,
18}
19
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub struct Error {
22    message: String,
23}
24
25impl fmt::Display for Error {
26    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27        formatter.write_str(&self.message)
28    }
29}
30
31impl std::error::Error for Error {}
32
33pub fn app_server_command(executable: &Path, working_directory: &Path) -> Command {
34    let mut command = Command::new(executable);
35    for value in CODEX_CONFIG.split('|') {
36        command.arg("-c").arg(value);
37    }
38    command
39        .args(["app-server", "--stdio"])
40        .current_dir(working_directory)
41        .env_remove("OPENAI_API_KEY")
42        .env_remove("CODEX_API_KEY");
43    command
44}
45
46pub fn initialize_params(version: &str) -> Value {
47    json!({
48        "clientInfo":{"name":"kcode-codex-terra","version":version},
49        "capabilities":{"experimentalApi":true}
50    })
51}
52
53pub fn thread_start_params(
54    working_directory: &Path,
55    tool_name: String,
56    tool_description: String,
57    input_schema: Value,
58) -> Value {
59    json!({
60        "model":MODEL,
61        "cwd":working_directory,
62        "approvalPolicy":"never",
63        "sandbox":"readOnly",
64        "baseInstructions":"",
65        "developerInstructions":DEVELOPER_INSTRUCTION,
66        "dynamicTools":[{
67            "type":"function",
68            "name":tool_name,
69            "description":tool_description,
70            "inputSchema":input_schema
71        }],
72        "ephemeral":true,
73        "environments":[]
74    })
75}
76
77pub fn turn_start_params(thread_id: &str, input: String) -> Value {
78    json!({
79        "threadId":thread_id,
80        "input":[{"type":"text","text":input}],
81        "approvalPolicy":"never"
82    })
83}
84
85pub fn tool_success_result() -> Value {
86    json!({"success":true,"contentItems":[{"type":"inputText","text":"ok"}]})
87}
88
89pub fn classify_notification(
90    method: &str,
91    params: &Value,
92    thread_id: &str,
93    turn_id: &str,
94) -> std::result::Result<NotificationKind, Error> {
95    match method {
96        "thread/started" => {
97            require_id(params, "/thread/id", thread_id)?;
98            Ok(NotificationKind::Continue)
99        }
100        "thread/tokenUsage/updated" => {
101            require_scope(params, thread_id, turn_id)?;
102            params
103                .get("tokenUsage")
104                .ok_or_else(|| protocol("Codex omitted token usage"))?;
105            Ok(NotificationKind::Usage)
106        }
107        "turn/started" => {
108            require_turn_object(params, thread_id, turn_id)?;
109            Ok(NotificationKind::Continue)
110        }
111        "item/started" | "item/completed" => {
112            require_scope(params, thread_id, turn_id)?;
113            validate_item(params)?;
114            Ok(NotificationKind::Continue)
115        }
116        "turn/completed" => {
117            require_turn_object(params, thread_id, turn_id)?;
118            if params.pointer("/turn/status").and_then(Value::as_str) != Some("completed") {
119                return Err(protocol("Codex turn failed"));
120            }
121            Ok(NotificationKind::TurnCompleted)
122        }
123        _ => {
124            validate_notification(method, params, thread_id, turn_id)?;
125            Ok(NotificationKind::Continue)
126        }
127    }
128}
129
130type Result<T> = std::result::Result<T, Error>;
131
132fn validate_notification(method: &str, params: &Value, thread: &str, turn: &str) -> Result<()> {
133    if method.contains("rerout") || DISABLED_EVENTS.split('|').any(|kind| method.contains(kind)) {
134        return Err(protocol("Codex attempted a disabled capability"));
135    }
136    if matches!(
137        method,
138        "model/safetyBuffering/updated" | "model/verification"
139    ) {
140        return require_scope(params, thread, turn);
141    }
142    if method.starts_with("thread/") {
143        return require_id(params, "/threadId", thread);
144    }
145    if method.starts_with("turn/") || method.starts_with("item/") {
146        require_scope(params, thread, turn)?;
147        if params.get("item").is_some() {
148            validate_item(params)?;
149        }
150        return Ok(());
151    }
152    Err(protocol("Codex emitted an unexpected event"))
153}
154
155fn validate_item(params: &Value) -> Result<()> {
156    match params.pointer("/item/type").and_then(Value::as_str) {
157        Some("userMessage" | "agentMessage" | "reasoning" | "dynamicToolCall") => Ok(()),
158        Some(_) => Err(protocol("Codex attempted a disabled built-in tool")),
159        None => Err(protocol("Codex item event omitted its item type")),
160    }
161}
162
163fn require_id(value: &Value, pointer: &str, expected: &str) -> Result<()> {
164    (value.pointer(pointer).and_then(Value::as_str) == Some(expected))
165        .then_some(())
166        .ok_or_else(|| protocol("Codex used a mismatched identifier"))
167}
168
169fn require_scope(value: &Value, thread: &str, turn: &str) -> Result<()> {
170    require_id(value, "/threadId", thread)?;
171    require_id(value, "/turnId", turn)
172}
173
174fn require_turn_object(value: &Value, thread: &str, turn: &str) -> Result<()> {
175    require_id(value, "/threadId", thread)?;
176    require_id(value, "/turn/id", turn)
177}
178
179fn protocol(message: impl Into<String>) -> Error {
180    Error {
181        message: message.into(),
182    }
183}