1use agcodex_core::protocol::AskForApproval;
4use agcodex_mcp_types::Tool;
5use agcodex_mcp_types::ToolInputSchema;
6use agcodex_protocol::config_types::SandboxMode;
7use schemars::JsonSchema;
8use schemars::generate::SchemaSettings;
9use serde::Deserialize;
10use serde::Serialize;
11use std::collections::HashMap;
12use std::path::PathBuf;
13
14use crate::json_to_toml::json_to_toml;
15
16#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
18#[serde(rename_all = "kebab-case")]
19pub struct CodexToolCallParam {
20 pub prompt: String,
22
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub model: Option<String>,
26
27 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub profile: Option<String>,
30
31 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub cwd: Option<String>,
35
36 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub approval_policy: Option<CodexToolCallApprovalPolicy>,
40
41 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub sandbox: Option<CodexToolCallSandboxMode>,
44
45 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub config: Option<HashMap<String, serde_json::Value>>,
49
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub base_instructions: Option<String>,
53
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub include_plan_tool: Option<bool>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
62#[serde(rename_all = "kebab-case")]
63pub enum CodexToolCallApprovalPolicy {
64 Untrusted,
65 OnFailure,
66 OnRequest,
67 Never,
68}
69
70impl From<CodexToolCallApprovalPolicy> for AskForApproval {
71 fn from(value: CodexToolCallApprovalPolicy) -> Self {
72 match value {
73 CodexToolCallApprovalPolicy::Untrusted => AskForApproval::UnlessTrusted,
74 CodexToolCallApprovalPolicy::OnFailure => AskForApproval::OnFailure,
75 CodexToolCallApprovalPolicy::OnRequest => AskForApproval::OnRequest,
76 CodexToolCallApprovalPolicy::Never => AskForApproval::Never,
77 }
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
84#[serde(rename_all = "kebab-case")]
85pub enum CodexToolCallSandboxMode {
86 ReadOnly,
87 WorkspaceWrite,
88 DangerFullAccess,
89}
90
91impl From<CodexToolCallSandboxMode> for SandboxMode {
92 fn from(value: CodexToolCallSandboxMode) -> Self {
93 match value {
94 CodexToolCallSandboxMode::ReadOnly => SandboxMode::ReadOnly,
95 CodexToolCallSandboxMode::WorkspaceWrite => SandboxMode::WorkspaceWrite,
96 CodexToolCallSandboxMode::DangerFullAccess => SandboxMode::DangerFullAccess,
97 }
98 }
99}
100
101pub(crate) fn create_tool_for_codex_tool_call_param() -> Tool {
103 let schema = SchemaSettings::draft2019_09()
104 .with(|s| {
105 s.inline_subschemas = true;
106 })
107 .into_generator()
108 .into_root_schema_for::<CodexToolCallParam>();
109
110 #[expect(clippy::expect_used)]
111 let schema_value =
112 serde_json::to_value(&schema).expect("Codex tool schema should serialise to JSON");
113
114 let tool_input_schema =
115 serde_json::from_value::<ToolInputSchema>(schema_value).unwrap_or_else(|e| {
116 panic!("failed to create Tool from schema: {e}");
117 });
118
119 Tool {
120 name: "agcodex".to_string(),
121 title: Some("Codex".to_string()),
122 input_schema: tool_input_schema,
123 output_schema: None,
125 description: Some(
126 "Run a Codex session. Accepts configuration parameters matching the Codex Config struct.".to_string(),
127 ),
128 annotations: None,
129 }
130}
131
132impl CodexToolCallParam {
133 pub fn into_config(
136 self,
137 codex_linux_sandbox_exe: Option<PathBuf>,
138 ) -> std::io::Result<(String, agcodex_core::config::Config)> {
139 let Self {
140 prompt,
141 model,
142 profile,
143 cwd,
144 approval_policy,
145 sandbox,
146 config: cli_overrides,
147 base_instructions,
148 include_plan_tool,
149 } = self;
150
151 let overrides = agcodex_core::config::ConfigOverrides {
153 model,
154 config_profile: profile,
155 cwd: cwd.map(PathBuf::from),
156 approval_policy: approval_policy.map(Into::into),
157 sandbox_mode: sandbox.map(Into::into),
158 model_provider: None,
159 codex_linux_sandbox_exe,
160 base_instructions,
161 include_plan_tool,
162 include_apply_patch_tool: None,
163 disable_response_storage: None,
164 show_raw_agent_reasoning: None,
165 };
166
167 let cli_overrides = cli_overrides
168 .unwrap_or_default()
169 .into_iter()
170 .map(|(k, v)| (k, json_to_toml(v)))
171 .collect();
172
173 let cfg = agcodex_core::config::Config::load_with_cli_overrides(cli_overrides, overrides)?;
174
175 Ok((prompt, cfg))
176 }
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
180#[serde(rename_all = "camelCase")]
181pub struct CodexToolCallReplyParam {
182 pub session_id: String,
184
185 pub prompt: String,
187}
188
189pub(crate) fn create_tool_for_codex_tool_call_reply_param() -> Tool {
191 let schema = SchemaSettings::draft2019_09()
192 .with(|s| {
193 s.inline_subschemas = true;
194 })
195 .into_generator()
196 .into_root_schema_for::<CodexToolCallReplyParam>();
197
198 #[expect(clippy::expect_used)]
199 let schema_value =
200 serde_json::to_value(&schema).expect("Codex reply tool schema should serialise to JSON");
201
202 let tool_input_schema =
203 serde_json::from_value::<ToolInputSchema>(schema_value).unwrap_or_else(|e| {
204 panic!("failed to create Tool from schema: {e}");
205 });
206
207 Tool {
208 name: "agcodex-reply".to_string(),
209 title: Some("Codex Reply".to_string()),
210 input_schema: tool_input_schema,
211 output_schema: None,
212 description: Some(
213 "Continue a Codex session by providing the session id and prompt.".to_string(),
214 ),
215 annotations: None,
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use pretty_assertions::assert_eq;
223
224 #[test]
236 fn verify_codex_tool_json_schema() {
237 let tool = create_tool_for_codex_tool_call_param();
238 let tool_json = serde_json::to_value(&tool).expect("tool serializes");
239 let expected_tool_json = serde_json::json!({
240 "name": "agcodex",
241 "title": "Codex",
242 "description": "Run a Codex session. Accepts configuration parameters matching the Codex Config struct.",
243 "inputSchema": {
244 "type": "object",
245 "properties": {
246 "approval-policy": {
247 "description": "Approval policy for shell commands generated by the model:\n`untrusted`, `on-failure`, `on-request`, `never`.",
248 "enum": [
249 "untrusted",
250 "on-failure",
251 "on-request",
252 "never",
253 null
254 ],
255 "type": [
256 "string",
257 "null"
258 ]
259 },
260 "sandbox": {
261 "description": "Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`.",
262 "enum": [
263 "read-only",
264 "workspace-write",
265 "danger-full-access",
266 null
267 ],
268 "type": [
269 "string",
270 "null"
271 ]
272 },
273 "config": {
274 "additionalProperties": true,
275 "description": "Individual config settings that will override what is in\nCODEX_HOME/config.toml.",
276 "type": [
277 "object",
278 "null"
279 ]
280 },
281 "cwd": {
282 "description": "Working directory for the session. If relative, it is resolved against\nthe server process's current working directory.",
283 "type": [
284 "string",
285 "null"
286 ]
287 },
288 "include-plan-tool": {
289 "description": "Whether to include the plan tool in the conversation.",
290 "type": [
291 "boolean",
292 "null"
293 ]
294 },
295 "model": {
296 "description": "Optional override for the model name (e.g. \"o3\", \"o4-mini\").",
297 "type": [
298 "string",
299 "null"
300 ]
301 },
302 "profile": {
303 "description": "Configuration profile from config.toml to specify default options.",
304 "type": [
305 "string",
306 "null"
307 ]
308 },
309 "prompt": {
310 "description": "The *initial user prompt* to start the Codex conversation.",
311 "type": "string"
312 },
313 "base-instructions": {
314 "description": "The set of instructions to use instead of the default ones.",
315 "type": [
316 "string",
317 "null"
318 ]
319 },
320 },
321 "required": [
322 "prompt"
323 ]
324 }
325 });
326 assert_eq!(expected_tool_json, tool_json);
327 }
328
329 #[test]
330 fn verify_codex_tool_reply_json_schema() {
331 let tool = create_tool_for_codex_tool_call_reply_param();
332 let tool_json = serde_json::to_value(&tool).expect("tool serializes");
333 let expected_tool_json = serde_json::json!({
334 "description": "Continue a Codex session by providing the session id and prompt.",
335 "inputSchema": {
336 "properties": {
337 "prompt": {
338 "description": "The *next user prompt* to continue the Codex conversation.",
339 "type": "string"
340 },
341 "sessionId": {
342 "description": "The *session id* for this conversation.",
343 "type": "string"
344 },
345 },
346 "required": [
347 "sessionId",
348 "prompt"
349 ],
350 "type": "object",
351 },
352 "name": "agcodex-reply",
353 "title": "Codex Reply",
354 });
355 assert_eq!(expected_tool_json, tool_json);
356 }
357}