auxilo-mcp 0.9.22 → 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/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.22' },
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.22",
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",
@@ -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',
@@ -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
- * Flags verified live against `codex exec --help` (codex-cli 0.144.5) before
13
- * this module was written — see BUILD-SPEC-EXTRACT-PER-CLIENT-W1 §1/§6:
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
- // codex exposes no per-call model identifier (no --json event stream is
118
- // requested here, and -o's last-message file carries prose/schema-shaped
119
- // output only) the CLI build version is the honest proxy for "which codex
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, reads the answer
179
- * back from the `-o` file (falling back to stdout only if that file cannot
180
- * be read see module comment on why: --output-last-message's own docs
181
- * promise a file is written on a normal completion, but say nothing about a
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 — auth-not-configured, every spawn-error/timeout classification,
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
- if (/not authenticated|not logged in|codex login/i.test(stdout) || /not authenticated|not logged in|codex login/i.test(String(res.stderr || ''))) {
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}: ${(stdout || String(res.stderr || '')).slice(0, 160)}`,
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
- // --output-last-message's own docs make no promise about a file existing
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 stdout was empty'
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, // codex exposes no per-call model id without --json (not requested)
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