@mmmbuto/masix 0.4.0 → 0.4.2
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.
- package/README.md +18 -14
- package/install.js +53 -27
- package/package.json +4 -3
- package/packages/plugin-base/codex-backend/0.1.4/SHA256SUMS +3 -0
- package/packages/plugin-base/codex-backend/0.1.4/codex-backend-android-aarch64-termux.pkg +0 -0
- package/packages/plugin-base/codex-backend/0.1.4/codex-backend-linux-x86_64.pkg +0 -0
- package/packages/plugin-base/codex-backend/0.1.4/codex-backend-macos-aarch64.pkg +0 -0
- package/packages/plugin-base/codex-backend/0.1.4/manifest.json +33 -0
- package/packages/plugin-base/codex-backend/CHANGELOG.md +17 -0
- package/packages/plugin-base/codex-backend/README.md +33 -0
- package/packages/plugin-base/codex-backend/source/Cargo.toml +25 -0
- package/packages/plugin-base/codex-backend/source/README-PACKAGE.txt +54 -0
- package/packages/plugin-base/codex-backend/source/plugin.manifest.json +103 -0
- package/packages/plugin-base/codex-backend/source/src/error.rs +60 -0
- package/packages/plugin-base/codex-backend/source/src/exec.rs +436 -0
- package/packages/plugin-base/codex-backend/source/src/http_backend.rs +1198 -0
- package/packages/plugin-base/codex-backend/source/src/lib.rs +328 -0
- package/packages/plugin-base/codex-backend/source/src/patch.rs +767 -0
- package/packages/plugin-base/codex-backend/source/src/policy.rs +297 -0
- package/packages/plugin-base/codex-backend/source/src/tools.rs +72 -0
- package/packages/plugin-base/codex-backend/source/src/workspace.rs +433 -0
- package/packages/plugin-base/codex-tools/0.1.3/SHA256SUMS +3 -0
- package/packages/plugin-base/codex-tools/0.1.3/codex-tools-android-aarch64-termux.pkg +0 -0
- package/packages/plugin-base/codex-tools/0.1.3/codex-tools-linux-x86_64.pkg +0 -0
- package/packages/plugin-base/codex-tools/0.1.3/codex-tools-macos-aarch64.pkg +0 -0
- package/packages/plugin-base/codex-tools/0.1.3/manifest.json +33 -0
- package/packages/plugin-base/codex-tools/CHANGELOG.md +17 -0
- package/packages/plugin-base/codex-tools/README.md +33 -0
- package/packages/plugin-base/codex-tools/source/Cargo.toml +23 -0
- package/packages/plugin-base/codex-tools/source/plugin.manifest.json +124 -0
- package/packages/plugin-base/codex-tools/source/src/main.rs +995 -0
- package/packages/plugin-base/discovery/0.2.4/SHA256SUMS +3 -0
- package/packages/plugin-base/discovery/0.2.4/discovery-android-aarch64-termux.pkg +0 -0
- package/packages/plugin-base/discovery/0.2.4/discovery-linux-x86_64.pkg +0 -0
- package/packages/plugin-base/discovery/0.2.4/discovery-macos-aarch64.pkg +0 -0
- package/packages/plugin-base/discovery/0.2.4/manifest.json +31 -0
- package/packages/plugin-base/discovery/CHANGELOG.md +17 -0
- package/packages/plugin-base/discovery/README.md +48 -0
- package/packages/plugin-base/discovery/source/Cargo.toml +14 -0
- package/packages/plugin-base/discovery/source/plugin.manifest.json +30 -0
- package/packages/plugin-base/discovery/source/src/main.rs +2570 -0
- 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
|
+
}
|