auxilo-mcp 0.9.13 → 0.9.15
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/lib/extraction-index.js +10 -1
- package/mcp-server.js +1 -1
- package/package.json +3 -2
- package/scripts/extract-local.js +134 -12
- package/scripts/providers/byo-key.js +85 -10
- package/scripts/providers/claude-code.js +182 -18
- package/scripts/providers/index.js +12 -1
- package/scripts/runner.js +5 -0
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/lib/extraction-index.js
CHANGED
|
@@ -14,7 +14,16 @@ const os = require('node:os');
|
|
|
14
14
|
const path = require('node:path');
|
|
15
15
|
const { findNearDuplicate, scoreChannels } = require('./similarity.js');
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// TEST-HOME-ISOLATION incident 2: same AUXILO_HOME-over-os.homedir() fallback
|
|
18
|
+
// as scripts/providers/index.js's PROVIDERS_STATE_PATH and
|
|
19
|
+
// scripts/providers/byo-key.js's DEFAULT_PROVIDERS_STATE_PATH (duplicated,
|
|
20
|
+
// not imported — see those files' docblocks). This makes the default resolve
|
|
21
|
+
// through the same seam isolation already uses elsewhere, so a caller that
|
|
22
|
+
// forgets an explicit indexPath is still isolated by construction whenever
|
|
23
|
+
// AUXILO_HOME is set (scripts/test/run-isolated.js sets both AUXILO_HOME and
|
|
24
|
+
// HOME, so this is defense-in-depth, not the primary guard — every call site
|
|
25
|
+
// should still pass an explicit indexPath; see test/no-real-home-writes.test.js).
|
|
26
|
+
const DEFAULT_INDEX_PATH = path.join(process.env.AUXILO_HOME || os.homedir(), '.auxilo', 'extracted-index.jsonl');
|
|
18
27
|
const HYDRATION_PAGE_SIZE = 500;
|
|
19
28
|
const PROMPT_MEMORY_MAX_TOKENS = 1200;
|
|
20
29
|
const PROMPT_MEMORY_MAX_ROWS = 40;
|
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.15' },
|
|
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.15",
|
|
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",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
34
|
"start": "node mcp-server.js",
|
|
35
|
-
"test": "node
|
|
35
|
+
"test": "node scripts/test/run-isolated.js",
|
|
36
|
+
"test:host": "node scripts/test/run-host.js"
|
|
36
37
|
},
|
|
37
38
|
"keywords": [
|
|
38
39
|
"mcp",
|
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') {
|
|
@@ -447,6 +458,12 @@ async function defaultInvokeModel(transcript, invokeOpts, opts) {
|
|
|
447
458
|
authStatus: result.authStatus,
|
|
448
459
|
extractionModel: await resolveExtractionModelIdentity(result, opts),
|
|
449
460
|
...(result.authDiscrepancy !== undefined && { authDiscrepancy: result.authDiscrepancy }),
|
|
461
|
+
// EXTRACTION-RUN-LOG (0.9.15): additive passthrough for the one-line-per-run
|
|
462
|
+
// provider summary logged at the end of extractLocally() below. Only
|
|
463
|
+
// claude-code's runModel() currently sets these (argv/cliVersion); other
|
|
464
|
+
// providers simply omit them, which the log builder renders as 'n/a'.
|
|
465
|
+
...(result.argv !== undefined && { argv: result.argv }),
|
|
466
|
+
...(result.cliVersion !== undefined && { cliVersion: result.cliVersion }),
|
|
450
467
|
};
|
|
451
468
|
}
|
|
452
469
|
|
|
@@ -465,7 +482,20 @@ function defaultInvokeJudge(opts) {
|
|
|
465
482
|
log: opts.log,
|
|
466
483
|
mode: 'judge',
|
|
467
484
|
});
|
|
468
|
-
return {
|
|
485
|
+
return {
|
|
486
|
+
ok: result.ok,
|
|
487
|
+
out: result.text,
|
|
488
|
+
usage: result.usage,
|
|
489
|
+
reason: result.reason,
|
|
490
|
+
// EXTRACTION-RUN-LOG (0.9.15): threaded through so runAnchoredJudge below
|
|
491
|
+
// can report the judge call's status/argv/CLI version on the run summary
|
|
492
|
+
// log line. Additive — every existing invokeJudge stub in this repo's
|
|
493
|
+
// tests returns a subset of {ok,out,usage,reason} and these three simply
|
|
494
|
+
// come back undefined for them, changing nothing they assert.
|
|
495
|
+
reasonCode: result.reasonCode,
|
|
496
|
+
argv: result.argv,
|
|
497
|
+
cliVersion: result.cliVersion,
|
|
498
|
+
};
|
|
469
499
|
};
|
|
470
500
|
}
|
|
471
501
|
|
|
@@ -505,12 +535,26 @@ function parseJudgeDecisions(raw, candidates, rankings) {
|
|
|
505
535
|
|
|
506
536
|
async function runAnchoredJudge(candidates, indexState, opts = {}) {
|
|
507
537
|
const input = Array.isArray(candidates) ? candidates : [];
|
|
538
|
+
// judgeAttempted/judgeSucceeded (EXTRACTION-RUN-LOG, 0.9.15): additive
|
|
539
|
+
// status fields for the provider-run log line only — every field already on
|
|
540
|
+
// `empty` (kept/dropped/called/prompt_tokens/completion_tokens) keeps its
|
|
541
|
+
// exact pre-existing meaning and every existing caller's assertions on those
|
|
542
|
+
// fields are unaffected. judgeAttempted=true means invokeJudge was actually
|
|
543
|
+
// called (regardless of outcome); judgeSucceeded=true means it returned
|
|
544
|
+
// ok:true AND its output parsed into real decisions (the one path that
|
|
545
|
+
// actually judged anything, as opposed to failing open and keeping
|
|
546
|
+
// everything). This is a finer distinction than `called` — `called` is
|
|
547
|
+
// true even for a malformed-JSON response (an attempted-and-failed case) —
|
|
548
|
+
// so the log builder uses these two instead of `called` to tell
|
|
549
|
+
// "ran"/"failed"/"skipped(no-candidates)" apart accurately.
|
|
508
550
|
const empty = {
|
|
509
551
|
kept: input.slice(),
|
|
510
552
|
dropped: [],
|
|
511
553
|
called: false,
|
|
512
554
|
prompt_tokens: 0,
|
|
513
555
|
completion_tokens: 0,
|
|
556
|
+
judgeAttempted: false,
|
|
557
|
+
judgeSucceeded: false,
|
|
514
558
|
};
|
|
515
559
|
if (!input.length || !indexState || !indexState.usable ||
|
|
516
560
|
!Array.isArray(indexState.rows) || !indexState.rows.length) {
|
|
@@ -527,17 +571,29 @@ async function runAnchoredJudge(candidates, indexState, opts = {}) {
|
|
|
527
571
|
result = await invokeJudge(prompt, { candidates: input, rankings });
|
|
528
572
|
} catch (error) {
|
|
529
573
|
loudLocal(opts, `anchored judge unavailable; keeping all candidates: ${error.message}`);
|
|
530
|
-
return empty;
|
|
574
|
+
return { ...empty, judgeAttempted: true };
|
|
531
575
|
}
|
|
532
576
|
if (!result || !result.ok) {
|
|
533
577
|
loudLocal(opts, `anchored judge unavailable; keeping all candidates: ${result && result.reason ? result.reason : 'unknown error'}`);
|
|
534
|
-
return
|
|
578
|
+
return {
|
|
579
|
+
...empty,
|
|
580
|
+
judgeAttempted: true,
|
|
581
|
+
judgeReasonCode: result && result.reasonCode,
|
|
582
|
+
judgeArgv: result && result.argv,
|
|
583
|
+
judgeCliVersion: result && result.cliVersion,
|
|
584
|
+
};
|
|
535
585
|
}
|
|
536
586
|
const parsed = parseJudgeDecisions(result.out, input, rankings);
|
|
537
587
|
const usage = judgeUsage(result.usage, prompt, result.out);
|
|
588
|
+
const judgeMeta = {
|
|
589
|
+
judgeAttempted: true,
|
|
590
|
+
judgeReasonCode: result.reasonCode,
|
|
591
|
+
judgeArgv: result.argv,
|
|
592
|
+
judgeCliVersion: result.cliVersion,
|
|
593
|
+
};
|
|
538
594
|
if (!parsed.ok) {
|
|
539
595
|
loudLocal(opts, `anchored judge malformed; keeping all candidates: ${parsed.reason}`);
|
|
540
|
-
return { ...empty, called: true, ...usage };
|
|
596
|
+
return { ...empty, called: true, ...usage, ...judgeMeta, judgeSucceeded: false };
|
|
541
597
|
}
|
|
542
598
|
|
|
543
599
|
const kept = [];
|
|
@@ -564,7 +620,7 @@ async function runAnchoredJudge(candidates, indexState, opts = {}) {
|
|
|
564
620
|
kept.push(input[index]);
|
|
565
621
|
}
|
|
566
622
|
}
|
|
567
|
-
return { kept, dropped, called: true, ...usage };
|
|
623
|
+
return { kept, dropped, called: true, ...usage, ...judgeMeta, judgeSucceeded: true };
|
|
568
624
|
}
|
|
569
625
|
|
|
570
626
|
/**
|
|
@@ -619,6 +675,68 @@ function immutableSet(ids) {
|
|
|
619
675
|
|
|
620
676
|
const EXTRACTABLE_SOURCES = immutableSet(EXTRACTABLE_SOURCE_IDS);
|
|
621
677
|
|
|
678
|
+
// ─── Provider-run log line (EXTRACTION-RUN-LOG, PUNCH-LIST P3, 0.9.15) ─────
|
|
679
|
+
//
|
|
680
|
+
// One line per extractLocally() run, written to the runner's log at the point
|
|
681
|
+
// each run completes — never prompt/transcript content. reasonCode/argv/
|
|
682
|
+
// cliVersion are provider-internal diagnostics only (every provider's own
|
|
683
|
+
// `reason` string is secret-free by the same contract runModel() documents in
|
|
684
|
+
// scripts/providers/index.js), so this line carries no learning text.
|
|
685
|
+
const PRE_SPAWN_SKIP_REASON_CODES = new Set([
|
|
686
|
+
'cli-unauthenticated',
|
|
687
|
+
'cli-not-installed',
|
|
688
|
+
'cli-billing-helper-configured',
|
|
689
|
+
'cli-settings-isolation-unsupported',
|
|
690
|
+
'provider-not-configured',
|
|
691
|
+
'providers-file-mode-unsafe',
|
|
692
|
+
'provider-not-installed',
|
|
693
|
+
'no-usable-provider',
|
|
694
|
+
'no-model-provider-available',
|
|
695
|
+
]);
|
|
696
|
+
|
|
697
|
+
function formatArgvForLog(argv) {
|
|
698
|
+
if (!Array.isArray(argv) || !argv.length) return 'n/a';
|
|
699
|
+
return argv.map((arg) => (arg === '' ? "''" : arg)).join(' ');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* `finder` and `judge` report whether each call was actually attempted
|
|
704
|
+
* (spawned) this run, not whether it succeeded — a spawn that ran and then
|
|
705
|
+
* hit a model error still counts as "ran" (it happened; the failure is in
|
|
706
|
+
* `reason`, not in whether isolation applied). `hooks` is `claude-code`-
|
|
707
|
+
* specific: 'isolated' whenever a claude-code spawn this run carried
|
|
708
|
+
* --setting-sources (the only state a spawn can be in per the fail-closed
|
|
709
|
+
* gate in scripts/providers/claude-code.js — it never spawns without the
|
|
710
|
+
* flag), 'unsupported' when the CLI was found not to support the flag at
|
|
711
|
+
* all, 'n/a' for a non-claude-code provider (codex-cli/byo-key isolate by a
|
|
712
|
+
* different mechanism entirely, out of this row's scope).
|
|
713
|
+
*/
|
|
714
|
+
function logProviderRunSummary(opts, runId, modelResult, judged) {
|
|
715
|
+
try {
|
|
716
|
+
const log = typeof opts.log === 'function' ? opts.log : console.error;
|
|
717
|
+
const provider = (modelResult.extractionModel && modelResult.extractionModel.provider) || 'unknown';
|
|
718
|
+
const finderRan = Boolean(modelResult.ok) || !PRE_SPAWN_SKIP_REASON_CODES.has(modelResult.reasonCode);
|
|
719
|
+
const finder = finderRan ? 'ran' : 'skipped';
|
|
720
|
+
let judgeState;
|
|
721
|
+
if (!judged || !judged.judgeAttempted) judgeState = 'skipped(no-candidates)';
|
|
722
|
+
else if (judged.judgeSucceeded) judgeState = 'ran';
|
|
723
|
+
else judgeState = 'failed';
|
|
724
|
+
const argv = (finderRan && modelResult.argv) || (judged && judged.judgeArgv) || null;
|
|
725
|
+
const cliVersion = modelResult.cliVersion || (judged && judged.judgeCliVersion) || null;
|
|
726
|
+
const finderUnsupported = modelResult.reasonCode === 'cli-settings-isolation-unsupported';
|
|
727
|
+
const judgeUnsupported = Boolean(judged && judged.judgeReasonCode === 'cli-settings-isolation-unsupported');
|
|
728
|
+
const hooks = provider === 'claude-code'
|
|
729
|
+
? ((finderUnsupported || judgeUnsupported) ? 'unsupported' : 'isolated')
|
|
730
|
+
: 'n/a';
|
|
731
|
+
log(
|
|
732
|
+
`[providers] run=${runId || 'unknown'} provider=${provider} cli=${cliVersion || '-'} ` +
|
|
733
|
+
`finder=${finder} judge=${judgeState} flags=${formatArgvForLog(argv)} hooks=${hooks}`
|
|
734
|
+
);
|
|
735
|
+
} catch {
|
|
736
|
+
// Logging must never block or fail extraction.
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
622
740
|
async function extractLocally(transcript, sourceType, opts = {}) {
|
|
623
741
|
if (sourceType && !EXTRACTABLE_SOURCES.has(sourceType)) {
|
|
624
742
|
return { learnings: [], skipped: `local extraction not implemented for "${sourceType}" — agent contributes via auxilo_contribute` };
|
|
@@ -660,6 +778,7 @@ async function extractLocally(transcript, sourceType, opts = {}) {
|
|
|
660
778
|
const modelResult = await invokeModel(transcript, { prompt });
|
|
661
779
|
const { ok, out, reason } = modelResult;
|
|
662
780
|
if (!ok) {
|
|
781
|
+
logProviderRunSummary(opts, opts.runId, modelResult, null);
|
|
663
782
|
return {
|
|
664
783
|
learnings: [],
|
|
665
784
|
skipped: reason,
|
|
@@ -726,6 +845,7 @@ async function extractLocally(transcript, sourceType, opts = {}) {
|
|
|
726
845
|
`lexical_filter=${lexicalDropped.length}, anchored_judge=${judged.dropped.length}`
|
|
727
846
|
);
|
|
728
847
|
}
|
|
848
|
+
logProviderRunSummary(opts, opts.runId, modelResult, judged);
|
|
729
849
|
return {
|
|
730
850
|
learnings: judged.kept,
|
|
731
851
|
dedup_dropped: allDropped.length,
|
|
@@ -753,4 +873,6 @@ module.exports = {
|
|
|
753
873
|
extractWithClaudeCode: claudeCodeProvider.extractWithClaudeCode,
|
|
754
874
|
checkClaudeAuthStatus: claudeCodeProvider.checkClaudeAuthStatus,
|
|
755
875
|
resolveClaudeBin: claudeCodeProvider.resolveClaudeBin,
|
|
876
|
+
// EXTRACTION-RUN-LOG (0.9.15) — exported for direct unit coverage.
|
|
877
|
+
formatArgvForLog, logProviderRunSummary, PRE_SPAWN_SKIP_REASON_CODES,
|
|
756
878
|
};
|
|
@@ -37,8 +37,23 @@ const os = require('os');
|
|
|
37
37
|
/** Same literal value as scripts/providers/index.js's PROVIDERS_STATE_PATH —
|
|
38
38
|
* duplicated (not imported) to avoid a circular require (index.js loads this
|
|
39
39
|
* module dynamically via loadOptionalProvider). test/byo-key-provider.test.js
|
|
40
|
-
* pins the two paths equal.
|
|
41
|
-
|
|
40
|
+
* pins the two paths equal.
|
|
41
|
+
*
|
|
42
|
+
* TEST-HOME-ISOLATION: `AUXILO_HOME`, when set, wins over `os.homedir()` —
|
|
43
|
+
* a dedicated override for auxilo's own state directory, distinct from the
|
|
44
|
+
* general-purpose `HOME` every other os.homedir()-based path in this repo
|
|
45
|
+
* reads (settings.json, the VERSION stamp, credentials.json, ...). Per-call
|
|
46
|
+
* `opts.providersStatePath` (every test in this repo passes one) still wins
|
|
47
|
+
* over BOTH — this only narrows what an omitted opts falls back to, closing
|
|
48
|
+
* the gap that let a bare `bin/auxilo-cli.js provider status|clear` call (no
|
|
49
|
+
* opts seam of its own — see cmdProvider in bin/auxilo-cli.js) or any future
|
|
50
|
+
* test that forgets its own override reach the real ~/.auxilo/providers.json
|
|
51
|
+
* (TEST-HOME-ISOLATION incident, 2026-09-06). Evaluated once at module load,
|
|
52
|
+
* same as before — every entry point that cares (scripts/test/run-isolated.js
|
|
53
|
+
* for `npm test`, scripts/check-test-count.sh for the CI gate) sets both
|
|
54
|
+
* AUXILO_HOME and HOME before node starts, so this still resolves correctly
|
|
55
|
+
* even though it's a frozen constant, not a per-call lookup. */
|
|
56
|
+
const DEFAULT_PROVIDERS_STATE_PATH = path.join(process.env.AUXILO_HOME || os.homedir(), '.auxilo', 'providers.json');
|
|
42
57
|
|
|
43
58
|
const VENDOR_DEFAULT_BASE_URL = Object.freeze({
|
|
44
59
|
anthropic: 'https://api.anthropic.com/v1',
|
|
@@ -70,6 +85,33 @@ function isHomeUnresolved(target) {
|
|
|
70
85
|
return typeof target !== 'string' || !target || !path.isAbsolute(target);
|
|
71
86
|
}
|
|
72
87
|
|
|
88
|
+
/**
|
|
89
|
+
* true iff something already sits at `target` and it is NOT a plain regular
|
|
90
|
+
* file owned by this process's own uid (EXTRACTION-LOW-FOLLOWUPS item 4).
|
|
91
|
+
* Guards the pre-rename chmod below: chmod follows symlinks, so a planted
|
|
92
|
+
* symlink at `target` (pointing at, say, another account's file, or a
|
|
93
|
+
* device node) would previously get its chmod(0600) applied to whatever it
|
|
94
|
+
* points at, not to providers.json itself — same-uid threat model only
|
|
95
|
+
* (matches item 2's TOCTOU acceptance above), but a fail-closed lstat is a
|
|
96
|
+
* one-line guard against it. ENOENT (nothing there yet) is safe — the write
|
|
97
|
+
* below creates it fresh with O_EXCL. Any other stat failure, a non-file
|
|
98
|
+
* (symlink/dir/fifo/device), or a foreign owner all fail CLOSED (refuse),
|
|
99
|
+
* never silently proceed.
|
|
100
|
+
*/
|
|
101
|
+
function isUnsafeExistingTarget(target, opts = {}) {
|
|
102
|
+
const lstatSyncImpl = typeof opts.lstatSyncImpl === 'function' ? opts.lstatSyncImpl : fs.lstatSync;
|
|
103
|
+
let stat;
|
|
104
|
+
try {
|
|
105
|
+
stat = lstatSyncImpl(target);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (err && err.code === 'ENOENT') return false; // nothing there — nothing to protect
|
|
108
|
+
return true; // cannot verify what's there — fail closed
|
|
109
|
+
}
|
|
110
|
+
if (!stat.isFile()) return true; // symlink, directory, fifo, device, … — never chmod/rename onto it
|
|
111
|
+
if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) return true; // foreign owner
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
73
115
|
/**
|
|
74
116
|
* ONE writer for every providers.json write (EXTRACT-PER-CLIENT W1 FIX
|
|
75
117
|
* GOV-3 item 1 + should-fix item 9) — writeByoConfig, clearProvidersFile,
|
|
@@ -78,14 +120,19 @@ function isHomeUnresolved(target) {
|
|
|
78
120
|
* right instead of three independent (and, before this fix, drifted) copies
|
|
79
121
|
* of it. Discipline, in order:
|
|
80
122
|
* 1. mkdir the parent dir 0700 (idempotent).
|
|
81
|
-
* 2.
|
|
123
|
+
* 2. lstat whatever already sits at `target` and refuse (reasonCode
|
|
124
|
+
* 'provider-state-target-unsafe') if it exists and is not a regular
|
|
125
|
+
* file owned by this uid (EXTRACTION-LOW-FOLLOWUPS item 4) — checked
|
|
126
|
+
* BEFORE the chmod below, which would otherwise follow a planted
|
|
127
|
+
* symlink.
|
|
128
|
+
* 3. If a file already sits at `target`, chmod it 0600 BEFORE the
|
|
82
129
|
* rename lands (belt-and-suspenders — the post-rename chmod below is
|
|
83
130
|
* the one that actually matters for a stale `.tmp`).
|
|
84
|
-
*
|
|
131
|
+
* 4. Unlink any leftover `${target}.tmp` first (a crashed prior run, or a
|
|
85
132
|
* planted symlink), THEN create it fresh with `flag:'wx'` (O_EXCL) —
|
|
86
133
|
* refuses to silently reuse or follow anything already at that path.
|
|
87
|
-
*
|
|
88
|
-
*
|
|
134
|
+
* 5. Atomic rename tmp -> target.
|
|
135
|
+
* 6. chmodSync(target, 0o600) AFTER the rename. This is the literal fix
|
|
89
136
|
* for GOV-3 finding 1: writeFileSync's `mode` option only applies on
|
|
90
137
|
* CREATION, so a stale `.tmp` that survived a crash at 0644 would
|
|
91
138
|
* rename onto `target` and KEEP 0644, silently falsifying the
|
|
@@ -93,7 +140,8 @@ function isHomeUnresolved(target) {
|
|
|
93
140
|
* `persistSelected` (scripts/providers/index.js) had this exact gap;
|
|
94
141
|
* this writer closes it everywhere at once.
|
|
95
142
|
* Throws (does not swallow) when the home directory could not be resolved
|
|
96
|
-
* (isHomeUnresolved)
|
|
143
|
+
* (isHomeUnresolved) or the existing target is unsafe (reasonCode
|
|
144
|
+
* 'provider-state-target-unsafe') — callers decide how to surface that; see
|
|
97
145
|
* writeByoConfig/clearProvidersFile below and cmdProvider in
|
|
98
146
|
* bin/auxilo-cli.js, which catches this rather than let a raw stack out
|
|
99
147
|
* (should-fix item 10).
|
|
@@ -106,6 +154,11 @@ function writeProvidersStateAtomic(state, opts = {}) {
|
|
|
106
154
|
throw err;
|
|
107
155
|
}
|
|
108
156
|
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
157
|
+
if (isUnsafeExistingTarget(target, opts)) {
|
|
158
|
+
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)');
|
|
159
|
+
err.reasonCode = 'provider-state-target-unsafe';
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
109
162
|
if (fs.existsSync(target)) fs.chmodSync(target, 0o600);
|
|
110
163
|
const tmp = `${target}.tmp`;
|
|
111
164
|
try { fs.unlinkSync(tmp); } catch { /* nothing there, or already gone — fine either way */ }
|
|
@@ -150,6 +203,12 @@ function readByoConfig(opts = {}) {
|
|
|
150
203
|
* source discipline; the discipline itself — chmod-then-tmp-write-then-
|
|
151
204
|
* rename, both 0600 — is copied, not the code).
|
|
152
205
|
*
|
|
206
|
+
* Throws a tagged error (writeProvidersStateAtomic) on 'provider-home-
|
|
207
|
+
* unresolved' OR, since EXTRACTION-LOW-FOLLOWUPS item 4, on
|
|
208
|
+
* 'provider-state-target-unsafe' (an existing entry at the target path is
|
|
209
|
+
* a symlink or owned by a different uid) — cmdProvider in bin/auxilo-cli.js
|
|
210
|
+
* catches both rather than let a raw stack out.
|
|
211
|
+
*
|
|
153
212
|
* @param {{provider:string, base_url?:string|null, model:string, api_key:string}} byoConfig
|
|
154
213
|
*/
|
|
155
214
|
function writeByoConfig(byoConfig, opts = {}) {
|
|
@@ -182,7 +241,11 @@ function writeByoConfig(byoConfig, opts = {}) {
|
|
|
182
241
|
* rather than guess a location to read/write. A stat/read failure other
|
|
183
242
|
* than ENOENT (e.g. EACCES) returns 'unreadable' rather than rethrow — this
|
|
184
243
|
* function's contract really is "never throws" now (should-fix item 10; the
|
|
185
|
-
* old rethrow on non-ENOENT contradicted this same docblock).
|
|
244
|
+
* old rethrow on non-ENOENT contradicted this same docblock). The rewrite
|
|
245
|
+
* path below can now also throw (writeProvidersStateAtomic's
|
|
246
|
+
* 'provider-state-target-unsafe', EXTRACTION-LOW-FOLLOWUPS item 4) — caught
|
|
247
|
+
* here too, folded into 'unreadable', so this function's own "never throws"
|
|
248
|
+
* contract holds regardless of what writeProvidersStateAtomic does.
|
|
186
249
|
*
|
|
187
250
|
* @returns {'removed-file'|'removed-byo'|'noop'|'unreadable'|'unresolved'}
|
|
188
251
|
* 'noop' covers both "no file" and "a file with no `byo` key to clear".
|
|
@@ -209,10 +272,18 @@ function clearProvidersFile(opts = {}) {
|
|
|
209
272
|
}
|
|
210
273
|
delete state.byo;
|
|
211
274
|
if (Object.keys(state).length === 0) {
|
|
212
|
-
|
|
275
|
+
try {
|
|
276
|
+
fs.unlinkSync(target);
|
|
277
|
+
} catch {
|
|
278
|
+
return 'unreadable'; // cannot remove it either — never throws (contract)
|
|
279
|
+
}
|
|
213
280
|
return 'removed-file';
|
|
214
281
|
}
|
|
215
|
-
|
|
282
|
+
try {
|
|
283
|
+
writeProvidersStateAtomic(state, opts);
|
|
284
|
+
} catch {
|
|
285
|
+
return 'unreadable'; // e.g. provider-state-target-unsafe — never throws (contract)
|
|
286
|
+
}
|
|
216
287
|
return 'removed-byo';
|
|
217
288
|
}
|
|
218
289
|
|
|
@@ -244,6 +315,8 @@ function baseUrlFor(vendor, configured) {
|
|
|
244
315
|
* throws.
|
|
245
316
|
*/
|
|
246
317
|
function isProvidersFileModeUnsafe(opts = {}) {
|
|
318
|
+
// EXTRACTION-LOW-FOLLOWUPS item 2 (TOCTOU, accepted on the record): a
|
|
319
|
+
// 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
320
|
const target = statePath(opts);
|
|
248
321
|
if (isHomeUnresolved(target)) return true; // can't even name the file — fail closed
|
|
249
322
|
const statSyncImpl = typeof opts.statSyncImpl === 'function' ? opts.statSyncImpl : fs.statSync;
|
|
@@ -627,5 +700,7 @@ module.exports = {
|
|
|
627
700
|
isHomeUnresolved,
|
|
628
701
|
isBaseUrlInsecure,
|
|
629
702
|
writeProvidersStateAtomic,
|
|
703
|
+
// Exported for direct unit coverage (EXTRACTION-LOW-FOLLOWUPS item 4).
|
|
704
|
+
isUnsafeExistingTarget,
|
|
630
705
|
MAX_RESPONSE_BYTES,
|
|
631
706
|
};
|
|
@@ -41,6 +41,93 @@ function resolveClaudeBin(opts = {}) {
|
|
|
41
41
|
return 'claude';
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// ─── Child settings/hooks isolation (EXTRACTION-CHILD-HOOKS, PUNCH-LIST P1,
|
|
45
|
+
// 0.9.15) ────────────────────────────────────────────────────────────────
|
|
46
|
+
//
|
|
47
|
+
// Every `claude -p` extraction child previously loaded the OPERATOR'S OWN
|
|
48
|
+
// ~/.claude/settings.json and therefore fired their personal SessionStart
|
|
49
|
+
// hooks (mandate.sh, session-context.sh, ...) — output that reaches the
|
|
50
|
+
// extraction/judge prompt without ever passing the package's scrubber. That
|
|
51
|
+
// is a privacy defect: content from the machine's own hook configuration
|
|
52
|
+
// (potentially personal notes, live queries, etc.) enters a transcript that
|
|
53
|
+
// gets sent to the resolved provider.
|
|
54
|
+
//
|
|
55
|
+
// `--setting-sources <sources>` ("Comma-separated list of setting sources to
|
|
56
|
+
// load (user, project, local)") is documented on both installed CLIs probed
|
|
57
|
+
// during investigation (2.1.12, 2.1.260) — scratchpad hooks-0914/
|
|
58
|
+
// EXTRACTION-CHILD-HOOKS-FINDINGS.md. This build (hooks-0915) tested LIVE
|
|
59
|
+
// whether an EMPTY list (`--setting-sources ''`) — the narrowest, fully
|
|
60
|
+
// cwd-independent value, loading none of user/project/local — is accepted:
|
|
61
|
+
// it is (2.1.12, exit 0, hook_response count 0, well-formed result). Shipping
|
|
62
|
+
// `''` means no fresh-temp-cwd workaround is needed: an empty source list
|
|
63
|
+
// loads nothing regardless of the child's cwd, unlike the `project,local`
|
|
64
|
+
// fallback (which still honors a target repo's own `.claude/settings.json`).
|
|
65
|
+
const SETTING_SOURCES_VALUE = '';
|
|
66
|
+
const SETTING_SOURCES_ARGS = Object.freeze(['--setting-sources', SETTING_SOURCES_VALUE]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Fail-closed detection of "this CLI build doesn't understand
|
|
70
|
+
* --setting-sources at all" (an old install predating the flag, or a rename).
|
|
71
|
+
* Detected from the SAME spawn that already carries the flag — no extra
|
|
72
|
+
* `--help`/`--version` probe call, so the happy-path spawn count for every
|
|
73
|
+
* existing caller/test is unchanged. A CLI rejecting an unknown flag exits
|
|
74
|
+
* non-zero with a message naming the flag (commander.js-style "error:
|
|
75
|
+
* unknown option '--setting-sources'"); this pattern-matches for that
|
|
76
|
+
* specific shape rather than treating every non-zero exit as unsupported (a
|
|
77
|
+
* real model/auth error must NOT be misreported as isolation-unsupported).
|
|
78
|
+
* Cached module-wide for the lifetime of the process ("once per run"): the
|
|
79
|
+
* first spawn that hits this failure marks the CLI unsupported and every
|
|
80
|
+
* subsequent runModel() call in the same process short-circuits BEFORE
|
|
81
|
+
* spawning again — it must never spawn without the flag, and re-attempting a
|
|
82
|
+
* doomed spawn every call would be silent waste, not safety.
|
|
83
|
+
*/
|
|
84
|
+
let cachedSettingSourcesUnsupported; // undefined = not yet observed; true once detected
|
|
85
|
+
|
|
86
|
+
function looksLikeUnsupportedSettingSourcesFlag(res) {
|
|
87
|
+
if (!res || res.status === 0) return false;
|
|
88
|
+
const combined = `${String(res.stdout || '')}\n${String(res.stderr || '')}`;
|
|
89
|
+
return /--setting-sources/.test(combined) && /\b(unknown|unrecognized|invalid)\b.{0,20}\b(option|argument|flag)\b/i.test(combined);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function settingSourcesIsolationUnsupportedResult(authStatus) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
text: '',
|
|
96
|
+
usage: null,
|
|
97
|
+
reason: 'installed Claude Code CLI does not support --setting-sources; extraction declines to run a child that would load the operator\'s own settings/hooks unisolated',
|
|
98
|
+
reasonCode: 'cli-settings-isolation-unsupported',
|
|
99
|
+
authStatus: authStatus || 'unknown',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Test-only: reset the module-level isolation-support cache between fixtures. */
|
|
104
|
+
function _resetSettingSourcesCacheForTests() {
|
|
105
|
+
cachedSettingSourcesUnsupported = undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── CLI version, for diagnostics only (no subprocess spawn) ───────────────
|
|
109
|
+
//
|
|
110
|
+
// Resolves the installed package's own package.json version by following the
|
|
111
|
+
// resolved binary's real path (e.g. `/usr/local/bin/claude` -> `.../
|
|
112
|
+
// node_modules/@anthropic-ai/claude-code/cli.js`) and reading the sibling
|
|
113
|
+
// package.json — filesystem-only, so it never adds a spawn to the extraction
|
|
114
|
+
// path (verified live: realpath + package.json read, no `claude --version`
|
|
115
|
+
// call). Best-effort: any failure (bare `claude` unresolved via PATH, an
|
|
116
|
+
// install layout that doesn't carry a sibling package.json, a fixture path in
|
|
117
|
+
// tests) yields null, never throws.
|
|
118
|
+
function getClaudeCliVersion(bin, opts = {}) {
|
|
119
|
+
const realpathSyncImpl = typeof opts.realpathSyncImpl === 'function' ? opts.realpathSyncImpl : fs.realpathSync;
|
|
120
|
+
const readFileSyncImpl = typeof opts.readFileSyncImpl === 'function' ? opts.readFileSyncImpl : fs.readFileSync;
|
|
121
|
+
try {
|
|
122
|
+
const real = realpathSyncImpl(bin);
|
|
123
|
+
const pkgPath = path.join(path.dirname(real), 'package.json');
|
|
124
|
+
const pkg = JSON.parse(readFileSyncImpl(pkgPath, 'utf8'));
|
|
125
|
+
return pkg && typeof pkg.version === 'string' ? pkg.version : null;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
44
131
|
// ─── Env scrub (EXTRACT-TOOLS-LOCK, PUNCH-LIST) ────────────────────────────
|
|
45
132
|
//
|
|
46
133
|
// SME-confirmed list (claude-code-guide, verified against official docs and
|
|
@@ -79,6 +166,20 @@ const SCRUBBED_CLIENT_ENV_VARS = Object.freeze([
|
|
|
79
166
|
'CLOUD_ML_REGION',
|
|
80
167
|
]);
|
|
81
168
|
|
|
169
|
+
// ─── Spawn argv (EXTRACTION-ZERO-TOOL-CALLS control) ───────────────────────
|
|
170
|
+
//
|
|
171
|
+
// Named + frozen so a byte-pinned test (test/extraction-zero-tool-calls.test.js,
|
|
172
|
+
// test/claude-code-provider.test.js) can assert the exact argv without
|
|
173
|
+
// duplicating the literal, and so a future flag change is a conscious,
|
|
174
|
+
// greppable edit here rather than a silent literal tweak buried in the two
|
|
175
|
+
// runXMode() functions below. No behavior change: the two spawnSyncImpl()
|
|
176
|
+
// call sites below now pass these constants instead of inline array literals
|
|
177
|
+
// of the identical contents. 0.9.15 (EXTRACTION-CHILD-HOOKS) appends
|
|
178
|
+
// SETTING_SOURCES_ARGS to both — the child loads none of user/project/local
|
|
179
|
+
// settings, so the operator's own SessionStart hooks never fire.
|
|
180
|
+
const EXTRACT_MODE_ARGV = Object.freeze(['-p', '--no-session-persistence', '--tools', '', ...SETTING_SOURCES_ARGS]);
|
|
181
|
+
const JUDGE_MODE_ARGV = Object.freeze(['-p', '--output-format', 'json', '--no-session-persistence', '--tools', '', ...SETTING_SOURCES_ARGS]);
|
|
182
|
+
|
|
82
183
|
/**
|
|
83
184
|
* Build the subscription-auth-only environment shared by BOTH the extraction and
|
|
84
185
|
* judge Claude CLI children — one function, no drift between the two spawns.
|
|
@@ -154,7 +255,19 @@ function managedSettingsPathForPlatform(opts) {
|
|
|
154
255
|
// otherwise names a fixed, real, OS-level location no test should touch).
|
|
155
256
|
if (typeof opts.managedSettingsPath === 'string') return opts.managedSettingsPath;
|
|
156
257
|
const platform = typeof opts.platform === 'string' ? opts.platform : process.platform;
|
|
157
|
-
|
|
258
|
+
// EXTRACTION-LOW-FOLLOWUPS item 1: a raw `[platform]` index is reachable
|
|
259
|
+
// (only via the test seam opts.platform, per the row) with a prototype key
|
|
260
|
+
// ('constructor', 'toString', '__proto__', …) and would return a truthy
|
|
261
|
+
// Object.prototype value instead of falling through to the Linux default —
|
|
262
|
+
// fails OPEN with a bogus path silently in place of the real managed-
|
|
263
|
+
// settings check. hasOwnProperty scopes the lookup to the object's own
|
|
264
|
+
// enumerable keys, mirroring the guard at scripts/providers/index.js:139,
|
|
265
|
+
// so an unknown or prototype-polluting key falls CLOSED to the Linux path
|
|
266
|
+
// exactly like any other unrecognized platform string does today.
|
|
267
|
+
if (Object.prototype.hasOwnProperty.call(MANAGED_SETTINGS_PATH_BY_PLATFORM, platform)) {
|
|
268
|
+
return MANAGED_SETTINGS_PATH_BY_PLATFORM[platform];
|
|
269
|
+
}
|
|
270
|
+
return MANAGED_SETTINGS_PATH_BY_PLATFORM.linux;
|
|
158
271
|
}
|
|
159
272
|
|
|
160
273
|
/**
|
|
@@ -167,7 +280,7 @@ function managedSettingsPathForPlatform(opts) {
|
|
|
167
280
|
* that matches Claude Code's own behavior for those files, which this
|
|
168
281
|
* managed path does not share.)
|
|
169
282
|
*/
|
|
170
|
-
function managedSettingsBlocksOrUnverifiable(filePath, existsSyncImpl, readFileSyncImpl) {
|
|
283
|
+
function managedSettingsBlocksOrUnverifiable(filePath, existsSyncImpl, readFileSyncImpl, log) {
|
|
171
284
|
let exists;
|
|
172
285
|
try {
|
|
173
286
|
exists = existsSyncImpl(filePath);
|
|
@@ -176,7 +289,17 @@ function managedSettingsBlocksOrUnverifiable(filePath, existsSyncImpl, readFileS
|
|
|
176
289
|
}
|
|
177
290
|
if (!exists) return false;
|
|
178
291
|
const parsed = readJsonSafe(filePath, readFileSyncImpl);
|
|
179
|
-
if (parsed === null)
|
|
292
|
+
if (parsed === null) {
|
|
293
|
+
// EXTRACTION-LOW-FOLLOWUPS item 3: this fail-closed branch silently
|
|
294
|
+
// switches the builder away from claude-code (reasonCode
|
|
295
|
+
// 'cli-billing-helper-configured', same as a real detected helper) with
|
|
296
|
+
// no visible signal that the cause was an UNVERIFIABLE managed-settings
|
|
297
|
+
// file rather than an actual foreign-billing helper. One stderr line
|
|
298
|
+
// naming the reason code — never the file's contents or any key
|
|
299
|
+
// material, both of which stay out of every log call in this module.
|
|
300
|
+
log('[providers] managed-settings.json is present but unreadable/unparseable; failing closed and switching away from claude-code (reasonCode cli-billing-helper-configured)');
|
|
301
|
+
return true; // present but unreadable/unparseable — fail closed
|
|
302
|
+
}
|
|
180
303
|
return settingsHasBillingHelper(parsed);
|
|
181
304
|
}
|
|
182
305
|
|
|
@@ -195,8 +318,9 @@ function detectBillingHelperConfigured(opts = {}) {
|
|
|
195
318
|
const existsSyncImpl = typeof opts.existsSyncImpl === 'function' ? opts.existsSyncImpl : fs.existsSync;
|
|
196
319
|
const homeDir = typeof opts.homeDir === 'string' ? opts.homeDir : os.homedir();
|
|
197
320
|
const cwd = typeof opts.cwd === 'string' ? opts.cwd : process.cwd();
|
|
321
|
+
const log = typeof opts.log === 'function' ? opts.log : console.error;
|
|
198
322
|
|
|
199
|
-
if (managedSettingsBlocksOrUnverifiable(managedSettingsPathForPlatform(opts), existsSyncImpl, readFileSyncImpl)) {
|
|
323
|
+
if (managedSettingsBlocksOrUnverifiable(managedSettingsPathForPlatform(opts), existsSyncImpl, readFileSyncImpl, log)) {
|
|
200
324
|
return true;
|
|
201
325
|
}
|
|
202
326
|
|
|
@@ -274,6 +398,7 @@ function detect(opts = {}) {
|
|
|
274
398
|
function runExtractMode(opts) {
|
|
275
399
|
const spawnSyncImpl = typeof opts.spawnSyncImpl === 'function' ? opts.spawnSyncImpl : spawnSync;
|
|
276
400
|
const bin = typeof opts.claudeBin === 'string' ? opts.claudeBin : resolveClaudeBin(opts);
|
|
401
|
+
if (cachedSettingSourcesUnsupported) return settingSourcesIsolationUnsupportedResult('unknown');
|
|
277
402
|
const authStatus = checkAuthStatus({ spawnSyncImpl, claudeBin: bin, ...opts });
|
|
278
403
|
if (authStatus === 'logged-out') {
|
|
279
404
|
return {
|
|
@@ -287,11 +412,18 @@ function runExtractMode(opts) {
|
|
|
287
412
|
}
|
|
288
413
|
const prompt = typeof opts.prompt === 'string' ? opts.prompt : '';
|
|
289
414
|
const stdin = prompt + String(opts.input || '').slice(0, 200000);
|
|
415
|
+
// --no-session-persistence (EXTRACT-PER-CLIENT W1 FIX GIVENS): matches the
|
|
416
|
+
// judge spawn below — an extraction run leaves no session file behind either.
|
|
417
|
+
// --setting-sources '' (EXTRACTION-CHILD-HOOKS, 0.9.15): the child loads none
|
|
418
|
+
// of user/project/local settings, so the operator's own SessionStart hooks
|
|
419
|
+
// never fire and their output never reaches this prompt.
|
|
420
|
+
const argv = EXTRACT_MODE_ARGV;
|
|
421
|
+
const cliVersion = getClaudeCliVersion(bin, opts);
|
|
290
422
|
let res;
|
|
291
423
|
try {
|
|
292
424
|
// --no-session-persistence (EXTRACT-PER-CLIENT W1 FIX GIVENS): matches the
|
|
293
425
|
// judge spawn below — an extraction run leaves no session file behind either.
|
|
294
|
-
res = spawnSyncImpl(bin,
|
|
426
|
+
res = spawnSyncImpl(bin, argv, {
|
|
295
427
|
input: stdin,
|
|
296
428
|
encoding: 'utf-8',
|
|
297
429
|
env: claudeChildEnv(),
|
|
@@ -299,14 +431,18 @@ function runExtractMode(opts) {
|
|
|
299
431
|
maxBuffer: 20 * 1024 * 1024,
|
|
300
432
|
});
|
|
301
433
|
} catch (error) {
|
|
302
|
-
return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): ${error.message}`, reasonCode: 'unknown', authStatus };
|
|
434
|
+
return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): ${error.message}`, reasonCode: 'unknown', authStatus, argv, cliVersion };
|
|
303
435
|
}
|
|
304
436
|
if (!res) {
|
|
305
|
-
return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): no process result`, reasonCode: 'unknown', authStatus };
|
|
437
|
+
return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): no process result`, reasonCode: 'unknown', authStatus, argv, cliVersion };
|
|
438
|
+
}
|
|
439
|
+
if (looksLikeUnsupportedSettingSourcesFlag(res)) {
|
|
440
|
+
cachedSettingSourcesUnsupported = true;
|
|
441
|
+
return { ...settingSourcesIsolationUnsupportedResult(authStatus), argv, cliVersion };
|
|
306
442
|
}
|
|
307
443
|
const out = String(res.stdout || '');
|
|
308
444
|
if (res.error) {
|
|
309
|
-
return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): ${res.error.message}`, reasonCode: 'unknown', authStatus };
|
|
445
|
+
return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): ${res.error.message}`, reasonCode: 'unknown', authStatus, argv, cliVersion };
|
|
310
446
|
}
|
|
311
447
|
// Claude prints auth failures ("API Error: 401 ... Please run /login") to stdout.
|
|
312
448
|
if (/Please run \/login|authentication_error|401/i.test(out) || /Please run \/login|authentication_error/i.test(String(res.stderr || ''))) {
|
|
@@ -317,6 +453,8 @@ function runExtractMode(opts) {
|
|
|
317
453
|
reason: 'local model not authenticated in this context (run `claude auth login` once); skipping deterministic extraction',
|
|
318
454
|
reasonCode: 'cli-unauthenticated',
|
|
319
455
|
authStatus,
|
|
456
|
+
argv,
|
|
457
|
+
cliVersion,
|
|
320
458
|
...(authStatus === 'logged-in' && { authDiscrepancy: true }),
|
|
321
459
|
};
|
|
322
460
|
}
|
|
@@ -328,9 +466,11 @@ function runExtractMode(opts) {
|
|
|
328
466
|
reason: `local model exited ${res.status}: ${(out || String(res.stderr || '')).slice(0, 160)}`,
|
|
329
467
|
reasonCode: 'model-error',
|
|
330
468
|
authStatus,
|
|
469
|
+
argv,
|
|
470
|
+
cliVersion,
|
|
331
471
|
};
|
|
332
472
|
}
|
|
333
|
-
return { ok: true, text: out, usage: null, reason: null, authStatus };
|
|
473
|
+
return { ok: true, text: out, usage: null, reason: null, authStatus, argv, cliVersion };
|
|
334
474
|
}
|
|
335
475
|
|
|
336
476
|
/**
|
|
@@ -353,14 +493,18 @@ function normalizeJudgeUsage(rawUsage) {
|
|
|
353
493
|
return { input_tokens: inputSum, output_tokens: output };
|
|
354
494
|
}
|
|
355
495
|
|
|
356
|
-
/** mode:'judge' — binary anchored-dedup decision. Argv byte-identical to pre-move
|
|
496
|
+
/** mode:'judge' — binary anchored-dedup decision. Argv byte-identical to pre-move plus
|
|
497
|
+
* the same --setting-sources '' isolation the extraction spawn above gains (0.9.15). */
|
|
357
498
|
function runJudgeMode(opts) {
|
|
358
499
|
const spawnSyncImpl = typeof opts.spawnSyncImpl === 'function' ? opts.spawnSyncImpl : spawnSync;
|
|
359
500
|
const bin = typeof opts.claudeBin === 'string' ? opts.claudeBin : resolveClaudeBin(opts);
|
|
501
|
+
if (cachedSettingSourcesUnsupported) return settingSourcesIsolationUnsupportedResult('unknown');
|
|
360
502
|
const prompt = typeof opts.prompt === 'string' ? opts.prompt : '';
|
|
503
|
+
const argv = JUDGE_MODE_ARGV;
|
|
504
|
+
const cliVersion = getClaudeCliVersion(bin, opts);
|
|
361
505
|
let res;
|
|
362
506
|
try {
|
|
363
|
-
res = spawnSyncImpl(bin,
|
|
507
|
+
res = spawnSyncImpl(bin, argv, {
|
|
364
508
|
input: prompt,
|
|
365
509
|
encoding: 'utf8',
|
|
366
510
|
env: claudeChildEnv(),
|
|
@@ -368,17 +512,21 @@ function runJudgeMode(opts) {
|
|
|
368
512
|
maxBuffer: 20 * 1024 * 1024,
|
|
369
513
|
});
|
|
370
514
|
} catch (error) {
|
|
371
|
-
return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): ${error.message}`, reasonCode: 'unknown', authStatus: 'unknown' };
|
|
515
|
+
return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): ${error.message}`, reasonCode: 'unknown', authStatus: 'unknown', argv, cliVersion };
|
|
372
516
|
}
|
|
373
517
|
if (!res) {
|
|
374
|
-
return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): no process result`, reasonCode: 'unknown', authStatus: 'unknown' };
|
|
518
|
+
return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): no process result`, reasonCode: 'unknown', authStatus: 'unknown', argv, cliVersion };
|
|
519
|
+
}
|
|
520
|
+
if (looksLikeUnsupportedSettingSourcesFlag(res)) {
|
|
521
|
+
cachedSettingSourcesUnsupported = true;
|
|
522
|
+
return { ...settingSourcesIsolationUnsupportedResult('unknown'), argv, cliVersion };
|
|
375
523
|
}
|
|
376
524
|
const stdout = String(res.stdout || '');
|
|
377
525
|
if (res.error) {
|
|
378
|
-
return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): ${res.error.message}`, reasonCode: 'unknown', authStatus: 'unknown' };
|
|
526
|
+
return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): ${res.error.message}`, reasonCode: 'unknown', authStatus: 'unknown', argv, cliVersion };
|
|
379
527
|
}
|
|
380
528
|
if (/Please run \/login|authentication_error|401/i.test(stdout) || /Please run \/login|authentication_error/i.test(String(res.stderr || ''))) {
|
|
381
|
-
return { ok: false, text: '', usage: null, reason: 'local judge model is not authenticated', reasonCode: 'cli-unauthenticated', authStatus: 'unknown' };
|
|
529
|
+
return { ok: false, text: '', usage: null, reason: 'local judge model is not authenticated', reasonCode: 'cli-unauthenticated', authStatus: 'unknown', argv, cliVersion };
|
|
382
530
|
}
|
|
383
531
|
if (res.status !== 0) {
|
|
384
532
|
return {
|
|
@@ -388,18 +536,20 @@ function runJudgeMode(opts) {
|
|
|
388
536
|
reason: `local judge exited ${res.status}: ${(stdout || String(res.stderr || '')).slice(0, 160)}`,
|
|
389
537
|
reasonCode: 'model-error',
|
|
390
538
|
authStatus: 'unknown',
|
|
539
|
+
argv,
|
|
540
|
+
cliVersion,
|
|
391
541
|
};
|
|
392
542
|
}
|
|
393
543
|
let wrapper;
|
|
394
544
|
try {
|
|
395
545
|
wrapper = JSON.parse(stdout);
|
|
396
546
|
} catch {
|
|
397
|
-
return { ok: false, text: '', usage: null, reason: 'local judge returned malformed JSON wrapper', reasonCode: 'model-error', authStatus: 'unknown' };
|
|
547
|
+
return { ok: false, text: '', usage: null, reason: 'local judge returned malformed JSON wrapper', reasonCode: 'model-error', authStatus: 'unknown', argv, cliVersion };
|
|
398
548
|
}
|
|
399
549
|
if (!wrapper || typeof wrapper.result !== 'string' || wrapper.is_error === true) {
|
|
400
|
-
return { ok: false, text: '', usage: null, reason: 'local judge returned no successful result', reasonCode: 'model-error', authStatus: 'unknown' };
|
|
550
|
+
return { ok: false, text: '', usage: null, reason: 'local judge returned no successful result', reasonCode: 'model-error', authStatus: 'unknown', argv, cliVersion };
|
|
401
551
|
}
|
|
402
|
-
return { ok: true, text: wrapper.result, usage: normalizeJudgeUsage(wrapper.usage), reason: null, authStatus: 'unknown' };
|
|
552
|
+
return { ok: true, text: wrapper.result, usage: normalizeJudgeUsage(wrapper.usage), reason: null, authStatus: 'unknown', argv, cliVersion };
|
|
403
553
|
}
|
|
404
554
|
|
|
405
555
|
/**
|
|
@@ -474,4 +624,18 @@ module.exports = {
|
|
|
474
624
|
// Exported for direct unit coverage (test/extract-w1-fix2.test.js, GOV-3 item 6).
|
|
475
625
|
managedSettingsPathForPlatform,
|
|
476
626
|
MANAGED_SETTINGS_PATH_BY_PLATFORM,
|
|
627
|
+
// Exported for direct byte-pinned coverage (test/extraction-zero-tool-calls.test.js,
|
|
628
|
+
// test/claude-code-provider.test.js; TRUST-PAGE control — SITE-PM: put the
|
|
629
|
+
// zero-tool-call assertion in the test suite). Carries the 0.9.15 argv
|
|
630
|
+
// (SETTING_SOURCES_ARGS included).
|
|
631
|
+
EXTRACT_MODE_ARGV,
|
|
632
|
+
JUDGE_MODE_ARGV,
|
|
633
|
+
// EXTRACTION-CHILD-HOOKS (0.9.15) — exported for direct unit coverage
|
|
634
|
+
// (test/claude-code-provider.test.js) and for extract-local.js's provider-run
|
|
635
|
+
// log line (getClaudeCliVersion).
|
|
636
|
+
SETTING_SOURCES_VALUE,
|
|
637
|
+
SETTING_SOURCES_ARGS,
|
|
638
|
+
getClaudeCliVersion,
|
|
639
|
+
looksLikeUnsupportedSettingSourcesFlag,
|
|
640
|
+
_resetSettingSourcesCacheForTests,
|
|
477
641
|
};
|
|
@@ -78,7 +78,13 @@ const PROVIDERS = {
|
|
|
78
78
|
'byo-key': byoKey,
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
// TEST-HOME-ISOLATION: same AUXILO_HOME-over-os.homedir() fallback as
|
|
82
|
+
// scripts/providers/byo-key.js's DEFAULT_PROVIDERS_STATE_PATH (duplicated,
|
|
83
|
+
// not imported — see that file's docblock; test/byo-key-provider.test.js
|
|
84
|
+
// pins the two byte-equal). opts.providersStatePath, threaded through every
|
|
85
|
+
// resolveProvider()/runModel()/persistSelected() call site in this repo's
|
|
86
|
+
// tests, still wins over both.
|
|
87
|
+
const PROVIDERS_STATE_PATH = path.join(process.env.AUXILO_HOME || os.homedir(), '.auxilo', 'providers.json');
|
|
82
88
|
|
|
83
89
|
function readProvidersState(statePath) {
|
|
84
90
|
try {
|
|
@@ -232,6 +238,11 @@ const NON_RETRYABLE_FOR_THIS_PROVIDER = new Set([
|
|
|
232
238
|
'cli-billing-helper-configured',
|
|
233
239
|
'provider-not-configured',
|
|
234
240
|
'providers-file-mode-unsafe',
|
|
241
|
+
// EXTRACTION-CHILD-HOOKS (0.9.15): the resolved claude-code CLI doesn't
|
|
242
|
+
// support --setting-sources, so it can never run isolated — same
|
|
243
|
+
// "cannot run at all right now" class as the codes above, safe to try the
|
|
244
|
+
// next provider in PROVIDER_ORDER rather than reporting a hard failure.
|
|
245
|
+
'cli-settings-isolation-unsupported',
|
|
235
246
|
]);
|
|
236
247
|
|
|
237
248
|
/**
|
package/scripts/runner.js
CHANGED
|
@@ -645,6 +645,11 @@ async function postExtractDetailed(transcript, sessionId, sourceType, _scrubRepo
|
|
|
645
645
|
captureVisibility: opts.captureVisibility || CAPTURE_VISIBILITY,
|
|
646
646
|
log: runnerLog,
|
|
647
647
|
auditLog: auditDropLog,
|
|
648
|
+
// EXTRACTION-RUN-LOG (0.9.15): the identifier the provider-run summary
|
|
649
|
+
// log line reports as `run=`. sessionId is already this call's natural
|
|
650
|
+
// run identity (it's what extraction_id: `client-${sessionId}` uses
|
|
651
|
+
// below); opts.runId (if a caller supplied one) wins over it.
|
|
652
|
+
runId: opts.runId || sessionId,
|
|
648
653
|
}));
|
|
649
654
|
} catch (err) {
|
|
650
655
|
throw new Error(`Local extraction failed: ${err.message}`);
|