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 +12 -5
- package/mcp-server.js +1 -1
- package/package.json +1 -1
- package/scripts/extract-local.js +18 -7
- package/scripts/providers/byo-key.js +68 -8
- package/scripts/providers/claude-code.js +27 -4
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
|
-
|
|
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
|
|
1406
|
-
//
|
|
1407
|
-
//
|
|
1408
|
-
//
|
|
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.
|
|
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.
|
|
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",
|
package/scripts/extract-local.js
CHANGED
|
@@ -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
|
|
405
|
-
* vendor})
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
88
|
-
*
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|