clearotron 0.3.2-beta.7 → 0.3.2-beta.8
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/.env.example +24 -23
- package/INSTALL.md +142 -75
- package/README.md +3 -3
- package/bin/onboard.mjs +637 -216
- package/bin/start.mjs +133 -23
- package/bin/update.mjs +58 -11
- package/build-info.json +2 -2
- package/docs/architecture/04-configuration-reference.md +26 -11
- package/docs/architecture/05-config-governance.md +17 -7
- package/driver/CHANGELOG.md +76 -0
- package/driver/band-size.mjs +59 -0
- package/driver/config-inventory.mjs +112 -9
- package/driver/contract-arm2-baseline.json +1 -3
- package/driver/contract-e3-backlog.mjs +26 -26
- package/driver/contract-vocabulary.mjs +44 -10
- package/driver/door-gates.mjs +41 -7
- package/driver/driver.config.mjs +272 -59
- package/driver/engine/CONTRACT.md +10 -3
- package/driver/engine/README.md +2 -2
- package/driver/engine/anthropic-agent.mjs +77 -21
- package/driver/engine/auth.mjs +129 -10
- package/driver/engine/jx-turn.mjs +7 -6
- package/driver/engine/mcp/recording-server.mjs +13 -0
- package/driver/engine/openai-agent.mjs +4 -2
- package/driver/engine/probe.mjs +110 -23
- package/driver/findings-model.mjs +1 -1
- package/driver/flag-snapshot.mjs +28 -5
- package/driver/gateway.mjs +24 -18
- package/driver/jx-lanes.mjs +21 -2
- package/driver/jx-units.mjs +6 -3
- package/driver/jx.mjs +4 -2
- package/driver/matter-frame-record.mjs +90 -1
- package/driver/named-band.mjs +34 -2
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +200 -23
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-report.mjs +15 -1
- package/driver/portal-service.mjs +46 -6
- package/driver/predelivery-lint.mjs +12 -2
- package/driver/publish/index.mjs +46 -5
- package/driver/publish/knockout.mjs +10 -1
- package/driver/publish/render-knockout.mjs +69 -7
- package/driver/publish/render.mjs +170 -59
- package/driver/publish/report-data.mjs +4 -1
- package/driver/publish/report-topbar.mjs +58 -0
- package/driver/publish/templates/report.css +18 -1
- package/driver/publish/xlsx.mjs +13 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-coverage.mjs +94 -1
- package/driver/register-digest-record.mjs +236 -11
- package/driver/register-plan.mjs +170 -0
- package/driver/result-noun-fields.mjs +2 -2
- package/driver/run-economics.mjs +41 -10
- package/driver/run-requirements.mjs +173 -9
- package/driver/runner.mjs +3 -3
- package/driver/stages.mjs +12 -8
- package/driver/suite-census.json +142 -64
- package/driver/systemd/README.md +7 -4
- package/driver/terminal-clamp.mjs +107 -1
- package/driver/tokens.mjs +169 -3
- package/driver/unit-environment.mjs +42 -15
- package/driver/unit-inventory.mjs +19 -2
- package/driver/verify.mjs +27 -0
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +15 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-6jzO9HiX.js} +155 -79
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/jx/README.md +2 -1
- package/providers/jx/src/turn-envelope.mjs +8 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/uspto-local/README.md +1 -1
- package/scripts/authority-boundary-probe.mjs +4 -2
- package/scripts/env-audit.mjs +12 -6
- package/scripts/freeze-example-run.mjs +49 -16
- package/scripts/generated-files-are-current.mjs +69 -4
- package/scripts/settings-render-check.mjs +75 -2
- package/scripts/test-full.mjs +96 -3
- package/scripts/test-run.mjs +10 -0
- package/shared/deployment-box.mjs +7 -2
- package/shared/driver-dir.mjs +1 -1
- package/shared/names-in-force.mjs +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Contract: engine/CONTRACT.md. runTurn() returns the registry-standard normalized tuple
|
|
8
8
|
// (`{code, killed, wall, stdout, stderr, laneWaitMs, json, usage, reads, readsTruncated, modelWire,
|
|
9
|
-
// sessionRef, signals}`), with a SYNTHESIZED `json` envelope in the classifier's shape so every downstream
|
|
9
|
+
// providerWire, sessionRef, signals}`), with a SYNTHESIZED `json` envelope in the classifier's shape so every downstream
|
|
10
10
|
// classifier in gateway.mjs (payloadText, json.status check, isEmbeddedFallback, isTimeout,
|
|
11
11
|
// isLaneWedge) works unchanged.
|
|
12
12
|
//
|
|
@@ -21,12 +21,18 @@ import { tmpdir } from "node:os";
|
|
|
21
21
|
import { join, dirname } from "node:path";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
23
|
import { resolveSpawnCwd, spawnGraceMs } from "./common.mjs";
|
|
24
|
-
import {
|
|
24
|
+
import { resolveEngineProgram } from "../driver.config.mjs"; // — the one place that finds the program; it reads every spelling of the setting
|
|
25
25
|
import { authorityTrees } from "../authority-trees.mjs";
|
|
26
26
|
import { recordEngineChild, clearEngineChild } from "./child-record.mjs"; //
|
|
27
|
+
import { billingMode } from "./auth.mjs"; // — the one parse of the billing word (see spawnEnv)
|
|
27
28
|
|
|
28
29
|
// Read per-call (not module-level) so tests can drive a short stall timeout / a mock binary.
|
|
29
|
-
|
|
30
|
+
// ONE place knows how to find the program (driver.config.mjs resolveEngineProgram): the explicit setting,
|
|
31
|
+
// then PATH, then the copy Clearotron installed. What it found is spawned by ABSOLUTE path, because a
|
|
32
|
+
// bare word lets spawn(2) walk PATH on its own and never reach the installed copy. When nothing resolved,
|
|
33
|
+
// what was asked for is spawned unchanged, so that failure reads exactly as it always has; the run door
|
|
34
|
+
// (preflightEngineBinary) refuses that case before any stage runs.
|
|
35
|
+
const claudeBin = () => { const r = resolveEngineProgram("anthropic-agent"); return r.resolved ?? r.bin; };
|
|
30
36
|
|
|
31
37
|
// AUTH TOGGLE (config, not code). The subscription path is the cost-saving default: claude -p with NO
|
|
32
38
|
// ANTHROPIC_API_KEY in its env falls back to the OAuth subscription credentials (apiKeySource:"none" →
|
|
@@ -34,10 +40,15 @@ const claudeBin = () => envFrom(process.env, "CLEAROTRON_CLAUDE_PATH") || "claud
|
|
|
34
40
|
// .env, and a present API key OVERRIDES the subscription — so we must STRIP it from the claude subprocess
|
|
35
41
|
// env. CLEAROTRON_AI_BILLING=api-key keeps the key (the standing fallback for when the subscription is
|
|
36
42
|
// revoked — Anthropic's advance notice = today's per-call API cost). Default = subscription.
|
|
43
|
+
//
|
|
44
|
+
// `cloud` strips it too, as that mode's acceptance asks: a key has no part in a turn the vendor's switches
|
|
45
|
+
// send to the reader's cloud account, and dropping it means a leftover key can never be what bills. A
|
|
46
|
+
// gateway's credential is its own ANTHROPIC_AUTH_TOKEN, which rides through like every other name. The
|
|
47
|
+
// word is parsed by auth.mjs (billingMode), the one place that reads it. This never validates and never
|
|
48
|
+
// throws, because the doors that do (the top of runStage, the probe, the jx runner) have already run.
|
|
37
49
|
export function spawnEnv(base = process.env) {
|
|
38
50
|
const env = { ...base };
|
|
39
|
-
|
|
40
|
-
if (mode !== "api-key") delete env.ANTHROPIC_API_KEY; // subscription: force OAuth/subscription billing
|
|
51
|
+
if (billingMode(base) !== "api-key") delete env.ANTHROPIC_API_KEY; // subscription and cloud
|
|
41
52
|
return env;
|
|
42
53
|
}
|
|
43
54
|
// 120s of ZERO streamed output = the silent-provider-stall abort. A healthy turn
|
|
@@ -110,15 +121,20 @@ const killEscalateMs = () => Math.max(50, Number(process.env.CLEAROTRON_KILL_ESC
|
|
|
110
121
|
// clock, so the watchdog never trips). Default 64MB (gateway parity); CLEAROTRON_ENGINE_MAX_BUFFER shrinks it for tests.
|
|
111
122
|
const engineMaxBufferChars = () => Math.max(1024, Number(process.env.CLEAROTRON_ENGINE_MAX_BUFFER || 64 * 1024 * 1024));
|
|
112
123
|
|
|
113
|
-
// tier/alias → claude -p model alias.
|
|
114
|
-
// the
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
124
|
+
// tier/alias → claude -p model alias. EVERY TIER GOES AS THE VENDOR'S OWN ALIAS — opus, sonnet, haiku,
|
|
125
|
+
// fable — so the CLI serves the newest model of that family, and a new one arrives with no edit here.
|
|
126
|
+
// opus and sonnet were pinned to claude-opus-5 / claude-sonnet-5 from 2026-07-27, when the bare "opus"
|
|
127
|
+
// still resolved to Opus 4.8 on the live CLI (2.1.209). The pin was reversed on 2026-09-14, for two
|
|
128
|
+
// reasons. A hand-pinned id is a silent downgrade on every clearance from the day a better model ships.
|
|
129
|
+
// And on Bedrock, Vertex and Foundry the CLI resolves an alias through the vendor's own
|
|
130
|
+
// ANTHROPIC_DEFAULT_OPUS_MODEL / _SONNET_MODEL / _HAIKU_MODEL / _FABLE_MODEL, which an exact id bypasses:
|
|
131
|
+
// a cloud with no deployment of that exact name refuses the turn. The cost is that a model can move under a
|
|
132
|
+
// clearance without a test; the witness is the id the CLI reports, recorded on every attempt row
|
|
133
|
+
// (`modelActual`) and on the published run. To hold a tier still, set the vendor's variable in the env file
|
|
134
|
+
// (ANTHROPIC_DEFAULT_OPUS_MODEL=<id>): the stage's environment is the driver's, so it reaches the CLI
|
|
135
|
+
// with no setting of Clearotron's own. ANTHROPIC_DEFAULT_FABLE_MODEL holds fable, which no stage asks for
|
|
136
|
+
// unless an override names it, as CLEAROTRON_SYNTHESIS_MODEL=fable does. A catalog id a caller names
|
|
137
|
+
// (anthropic/claude-opus-5) still goes as that exact id. The non-anthropic tiers (gemini skeptic, deepseek refutation, azure) have no claude equivalent →
|
|
122
138
|
// substituted with an anthropic model (also GRADE-MOVING, A/B-only); their bare-alias substitutes
|
|
123
139
|
// (e.g. deepseek → "opus") are legacy aliases no stage names today, intentionally left un-pinned. They
|
|
124
140
|
// stay registered so a stage that names one is SUBSTITUTED loudly rather than caught by the regex
|
|
@@ -141,7 +157,7 @@ const engineMaxBufferChars = () => Math.max(1024, Number(process.env.CLEAROTRON_
|
|
|
141
157
|
// non-GPT id. That is the issue's requirement in one line: an unhonoured model override is an error,
|
|
142
158
|
// not a substitution.
|
|
143
159
|
const CLAUDE_MODEL = {
|
|
144
|
-
opus: "
|
|
160
|
+
opus: "opus", sonnet: "sonnet", haiku: "haiku", fable: "fable",
|
|
145
161
|
"anthropic/claude-opus-5": "claude-opus-5", "anthropic/claude-sonnet-5": "claude-sonnet-5",
|
|
146
162
|
"anthropic/claude-sonnet-4-6": "sonnet", "anthropic/claude-haiku-4-5": "haiku",
|
|
147
163
|
};
|
|
@@ -552,7 +568,16 @@ export const anthropicAgentEngine = {
|
|
|
552
568
|
// It never falls back to the requested alias — a record that says "actual: <what we asked for>"
|
|
553
569
|
// when nothing was observed is precisely the absence-read-as-a-pass this issue exists to end. The
|
|
554
570
|
// comparison and the policy live in gateway.mjs; this reports, it does not judge.
|
|
555
|
-
|
|
571
|
+
//
|
|
572
|
+
// A MESSAGE THE CLI WROTE ITSELF NAMES NO MODEL. The CLI labels such a message `<synthetic>` in the
|
|
573
|
+
// model field, measured in testing on Azure Foundry (2026-09-14): with the opus pin naming a
|
|
574
|
+
// deployment that did not exist, the turn exited 1 with the CLI's own error and its assistant event
|
|
575
|
+
// said `<synthetic>`. No model served that turn, so the label is never taken as a served id, and
|
|
576
|
+
// `answeredItself` stops init's answer from standing in for one: init says what the session was
|
|
577
|
+
// configured with, and naming it here would name a model for a turn no model served. A real id on
|
|
578
|
+
// an earlier assistant event of the same turn now stands, because that model did serve a call;
|
|
579
|
+
// before the label was refused, the label that followed overwrote it.
|
|
580
|
+
let wireModelInit = null, wireModelAssistant = null, answeredItself = false;
|
|
556
581
|
// READS GAUGE (AD-4, 2026-07-30 addendum): which files this turn actually OPENED, from the stream's
|
|
557
582
|
// completed Read tool_use blocks. The stage prompt OFFERS a set of documents (declared inputs +
|
|
558
583
|
// skill refs); nothing recorded whether the turn could and did read them — and one review
|
|
@@ -703,11 +728,14 @@ export const anthropicAgentEngine = {
|
|
|
703
728
|
else if (ev.type === "rate_limit_event") rateLimitEvent = ev;
|
|
704
729
|
else if (ev.type === "system" && ev.subtype === "init") {
|
|
705
730
|
// MODEL GAUGE — the session's configured model, the earliest wire statement of what will run.
|
|
706
|
-
if (typeof ev.model === "string" && ev.model) wireModelInit ??= ev.model;
|
|
731
|
+
if (typeof ev.model === "string" && ev.model && !isCliLabel(ev.model)) wireModelInit ??= ev.model;
|
|
707
732
|
}
|
|
708
733
|
else if (ev.type === "assistant") {
|
|
709
734
|
// MODEL GAUGE — the model that served THIS API call. Authoritative over init (see above).
|
|
710
|
-
if (typeof ev.message?.model === "string" && ev.message.model)
|
|
735
|
+
if (typeof ev.message?.model === "string" && ev.message.model) {
|
|
736
|
+
if (isCliLabel(ev.message.model)) answeredItself = true;
|
|
737
|
+
else wireModelAssistant = ev.message.model;
|
|
738
|
+
}
|
|
711
739
|
// THINKING GAUGE — block presence + signature, never the text (display defaults to "omitted",
|
|
712
740
|
// so an engaged block carries a zero-length `thinking` string). See the declaration above.
|
|
713
741
|
if (!thought && ev.message?.content?.some?.((b) => b?.type === "thinking")) thought = true;
|
|
@@ -1077,8 +1105,13 @@ export const anthropicAgentEngine = {
|
|
|
1077
1105
|
toolWaitUnmeasurable: [...unmeasurable],
|
|
1078
1106
|
// MODEL GAUGE: the id the WIRE reported, or null when the stream never said. Assistant
|
|
1079
1107
|
// message first (what served the call), init second (what the session was configured with).
|
|
1080
|
-
// Never the requested alias — see the
|
|
1081
|
-
|
|
1108
|
+
// Never the requested alias, and never init's answer for a turn only the CLI answered — see the
|
|
1109
|
+
// declaration above.
|
|
1110
|
+
modelWire: wireModelAssistant ?? (answeredItself ? null : wireModelInit),
|
|
1111
|
+
// PROVIDER GAUGE: the program's own word for who served the turn, read from the result's per-model
|
|
1112
|
+
// usage ("firstParty" on Anthropic's own API and "foundry" on Azure Foundry, measured 2026-09-14),
|
|
1113
|
+
// or null when the stream never said or its models disagree. Recorded, never inferred from config.
|
|
1114
|
+
providerWire: providerOf(resultEvent),
|
|
1082
1115
|
sessionRef: resultEvent?.session_id ?? resumeRef ?? null,
|
|
1083
1116
|
// The raw result event's total_cost_usd is a provider-side field and stays in the provider's
|
|
1084
1117
|
// own stream; the tuple carries no currency (tokens-only directive 2026-07-11) — `usage` is
|
|
@@ -1116,6 +1149,29 @@ function errResult(t0, e, resumeRef) {
|
|
|
1116
1149
|
// reads: a spawn error means NO turn ran — [] is the true observation (nothing was read), not a gap.
|
|
1117
1150
|
// modelWire: null for the opposite reason — no turn ran, so the wire said nothing about a model, and
|
|
1118
1151
|
// the record must say UNKNOWN rather than inherit the alias that was asked for.
|
|
1119
|
-
json: null, usage: null, reads: [], readsTruncated: false, modelWire: null, sessionRef: resumeRef ?? null,
|
|
1152
|
+
json: null, usage: null, reads: [], readsTruncated: false, modelWire: null, providerWire: null, sessionRef: resumeRef ?? null,
|
|
1120
1153
|
};
|
|
1121
1154
|
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* The program's own word for which provider served a turn, from the result event's per-model usage:
|
|
1158
|
+
* `modelUsage[<model>].provider`, "firstParty" on Anthropic's own API and "foundry" on Azure Foundry
|
|
1159
|
+
* (measured 2026-09-14, CLI 2.1.263). One word when every model the turn used names the same provider;
|
|
1160
|
+
* null when none does or they disagree, because a single word would then be a guess.
|
|
1161
|
+
*/
|
|
1162
|
+
export function providerOf(resultEvent) {
|
|
1163
|
+
const words = new Set();
|
|
1164
|
+
for (const u of Object.values(resultEvent?.modelUsage ?? {})) {
|
|
1165
|
+
const w = typeof u?.provider === "string" ? u.provider.trim() : "";
|
|
1166
|
+
if (w) words.add(w);
|
|
1167
|
+
}
|
|
1168
|
+
return words.size === 1 ? [...words][0] : null;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Whether a model field holds one of the CLI's own bracketed labels, such as `<synthetic>` on a message
|
|
1173
|
+
* it wrote itself, rather than the id of a model. The reports skip the same shape when they name models.
|
|
1174
|
+
*/
|
|
1175
|
+
function isCliLabel(id) {
|
|
1176
|
+
return /^<.*>$/.test(String(id).trim());
|
|
1177
|
+
}
|
package/driver/engine/auth.mjs
CHANGED
|
@@ -20,28 +20,147 @@
|
|
|
20
20
|
// spelling list let the OpenAI half decide how an Anthropic run bills. deleted the old names, so
|
|
21
21
|
// there is one variable, only the selected engine is ever consulted, and the hazard has no route left.
|
|
22
22
|
//
|
|
23
|
-
// And deliberately written OUT
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
23
|
+
// And deliberately written OUT as a literal rather than passed to a helper as an argument: the env
|
|
24
|
+
// audit finds a product read by the literal `env.NAME`, so a helper taking the name as an argument makes
|
|
25
|
+
// the read invisible to it — measured, it turned the reads harness-only and put the name on the "no
|
|
26
|
+
// longer read by product code" list. `billingMode` below spells the name out, which keeps it visible.
|
|
27
|
+
|
|
28
|
+
// THREE MODES FOR CLAUDE, AND NOTHING ELSE IS A MODE. `cloud` bills Claude through the reader's own
|
|
29
|
+
// Google, Microsoft or Amazon account, or through a gateway in front of one (ANTHROPIC_BASE_URL). The
|
|
30
|
+
// vendor's program already routes on its own switches, which reach it because the stage environment is
|
|
31
|
+
// the driver's; what was missing was a billing word that says so. Without it a cloud machine had two
|
|
32
|
+
// choices and both were wrong: `api-key` refused for want of an Anthropic key the machine does not have,
|
|
33
|
+
// and `subscription` ran and stamped every row as billed to a subscription nobody was paying.
|
|
34
|
+
//
|
|
35
|
+
// AN UNKNOWN WORD IS REFUSED. It used to run as `subscription` on both engines, so a typo in the one
|
|
36
|
+
// setting that decides who pays was a quiet bill to the wrong account. It refuses here, at the top of
|
|
37
|
+
// runStage, in the probe and in the jx runner, before any turn runs.
|
|
38
|
+
export const BILLING_MODES = Object.freeze(["subscription", "api-key", "cloud"]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The billing word as the environment writes it, normalised and NOT validated: unset or blank reads as
|
|
42
|
+
* the default. The one parse of the word. `resolveAuthMode` validates it; the anthropic adapter's
|
|
43
|
+
* `spawnEnv`, which must never throw, reads it through here rather than parsing it a second way.
|
|
44
|
+
*/
|
|
45
|
+
export function billingMode(env = process.env) {
|
|
46
|
+
return String(env.CLEAROTRON_AI_BILLING ?? "").trim().toLowerCase() || "subscription";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The vendor's own switches, read the way its program reads them: "1", "true", "yes" or "on" switches
|
|
50
|
+
// one on. Spelled out one per line for the reason given above. The order is only the order a refusal
|
|
51
|
+
// names them in.
|
|
52
|
+
const switchedOn = (v) => ["1", "true", "yes", "on"].includes(String(v ?? "").trim().toLowerCase());
|
|
53
|
+
export const CLOUD_SWITCH = Object.freeze({ vertex: "CLAUDE_CODE_USE_VERTEX", foundry: "CLAUDE_CODE_USE_FOUNDRY", bedrock: "CLAUDE_CODE_USE_BEDROCK" });
|
|
54
|
+
|
|
55
|
+
// EVERY NAME THE PROGRAM READS TO REACH AND PAY A CLOUD, as setup writes them and the install page lists
|
|
56
|
+
// them: each cloud's switch and its least settings, the gateway pair, and the four model pins a cloud
|
|
57
|
+
// deployment is named by. A run takes every line of its settings file, so it has these already. Setup's
|
|
58
|
+
// proof turn and doctor read the file name by name, and carry these so a check proves the account a run
|
|
59
|
+
// bills rather than whatever the shell happened to hold.
|
|
60
|
+
//
|
|
61
|
+
// THE FABLE PIN IS ON IT, THOUGH SETUP NEVER ASKS FOR IT. The program reads a pin for every tier it takes as
|
|
62
|
+
// an alias, fable included, and no stage asks for fable unless an override names it. On Foundry that
|
|
63
|
+
// alias resolves to nothing unless the pin names a deployment, so a reader who sets the override sets the
|
|
64
|
+
// pin by hand, and doctor, setup's proof turn and a background start must carry it like the other three.
|
|
65
|
+
//
|
|
66
|
+
// THE STANDARD AWS KEY VARIABLES ARE ON IT. A machine with no AWS profile and no instance role keeps its
|
|
67
|
+
// Amazon keys in the settings file, and a search reads them from there. Without these three names doctor's
|
|
68
|
+
// proof turn ran without the keys and reported a fault on a machine whose searches worked.
|
|
69
|
+
export const CLOUD_SETTINGS = Object.freeze([
|
|
70
|
+
...Object.values(CLOUD_SWITCH),
|
|
71
|
+
"ANTHROPIC_VERTEX_PROJECT_ID", "CLOUD_ML_REGION", "GOOGLE_APPLICATION_CREDENTIALS",
|
|
72
|
+
"ANTHROPIC_FOUNDRY_RESOURCE", "ANTHROPIC_FOUNDRY_API_KEY",
|
|
73
|
+
"AWS_REGION", "AWS_PROFILE", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN",
|
|
74
|
+
"ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN",
|
|
75
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL", "ANTHROPIC_DEFAULT_FABLE_MODEL",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
// THE ONES THAT HOLD A SECRET, by name. Wherever a cloud setting is shown, one of these is shown as set and
|
|
79
|
+
// never with its value. Named rather than matched by suffix: AWS_ACCESS_KEY_ID ends like the Google project
|
|
80
|
+
// id beside it, and only one of the two is a credential.
|
|
81
|
+
export const CLOUD_SECRETS = Object.freeze([
|
|
82
|
+
"ANTHROPIC_FOUNDRY_API_KEY", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "ANTHROPIC_AUTH_TOKEN",
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
// WHAT A READER CHECKS WHEN A CLOUD REFUSES THE CREDENTIALS, per cloud: who refused, by the name a reader
|
|
86
|
+
// knows it by, and the settings and sign-in that decide it. Names only, never a value. The remedy for a
|
|
87
|
+
// subscription, run the program once and sign in, means nothing on a machine that pays through a cloud, and
|
|
88
|
+
// it was the only remedy the checks gave. Every setting named here is on CLOUD_SETTINGS, so doctor and
|
|
89
|
+
// setup's proof turn carry what this tells a reader to check.
|
|
90
|
+
export const CLOUD_CREDENTIAL_CHECK = Object.freeze({
|
|
91
|
+
vertex: Object.freeze({ who: "Google Cloud",
|
|
92
|
+
check: "ANTHROPIC_VERTEX_PROJECT_ID, CLOUD_ML_REGION, and the Google sign-in on this machine (gcloud's, or the key GOOGLE_APPLICATION_CREDENTIALS names)" }),
|
|
93
|
+
foundry: Object.freeze({ who: "Microsoft Azure",
|
|
94
|
+
check: "ANTHROPIC_FOUNDRY_API_KEY, or the Azure sign-in on this machine, and ANTHROPIC_FOUNDRY_RESOURCE" }),
|
|
95
|
+
bedrock: Object.freeze({ who: "Amazon Bedrock",
|
|
96
|
+
check: "AWS_REGION and the AWS credentials on this machine (a profile, an instance role, or AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY)" }),
|
|
97
|
+
gateway: Object.freeze({ who: "the gateway", check: "ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN" }),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export function cloudsSwitchedOn(env = process.env) {
|
|
101
|
+
const on = [];
|
|
102
|
+
if (switchedOn(env.CLAUDE_CODE_USE_VERTEX)) on.push("vertex");
|
|
103
|
+
if (switchedOn(env.CLAUDE_CODE_USE_FOUNDRY)) on.push("foundry");
|
|
104
|
+
if (switchedOn(env.CLAUDE_CODE_USE_BEDROCK)) on.push("bedrock");
|
|
105
|
+
return on;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Every refusal below carries `billingRefusal: true`, so a reader classifies it by what it is rather than
|
|
109
|
+
// by its wording: the probe does, and a sign-in error that also names this setting is not one of these.
|
|
110
|
+
const refuse = (message) => Object.assign(new Error(message), { billingRefusal: true });
|
|
111
|
+
const notAMode = (mode, modes) => refuse(
|
|
112
|
+
`CLEAROTRON_AI_BILLING=${mode} is not a billing mode — refusing to guess, because the guess would bill ` +
|
|
113
|
+
`the subscription. One of: ${modes.join(", ")}.`);
|
|
28
114
|
|
|
29
115
|
export function resolveAuthMode({ engineName, env = process.env } = {}) {
|
|
30
116
|
const name = String(engineName || "").toLowerCase();
|
|
31
117
|
|
|
32
118
|
if (name === "anthropic-agent") {
|
|
33
|
-
const mode = (env
|
|
119
|
+
const mode = billingMode(env);
|
|
34
120
|
if (mode === "api-key" && !env.ANTHROPIC_API_KEY)
|
|
35
|
-
throw
|
|
121
|
+
throw refuse(
|
|
36
122
|
`CLEAROTRON_AI_BILLING=api-key but ANTHROPIC_API_KEY is not set — refusing to silently bill the ` +
|
|
37
123
|
`subscription instead. Set the key, or use CLEAROTRON_AI_BILLING=subscription.`);
|
|
38
|
-
|
|
124
|
+
const on = cloudsSwitchedOn(env);
|
|
125
|
+
// A cloud's switch sends the program to that cloud whatever the billing word says. Measured on Foundry,
|
|
126
|
+
// 2026-09-14: with the switch on and the word unset, the program reported Foundry as its provider and
|
|
127
|
+
// the row was stamped as the subscription's. So a switch beside `subscription` or `api-key` is refused,
|
|
128
|
+
// after a missing key, which is the fault the config page names first.
|
|
129
|
+
if ((mode === "subscription" || mode === "api-key") && on.length)
|
|
130
|
+
throw refuse(
|
|
131
|
+
`${on.map((c) => CLOUD_SWITCH[c]).join(" and ")} ${on.length > 1 ? "are" : "is"} on, which sends Claude to ` +
|
|
132
|
+
`that cloud account, while CLEAROTRON_AI_BILLING says ${mode} — refusing rather than record the wrong ` +
|
|
133
|
+
`account. Use CLEAROTRON_AI_BILLING=cloud, or ${on.length > 1 ? "turn them off" : "turn the switch off"}.`);
|
|
134
|
+
if (mode === "subscription") return { provider: "anthropic", mode, apiBilled: false };
|
|
135
|
+
if (mode === "api-key") return { provider: "anthropic", mode, apiBilled: true };
|
|
136
|
+
if (mode === "cloud") {
|
|
137
|
+
if (on.length > 1)
|
|
138
|
+
throw refuse(
|
|
139
|
+
`CLEAROTRON_AI_BILLING=cloud but more than one cloud is switched on ` +
|
|
140
|
+
`(${on.map((c) => CLOUD_SWITCH[c]).join(", ")}) — set exactly one, so the run can say which account it bills.`);
|
|
141
|
+
// A switch names the cloud. ANTHROPIC_BASE_URL alone is the gateway form: a cloud reached through the
|
|
142
|
+
// reader's own proxy. With a switch also set, the switch is what the program routes on.
|
|
143
|
+
const cloud = on[0] ?? (env.ANTHROPIC_BASE_URL ? "gateway" : null);
|
|
144
|
+
if (!cloud)
|
|
145
|
+
throw refuse(
|
|
146
|
+
`CLEAROTRON_AI_BILLING=cloud but none of CLAUDE_CODE_USE_VERTEX, CLAUDE_CODE_USE_FOUNDRY, ` +
|
|
147
|
+
`CLAUDE_CODE_USE_BEDROCK or ANTHROPIC_BASE_URL is set — refusing to silently bill the subscription ` +
|
|
148
|
+
`instead. Set the one for your cloud (INSTALL.md), or use CLEAROTRON_AI_BILLING=subscription.`);
|
|
149
|
+
return { provider: "anthropic", mode, apiBilled: true, cloud };
|
|
150
|
+
}
|
|
151
|
+
throw notAMode(mode, BILLING_MODES);
|
|
39
152
|
}
|
|
40
153
|
|
|
41
154
|
if (name === "openai-agent") {
|
|
42
|
-
const mode = (env
|
|
155
|
+
const mode = billingMode(env);
|
|
156
|
+
if (mode === "cloud")
|
|
157
|
+
throw refuse(
|
|
158
|
+
`CLEAROTRON_AI_BILLING=cloud bills Claude through a cloud account, and this machine runs the Codex ` +
|
|
159
|
+
`engine — refusing rather than billing the ChatGPT subscription instead. Use subscription or api-key ` +
|
|
160
|
+
`with Codex, or CLEAROTRON_AI=anthropic-agent for a cloud account.`);
|
|
161
|
+
if (mode !== "subscription" && mode !== "api-key") throw notAMode(mode, ["subscription", "api-key"]);
|
|
43
162
|
if (mode === "api-key" && !env.CODEX_API_KEY)
|
|
44
|
-
throw
|
|
163
|
+
throw refuse(
|
|
45
164
|
`CLEAROTRON_AI_BILLING=api-key but CODEX_API_KEY is not set — refusing to silently bill the ChatGPT ` +
|
|
46
165
|
`subscription instead. Set the key, or use CLEAROTRON_AI_BILLING=subscription.`);
|
|
47
166
|
return { provider: "openai", mode, apiBilled: mode === "api-key" };
|
|
@@ -68,7 +68,7 @@ export function turnText(tuple) {
|
|
|
68
68
|
* Read one normalized tuple into the shape the jx lanes consume. PURE, so every branch is assertable
|
|
69
69
|
* from a literal rather than from a spawned CLI.
|
|
70
70
|
*/
|
|
71
|
-
export function readJxTuple(tuple, { vendor, authMode, engine }) {
|
|
71
|
+
export function readJxTuple(tuple, { vendor, authMode, cloud = null, engine }) {
|
|
72
72
|
// CANONICAL Usage, whole (engine/CONTRACT.md §2: {input, output, cacheRead, cacheWrite, total}). The old
|
|
73
73
|
// Messages-API rows carried input/output only because that is all the API returned; keeping only those
|
|
74
74
|
// two now would drop cache and total tokens from the rollup on the very lanes this change puts on the
|
|
@@ -82,7 +82,7 @@ export function readJxTuple(tuple, { vendor, authMode, engine }) {
|
|
|
82
82
|
// the receipt names who did the native-language work, and an alias does not name anyone. Both adapters
|
|
83
83
|
// populate it (anthropic-agent from the assistant/init events, openai-agent from `ev.model`).
|
|
84
84
|
const model = tuple?.modelWire ?? null;
|
|
85
|
-
const base = { model, vendor, authMode, usage, engine };
|
|
85
|
+
const base = { model, vendor, authMode, cloud, usage, engine };
|
|
86
86
|
// WHETHER TRUNCATION IS OBSERVABLE IS A FACT ABOUT THE ADAPTER, NOT ABOUT THIS TURN. It is keyed on the
|
|
87
87
|
// engine deliberately: `anthropic-agent` writes `stopReason: r?.stop_reason` unconditionally, so the
|
|
88
88
|
// KEY is present on every one of its turns whether or not the wire said anything — testing for the key
|
|
@@ -108,7 +108,7 @@ export function readJxTuple(tuple, { vendor, authMode, engine }) {
|
|
|
108
108
|
/**
|
|
109
109
|
* A `turn` runner for the jx lanes, bound to the run's engine and billing mode.
|
|
110
110
|
*
|
|
111
|
-
* Returns `{ turn, vendor, authMode, engine }`, or `{ error }` when the configuration refuses — the
|
|
111
|
+
* Returns `{ turn, vendor, authMode, cloud, engine }`, or `{ error }` when the configuration refuses — the
|
|
112
112
|
* caller degrades the lane with that cause rather than this throwing into a pipeline stage.
|
|
113
113
|
*/
|
|
114
114
|
export async function makeJxTurnRunner({
|
|
@@ -149,16 +149,17 @@ export async function makeJxTurnRunner({
|
|
|
149
149
|
|
|
150
150
|
const vendor = auth.provider;
|
|
151
151
|
const authMode = auth.mode;
|
|
152
|
+
const cloud = auth.cloud ?? null; // which cloud account bills, under the cloud mode; null otherwise
|
|
152
153
|
return {
|
|
153
|
-
vendor, authMode, engine: id,
|
|
154
|
+
vendor, authMode, cloud, engine: id,
|
|
154
155
|
async turn({ prompt }) {
|
|
155
156
|
let tuple;
|
|
156
157
|
try { tuple = await runTurn({ message: prompt, model, thinking, timeoutSec, stallSec }); }
|
|
157
158
|
catch (e) {
|
|
158
159
|
return { ok: false, cause: `the engine turn threw: ${String(e?.message ?? e).slice(0, 200)}`,
|
|
159
|
-
model: null, vendor, authMode, engine: id, usage: { input: 0, output: 0 }, truncationObservable: false };
|
|
160
|
+
model: null, vendor, authMode, cloud, engine: id, usage: { input: 0, output: 0 }, truncationObservable: false };
|
|
160
161
|
}
|
|
161
|
-
return readJxTuple(tuple, { vendor, authMode, engine: id });
|
|
162
|
+
return readJxTuple(tuple, { vendor, authMode, cloud, engine: id });
|
|
162
163
|
},
|
|
163
164
|
};
|
|
164
165
|
}
|
|
@@ -627,6 +627,19 @@ serve({
|
|
|
627
627
|
},
|
|
628
628
|
},
|
|
629
629
|
},
|
|
630
|
+
batch: {
|
|
631
|
+
type: "integer",
|
|
632
|
+
minimum: 1,
|
|
633
|
+
description:
|
|
634
|
+
"The batch of records this call accounts for, when the dispatch splits the band into " +
|
|
635
|
+
"batches. Send one call per batch, carrying its number. Every record in THAT batch must end " +
|
|
636
|
+
"in this call — a findings row, an incumbent row, a Negative-results drop, or a " +
|
|
637
|
+
"Disagreement resolution — and the call is refused naming any that end nowhere; the records " +
|
|
638
|
+
"in every other batch are not this call's business. A batch call MERGES onto what you have " +
|
|
639
|
+
"already recorded, so earlier batches are kept without re-sending them, and a record ended " +
|
|
640
|
+
"under one batch cannot be ended again under another. Omit it only when you are sending the " +
|
|
641
|
+
"whole band in one call, which the dispatch tells you when it is.",
|
|
642
|
+
},
|
|
630
643
|
patch: {
|
|
631
644
|
type: "boolean",
|
|
632
645
|
description:
|
|
@@ -29,9 +29,11 @@ import { join } from "node:path";
|
|
|
29
29
|
import { runStreamingChild, absolutizeSkillRefs, WRITE_DISCIPLINE, buildEnvelope, resolveSpawnCwd } from "./common.mjs";
|
|
30
30
|
import { renderCodexConfigToml } from "./mcp/codex-config.mjs";
|
|
31
31
|
import { resolveAuthMode } from "./auth.mjs";
|
|
32
|
-
import {
|
|
32
|
+
import { resolveEngineProgram } from "../driver.config.mjs"; // — the one place that finds the program; it reads every spelling of the setting
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
// The same one resolver as the claude adapter (driver.config.mjs resolveEngineProgram), for the same reason:
|
|
35
|
+
// the absolute path it found, or what was asked for when it found nothing.
|
|
36
|
+
const codexBin = () => { const r = resolveEngineProgram("openai-agent"); return r.resolved ?? r.bin; };
|
|
35
37
|
|
|
36
38
|
// tier/alias → codex `-m` model id. opus/sonnet/haiku are the driver's abstract tiers (CONTRACT §3). The
|
|
37
39
|
// GPT ids are ENV-OVERRIDABLE and default to three DISTINCT rungs of the codex ladder — `gpt-5.6-sol`,
|