auxilo-mcp 0.9.26 → 0.9.28
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/README.md +4 -0
- package/bin/auxilo-cli.js +2 -2
- package/mcp-server.js +6 -6
- package/package.json +1 -1
- package/scripts/extract-local.js +28 -4
- package/scripts/providers/byo-key.js +20 -5
- package/scripts/providers/claude-code.js +117 -141
- package/scripts/providers/codex-cli.js +25 -9
- package/scripts/providers/index.js +142 -155
- package/scripts/providers/provider.interface.js +24 -17
- package/scripts/providers/route-binding.js +232 -0
- package/scripts/runner.js +40 -11
|
@@ -32,6 +32,12 @@ const fs = require('fs');
|
|
|
32
32
|
const path = require('path');
|
|
33
33
|
const os = require('os');
|
|
34
34
|
const crypto = require('crypto');
|
|
35
|
+
const { invocationGate } = require('./route-binding.js');
|
|
36
|
+
|
|
37
|
+
const KNOWN_DISALLOWED_ITEM_TYPES = Object.freeze([
|
|
38
|
+
'command_execution', 'file_change', 'mcp_tool_call',
|
|
39
|
+
'collab_tool_call', 'web_search', 'dynamic_tool_call',
|
|
40
|
+
]);
|
|
35
41
|
|
|
36
42
|
const { SCRUBBED_CLIENT_ENV_VARS } = require('./claude-code.js');
|
|
37
43
|
|
|
@@ -264,12 +270,12 @@ function makeOutputLocation(opts, mode) {
|
|
|
264
270
|
|
|
265
271
|
function classifySpawnError(error, bin) {
|
|
266
272
|
if (error && error.code === 'ENOENT') {
|
|
267
|
-
return { reasonCode: 'cli-not-installed', reason:
|
|
273
|
+
return { reasonCode: 'cli-not-installed', reason: 'codex binary not found' };
|
|
268
274
|
}
|
|
269
275
|
if (error && (error.code === 'ETIMEDOUT' || error.code === 'ETIMEOUT')) {
|
|
270
|
-
return { reasonCode: 'cli-timeout', reason:
|
|
276
|
+
return { reasonCode: 'cli-timeout', reason: 'codex exec timed out' };
|
|
271
277
|
}
|
|
272
|
-
return { reasonCode: 'unknown', reason:
|
|
278
|
+
return { reasonCode: 'unknown', reason: 'codex spawn failed' };
|
|
273
279
|
}
|
|
274
280
|
|
|
275
281
|
/**
|
|
@@ -304,6 +310,7 @@ function invoke(opts, mode) {
|
|
|
304
310
|
usage: null,
|
|
305
311
|
reason: 'codex CLI is not authenticated in this context (run `codex login` once); skipping deterministic extraction',
|
|
306
312
|
reasonCode: 'cli-unauthenticated',
|
|
313
|
+
refusal: 'pre-invocation',
|
|
307
314
|
authStatus: 'logged-out',
|
|
308
315
|
};
|
|
309
316
|
}
|
|
@@ -319,8 +326,9 @@ function invoke(opts, mode) {
|
|
|
319
326
|
ok: false,
|
|
320
327
|
text: '',
|
|
321
328
|
usage: null,
|
|
322
|
-
reason:
|
|
329
|
+
reason: 'codex system configuration is present',
|
|
323
330
|
reasonCode: 'isolation-precondition',
|
|
331
|
+
refusal: 'pre-invocation',
|
|
324
332
|
authStatus: 'unknown',
|
|
325
333
|
};
|
|
326
334
|
}
|
|
@@ -351,6 +359,8 @@ function invoke(opts, mode) {
|
|
|
351
359
|
];
|
|
352
360
|
|
|
353
361
|
let res;
|
|
362
|
+
const gate = invocationGate(opts);
|
|
363
|
+
if (gate) return gate;
|
|
354
364
|
try {
|
|
355
365
|
res = spawnSyncImpl(bin, args, {
|
|
356
366
|
input: stdin,
|
|
@@ -365,7 +375,7 @@ function invoke(opts, mode) {
|
|
|
365
375
|
return { ok: false, text: '', usage: null, authStatus: 'unknown', ...classified };
|
|
366
376
|
}
|
|
367
377
|
if (!res) {
|
|
368
|
-
return { ok: false, text: '', usage: null, reason:
|
|
378
|
+
return { ok: false, text: '', usage: null, reason: 'codex spawn returned no process result', reasonCode: 'unknown', authStatus: 'unknown' };
|
|
369
379
|
}
|
|
370
380
|
if (res.error) {
|
|
371
381
|
const classified = classifySpawnError(res.error, bin);
|
|
@@ -374,7 +384,7 @@ function invoke(opts, mode) {
|
|
|
374
384
|
// spawnSync signals a timeout via `signal` (no `error`) when the child was
|
|
375
385
|
// killed for exceeding `timeout` and didn't set its own exit code.
|
|
376
386
|
if (res.signal && res.status === null) {
|
|
377
|
-
return { ok: false, text: '', usage: null, reason:
|
|
387
|
+
return { ok: false, text: '', usage: null, reason: 'codex exec timed out', reasonCode: 'cli-timeout', authStatus: 'unknown' };
|
|
378
388
|
}
|
|
379
389
|
|
|
380
390
|
const stdout = String(res.stdout || '');
|
|
@@ -395,7 +405,7 @@ function invoke(opts, mode) {
|
|
|
395
405
|
ok: false,
|
|
396
406
|
text: '',
|
|
397
407
|
usage: null,
|
|
398
|
-
reason:
|
|
408
|
+
reason: 'codex exec failed',
|
|
399
409
|
reasonCode: 'model-error',
|
|
400
410
|
authStatus: 'unknown',
|
|
401
411
|
};
|
|
@@ -422,7 +432,7 @@ function invoke(opts, mode) {
|
|
|
422
432
|
ok: false,
|
|
423
433
|
text: '',
|
|
424
434
|
usage: null,
|
|
425
|
-
reason: `codex exec emitted disallowed item type: ${itemType}`,
|
|
435
|
+
reason: `codex exec emitted disallowed item type: ${KNOWN_DISALLOWED_ITEM_TYPES.includes(itemType) ? itemType : 'unrecognized'}`,
|
|
426
436
|
reasonCode: 'isolation-violation',
|
|
427
437
|
authStatus: 'unknown',
|
|
428
438
|
};
|
|
@@ -472,6 +482,9 @@ function invoke(opts, mode) {
|
|
|
472
482
|
const identity = {
|
|
473
483
|
provider: 'codex-cli',
|
|
474
484
|
model: null, // the route intentionally preserves its existing identity contract
|
|
485
|
+
requested_model: null,
|
|
486
|
+
observed_model: null,
|
|
487
|
+
identity_unresolved: 'missing',
|
|
475
488
|
version: getCodexVersion(opts),
|
|
476
489
|
vendor: null,
|
|
477
490
|
};
|
|
@@ -503,7 +516,8 @@ function invoke(opts, mode) {
|
|
|
503
516
|
/** runModel(opts) — the provider.interface.js contract. */
|
|
504
517
|
async function runModel(opts = {}) {
|
|
505
518
|
const mode = opts.mode === 'judge' ? 'judge' : 'extract';
|
|
506
|
-
return invoke(opts, mode);
|
|
519
|
+
try { return invoke(opts, mode); }
|
|
520
|
+
catch { return { ok: false, text: '', usage: null, reason: 'codex invocation failed', reasonCode: 'unknown', authStatus: 'unknown' }; }
|
|
507
521
|
}
|
|
508
522
|
|
|
509
523
|
module.exports = {
|
|
@@ -511,6 +525,8 @@ module.exports = {
|
|
|
511
525
|
detect,
|
|
512
526
|
resolveCodexBin,
|
|
513
527
|
readAuthMode,
|
|
528
|
+
checkAuthStatus: opts => readAuthMode(opts) ? 'logged-in' : 'logged-out',
|
|
529
|
+
KNOWN_DISALLOWED_ITEM_TYPES,
|
|
514
530
|
codexChildEnv,
|
|
515
531
|
getCodexVersion,
|
|
516
532
|
neutralizeSkillMentions,
|
|
@@ -1,41 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
/*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* persisted — "wins, never writes") → else the persisted `selected` choice
|
|
10
|
-
* from ~/.auxilo/providers.json if it is STILL usable (re-verified via its
|
|
11
|
-
* own detect() every call, never trusted blindly — a stale persisted choice
|
|
12
|
-
* falls through to a full re-scan rather than failing) → else the first
|
|
13
|
-
* provider whose detect() is true, in automatic order (claude-code →
|
|
14
|
-
* byo-key) → else ok:false, with a reason naming every automatic provider
|
|
15
|
-
* tried. codex-cli remains available only through the explicit env override.
|
|
16
|
-
*
|
|
17
|
-
* runModel(): resolves via resolveProvider(), then actually RUNS it. A
|
|
18
|
-
* non-override resolution that fails with a reasonCode meaning "this
|
|
19
|
-
* provider cannot run at all" (NON_RETRYABLE_FOR_THIS_PROVIDER — e.g.
|
|
20
|
-
* unauthenticated, not installed, a billing helper is configured) falls
|
|
21
|
-
* through to the next provider in AUTOMATIC_PROVIDER_ORDER rather than
|
|
22
|
-
* reporting a hard failure; a working provider that merely failed once
|
|
23
|
-
* (timeout, model error) does not fall through — that is still the builder's
|
|
24
|
-
* chosen provider having a bad run, not a reason to switch under them. An
|
|
25
|
-
* explicit env override, including codex-cli, never falls through, honoring
|
|
26
|
-
* the operator's explicit choice. Every automatic provider exhausted →
|
|
27
|
-
* reasonCode 'no-usable-provider' with every attempt's reason summarized in
|
|
28
|
-
* `reason`.
|
|
29
|
-
*
|
|
30
|
-
* codex-cli.js and byo-key.js don't exist yet (PART B/C). loadOptionalProvider()
|
|
31
|
-
* degrades a missing module into a "not installed yet" stub so this file — and
|
|
32
|
-
* everything that calls into it — never throws on a fresh PART A checkout; PART
|
|
33
|
-
* B/C only need to add their files, not touch this registration.
|
|
3
|
+
* EPC2-2 A+B: routing is pinned per job; initial automatic precedence is
|
|
4
|
+
* unchanged with Codex dark. An existing binding wins over new configuration.
|
|
5
|
+
* Only a fresh, continuously owned automatic attempt with no invocation may
|
|
6
|
+
* fall back on an explicit pre-invocation refusal. CLI model consistency is
|
|
7
|
+
* observed, not enforced. Runner ownership does not prove a prior remote
|
|
8
|
+
* request stopped; a recovered request can consume quota or bill a key again.
|
|
34
9
|
*/
|
|
35
10
|
|
|
36
11
|
const fs = require('fs');
|
|
37
12
|
const path = require('path');
|
|
38
13
|
const os = require('os');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
const bindings = require('./route-binding.js');
|
|
16
|
+
const SOURCE_ROUTES = Object.freeze({ 'claude-code': 'claude-code' });
|
|
39
17
|
|
|
40
18
|
const claudeCode = require('./claude-code.js');
|
|
41
19
|
|
|
@@ -52,6 +30,7 @@ function notInstalledProvider(id) {
|
|
|
52
30
|
usage: null,
|
|
53
31
|
reasonCode: 'provider-not-installed',
|
|
54
32
|
reason: `${id} support is not installed yet`,
|
|
33
|
+
refusal: 'pre-invocation',
|
|
55
34
|
authStatus: 'unknown',
|
|
56
35
|
};
|
|
57
36
|
},
|
|
@@ -121,6 +100,7 @@ function persistSelected(providerId, opts) {
|
|
|
121
100
|
try {
|
|
122
101
|
if (typeof byoKey.writeProvidersStateAtomic !== 'function') return; // PART C not installed yet
|
|
123
102
|
const state = readProvidersState(statePath);
|
|
103
|
+
state.origin = 'auto';
|
|
124
104
|
state.selected = providerId; // merge only this field — `byo` (if present) passes through as-read
|
|
125
105
|
byoKey.writeProvidersStateAtomic(state, opts);
|
|
126
106
|
} catch {
|
|
@@ -142,16 +122,18 @@ function persistSelected(providerId, opts) {
|
|
|
142
122
|
const defaultCache = {};
|
|
143
123
|
|
|
144
124
|
async function resolveProvider(opts = {}) {
|
|
125
|
+
const registry = opts.providerRegistry || PROVIDERS;
|
|
126
|
+
const sourceMap = opts.sourceMap || SOURCE_ROUTES;
|
|
145
127
|
const env = opts.env || process.env;
|
|
146
128
|
const override = env.AUXILO_EXTRACTION_PROVIDER;
|
|
147
129
|
if (override) {
|
|
148
|
-
if (!Object.prototype.hasOwnProperty.call(
|
|
130
|
+
if (!Object.prototype.hasOwnProperty.call(registry, override)) {
|
|
149
131
|
return {
|
|
150
132
|
ok: false,
|
|
151
133
|
reason: `AUXILO_EXTRACTION_PROVIDER="${override}" is not a known provider (expected one of: ${KNOWN_PROVIDER_IDS.join(', ')})`,
|
|
152
134
|
};
|
|
153
135
|
}
|
|
154
|
-
return { ok: true, id: override, module:
|
|
136
|
+
return { ok: true, id: override, module: registry[override] };
|
|
155
137
|
}
|
|
156
138
|
|
|
157
139
|
// Fast path (EXTRACT-PER-CLIENT W1 FIX, PUNCH-LIST P1, item 3): the
|
|
@@ -206,8 +188,8 @@ async function resolveProvider(opts = {}) {
|
|
|
206
188
|
if (cache.resolved && cache.resolved.id === 'codex-cli') delete cache.resolved;
|
|
207
189
|
|
|
208
190
|
const persistedId = persistedState.selected;
|
|
209
|
-
if (typeof persistedId === 'string' && Object.prototype.hasOwnProperty.call(
|
|
210
|
-
const persistedMod =
|
|
191
|
+
if (typeof persistedId === 'string' && Object.prototype.hasOwnProperty.call(registry, persistedId)) {
|
|
192
|
+
const persistedMod = registry[persistedId];
|
|
211
193
|
let stillUsable = false;
|
|
212
194
|
try {
|
|
213
195
|
stillUsable = await persistedMod.detect(opts);
|
|
@@ -223,8 +205,10 @@ async function resolveProvider(opts = {}) {
|
|
|
223
205
|
}
|
|
224
206
|
|
|
225
207
|
const tried = [];
|
|
226
|
-
|
|
227
|
-
|
|
208
|
+
const native = sourceMap[opts.source];
|
|
209
|
+
const order = [...new Set([...(AUTOMATIC_PROVIDER_ORDER.includes(native) ? [native] : []), ...AUTOMATIC_PROVIDER_ORDER])];
|
|
210
|
+
for (const id of order) {
|
|
211
|
+
const mod = registry[id];
|
|
228
212
|
tried.push(id);
|
|
229
213
|
let available = false;
|
|
230
214
|
try {
|
|
@@ -242,27 +226,13 @@ async function resolveProvider(opts = {}) {
|
|
|
242
226
|
return { ok: false, reason: `no extraction model provider available — tried: ${tried.join(', ')}` };
|
|
243
227
|
}
|
|
244
228
|
|
|
245
|
-
/**
|
|
246
|
-
* reasonCodes meaning "this provider cannot run at all right now" — safe to
|
|
247
|
-
* try the NEXT provider in AUTOMATIC_PROVIDER_ORDER rather than reporting a hard
|
|
248
|
-
* failure (EXTRACT-PER-CLIENT W1 FIX, PUNCH-LIST P1, item 2). Everything
|
|
249
|
-
* else (timeouts, model errors, malformed output, rate limits) means the
|
|
250
|
-
* chosen provider DID run and failed on THIS call — that is not a signal to
|
|
251
|
-
* silently switch providers out from under the builder, so those propagate
|
|
252
|
-
* as-is (a working provider that failed once is still the builder's chosen
|
|
253
|
-
* provider).
|
|
254
|
-
*/
|
|
229
|
+
/** Bounded message classification only. Reason codes never authorize fallback. */
|
|
255
230
|
const NON_RETRYABLE_FOR_THIS_PROVIDER = new Set([
|
|
256
231
|
'cli-unauthenticated',
|
|
257
232
|
'cli-not-installed',
|
|
258
233
|
'cli-billing-helper-configured',
|
|
259
234
|
'provider-not-configured',
|
|
260
235
|
'providers-file-mode-unsafe',
|
|
261
|
-
// EXTRACTION-CHILD-HOOKS (0.9.15): the resolved claude-code CLI doesn't
|
|
262
|
-
// support --setting-sources, so it can never run isolated — same
|
|
263
|
-
// "cannot run at all right now" class as the codes above, safe to try the
|
|
264
|
-
// next automatic provider rather than reporting a hard failure. codex-cli
|
|
265
|
-
// remains reachable only through the explicit override, which never walks.
|
|
266
236
|
'cli-settings-isolation-unsupported',
|
|
267
237
|
]);
|
|
268
238
|
|
|
@@ -289,51 +259,23 @@ function hasUsableIdentity(identity) {
|
|
|
289
259
|
);
|
|
290
260
|
}
|
|
291
261
|
|
|
292
|
-
/**
|
|
293
|
-
* Derive an identity for the module actually invoked as `id`, used only when
|
|
294
|
-
* that module's own result didn't already carry one. The derivation differs
|
|
295
|
-
* by outcome, because a SUCCESS identity can reach a published learning and
|
|
296
|
-
* the clean-lane calibration gate, while a FAILURE identity is purely
|
|
297
|
-
* diagnostic (extract-local.js returns before stamping anything onto a
|
|
298
|
-
* candidate on `ok:false` — see its `:824-834`):
|
|
299
|
-
*
|
|
300
|
-
* - claude-code: always gets a real, provider-specific identity — it
|
|
301
|
-
* already has `cliVersion` in hand on every returned result (success or
|
|
302
|
-
* failure), richer than the null/null/null triple this used to guess, and
|
|
303
|
-
* claude-code is the one provider documented to never self-stamp
|
|
304
|
-
* `identity` at all, so this is filling a KNOWN, structural gap, not
|
|
305
|
-
* papering over a violated contract.
|
|
306
|
-
* - Every other provider on a FAILURE: `{provider:id, model:null,
|
|
307
|
-
* version:null, vendor:null}` — naming the module we actually invoked is
|
|
308
|
-
* a plain structural fact (we chose to call it), not a guess, and it is
|
|
309
|
-
* what lets the per-run `[providers]` log name the provider that actually
|
|
310
|
-
* ran even when that provider's own failure return carries no identity
|
|
311
|
-
* (codex-cli and byo-key only self-stamp on their SINGLE success return —
|
|
312
|
-
* the automatic fall-through failure case is now: claude-code skipped,
|
|
313
|
-
* byo-key ran and failed, no identity of its own, and the label must name
|
|
314
|
-
* byo-key rather than be re-guessed as claude-code by the old fallback).
|
|
315
|
-
* - Every other provider on a SUCCESS: `{provider:'unknown', ...}` —
|
|
316
|
-
* byo-key.js and codex-cli.js both self-stamp `identity` on their only
|
|
317
|
-
* success return BY CONTRACT; a success with none means that contract was
|
|
318
|
-
* violated, so this registry has no provider-reported basis for the
|
|
319
|
-
* claim. Confidently naming the module here would look like provenance
|
|
320
|
-
* without being backed by anything the provider itself reported — since
|
|
321
|
-
* this stamp CAN reach a published learning, the fail-closed, honest
|
|
322
|
-
* answer is 'unknown', never a guess dressed up as a fact.
|
|
323
|
-
*/
|
|
262
|
+
/** Report only known provider/version facts; never infer an observed model. */
|
|
324
263
|
function deriveIdentity(id, result) {
|
|
325
264
|
if (id === 'claude-code') {
|
|
326
265
|
return {
|
|
327
266
|
provider: 'claude-code',
|
|
328
267
|
model: null,
|
|
268
|
+
requested_model: null,
|
|
269
|
+
observed_model: null,
|
|
270
|
+
identity_unresolved: 'missing',
|
|
329
271
|
version: (result && result.cliVersion) || null,
|
|
330
272
|
vendor: 'anthropic',
|
|
331
273
|
};
|
|
332
274
|
}
|
|
333
275
|
if (!(result && result.ok)) {
|
|
334
|
-
return { provider: id, model: null, version: null, vendor: null };
|
|
276
|
+
return { provider: id, model: null, requested_model: null, observed_model: null, identity_unresolved: 'missing', version: null, vendor: null };
|
|
335
277
|
}
|
|
336
|
-
return { provider: 'unknown', model: null, version: null, vendor: null };
|
|
278
|
+
return { provider: 'unknown', model: null, requested_model: null, observed_model: null, identity_unresolved: 'missing', version: null, vendor: null };
|
|
337
279
|
}
|
|
338
280
|
|
|
339
281
|
/**
|
|
@@ -352,88 +294,133 @@ function withIdentity(id, result) {
|
|
|
352
294
|
return { ...result, identity: deriveIdentity(id, result) };
|
|
353
295
|
}
|
|
354
296
|
|
|
355
|
-
/**
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
* beginning so every automatic provider still gets an actual runModel()
|
|
366
|
-
* call and contributes its own reason (not just a single generic "no
|
|
367
|
-
* provider available"). An explicit AUXILO_EXTRACTION_PROVIDER override,
|
|
368
|
-
* including codex-cli, never falls through — it is the operator's explicit
|
|
369
|
-
* choice, so its own failure reason is reported as-is. When every provider
|
|
370
|
-
* tried is exhausted, returns reasonCode
|
|
371
|
-
* 'no-usable-provider' with a bounded summary of every provider's reason in
|
|
372
|
-
* `reason` (no secrets — each provider's own reason string is already
|
|
373
|
-
* secret-free by contract) and NO identity — nothing actually ran to
|
|
374
|
-
* completion, so the caller (extract-local.js) stamps `provider:'unknown'`
|
|
375
|
-
* rather than have this registry guess one. Never throws.
|
|
376
|
-
*/
|
|
377
|
-
async function runModel(opts = {}) {
|
|
378
|
-
const mode = opts.mode === 'judge' ? 'judge' : 'extract';
|
|
379
|
-
const env = opts.env || process.env;
|
|
380
|
-
const override = env.AUXILO_EXTRACTION_PROVIDER;
|
|
381
|
-
|
|
382
|
-
if (override) {
|
|
383
|
-
const resolved = await resolveProvider(opts);
|
|
384
|
-
if (!resolved.ok) {
|
|
385
|
-
return {
|
|
386
|
-
ok: false,
|
|
387
|
-
text: '',
|
|
388
|
-
usage: null,
|
|
389
|
-
reasonCode: 'no-model-provider-available',
|
|
390
|
-
reason: resolved.reason,
|
|
391
|
-
authStatus: 'unknown',
|
|
392
|
-
};
|
|
297
|
+
/** Credential-free binding destination, calculated exactly as the adapter does. */
|
|
298
|
+
function routeMetadata(id, opts, origin) {
|
|
299
|
+
let destinationFingerprint = null;
|
|
300
|
+
let cliFingerprint = null;
|
|
301
|
+
let cliVersion = null;
|
|
302
|
+
if (id === 'byo-key') {
|
|
303
|
+
const config = byoKey.readByoConfig(opts);
|
|
304
|
+
if (config) {
|
|
305
|
+
const vendor = byoKey.resolveVendor(config.provider);
|
|
306
|
+
destinationFingerprint = bindings.hash(vendor + '\n' + byoKey.baseUrlFor(vendor, config.base_url) + '\n' + config.model);
|
|
393
307
|
}
|
|
394
|
-
|
|
395
|
-
|
|
308
|
+
} else if (id === 'claude-code') {
|
|
309
|
+
const bin = opts.claudeBin || claudeCode.resolveClaudeBin(opts);
|
|
310
|
+
cliFingerprint = bindings.hash(bin);
|
|
311
|
+
cliVersion = claudeCode.getClaudeCliVersion(bin, opts);
|
|
312
|
+
} else if (id === 'codex-cli') {
|
|
313
|
+
const bin = opts.codexBin || codexCli.resolveCodexBin(opts);
|
|
314
|
+
cliFingerprint = bindings.hash(bin);
|
|
315
|
+
// getCodexVersion is --version, never a model-capable request.
|
|
316
|
+
cliVersion = codexCli.getCodexVersion(opts);
|
|
396
317
|
}
|
|
318
|
+
return { route: id, origin, billingMode: id === 'byo-key' ? 'byo-key' : 'cli-login', destinationFingerprint, cliFingerprint, cliVersion };
|
|
319
|
+
}
|
|
397
320
|
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
? AUTOMATIC_PROVIDER_ORDER.slice()
|
|
403
|
-
: AUTOMATIC_PROVIDER_ORDER.slice(startIdx);
|
|
321
|
+
function dispositionResult(disposition) {
|
|
322
|
+
const code = disposition.hold || disposition.deferred;
|
|
323
|
+
return { ok: false, text: '', usage: null, reasonCode: code, reason: 'job route is held or deferred', authStatus: 'unknown', ...disposition };
|
|
324
|
+
}
|
|
404
325
|
|
|
326
|
+
function finishJob(context) {
|
|
327
|
+
if (!context || !context.store) return {};
|
|
328
|
+
const existing = context.store.disposition;
|
|
329
|
+
if (existing.hold || existing.deferred) return existing;
|
|
330
|
+
if (context.hold) return { hold: context.hold };
|
|
331
|
+
const result = context.store.complete();
|
|
332
|
+
return result.hold ? { hold: result.hold } : {};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function runModel(opts = {}) {
|
|
336
|
+
const mode = opts.mode === 'judge' ? 'judge' : 'extract';
|
|
337
|
+
const registry = opts.providerRegistry || PROVIDERS;
|
|
338
|
+
const context = opts.routeContext || {};
|
|
339
|
+
const standalone = !opts.routeContext;
|
|
340
|
+
if (context.hold) return dispositionResult({ hold: context.hold });
|
|
341
|
+
if (!context.store) {
|
|
342
|
+
const identity = {
|
|
343
|
+
source: opts.source || '', sessionId: opts.sessionId || opts.runId || crypto.randomUUID(),
|
|
344
|
+
jobSha: opts.jobSha || bindings.hash(String(opts.input || opts.prompt || '')),
|
|
345
|
+
};
|
|
346
|
+
context.store = bindings.createStore(identity, opts);
|
|
347
|
+
const acquired = await context.store.acquire(async () => {
|
|
348
|
+
const resolved = await resolveProvider({ ...opts, providerCache: opts.providerCache || {} });
|
|
349
|
+
const explicit = Boolean((opts.env || process.env).AUXILO_EXTRACTION_PROVIDER);
|
|
350
|
+
if (!resolved.ok && explicit) return { hold: 'pinned-route-unusable' };
|
|
351
|
+
return routeMetadata(resolved.ok ? resolved.id : 'claude-code', opts, explicit ? 'explicit' : 'auto');
|
|
352
|
+
});
|
|
353
|
+
if (acquired.hold || acquired.deferred) return dispositionResult(context.store.disposition);
|
|
354
|
+
}
|
|
355
|
+
const store = context.store;
|
|
356
|
+
if (store.disposition.hold || store.disposition.deferred) return dispositionResult(store.disposition);
|
|
357
|
+
const record = store.record;
|
|
358
|
+
const current = routeMetadata(record.route, opts, record.origin);
|
|
359
|
+
if (['destinationFingerprint', 'cliFingerprint', 'cliVersion', 'billingMode'].some(k => current[k] !== record[k])) {
|
|
360
|
+
const release = store.hold('pinned-route-changed');
|
|
361
|
+
context.hold = release.hold || 'pinned-route-changed';
|
|
362
|
+
return dispositionResult({ hold: context.hold });
|
|
363
|
+
}
|
|
364
|
+
const configuredByo = Boolean(byoKey.readByoConfig(opts));
|
|
365
|
+
// A retained BYO cache refusal rescans the ordinary order, including BYO.
|
|
366
|
+
const order = [record.route, ...AUTOMATIC_PROVIDER_ORDER.filter(id =>
|
|
367
|
+
(id !== record.route || record.route === 'byo-key') && (id !== 'byo-key' || configuredByo))];
|
|
405
368
|
const attempts = [];
|
|
369
|
+
let firstFailure = null;
|
|
406
370
|
for (const id of order) {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
371
|
+
if (id !== store.record.route) {
|
|
372
|
+
const updated = store.mutate(routeMetadata(id, opts, 'auto'));
|
|
373
|
+
if (updated.hold) return dispositionResult(store.disposition);
|
|
374
|
+
}
|
|
375
|
+
const mod = registry[id];
|
|
376
|
+
let called = false;
|
|
377
|
+
const beforeModelInvocation = () => {
|
|
378
|
+
if (called) return false;
|
|
379
|
+
called = true;
|
|
380
|
+
return store.beforeInvocation(mode, opts.timeoutMs || 120000);
|
|
381
|
+
};
|
|
382
|
+
let raw;
|
|
383
|
+
try {
|
|
384
|
+
raw = mod ? await mod.runModel({ ...opts, mode, beforeModelInvocation }) :
|
|
385
|
+
{ ok: false, text: '', usage: null, reason: 'provider is not installed', reasonCode: 'provider-not-installed', refusal: 'pre-invocation' };
|
|
386
|
+
} catch {
|
|
387
|
+
raw = { ok: false, text: '', usage: null, reason: 'provider invocation failed', reasonCode: 'unknown' };
|
|
388
|
+
}
|
|
389
|
+
let result = withIdentity(id, raw);
|
|
390
|
+
const stopped = store.disposition;
|
|
391
|
+
if (stopped.hold || stopped.deferred) return { ...result, ...stopped };
|
|
392
|
+
if (result.ok) {
|
|
393
|
+
const observed = store.observe(result.identity && result.identity.observed_model);
|
|
394
|
+
if (observed && observed.hold) return { ...result, hold: observed.hold };
|
|
395
|
+
if (standalone) result = { ...result, ...finishJob(context) };
|
|
414
396
|
return result;
|
|
415
397
|
}
|
|
416
|
-
|
|
398
|
+
if (!attempts.length) firstFailure = result;
|
|
399
|
+
attempts.push(id + '=' + (NON_RETRYABLE_FOR_THIS_PROVIDER.has(result.reasonCode) ? result.reasonCode : 'unavailable'));
|
|
400
|
+
if (result.refusal !== 'pre-invocation' || !store.canFallback()) {
|
|
401
|
+
const release = store.hold('pinned-route-unusable');
|
|
402
|
+
context.hold = release.hold || 'pinned-route-unusable';
|
|
403
|
+
return { ...result, hold: context.hold };
|
|
404
|
+
}
|
|
405
|
+
(opts.log || console.error)('[providers] ' + id + ' refused before invocation; trying next provider');
|
|
417
406
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
reasonCode: 'no-usable-provider',
|
|
430
|
-
reason: `${lastReason} (tried: ${summary})`.slice(0, 600),
|
|
431
|
-
authStatus: 'unknown',
|
|
432
|
-
};
|
|
407
|
+
const released = store.hold('pinned-route-unusable');
|
|
408
|
+
context.hold = released.hold || 'pinned-route-unusable';
|
|
409
|
+
if (!configuredByo && !attempts.some(attempt => attempt.startsWith('byo-key='))) attempts.push('byo-key=provider-not-configured');
|
|
410
|
+
// EXT-0806c: a generic 'no-usable-provider' reads as UNKNOWN downstream and never alerts. When the
|
|
411
|
+
// first-choice provider was refused for an unauthenticated CLI, the chain already knows the
|
|
412
|
+
// actionable cause, so surface it; the attempts list stays in `reason`. Read from the first
|
|
413
|
+
// attempt's result object, never from the attempts strings.
|
|
414
|
+
const unauthenticated = Boolean(firstFailure) && firstFailure.reasonCode === 'cli-unauthenticated';
|
|
415
|
+
return { ok: false, text: '', usage: null, authStatus: unauthenticated ? 'logged-out' : 'unknown',
|
|
416
|
+
reasonCode: unauthenticated ? 'cli-unauthenticated' : 'no-usable-provider',
|
|
417
|
+
reason: 'no usable extraction provider (tried: ' + attempts.join('; ') + ')', hold: context.hold };
|
|
433
418
|
}
|
|
434
419
|
|
|
435
420
|
module.exports = {
|
|
436
421
|
runModel,
|
|
422
|
+
finishJob,
|
|
423
|
+
SOURCE_ROUTES,
|
|
437
424
|
resolveProvider,
|
|
438
425
|
KNOWN_PROVIDER_IDS,
|
|
439
426
|
AUTOMATIC_PROVIDER_ORDER,
|
|
@@ -17,10 +17,8 @@
|
|
|
17
17
|
* @typedef {'extract'|'judge'} RunModelMode
|
|
18
18
|
* 'extract' (default) — draft learnings from a transcript.
|
|
19
19
|
* 'judge' — binary anchored-dedup decision against previously captured lessons.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* mode-specific behavior (e.g. codex-cli, PART B) may ignore `mode` and always use
|
|
23
|
-
* the same invocation shape.
|
|
20
|
+
* Claude Code uses the same isolated JSON transport and strict decoder in
|
|
21
|
+
* both modes. Prompt bytes and candidate parsing are separate from transport.
|
|
24
22
|
*/
|
|
25
23
|
|
|
26
24
|
/**
|
|
@@ -44,6 +42,15 @@
|
|
|
44
42
|
* @property {Function} [fetchImpl] - Injectable `fetch` replacement (HTTP providers
|
|
45
43
|
* only) — the test-injection seam.
|
|
46
44
|
* @property {RunModelMode} [mode] - 'extract' (default) or 'judge'.
|
|
45
|
+
* @property {Function} beforeModelInvocation - Required at every model-capable
|
|
46
|
+
* adapter boundary. Called exactly once immediately before spawn/fetch; only
|
|
47
|
+
* literal true authorizes it. The registry supplies the owned binding hook.
|
|
48
|
+
* Pre-invocation refusals return without consulting the hook.
|
|
49
|
+
* @property {string} [source] - Transcript source (unknown means no native route).
|
|
50
|
+
* @property {string} [sessionId] - Source session identifier.
|
|
51
|
+
* @property {string} [jobSha] - SHA256 of the exact received transcript bytes.
|
|
52
|
+
* @property {object} [routeContext] - Job-local owned binding shared by stages.
|
|
53
|
+
* @property {string} [routeBindingsDir] - Test seam only; never read from env.
|
|
47
54
|
* @property {object} [schema] - Optional JSON-Schema hint a provider MAY use to
|
|
48
55
|
* constrain its output. Claude Code ignores this — `extractJsonValue`'s
|
|
49
56
|
* fence-strip + brace-scan parser is already model-agnostic and needs no schema
|
|
@@ -63,21 +70,21 @@
|
|
|
63
70
|
* (e.g. 'cli-unauthenticated', 'cli-billing-helper-configured', 'model-error',
|
|
64
71
|
* 'isolation-precondition', 'isolation-unverified', 'isolation-violation', 'output-schema-rejected',
|
|
65
72
|
* 'unknown'). Present on both success and failure paths where applicable.
|
|
66
|
-
* @property {string|null} [reason] -
|
|
73
|
+
* @property {string|null} [reason] - Fixed classification, present when !ok;
|
|
74
|
+
* never raw output, errors, local paths or configured URLs.
|
|
75
|
+
* @property {'pre-invocation'} [refusal] - Positive pre-invocation refusal only.
|
|
76
|
+
* @property {string} [hold] - Blocks further inference, not candidate submission.
|
|
77
|
+
* @property {string} [deferred] - Ownership or recovery-wait disposition.
|
|
67
78
|
* @property {string} [authStatus] - 'logged-in' | 'logged-out' | 'unknown', when
|
|
68
79
|
* the provider has a meaningful concept of local auth state.
|
|
69
|
-
* @property {object} [identity] - {provider, model,
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* CLIENT W1 FIX GATE-A item (a) — codex-cli.js used to export this same
|
|
78
|
-
* data under the field name `extraction_model`, which nothing here ever
|
|
79
|
-
* read, so its real version silently never reached the stamp; that field
|
|
80
|
-
* name survives one more release as a deprecated alias of `identity`).
|
|
80
|
+
* @property {object} [identity] - {provider, model, requested_model,
|
|
81
|
+
* observed_model, version, vendor, identity_unresolved?}. Unknown is null.
|
|
82
|
+
* requested_model is the BYO configuration, never evidence of what ran.
|
|
83
|
+
* observed_model comes only from the vendor response or Claude canonicalModel;
|
|
84
|
+
* zero distinct canonical values means missing, several means ambiguous.
|
|
85
|
+
* Codex remains unobserved (missing). Legacy model is observed_model or null.
|
|
86
|
+
* version is the actual CLI version. Usage never implies model identity.
|
|
87
|
+
* These fields reach the local stamp; server preservation is EPC2-3's scope.
|
|
81
88
|
*/
|
|
82
89
|
|
|
83
90
|
/**
|