auxilo-mcp 0.9.13 → 0.9.14

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 CHANGED
@@ -167,7 +167,7 @@ const CONSENT_TEXT = `
167
167
  • SCRUBS it locally (sensitivity filter: API keys, tokens, emails, PII
168
168
  are redacted first),
169
169
  • EXTRACTS reusable learnings locally through the first model client you
170
- have installed (Claude Code, then Codex) or, when neither is
170
+ are signed in to (Claude Code, then Codex) or, when neither is
171
171
  available, a provider key you set yourself. For this step your
172
172
  scrubbed transcript goes only to that provider, under your own
173
173
  account with them, and any use is charged to that account, never to
@@ -1402,10 +1402,13 @@ async function cmdProvider(flags) {
1402
1402
  return;
1403
1403
  }
1404
1404
 
1405
- // writeByoConfig throws ONLY on an unresolved home directory (GOV-3 item
1406
- // 13) caught here so that reaches a clean reason + exit(1), never a raw
1407
- // stack (should-fix item 10), matching the fail-closed contract every
1408
- // other providers.json entry point in this file now follows.
1405
+ // writeByoConfig throws on an unresolved home directory (GOV-3 item 13)
1406
+ // OR, since EXTRACTION-LOW-FOLLOWUPS item 4, on an existing providers.json
1407
+ // path that is not a regular file owned by this account (a symlink or a
1408
+ // foreign owner) both caught here so they reach a clean reason +
1409
+ // exit(1), never a raw stack (should-fix item 10), matching the
1410
+ // fail-closed contract every other providers.json entry point in this
1411
+ // file now follows.
1409
1412
  let written;
1410
1413
  try {
1411
1414
  written = byoKeyProvider.writeByoConfig({
@@ -1419,6 +1422,10 @@ async function cmdProvider(flags) {
1419
1422
  console.error(`auxilo provider set could not resolve your home directory (reasonCode: provider-home-unresolved). ${err.message}`);
1420
1423
  process.exit(1);
1421
1424
  }
1425
+ if (err && err.reasonCode === 'provider-state-target-unsafe') {
1426
+ console.error(`auxilo provider set refuses to continue: ${err.message} (reasonCode: provider-state-target-unsafe). Remove or replace it, then try again.`);
1427
+ process.exit(1);
1428
+ }
1422
1429
  throw err;
1423
1430
  }
1424
1431
  console.log(`\n✓ Saved to ${written} (mode 0600). This machine will use your own ${vendor} key for extraction when no earlier provider in the order is available.`);
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.13' },
201
+ { name: 'auxilo', version: '0.9.14' },
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.13",
3
+ "version": "0.9.14",
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",
@@ -401,13 +401,24 @@ function judgeUsage(usage, prompt, completion) {
401
401
  /**
402
402
  * PART C — resolve the extraction_model identity for a runModel result.
403
403
  * Prefers the additive `identity` field a provider's runModel result may
404
- * carry (byo-key.js always sets one: {provider:'byo-key', model, version,
405
- * vendor}). claude-code.js/codex-cli.js shipped (PART A/B) before this stamp
406
- * existed and don't set one — rather than touch those provider modules
407
- * (outside this part's disjoint file scope, AGENTS.md's one-build rule),
408
- * this falls back to the resolved provider id alone (model/version/vendor
409
- * null) so every provider gets SOME stamp, never silently none. Best-effort:
410
- * a resolution failure here must never block extraction itself.
404
+ * carry. Current state (post Gate-A item a): byo-key.js always sets one
405
+ * ({provider:'byo-key', model, version:null, vendor}); codex-cli.js sets one
406
+ * on success ({provider:'codex-cli', model:null, version:<codex --version>,
407
+ * vendor:null} its result also carries the same object under the
408
+ * deprecated `extraction_model` alias, kept for one release only for
409
+ * test/codex-cli-provider.test.js). claude-code.js is the one provider that
410
+ * still sets no `identity` on its result that's the case this function's
411
+ * fallback exists for: it re-resolves via providers.resolveProvider() and
412
+ * stamps {provider: resolved.id, model: null, version: null, vendor: null},
413
+ * so every provider gets SOME stamp, never silently none. That re-resolution
414
+ * walks scripts/providers/index.js's PROVIDER_ORDER (claude-code →
415
+ * codex-cli → byo-key); resolveProvider/runModel there fall through from one
416
+ * provider to the next only on a NON_RETRYABLE_FOR_THIS_PROVIDER reasonCode
417
+ * (unauthenticated, not installed, a billing helper configured, an
418
+ * unconfigured BYO key, or an unsafe providers.json mode) — a provider that
419
+ * merely failed once (a timeout, a model error) is not retried under a
420
+ * different one. Best-effort throughout: a resolution failure here must
421
+ * never block extraction itself.
411
422
  */
412
423
  async function resolveExtractionModelIdentity(runModelResult, opts) {
413
424
  if (runModelResult && runModelResult.identity && typeof runModelResult.identity === 'object') {
@@ -70,6 +70,33 @@ function isHomeUnresolved(target) {
70
70
  return typeof target !== 'string' || !target || !path.isAbsolute(target);
71
71
  }
72
72
 
73
+ /**
74
+ * true iff something already sits at `target` and it is NOT a plain regular
75
+ * file owned by this process's own uid (EXTRACTION-LOW-FOLLOWUPS item 4).
76
+ * Guards the pre-rename chmod below: chmod follows symlinks, so a planted
77
+ * symlink at `target` (pointing at, say, another account's file, or a
78
+ * device node) would previously get its chmod(0600) applied to whatever it
79
+ * points at, not to providers.json itself — same-uid threat model only
80
+ * (matches item 2's TOCTOU acceptance above), but a fail-closed lstat is a
81
+ * one-line guard against it. ENOENT (nothing there yet) is safe — the write
82
+ * below creates it fresh with O_EXCL. Any other stat failure, a non-file
83
+ * (symlink/dir/fifo/device), or a foreign owner all fail CLOSED (refuse),
84
+ * never silently proceed.
85
+ */
86
+ function isUnsafeExistingTarget(target, opts = {}) {
87
+ const lstatSyncImpl = typeof opts.lstatSyncImpl === 'function' ? opts.lstatSyncImpl : fs.lstatSync;
88
+ let stat;
89
+ try {
90
+ stat = lstatSyncImpl(target);
91
+ } catch (err) {
92
+ if (err && err.code === 'ENOENT') return false; // nothing there — nothing to protect
93
+ return true; // cannot verify what's there — fail closed
94
+ }
95
+ if (!stat.isFile()) return true; // symlink, directory, fifo, device, … — never chmod/rename onto it
96
+ if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) return true; // foreign owner
97
+ return false;
98
+ }
99
+
73
100
  /**
74
101
  * ONE writer for every providers.json write (EXTRACT-PER-CLIENT W1 FIX
75
102
  * GOV-3 item 1 + should-fix item 9) — writeByoConfig, clearProvidersFile,
@@ -78,14 +105,19 @@ function isHomeUnresolved(target) {
78
105
  * right instead of three independent (and, before this fix, drifted) copies
79
106
  * of it. Discipline, in order:
80
107
  * 1. mkdir the parent dir 0700 (idempotent).
81
- * 2. If a file already sits at `target`, chmod it 0600 BEFORE the
108
+ * 2. lstat whatever already sits at `target` and refuse (reasonCode
109
+ * 'provider-state-target-unsafe') if it exists and is not a regular
110
+ * file owned by this uid (EXTRACTION-LOW-FOLLOWUPS item 4) — checked
111
+ * BEFORE the chmod below, which would otherwise follow a planted
112
+ * symlink.
113
+ * 3. If a file already sits at `target`, chmod it 0600 BEFORE the
82
114
  * rename lands (belt-and-suspenders — the post-rename chmod below is
83
115
  * the one that actually matters for a stale `.tmp`).
84
- * 3. Unlink any leftover `${target}.tmp` first (a crashed prior run, or a
116
+ * 4. Unlink any leftover `${target}.tmp` first (a crashed prior run, or a
85
117
  * planted symlink), THEN create it fresh with `flag:'wx'` (O_EXCL) —
86
118
  * refuses to silently reuse or follow anything already at that path.
87
- * 4. Atomic rename tmp -> target.
88
- * 5. chmodSync(target, 0o600) AFTER the rename. This is the literal fix
119
+ * 5. Atomic rename tmp -> target.
120
+ * 6. chmodSync(target, 0o600) AFTER the rename. This is the literal fix
89
121
  * for GOV-3 finding 1: writeFileSync's `mode` option only applies on
90
122
  * CREATION, so a stale `.tmp` that survived a crash at 0644 would
91
123
  * rename onto `target` and KEEP 0644, silently falsifying the
@@ -93,7 +125,8 @@ function isHomeUnresolved(target) {
93
125
  * `persistSelected` (scripts/providers/index.js) had this exact gap;
94
126
  * this writer closes it everywhere at once.
95
127
  * Throws (does not swallow) when the home directory could not be resolved
96
- * (isHomeUnresolved) callers decide how to surface that; see
128
+ * (isHomeUnresolved) or the existing target is unsafe (reasonCode
129
+ * 'provider-state-target-unsafe') — callers decide how to surface that; see
97
130
  * writeByoConfig/clearProvidersFile below and cmdProvider in
98
131
  * bin/auxilo-cli.js, which catches this rather than let a raw stack out
99
132
  * (should-fix item 10).
@@ -106,6 +139,11 @@ function writeProvidersStateAtomic(state, opts = {}) {
106
139
  throw err;
107
140
  }
108
141
  fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
142
+ if (isUnsafeExistingTarget(target, opts)) {
143
+ const err = new Error('refusing to write ~/.auxilo/providers.json — an existing entry at that path is not a regular file owned by this account (possible symlink or foreign owner)');
144
+ err.reasonCode = 'provider-state-target-unsafe';
145
+ throw err;
146
+ }
109
147
  if (fs.existsSync(target)) fs.chmodSync(target, 0o600);
110
148
  const tmp = `${target}.tmp`;
111
149
  try { fs.unlinkSync(tmp); } catch { /* nothing there, or already gone — fine either way */ }
@@ -150,6 +188,12 @@ function readByoConfig(opts = {}) {
150
188
  * source discipline; the discipline itself — chmod-then-tmp-write-then-
151
189
  * rename, both 0600 — is copied, not the code).
152
190
  *
191
+ * Throws a tagged error (writeProvidersStateAtomic) on 'provider-home-
192
+ * unresolved' OR, since EXTRACTION-LOW-FOLLOWUPS item 4, on
193
+ * 'provider-state-target-unsafe' (an existing entry at the target path is
194
+ * a symlink or owned by a different uid) — cmdProvider in bin/auxilo-cli.js
195
+ * catches both rather than let a raw stack out.
196
+ *
153
197
  * @param {{provider:string, base_url?:string|null, model:string, api_key:string}} byoConfig
154
198
  */
155
199
  function writeByoConfig(byoConfig, opts = {}) {
@@ -182,7 +226,11 @@ function writeByoConfig(byoConfig, opts = {}) {
182
226
  * rather than guess a location to read/write. A stat/read failure other
183
227
  * than ENOENT (e.g. EACCES) returns 'unreadable' rather than rethrow — this
184
228
  * function's contract really is "never throws" now (should-fix item 10; the
185
- * old rethrow on non-ENOENT contradicted this same docblock).
229
+ * old rethrow on non-ENOENT contradicted this same docblock). The rewrite
230
+ * path below can now also throw (writeProvidersStateAtomic's
231
+ * 'provider-state-target-unsafe', EXTRACTION-LOW-FOLLOWUPS item 4) — caught
232
+ * here too, folded into 'unreadable', so this function's own "never throws"
233
+ * contract holds regardless of what writeProvidersStateAtomic does.
186
234
  *
187
235
  * @returns {'removed-file'|'removed-byo'|'noop'|'unreadable'|'unresolved'}
188
236
  * 'noop' covers both "no file" and "a file with no `byo` key to clear".
@@ -209,10 +257,18 @@ function clearProvidersFile(opts = {}) {
209
257
  }
210
258
  delete state.byo;
211
259
  if (Object.keys(state).length === 0) {
212
- fs.unlinkSync(target);
260
+ try {
261
+ fs.unlinkSync(target);
262
+ } catch {
263
+ return 'unreadable'; // cannot remove it either — never throws (contract)
264
+ }
213
265
  return 'removed-file';
214
266
  }
215
- writeProvidersStateAtomic(state, opts);
267
+ try {
268
+ writeProvidersStateAtomic(state, opts);
269
+ } catch {
270
+ return 'unreadable'; // e.g. provider-state-target-unsafe — never throws (contract)
271
+ }
216
272
  return 'removed-byo';
217
273
  }
218
274
 
@@ -244,6 +300,8 @@ function baseUrlFor(vendor, configured) {
244
300
  * throws.
245
301
  */
246
302
  function isProvidersFileModeUnsafe(opts = {}) {
303
+ // EXTRACTION-LOW-FOLLOWUPS item 2 (TOCTOU, accepted on the record): a
304
+ // window exists between this check and readByoConfig()'s read below; only the same uid could win that race, and that uid already owns the key on disk, so it is accepted rather than replaced with an fd-based check-then-read.
247
305
  const target = statePath(opts);
248
306
  if (isHomeUnresolved(target)) return true; // can't even name the file — fail closed
249
307
  const statSyncImpl = typeof opts.statSyncImpl === 'function' ? opts.statSyncImpl : fs.statSync;
@@ -627,5 +685,7 @@ module.exports = {
627
685
  isHomeUnresolved,
628
686
  isBaseUrlInsecure,
629
687
  writeProvidersStateAtomic,
688
+ // Exported for direct unit coverage (EXTRACTION-LOW-FOLLOWUPS item 4).
689
+ isUnsafeExistingTarget,
630
690
  MAX_RESPONSE_BYTES,
631
691
  };
@@ -154,7 +154,19 @@ function managedSettingsPathForPlatform(opts) {
154
154
  // otherwise names a fixed, real, OS-level location no test should touch).
155
155
  if (typeof opts.managedSettingsPath === 'string') return opts.managedSettingsPath;
156
156
  const platform = typeof opts.platform === 'string' ? opts.platform : process.platform;
157
- return MANAGED_SETTINGS_PATH_BY_PLATFORM[platform] || MANAGED_SETTINGS_PATH_BY_PLATFORM.linux;
157
+ // EXTRACTION-LOW-FOLLOWUPS item 1: a raw `[platform]` index is reachable
158
+ // (only via the test seam opts.platform, per the row) with a prototype key
159
+ // ('constructor', 'toString', '__proto__', …) and would return a truthy
160
+ // Object.prototype value instead of falling through to the Linux default —
161
+ // fails OPEN with a bogus path silently in place of the real managed-
162
+ // settings check. hasOwnProperty scopes the lookup to the object's own
163
+ // enumerable keys, mirroring the guard at scripts/providers/index.js:139,
164
+ // so an unknown or prototype-polluting key falls CLOSED to the Linux path
165
+ // exactly like any other unrecognized platform string does today.
166
+ if (Object.prototype.hasOwnProperty.call(MANAGED_SETTINGS_PATH_BY_PLATFORM, platform)) {
167
+ return MANAGED_SETTINGS_PATH_BY_PLATFORM[platform];
168
+ }
169
+ return MANAGED_SETTINGS_PATH_BY_PLATFORM.linux;
158
170
  }
159
171
 
160
172
  /**
@@ -167,7 +179,7 @@ function managedSettingsPathForPlatform(opts) {
167
179
  * that matches Claude Code's own behavior for those files, which this
168
180
  * managed path does not share.)
169
181
  */
170
- function managedSettingsBlocksOrUnverifiable(filePath, existsSyncImpl, readFileSyncImpl) {
182
+ function managedSettingsBlocksOrUnverifiable(filePath, existsSyncImpl, readFileSyncImpl, log) {
171
183
  let exists;
172
184
  try {
173
185
  exists = existsSyncImpl(filePath);
@@ -176,7 +188,17 @@ function managedSettingsBlocksOrUnverifiable(filePath, existsSyncImpl, readFileS
176
188
  }
177
189
  if (!exists) return false;
178
190
  const parsed = readJsonSafe(filePath, readFileSyncImpl);
179
- if (parsed === null) return true; // present but unreadable/unparseable — fail closed
191
+ if (parsed === null) {
192
+ // EXTRACTION-LOW-FOLLOWUPS item 3: this fail-closed branch silently
193
+ // switches the builder away from claude-code (reasonCode
194
+ // 'cli-billing-helper-configured', same as a real detected helper) with
195
+ // no visible signal that the cause was an UNVERIFIABLE managed-settings
196
+ // file rather than an actual foreign-billing helper. One stderr line
197
+ // naming the reason code — never the file's contents or any key
198
+ // material, both of which stay out of every log call in this module.
199
+ log('[providers] managed-settings.json is present but unreadable/unparseable; failing closed and switching away from claude-code (reasonCode cli-billing-helper-configured)');
200
+ return true; // present but unreadable/unparseable — fail closed
201
+ }
180
202
  return settingsHasBillingHelper(parsed);
181
203
  }
182
204
 
@@ -195,8 +217,9 @@ function detectBillingHelperConfigured(opts = {}) {
195
217
  const existsSyncImpl = typeof opts.existsSyncImpl === 'function' ? opts.existsSyncImpl : fs.existsSync;
196
218
  const homeDir = typeof opts.homeDir === 'string' ? opts.homeDir : os.homedir();
197
219
  const cwd = typeof opts.cwd === 'string' ? opts.cwd : process.cwd();
220
+ const log = typeof opts.log === 'function' ? opts.log : console.error;
198
221
 
199
- if (managedSettingsBlocksOrUnverifiable(managedSettingsPathForPlatform(opts), existsSyncImpl, readFileSyncImpl)) {
222
+ if (managedSettingsBlocksOrUnverifiable(managedSettingsPathForPlatform(opts), existsSyncImpl, readFileSyncImpl, log)) {
200
223
  return true;
201
224
  }
202
225