clearotron 0.3.2-beta.7 → 0.3.2-beta.9
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 +58 -26
- package/CONTRIBUTING.md +8 -8
- package/INSTALL.md +148 -81
- package/README.md +3 -3
- package/SECURITY.md +3 -3
- package/bin/brandowner.mjs +3 -3
- package/bin/framework-preflight.mjs +1 -1
- package/bin/onboard.mjs +637 -216
- package/bin/start.mjs +151 -27
- package/bin/update.mjs +58 -11
- package/build-info.json +2 -2
- package/docs/DELIVERY.md +2 -1
- package/docs/INTAKE.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/architecture/03-run-lifecycle.md +6 -6
- package/docs/architecture/04-configuration-reference.md +32 -14
- package/docs/architecture/05-config-governance.md +23 -8
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/architecture/06-operations-runbook.md +3 -3
- package/docs/architecture/08-development-guide.md +6 -6
- package/docs/configuration.md +5 -5
- package/docs/decisions/0003-credential-model.md +1 -1
- package/docs/writing-standard.md +4 -0
- package/driver/CHANGELOG.md +124 -0
- package/driver/README.md +3 -3
- package/driver/band-size.mjs +59 -0
- package/driver/binding-layers.mjs +1 -1
- package/driver/citation-census.json +3 -3
- package/driver/{prelim-variants-record.mjs → clearance-variants-record.mjs} +24 -24
- package/driver/common-law-receipts.mjs +2 -2
- package/driver/company-bundle.mjs +3 -3
- package/driver/compose-read.mjs +8 -14
- package/driver/config-inventory.mjs +112 -9
- package/driver/consumption-ledger.mjs +2 -2
- package/driver/contract-arm2-baseline.json +2 -5
- package/driver/contract-dictation-registry.mjs +19 -19
- package/driver/contract-e3-backlog.mjs +43 -43
- package/driver/contract-e3-baseline.json +14 -14
- package/driver/contract-vocabulary.mjs +68 -27
- package/driver/deliver-trigger.sh +16 -16
- package/driver/demo-container.mjs +3 -3
- package/driver/dev-portal.mjs +3 -3
- package/driver/disposition-call.mjs +1 -1
- package/driver/door-gates.mjs +41 -7
- package/driver/doubt-ledger.mjs +2 -2
- package/driver/drainer-identity.mjs +34 -8
- package/driver/driver.config.mjs +367 -104
- 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/README.md +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +3 -3
- package/driver/engine/mcp/gather-config.mjs +9 -9
- package/driver/engine/mcp/perplexity-server.mjs +2 -2
- package/driver/engine/mcp/recording-server.mjs +18 -5
- package/driver/engine/openai-agent.mjs +4 -2
- package/driver/engine/probe.mjs +110 -23
- package/driver/enqueue-schema.mjs +6 -2
- package/driver/findings-model.mjs +6 -3
- package/driver/flag-snapshot.mjs +34 -8
- package/driver/form-neighbourhood.mjs +54 -7
- package/driver/framework.mjs +4 -4
- package/driver/gateway.mjs +36 -24
- package/driver/jx-lanes.mjs +23 -4
- package/driver/jx-units.mjs +7 -4
- package/driver/jx.mjs +34 -4
- package/driver/knockout-review-record.mjs +56 -4
- package/driver/known-conflicts.mjs +1 -1
- package/driver/matter-frame-record.mjs +90 -1
- package/driver/named-band.mjs +1 -1
- package/driver/ordinary-words.mjs +51 -0
- package/driver/outbox-backoff.mjs +31 -16
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +2 -2
- package/driver/phase0.mjs +3 -3
- package/driver/pipeline-knockout.mjs +5 -5
- package/driver/pipeline.mjs +396 -81
- package/driver/placement-form.mjs +77 -1
- package/driver/placement-model.mjs +1 -1
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-report.mjs +107 -6
- package/driver/portal-service.mjs +80 -14
- package/driver/portal-upstream.mjs +1 -1
- package/driver/predelivery-lint.mjs +12 -2
- package/driver/preserve-merge.mjs +3 -3
- package/driver/product-rows.mjs +2 -2
- package/driver/products.mjs +1 -1
- package/driver/profiles/README.md +3 -3
- package/driver/profiles/demo-brand-owner.json +2 -2
- package/driver/profiles.mjs +55 -17
- package/driver/progress.mjs +18 -8
- package/driver/provider-usage.mjs +8 -8
- package/driver/publish/index.mjs +154 -8
- package/driver/publish/knockout.mjs +39 -5
- package/driver/publish/pool-admin.mjs +1 -1
- package/driver/publish/publish-inputs.mjs +18 -2
- package/driver/publish/render-knockout.mjs +184 -31
- package/driver/publish/render.mjs +323 -93
- package/driver/publish/report-data.mjs +4 -1
- package/driver/publish/report-topbar.mjs +58 -0
- package/driver/publish/search-depth.mjs +133 -4
- package/driver/publish/templates/report.css +78 -4
- package/driver/publish/xlsx.mjs +20 -1
- package/driver/queue-order.mjs +2 -2
- package/driver/recording-agreement.mjs +1 -1
- package/driver/reference-score.mjs +1 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-count.mjs +50 -5
- package/driver/register-coverage.mjs +161 -1
- package/driver/register-digest-record.mjs +236 -11
- package/driver/register-grant-vocabulary.mjs +1 -1
- package/driver/register-plan.mjs +189 -2
- package/driver/registry-fidelity.mjs +3 -3
- package/driver/repair-composers.mjs +1 -1
- package/driver/repair-contract.mjs +1 -1
- package/driver/replay-archive.mjs +6 -6
- package/driver/report-overview-record.mjs +2 -2
- 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 +5 -5
- package/driver/scope-facts.mjs +20 -5
- package/driver/scope-ledger.mjs +5 -5
- package/driver/search-policy.mjs +22 -12
- package/driver/skills/README.md +15 -15
- package/driver/skills/blind-frame/SKILL.md +2 -2
- package/driver/skills/case-law-citation/SKILL.md +4 -4
- package/driver/skills/case-law-citation/sources/eurlex.md +1 -1
- package/driver/skills/{prelim-common-law → clearance-common-law}/SKILL.md +22 -22
- package/driver/skills/{prelim-common-law → clearance-common-law}/perplexity-prompts.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/SKILL.md +10 -10
- package/driver/skills/{prelim-register → clearance-register}/digest.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/providers/README.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/providers/clarivate.md +37 -35
- package/driver/skills/{prelim-register → clearance-register}/providers/corsearch.md +20 -11
- package/driver/skills/{prelim-register → clearance-register}/providers/signa.md +5 -5
- package/driver/skills/{prelim-register → clearance-register}/register-recipes.md +3 -3
- package/driver/skills/{prelim-register → clearance-register}/status-rules.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/stealth-filer-indicators.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/unit.md +2 -2
- package/driver/skills/{prelim-search → clearance-search}/SKILL.md +31 -31
- package/driver/skills/{prelim-search → clearance-search}/delivery-contract.md +1 -1
- package/driver/skills/{prelim-search → clearance-search}/phase2-execution.md +18 -18
- package/driver/skills/{prelim-search → clearance-search}/synthesis-rules.md +7 -7
- package/driver/skills/{prelim-variants → clearance-variants}/SKILL.md +18 -18
- package/driver/skills/{prelim-variants → clearance-variants}/transliteration-scripts.md +5 -5
- package/driver/skills/frame-diff/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +10 -7
- package/driver/skills/matter-frame/SKILL.md +3 -3
- package/driver/skills/narrative-refutation/SKILL.md +9 -9
- package/driver/skills/placement-inquiry/SKILL.md +5 -5
- package/driver/stage-context.mjs +1 -1
- package/driver/stages-knockout.mjs +4 -4
- package/driver/stages.mjs +65 -61
- package/driver/status-snapshot.mjs +2 -2
- package/driver/suite-census.json +340 -136
- package/driver/surface-exit-verdict.mjs +58 -0
- package/driver/systemd/README.md +9 -6
- package/driver/systemd/clearotron-worker.service +1 -1
- package/driver/terminal-clamp.mjs +109 -1
- package/driver/tokens.mjs +169 -3
- package/driver/unit-environment.mjs +42 -15
- package/driver/unit-inventory.mjs +19 -2
- package/driver/usage-ledger.mjs +1 -1
- package/driver/variant-manifest-model.mjs +4 -4
- package/driver/verify-knockout.mjs +27 -0
- package/driver/verify.mjs +94 -6
- package/driver/whatif-queue.mjs +1 -1
- package/driver/wordlists/en.txt +63906 -0
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/README.md +1 -1
- package/mcp-server/lib/README.md +1 -1
- package/mcp-server/lib/options.mjs +8 -7
- package/mcp-server/lib/plan.mjs +18 -2
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/lib/usage.mjs +3 -3
- package/mcp-server/lib/whatif.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +18 -1
- package/package.json +12 -11
- package/portal-ui/dist/assets/{index-CVOIvdhc.css → index-CtvwLCti.css} +207 -3
- package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-EVaSo5-g.js} +1580 -527
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/README.md +1 -1
- package/providers/_shared/enumerate.mjs +6 -6
- package/providers/_shared/execute-plan.mjs +3 -3
- package/providers/_shared/ledger.mjs +119 -5
- package/providers/_shared/provider-text.mjs +2 -2
- package/providers/_shared/screen.mjs +2 -2
- package/providers/_shared/script-form.mjs +3 -3
- package/providers/_shared/territory-codes.mjs +23 -3
- package/providers/clarivate/README.md +1 -1
- package/providers/clarivate/src/capabilities.js +12 -12
- package/providers/clarivate/src/core.js +37 -43
- package/providers/corsearch/README.md +1 -1
- package/providers/corsearch/src/capabilities.js +5 -5
- package/providers/corsearch/src/core.js +3 -3
- package/providers/jx/README.md +2 -1
- package/providers/jx/src/turn-envelope.mjs +8 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/perplexity/src/core.js +1 -1
- package/providers/signa/README.md +1 -1
- package/providers/signa/src/capabilities.js +42 -49
- package/providers/signa/src/core.js +106 -29
- package/providers/uspto-local/README.md +1 -1
- package/providers/uspto-local/src/sync.js +1 -1
- package/scripts/README.md +1 -0
- package/scripts/ask-ai-render-check.mjs +127 -1
- package/scripts/authority-boundary-probe.mjs +8 -6
- package/scripts/backfill-started-at.mjs +2 -2
- package/scripts/census-merge-driver.mjs +33 -2
- package/scripts/citation-anchor-report.mjs +181 -0
- package/scripts/dead-names.mjs +1 -1
- package/scripts/deprecate-below.mjs +66 -8
- package/scripts/drain-preflight.mjs +1 -1
- package/scripts/e2e.mjs +174 -0
- package/scripts/env-audit.mjs +39 -6
- package/scripts/env-classify.mjs +20 -2
- package/scripts/freeze-example-run.mjs +61 -18
- package/scripts/generated-files-are-current.mjs +69 -4
- package/scripts/live-surface-check.mjs +124 -41
- package/scripts/markdown-link-check.mjs +1 -1
- package/scripts/merge-shape-check.mjs +242 -0
- package/scripts/mint-names-in-force.mjs +5 -5
- package/scripts/mint-offered-territories.mjs +72 -0
- package/scripts/mint-public-residue.mjs +2 -2
- package/scripts/mint-reference-strip-backlog.mjs +2 -2
- package/scripts/mint-suite-census.mjs +75 -2
- package/scripts/mint-writing-standard-backlog.mjs +2 -2
- package/scripts/purge-runs.mjs +7 -7
- package/scripts/reconcile-runs.mjs +2 -2
- package/scripts/release-approve-parked.mjs +20 -2
- package/scripts/release-await-cut.mjs +120 -1
- package/scripts/release-note-required.mjs +76 -8
- package/scripts/report-header-render-check.mjs +164 -0
- package/scripts/settings-render-check.mjs +75 -2
- package/scripts/test-full.mjs +96 -3
- package/scripts/test-run.mjs +10 -0
- package/shared/brand.mjs +27 -0
- package/shared/connect-clients.mjs +39 -11
- package/shared/deployment-box.mjs +7 -2
- package/shared/driver-dir.mjs +1 -1
- package/shared/env-aliases.mjs +1 -1
- package/shared/identifier-scan.mjs +65 -9
- package/shared/identifier-sentinels.mjs +22 -0
- package/shared/names-in-force.mjs +4 -2
- package/shared/offered-territories.json +738 -0
- package/shared/pre-rename-spellings.mjs +53 -0
- package/shared/reference-guard-classes.mjs +40 -2
- package/shared/stdio-connect.mjs +39 -4
- package/shared/tree-commit.mjs +48 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/euipo.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/free-tier.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/uspto-local.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/field-doctrine-pharma.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/firm-wide-reasoning.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/report-prose.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/template-formatting.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/email/generic.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/search-request-form.html +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples.md +0 -0
|
@@ -41,6 +41,8 @@ ladder consumes it without knowing which engine produced it ([gateway.mjs](../ga
|
|
|
41
41
|
sessionRef: string | null, // opaque resume handle (claude session_id | codex thread_id)
|
|
42
42
|
modelWire: string | null, // MODEL GAUGE — the served model id this turn observed (§3);
|
|
43
43
|
// null = nothing was observed, never the requested alias
|
|
44
|
+
providerWire: string | null, // PROVIDER GAUGE — the program's own word for who served the turn
|
|
45
|
+
// ("firstParty", "foundry"); null = not said, or said inconsistently
|
|
44
46
|
signals: { stalled?, noProgress?, hardWall?, rateLimited?, rateLimitBasis?, resetsAt?,
|
|
45
47
|
resetsAtBasis?, usageStreamed?, noStreamEvents?, thought: bool|null },
|
|
46
48
|
// THINKING GAUGE — see below.
|
|
@@ -139,8 +141,8 @@ Stages must name **abstract tiers**, not provider aliases. Per-engine maps:
|
|
|
139
141
|
|
|
140
142
|
| tier | role (stage examples) | anthropic-agent (claude alias) | openai-agent (codex `-m`) |
|
|
141
143
|
|---|---|---|---|
|
|
142
|
-
| `judgment` | matter-frame, register-digest, synthesis, narrative-refutation | `
|
|
143
|
-
| `sweep` | register-unit, case-law, skeptic, report-overview, report-card | `
|
|
144
|
+
| `judgment` | matter-frame, register-digest, synthesis, narrative-refutation | `opus` | `$CLEAROTRON_OPENAI_MODEL_JUDGMENT` |
|
|
145
|
+
| `sweep` | register-unit, case-law, skeptic, report-overview, report-card | `sonnet` | `$CLEAROTRON_OPENAI_MODEL_SWEEP` |
|
|
144
146
|
| `cheap` | saturation-probe | `haiku` | `$CLEAROTRON_OPENAI_MODEL_CHEAP` |
|
|
145
147
|
|
|
146
148
|
**AN UNHONOURED OVERRIDE IS AN ERROR, NOT A SUBSTITUTION** ( corruption 3, 2026-08-03). This
|
|
@@ -150,7 +152,12 @@ anthropic engine, "grade-moving, validated only in the paid A/B". The substituti
|
|
|
150
152
|
and could not be: the telemetry logged the alias that was ASKED FOR, so an arm run at gemini reported
|
|
151
153
|
gemini and ran sonnet. Both tiers are gone — the failover chain was deleted in and both stages
|
|
152
154
|
declare an anthropic tier in `STAGES` — and every engine's model map now **refuses** an alias it cannot
|
|
153
|
-
run (`claudeModel`, `openaiModel`).
|
|
155
|
+
run (`claudeModel`, `openaiModel`). On the anthropic engine a tier goes as the vendor's alias, a catalog
|
|
156
|
+
id in the table (`anthropic/claude-opus-5`) goes as itself, a bare or dated `claude-*` id goes as its
|
|
157
|
+
family's alias, and anything else throws. To hold a tier on one model, set the vendor's own
|
|
158
|
+
`ANTHROPIC_DEFAULT_OPUS_MODEL` / `_SONNET_MODEL` / `_HAIKU_MODEL`, or `ANTHROPIC_DEFAULT_FABLE_MODEL` for
|
|
159
|
+
`fable`, which no stage asks for unless an override names it, as `CLEAROTRON_SYNTHESIS_MODEL=fable` does; each
|
|
160
|
+
reaches the CLI through the stage's environment.
|
|
154
161
|
|
|
155
162
|
**Model provenance — two fields, never collapsed.** Every dispatch row (`_driver/<stage>.jsonl`) and
|
|
156
163
|
every `attempt` row (`_driver/run.jsonl`) carries:
|
package/driver/engine/README.md
CHANGED
|
@@ -13,7 +13,7 @@ does not exist — that is the design, and [`CONTRACT.md`](CONTRACT.md) is the d
|
|
|
13
13
|
| [`CONTRACT.md`](CONTRACT.md) | **The adapter contract.** What an engine must implement, the model-tier map, and what a turn is allowed to assume. Read this before either adapter |
|
|
14
14
|
| `anthropic-agent.mjs` | Spawns `claude -p`. Skill-reference absolutization, `--add-dir` grants, rate-limit and no-progress handling |
|
|
15
15
|
| `openai-agent.mjs` | Spawns `codex exec`. A per-run `CODEX_HOME` carrying a rendered `config.toml`, and the session-rollout reader that recovers the turn's usage |
|
|
16
|
-
| `auth.mjs` | `resolveAuthMode()` — subscription or
|
|
16
|
+
| `auth.mjs` | `resolveAuthMode()` — subscription, API key or (Claude only) cloud account, resolved once per turn and stamped on the telemetry |
|
|
17
17
|
| `probe.mjs` | Drives one cheap turn through whichever adapter is configured, to prove the engine can complete a turn at all. What `npm run setup` spends |
|
|
18
18
|
| `common.mjs` | Helpers both adapters share |
|
|
19
19
|
| `deny-authority-write.mjs` | A PreToolUse hook. `--add-dir` has no read-only form, so the read-only intent over the skills tree is enforced here |
|
|
@@ -27,7 +27,7 @@ succeeding on the other one. A run's manifest records which engine served it.
|
|
|
27
27
|
|
|
28
28
|
## Billing mode is resolved here, and it fails loud
|
|
29
29
|
|
|
30
|
-
`resolveAuthMode()` is the single place
|
|
30
|
+
`resolveAuthMode()` is the single place a turn's billing mode (subscription, API key or cloud account) is decided, and it is deliberately
|
|
31
31
|
unforgiving in one direction:
|
|
32
32
|
|
|
33
33
|
```
|
|
@@ -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
|
}
|
|
@@ -14,7 +14,7 @@ The active register provider is mounted under the single, neutral server key `re
|
|
|
14
14
|
named `register_*` — so the namespaced ids the prompts and allowlists carry (`mcp__register__register_enumerate`)
|
|
15
15
|
stay stable across a provider swap. What is PINNED is the vendor's TOOL tokens, not its name:
|
|
16
16
|
`../../test/provider-neutral-prose.test.mjs` walks `driver/` (skipping `fixtures/`) and fails any `<vendor>_<tool>`
|
|
17
|
-
outside the six `<provider>-server.mjs` files and `../../skills/
|
|
17
|
+
outside the six `<provider>-server.mjs` files and `../../skills/clearance-register/providers/`, exempting a core's own
|
|
18
18
|
`ERROR: … HTTP` diagnostics. The plain vendor NAME is unrestricted: `REGISTER_SERVERS` is keyed by it, and 134 other
|
|
19
19
|
files under `driver/` carry `corsearch`.
|
|
20
20
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// held by FOUR stages — `common-law`, `common-law-half`, `narrative-refutation`, `synthesis`. Because
|
|
13
13
|
// `allowedToolsFor` enumerates every tool on every entry a group resolves to, all four carried
|
|
14
14
|
// `mcp__perplexity__record_dispositions`, while every doctrinal mention of the tool is common-law's:
|
|
15
|
-
// `driver/skills/
|
|
15
|
+
// `driver/skills/clearance-common-law/SKILL.md`, and the two common-law stage dictations in `stages.mjs`.
|
|
16
16
|
// Zero occurrences in synthesis's dictation block, zero in narrative-refutation's doctrine.
|
|
17
17
|
//
|
|
18
18
|
// That is GRANTED-BUT-NEVER-ORDERED, the defect class, in its mirror form: not a stage ordered to
|
|
@@ -75,8 +75,8 @@ async function record_dispositions(params) {
|
|
|
75
75
|
let spec;
|
|
76
76
|
try { spec = validateGridSpec(JSON.parse(readFileSync(grid_spec_path, "utf8"))); }
|
|
77
77
|
catch (err) { return { isError: true, text: `ERROR: grid_spec_path unreadable/invalid (${err.message}). The driver writes this file; do not hand-author it.` }; }
|
|
78
|
-
if (!/\/studio\/prelim-search\//.test(spec.output_path))
|
|
79
|
-
return { isError: true, text: `ERROR: grid spec.output_path must be within a studio/
|
|
78
|
+
if (!/\/studio\/(?:prelim|clearance)-search\//.test(spec.output_path)) // either spelling: an install keeps the studio segment it has
|
|
79
|
+
return { isError: true, text: `ERROR: grid spec.output_path must be within a studio/clearance-search run dir; got ${spec.output_path}` };
|
|
80
80
|
// NEVER THROWN PAST THIS POINT. An exception surfaces to the seat as a tool error naming no row, which
|
|
81
81
|
// tells it nothing about what to fix — the failure mode this transport exists to end.
|
|
82
82
|
try {
|
|
@@ -23,7 +23,7 @@ const BRIDGE = process.env.CLEAROTRON_OAUTH_BRIDGE || join(MCP_DIR, "..", "..",
|
|
|
23
23
|
// ── The register surface is PROVIDER-NEUTRAL ────────────────────────────────────────────────────
|
|
24
24
|
// The active register provider is mounted under the single server key `register`, and every register
|
|
25
25
|
// tool is named `register_*`. The vendor's name survives ONLY inside its own *-server.mjs file and in
|
|
26
|
-
// skills/
|
|
26
|
+
// skills/clearance-register/providers/<provider>.md (the vocabulary doc the spawns are told to read).
|
|
27
27
|
//
|
|
28
28
|
// Why neutral names rather than interpolating `${PROVIDER}_enumerate` into the prose: the register
|
|
29
29
|
// instructions in stages.mjs / pipeline.mjs / gateway.mjs are then correct BY CONSTRUCTION for every
|
|
@@ -275,7 +275,7 @@ const RECORDING = Object.freeze({
|
|
|
275
275
|
+ "DISCOVERY over the run dir (O3c: 21 calls, 1 write / 15 attempts, `ls`/`find`/`cat`), not the "
|
|
276
276
|
+ "enumerable pair frame-diff's Read grant covers",
|
|
277
277
|
},
|
|
278
|
-
// FIFTH —
|
|
278
|
+
// FIFTH — clearance-variants, conversion 3. Classes 3 + 2, and the CLASS 3 half is what makes it
|
|
279
279
|
// different from every conversion before it: O3c measured 9 Bash calls with 4 WRITES across 15
|
|
280
280
|
// attempts, and the shape is `python3 -c` over `variant-manifest.json` — the seat PRE-CHECKING its own
|
|
281
281
|
// JSON before saving it. That is a constraint the transport can check, so under the design's Class 3
|
|
@@ -287,12 +287,12 @@ const RECORDING = Object.freeze({
|
|
|
287
287
|
// enumerable — it derives the manifest from material already on disk that the dictation names — so the
|
|
288
288
|
// seeded `Read` grant carries them. Granting a search tool to a stage whose reads can be listed is
|
|
289
289
|
// what the sanctioned-equivalents design refuses. Same ruling as frame-diff, opposite to matter-frame.
|
|
290
|
-
"
|
|
290
|
+
"clearance-variants": {
|
|
291
291
|
seatWrites: false,
|
|
292
|
-
tools: Object.freeze(["
|
|
292
|
+
tools: Object.freeze(["record_clearance_variants"]),
|
|
293
293
|
reason: "hands back the variant manifest — mark, dominant element, elements, variants with their "
|
|
294
294
|
+ "romanisations, incumbent classes, watchlist owners and the scope-ledger rows — through "
|
|
295
|
-
+ "
|
|
295
|
+
+ "record_clearance_variants instead of hand-formatting a JSON skeleton the driver already "
|
|
296
296
|
+ "strict-parses and a prose twin that restates it; the driver serialises variant-manifest.json, "
|
|
297
297
|
+ "renders variant-manifest.md, and derives scope-ledger.json from the same values rather than by "
|
|
298
298
|
+ "re-parsing a markdown table the seat typed. Gains NO retrieval server and NO search tool (its "
|
|
@@ -320,7 +320,7 @@ const RECORDING = Object.freeze({
|
|
|
320
320
|
// NO `search_run_artifacts`: the dictation names this stage's ONLY two inputs — the settled narrative
|
|
321
321
|
// and findings.json — and already trimmed its declared reads to exactly those two. A read set that
|
|
322
322
|
// short is enumerable by definition, and the design forbids handing a search tool to a stage whose
|
|
323
|
-
// reads can be listed. Same ruling as frame-diff and
|
|
323
|
+
// reads can be listed. Same ruling as frame-diff and clearance-variants.
|
|
324
324
|
"report-overview": {
|
|
325
325
|
seatWrites: false,
|
|
326
326
|
tools: Object.freeze(["record_report_overview"]),
|
|
@@ -671,7 +671,7 @@ const LOCAL = {
|
|
|
671
671
|
//
|
|
672
672
|
// GRANTED BY EXACTLY ONE LANE'S GROUP LIST — `common-law` and its `common-law-half` variant, through
|
|
673
673
|
// the prefix branch in toolGroupsForStage. Every doctrinal mention of the tool is theirs:
|
|
674
|
-
// driver/skills/
|
|
674
|
+
// driver/skills/clearance-common-law/SKILL.md and the two common-law stage dictations in stages.mjs.
|
|
675
675
|
//
|
|
676
676
|
// NOT an allowlist growing by a token this time: it SHRINKS synthesis's and narrative-refutation's
|
|
677
677
|
// argv and RENAMES the token on common-law's (`mcp__perplexity__record_dispositions` →
|
|
@@ -1222,8 +1222,8 @@ export const RECORDING_TOOLS = Object.freeze({
|
|
|
1222
1222
|
// so deriving it would compare a value with itself. matter-frame carries the search tool for the same
|
|
1223
1223
|
// reason skeptic does, on its OWN key — a shared key would hand skeptic a writer into the frame.
|
|
1224
1224
|
// BY HAND, like every row here — O1 compares the resolved grant against it, so a derived row would
|
|
1225
|
-
// compare a value with itself.
|
|
1226
|
-
"
|
|
1225
|
+
// compare a value with itself. clearance-variants carries NO search tool: enumerable inputs, Read serves.
|
|
1226
|
+
"clearance-variants": Object.freeze(["Read", "mcp__recording-clearance-variants__record_clearance_variants"]),
|
|
1227
1227
|
"matter-frame": Object.freeze(["Read", "mcp__recording-matter-frame__record_matter_frame",
|
|
1228
1228
|
"mcp__recording-matter-frame__search_run_artifacts"]),
|
|
1229
1229
|
skeptic: Object.freeze(["Read", "mcp__recording-skeptic__record_skeptic",
|
|
@@ -154,8 +154,8 @@ async function research(params) {
|
|
|
154
154
|
let spec;
|
|
155
155
|
try { spec = validateGridSpec(JSON.parse(readFileSync(grid_spec_path, "utf8"))); }
|
|
156
156
|
catch (err) { return `ERROR: grid_spec_path unreadable/invalid (${err.message}). The driver writes this file; do not hand-author it.`; }
|
|
157
|
-
if (!/\/studio\/prelim-search\//.test(spec.output_path))
|
|
158
|
-
return requiredLedgerRefusal(`ERROR: grid spec.output_path must be within a studio/
|
|
157
|
+
if (!/\/studio\/(?:prelim|clearance)-search\//.test(spec.output_path)) // either spelling: an install keeps the studio segment it has
|
|
158
|
+
return requiredLedgerRefusal(`ERROR: grid spec.output_path must be within a studio/clearance-search run dir; got ${spec.output_path}`, { spec, gridSpecPath: grid_spec_path });
|
|
159
159
|
// — already recorded and complete? Answer from the ledger; do not buy the grid twice.
|
|
160
160
|
const already = recordedLedgerFor(spec);
|
|
161
161
|
if (already) {
|
|
@@ -44,7 +44,7 @@ import { VARIANT_DIRECTIONS, RANKING_BASES } from "../../blind-frame-model.mjs";
|
|
|
44
44
|
import { recordSkeptic } from "../../skeptic-record.mjs";
|
|
45
45
|
import { recordFrameDiff } from "../../frame-diff-record.mjs";
|
|
46
46
|
import { recordMatterFrame, INTAKE_ASK_OWNERS, SCOPE_BASES } from "../../matter-frame-record.mjs";
|
|
47
|
-
import {
|
|
47
|
+
import { recordClearanceVariants, SCOPE_LAYERS, SCOPE_STATUS } from "../../clearance-variants-record.mjs";
|
|
48
48
|
import { recordReportOverview } from "../../report-overview-record.mjs";
|
|
49
49
|
import { recordReportCard } from "../../report-card-record.mjs"; // conversion 5 — the fan-out transport
|
|
50
50
|
import { recordRefutation, REVIEW_VERDICTS } from "../../narrative-refutation-record.mjs"; // conversion 9
|
|
@@ -125,12 +125,12 @@ async function record_knockout_review(params) {
|
|
|
125
125
|
return recordKnockoutReview(runDir, params);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
async function
|
|
128
|
+
async function record_clearance_variants(params) {
|
|
129
129
|
const runDir = String(process.env.CLEAROTRON_BAND_RUN_DIR ?? "");
|
|
130
130
|
if (!runDir) {
|
|
131
131
|
return { error: "this server was started without a run — the driver wires it per run; there is no parameter for it and this tool never guesses one" };
|
|
132
132
|
}
|
|
133
|
-
return
|
|
133
|
+
return recordClearanceVariants(runDir, params);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
async function record_report_card(params) {
|
|
@@ -444,7 +444,7 @@ serve({
|
|
|
444
444
|
//
|
|
445
445
|
// `scope_ledger` is the one genuinely new field, and it is what deletes a derivation: those rows used
|
|
446
446
|
// to reach the driver only by re-parsing a markdown table out of the prose manifest.
|
|
447
|
-
name: "
|
|
447
|
+
name: "record_clearance_variants",
|
|
448
448
|
description:
|
|
449
449
|
"Hand back the variant manifest as VALUES. The driver serialises variant-manifest.json, renders " +
|
|
450
450
|
"variant-manifest.md and writes scope-ledger.json from what you send, so you never format JSON, " +
|
|
@@ -516,7 +516,7 @@ serve({
|
|
|
516
516
|
},
|
|
517
517
|
},
|
|
518
518
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
519
|
-
handler:
|
|
519
|
+
handler: record_clearance_variants,
|
|
520
520
|
}, {
|
|
521
521
|
// ── CONVERSION 4 — THE REPORT SHELL, AND THE FIRST ARTIFACT A CLIENT READS ────────────────────
|
|
522
522
|
//
|
|
@@ -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:
|