@mmmbuto/masix 0.3.8 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +17 -15
  2. package/install.js +89 -26
  3. package/package.json +4 -3
  4. package/packages/plugin-base/codex-backend/0.1.4/SHA256SUMS +3 -0
  5. package/packages/plugin-base/codex-backend/0.1.4/codex-backend-android-aarch64-termux.pkg +0 -0
  6. package/packages/plugin-base/codex-backend/0.1.4/codex-backend-linux-x86_64.pkg +0 -0
  7. package/packages/plugin-base/codex-backend/0.1.4/codex-backend-macos-aarch64.pkg +0 -0
  8. package/packages/plugin-base/codex-backend/0.1.4/manifest.json +33 -0
  9. package/packages/plugin-base/codex-backend/CHANGELOG.md +17 -0
  10. package/packages/plugin-base/codex-backend/README.md +33 -0
  11. package/packages/plugin-base/codex-backend/source/Cargo.toml +25 -0
  12. package/packages/plugin-base/codex-backend/source/README-PACKAGE.txt +54 -0
  13. package/packages/plugin-base/codex-backend/source/plugin.manifest.json +103 -0
  14. package/packages/plugin-base/codex-backend/source/src/error.rs +60 -0
  15. package/packages/plugin-base/codex-backend/source/src/exec.rs +436 -0
  16. package/packages/plugin-base/codex-backend/source/src/http_backend.rs +1198 -0
  17. package/packages/plugin-base/codex-backend/source/src/lib.rs +328 -0
  18. package/packages/plugin-base/codex-backend/source/src/patch.rs +767 -0
  19. package/packages/plugin-base/codex-backend/source/src/policy.rs +297 -0
  20. package/packages/plugin-base/codex-backend/source/src/tools.rs +72 -0
  21. package/packages/plugin-base/codex-backend/source/src/workspace.rs +433 -0
  22. package/packages/plugin-base/codex-tools/0.1.3/SHA256SUMS +3 -0
  23. package/packages/plugin-base/codex-tools/0.1.3/codex-tools-android-aarch64-termux.pkg +0 -0
  24. package/packages/plugin-base/codex-tools/0.1.3/codex-tools-linux-x86_64.pkg +0 -0
  25. package/packages/plugin-base/codex-tools/0.1.3/codex-tools-macos-aarch64.pkg +0 -0
  26. package/packages/plugin-base/codex-tools/0.1.3/manifest.json +33 -0
  27. package/packages/plugin-base/codex-tools/CHANGELOG.md +17 -0
  28. package/packages/plugin-base/codex-tools/README.md +33 -0
  29. package/packages/plugin-base/codex-tools/source/Cargo.toml +23 -0
  30. package/packages/plugin-base/codex-tools/source/plugin.manifest.json +124 -0
  31. package/packages/plugin-base/codex-tools/source/src/main.rs +995 -0
  32. package/packages/plugin-base/discovery/0.2.4/SHA256SUMS +3 -0
  33. package/packages/plugin-base/discovery/0.2.4/discovery-android-aarch64-termux.pkg +0 -0
  34. package/packages/plugin-base/discovery/0.2.4/discovery-linux-x86_64.pkg +0 -0
  35. package/packages/plugin-base/discovery/0.2.4/discovery-macos-aarch64.pkg +0 -0
  36. package/packages/plugin-base/discovery/0.2.4/manifest.json +31 -0
  37. package/packages/plugin-base/discovery/CHANGELOG.md +17 -0
  38. package/packages/plugin-base/discovery/README.md +48 -0
  39. package/packages/plugin-base/discovery/source/Cargo.toml +14 -0
  40. package/packages/plugin-base/discovery/source/plugin.manifest.json +30 -0
  41. package/packages/plugin-base/discovery/source/src/main.rs +2570 -0
  42. package/prebuilt/masix +0 -0
@@ -0,0 +1,995 @@
1
+ //! MCP binary plugin exposing codex-backend tools for MasiX runtime
2
+ //!
3
+ //! This plugin wraps the codex-backend library and exposes it as MCP tools
4
+ //! over stdio JSON-RPC. All tools require admin privileges.
5
+
6
+ use anyhow::{anyhow, Result};
7
+ use clap::{Parser, Subcommand};
8
+ use masix_codex_backend::http_backend::HttpBackendConfig;
9
+ use masix_codex_backend::{
10
+ self as backend, CodingBackend, CodingMode, CodingTask, HttpBackend, PermissionLevel,
11
+ PolicyContext, ProviderType, DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_MAX_TOKENS,
12
+ DEFAULT_TIMEOUT_SECS,
13
+ };
14
+ use masix_codex_backend::{
15
+ apply_patch, preview_patch, resolve_workspace, rollback_patch, CommandExecutor, ExecRequest,
16
+ ExecutionProfile, WorkspaceConfig,
17
+ };
18
+ use serde::{Deserialize, Serialize};
19
+ use std::io::{self, BufRead, Write};
20
+ use std::path::PathBuf;
21
+ use std::time::Duration;
22
+ use tracing::{debug, error, info};
23
+
24
+ const MODULE_VERSION: &str = env!("CARGO_PKG_VERSION");
25
+ const BACKEND_VERSION: &str = backend::MODULE_VERSION;
26
+
27
+ /// Detect current platform at runtime
28
+ fn detect_platform() -> String {
29
+ #[cfg(target_os = "android")]
30
+ {
31
+ format!("android-{}-termux", std::env::consts::ARCH)
32
+ }
33
+ #[cfg(not(target_os = "android"))]
34
+ {
35
+ format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH)
36
+ }
37
+ }
38
+
39
+ #[derive(Parser)]
40
+ #[command(name = "masix-plugin-codex-tools")]
41
+ #[command(about = "MCP tools for codex-backend - coding agent tools for MasiX")]
42
+ struct Cli {
43
+ #[command(subcommand)]
44
+ command: Commands,
45
+ }
46
+
47
+ #[derive(Subcommand)]
48
+ enum Commands {
49
+ /// Print plugin metadata
50
+ Manifest,
51
+ /// Run MCP server over stdio (JSON-RPC)
52
+ ServeMcp,
53
+ /// Run a coding task directly (CLI mode, for testing)
54
+ Run {
55
+ /// Repository path
56
+ #[arg(short, long)]
57
+ repo: String,
58
+ /// Instructions for the coding agent
59
+ #[arg(short, long)]
60
+ instructions: String,
61
+ /// Mode: read_only or workspace_write
62
+ #[arg(long, default_value = "read_only")]
63
+ mode: String,
64
+ /// Timeout in seconds
65
+ #[arg(long, default_value_t = 300)]
66
+ timeout: u64,
67
+ },
68
+ }
69
+
70
+ #[derive(Debug, Deserialize)]
71
+ struct JsonRpcRequest {
72
+ #[allow(dead_code)]
73
+ jsonrpc: String,
74
+ id: Option<serde_json::Value>,
75
+ method: String,
76
+ #[serde(default)]
77
+ params: serde_json::Value,
78
+ }
79
+
80
+ #[derive(Debug, Serialize)]
81
+ struct JsonRpcResponse {
82
+ jsonrpc: String,
83
+ id: Option<serde_json::Value>,
84
+ #[serde(skip_serializing_if = "Option::is_none")]
85
+ result: Option<serde_json::Value>,
86
+ #[serde(skip_serializing_if = "Option::is_none")]
87
+ error: Option<JsonRpcError>,
88
+ }
89
+
90
+ #[derive(Debug, Serialize)]
91
+ struct JsonRpcError {
92
+ code: i32,
93
+ message: String,
94
+ }
95
+
96
+ #[derive(Debug, Serialize)]
97
+ struct ToolDefinition {
98
+ name: String,
99
+ description: String,
100
+ input_schema: serde_json::Value,
101
+ }
102
+
103
+ #[derive(Debug, Serialize)]
104
+ struct ToolResult {
105
+ content: Vec<ToolContent>,
106
+ #[serde(skip_serializing_if = "is_false")]
107
+ is_error: bool,
108
+ }
109
+
110
+ fn is_false(b: &bool) -> bool {
111
+ !b
112
+ }
113
+
114
+ #[derive(Debug, Serialize)]
115
+ struct ToolContent {
116
+ #[serde(rename = "type")]
117
+ content_type: String,
118
+ text: String,
119
+ }
120
+
121
+ // Tool input schemas
122
+ #[derive(Debug, Deserialize)]
123
+ struct CodexRunInput {
124
+ repo_path: String,
125
+ instructions: String,
126
+ #[serde(default)]
127
+ mode: Option<String>,
128
+ #[serde(default = "default_timeout")]
129
+ timeout_secs: u64,
130
+ #[serde(default = "default_max_steps")]
131
+ max_steps: u32,
132
+ }
133
+
134
+ fn default_timeout() -> u64 {
135
+ DEFAULT_TIMEOUT_SECS
136
+ }
137
+
138
+ fn default_max_steps() -> u32 {
139
+ 50
140
+ }
141
+
142
+ /// Input for codex_exec_command
143
+ #[derive(Debug, Deserialize)]
144
+ struct CodexExecInput {
145
+ /// Command to execute
146
+ command: String,
147
+ /// Command arguments
148
+ #[serde(default)]
149
+ args: Vec<String>,
150
+ /// Working directory (optional, defaults to workspace root)
151
+ #[serde(default)]
152
+ cwd: Option<String>,
153
+ /// Timeout in seconds (default 60, max 300)
154
+ #[serde(default = "default_exec_timeout")]
155
+ timeout_secs: u64,
156
+ /// Maximum output bytes (default 1MB)
157
+ #[serde(default = "default_max_output")]
158
+ max_output_bytes: usize,
159
+ /// Execution profile (default: workspace_locked)
160
+ #[serde(default)]
161
+ profile: Option<String>,
162
+ /// Repository path for workspace resolution (optional)
163
+ #[serde(default)]
164
+ repo_path: Option<String>,
165
+ }
166
+
167
+ fn default_exec_timeout() -> u64 {
168
+ 60
169
+ }
170
+
171
+ fn default_max_output() -> usize {
172
+ DEFAULT_MAX_OUTPUT_BYTES
173
+ }
174
+
175
+ /// Output for codex_exec_command
176
+ #[derive(Debug, Serialize)]
177
+ struct CodexExecOutput {
178
+ command: String,
179
+ args: Vec<String>,
180
+ exit_code: Option<i32>,
181
+ stdout: String,
182
+ stderr: String,
183
+ timed_out: bool,
184
+ output_truncated: bool,
185
+ duration_ms: u64,
186
+ workspace_root: String,
187
+ workspace_source: String,
188
+ profile: String,
189
+ }
190
+
191
+ #[derive(Debug, Serialize)]
192
+ struct CodexRunOutput {
193
+ summary: String,
194
+ exit_status: String,
195
+ #[serde(skip_serializing_if = "Option::is_none")]
196
+ patch: Option<String>,
197
+ logs: Vec<String>,
198
+ /// Structured event traces for AI workers
199
+ events: Vec<serde_json::Value>,
200
+ }
201
+
202
+ #[derive(Debug, Serialize)]
203
+ struct CodexDryRunOutput {
204
+ backend: &'static str,
205
+ provider_type: String,
206
+ #[serde(skip_serializing_if = "Option::is_none")]
207
+ base_url: Option<String>,
208
+ #[serde(skip_serializing_if = "Option::is_none")]
209
+ model: Option<String>,
210
+ timeout_secs: u64,
211
+ mode: String,
212
+ repo_path: String,
213
+ policy_allowed: bool,
214
+ requires_admin: bool,
215
+ }
216
+
217
+ #[derive(Debug, Serialize)]
218
+ struct CodexStatusOutput {
219
+ module_id: &'static str,
220
+ module_version: &'static str,
221
+ backend_version: &'static str,
222
+ provider_mode: String,
223
+ limits: LimitsInfo,
224
+ safety: SafetyInfo,
225
+ }
226
+
227
+ #[derive(Debug, Serialize)]
228
+ struct LimitsInfo {
229
+ default_timeout_secs: u64,
230
+ max_output_bytes: usize,
231
+ max_log_entries: usize,
232
+ }
233
+
234
+ #[derive(Debug, Serialize)]
235
+ struct SafetyInfo {
236
+ path_traversal_guard: bool,
237
+ symlink_escape_guard: bool,
238
+ output_truncation: bool,
239
+ }
240
+
241
+ #[derive(Debug, Serialize)]
242
+ struct CodexCapabilitiesOutput {
243
+ tools: &'static [&'static str],
244
+ limits: LimitsInfo,
245
+ platform: String,
246
+ requires_admin: bool,
247
+ }
248
+
249
+ fn get_tool_definitions() -> Vec<ToolDefinition> {
250
+ vec![
251
+ ToolDefinition {
252
+ name: "codex_run".to_string(),
253
+ description: "Execute a coding task via the codex HTTP backend (requires admin)"
254
+ .to_string(),
255
+ input_schema: serde_json::json!({
256
+ "type": "object",
257
+ "properties": {
258
+ "repo_path": {
259
+ "type": "string",
260
+ "description": "Absolute path to repository root"
261
+ },
262
+ "instructions": {
263
+ "type": "string",
264
+ "description": "Task instructions for the coding agent"
265
+ },
266
+ "mode": {
267
+ "type": "string",
268
+ "enum": ["read_only", "workspace_write"],
269
+ "default": "read_only",
270
+ "description": "Execution mode"
271
+ },
272
+ "timeout_secs": {
273
+ "type": "integer",
274
+ "default": 300,
275
+ "minimum": 10,
276
+ "maximum": 3600,
277
+ "description": "Maximum execution time"
278
+ },
279
+ "max_steps": {
280
+ "type": "integer",
281
+ "default": 50,
282
+ "minimum": 1,
283
+ "maximum": 200,
284
+ "description": "Maximum tool call iterations"
285
+ }
286
+ },
287
+ "required": ["repo_path", "instructions"]
288
+ }),
289
+ },
290
+ ToolDefinition {
291
+ name: "codex_dry_run".to_string(),
292
+ description: "Preview execution settings without making changes".to_string(),
293
+ input_schema: serde_json::json!({
294
+ "type": "object",
295
+ "properties": {
296
+ "repo_path": {
297
+ "type": "string",
298
+ "description": "Absolute path to repository root"
299
+ },
300
+ "instructions": {
301
+ "type": "string",
302
+ "description": "Task instructions (for context only)"
303
+ },
304
+ "mode": {
305
+ "type": "string",
306
+ "enum": ["read_only", "workspace_write"],
307
+ "default": "read_only"
308
+ },
309
+ "timeout_secs": {
310
+ "type": "integer",
311
+ "default": 300
312
+ },
313
+ "max_steps": {
314
+ "type": "integer",
315
+ "default": 50
316
+ }
317
+ },
318
+ "required": ["repo_path", "instructions"]
319
+ }),
320
+ },
321
+ ToolDefinition {
322
+ name: "codex_status".to_string(),
323
+ description: "Get module version and health information".to_string(),
324
+ input_schema: serde_json::json!({
325
+ "type": "object",
326
+ "properties": {},
327
+ "additionalProperties": false
328
+ }),
329
+ },
330
+ ToolDefinition {
331
+ name: "codex_capabilities".to_string(),
332
+ description: "Get machine-readable metadata for AI workers".to_string(),
333
+ input_schema: serde_json::json!({
334
+ "type": "object",
335
+ "properties": {},
336
+ "additionalProperties": false
337
+ }),
338
+ },
339
+ ToolDefinition {
340
+ name: "codex_exec_command".to_string(),
341
+ description: "Execute a command in a bounded, secure environment (requires admin)"
342
+ .to_string(),
343
+ input_schema: serde_json::json!({
344
+ "type": "object",
345
+ "properties": {
346
+ "command": {
347
+ "type": "string",
348
+ "description": "Command to execute (must be in allowlist)"
349
+ },
350
+ "args": {
351
+ "type": "array",
352
+ "items": { "type": "string" },
353
+ "default": [],
354
+ "description": "Command arguments"
355
+ },
356
+ "cwd": {
357
+ "type": "string",
358
+ "description": "Working directory (must be within workspace)"
359
+ },
360
+ "timeout_secs": {
361
+ "type": "integer",
362
+ "default": 60,
363
+ "minimum": 1,
364
+ "maximum": 300,
365
+ "description": "Maximum execution time"
366
+ },
367
+ "max_output_bytes": {
368
+ "type": "integer",
369
+ "default": 1048576,
370
+ "minimum": 1024,
371
+ "maximum": 10485760,
372
+ "description": "Maximum output size"
373
+ },
374
+ "profile": {
375
+ "type": "string",
376
+ "enum": ["workspace_locked", "workspace_plus_roots", "system_unlocked"],
377
+ "default": "workspace_locked",
378
+ "description": "Execution profile"
379
+ },
380
+ "repo_path": {
381
+ "type": "string",
382
+ "description": "Repository path for workspace resolution (optional)"
383
+ }
384
+ },
385
+ "required": ["command"]
386
+ }),
387
+ },
388
+ ToolDefinition {
389
+ name: "codex_patch_preview".to_string(),
390
+ description: "Preview a diff patch without applying it (requires admin)".to_string(),
391
+ input_schema: serde_json::json!({
392
+ "type": "object",
393
+ "properties": {
394
+ "diff": {
395
+ "type": "string",
396
+ "description": "Unified diff content to preview"
397
+ },
398
+ "repo_path": {
399
+ "type": "string",
400
+ "description": "Repository path for workspace resolution"
401
+ }
402
+ },
403
+ "required": ["diff", "repo_path"]
404
+ }),
405
+ },
406
+ ToolDefinition {
407
+ name: "codex_apply_patch".to_string(),
408
+ description: "Apply a diff patch with automatic backup (requires admin)".to_string(),
409
+ input_schema: serde_json::json!({
410
+ "type": "object",
411
+ "properties": {
412
+ "diff": {
413
+ "type": "string",
414
+ "description": "Unified diff content to apply"
415
+ },
416
+ "repo_path": {
417
+ "type": "string",
418
+ "description": "Repository path for workspace resolution"
419
+ },
420
+ "create_backup": {
421
+ "type": "boolean",
422
+ "default": true,
423
+ "description": "Create backup before applying (recommended)"
424
+ }
425
+ },
426
+ "required": ["diff", "repo_path"]
427
+ }),
428
+ },
429
+ ToolDefinition {
430
+ name: "codex_rollback_patch".to_string(),
431
+ description: "Rollback a previously applied patch using backup ID (requires admin)"
432
+ .to_string(),
433
+ input_schema: serde_json::json!({
434
+ "type": "object",
435
+ "properties": {
436
+ "backup_id": {
437
+ "type": "string",
438
+ "description": "Backup ID returned from codex_apply_patch"
439
+ },
440
+ "repo_path": {
441
+ "type": "string",
442
+ "description": "Repository path for workspace resolution"
443
+ }
444
+ },
445
+ "required": ["backup_id", "repo_path"]
446
+ }),
447
+ },
448
+ ]
449
+ }
450
+
451
+ fn parse_mode(s: Option<&str>) -> CodingMode {
452
+ match s.map(|s| s.to_lowercase()).as_deref() {
453
+ Some("workspace_write") | Some("write") => CodingMode::WorkspaceWrite,
454
+ _ => CodingMode::ReadOnly,
455
+ }
456
+ }
457
+
458
+ fn policy_context_from_env() -> Option<PolicyContext> {
459
+ let level = std::env::var("MASIX_CALLER_PERMISSION_LEVEL")
460
+ .ok()
461
+ .and_then(|v| PermissionLevel::from_str(v.trim()));
462
+ level.map(PolicyContext::new)
463
+ }
464
+
465
+ fn check_admin_policy() -> Result<()> {
466
+ use masix_codex_backend::AdminPolicy;
467
+ let policy = AdminPolicy::new();
468
+ if let Some(ctx) = policy_context_from_env() {
469
+ policy
470
+ .check(Some(&ctx))
471
+ .map_err(|e| anyhow!("Admin policy failed: {}", e))
472
+ } else {
473
+ // Compatibility mode: core runtime already enforces admin_only tools before MCP calls.
474
+ Ok(())
475
+ }
476
+ }
477
+
478
+ fn resolve_repo_root(repo_path: &str) -> Result<PathBuf> {
479
+ let mut workspace_config = WorkspaceConfig::default().with_repo_path(repo_path);
480
+ if let Ok(cwd) = std::env::current_dir() {
481
+ workspace_config = workspace_config.with_runtime_workdir(cwd);
482
+ }
483
+
484
+ let workspace = resolve_workspace(&workspace_config)
485
+ .map_err(|e| anyhow!("Workspace resolution failed: {}", e))?;
486
+ Ok(workspace.root)
487
+ }
488
+
489
+ fn get_backend_from_env() -> Result<HttpBackend> {
490
+ let api_key = std::env::var("CODEX_API_KEY")
491
+ .or_else(|_| std::env::var("OPENAI_API_KEY"))
492
+ .map_err(|_| anyhow!("No API key found. Set CODEX_API_KEY or OPENAI_API_KEY"))?;
493
+
494
+ let provider_type = std::env::var("CODEX_PROVIDER")
495
+ .ok()
496
+ .and_then(|s| ProviderType::from_str(&s))
497
+ .unwrap_or(ProviderType::OpenAI);
498
+
499
+ let base_url = std::env::var("CODEX_BASE_URL").ok();
500
+ let model = std::env::var("CODEX_MODEL").ok();
501
+ let timeout_secs = std::env::var("CODEX_TIMEOUT_SECS")
502
+ .ok()
503
+ .and_then(|s| s.parse().ok())
504
+ .unwrap_or(DEFAULT_TIMEOUT_SECS);
505
+
506
+ let config = HttpBackendConfig {
507
+ provider_type,
508
+ api_key,
509
+ base_url,
510
+ model,
511
+ timeout: Duration::from_secs(timeout_secs),
512
+ max_tokens: DEFAULT_MAX_TOKENS,
513
+ max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
514
+ max_log_bytes: 64 * 1024,
515
+ legacy_fallback: false,
516
+ };
517
+
518
+ HttpBackend::new(config).map_err(|e| anyhow!("Backend init failed: {}", e))
519
+ }
520
+
521
+ async fn run_mcp_server() -> Result<()> {
522
+ info!("Starting codex-tools MCP server");
523
+ let stdin = io::stdin();
524
+ let mut stdout = io::stdout();
525
+
526
+ for line in stdin.lock().lines() {
527
+ let line = line?;
528
+ let line = line.trim();
529
+ if line.is_empty() {
530
+ continue;
531
+ }
532
+
533
+ debug!("Received: {}", line.chars().take(200).collect::<String>());
534
+
535
+ let request: JsonRpcRequest = match serde_json::from_str(line) {
536
+ Ok(req) => req,
537
+ Err(e) => {
538
+ let response = JsonRpcResponse {
539
+ jsonrpc: "2.0".to_string(),
540
+ id: None,
541
+ result: None,
542
+ error: Some(JsonRpcError {
543
+ code: -32700,
544
+ message: format!("Parse error: {}", e),
545
+ }),
546
+ };
547
+ writeln!(stdout, "{}", serde_json::to_string(&response)?)?;
548
+ stdout.flush()?;
549
+ continue;
550
+ }
551
+ };
552
+
553
+ let response = handle_mcp_request(&request).await;
554
+ let response_str = serde_json::to_string(&response)?;
555
+ writeln!(stdout, "{}", response_str)?;
556
+ stdout.flush()?;
557
+ }
558
+
559
+ Ok(())
560
+ }
561
+
562
+ async fn handle_mcp_request(request: &JsonRpcRequest) -> JsonRpcResponse {
563
+ match request.method.as_str() {
564
+ "initialize" => JsonRpcResponse {
565
+ jsonrpc: "2.0".to_string(),
566
+ id: request.id.clone(),
567
+ result: Some(serde_json::json!({
568
+ "protocolVersion": "2024-11-05",
569
+ "capabilities": {
570
+ "tools": {}
571
+ },
572
+ "serverInfo": {
573
+ "name": "masix-codex-tools",
574
+ "version": MODULE_VERSION
575
+ }
576
+ })),
577
+ error: None,
578
+ },
579
+ "notifications/initialized" => JsonRpcResponse {
580
+ jsonrpc: "2.0".to_string(),
581
+ id: None,
582
+ result: None,
583
+ error: None,
584
+ },
585
+ "tools/list" => {
586
+ let tools = get_tool_definitions();
587
+ JsonRpcResponse {
588
+ jsonrpc: "2.0".to_string(),
589
+ id: request.id.clone(),
590
+ result: Some(serde_json::json!({ "tools": tools })),
591
+ error: None,
592
+ }
593
+ }
594
+ "tools/call" => {
595
+ let params = &request.params;
596
+ let tool_name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
597
+ let arguments = params
598
+ .get("arguments")
599
+ .cloned()
600
+ .unwrap_or(serde_json::json!({}));
601
+
602
+ match handle_tool_call(tool_name, arguments).await {
603
+ Ok(result) => JsonRpcResponse {
604
+ jsonrpc: "2.0".to_string(),
605
+ id: request.id.clone(),
606
+ result: Some(serde_json::to_value(result).unwrap_or(serde_json::json!({}))),
607
+ error: None,
608
+ },
609
+ Err(e) => {
610
+ error!("Tool call error: {}", e);
611
+ JsonRpcResponse {
612
+ jsonrpc: "2.0".to_string(),
613
+ id: request.id.clone(),
614
+ result: Some(
615
+ serde_json::to_value(ToolResult {
616
+ content: vec![ToolContent {
617
+ content_type: "text".to_string(),
618
+ text: format!("Error: {}", e),
619
+ }],
620
+ is_error: true,
621
+ })
622
+ .unwrap_or(serde_json::json!({})),
623
+ ),
624
+ error: None,
625
+ }
626
+ }
627
+ }
628
+ }
629
+ _ => JsonRpcResponse {
630
+ jsonrpc: "2.0".to_string(),
631
+ id: request.id.clone(),
632
+ result: None,
633
+ error: Some(JsonRpcError {
634
+ code: -32601,
635
+ message: format!("Method not found: {}", request.method),
636
+ }),
637
+ },
638
+ }
639
+ }
640
+
641
+ async fn handle_tool_call(name: &str, arguments: serde_json::Value) -> Result<ToolResult> {
642
+ match name {
643
+ "codex_run" => {
644
+ let input: CodexRunInput =
645
+ serde_json::from_value(arguments).map_err(|e| anyhow!("Invalid input: {}", e))?;
646
+
647
+ let backend = get_backend_from_env()?;
648
+ let mode = parse_mode(input.mode.as_deref());
649
+
650
+ let task = CodingTask {
651
+ repo_path: resolve_repo_root(&input.repo_path)?,
652
+ instructions: input.instructions,
653
+ mode,
654
+ timeout: Some(Duration::from_secs(input.timeout_secs)),
655
+ max_steps: Some(input.max_steps),
656
+ policy: policy_context_from_env().or_else(|| Some(PolicyContext::admin())),
657
+ };
658
+
659
+ let result = backend.run(task).await?;
660
+
661
+ let output = CodexRunOutput {
662
+ summary: result.summary,
663
+ exit_status: result.exit_status.to_string(),
664
+ patch: result.patch,
665
+ logs: result.logs,
666
+ events: result
667
+ .events
668
+ .iter()
669
+ .map(|e| serde_json::to_value(e).unwrap_or_default())
670
+ .collect(),
671
+ };
672
+
673
+ Ok(ToolResult {
674
+ content: vec![ToolContent {
675
+ content_type: "text".to_string(),
676
+ text: serde_json::to_string_pretty(&output)?,
677
+ }],
678
+ is_error: false,
679
+ })
680
+ }
681
+ "codex_dry_run" => {
682
+ let input: CodexRunInput =
683
+ serde_json::from_value(arguments).map_err(|e| anyhow!("Invalid input: {}", e))?;
684
+
685
+ // Build DryRunInfo directly from env vars — no API key required
686
+ let provider_type = std::env::var("CODEX_PROVIDER")
687
+ .ok()
688
+ .and_then(|s| ProviderType::from_str(&s))
689
+ .unwrap_or(ProviderType::OpenAI);
690
+
691
+ let base_url = std::env::var("CODEX_BASE_URL").ok();
692
+ let model = std::env::var("CODEX_MODEL").ok();
693
+ let has_api_key = std::env::var("CODEX_API_KEY")
694
+ .or_else(|_| std::env::var("OPENAI_API_KEY"))
695
+ .is_ok();
696
+
697
+ let mode = parse_mode(input.mode.as_deref());
698
+ let repo_path = resolve_repo_root(&input.repo_path)?;
699
+
700
+ let output = CodexDryRunOutput {
701
+ backend: "http",
702
+ provider_type: provider_type.as_str().to_string(),
703
+ base_url,
704
+ model,
705
+ timeout_secs: input.timeout_secs,
706
+ mode: match mode {
707
+ CodingMode::ReadOnly => "read-only".to_string(),
708
+ CodingMode::WorkspaceWrite => "workspace-write".to_string(),
709
+ },
710
+ repo_path: repo_path.to_string_lossy().to_string(),
711
+ policy_allowed: has_api_key,
712
+ requires_admin: true,
713
+ };
714
+
715
+ Ok(ToolResult {
716
+ content: vec![ToolContent {
717
+ content_type: "text".to_string(),
718
+ text: serde_json::to_string_pretty(&output)?,
719
+ }],
720
+ is_error: false,
721
+ })
722
+ }
723
+ "codex_status" => {
724
+ let provider_mode = std::env::var("CODEX_PROVIDER")
725
+ .ok()
726
+ .unwrap_or_else(|| "openai".to_string());
727
+
728
+ let output = CodexStatusOutput {
729
+ module_id: "codex-tools",
730
+ module_version: MODULE_VERSION,
731
+ backend_version: BACKEND_VERSION,
732
+ provider_mode,
733
+ limits: LimitsInfo {
734
+ default_timeout_secs: DEFAULT_TIMEOUT_SECS,
735
+ max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
736
+ max_log_entries: backend::MAX_LOG_ENTRIES,
737
+ },
738
+ safety: SafetyInfo {
739
+ path_traversal_guard: true,
740
+ symlink_escape_guard: true,
741
+ output_truncation: true,
742
+ },
743
+ };
744
+
745
+ Ok(ToolResult {
746
+ content: vec![ToolContent {
747
+ content_type: "text".to_string(),
748
+ text: serde_json::to_string_pretty(&output)?,
749
+ }],
750
+ is_error: false,
751
+ })
752
+ }
753
+ "codex_capabilities" => {
754
+ let output = CodexCapabilitiesOutput {
755
+ tools: &[
756
+ "codex_run",
757
+ "codex_dry_run",
758
+ "codex_status",
759
+ "codex_capabilities",
760
+ "codex_exec_command",
761
+ "codex_patch_preview",
762
+ "codex_apply_patch",
763
+ "codex_rollback_patch",
764
+ ],
765
+ limits: LimitsInfo {
766
+ default_timeout_secs: DEFAULT_TIMEOUT_SECS,
767
+ max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
768
+ max_log_entries: backend::MAX_LOG_ENTRIES,
769
+ },
770
+ platform: detect_platform(),
771
+ requires_admin: true,
772
+ };
773
+
774
+ Ok(ToolResult {
775
+ content: vec![ToolContent {
776
+ content_type: "text".to_string(),
777
+ text: serde_json::to_string_pretty(&output)?,
778
+ }],
779
+ is_error: false,
780
+ })
781
+ }
782
+ "codex_exec_command" => {
783
+ check_admin_policy()?;
784
+ let input: CodexExecInput =
785
+ serde_json::from_value(arguments).map_err(|e| anyhow!("Invalid input: {}", e))?;
786
+
787
+ // Resolve workspace
788
+ let profile = input
789
+ .profile
790
+ .as_deref()
791
+ .and_then(ExecutionProfile::from_str)
792
+ .unwrap_or_default();
793
+
794
+ let mut workspace_config = WorkspaceConfig::default().with_profile(profile);
795
+ if let Some(ref repo_path) = input.repo_path {
796
+ workspace_config = workspace_config.with_repo_path(repo_path);
797
+ }
798
+ if let Ok(cwd) = std::env::current_dir() {
799
+ workspace_config = workspace_config.with_runtime_workdir(cwd);
800
+ }
801
+
802
+ let workspace = resolve_workspace(&workspace_config)
803
+ .map_err(|e| anyhow!("Workspace resolution failed: {}", e))?;
804
+
805
+ // Create executor
806
+ let executor = CommandExecutor::new(workspace.clone()).with_profile(workspace.profile);
807
+
808
+ // Build request
809
+ let exec_request = ExecRequest::new(&input.command)
810
+ .args(&input.args)
811
+ .timeout_secs(input.timeout_secs)
812
+ .max_output_bytes(input.max_output_bytes);
813
+
814
+ let exec_request = if let Some(ref cwd) = input.cwd {
815
+ exec_request.cwd(cwd)
816
+ } else {
817
+ exec_request
818
+ };
819
+
820
+ // Execute
821
+ let result = executor.execute(exec_request).await?;
822
+
823
+ // Determine error status before moving result fields
824
+ let is_error = !result.success() && !result.timed_out;
825
+
826
+ let output = CodexExecOutput {
827
+ command: input.command,
828
+ args: input.args,
829
+ exit_code: result.exit_code,
830
+ stdout: result.stdout,
831
+ stderr: result.stderr,
832
+ timed_out: result.timed_out,
833
+ output_truncated: result.output_truncated,
834
+ duration_ms: result.duration.as_millis() as u64,
835
+ workspace_root: workspace.root.to_string_lossy().to_string(),
836
+ workspace_source: workspace.source.to_string(),
837
+ profile: workspace.profile.to_string(),
838
+ };
839
+
840
+ Ok(ToolResult {
841
+ content: vec![ToolContent {
842
+ content_type: "text".to_string(),
843
+ text: serde_json::to_string_pretty(&output)?,
844
+ }],
845
+ is_error,
846
+ })
847
+ }
848
+ "codex_patch_preview" => {
849
+ check_admin_policy()?;
850
+ #[derive(Debug, Deserialize)]
851
+ struct PatchPreviewInput {
852
+ diff: String,
853
+ repo_path: String,
854
+ }
855
+
856
+ let input: PatchPreviewInput =
857
+ serde_json::from_value(arguments).map_err(|e| anyhow!("Invalid input: {}", e))?;
858
+
859
+ let workspace_config = WorkspaceConfig::default().with_repo_path(&input.repo_path);
860
+ let workspace = resolve_workspace(&workspace_config)
861
+ .map_err(|e| anyhow!("Workspace resolution failed: {}", e))?;
862
+
863
+ let preview = preview_patch(&input.diff, &workspace)
864
+ .map_err(|e| anyhow!("Patch preview failed: {}", e))?;
865
+
866
+ Ok(ToolResult {
867
+ content: vec![ToolContent {
868
+ content_type: "text".to_string(),
869
+ text: serde_json::to_string_pretty(&preview)?,
870
+ }],
871
+ is_error: false,
872
+ })
873
+ }
874
+ "codex_apply_patch" => {
875
+ check_admin_policy()?;
876
+ #[derive(Debug, Deserialize)]
877
+ struct PatchApplyInput {
878
+ diff: String,
879
+ repo_path: String,
880
+ #[serde(default = "default_create_backup")]
881
+ create_backup: bool,
882
+ }
883
+
884
+ fn default_create_backup() -> bool {
885
+ true
886
+ }
887
+
888
+ let input: PatchApplyInput =
889
+ serde_json::from_value(arguments).map_err(|e| anyhow!("Invalid input: {}", e))?;
890
+
891
+ let workspace_config = WorkspaceConfig::default().with_repo_path(&input.repo_path);
892
+ let workspace = resolve_workspace(&workspace_config)
893
+ .map_err(|e| anyhow!("Workspace resolution failed: {}", e))?;
894
+
895
+ let result = apply_patch(&input.diff, &workspace, input.create_backup)
896
+ .map_err(|e| anyhow!("Patch apply failed: {}", e))?;
897
+
898
+ Ok(ToolResult {
899
+ content: vec![ToolContent {
900
+ content_type: "text".to_string(),
901
+ text: serde_json::to_string_pretty(&result)?,
902
+ }],
903
+ is_error: !result.success,
904
+ })
905
+ }
906
+ "codex_rollback_patch" => {
907
+ check_admin_policy()?;
908
+ #[derive(Debug, Deserialize)]
909
+ struct RollbackInput {
910
+ backup_id: String,
911
+ repo_path: String,
912
+ }
913
+
914
+ let input: RollbackInput =
915
+ serde_json::from_value(arguments).map_err(|e| anyhow!("Invalid input: {}", e))?;
916
+
917
+ let workspace_config = WorkspaceConfig::default().with_repo_path(&input.repo_path);
918
+ let workspace = resolve_workspace(&workspace_config)
919
+ .map_err(|e| anyhow!("Workspace resolution failed: {}", e))?;
920
+
921
+ let restored = rollback_patch(&input.backup_id, &workspace)
922
+ .map_err(|e| anyhow!("Rollback failed: {}", e))?;
923
+
924
+ let output = serde_json::json!({
925
+ "success": true,
926
+ "backup_id": input.backup_id,
927
+ "restored_files": restored
928
+ });
929
+
930
+ Ok(ToolResult {
931
+ content: vec![ToolContent {
932
+ content_type: "text".to_string(),
933
+ text: serde_json::to_string_pretty(&output)?,
934
+ }],
935
+ is_error: false,
936
+ })
937
+ }
938
+ _ => Err(anyhow!("Unknown tool: {}", name)),
939
+ }
940
+ }
941
+
942
+ #[tokio::main]
943
+ async fn main() -> Result<()> {
944
+ tracing_subscriber::fmt()
945
+ .with_env_filter(
946
+ tracing_subscriber::EnvFilter::from_default_env()
947
+ .add_directive("masix_plugin_codex_tools=info".parse()?),
948
+ )
949
+ .with_writer(std::io::stderr)
950
+ .init();
951
+
952
+ let cli = Cli::parse();
953
+
954
+ match cli.command {
955
+ Commands::Manifest => {
956
+ let manifest = serde_json::json!({
957
+ "id": "codex-tools",
958
+ "name": "MasiX Codex Tools",
959
+ "version": MODULE_VERSION,
960
+ "tools": [
961
+ "codex_run", "codex_dry_run", "codex_status", "codex_capabilities",
962
+ "codex_exec_command", "codex_patch_preview", "codex_apply_patch",
963
+ "codex_rollback_patch"
964
+ ],
965
+ "capabilities": ["http_client", "file_read", "file_write", "command_exec", "patch_apply"],
966
+ "requires_admin": true,
967
+ "backend_version": BACKEND_VERSION
968
+ });
969
+ println!("{}", serde_json::to_string_pretty(&manifest)?);
970
+ }
971
+ Commands::ServeMcp => {
972
+ run_mcp_server().await?;
973
+ }
974
+ Commands::Run {
975
+ repo,
976
+ instructions,
977
+ mode,
978
+ timeout,
979
+ } => {
980
+ let backend = get_backend_from_env()?;
981
+ let task = CodingTask {
982
+ repo_path: resolve_repo_root(&repo)?,
983
+ instructions,
984
+ mode: parse_mode(Some(&mode)),
985
+ timeout: Some(Duration::from_secs(timeout)),
986
+ max_steps: Some(50),
987
+ policy: Some(PolicyContext::unenforced()), // CLI mode
988
+ };
989
+ let result = backend.run(task).await?;
990
+ println!("{}", serde_json::to_string_pretty(&result)?);
991
+ }
992
+ }
993
+
994
+ Ok(())
995
+ }