@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.
- package/README.md +17 -15
- package/install.js +89 -26
- 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,433 @@
|
|
|
1
|
+
//! Workspace resolution with fallback chain
|
|
2
|
+
//!
|
|
3
|
+
//! Provides robust path resolution that falls back through:
|
|
4
|
+
//! 1. Explicit repo_path if valid and existing
|
|
5
|
+
//! 2. Runtime-provided workdir
|
|
6
|
+
//! 3. Module-configured default root
|
|
7
|
+
//!
|
|
8
|
+
//! All paths are canonicalized and validated against the active execution profile.
|
|
9
|
+
|
|
10
|
+
use crate::CodingError;
|
|
11
|
+
use std::fmt;
|
|
12
|
+
use std::path::{Path, PathBuf};
|
|
13
|
+
|
|
14
|
+
/// Default workspace root for Termux environments
|
|
15
|
+
#[cfg(target_os = "android")]
|
|
16
|
+
pub fn default_workspace_root() -> PathBuf {
|
|
17
|
+
PathBuf::from("/data/data/com.termux/files/home")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Default workspace root for non-Termux environments
|
|
21
|
+
#[cfg(not(target_os = "android"))]
|
|
22
|
+
pub fn default_workspace_root() -> PathBuf {
|
|
23
|
+
std::env::var("HOME")
|
|
24
|
+
.map(PathBuf::from)
|
|
25
|
+
.or_else(|_| std::env::var("USERPROFILE").map(PathBuf::from))
|
|
26
|
+
.unwrap_or_else(|_| PathBuf::from("/tmp"))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Execution profile controlling path access scope
|
|
30
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
31
|
+
pub enum ExecutionProfile {
|
|
32
|
+
/// Workspace locked - only paths under resolved workspace root
|
|
33
|
+
#[default]
|
|
34
|
+
WorkspaceLocked,
|
|
35
|
+
/// Workspace plus configured roots - workspace + allowlisted paths
|
|
36
|
+
WorkspacePlusRoots,
|
|
37
|
+
/// System unlocked - admin-only, full system access with logging
|
|
38
|
+
SystemUnlocked,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
impl ExecutionProfile {
|
|
42
|
+
pub fn as_str(&self) -> &'static str {
|
|
43
|
+
match self {
|
|
44
|
+
ExecutionProfile::WorkspaceLocked => "workspace_locked",
|
|
45
|
+
ExecutionProfile::WorkspacePlusRoots => "workspace_plus_roots",
|
|
46
|
+
ExecutionProfile::SystemUnlocked => "system_unlocked",
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub fn from_str(s: &str) -> Option<Self> {
|
|
51
|
+
match s.to_lowercase().as_str() {
|
|
52
|
+
"workspace_locked" | "workspacelocked" => Some(ExecutionProfile::WorkspaceLocked),
|
|
53
|
+
"workspace_plus_roots" | "workspaceplusroots" => {
|
|
54
|
+
Some(ExecutionProfile::WorkspacePlusRoots)
|
|
55
|
+
}
|
|
56
|
+
"system_unlocked" | "systemunlocked" => Some(ExecutionProfile::SystemUnlocked),
|
|
57
|
+
_ => None,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
impl fmt::Display for ExecutionProfile {
|
|
63
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
64
|
+
write!(f, "{}", self.as_str())
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Configuration for workspace resolution
|
|
69
|
+
#[derive(Debug, Clone)]
|
|
70
|
+
pub struct WorkspaceConfig {
|
|
71
|
+
/// Explicit repo path from tool input
|
|
72
|
+
pub repo_path: Option<PathBuf>,
|
|
73
|
+
/// Runtime-provided working directory
|
|
74
|
+
pub runtime_workdir: Option<PathBuf>,
|
|
75
|
+
/// Module-configured default root
|
|
76
|
+
pub default_root: Option<PathBuf>,
|
|
77
|
+
/// Additional allowed roots (for WorkspacePlusRoots profile)
|
|
78
|
+
pub allowed_roots: Vec<PathBuf>,
|
|
79
|
+
/// Execution profile
|
|
80
|
+
pub profile: ExecutionProfile,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
impl Default for WorkspaceConfig {
|
|
84
|
+
fn default() -> Self {
|
|
85
|
+
Self {
|
|
86
|
+
repo_path: None,
|
|
87
|
+
runtime_workdir: None,
|
|
88
|
+
default_root: Some(default_workspace_root()),
|
|
89
|
+
allowed_roots: Vec::new(),
|
|
90
|
+
profile: ExecutionProfile::WorkspaceLocked,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
impl WorkspaceConfig {
|
|
96
|
+
/// Create a new workspace config with explicit repo path
|
|
97
|
+
pub fn with_repo_path(mut self, path: impl Into<PathBuf>) -> Self {
|
|
98
|
+
let path: PathBuf = path.into();
|
|
99
|
+
if path.as_os_str().is_empty() {
|
|
100
|
+
self.repo_path = None;
|
|
101
|
+
} else {
|
|
102
|
+
self.repo_path = Some(path);
|
|
103
|
+
}
|
|
104
|
+
self
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Set runtime workdir
|
|
108
|
+
pub fn with_runtime_workdir(mut self, path: impl Into<PathBuf>) -> Self {
|
|
109
|
+
self.runtime_workdir = Some(path.into());
|
|
110
|
+
self
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Set default root
|
|
114
|
+
pub fn with_default_root(mut self, path: impl Into<PathBuf>) -> Self {
|
|
115
|
+
self.default_root = Some(path.into());
|
|
116
|
+
self
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Add an allowed root
|
|
120
|
+
pub fn with_allowed_root(mut self, path: impl Into<PathBuf>) -> Self {
|
|
121
|
+
self.allowed_roots.push(path.into());
|
|
122
|
+
self
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Set execution profile
|
|
126
|
+
pub fn with_profile(mut self, profile: ExecutionProfile) -> Self {
|
|
127
|
+
self.profile = profile;
|
|
128
|
+
self
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// Resolved workspace with validated root
|
|
133
|
+
#[derive(Debug, Clone)]
|
|
134
|
+
pub struct ResolvedWorkspace {
|
|
135
|
+
/// Canonical workspace root
|
|
136
|
+
pub root: PathBuf,
|
|
137
|
+
/// Source of the resolution
|
|
138
|
+
pub source: WorkspaceSource,
|
|
139
|
+
/// Execution profile in effect
|
|
140
|
+
pub profile: ExecutionProfile,
|
|
141
|
+
/// Canonical allowed roots for WorkspacePlusRoots profile
|
|
142
|
+
pub allowed_roots: Vec<PathBuf>,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Source of workspace resolution
|
|
146
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
147
|
+
pub enum WorkspaceSource {
|
|
148
|
+
/// Explicit repo_path was used
|
|
149
|
+
ExplicitRepoPath,
|
|
150
|
+
/// Runtime workdir was used
|
|
151
|
+
RuntimeWorkdir,
|
|
152
|
+
/// Default root was used
|
|
153
|
+
DefaultRoot,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
impl fmt::Display for WorkspaceSource {
|
|
157
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
158
|
+
match self {
|
|
159
|
+
WorkspaceSource::ExplicitRepoPath => write!(f, "explicit_repo_path"),
|
|
160
|
+
WorkspaceSource::RuntimeWorkdir => write!(f, "runtime_workdir"),
|
|
161
|
+
WorkspaceSource::DefaultRoot => write!(f, "default_root"),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
impl ResolvedWorkspace {
|
|
167
|
+
/// Check if a path is within the workspace (or allowed roots)
|
|
168
|
+
pub fn is_path_allowed(&self, path: &Path) -> Result<PathBuf, CodingError> {
|
|
169
|
+
// Canonicalize the path (or its parent if it doesn't exist yet)
|
|
170
|
+
let canonical_path = if path.exists() {
|
|
171
|
+
path.canonicalize().map_err(|e| {
|
|
172
|
+
CodingError::PathSecurityViolation(format!("Cannot canonicalize path: {}", e))
|
|
173
|
+
})?
|
|
174
|
+
} else {
|
|
175
|
+
// For non-existent paths, canonicalize parent and join
|
|
176
|
+
let mut check_path = path.to_path_buf();
|
|
177
|
+
while !check_path.exists() {
|
|
178
|
+
if !check_path.pop() {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
let canonical_parent = check_path.canonicalize().map_err(|e| {
|
|
183
|
+
CodingError::PathSecurityViolation(format!(
|
|
184
|
+
"Cannot canonicalize parent path: {}",
|
|
185
|
+
e
|
|
186
|
+
))
|
|
187
|
+
})?;
|
|
188
|
+
canonical_parent.join(path.strip_prefix(&check_path).unwrap_or(path))
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Canonicalize workspace root as well to avoid false negatives on platforms
|
|
192
|
+
// where temp/system paths have alias forms (e.g. /var vs /private/var on macOS).
|
|
193
|
+
let canonical_root = self
|
|
194
|
+
.root
|
|
195
|
+
.canonicalize()
|
|
196
|
+
.unwrap_or_else(|_| self.root.clone());
|
|
197
|
+
|
|
198
|
+
// SystemUnlocked allows anything (admin-only)
|
|
199
|
+
if self.profile == ExecutionProfile::SystemUnlocked {
|
|
200
|
+
return Ok(canonical_path);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Check against workspace root
|
|
204
|
+
if canonical_path.starts_with(&canonical_root) {
|
|
205
|
+
return Ok(canonical_path);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check against allowed roots (for WorkspacePlusRoots profile)
|
|
209
|
+
if self.profile == ExecutionProfile::WorkspacePlusRoots {
|
|
210
|
+
for allowed in &self.allowed_roots {
|
|
211
|
+
let canonical_allowed = allowed.canonicalize().unwrap_or_else(|_| allowed.clone());
|
|
212
|
+
if canonical_path.starts_with(&canonical_allowed) {
|
|
213
|
+
return Ok(canonical_path);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
Err(CodingError::PathSecurityViolation(format!(
|
|
219
|
+
"Path '{}' is outside workspace root '{}'",
|
|
220
|
+
path.display(),
|
|
221
|
+
canonical_root.display()
|
|
222
|
+
)))
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// Resolve workspace using fallback chain
|
|
227
|
+
pub fn resolve_workspace(config: &WorkspaceConfig) -> Result<ResolvedWorkspace, CodingError> {
|
|
228
|
+
let canonical_allowed_roots: Vec<PathBuf> = config
|
|
229
|
+
.allowed_roots
|
|
230
|
+
.iter()
|
|
231
|
+
.filter(|p| p.exists())
|
|
232
|
+
.filter_map(|p| p.canonicalize().ok())
|
|
233
|
+
.collect();
|
|
234
|
+
|
|
235
|
+
let build = |root: PathBuf, source: WorkspaceSource| ResolvedWorkspace {
|
|
236
|
+
root,
|
|
237
|
+
source,
|
|
238
|
+
profile: config.profile,
|
|
239
|
+
allowed_roots: canonical_allowed_roots.clone(),
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Try explicit repo_path first
|
|
243
|
+
if let Some(ref repo_path) = config.repo_path {
|
|
244
|
+
if repo_path.exists() {
|
|
245
|
+
let canonical = repo_path.canonicalize().map_err(|e| {
|
|
246
|
+
CodingError::IoError(format!(
|
|
247
|
+
"Cannot canonicalize repo_path '{}': {}",
|
|
248
|
+
repo_path.display(),
|
|
249
|
+
e
|
|
250
|
+
))
|
|
251
|
+
})?;
|
|
252
|
+
return Ok(build(canonical, WorkspaceSource::ExplicitRepoPath));
|
|
253
|
+
}
|
|
254
|
+
// repo_path was provided but doesn't exist - this is no longer a hard error
|
|
255
|
+
// We fall through to fallbacks
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Try runtime workdir
|
|
259
|
+
if let Some(ref workdir) = config.runtime_workdir {
|
|
260
|
+
if workdir.exists() {
|
|
261
|
+
let canonical = workdir.canonicalize().map_err(|e| {
|
|
262
|
+
CodingError::IoError(format!(
|
|
263
|
+
"Cannot canonicalize runtime_workdir '{}': {}",
|
|
264
|
+
workdir.display(),
|
|
265
|
+
e
|
|
266
|
+
))
|
|
267
|
+
})?;
|
|
268
|
+
return Ok(build(canonical, WorkspaceSource::RuntimeWorkdir));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Fall back to default root
|
|
273
|
+
if let Some(ref default) = config.default_root {
|
|
274
|
+
if default.exists() {
|
|
275
|
+
let canonical = default.canonicalize().map_err(|e| {
|
|
276
|
+
CodingError::IoError(format!(
|
|
277
|
+
"Cannot canonicalize default_root '{}': {}",
|
|
278
|
+
default.display(),
|
|
279
|
+
e
|
|
280
|
+
))
|
|
281
|
+
})?;
|
|
282
|
+
return Ok(build(canonical, WorkspaceSource::DefaultRoot));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Last resort: use current directory
|
|
287
|
+
let cwd = std::env::current_dir()
|
|
288
|
+
.map_err(|e| CodingError::IoError(format!("Cannot get current directory: {}", e)))?;
|
|
289
|
+
let canonical = cwd
|
|
290
|
+
.canonicalize()
|
|
291
|
+
.map_err(|e| CodingError::IoError(format!("Cannot canonicalize cwd: {}", e)))?;
|
|
292
|
+
|
|
293
|
+
Ok(build(canonical, WorkspaceSource::DefaultRoot))
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/// Resolve a relative path within a workspace
|
|
297
|
+
pub fn resolve_relative_path(
|
|
298
|
+
relative: &str,
|
|
299
|
+
workspace: &ResolvedWorkspace,
|
|
300
|
+
) -> Result<PathBuf, CodingError> {
|
|
301
|
+
// Security checks
|
|
302
|
+
let normalized = relative.replace('\\', "/");
|
|
303
|
+
|
|
304
|
+
// Reject absolute paths
|
|
305
|
+
if normalized.starts_with('/') || normalized.starts_with('~') {
|
|
306
|
+
return Err(CodingError::PathSecurityViolation(
|
|
307
|
+
"Absolute/home paths not allowed".into(),
|
|
308
|
+
));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Reject path traversal
|
|
312
|
+
for component in normalized.split('/') {
|
|
313
|
+
if component == ".." {
|
|
314
|
+
return Err(CodingError::PathSecurityViolation(
|
|
315
|
+
"Path traversal not allowed".into(),
|
|
316
|
+
));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let resolved = workspace.root.join(&normalized);
|
|
321
|
+
|
|
322
|
+
// Verify the path is within workspace
|
|
323
|
+
workspace.is_path_allowed(&resolved)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#[cfg(test)]
|
|
327
|
+
mod tests {
|
|
328
|
+
use super::*;
|
|
329
|
+
use std::fs;
|
|
330
|
+
|
|
331
|
+
#[test]
|
|
332
|
+
fn test_execution_profile_from_str() {
|
|
333
|
+
assert_eq!(
|
|
334
|
+
ExecutionProfile::from_str("workspace_locked"),
|
|
335
|
+
Some(ExecutionProfile::WorkspaceLocked)
|
|
336
|
+
);
|
|
337
|
+
assert_eq!(
|
|
338
|
+
ExecutionProfile::from_str("system_unlocked"),
|
|
339
|
+
Some(ExecutionProfile::SystemUnlocked)
|
|
340
|
+
);
|
|
341
|
+
assert_eq!(ExecutionProfile::from_str("invalid"), None);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
#[test]
|
|
345
|
+
fn test_workspace_config_builder() {
|
|
346
|
+
let config = WorkspaceConfig::default()
|
|
347
|
+
.with_repo_path("/tmp/test")
|
|
348
|
+
.with_profile(ExecutionProfile::WorkspaceLocked);
|
|
349
|
+
|
|
350
|
+
assert_eq!(config.repo_path, Some(PathBuf::from("/tmp/test")));
|
|
351
|
+
assert_eq!(config.profile, ExecutionProfile::WorkspaceLocked);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#[test]
|
|
355
|
+
fn test_resolve_workspace_with_existing_path() {
|
|
356
|
+
let tmp_dir = std::env::temp_dir();
|
|
357
|
+
let test_path = tmp_dir.join("masix_test_workspace");
|
|
358
|
+
fs::create_dir_all(&test_path).ok();
|
|
359
|
+
|
|
360
|
+
let config = WorkspaceConfig::default().with_repo_path(&test_path);
|
|
361
|
+
let result = resolve_workspace(&config);
|
|
362
|
+
|
|
363
|
+
assert!(result.is_ok());
|
|
364
|
+
let workspace = result.unwrap();
|
|
365
|
+
assert_eq!(workspace.source, WorkspaceSource::ExplicitRepoPath);
|
|
366
|
+
|
|
367
|
+
fs::remove_dir(&test_path).ok();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
#[test]
|
|
371
|
+
fn test_resolve_workspace_fallback_to_default() {
|
|
372
|
+
let config = WorkspaceConfig {
|
|
373
|
+
repo_path: Some(PathBuf::from("/nonexistent/path/12345")),
|
|
374
|
+
runtime_workdir: None,
|
|
375
|
+
default_root: Some(std::env::temp_dir()),
|
|
376
|
+
allowed_roots: Vec::new(),
|
|
377
|
+
profile: ExecutionProfile::WorkspaceLocked,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
let result = resolve_workspace(&config);
|
|
381
|
+
assert!(result.is_ok());
|
|
382
|
+
let workspace = result.unwrap();
|
|
383
|
+
assert_eq!(workspace.source, WorkspaceSource::DefaultRoot);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
#[test]
|
|
387
|
+
fn test_resolve_relative_path_rejects_absolute() {
|
|
388
|
+
let workspace = ResolvedWorkspace {
|
|
389
|
+
root: PathBuf::from("/tmp"),
|
|
390
|
+
source: WorkspaceSource::DefaultRoot,
|
|
391
|
+
profile: ExecutionProfile::WorkspaceLocked,
|
|
392
|
+
allowed_roots: Vec::new(),
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
let result = resolve_relative_path("/etc/passwd", &workspace);
|
|
396
|
+
assert!(matches!(result, Err(CodingError::PathSecurityViolation(_))));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#[test]
|
|
400
|
+
fn test_resolve_relative_path_rejects_traversal() {
|
|
401
|
+
let workspace = ResolvedWorkspace {
|
|
402
|
+
root: PathBuf::from("/tmp"),
|
|
403
|
+
source: WorkspaceSource::DefaultRoot,
|
|
404
|
+
profile: ExecutionProfile::WorkspaceLocked,
|
|
405
|
+
allowed_roots: Vec::new(),
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
let result = resolve_relative_path("../../../etc/passwd", &workspace);
|
|
409
|
+
assert!(matches!(result, Err(CodingError::PathSecurityViolation(_))));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
#[test]
|
|
413
|
+
fn test_workspace_plus_roots_allows_extra_root() {
|
|
414
|
+
let tmp_dir = std::env::temp_dir();
|
|
415
|
+
let workspace_root = tmp_dir.join("masix_workspace_root");
|
|
416
|
+
let allowed_root = tmp_dir.join("masix_allowed_root");
|
|
417
|
+
fs::create_dir_all(&workspace_root).ok();
|
|
418
|
+
fs::create_dir_all(&allowed_root).ok();
|
|
419
|
+
|
|
420
|
+
let config = WorkspaceConfig::default()
|
|
421
|
+
.with_repo_path(&workspace_root)
|
|
422
|
+
.with_profile(ExecutionProfile::WorkspacePlusRoots)
|
|
423
|
+
.with_allowed_root(&allowed_root);
|
|
424
|
+
let resolved = resolve_workspace(&config).unwrap();
|
|
425
|
+
|
|
426
|
+
let target = allowed_root.join("nested").join("file.txt");
|
|
427
|
+
let allowed = resolved.is_path_allowed(&target);
|
|
428
|
+
assert!(allowed.is_ok());
|
|
429
|
+
|
|
430
|
+
fs::remove_dir_all(&workspace_root).ok();
|
|
431
|
+
fs::remove_dir_all(&allowed_root).ok();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
b3daf8cfe3531fdf675ddf4578a65c3f5186ad9e490d02ea2a9b90d1653b4920 codex-tools-android-aarch64-termux.pkg
|
|
2
|
+
d1b39e3c8c2c6ca4234a0f4a19f20fb1eb63faf6ce0bde3447b6dcbff08a18b3 codex-tools-linux-x86_64.pkg
|
|
3
|
+
d52ae0ccd97060dce260cb542ac820b7eb0bd664c82820c126c2a7f032c9b383 codex-tools-macos-aarch64.pkg
|
|
Binary file
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"plugin_id": "codex-tools",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"visibility": "plugin-base",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"admin_only": true,
|
|
7
|
+
"package_type": "mcp_binary",
|
|
8
|
+
"platforms": [
|
|
9
|
+
{
|
|
10
|
+
"id": "android-aarch64-termux",
|
|
11
|
+
"file": "codex-tools-android-aarch64-termux.pkg",
|
|
12
|
+
"sha256": "b3daf8cfe3531fdf675ddf4578a65c3f5186ad9e490d02ea2a9b90d1653b4920"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "linux-x86_64",
|
|
16
|
+
"file": "codex-tools-linux-x86_64.pkg",
|
|
17
|
+
"sha256": "d1b39e3c8c2c6ca4234a0f4a19f20fb1eb63faf6ce0bde3447b6dcbff08a18b3"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "macos-aarch64",
|
|
21
|
+
"file": "codex-tools-macos-aarch64.pkg",
|
|
22
|
+
"sha256": "d52ae0ccd97060dce260cb542ac820b7eb0bd664c82820c126c2a7f032c9b383"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"install": {
|
|
26
|
+
"command": "masix plugin install-file --file <path-to-pkg> --plugin codex-tools --version 0.1.3 --package-type mcp_binary --admin-only"
|
|
27
|
+
},
|
|
28
|
+
"notes": [
|
|
29
|
+
"Admin-only module. Non-admin users cannot call codex tools.",
|
|
30
|
+
"Package usable without plugin server.",
|
|
31
|
+
"Requires codex-backend enabled for full functionality."
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# codex-tools Package Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.3 - 2026-03-05
|
|
4
|
+
|
|
5
|
+
- Refreshed package artifacts for android-aarch64-termux, linux-x86_64, macos-aarch64.
|
|
6
|
+
- Added `packages/plugin-base/codex-tools/0.1.3/` with updated `manifest.json` and `SHA256SUMS`.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## 0.1.2 - 2026-03-02
|
|
10
|
+
|
|
11
|
+
- Added MIT package publication layout under `packages/plugin-base/codex-tools/0.1.2`.
|
|
12
|
+
- Included platform artifacts:
|
|
13
|
+
- `codex-tools-android-aarch64-termux.pkg`
|
|
14
|
+
- `codex-tools-linux-x86_64.pkg`
|
|
15
|
+
- `codex-tools-macos-aarch64.pkg`
|
|
16
|
+
- Added `manifest.json` with platform metadata and checksums.
|
|
17
|
+
- Added `SHA256SUMS` for manual integrity verification.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# codex-tools (`plugin-base`)
|
|
2
|
+
|
|
3
|
+
`codex-tools` exposes MCP tools for coding workflows and patch operations.
|
|
4
|
+
|
|
5
|
+
## What It Is
|
|
6
|
+
|
|
7
|
+
- Package type: `mcp_binary`
|
|
8
|
+
- Visibility: `plugin-base`
|
|
9
|
+
- Admin-only: `true`
|
|
10
|
+
- Distribution: local `.pkg` install supported (`install-file`)
|
|
11
|
+
|
|
12
|
+
## Install Example
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
masix plugin install-file \
|
|
16
|
+
--file packages/plugin-base/codex-tools/0.1.3/codex-tools-linux-x86_64.pkg \
|
|
17
|
+
--plugin codex-tools \
|
|
18
|
+
--version 0.1.3 \
|
|
19
|
+
--package-type mcp_binary \
|
|
20
|
+
--admin-only
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then enable and restart:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
masix plugin enable codex-tools
|
|
27
|
+
masix restart
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Notes
|
|
31
|
+
|
|
32
|
+
- For full coding flow, install `codex-backend` and `codex-tools` together.
|
|
33
|
+
- Tools are filtered by admin policy at runtime.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "masix-plugin-codex-tools"
|
|
3
|
+
version = "0.1.3"
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
license.workspace = true
|
|
6
|
+
description = "MCP binary plugin exposing codex-backend tools for MasiX runtime"
|
|
7
|
+
|
|
8
|
+
[[bin]]
|
|
9
|
+
name = "masix-plugin-codex-tools"
|
|
10
|
+
path = "src/main.rs"
|
|
11
|
+
|
|
12
|
+
[dependencies]
|
|
13
|
+
anyhow.workspace = true
|
|
14
|
+
clap.workspace = true
|
|
15
|
+
masix-plugin-codex-backend = { path = "../codex-backend" }
|
|
16
|
+
serde.workspace = true
|
|
17
|
+
serde_json.workspace = true
|
|
18
|
+
tokio.workspace = true
|
|
19
|
+
tracing.workspace = true
|
|
20
|
+
tracing-subscriber.workspace = true
|
|
21
|
+
|
|
22
|
+
[dev-dependencies]
|
|
23
|
+
# none yet
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "codex-tools",
|
|
3
|
+
"name": "MasiX Codex Tools",
|
|
4
|
+
"version": "0.1.3",
|
|
5
|
+
"visibility": "plugin-base",
|
|
6
|
+
"package_type": "mcp_binary",
|
|
7
|
+
"entrypoint": "masix-plugin-codex-tools",
|
|
8
|
+
"admin_only": true,
|
|
9
|
+
"permissions": {
|
|
10
|
+
"required": [
|
|
11
|
+
"admin"
|
|
12
|
+
],
|
|
13
|
+
"description": "Codex tools require admin privileges for file operations and API access"
|
|
14
|
+
},
|
|
15
|
+
"platforms_supported": [
|
|
16
|
+
"linux-x86_64",
|
|
17
|
+
"android-aarch64-termux",
|
|
18
|
+
"macos-aarch64"
|
|
19
|
+
],
|
|
20
|
+
"platforms_tested": [
|
|
21
|
+
"linux-x86_64",
|
|
22
|
+
"android-aarch64-termux",
|
|
23
|
+
"macos-aarch64"
|
|
24
|
+
],
|
|
25
|
+
"platforms_planned": [
|
|
26
|
+
"linux-aarch64"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"codex-backend": ">=0.1.0"
|
|
30
|
+
},
|
|
31
|
+
"config": {
|
|
32
|
+
"properties": {
|
|
33
|
+
"provider": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"enum": [
|
|
36
|
+
"openai",
|
|
37
|
+
"anthropic",
|
|
38
|
+
"openai-compatible"
|
|
39
|
+
],
|
|
40
|
+
"default": "openai"
|
|
41
|
+
},
|
|
42
|
+
"api_key_env": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"default": "CODEX_API_KEY",
|
|
45
|
+
"description": "Environment variable name for API key"
|
|
46
|
+
},
|
|
47
|
+
"base_url": {
|
|
48
|
+
"type": "string"
|
|
49
|
+
},
|
|
50
|
+
"model": {
|
|
51
|
+
"type": "string"
|
|
52
|
+
},
|
|
53
|
+
"timeout_secs": {
|
|
54
|
+
"type": "integer",
|
|
55
|
+
"default": 300,
|
|
56
|
+
"minimum": 10,
|
|
57
|
+
"maximum": 3600
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"tools": [
|
|
62
|
+
{
|
|
63
|
+
"name": "codex_run",
|
|
64
|
+
"description": "Execute a coding task via HTTP backend",
|
|
65
|
+
"requires_admin": true
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "codex_dry_run",
|
|
69
|
+
"description": "Preview execution settings without mutation",
|
|
70
|
+
"requires_admin": false
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "codex_status",
|
|
74
|
+
"description": "Get module version and health info",
|
|
75
|
+
"requires_admin": false
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "codex_capabilities",
|
|
79
|
+
"description": "Get machine-readable metadata for AI workers",
|
|
80
|
+
"requires_admin": false
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "codex_exec_command",
|
|
84
|
+
"description": "Execute a command in a bounded, secure environment",
|
|
85
|
+
"requires_admin": true
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "codex_patch_preview",
|
|
89
|
+
"description": "Preview a diff patch without applying it",
|
|
90
|
+
"requires_admin": true
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "codex_apply_patch",
|
|
94
|
+
"description": "Apply a diff patch with automatic backup",
|
|
95
|
+
"requires_admin": true
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "codex_rollback_patch",
|
|
99
|
+
"description": "Rollback a previously applied patch using backup ID",
|
|
100
|
+
"requires_admin": true
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
"min_masix_version": "0.3.0",
|
|
104
|
+
"max_masix_version": "0.4.1",
|
|
105
|
+
"capabilities": [
|
|
106
|
+
"http_client",
|
|
107
|
+
"file_read",
|
|
108
|
+
"file_write",
|
|
109
|
+
"command_exec"
|
|
110
|
+
],
|
|
111
|
+
"constraints": {
|
|
112
|
+
"max_concurrent_tasks": 1,
|
|
113
|
+
"requires_isolation": false,
|
|
114
|
+
"timeout_enforcement": "strict"
|
|
115
|
+
},
|
|
116
|
+
"tool_access": {
|
|
117
|
+
"default_required_role": "admin",
|
|
118
|
+
"per_tool_required_role": {
|
|
119
|
+
"codex_dry_run": "user",
|
|
120
|
+
"codex_status": "user",
|
|
121
|
+
"codex_capabilities": "user"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|