audit-tools 0.32.68 → 0.33.1
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 +7 -0
- package/dist/audit/cli/advanceAuditCommand.d.ts.map +1 -1
- package/dist/audit/cli/advanceAuditCommand.js +8 -3
- package/dist/audit/cli/advanceAuditCommand.js.map +1 -1
- package/dist/audit/cli/args.d.ts +9 -27
- package/dist/audit/cli/args.d.ts.map +1 -1
- package/dist/audit/cli/args.js +119 -50
- package/dist/audit/cli/args.js.map +1 -1
- package/dist/audit/cli/auditStep.d.ts +15 -1
- package/dist/audit/cli/auditStep.d.ts.map +1 -1
- package/dist/audit/cli/auditStep.js +2 -0
- package/dist/audit/cli/auditStep.js.map +1 -1
- package/dist/audit/cli/dispatch/quotaPool.d.ts +2 -1
- package/dist/audit/cli/dispatch/quotaPool.d.ts.map +1 -1
- package/dist/audit/cli/dispatch/quotaPool.js +43 -77
- package/dist/audit/cli/dispatch/quotaPool.js.map +1 -1
- package/dist/audit/cli/dispatch.d.ts.map +1 -1
- package/dist/audit/cli/dispatch.js +14 -6
- package/dist/audit/cli/dispatch.js.map +1 -1
- package/dist/audit/cli/hybridDispatch.d.ts +3 -1
- package/dist/audit/cli/hybridDispatch.d.ts.map +1 -1
- package/dist/audit/cli/hybridDispatch.js +6 -0
- package/dist/audit/cli/hybridDispatch.js.map +1 -1
- package/dist/audit/cli/nextStepCommand.d.ts +0 -9
- package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
- package/dist/audit/cli/nextStepCommand.js +173 -92
- package/dist/audit/cli/nextStepCommand.js.map +1 -1
- package/dist/audit/cli/nextStepHelpers.d.ts +11 -2
- package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
- package/dist/audit/cli/nextStepHelpers.js +56 -8
- package/dist/audit/cli/nextStepHelpers.js.map +1 -1
- package/dist/audit/cli/prepareDispatchCommand.d.ts.map +1 -1
- package/dist/audit/cli/prepareDispatchCommand.js +15 -9
- package/dist/audit/cli/prepareDispatchCommand.js.map +1 -1
- package/dist/audit/cli/prompts.d.ts +12 -32
- package/dist/audit/cli/prompts.d.ts.map +1 -1
- package/dist/audit/cli/prompts.js +19 -33
- package/dist/audit/cli/prompts.js.map +1 -1
- package/dist/audit/cli/providerConfirmationStep.d.ts +13 -5
- package/dist/audit/cli/providerConfirmationStep.d.ts.map +1 -1
- package/dist/audit/cli/providerConfirmationStep.js +47 -8
- package/dist/audit/cli/providerConfirmationStep.js.map +1 -1
- package/dist/audit/cli/quotaCommand.d.ts.map +1 -1
- package/dist/audit/cli/quotaCommand.js +16 -10
- package/dist/audit/cli/quotaCommand.js.map +1 -1
- package/dist/audit/cli/rollingAuditDispatch.d.ts.map +1 -1
- package/dist/audit/cli/rollingAuditDispatch.js +14 -1
- package/dist/audit/cli/rollingAuditDispatch.js.map +1 -1
- package/dist/audit/cli/semanticReviewStep.d.ts +11 -1
- package/dist/audit/cli/semanticReviewStep.d.ts.map +1 -1
- package/dist/audit/cli/semanticReviewStep.js +11 -15
- package/dist/audit/cli/semanticReviewStep.js.map +1 -1
- package/dist/audit/cli/validateCommand.d.ts.map +1 -1
- package/dist/audit/cli/validateCommand.js +3 -3
- package/dist/audit/cli/validateCommand.js.map +1 -1
- package/dist/audit/cli.js +3 -3
- package/dist/audit/cli.js.map +1 -1
- package/dist/audit/contracts/wrapperResponse.d.ts +6 -6
- package/dist/audit/orchestrator/advance.d.ts.map +1 -1
- package/dist/audit/orchestrator/advance.js +39 -7
- package/dist/audit/orchestrator/advance.js.map +1 -1
- package/dist/audit/orchestrator/advanceTypes.d.ts +52 -1
- package/dist/audit/orchestrator/advanceTypes.d.ts.map +1 -1
- package/dist/audit/orchestrator/executorRunners.d.ts.map +1 -1
- package/dist/audit/orchestrator/executorRunners.js +9 -1
- package/dist/audit/orchestrator/executorRunners.js.map +1 -1
- package/dist/audit/orchestrator/intakeExecutors.d.ts +27 -3
- package/dist/audit/orchestrator/intakeExecutors.d.ts.map +1 -1
- package/dist/audit/orchestrator/intakeExecutors.js +119 -19
- package/dist/audit/orchestrator/intakeExecutors.js.map +1 -1
- package/dist/audit/orchestrator/nextStep.d.ts +7 -0
- package/dist/audit/orchestrator/nextStep.d.ts.map +1 -1
- package/dist/audit/orchestrator/nextStep.js +3 -0
- package/dist/audit/orchestrator/nextStep.js.map +1 -1
- package/dist/audit/orchestrator/providerConfirmation.d.ts.map +1 -1
- package/dist/audit/orchestrator/providerConfirmation.js +0 -2
- package/dist/audit/orchestrator/providerConfirmation.js.map +1 -1
- package/dist/audit/orchestrator/rollingDispatch.d.ts.map +1 -1
- package/dist/audit/orchestrator/rollingDispatch.js +2 -0
- package/dist/audit/orchestrator/rollingDispatch.js.map +1 -1
- package/dist/audit/orchestrator/state.d.ts +17 -0
- package/dist/audit/orchestrator/state.d.ts.map +1 -1
- package/dist/audit/orchestrator/state.js +13 -1
- package/dist/audit/orchestrator/state.js.map +1 -1
- package/dist/audit/providers/claudeWorkerProvider.d.ts +10 -0
- package/dist/audit/providers/claudeWorkerProvider.d.ts.map +1 -0
- package/dist/audit/providers/claudeWorkerProvider.js +15 -0
- package/dist/audit/providers/claudeWorkerProvider.js.map +1 -0
- package/dist/audit/providers/index.d.ts.map +1 -1
- package/dist/audit/providers/index.js +2 -0
- package/dist/audit/providers/index.js.map +1 -1
- package/dist/audit/quota/index.d.ts +133 -128
- package/dist/audit/quota/index.d.ts.map +1 -1
- package/dist/audit/supervisor/sessionConfig.d.ts +3 -11
- package/dist/audit/supervisor/sessionConfig.d.ts.map +1 -1
- package/dist/audit/supervisor/sessionConfig.js +3 -25
- package/dist/audit/supervisor/sessionConfig.js.map +1 -1
- package/dist/audit/types/analyzerCapability.d.ts +6 -6
- package/dist/audit/types/externalAnalyzer.d.ts +10 -10
- package/dist/remediate/index.d.ts.map +1 -1
- package/dist/remediate/index.js +2 -2
- package/dist/remediate/index.js.map +1 -1
- package/dist/remediate/phases/plan.d.ts.map +1 -1
- package/dist/remediate/phases/plan.js +5 -4
- package/dist/remediate/phases/plan.js.map +1 -1
- package/dist/remediate/providers/claudeWorkerProvider.d.ts +11 -0
- package/dist/remediate/providers/claudeWorkerProvider.d.ts.map +1 -0
- package/dist/remediate/providers/claudeWorkerProvider.js +16 -0
- package/dist/remediate/providers/claudeWorkerProvider.js.map +1 -0
- package/dist/remediate/providers/index.d.ts.map +1 -1
- package/dist/remediate/providers/index.js +2 -0
- package/dist/remediate/providers/index.js.map +1 -1
- package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
- package/dist/remediate/steps/contractPipeline.js +9 -3
- package/dist/remediate/steps/contractPipeline.js.map +1 -1
- package/dist/remediate/steps/dispatch/acceptNode.d.ts +1 -1
- package/dist/remediate/steps/dispatch/acceptNode.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/acceptNode.js.map +1 -1
- package/dist/remediate/steps/dispatch/marshal.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/marshal.js +5 -4
- package/dist/remediate/steps/dispatch/marshal.js.map +1 -1
- package/dist/remediate/steps/dispatch/waveScheduling.d.ts +4 -2
- package/dist/remediate/steps/dispatch/waveScheduling.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/waveScheduling.js +33 -85
- package/dist/remediate/steps/dispatch/waveScheduling.js.map +1 -1
- package/dist/remediate/steps/nextStep.d.ts +21 -10
- package/dist/remediate/steps/nextStep.d.ts.map +1 -1
- package/dist/remediate/steps/nextStep.js +45 -25
- package/dist/remediate/steps/nextStep.js.map +1 -1
- package/dist/remediate/steps/sessionConfigLoad.d.ts +46 -0
- package/dist/remediate/steps/sessionConfigLoad.d.ts.map +1 -0
- package/dist/remediate/steps/sessionConfigLoad.js +39 -0
- package/dist/remediate/steps/sessionConfigLoad.js.map +1 -0
- package/dist/shared/config/resolveSessionConfig.d.ts +75 -0
- package/dist/shared/config/resolveSessionConfig.d.ts.map +1 -0
- package/dist/shared/config/resolveSessionConfig.js +125 -0
- package/dist/shared/config/resolveSessionConfig.js.map +1 -0
- package/dist/shared/dispatch/admissionLoop.d.ts +12 -12
- package/dist/shared/dispatch/coordinator.d.ts.map +1 -1
- package/dist/shared/dispatch/coordinator.js +52 -6
- package/dist/shared/dispatch/coordinator.js.map +1 -1
- package/dist/shared/dispatch/hybridDispatch.d.ts +6 -0
- package/dist/shared/dispatch/hybridDispatch.d.ts.map +1 -1
- package/dist/shared/dispatch/hybridDispatch.js +6 -0
- package/dist/shared/dispatch/hybridDispatch.js.map +1 -1
- package/dist/shared/dispatch/providerLaunchFinalize.d.ts +2 -1
- package/dist/shared/dispatch/providerLaunchFinalize.d.ts.map +1 -1
- package/dist/shared/dispatch/providerLaunchFinalize.js +123 -68
- package/dist/shared/dispatch/providerLaunchFinalize.js.map +1 -1
- package/dist/shared/dispatch/rollingDispatch.d.ts +69 -2
- package/dist/shared/dispatch/rollingDispatch.d.ts.map +1 -1
- package/dist/shared/dispatch/rollingDispatch.js +177 -7
- package/dist/shared/dispatch/rollingDispatch.js.map +1 -1
- package/dist/shared/dispatch/unifiedRolling.d.ts +23 -0
- package/dist/shared/dispatch/unifiedRolling.d.ts.map +1 -1
- package/dist/shared/dispatch/unifiedRolling.js +2 -0
- package/dist/shared/dispatch/unifiedRolling.js.map +1 -1
- package/dist/shared/friction/stepBoundaryCapture.d.ts +52 -1
- package/dist/shared/friction/stepBoundaryCapture.d.ts.map +1 -1
- package/dist/shared/friction/stepBoundaryCapture.js +87 -0
- package/dist/shared/friction/stepBoundaryCapture.js.map +1 -1
- package/dist/shared/index.d.ts +20 -10
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +20 -6
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/io/stateDir.d.ts +27 -0
- package/dist/shared/io/stateDir.d.ts.map +1 -0
- package/dist/shared/io/stateDir.js +40 -0
- package/dist/shared/io/stateDir.js.map +1 -0
- package/dist/shared/providers/auditorSources.d.ts +169 -0
- package/dist/shared/providers/auditorSources.d.ts.map +1 -0
- package/dist/shared/providers/auditorSources.js +453 -0
- package/dist/shared/providers/auditorSources.js.map +1 -0
- package/dist/shared/providers/claudeWorkerProvider.d.ts +71 -0
- package/dist/shared/providers/claudeWorkerProvider.d.ts.map +1 -0
- package/dist/shared/providers/claudeWorkerProvider.js +133 -0
- package/dist/shared/providers/claudeWorkerProvider.js.map +1 -0
- package/dist/shared/providers/providerConfirmation.d.ts +3 -20
- package/dist/shared/providers/providerConfirmation.d.ts.map +1 -1
- package/dist/shared/providers/providerConfirmation.js +27 -40
- package/dist/shared/providers/providerConfirmation.js.map +1 -1
- package/dist/shared/providers/providerFactory.d.ts +13 -1
- package/dist/shared/providers/providerFactory.d.ts.map +1 -1
- package/dist/shared/providers/providerFactory.js +12 -0
- package/dist/shared/providers/providerFactory.js.map +1 -1
- package/dist/shared/providers/proxyCatalog.d.ts +95 -0
- package/dist/shared/providers/proxyCatalog.d.ts.map +1 -0
- package/dist/shared/providers/proxyCatalog.js +426 -0
- package/dist/shared/providers/proxyCatalog.js.map +1 -0
- package/dist/shared/providers/sharedProviderConfirmation.d.ts +264 -75
- package/dist/shared/providers/sharedProviderConfirmation.d.ts.map +1 -1
- package/dist/shared/providers/sharedProviderConfirmation.js +534 -113
- package/dist/shared/providers/sharedProviderConfirmation.js.map +1 -1
- package/dist/shared/quota/apiPool.d.ts +34 -11
- package/dist/shared/quota/apiPool.d.ts.map +1 -1
- package/dist/shared/quota/apiPool.js +52 -23
- package/dist/shared/quota/apiPool.js.map +1 -1
- package/dist/shared/quota/capacity.d.ts +79 -50
- package/dist/shared/quota/capacity.d.ts.map +1 -1
- package/dist/shared/quota/capacity.js +40 -5
- package/dist/shared/quota/capacity.js.map +1 -1
- package/dist/shared/quota/errorParsing.d.ts +39 -0
- package/dist/shared/quota/errorParsing.d.ts.map +1 -1
- package/dist/shared/quota/errorParsing.js +76 -0
- package/dist/shared/quota/errorParsing.js.map +1 -1
- package/dist/shared/quota/hostPool.d.ts +107 -0
- package/dist/shared/quota/hostPool.d.ts.map +1 -0
- package/dist/shared/quota/hostPool.js +103 -0
- package/dist/shared/quota/hostPool.js.map +1 -0
- package/dist/shared/quota/quotaSource.d.ts +2 -2
- package/dist/shared/types/auditorDescriptor.d.ts +120 -0
- package/dist/shared/types/auditorDescriptor.d.ts.map +1 -0
- package/dist/shared/types/auditorDescriptor.js +28 -0
- package/dist/shared/types/auditorDescriptor.js.map +1 -0
- package/dist/shared/types/providerConfirmation.d.ts +64 -6
- package/dist/shared/types/providerConfirmation.d.ts.map +1 -1
- package/dist/shared/types/providerConfirmation.js +1 -1
- package/dist/shared/types/rollingDispatch.d.ts +45 -1
- package/dist/shared/types/rollingDispatch.d.ts.map +1 -1
- package/dist/shared/types/sessionConfig.d.ts +149 -67
- package/dist/shared/types/sessionConfig.d.ts.map +1 -1
- package/dist/shared/types/sessionConfig.js +87 -2
- package/dist/shared/types/sessionConfig.js.map +1 -1
- package/dist/shared/validation/sessionConfig.d.ts +26 -14
- package/dist/shared/validation/sessionConfig.d.ts.map +1 -1
- package/dist/shared/validation/sessionConfig.js +95 -69
- package/dist/shared/validation/sessionConfig.js.map +1 -1
- package/docs/audit-pkg/contracts.md +4 -4
- package/docs/audit-pkg/operator-guide.md +43 -24
- package/package.json +1 -1
- package/skills/audit-code/SKILL.md +17 -14
- package/skills/audit-code/audit-code.prompt.md +17 -16
- package/wrapper/audit-code-wrapper-install-hosts.mjs +6 -10
- package/wrapper/remediate-code-wrapper-install-hosts.mjs +4 -8
- package/dist/shared/quota/repairProxyRegistry.d.ts +0 -84
- package/dist/shared/quota/repairProxyRegistry.d.ts.map +0 -1
- package/dist/shared/quota/repairProxyRegistry.js +0 -144
- package/dist/shared/quota/repairProxyRegistry.js.map +0 -1
|
@@ -2,41 +2,51 @@
|
|
|
2
2
|
* DC-2 — shared, session-scoped provider confirmation (Gate-0).
|
|
3
3
|
*
|
|
4
4
|
* The design wants ONE provider confirmation spanning an audit→remediate run:
|
|
5
|
-
* the first tool to run writes the confirmed
|
|
6
|
-
* at `<root>/.audit-tools/provider-confirmation.json` (NOT the
|
|
7
|
-
* artifacts dir); the second tool reads and honors it
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
5
|
+
* the first tool to run writes the operator's confirmed route DECISION to a
|
|
6
|
+
* SHARED artifact at `<root>/.audit-tools/provider-confirmation.json` (NOT the
|
|
7
|
+
* per-tool audit artifacts dir); the second tool reads and honors it. "Session" =
|
|
8
|
+
* the shared `.audit-tools` dir for that repo+run, so no new identity scheme is
|
|
9
|
+
* needed.
|
|
10
|
+
*
|
|
11
|
+
* **What this artifact carries is POLICY, not reach (G3).** The operator's
|
|
12
|
+
* decision — exclusions, cost order, λ — is a set of *rules*, valid for any
|
|
13
|
+
* auditor. What is *reachable* is per-auditor capability and is re-resolved from
|
|
14
|
+
* live env/PATH at the moment of use, never inherited from whoever wrote this
|
|
15
|
+
* file. So every read here is reach-free: it returns the persisted decision and
|
|
16
|
+
* nothing else.
|
|
17
|
+
*
|
|
18
|
+
* INV-DC1-6 (never-block) is the only invariant left in tension, and it now
|
|
19
|
+
* resolves to a plain two-valued read: a remediate run standalone with no prior
|
|
20
|
+
* audit resolves its provider independently — absence or corruption of the
|
|
21
|
+
* artifact is NOT an error, it is `null`.
|
|
22
|
+
*
|
|
23
|
+
* The former roster-staleness check (and its CE-012 three-valued `reconfirm`
|
|
24
|
+
* state) is GONE. It compared the *writing* auditor's roster against the reader's
|
|
25
|
+
* — meaningless cross-auditor by construction — and answered a real event (a
|
|
26
|
+
* backend the operator never confirmed became reachable) by silently discarding
|
|
27
|
+
* the operator's cost order and λ, while reaching no obligation at all. The
|
|
28
|
+
* `autonomous_mode`-keyed reconciliation gate
|
|
29
|
+
* ({@link computeNewlyReachableBackends}) replaces it: it compares the operator's
|
|
30
|
+
* DECISION against *this* auditor's freshly-resolved reach, which is well-defined
|
|
31
|
+
* across auditors, and it actually fires.
|
|
24
32
|
*
|
|
25
33
|
* CE-003 (lockless read races the writer rename): writes go through the shared
|
|
26
34
|
* atomic writer (temp + atomic rename) under `withFileLock`, so a lockless
|
|
27
35
|
* reader always observes either the complete old file or the complete new file —
|
|
28
36
|
* never a torn intermediate.
|
|
29
37
|
*
|
|
30
|
-
* PB-1 (opencode opt-in): the
|
|
31
|
-
* already withholds a bare-PATH opencode unless it is explicitly
|
|
32
|
-
* the shared confirmation inherits that opt-in for free.
|
|
38
|
+
* PB-1 (opencode opt-in): the confirmed pool is derived from `discoverProviders`,
|
|
39
|
+
* which already withholds a bare-PATH opencode unless it is explicitly
|
|
40
|
+
* configured, so the shared confirmation inherits that opt-in for free.
|
|
33
41
|
*/
|
|
34
|
-
import { mkdir } from "node:fs/promises";
|
|
42
|
+
import { mkdir, unlink } from "node:fs/promises";
|
|
35
43
|
import { join } from "node:path";
|
|
44
|
+
import { PROVIDER_NAMES } from "../types/sessionConfig.js";
|
|
45
|
+
import { isSelfSpawnBlocked } from "./providerPathGuard.js";
|
|
36
46
|
import { auditToolsDir } from "../io/auditToolsPaths.js";
|
|
37
47
|
import { readJsonFile, writeJsonFile } from "../io/json.js";
|
|
38
48
|
import { withFileLock } from "../quota/fileLock.js";
|
|
39
|
-
import { discoverProviders, annotateConfirmedPool, } from "./providerConfirmation.js";
|
|
49
|
+
import { discoverProviders, annotateConfirmedPool, representativeModelId, } from "./providerConfirmation.js";
|
|
40
50
|
import { resolveConfirmedCostPositions } from "../dispatch/costRank.js";
|
|
41
51
|
import { PROVIDER_CONFIRMATION_INPUT_VERSION } from "../types/providerConfirmation.js";
|
|
42
52
|
// ---------------------------------------------------------------------------
|
|
@@ -45,8 +55,8 @@ import { PROVIDER_CONFIRMATION_INPUT_VERSION } from "../types/providerConfirmati
|
|
|
45
55
|
/**
|
|
46
56
|
* Schema version for the shared confirmation artifact. Bumped independently of
|
|
47
57
|
* the per-tool seam contract (PROVIDER_CONFIRMATION_RESULT_VERSION) — this is
|
|
48
|
-
* the cross-tool session artifact
|
|
49
|
-
*
|
|
58
|
+
* the cross-tool session artifact carrying the operator's route DECISION
|
|
59
|
+
* (exclusions, cost order, λ), a distinct shape from the seam's pool snapshot.
|
|
50
60
|
*/
|
|
51
61
|
export const SHARED_PROVIDER_CONFIRMATION_VERSION = "1.0.0";
|
|
52
62
|
/**
|
|
@@ -65,33 +75,37 @@ export const SHARED_PROVIDER_CONFIRMATION_FILENAME = "provider-confirmation.json
|
|
|
65
75
|
export function sharedProviderConfirmationPath(root) {
|
|
66
76
|
return join(auditToolsDir(root), SHARED_PROVIDER_CONFIRMATION_FILENAME);
|
|
67
77
|
}
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
// Roster derivation
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
78
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* the
|
|
79
|
+
* Project the render DTO down to what actually gets PERSISTED: the operator's
|
|
80
|
+
* decision, with this auditor's reach assessment dropped (G3 B+D).
|
|
81
|
+
*
|
|
82
|
+
* The PRODUCER is split — not the write site. `writeSharedProviderConfirmation`
|
|
83
|
+
* receives an already-typed value, so projecting THERE would leave the reach fields
|
|
84
|
+
* representable on the persisted type and a future caller could put them back.
|
|
85
|
+
* Projecting here makes the persisted shape carry no reach BY CONSTRUCTION.
|
|
77
86
|
*/
|
|
78
|
-
export function
|
|
79
|
-
const
|
|
80
|
-
return
|
|
87
|
+
export function buildSharedProviderConfirmation(...args) {
|
|
88
|
+
const rendered = buildProviderConfirmationRender(...args);
|
|
89
|
+
return {
|
|
90
|
+
...rendered,
|
|
91
|
+
provider_pool: rendered.provider_pool.map(toPersistedPoolEntry),
|
|
92
|
+
};
|
|
81
93
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
/** The decision half of one pool entry. Reach is dropped, never persisted. */
|
|
95
|
+
function toPersistedPoolEntry(entry) {
|
|
96
|
+
return {
|
|
97
|
+
name: entry.name,
|
|
98
|
+
...(entry.model_id !== undefined ? { model_id: entry.model_id } : {}),
|
|
99
|
+
...(entry.cost_order !== undefined ? { cost_order: entry.cost_order } : {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function sortNames(names) {
|
|
103
|
+
// Deduplicate + sort so a persisted list is order-insensitive and stable.
|
|
85
104
|
return [...new Set(names)].sort();
|
|
86
105
|
}
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
for (let i = 0; i < a.length; i++) {
|
|
91
|
-
if (a[i] !== b[i])
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
return true;
|
|
106
|
+
function sortStrings(values) {
|
|
107
|
+
// Deduplicate + sort so a persisted list is order-insensitive and stable.
|
|
108
|
+
return [...new Set(values)].sort();
|
|
95
109
|
}
|
|
96
110
|
// ---------------------------------------------------------------------------
|
|
97
111
|
// Build
|
|
@@ -113,7 +127,10 @@ function rostersEqual(a, b) {
|
|
|
113
127
|
*
|
|
114
128
|
* @param sessionConfig - Current session config; may be an empty `{}`.
|
|
115
129
|
* @param env - Process env snapshot; defaults to `process.env`.
|
|
116
|
-
* @param exclude -
|
|
130
|
+
* @param exclude - {@link DispatchExclusionPattern}s to pre-exclude (from a
|
|
131
|
+
* prior gate). A `provider:model` pattern marks a pool entry excluded only when
|
|
132
|
+
* that entry's `representativeModelId` IS that model — the same key the routing
|
|
133
|
+
* filter matches on, so display and dispatch cannot disagree.
|
|
117
134
|
* @param include - Provider names the operator explicitly opts back IN,
|
|
118
135
|
* overriding the default self-spawn-blocked exclusion for those names.
|
|
119
136
|
* @param detectCommand - Injectable PATH-detection hook, forwarded to
|
|
@@ -126,10 +143,26 @@ function rostersEqual(a, b) {
|
|
|
126
143
|
* passed via the dedicated params above (the executor forwards them from the
|
|
127
144
|
* same input), so this arg governs ordering + host roster only.
|
|
128
145
|
*/
|
|
129
|
-
export function
|
|
146
|
+
export function buildProviderConfirmationRender(sessionConfig = {}, env = process.env, exclude = [], include = [], detectCommand, input, sources = []) {
|
|
130
147
|
const discovered = discoverProviders(sessionConfig, env, detectCommand);
|
|
131
|
-
const
|
|
148
|
+
const operatorExcluded = buildExclusion(exclude);
|
|
132
149
|
const includeSet = new Set(include);
|
|
150
|
+
// Evaluate a pool entry against the operator's rules at the SAME key the routing
|
|
151
|
+
// filter uses — `representativeModelId` is what `computeNewlyReachableBackends`
|
|
152
|
+
// and `annotateConfirmedPool` key a provider by, so a `provider:model` rule marks
|
|
153
|
+
// exactly the entry it will later filter.
|
|
154
|
+
//
|
|
155
|
+
// ⚠ The provider + model tiers ONLY. `provider_pool` is provider-granular and an
|
|
156
|
+
// entry has no endpoint, so an endpoint-host rule can never mark one — that tier
|
|
157
|
+
// addresses SOURCES, which this pool does not enumerate. The direction is safe
|
|
158
|
+
// (the rule is still honored at `buildSourcePools`; the Gate-0 table merely
|
|
159
|
+
// under-reports it as "included"), but it is a real display/routing gap and it is
|
|
160
|
+
// NOT closed here. Backlog: the Gate-0 sources table carries no status column at
|
|
161
|
+
// all, so no tier is reflected for a source today.
|
|
162
|
+
const ruledOut = (name) => operatorExcluded.excludes({
|
|
163
|
+
provider: name,
|
|
164
|
+
model: representativeModelId(name, sessionConfig),
|
|
165
|
+
});
|
|
133
166
|
const pool = [];
|
|
134
167
|
// Always include worker-command as a fallback — it's always available and is
|
|
135
168
|
// never surfaced by PATH discovery (it blocks auto-dispatch by design).
|
|
@@ -137,24 +170,21 @@ export function buildSharedProviderConfirmation(sessionConfig = {}, env = proces
|
|
|
137
170
|
pool.push({
|
|
138
171
|
name: "worker-command",
|
|
139
172
|
capability_tier: "unknown",
|
|
140
|
-
excluded:
|
|
141
|
-
reason: "always-available fallback; no PATH detection required",
|
|
173
|
+
excluded: ruledOut("worker-command"),
|
|
142
174
|
});
|
|
143
175
|
}
|
|
144
176
|
for (const provider of discovered) {
|
|
145
177
|
// Self-spawn-blocked providers are excluded from the dispatchable pool by
|
|
146
178
|
// default; the operator can opt one back in via `include`. An operator-named
|
|
147
|
-
// `exclude` always wins.
|
|
148
|
-
// consumers never have to parse `reason`.
|
|
179
|
+
// `exclude` always wins.
|
|
149
180
|
const blocked = provider.selfSpawnBlocked === true;
|
|
150
181
|
const operatorIncluded = includeSet.has(provider.name);
|
|
151
|
-
const excluded =
|
|
182
|
+
const excluded = ruledOut(provider.name) || (blocked && !operatorIncluded);
|
|
152
183
|
pool.push({
|
|
153
184
|
name: provider.name,
|
|
154
185
|
capability_tier: provider.capabilityTier,
|
|
155
186
|
excluded,
|
|
156
187
|
...(blocked ? { self_spawn_blocked: true } : {}),
|
|
157
|
-
reason: provider.reason,
|
|
158
188
|
});
|
|
159
189
|
}
|
|
160
190
|
// Cost-first routing: annotate with representative model price + cost_order,
|
|
@@ -166,6 +196,8 @@ export function buildSharedProviderConfirmation(sessionConfig = {}, env = proces
|
|
|
166
196
|
session_level: true,
|
|
167
197
|
confirmed_at: new Date().toISOString(),
|
|
168
198
|
provider_pool: annotated.provider_pool,
|
|
199
|
+
// NOTE: the FULL entries. This is the render DTO; `buildSharedProviderConfirmation`
|
|
200
|
+
// projects them to `PersistedPoolEntry[]` on the way to disk.
|
|
169
201
|
...(annotated.host_model_cost_order.length > 0
|
|
170
202
|
? { host_model_cost_order: annotated.host_model_cost_order }
|
|
171
203
|
: {}),
|
|
@@ -175,9 +207,309 @@ export function buildSharedProviderConfirmation(sessionConfig = {}, env = proces
|
|
|
175
207
|
...(clampDispatchBias(input?.dispatch_bias) != null
|
|
176
208
|
? { dispatch_bias: clampDispatchBias(input?.dispatch_bias) }
|
|
177
209
|
: {}),
|
|
178
|
-
|
|
210
|
+
...(buildConfirmedDispatchPolicy(exclude, include) ?? {}),
|
|
179
211
|
};
|
|
180
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Lift the operator's explicit route decision out of their Gate-0 input into the
|
|
215
|
+
* persisted policy half. Returns `undefined` when the operator named neither list
|
|
216
|
+
* (so the field stays absent rather than persisting an empty shell).
|
|
217
|
+
*/
|
|
218
|
+
function buildConfirmedDispatchPolicy(exclude, include) {
|
|
219
|
+
if (exclude.length === 0 && include.length === 0)
|
|
220
|
+
return undefined;
|
|
221
|
+
return {
|
|
222
|
+
policy: {
|
|
223
|
+
...(exclude.length > 0 ? { exclude: sortStrings(exclude) } : {}),
|
|
224
|
+
...(include.length > 0 ? { include: sortNames(include) } : {}),
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Keep every non-empty pattern verbatim — **no membership check.** Unlike
|
|
230
|
+
* {@link parseProviderNameList}, this list is an open grammar: a pattern whose head
|
|
231
|
+
* is not a provider name is a legitimate endpoint-host rule, so "unknown ⇒ drop"
|
|
232
|
+
* would silently delete the operator's endpoint tier. An unmatchable pattern is
|
|
233
|
+
* inert (it simply matches nothing), which is the safe direction for a filter.
|
|
234
|
+
*
|
|
235
|
+
* Returns `undefined` for a non-array or an all-empty array, so the field stays
|
|
236
|
+
* absent rather than persisting an empty shell.
|
|
237
|
+
*/
|
|
238
|
+
function parseExclusionPatterns(value) {
|
|
239
|
+
if (!Array.isArray(value))
|
|
240
|
+
return undefined;
|
|
241
|
+
const patterns = value.filter((v) => typeof v === "string" && v.trim().length > 0);
|
|
242
|
+
return patterns.length > 0 ? patterns.map((p) => p.trim()) : undefined;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Keep only real provider names, dropping anything unknown. Returns `undefined` for a
|
|
246
|
+
* non-array or an array with no recognizable name, so an unknown entry degrades that
|
|
247
|
+
* entry — never the whole list.
|
|
248
|
+
*
|
|
249
|
+
* Retained for `include` ONLY: that list opts a *self-spawn-blocked provider* back in,
|
|
250
|
+
* and self-spawn-blockedness is a property of a provider (`isSelfSpawnBlocked` keys on
|
|
251
|
+
* the provider name), so its keyspace is genuinely the closed name set — not the open
|
|
252
|
+
* exclusion grammar.
|
|
253
|
+
*/
|
|
254
|
+
function parseProviderNameList(value) {
|
|
255
|
+
if (!Array.isArray(value))
|
|
256
|
+
return undefined;
|
|
257
|
+
const names = value.filter((v) => typeof v === "string" && RESOLVED_PROVIDER_NAMES.includes(v));
|
|
258
|
+
return names.length > 0 ? names : undefined;
|
|
259
|
+
}
|
|
260
|
+
/** Every concrete provider name — `auto` is a resolution directive, not a backend. */
|
|
261
|
+
const RESOLVED_PROVIDER_NAMES = PROVIDER_NAMES.filter((name) => name !== "auto");
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// The reconciliation gate (G3): operator DECISION vs THIS auditor's reach
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
/**
|
|
266
|
+
* The gate's comparison key for one backend: **the model where it is knowable,
|
|
267
|
+
* else the coarse provider name.**
|
|
268
|
+
*
|
|
269
|
+
* This is the operator-facing exclusion grammar's own provision (`provider:model`,
|
|
270
|
+
* with `provider` as the coarser pattern — spec/unified-dispatch-worker-model.md).
|
|
271
|
+
* Model granularity is the POINT, not a refinement: the operator confirms *model*
|
|
272
|
+
* choices, so a second model under an already-confirmed `openai-compatible` is a
|
|
273
|
+
* new choice even though it introduces no new provider.
|
|
274
|
+
*
|
|
275
|
+
* But a bare-`model_id` key would be worse than useless: `representativeModelId`
|
|
276
|
+
* knows a model only for `openai-compatible` and `codex` — for claude-code / agy /
|
|
277
|
+
* opencode / worker-command a CLI backend's model arrives only at the dispatch
|
|
278
|
+
* handshake. Such a backend would contribute NO key, so installing `agy` on PATH
|
|
279
|
+
* would leave the delta empty and the gate would silently dispatch it — re-opening
|
|
280
|
+
* the exact PATH-appearance case the gate exists to catch, blind rather than loud.
|
|
281
|
+
*
|
|
282
|
+
* ⚠ This is a THIRD keyspace, deliberately distinct from the quota-ledger pool
|
|
283
|
+
* identity (`provider[#account]/model`, `buildProviderModelKey`) — an account is
|
|
284
|
+
* irrelevant to a rule about a backend. Do not unify them.
|
|
285
|
+
*/
|
|
286
|
+
function backendGateKey(modelId, providerName) {
|
|
287
|
+
return modelId ?? providerName;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* The keys of the operator's persisted DECISION — the CONFIRMED half of the gate.
|
|
291
|
+
*
|
|
292
|
+
* All three pools contribute, and each is load-bearing: `annotateConfirmedPool`
|
|
293
|
+
* folds a source away when its model is already claimed by a provider entry, so a
|
|
294
|
+
* source can be represented ONLY by `provider_pool[].model_id`; and a host tier
|
|
295
|
+
* appears only in `host_model_cost_order`. Reading fewer than all three would
|
|
296
|
+
* manufacture a phantom delta for an already-confirmed backend.
|
|
297
|
+
*/
|
|
298
|
+
export function confirmedBackendKeys(confirmation) {
|
|
299
|
+
const keys = new Set();
|
|
300
|
+
for (const entry of confirmation.provider_pool) {
|
|
301
|
+
keys.add(backendGateKey(entry.model_id, entry.name));
|
|
302
|
+
}
|
|
303
|
+
for (const entry of confirmation.source_pool_cost_order ?? []) {
|
|
304
|
+
keys.add(backendGateKey(entry.model_id, entry.provider));
|
|
305
|
+
}
|
|
306
|
+
for (const entry of confirmation.host_model_cost_order ?? []) {
|
|
307
|
+
keys.add(entry.model_id);
|
|
308
|
+
}
|
|
309
|
+
return keys;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* The pattern that rules out one backend at the finest granularity its model is
|
|
313
|
+
* knowable at. The inverse of {@link backendGateKey}, and deliberately adjacent to
|
|
314
|
+
* it: a `provider:model` rule matches only when the routing filter sees that exact
|
|
315
|
+
* model, so a backend whose model arrives at the dispatch handshake (a CLI) must be
|
|
316
|
+
* ruled out at the coarse `provider` tier or the rule would never match.
|
|
317
|
+
*/
|
|
318
|
+
function backendExclusionPattern(modelId, providerName) {
|
|
319
|
+
return modelId ? `${providerName}:${modelId}` : providerName;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* DELTA = **REACH-NOW \ CONFIRMED**: the backends this auditor can reach *right
|
|
323
|
+
* now* that the operator's persisted decision never mentions. Sorted by key, so the
|
|
324
|
+
* result is stable for prompt rendering + comparison.
|
|
325
|
+
*
|
|
326
|
+
* This is a **set difference — a FILTER over fresh reach, never additive.** The
|
|
327
|
+
* opposite direction (CONFIRMED \ REACH-NOW: a backend the operator confirmed that
|
|
328
|
+
* this auditor cannot reach) is the harmless *subset* case and is deliberately
|
|
329
|
+
* silent — it is also why the synthetic `worker-command` entry and
|
|
330
|
+
* `host_model_cost_order` need no special-casing here.
|
|
331
|
+
*
|
|
332
|
+
* @param confirmation - The persisted decision (CONFIRMED).
|
|
333
|
+
* @param sessionConfig - The EFFECTIVE config, so `representativeModelId` derives
|
|
334
|
+
* keys identically to the write side.
|
|
335
|
+
* @param sources - REACH-NOW's source half. MUST come from the
|
|
336
|
+
* `gatherDispatchableSources` chokepoint — the single async source-gather point
|
|
337
|
+
* both `buildSourcePools` and the Gate-0 surface consume, so what the operator
|
|
338
|
+
* confirms is exactly what routes. Re-deriving it from `resolveAmbientSources`
|
|
339
|
+
* would reintroduce the display/dispatch drift that invariant forbids, and is
|
|
340
|
+
* structurally blind to descriptor-supplied sources, the demoted primary, and the
|
|
341
|
+
* legacy `openai_compatible` fold.
|
|
342
|
+
* @param env - Process env, for `discoverProviders` (REACH-NOW's provider half).
|
|
343
|
+
* @param detectCommand - Injectable PATH-detection hook so tests drive discovery
|
|
344
|
+
* deterministically instead of shelling out.
|
|
345
|
+
*/
|
|
346
|
+
export function computeNewlyReachableBackends(confirmation, sessionConfig, sources = [], env = process.env, detectCommand) {
|
|
347
|
+
const confirmed = confirmedBackendKeys(confirmation);
|
|
348
|
+
const reachNow = new Map();
|
|
349
|
+
const record = (modelId, provider) => {
|
|
350
|
+
reachNow.set(backendGateKey(modelId, provider), {
|
|
351
|
+
key: backendGateKey(modelId, provider),
|
|
352
|
+
provider,
|
|
353
|
+
exclusion_pattern: backendExclusionPattern(modelId, provider),
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
for (const provider of discoverProviders(sessionConfig, env, detectCommand)) {
|
|
357
|
+
record(representativeModelId(provider.name, sessionConfig), provider.name);
|
|
358
|
+
}
|
|
359
|
+
for (const source of sources) {
|
|
360
|
+
record(source.model, source.provider);
|
|
361
|
+
}
|
|
362
|
+
return [...reachNow.values()]
|
|
363
|
+
.filter((backend) => !confirmed.has(backend.key))
|
|
364
|
+
.sort((a, b) => a.key.localeCompare(b.key));
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* The dispatchable-pool exclusion rules for THIS process: the operator's explicit
|
|
368
|
+
* {@link DispatchExclusionPattern}s, plus every provider that is self-spawn-blocked
|
|
369
|
+
* *in this process's env* and was not explicitly opted back in.
|
|
370
|
+
*
|
|
371
|
+
* Reach is recomputed here rather than read from the artifact's derived `excluded`
|
|
372
|
+
* flag — that flag encodes the WRITING auditor's env, and an auditor for whom a
|
|
373
|
+
* provider is perfectly spawnable must not inherit another's block. The operator's
|
|
374
|
+
* decision is inherited (it is a rule); the reach assessment is not.
|
|
375
|
+
*
|
|
376
|
+
* ⚠ **These rules are only safe to apply to SOURCE pools.** Inside any agent session
|
|
377
|
+
* the self-spawn half ALWAYS names that agent (`CLAUDECODE` ⇒ `claude-code`, `CODEX`
|
|
378
|
+
* ⇒ `codex`) — i.e. the conversation host itself. Applying them to HOST pools would
|
|
379
|
+
* zero out dispatch entirely: the driver would exclude itself. It is harmless at
|
|
380
|
+
* `buildSourcePools` only because a host can never BE a source — `claude-code` is
|
|
381
|
+
* structurally absent from `DISPATCHABLE_SOURCE_PROVIDERS`, so in a Claude Code
|
|
382
|
+
* session the filter is a no-op. Honoring an operator exclusion of the host/primary
|
|
383
|
+
* provider therefore is NOT a matter of passing these rules to the host-pool builder;
|
|
384
|
+
* it needs a separate decision about what excluding your own driver should even mean.
|
|
385
|
+
*/
|
|
386
|
+
export function resolveDispatchExclusion(policy, env = process.env) {
|
|
387
|
+
const included = new Set(policy?.include ?? []);
|
|
388
|
+
// The local reach half: a self-spawn-blocked provider is ruled out at PROVIDER
|
|
389
|
+
// granularity (blockedness is a property of the provider, not of one of its
|
|
390
|
+
// models), recomputed against THIS process's env rather than inherited.
|
|
391
|
+
const blocked = RESOLVED_PROVIDER_NAMES.filter((name) => !included.has(name) && isSelfSpawnBlocked(name, env));
|
|
392
|
+
return buildExclusion([...(policy?.exclude ?? []), ...blocked]);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* The pure-policy matcher: the operator's patterns and nothing else. Split out
|
|
396
|
+
* because the Gate-0 pool BUILDER must evaluate the operator's rules WITHOUT the
|
|
397
|
+
* local self-spawn fold — it derives `self_spawn_blocked` from `discoverProviders`
|
|
398
|
+
* on its own and would otherwise conflate the two into one indistinguishable
|
|
399
|
+
* `excluded` verdict.
|
|
400
|
+
*/
|
|
401
|
+
function buildExclusion(patterns) {
|
|
402
|
+
const rules = patterns.map(parseExclusionRule);
|
|
403
|
+
return { excludes: (backend) => rules.some((rule) => ruleMatches(rule, backend)) };
|
|
404
|
+
}
|
|
405
|
+
function parseExclusionRule(pattern) {
|
|
406
|
+
const colon = pattern.indexOf(":");
|
|
407
|
+
if (colon === -1) {
|
|
408
|
+
return isResolvedProviderName(pattern)
|
|
409
|
+
? { kind: "provider", provider: pattern }
|
|
410
|
+
: { kind: "endpoint", host: pattern.toLowerCase() };
|
|
411
|
+
}
|
|
412
|
+
const head = pattern.slice(0, colon);
|
|
413
|
+
const tail = pattern.slice(colon + 1);
|
|
414
|
+
if (!isResolvedProviderName(head)) {
|
|
415
|
+
return { kind: "endpoint", host: pattern.toLowerCase() };
|
|
416
|
+
}
|
|
417
|
+
// The head decides the tier — an empty tail does NOT demote a provider-name head
|
|
418
|
+
// to the endpoint tier. `codex:` reads as "codex, every model"; classifying it as
|
|
419
|
+
// an (unmatchable) endpoint rule would silently drop the operator's intent, and
|
|
420
|
+
// the head-decides rule this type documents would not actually hold.
|
|
421
|
+
return tail.length > 0
|
|
422
|
+
? { kind: "provider_model", provider: head, model: tail }
|
|
423
|
+
: { kind: "provider", provider: head };
|
|
424
|
+
}
|
|
425
|
+
function isResolvedProviderName(value) {
|
|
426
|
+
return RESOLVED_PROVIDER_NAMES.includes(value);
|
|
427
|
+
}
|
|
428
|
+
function ruleMatches(rule, backend) {
|
|
429
|
+
switch (rule.kind) {
|
|
430
|
+
case "provider":
|
|
431
|
+
return backend.provider === rule.provider;
|
|
432
|
+
case "provider_model":
|
|
433
|
+
// A model-granular rule matches ONLY that model. A backend of the same
|
|
434
|
+
// provider carrying no model (a CLI whose model arrives at the dispatch
|
|
435
|
+
// handshake) is NOT matched: the operator ruled out one model, not the
|
|
436
|
+
// backend — the coarse `provider` tier is how they rule out the backend.
|
|
437
|
+
return backend.provider === rule.provider && backend.model === rule.model;
|
|
438
|
+
case "endpoint":
|
|
439
|
+
return endpointHosts(backend.endpoint).includes(rule.host);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* The forms of a source endpoint an operator pattern may name: `hostname` (port-
|
|
444
|
+
* agnostic — `integrate.api.nvidia.com` rules out that host on any port) and
|
|
445
|
+
* `host:port` (port-specific — `localhost:8000` rules out one of several local
|
|
446
|
+
* endpoints). Both are offered so the pattern's own specificity decides.
|
|
447
|
+
*
|
|
448
|
+
* An endpoint that is not a URL (a CLI launcher command) degrades to the raw
|
|
449
|
+
* lowercased string, which then only ever matches an identical literal pattern —
|
|
450
|
+
* never a false positive against a real host.
|
|
451
|
+
*
|
|
452
|
+
* ⚠ The authority check is load-bearing, not defensive: `new URL()` accepts ANY
|
|
453
|
+
* scheme-shaped string, so it does NOT throw on `localhost:8000` (protocol
|
|
454
|
+
* `localhost:`) or on a Windows command path like `C:\tools\codex.cmd` (protocol
|
|
455
|
+
* `c:`) — both parse to an EMPTY hostname. Relying on the `catch` alone would
|
|
456
|
+
* therefore silently yield no hosts for exactly those endpoints, making an
|
|
457
|
+
* operator's literal-identical rule match nothing.
|
|
458
|
+
*/
|
|
459
|
+
function endpointHosts(endpoint) {
|
|
460
|
+
if (!endpoint)
|
|
461
|
+
return [];
|
|
462
|
+
const raw = endpoint.toLowerCase();
|
|
463
|
+
if (endpoint.includes("//")) {
|
|
464
|
+
try {
|
|
465
|
+
const url = new URL(endpoint);
|
|
466
|
+
if (url.hostname.length > 0) {
|
|
467
|
+
return [url.hostname.toLowerCase(), url.host.toLowerCase()];
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// Not a URL after all — fall through to the raw literal.
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return [raw];
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Read the operator's confirmed route policy from the shared Gate-0 confirmation.
|
|
478
|
+
*
|
|
479
|
+
* Deliberately reads the artifact DIRECTLY rather than going through
|
|
480
|
+
* {@link readSharedProviderConfirmation}, so that **a corrupt sibling field cannot
|
|
481
|
+
* discard the decision**: `parseSharedProviderConfirmation` returns `null` wholesale
|
|
482
|
+
* on any malformed required field or a `schema_version` mismatch. Routing policy
|
|
483
|
+
* through it would make an unrelated corruption (or a future version bump) silently
|
|
484
|
+
* lift the operator's exclusions — failing OPEN on the one field that must fail
|
|
485
|
+
* closed. Parsing `policy` on its own keeps that blast radius out.
|
|
486
|
+
*
|
|
487
|
+
* (Before G3 this bypass carried a second rationale — dodging the roster-freshness
|
|
488
|
+
* gate. That gate is gone: no read of this artifact is reach-gated any more, so the
|
|
489
|
+
* remaining reason is blast radius alone.)
|
|
490
|
+
*
|
|
491
|
+
* **Honest limit — this is not absolutely fail-closed.** An absent or unparseable
|
|
492
|
+
* artifact yields `null` (no operator policy). That residue is irreducible here: with
|
|
493
|
+
* no readable decision on disk there is nothing to fail closed ON. Self-spawn-blocked
|
|
494
|
+
* providers are still excluded locally by {@link resolveDispatchExclusion}, which
|
|
495
|
+
* needs no artifact.
|
|
496
|
+
*/
|
|
497
|
+
export async function readConfirmedDispatchPolicy(root) {
|
|
498
|
+
if (!root)
|
|
499
|
+
return null;
|
|
500
|
+
let raw;
|
|
501
|
+
try {
|
|
502
|
+
raw = await readJsonFile(sharedProviderConfirmationPath(root));
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// Absent (ENOENT) / unreadable / invalid JSON — never-block, same contract as
|
|
506
|
+
// every other read of this artifact.
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
510
|
+
return null;
|
|
511
|
+
return parseConfirmedDispatchPolicy(raw.policy) ?? null;
|
|
512
|
+
}
|
|
181
513
|
// ---------------------------------------------------------------------------
|
|
182
514
|
// Write (audit writes it)
|
|
183
515
|
// ---------------------------------------------------------------------------
|
|
@@ -203,17 +535,21 @@ export async function writeSharedProviderConfirmation(root, confirmation, logger
|
|
|
203
535
|
// ---------------------------------------------------------------------------
|
|
204
536
|
// Validation
|
|
205
537
|
// ---------------------------------------------------------------------------
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
538
|
+
/**
|
|
539
|
+
* The persisted pool entry's gate: `name` ONLY.
|
|
540
|
+
*
|
|
541
|
+
* B+D: this gate previously hard-required `capability_tier` AND `excluded` — the
|
|
542
|
+
* exact reach fields B removes from the persisted shape. That coupling is why B and
|
|
543
|
+
* D are ONE commit: a post-B artifact failing a pre-B gate parses to `null`, which
|
|
544
|
+
* degrades SILENTLY to empty cost positions and λ=0. Requiring only `name` also
|
|
545
|
+
* makes the gate forward-tolerant of a confirmation written before B (its extra
|
|
546
|
+
* reach fields are simply ignored on read, never re-persisted).
|
|
547
|
+
*/
|
|
548
|
+
function isPersistedPoolEntry(value) {
|
|
210
549
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
211
550
|
return false;
|
|
212
551
|
}
|
|
213
|
-
|
|
214
|
-
return (typeof obj.name === "string" &&
|
|
215
|
-
typeof obj.capability_tier === "string" &&
|
|
216
|
-
typeof obj.excluded === "boolean");
|
|
552
|
+
return typeof value.name === "string";
|
|
217
553
|
}
|
|
218
554
|
function isHostModelCostEntry(value) {
|
|
219
555
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -254,11 +590,9 @@ function parseSharedProviderConfirmation(value) {
|
|
|
254
590
|
if (typeof obj.confirmed_at !== "string")
|
|
255
591
|
return null;
|
|
256
592
|
if (!Array.isArray(obj.provider_pool) ||
|
|
257
|
-
!obj.provider_pool.every(
|
|
593
|
+
!obj.provider_pool.every(isPersistedPoolEntry)) {
|
|
258
594
|
return null;
|
|
259
595
|
}
|
|
260
|
-
if (!isResolvedProviderNameArray(obj.roster))
|
|
261
|
-
return null;
|
|
262
596
|
// host_model_cost_order is optional + additive; a malformed value degrades to
|
|
263
597
|
// absent (the field never blocks parsing — INV-DC1-6 never-block spirit).
|
|
264
598
|
const hostModels = Array.isArray(obj.host_model_cost_order) &&
|
|
@@ -273,6 +607,11 @@ function parseSharedProviderConfirmation(value) {
|
|
|
273
607
|
// dispatch_bias is optional + additive; a malformed/out-of-range value degrades to
|
|
274
608
|
// the cost-first default (clamp → undefined only when non-finite), never blocking.
|
|
275
609
|
const dispatchBias = clampDispatchBias(obj.dispatch_bias);
|
|
610
|
+
// policy is optional + additive; a malformed value degrades to absent. Note the
|
|
611
|
+
// asymmetry with the fields above: degrading policy to absent fails OPEN (the
|
|
612
|
+
// operator's exclusions stop applying), so each list is validated independently —
|
|
613
|
+
// a malformed `include` must not silently discard a well-formed `exclude`.
|
|
614
|
+
const policy = parseConfirmedDispatchPolicy(obj.policy);
|
|
276
615
|
return {
|
|
277
616
|
schema_version: SHARED_PROVIDER_CONFIRMATION_VERSION,
|
|
278
617
|
session_level: true,
|
|
@@ -285,79 +624,132 @@ function parseSharedProviderConfirmation(value) {
|
|
|
285
624
|
? { source_pool_cost_order: sourcePools }
|
|
286
625
|
: {}),
|
|
287
626
|
...(dispatchBias != null ? { dispatch_bias: dispatchBias } : {}),
|
|
288
|
-
|
|
627
|
+
...(policy ? { policy } : {}),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Parse the optional policy half. Each list is validated on its own so one
|
|
632
|
+
* malformed list cannot discard the other — degrading a well-formed `exclude` to
|
|
633
|
+
* absent would fail OPEN and route to a backend the operator ruled out.
|
|
634
|
+
*/
|
|
635
|
+
function parseConfirmedDispatchPolicy(value) {
|
|
636
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
637
|
+
return undefined;
|
|
638
|
+
}
|
|
639
|
+
const obj = value;
|
|
640
|
+
// `exclude` is the OPEN exclusion grammar (a pattern's head need not be a provider
|
|
641
|
+
// name — the endpoint tier's never is), so it is kept verbatim; an unmatchable
|
|
642
|
+
// pattern is inert, which is the safe direction for a filter. `include` is the
|
|
643
|
+
// CLOSED provider-name set and stays membership-checked, so an unknown name cannot
|
|
644
|
+
// type-assert its way into overriding a self-spawn block.
|
|
645
|
+
const exclude = parseExclusionPatterns(obj.exclude);
|
|
646
|
+
const include = parseProviderNameList(obj.include);
|
|
647
|
+
if (!exclude?.length && !include?.length)
|
|
648
|
+
return undefined;
|
|
649
|
+
return {
|
|
650
|
+
...(exclude?.length ? { exclude } : {}),
|
|
651
|
+
...(include?.length ? { include } : {}),
|
|
289
652
|
};
|
|
290
653
|
}
|
|
291
654
|
// ---------------------------------------------------------------------------
|
|
292
655
|
// Read (remediate gains this)
|
|
293
656
|
// ---------------------------------------------------------------------------
|
|
294
657
|
/**
|
|
295
|
-
* Read +
|
|
658
|
+
* Read + parse the shared confirmation for `root`. TWO-valued:
|
|
296
659
|
*
|
|
297
660
|
* - returns `null` when the artifact is ABSENT or MALFORMED — the caller then
|
|
298
661
|
* resolves its provider independently, exactly as today (INV-DC1-6
|
|
299
662
|
* never-block). Absence is the standalone-remediate case and is not an error.
|
|
300
|
-
* - returns
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
663
|
+
* - returns the parsed confirmation otherwise — the operator's persisted route
|
|
664
|
+
* DECISION, honored as-is.
|
|
665
|
+
*
|
|
666
|
+
* **Reach-free by construction (G3).** This read does NOT check whether the
|
|
667
|
+
* reachable backend set still matches whatever the writing auditor saw. It cannot
|
|
668
|
+
* meaningfully: a *different* auditor legitimately has different reach, so that
|
|
669
|
+
* comparison was noise cross-auditor — and answering it by discarding the
|
|
670
|
+
* operator's decision fails OPEN on a policy question. A backend becoming newly
|
|
671
|
+
* reachable is a real event, handled by the reconciliation gate
|
|
672
|
+
* ({@link computeNewlyReachableBackends}), which compares the DECISION against
|
|
673
|
+
* *this* auditor's reach and is keyed on `autonomous_mode`.
|
|
308
674
|
*
|
|
309
675
|
* Never throws: a read/parse failure is treated as absent/malformed → `null`.
|
|
310
676
|
* The read is lockless (no lock needed: the writer's atomic rename guarantees a
|
|
311
677
|
* complete file either way — CE-003) and so cannot deadlock against a writer.
|
|
312
678
|
*/
|
|
313
|
-
export async function readSharedProviderConfirmation(root
|
|
314
|
-
const path = sharedProviderConfirmationPath(root);
|
|
679
|
+
export async function readSharedProviderConfirmation(root) {
|
|
315
680
|
let raw;
|
|
316
681
|
try {
|
|
317
|
-
raw = await readJsonFile(
|
|
682
|
+
raw = await readJsonFile(sharedProviderConfirmationPath(root));
|
|
318
683
|
}
|
|
319
684
|
catch {
|
|
320
685
|
// Absent (ENOENT) OR unreadable / invalid-JSON both degrade to the
|
|
321
686
|
// never-block path — a missing or corrupt artifact is never an error here.
|
|
322
687
|
return null;
|
|
323
688
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
689
|
+
// Malformed (wrong shape / version drift) → never-block, but never SILENT (D).
|
|
690
|
+
const parsed = parseSharedProviderConfirmation(raw);
|
|
691
|
+
if (parsed === null)
|
|
692
|
+
warnConfirmationRejected(raw, root);
|
|
693
|
+
return parsed;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* D — loud rejection. `null` from the parser is indistinguishable at the call sites
|
|
697
|
+
* from "no confirmation exists", and every consumer treats that as "no operator
|
|
698
|
+
* decision": empty cost positions, λ=0, no exclusions. So a `schema_version` bump —
|
|
699
|
+
* or any shape drift — would SILENTLY discard the operator's whole route decision
|
|
700
|
+
* and quietly re-route the run. Absence is legitimately silent; a file that EXISTS
|
|
701
|
+
* and was rejected is not.
|
|
702
|
+
*
|
|
703
|
+
* A warning, not a throw: INV-DC1-6 (never-block) is the standing invariant here —
|
|
704
|
+
* the same loud-degrade shape as `readQuotaStateOrDegrade` and the blind-dispatch
|
|
705
|
+
* warning. Reached only on the rejection path, so it cannot become hot.
|
|
706
|
+
*/
|
|
707
|
+
function warnConfirmationRejected(raw, root) {
|
|
708
|
+
const version = raw !== null && typeof raw === "object" && !Array.isArray(raw)
|
|
709
|
+
? raw.schema_version
|
|
710
|
+
: undefined;
|
|
711
|
+
const why = version !== undefined && version !== SHARED_PROVIDER_CONFIRMATION_VERSION
|
|
712
|
+
? `schema_version "${String(version)}" != expected "${SHARED_PROVIDER_CONFIRMATION_VERSION}"`
|
|
713
|
+
: "malformed shape";
|
|
714
|
+
// Scoped to what the rejection ACTUALLY discards. Exclusions deliberately survive:
|
|
715
|
+
// `readConfirmedDispatchPolicy` bypasses this parser (and its version check) and reads
|
|
716
|
+
// `policy` straight off the raw JSON, so an exclusion keeps failing CLOSED through a
|
|
717
|
+
// schema drift — the correct direction, and the reason this message must NOT claim the
|
|
718
|
+
// pool is unfiltered. Saying otherwise sends the operator hunting a routing change that
|
|
719
|
+
// did not happen.
|
|
720
|
+
process.stderr.write(`WARNING: ignoring the provider confirmation at ${sharedProviderConfirmationPath(root)} ` +
|
|
721
|
+
`(${why}). The operator's confirmed COST ORDER and DISPATCH BIAS are NOT being applied ` +
|
|
722
|
+
`to this run — dispatch falls back to price-then-tier at the default bias. ` +
|
|
723
|
+
`(Exclusions are read separately and still apply.) Re-run the provider-confirmation ` +
|
|
724
|
+
`gate to rewrite it.
|
|
725
|
+
`);
|
|
339
726
|
}
|
|
340
727
|
/**
|
|
341
728
|
* Read the operator-confirmed cost ordering (rung 1 of costRank; see
|
|
342
729
|
* spec/cost-first-routing.md) from the shared Gate-0 confirmation as a model-keyed
|
|
343
730
|
* `Map<model_id, cost_order>` for the dispatch build sites. Single-sourced so audit
|
|
344
731
|
* and remediate honor it identically. Best-effort and never throws: an absent
|
|
345
|
-
* `root
|
|
346
|
-
*
|
|
347
|
-
*
|
|
732
|
+
* `root` or a missing/malformed confirmation yields an empty map — dispatch then
|
|
733
|
+
* falls to real price then tier.
|
|
734
|
+
*
|
|
735
|
+
* **Not gated on reach (G3 step 1).** The cost order is the operator's POLICY —
|
|
736
|
+
* "the operator may reorder" — so a shift in what happens to be reachable must not
|
|
737
|
+
* silently discard it. The former roster-freshness gate did exactly that, on the
|
|
738
|
+
* false premise that these positions are reach-derived; they are not, and it was
|
|
739
|
+
* the live defect this fixes.
|
|
348
740
|
*/
|
|
349
|
-
export async function readConfirmedCostPositions(root
|
|
741
|
+
export async function readConfirmedCostPositions(root) {
|
|
350
742
|
if (!root)
|
|
351
743
|
return new Map();
|
|
352
|
-
const
|
|
353
|
-
if (!
|
|
744
|
+
const confirmation = await readSharedProviderConfirmation(root);
|
|
745
|
+
if (!confirmation)
|
|
354
746
|
return new Map();
|
|
355
747
|
// Provider-pool positions (configured models) PLUS any host-native tiers the
|
|
356
748
|
// operator confirmed at Gate-0 (follow-up c). Both are model-keyed; a host tier
|
|
357
749
|
// and a configured pool thread to dispatch identically. Host entries are already
|
|
358
750
|
// in the single unified cost order, so a plain merge preserves the total order.
|
|
359
|
-
const positions = resolveConfirmedCostPositions(
|
|
360
|
-
for (const entry of
|
|
751
|
+
const positions = resolveConfirmedCostPositions(confirmation.provider_pool);
|
|
752
|
+
for (const entry of confirmation.host_model_cost_order ?? []) {
|
|
361
753
|
if (entry.model_id &&
|
|
362
754
|
Number.isFinite(entry.cost_order) &&
|
|
363
755
|
entry.cost_order >= 0) {
|
|
@@ -368,7 +760,7 @@ export async function readConfirmedCostPositions(root, sessionConfig = {}, env =
|
|
|
368
760
|
// position keyed on the source's model id — the SAME model-keyed lookup a repair-proxy
|
|
369
761
|
// dispatch pool resolves against (pool.model = the namespaced `provider/model`). An entry
|
|
370
762
|
// without a model_id is display-only and contributes no dispatch position.
|
|
371
|
-
for (const entry of
|
|
763
|
+
for (const entry of confirmation.source_pool_cost_order ?? []) {
|
|
372
764
|
if (entry.model_id &&
|
|
373
765
|
Number.isFinite(entry.cost_order) &&
|
|
374
766
|
entry.cost_order >= 0) {
|
|
@@ -381,17 +773,20 @@ export async function readConfirmedCostPositions(root, sessionConfig = {}, env =
|
|
|
381
773
|
* Read the operator-confirmed cost↔speed dispatch bias (λ ∈ [0,1]) from the shared
|
|
382
774
|
* Gate-0 confirmation for the dispatch build sites (spec/dispatch-cost-speed-dial.md).
|
|
383
775
|
* Single-sourced so audit and remediate apply the identical operating point.
|
|
384
|
-
* Best-effort and never throws: an absent `root`, a missing/malformed confirmation,
|
|
385
|
-
*
|
|
386
|
-
*
|
|
776
|
+
* Best-effort and never throws: an absent `root`, a missing/malformed confirmation,
|
|
777
|
+
* or an absent field all yield the cost-first default `0`.
|
|
778
|
+
*
|
|
779
|
+
* **Not gated on reach (G3 step 1)** — λ is the operator's durable operating point
|
|
780
|
+
* on the cost-vs-throughput frontier, i.e. POLICY. See
|
|
781
|
+
* {@link readConfirmedCostPositions}.
|
|
387
782
|
*/
|
|
388
|
-
export async function readConfirmedDispatchBias(root
|
|
783
|
+
export async function readConfirmedDispatchBias(root) {
|
|
389
784
|
if (!root)
|
|
390
785
|
return 0;
|
|
391
|
-
const
|
|
392
|
-
if (!
|
|
786
|
+
const confirmation = await readSharedProviderConfirmation(root);
|
|
787
|
+
if (!confirmation)
|
|
393
788
|
return 0;
|
|
394
|
-
return clampDispatchBias(
|
|
789
|
+
return clampDispatchBias(confirmation.dispatch_bias) ?? 0;
|
|
395
790
|
}
|
|
396
791
|
// ---------------------------------------------------------------------------
|
|
397
792
|
// Interactive Gate-0 operator input (spec/cost-first-routing.md — Gate-0)
|
|
@@ -416,6 +811,9 @@ export function parseProviderConfirmationInput(value) {
|
|
|
416
811
|
? v
|
|
417
812
|
: undefined;
|
|
418
813
|
const costOrder = stringArray(obj.cost_order);
|
|
814
|
+
// No cast: `exclude` is the OPEN exclusion grammar, so asserting the operator's
|
|
815
|
+
// raw strings into the closed provider-name union would be a lie — and the exact
|
|
816
|
+
// type-assert-your-way-in move the policy parser refuses for `include`.
|
|
419
817
|
const exclude = stringArray(obj.exclude);
|
|
420
818
|
const include = stringArray(obj.include);
|
|
421
819
|
const dispatchBias = clampDispatchBias(obj.dispatch_bias);
|
|
@@ -456,4 +854,27 @@ export async function readProviderConfirmationInput(artifactsDir) {
|
|
|
456
854
|
}
|
|
457
855
|
return parseProviderConfirmationInput(raw);
|
|
458
856
|
}
|
|
857
|
+
/**
|
|
858
|
+
* Invalidate a CONSUMED Gate-0 input by deleting it — the second half of
|
|
859
|
+
* consume-and-invalidate, paired here with {@link readProviderConfirmationInput} so
|
|
860
|
+
* the two cannot drift apart.
|
|
861
|
+
*
|
|
862
|
+
* The input's presence is the "operator has acted" signal the gate reads to decide
|
|
863
|
+
* emit-vs-consume. Once promoted into the canonical artifacts it is SPENT: leaving it
|
|
864
|
+
* on disk means a later reconciliation delta silently re-consumes a submission that
|
|
865
|
+
* answered an older question, auto-satisfying the gate instead of asking the
|
|
866
|
+
* operator. Deleting it is what makes the gate able to fire a second time at all.
|
|
867
|
+
*
|
|
868
|
+
* Best-effort and never throws: an already-absent file is the desired end state, and
|
|
869
|
+
* a failed unlink must not break the in-flight obligation (the promotion itself
|
|
870
|
+
* already succeeded).
|
|
871
|
+
*/
|
|
872
|
+
export async function unlinkProviderConfirmationInput(artifactsDir) {
|
|
873
|
+
try {
|
|
874
|
+
await unlink(join(artifactsDir, PROVIDER_CONFIRMATION_INPUT_FILENAME));
|
|
875
|
+
}
|
|
876
|
+
catch {
|
|
877
|
+
// Absent / locked / read-only — nothing to invalidate, or nothing we can do.
|
|
878
|
+
}
|
|
879
|
+
}
|
|
459
880
|
//# sourceMappingURL=sharedProviderConfirmation.js.map
|