auxilo-mcp 0.9.21 → 0.9.23
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/bin/auxilo-cli.js +2 -0
- package/lib/installer.js +25 -0
- package/mcp-server.js +1 -1
- package/package.json +1 -1
- package/scripts/extract-local.js +1 -0
- package/scripts/providers/claude-code.js +55 -10
- package/scripts/providers/codex-cli.js +184 -23
- package/scripts/providers/provider.interface.js +1 -0
package/bin/auxilo-cli.js
CHANGED
|
@@ -299,6 +299,8 @@ async function cmdSetup(flags) {
|
|
|
299
299
|
console.log('');
|
|
300
300
|
try {
|
|
301
301
|
const { binRoot } = installer.installRunner(HOME);
|
|
302
|
+
const claudeBin = installer.recordClaudeBin(HOME, process.env.PATH);
|
|
303
|
+
if (claudeBin !== null) console.log(` ✓ Claude Code CLI recorded: ${claudeBin}`);
|
|
302
304
|
console.log(` ✓ Extraction runner installed to ${binRoot}`);
|
|
303
305
|
} catch (err) {
|
|
304
306
|
console.error(` ✗ Runner install failed: ${err.message}`);
|
package/lib/installer.js
CHANGED
|
@@ -1061,6 +1061,29 @@ function writeRunnerConfig(homeDir, patch) {
|
|
|
1061
1061
|
return merged;
|
|
1062
1062
|
}
|
|
1063
1063
|
|
|
1064
|
+
/** Find the first regular executable in PATH, preserving its symlink path. */
|
|
1065
|
+
function findExecutableOnPath(name, envPath, fsImpl = fs) {
|
|
1066
|
+
if (typeof envPath !== 'string' || !envPath) return null;
|
|
1067
|
+
for (const dir of envPath.split(path.delimiter)) {
|
|
1068
|
+
if (!dir) continue;
|
|
1069
|
+
const candidate = path.resolve(dir, name);
|
|
1070
|
+
try {
|
|
1071
|
+
if (!fsImpl.statSync(candidate).isFile()) continue;
|
|
1072
|
+
fsImpl.accessSync(candidate, fs.constants.X_OK);
|
|
1073
|
+
return candidate;
|
|
1074
|
+
} catch (_) { /* missing, inaccessible or non-executable — try the next entry */ }
|
|
1075
|
+
}
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/** Setup only: record the user's CLI and confirm the best-effort write persisted. */
|
|
1080
|
+
function recordClaudeBin(homeDir, envPath, fsImpl = fs) {
|
|
1081
|
+
const bin = findExecutableOnPath('claude', envPath, fsImpl);
|
|
1082
|
+
if (bin === null) return null;
|
|
1083
|
+
writeRunnerConfig(homeDir, { claude_bin: bin });
|
|
1084
|
+
return readRunnerConfig(homeDir).claude_bin === bin ? bin : null;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1064
1087
|
// ─── Device-code auth (spec §LW-12 step 2; server.js /auth/device) ──────────
|
|
1065
1088
|
|
|
1066
1089
|
/**
|
|
@@ -2411,6 +2434,8 @@ module.exports = {
|
|
|
2411
2434
|
runnerConfigPath,
|
|
2412
2435
|
readRunnerConfig,
|
|
2413
2436
|
writeRunnerConfig,
|
|
2437
|
+
findExecutableOnPath,
|
|
2438
|
+
recordClaudeBin,
|
|
2414
2439
|
writeEnvFile,
|
|
2415
2440
|
deviceLogin,
|
|
2416
2441
|
binRootFor,
|
package/mcp-server.js
CHANGED
|
@@ -198,7 +198,7 @@ async function postBulkChunks(headers, decisions) {
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
const server = new Server(
|
|
201
|
-
{ name: 'auxilo', version: '0.9.
|
|
201
|
+
{ name: 'auxilo', version: '0.9.23' },
|
|
202
202
|
{
|
|
203
203
|
capabilities: { tools: {} },
|
|
204
204
|
instructions: `You are connected to Auxilo, a knowledge marketplace where AI agents buy and sell operational learnings.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auxilo-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.23",
|
|
4
4
|
"mcpName": "io.github.silent-architects/auxilo",
|
|
5
5
|
"description": "MCP server for Auxilo. Your agent stops solving the same problem twice: auto-extracted learnings, free self-unlocks, and earnings when other agents unlock yours.",
|
|
6
6
|
"main": "mcp-server.js",
|
package/scripts/extract-local.js
CHANGED
|
@@ -639,6 +639,7 @@ const PRE_SPAWN_SKIP_REASON_CODES = new Set([
|
|
|
639
639
|
'cli-not-installed',
|
|
640
640
|
'cli-billing-helper-configured',
|
|
641
641
|
'cli-settings-isolation-unsupported',
|
|
642
|
+
'isolation-precondition',
|
|
642
643
|
'provider-not-configured',
|
|
643
644
|
'providers-file-mode-unsafe',
|
|
644
645
|
'provider-not-installed',
|
|
@@ -22,23 +22,56 @@ const fs = require('fs');
|
|
|
22
22
|
const path = require('path');
|
|
23
23
|
const os = require('os');
|
|
24
24
|
|
|
25
|
-
/**
|
|
25
|
+
/** Leading major.minor.patch only; unreadable versions retain fallback behavior. */
|
|
26
|
+
function versionParts(version) {
|
|
27
|
+
const match = typeof version === 'string' && /^(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
28
|
+
return match ? match.slice(1).map(Number) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function compareVersions(left, right) {
|
|
32
|
+
for (let i = 0; i < 3; i += 1) {
|
|
33
|
+
if (left[i] !== right[i]) return left[i] > right[i] ? 1 : -1;
|
|
34
|
+
}
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Resolve the newest readable CLI — hook/launchd env may have a minimal PATH. */
|
|
26
39
|
function resolveClaudeBin(opts = {}) {
|
|
27
40
|
const homeDir = typeof opts.homeDir === 'string' ? opts.homeDir : os.homedir();
|
|
28
41
|
const existsSync = typeof opts.existsSync === 'function' ? opts.existsSync : fs.existsSync;
|
|
42
|
+
const readFileSyncImpl = typeof opts.readFileSyncImpl === 'function' ? opts.readFileSyncImpl : fs.readFileSync;
|
|
29
43
|
const candidates = [
|
|
30
44
|
path.join(homeDir, '.claude', 'local', 'claude'),
|
|
31
45
|
'/usr/local/bin/claude',
|
|
32
46
|
'/opt/homebrew/bin/claude',
|
|
33
47
|
path.join(homeDir, '.local', 'bin', 'claude'),
|
|
48
|
+
path.join(homeDir, '.npm-global', 'bin', 'claude'),
|
|
34
49
|
];
|
|
50
|
+
// Read directly: the sweeper install does not include lib/installer.js.
|
|
51
|
+
try {
|
|
52
|
+
const config = JSON.parse(readFileSyncImpl(path.join(homeDir, '.auxilo', 'runner-config.json'), 'utf8'));
|
|
53
|
+
const recorded = config && config.claude_bin;
|
|
54
|
+
if (typeof recorded === 'string' && path.isAbsolute(recorded) && path.basename(recorded) === 'claude') {
|
|
55
|
+
candidates.unshift(recorded);
|
|
56
|
+
}
|
|
57
|
+
} catch (_) { /* missing/malformed config means no recorded candidate */ }
|
|
58
|
+
|
|
59
|
+
let firstExisting;
|
|
60
|
+
let newest;
|
|
61
|
+
let newestVersion;
|
|
35
62
|
for (const c of candidates) {
|
|
36
63
|
try {
|
|
37
|
-
if (existsSync(c))
|
|
64
|
+
if (!existsSync(c)) continue;
|
|
65
|
+
if (!firstExisting) firstExisting = c;
|
|
66
|
+
const version = versionParts(getClaudeCliVersion(c, opts));
|
|
67
|
+
if (version && (!newestVersion || compareVersions(version, newestVersion) > 0)) {
|
|
68
|
+
newest = c;
|
|
69
|
+
newestVersion = version;
|
|
70
|
+
}
|
|
38
71
|
} catch (_) { /* ignore */ }
|
|
39
72
|
}
|
|
40
73
|
// Absolute launchd fallbacks are absent; let PATH resolve the final option.
|
|
41
|
-
return 'claude';
|
|
74
|
+
return newest || firstExisting || 'claude';
|
|
42
75
|
}
|
|
43
76
|
|
|
44
77
|
// ─── Child settings/hooks isolation (EXTRACTION-CHILD-HOOKS, PUNCH-LIST P1,
|
|
@@ -105,27 +138,35 @@ function _resetSettingSourcesCacheForTests() {
|
|
|
105
138
|
cachedSettingSourcesUnsupported = undefined;
|
|
106
139
|
}
|
|
107
140
|
|
|
108
|
-
// ─── CLI version, for
|
|
141
|
+
// ─── CLI version, for selection, auth gating and provenance (no spawn) ──────
|
|
109
142
|
//
|
|
110
143
|
// Resolves the installed package's own package.json version by following the
|
|
111
144
|
// resolved binary's real path (e.g. `/usr/local/bin/claude` -> `.../
|
|
112
145
|
// node_modules/@anthropic-ai/claude-code/cli.js`) and reading the sibling
|
|
113
|
-
// package.json
|
|
146
|
+
// package.json, or the named parent package for the native bin/claude.exe
|
|
147
|
+
// layout — filesystem-only, so it never adds a spawn to the extraction
|
|
114
148
|
// path (verified live: realpath + package.json read, no `claude --version`
|
|
115
149
|
// call). Best-effort: any failure (bare `claude` unresolved via PATH, an
|
|
116
|
-
// install layout that doesn't carry
|
|
150
|
+
// install layout that doesn't carry either package.json, a fixture path in
|
|
117
151
|
// tests) yields null, never throws.
|
|
118
152
|
function getClaudeCliVersion(bin, opts = {}) {
|
|
119
153
|
const realpathSyncImpl = typeof opts.realpathSyncImpl === 'function' ? opts.realpathSyncImpl : fs.realpathSync;
|
|
120
154
|
const readFileSyncImpl = typeof opts.readFileSyncImpl === 'function' ? opts.readFileSyncImpl : fs.readFileSync;
|
|
121
155
|
try {
|
|
122
156
|
const real = realpathSyncImpl(bin);
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
157
|
+
const dir = path.dirname(real);
|
|
158
|
+
for (const [pkgDir, requireName] of [[dir, false], [path.dirname(dir), true]]) {
|
|
159
|
+
try {
|
|
160
|
+
const pkg = JSON.parse(readFileSyncImpl(path.join(pkgDir, 'package.json'), 'utf8'));
|
|
161
|
+
if (pkg && typeof pkg.version === 'string' && (!requireName || pkg.name === '@anthropic-ai/claude-code')) {
|
|
162
|
+
return pkg.version;
|
|
163
|
+
}
|
|
164
|
+
} catch (_) { /* unreadable sibling may still have a valid parent */ }
|
|
165
|
+
}
|
|
126
166
|
} catch {
|
|
127
|
-
|
|
167
|
+
/* unresolved binary */
|
|
128
168
|
}
|
|
169
|
+
return null;
|
|
129
170
|
}
|
|
130
171
|
|
|
131
172
|
// ─── Env scrub (EXTRACT-TOOLS-LOCK, PUNCH-LIST) ────────────────────────────
|
|
@@ -345,6 +386,10 @@ function detectBillingHelperConfigured(opts = {}) {
|
|
|
345
386
|
function checkAuthStatus(opts = {}) {
|
|
346
387
|
const spawnSyncImpl = typeof opts.spawnSyncImpl === 'function' ? opts.spawnSyncImpl : spawnSync;
|
|
347
388
|
const bin = typeof opts.claudeBin === 'string' ? opts.claudeBin : resolveClaudeBin(opts);
|
|
389
|
+
const version = versionParts(getClaudeCliVersion(bin, opts));
|
|
390
|
+
// Older CLIs treat `auth status` as a model prompt. Unknown versions retain
|
|
391
|
+
// the existing probe; only a known-old build can safely skip it here.
|
|
392
|
+
if (version && compareVersions(version, [2, 1, 41]) < 0) return 'unknown';
|
|
348
393
|
let res;
|
|
349
394
|
try {
|
|
350
395
|
res = spawnSyncImpl(bin, ['auth', 'status'], {
|
|
@@ -9,13 +9,18 @@
|
|
|
9
9
|
* builder's own `codex` login (or their own OPENAI_API_KEY, see detect()
|
|
10
10
|
* below), scrubbed of every var that could redirect billing elsewhere.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Isolation flags and config keys were source-verified against codex-cli
|
|
13
|
+
* 0.144.5 — see BUILD-SPEC-CODEX-ROUTE-ISOLATION §2–§3:
|
|
14
14
|
* -s, --sandbox <read-only|workspace-write|danger-full-access>
|
|
15
15
|
* --skip-git-repo-check (codex refuses to run outside a git repo otherwise)
|
|
16
16
|
* --ephemeral (no session file left behind)
|
|
17
17
|
* --ignore-user-config (don't load ~/.codex/config.toml — auth still
|
|
18
18
|
* comes from CODEX_HOME/auth.json regardless)
|
|
19
|
+
* --ignore-rules (don't load project instruction files)
|
|
20
|
+
* --strict-config (reject unknown config keys before model use)
|
|
21
|
+
* -C <DIR> (run from a fresh private empty directory)
|
|
22
|
+
* --json (emit the lifecycle stream audited below)
|
|
23
|
+
* --disable / -c (remove optional tools/context injection)
|
|
19
24
|
* --output-schema <FILE> (a JSON-Schema HINT, not a hard parser — see
|
|
20
25
|
* schemas/*.schema.json for the shapes)
|
|
21
26
|
* -o, --output-last-message <FILE> (where the final answer lands)
|
|
@@ -33,6 +38,45 @@ const { SCRUBBED_CLIENT_ENV_VARS } = require('./claude-code.js');
|
|
|
33
38
|
const EXTRACTION_SCHEMA_PATH = path.join(__dirname, 'schemas', 'extraction-envelope.schema.json');
|
|
34
39
|
const JUDGE_SCHEMA_PATH = path.join(__dirname, 'schemas', 'judge-decisions.schema.json');
|
|
35
40
|
|
|
41
|
+
const ISOLATION_DISABLED_FEATURES = Object.freeze([
|
|
42
|
+
'shell_tool',
|
|
43
|
+
'unified_exec',
|
|
44
|
+
'shell_snapshot',
|
|
45
|
+
'hooks',
|
|
46
|
+
'multi_agent',
|
|
47
|
+
'apps',
|
|
48
|
+
'plugins',
|
|
49
|
+
'remote_plugin',
|
|
50
|
+
'tool_suggest',
|
|
51
|
+
'image_generation',
|
|
52
|
+
'goals',
|
|
53
|
+
'memories',
|
|
54
|
+
'skill_mcp_dependency_install',
|
|
55
|
+
'guardian_approval',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
const ISOLATION_CONFIG_OVERRIDES = Object.freeze([
|
|
59
|
+
'web_search="disabled"',
|
|
60
|
+
'notify=[]',
|
|
61
|
+
'tools.experimental_request_user_input.enabled=false',
|
|
62
|
+
'project_doc_max_bytes=0',
|
|
63
|
+
'skills.include_instructions=false',
|
|
64
|
+
'orchestrator.skills.enabled=false',
|
|
65
|
+
'include_environment_context=false',
|
|
66
|
+
'include_apps_instructions=false',
|
|
67
|
+
'include_permissions_instructions=false',
|
|
68
|
+
'include_collaboration_mode_instructions=false',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const DEFAULT_SYSTEM_CONFIG_PATHS = Object.freeze([
|
|
72
|
+
'/etc/codex/config.toml',
|
|
73
|
+
'/etc/codex/requirements.toml',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
const ALLOWED_ITEM_TYPES = new Set(['agent_message', 'reasoning', 'todo_list', 'error']);
|
|
77
|
+
const AUDITED_ITEM_EVENTS = new Set(['item.started', 'item.updated', 'item.completed']);
|
|
78
|
+
const AUTH_FAILURE_RE = /not authenticated|not logged in|codex login/i;
|
|
79
|
+
|
|
36
80
|
/** Resolve the `codex` binary — hook/launchd env may have a minimal PATH. */
|
|
37
81
|
function resolveCodexBin(opts = {}) {
|
|
38
82
|
const homeDir = typeof opts.homeDir === 'string' ? opts.homeDir : os.homedir();
|
|
@@ -66,9 +110,64 @@ function codexChildEnv() {
|
|
|
66
110
|
const childEnv = { ...process.env, AUXILO_EXTRACTING: '1' };
|
|
67
111
|
for (const key of SCRUBBED_CLIENT_ENV_VARS) delete childEnv[key];
|
|
68
112
|
delete childEnv.OPENAI_API_KEY;
|
|
113
|
+
for (const key of Object.keys(childEnv)) {
|
|
114
|
+
if (key.startsWith('CODEX_EXEC_SERVER_')) delete childEnv[key];
|
|
115
|
+
}
|
|
116
|
+
childEnv.CODEX_EXEC_SERVER_URL = 'none';
|
|
69
117
|
return childEnv;
|
|
70
118
|
}
|
|
71
119
|
|
|
120
|
+
function neutralizeSkillMentions(text) {
|
|
121
|
+
return String(text).replace(/\$(?=[A-Za-z0-9_:-])/g, '$\u200B');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function stripNeutralizationMarker(text) {
|
|
125
|
+
return String(text).replace(/\u200B/g, '');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseJsonlEvents(stdout) {
|
|
129
|
+
const events = [];
|
|
130
|
+
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
131
|
+
try {
|
|
132
|
+
const parsed = JSON.parse(line);
|
|
133
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.type === 'string') {
|
|
134
|
+
events.push(parsed);
|
|
135
|
+
}
|
|
136
|
+
} catch { /* non-JSON stdout lines are ignored */ }
|
|
137
|
+
}
|
|
138
|
+
return events;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function eventAuthMessages(events) {
|
|
142
|
+
const messages = [];
|
|
143
|
+
for (const event of events) {
|
|
144
|
+
if (event.type === 'error' || event.type === 'turn.failed') {
|
|
145
|
+
if (typeof event.message === 'string') messages.push(event.message);
|
|
146
|
+
if (event.error && typeof event.error.message === 'string') messages.push(event.error.message);
|
|
147
|
+
}
|
|
148
|
+
if (AUDITED_ITEM_EVENTS.has(event.type)
|
|
149
|
+
&& event.item
|
|
150
|
+
&& event.item.type === 'error'
|
|
151
|
+
&& typeof event.item.message === 'string') {
|
|
152
|
+
messages.push(event.item.message);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return messages;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function lastCompletedAgentMessage(events) {
|
|
159
|
+
let text = '';
|
|
160
|
+
for (const event of events) {
|
|
161
|
+
if (event.type === 'item.completed'
|
|
162
|
+
&& event.item
|
|
163
|
+
&& event.item.type === 'agent_message'
|
|
164
|
+
&& typeof event.item.text === 'string') {
|
|
165
|
+
text = event.item.text;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return text;
|
|
169
|
+
}
|
|
170
|
+
|
|
72
171
|
/**
|
|
73
172
|
* Read ~/.codex/auth.json and return its `auth_mode` string, or null when the
|
|
74
173
|
* file is missing, unreadable, malformed, or auth_mode is absent/falsy. Never
|
|
@@ -114,10 +213,9 @@ function detect(opts = {}) {
|
|
|
114
213
|
|
|
115
214
|
// ─── codex --version capture (extraction_model.version) ───────────────────
|
|
116
215
|
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
// build ran this extraction". Captured once per process and cached: every
|
|
216
|
+
// This route preserves its existing null per-call model identifier; the CLI
|
|
217
|
+
// build version is the honest proxy for "which codex build ran this
|
|
218
|
+
// extraction". Captured once per process and cached: every
|
|
121
219
|
// runModel() call after the first reuses the cached value, so a session that
|
|
122
220
|
// calls runModel() twice (extract, then judge) only pays for one version
|
|
123
221
|
// probe. `undefined` = not yet probed; `null` = probed, could not determine.
|
|
@@ -175,17 +273,14 @@ function classifySpawnError(error, bin) {
|
|
|
175
273
|
}
|
|
176
274
|
|
|
177
275
|
/**
|
|
178
|
-
* Shared invocation for both modes: builds argv, spawns,
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* crash/timeout/schema-rejection path, so a defensive stdout fallback covers
|
|
183
|
-
* the cases where no file ever landed; text as documented is the file's
|
|
184
|
-
* content and stdout is treated as the exception path, never the default).
|
|
276
|
+
* Shared invocation for both modes: builds the isolated argv, spawns, audits
|
|
277
|
+
* the JSONL lifecycle stream, and reads the answer back from the `-o` file.
|
|
278
|
+
* If that file cannot be read, only the last completed agent-message event is
|
|
279
|
+
* eligible as a fallback; raw stdout is never returned.
|
|
185
280
|
*
|
|
186
281
|
* The `-o` file's private-dir creation, 0600 chmod, and cleanup (GOV-3
|
|
187
282
|
* should-fix item 11) are handled by an outer try/finally so EVERY exit
|
|
188
|
-
* path —
|
|
283
|
+
* path after directory creation — every spawn-error/timeout classification,
|
|
189
284
|
* non-zero exit, empty output, and the normal success path — cleans up the
|
|
190
285
|
* same way. `cleanupDir` is null (nothing to remove) when the caller
|
|
191
286
|
* supplied its own `opts.outputPath`.
|
|
@@ -196,6 +291,9 @@ function invoke(opts, mode) {
|
|
|
196
291
|
const unlinkSyncImpl = typeof opts.unlinkSyncImpl === 'function' ? opts.unlinkSyncImpl : fs.unlinkSync;
|
|
197
292
|
const rmdirSyncImpl = typeof opts.rmdirSyncImpl === 'function' ? opts.rmdirSyncImpl : fs.rmdirSync;
|
|
198
293
|
const chmodSyncImpl = typeof opts.chmodSyncImpl === 'function' ? opts.chmodSyncImpl : fs.chmodSync;
|
|
294
|
+
const mkdtempSyncImpl = typeof opts.mkdtempSyncImpl === 'function' ? opts.mkdtempSyncImpl : fs.mkdtempSync;
|
|
295
|
+
const rmSyncImpl = typeof opts.rmSyncImpl === 'function' ? opts.rmSyncImpl : fs.rmSync;
|
|
296
|
+
const existsSync = typeof opts.existsSync === 'function' ? opts.existsSync : fs.existsSync;
|
|
199
297
|
const bin = typeof opts.codexBin === 'string' ? opts.codexBin : resolveCodexBin(opts);
|
|
200
298
|
|
|
201
299
|
const authMode = readAuthMode(opts);
|
|
@@ -210,17 +308,43 @@ function invoke(opts, mode) {
|
|
|
210
308
|
};
|
|
211
309
|
}
|
|
212
310
|
|
|
311
|
+
const systemConfigPaths = Array.isArray(opts.systemConfigPaths)
|
|
312
|
+
? opts.systemConfigPaths
|
|
313
|
+
: DEFAULT_SYSTEM_CONFIG_PATHS;
|
|
314
|
+
for (const systemConfigPath of systemConfigPaths) {
|
|
315
|
+
let exists = false;
|
|
316
|
+
try { exists = existsSync(systemConfigPath); } catch { /* unreadable is treated as absent */ }
|
|
317
|
+
if (exists) {
|
|
318
|
+
return {
|
|
319
|
+
ok: false,
|
|
320
|
+
text: '',
|
|
321
|
+
usage: null,
|
|
322
|
+
reason: `codex system configuration is present at ${systemConfigPath}`,
|
|
323
|
+
reasonCode: 'isolation-precondition',
|
|
324
|
+
authStatus: 'unknown',
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
213
329
|
const { outputPath, cleanupDir } = makeOutputLocation(opts, mode);
|
|
330
|
+
let workDir = null;
|
|
214
331
|
try {
|
|
332
|
+
workDir = mkdtempSyncImpl(path.join(os.tmpdir(), 'auxilo-codex-cwd-'));
|
|
215
333
|
const schemaFile = mode === 'judge' ? JUDGE_SCHEMA_PATH : EXTRACTION_SCHEMA_PATH;
|
|
216
334
|
const prompt = typeof opts.prompt === 'string' ? opts.prompt : '';
|
|
217
|
-
const stdin = prompt + String(opts.input || '');
|
|
335
|
+
const stdin = neutralizeSkillMentions(prompt + String(opts.input || ''));
|
|
218
336
|
const args = [
|
|
219
337
|
'exec',
|
|
220
338
|
'-s', 'read-only',
|
|
221
339
|
'--skip-git-repo-check',
|
|
222
340
|
'--ephemeral',
|
|
223
341
|
'--ignore-user-config',
|
|
342
|
+
'--ignore-rules',
|
|
343
|
+
'--strict-config',
|
|
344
|
+
'-C', workDir,
|
|
345
|
+
'--json',
|
|
346
|
+
...ISOLATION_DISABLED_FEATURES.flatMap((feature) => ['--disable', feature]),
|
|
347
|
+
...ISOLATION_CONFIG_OVERRIDES.flatMap((override) => ['-c', override]),
|
|
224
348
|
'--output-schema', schemaFile,
|
|
225
349
|
'-o', outputPath,
|
|
226
350
|
'-',
|
|
@@ -232,6 +356,7 @@ function invoke(opts, mode) {
|
|
|
232
356
|
input: stdin,
|
|
233
357
|
encoding: 'utf-8',
|
|
234
358
|
env: codexChildEnv(),
|
|
359
|
+
cwd: workDir,
|
|
235
360
|
timeout: opts.timeoutMs || 120000,
|
|
236
361
|
maxBuffer: 20 * 1024 * 1024,
|
|
237
362
|
});
|
|
@@ -253,7 +378,9 @@ function invoke(opts, mode) {
|
|
|
253
378
|
}
|
|
254
379
|
|
|
255
380
|
const stdout = String(res.stdout || '');
|
|
256
|
-
|
|
381
|
+
const stderr = String(res.stderr || '');
|
|
382
|
+
const events = parseJsonlEvents(stdout);
|
|
383
|
+
if (AUTH_FAILURE_RE.test(stderr) || eventAuthMessages(events).some((message) => AUTH_FAILURE_RE.test(message))) {
|
|
257
384
|
return { ok: false, text: '', usage: null, reason: 'codex CLI reported it is not authenticated', reasonCode: 'cli-unauthenticated', authStatus: 'unknown' };
|
|
258
385
|
}
|
|
259
386
|
if (res.status !== 0) {
|
|
@@ -261,12 +388,40 @@ function invoke(opts, mode) {
|
|
|
261
388
|
ok: false,
|
|
262
389
|
text: '',
|
|
263
390
|
usage: null,
|
|
264
|
-
reason: `codex exec exited ${res.status}: ${
|
|
391
|
+
reason: `codex exec exited ${res.status}: ${stderr.slice(0, 160)}`,
|
|
265
392
|
reasonCode: 'model-error',
|
|
266
393
|
authStatus: 'unknown',
|
|
267
394
|
};
|
|
268
395
|
}
|
|
269
396
|
|
|
397
|
+
if (events.length === 0) {
|
|
398
|
+
return {
|
|
399
|
+
ok: false,
|
|
400
|
+
text: '',
|
|
401
|
+
usage: null,
|
|
402
|
+
reason: 'codex exec emitted no parseable lifecycle event',
|
|
403
|
+
reasonCode: 'isolation-unverified',
|
|
404
|
+
authStatus: 'unknown',
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
for (const event of events) {
|
|
409
|
+
if (!AUDITED_ITEM_EVENTS.has(event.type)) continue;
|
|
410
|
+
const itemType = event.item && typeof event.item.type === 'string'
|
|
411
|
+
? event.item.type
|
|
412
|
+
: 'unknown';
|
|
413
|
+
if (!ALLOWED_ITEM_TYPES.has(itemType)) {
|
|
414
|
+
return {
|
|
415
|
+
ok: false,
|
|
416
|
+
text: '',
|
|
417
|
+
usage: null,
|
|
418
|
+
reason: `codex exec emitted disallowed item type: ${itemType}`,
|
|
419
|
+
reasonCode: 'isolation-violation',
|
|
420
|
+
authStatus: 'unknown',
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
270
425
|
// Force 0600 before reading — codex writes this file itself, under its
|
|
271
426
|
// own umask, which may not match. Best-effort: a file that doesn't
|
|
272
427
|
// exist (never written) or can't be chmod'd fails silently here and the
|
|
@@ -278,20 +433,19 @@ function invoke(opts, mode) {
|
|
|
278
433
|
try {
|
|
279
434
|
text = String(readFileSyncImpl(outputPath, 'utf8'));
|
|
280
435
|
} catch {
|
|
281
|
-
|
|
282
|
-
// outside a normal completion — fall back to stdout rather than
|
|
283
|
-
// reporting a false failure when codex exited 0 but the file is absent.
|
|
284
|
-
text = stdout;
|
|
436
|
+
text = lastCompletedAgentMessage(events);
|
|
285
437
|
usedStdoutFallback = true;
|
|
286
438
|
}
|
|
287
439
|
|
|
440
|
+
text = stripNeutralizationMarker(text);
|
|
441
|
+
|
|
288
442
|
if (!text.trim()) {
|
|
289
443
|
return {
|
|
290
444
|
ok: false,
|
|
291
445
|
text: '',
|
|
292
446
|
usage: null,
|
|
293
447
|
reason: usedStdoutFallback
|
|
294
|
-
? 'codex exec produced no output-last-message file and
|
|
448
|
+
? 'codex exec produced no output-last-message file and no completed agent message'
|
|
295
449
|
: 'codex exec produced an empty output-last-message file',
|
|
296
450
|
reasonCode: 'cli-bad-output',
|
|
297
451
|
authStatus: 'unknown',
|
|
@@ -310,7 +464,7 @@ function invoke(opts, mode) {
|
|
|
310
464
|
// until that test (and any external consumer) moves to `.identity`.
|
|
311
465
|
const identity = {
|
|
312
466
|
provider: 'codex-cli',
|
|
313
|
-
model: null, //
|
|
467
|
+
model: null, // the route intentionally preserves its existing identity contract
|
|
314
468
|
version: getCodexVersion(opts),
|
|
315
469
|
vendor: null,
|
|
316
470
|
};
|
|
@@ -333,6 +487,9 @@ function invoke(opts, mode) {
|
|
|
333
487
|
if (cleanupDir) {
|
|
334
488
|
try { rmdirSyncImpl(cleanupDir); } catch { /* best-effort cleanup only */ }
|
|
335
489
|
}
|
|
490
|
+
if (workDir) {
|
|
491
|
+
try { rmSyncImpl(workDir, { recursive: true, force: true }); } catch { /* best-effort cleanup only */ }
|
|
492
|
+
}
|
|
336
493
|
}
|
|
337
494
|
}
|
|
338
495
|
|
|
@@ -349,6 +506,10 @@ module.exports = {
|
|
|
349
506
|
readAuthMode,
|
|
350
507
|
codexChildEnv,
|
|
351
508
|
getCodexVersion,
|
|
509
|
+
neutralizeSkillMentions,
|
|
510
|
+
stripNeutralizationMarker,
|
|
511
|
+
ISOLATION_DISABLED_FEATURES,
|
|
512
|
+
ISOLATION_CONFIG_OVERRIDES,
|
|
352
513
|
EXTRACTION_SCHEMA_PATH,
|
|
353
514
|
JUDGE_SCHEMA_PATH,
|
|
354
515
|
_resetVersionCacheForTests,
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
* are not required to estimate on the caller's behalf.
|
|
62
62
|
* @property {string} [reasonCode] - Machine-matchable failure/skip classifier
|
|
63
63
|
* (e.g. 'cli-unauthenticated', 'cli-billing-helper-configured', 'model-error',
|
|
64
|
+
* 'isolation-precondition', 'isolation-unverified', 'isolation-violation',
|
|
64
65
|
* 'unknown'). Present on both success and failure paths where applicable.
|
|
65
66
|
* @property {string|null} [reason] - Human-readable reason, present when !ok.
|
|
66
67
|
* @property {string} [authStatus] - 'logged-in' | 'logged-out' | 'unknown', when
|