kcode_codex_terra_protocol/
lib.rs1use 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}