clearotron 0.3.2-beta.12 → 0.3.2-beta.14
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/CONTRIBUTING.md +6 -5
- package/INSTALL.md +3 -4
- package/README.md +2 -1
- package/bin/clearotron.mjs +7 -1
- package/bin/example.mjs +61 -23
- package/bin/onboard.mjs +16 -2
- package/bin/start.mjs +6 -3
- package/build-info.json +2 -2
- package/docs/CLIENT-MCP.md +6 -6
- package/docs/DELIVERY.md +3 -3
- package/docs/ONBOARDING.md +1 -1
- package/docs/PORTAL.md +3 -3
- package/docs/RELEASES.md +1 -1
- package/docs/SECURITY.md +1 -1
- package/docs/architecture/04-configuration-reference.md +4 -4
- package/docs/architecture/05-config-governance.md +10 -10
- package/docs/architecture/06-operations-runbook.md +3 -3
- package/docs/architecture/07-quality-and-audit.md +1 -1
- package/docs/architecture/08-development-guide.md +2 -2
- package/docs/architecture/09-security-and-data.md +1 -1
- package/docs/decisions/0002-no-dark-functionality.md +1 -1
- package/docs/decisions/0006-what-the-public-repository-carries.md +3 -3
- package/driver/CHANGELOG.md +21 -0
- package/driver/ask-ledger.mjs +1 -1
- package/driver/band-shape.mjs +1 -1
- package/driver/bundled-demos.mjs +2 -2
- package/driver/card-budget.mjs +2 -2
- package/driver/case-law-ledger.mjs +2 -2
- package/driver/connotation-search.mjs +5 -5
- package/driver/contract-e3-backlog.mjs +13 -13
- package/driver/coverage-form.mjs +2 -2
- package/driver/demo-container.mjs +26 -2
- package/driver/disposition-tool.mjs +2 -2
- package/driver/engine/CONTRACT.md +3 -3
- package/driver/engine/mcp/codex-config.mjs +3 -11
- package/driver/engine/mcp/gather-config.mjs +1 -1
- package/driver/engine/openai-agent.mjs +2 -2
- package/driver/findings-model.mjs +21 -6
- package/driver/gateway.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +1 -1
- package/driver/pipeline.mjs +4 -4
- package/driver/placement-union.mjs +1 -1
- package/driver/portal-mcp-client.mjs +1 -1
- package/driver/portal-report.mjs +4 -1
- package/driver/portal-service.mjs +17 -5
- package/driver/predelivery-lint.mjs +9 -4
- package/driver/progress.mjs +37 -1
- package/driver/publish/render-knockout.mjs +31 -6
- package/driver/publish/render.mjs +6 -6
- package/driver/record-discard.mjs +1 -1
- package/driver/register-count.mjs +1 -1
- package/driver/register-digest-record.mjs +1 -1
- package/driver/report-card-record.mjs +2 -2
- package/driver/roster-verdict.mjs +2 -2
- package/driver/search-policy.mjs +1 -1
- package/driver/skeptic-record.mjs +1 -1
- package/driver/stages.mjs +1 -1
- package/driver/suite-census.json +40 -10
- package/driver/systemd/README.md +1 -1
- package/driver/unit-inventory.mjs +2 -2
- package/driver/verify.mjs +1 -1
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/CONNECT.md +8 -8
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/serve.mjs +27 -0
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/free-tier/src/capabilities.js +2 -2
- package/providers/jx/src/core.js +3 -3
- package/providers/jx/src/turn-envelope.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/perplexity/README.md +1 -1
- package/providers/signa/src/capabilities.js +2 -2
- package/providers/signa/src/core.js +2 -2
- package/providers/uspto-local/src/core.js +1 -1
- package/providers/uspto-local/src/sync.js +1 -1
- package/scripts/README.md +2 -6
- package/scripts/citation-anchor-report.mjs +1 -1
- package/scripts/citation-line-check.mjs +3 -3
- package/scripts/e2e.mjs +1 -1
- package/scripts/env-audit.mjs +1 -1
- package/scripts/env-classify.mjs +1 -1
- package/scripts/pack-publishable.mjs +1 -1
- package/scripts/release-artifact-seal.mjs +2 -2
- package/scripts/settings-render-check.mjs +5 -1
- package/scripts/strip-tracker-citations.mjs +4 -4
- package/scripts/test-run.mjs +46 -3
- package/shared/browser-temp-root.mjs +10 -3
- package/shared/client-door.mjs +18 -6
- package/shared/connect-clients.mjs +2 -0
- package/shared/demo-start-args.mjs +10 -0
- package/shared/identifier-scan.mjs +2 -2
- package/shared/invocation.mjs +1 -1
- package/shared/reference-guard-classes.mjs +5 -3
- package/shared/stdio-connect.mjs +80 -28
- package/shared/toml-string.mjs +25 -0
- package/shared/writing-standard-classes.mjs +2 -3
package/CONTRIBUTING.md
CHANGED
|
@@ -60,9 +60,10 @@ Four more things run for free:
|
|
|
60
60
|
| `npx clearotron demo` | Replays `demo/` — a real run on a fictional mark — through the real publisher into `~/trademark-demo/pool` and serves it. No keys, no model, no engine. |
|
|
61
61
|
|
|
62
62
|
**About `npx clearotron demo`.** `npx clearotron demo --no-open --once` publishes the sample and exits without
|
|
63
|
-
touching a browser. `
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
touching a browser. Without `--once` it serves the portal until it is stopped: `--no-open` keeps it from
|
|
64
|
+
opening a browser, `--port <n>` moves its three doors to `n`, `n+1` and `n+2`, and `--keep` leaves its folder
|
|
65
|
+
behind when it stops. `npx clearotron demo --pool <dir>` puts the pool somewhere else, and
|
|
66
|
+
`npx clearotron demo --run-dir <dir>` replays a finished run of your own instead of the shipped example.
|
|
66
67
|
|
|
67
68
|
Fix a bug, add a jurisdiction, add a doctrine test case: all of it is reachable from here.
|
|
68
69
|
|
|
@@ -90,7 +91,7 @@ this tree does not carry has to be declared with its reason, and the publication
|
|
|
90
91
|
fails an undeclared one.
|
|
91
92
|
|
|
92
93
|
**`driver/skills/**` is engine input, not documentation.** Those Markdown files are the prompt payload
|
|
93
|
-
served to the model at run time — `synthesis-rules.md` is a
|
|
94
|
+
served to the model at run time — `synthesis-rules.md` is a 12,000-word program. Editing them for
|
|
94
95
|
brevity, tone or tidiness changes what a clearance concludes. Nothing in this section, and nothing in
|
|
95
96
|
any writing pass over the documentation, applies to them.
|
|
96
97
|
|
|
@@ -192,7 +193,7 @@ with no runner assigned and no steps recorded, and every job that gates on it is
|
|
|
192
193
|
this repository: two runs on one SHA, thirty minutes apart, the push run green on every job and the
|
|
193
194
|
scheduled one red having run nothing.
|
|
194
195
|
|
|
195
|
-
That is **could
|
|
196
|
+
That is **a check that could not look**, not a fault, and this tool says so rather than making you open the run and
|
|
196
197
|
read timings to find out. It does not block on it — nothing ran, so nothing can have regressed.
|
|
197
198
|
|
|
198
199
|
**It also does not call it green.** A run that never ran told you nothing about `main`, and the last
|
package/INSTALL.md
CHANGED
|
@@ -242,16 +242,15 @@ refuses for every real user.
|
|
|
242
242
|
|
|
243
243
|
Two further constraints, both of which stop a package being cut from just anywhere:
|
|
244
244
|
|
|
245
|
-
- **The de-identification scan needs a full clone.** It refuses a shallow one (exit 2 — could
|
|
246
|
-
not a pass) because it walks history it cannot see.
|
|
245
|
+
- **The de-identification scan needs a full clone.** It refuses a shallow one (exit 2 — a check that could
|
|
246
|
+
not look, not a pass) because it walks history it cannot see.
|
|
247
247
|
- **It also needs its table**, passed with `--blocklist` (or `CLEAROTRON_IDENTIFIER_BLOCKLIST`), and
|
|
248
248
|
that table lives in the private config store rather than in this repository. Without it the scan
|
|
249
249
|
exits 2 rather than arming a weaker rule set quietly.
|
|
250
250
|
|
|
251
251
|
Both refusals are correct and neither is a workaround to route around.
|
|
252
252
|
|
|
253
|
-
|
|
254
|
-
stylistic choice.** This repository *is* the `clearotron` package, and npm links a package's `bin` into
|
|
253
|
+
This repository *is* the `clearotron` package, and npm links a package's `bin` into
|
|
255
254
|
`node_modules/.bin` only for its **dependencies** — never for the package itself. So after `npm install`
|
|
256
255
|
succeeds there is no `clearotron` on your `PATH` and none in `node_modules/.bin`; a bare `clearotron`
|
|
257
256
|
would be `command not found` with nothing having failed.
|
package/README.md
CHANGED
|
@@ -33,7 +33,8 @@ the address in your browser. No sign-up, no credentials, no network calls to us.
|
|
|
33
33
|
The demo runs for as long as that window stays open, and removes everything it made when you close it —
|
|
34
34
|
nothing of it is left on the machine, and running it again later starts clean. If you want to keep the
|
|
35
35
|
sample reports after closing the window, run `npx clearotron demo --keep`; it prints the one command
|
|
36
|
-
that removes the folder when you are done with it.
|
|
36
|
+
that removes the folder when you are done with it. `npx clearotron demo --once` publishes the reports and
|
|
37
|
+
exits, and keeps the folder the same way.
|
|
37
38
|
|
|
38
39
|
**Then install it.**
|
|
39
40
|
|
package/bin/clearotron.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
|
25
25
|
import { nodeFloorVerdict, nodeFloorRefusal } from "../shared/node-floor.mjs"; // — one floor, read from package.json
|
|
26
26
|
import { invocationPrefix } from "../shared/invocation.mjs"; // — print a command the reader can type
|
|
27
27
|
import { watchParent } from "../shared/parent-watch.mjs";
|
|
28
|
+
import { EXPERIMENTAL_WARNING_OFF } from "../shared/demo-start-args.mjs";
|
|
28
29
|
|
|
29
30
|
export const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
30
31
|
|
|
@@ -193,7 +194,12 @@ const [verb, ...rest] = process.argv.slice(2);
|
|
|
193
194
|
// — tell the child how the READER reached us, so its own advice names a command they can type.
|
|
194
195
|
// Without this every spawned verb sees argv[1] = its own implementation file and would print `npx`
|
|
195
196
|
// even for somebody who typed a bare `clearotron`.
|
|
196
|
-
|
|
197
|
+
// THE DEMO RUNS WITHOUT NODE'S EXPERIMENTAL-FEATURE WARNING. Node prints one the first time anything loads
|
|
198
|
+
// its built-in SQLite, and on the demo it landed on the first screen, above the sentence saying what the
|
|
199
|
+
// demo is. A flag on the demo's own process; `bin/example.mjs` hands it on to the services it starts
|
|
200
|
+
// (EXPERIMENTAL_WARNING_OFF). Every other warning still prints.
|
|
201
|
+
const quiet = verb === "demo" ? [EXPERIMENTAL_WARNING_OFF] : [];
|
|
202
|
+
const child = spawn(process.execPath, [...quiet, target, ...builtin, ...rest], {
|
|
197
203
|
stdio: "inherit",
|
|
198
204
|
env: { ...process.env, CLEAROTRON_INVOKED_AS: process.argv[1] ?? "" },
|
|
199
205
|
});
|
package/bin/example.mjs
CHANGED
|
@@ -38,7 +38,7 @@ import "../shared/env-local.mjs"; // step 4 / — FIRST: this program read a
|
|
|
38
38
|
// handed it nothing and the read fell through to a default. Proven both ways from one
|
|
39
39
|
// environment: without this import the value is invisible, with it the retired spelling is
|
|
40
40
|
// back-filled. Placed above every other import because a side-effecting import runs in order.
|
|
41
|
-
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
41
|
+
import { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
42
42
|
import { homedir, tmpdir } from "node:os";
|
|
43
43
|
import { removeDirectory } from "../shared/os-advice.mjs";
|
|
44
44
|
import { invoke } from "../shared/invocation.mjs"; // — the printed command is resolved once, for the reader who is actually standing there
|
|
@@ -47,13 +47,13 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
47
47
|
import { spawn } from "node:child_process";
|
|
48
48
|
import { BRAND } from "../shared/brand.mjs"; // — the installer's own name, from the tenant seam
|
|
49
49
|
import { envFrom } from "../shared/env-aliases.mjs"; // — resolves EITHER spelling; names the retired one because that is the live-writable half
|
|
50
|
-
import { isFrozen, demoChildren, demoInventory, prepareSample } from "../driver/demo-container.mjs"; // — one definition of what a frozen demo is, for the player AND the gate
|
|
50
|
+
import { isFrozen, demoChildren, demoInventory, prepareSample, releaseDemoCopies } from "../driver/demo-container.mjs"; // — one definition of what a frozen demo is, for the player AND the gate
|
|
51
51
|
import { ensureDemoProgram, demoProgramEnv } from "../shared/permanent-install.mjs";
|
|
52
52
|
|
|
53
53
|
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
54
54
|
|
|
55
55
|
import { usageBlock } from "../shared/usage-block.mjs";
|
|
56
|
-
import { demoStartArgs } from "../shared/demo-start-args.mjs";
|
|
56
|
+
import { demoStartArgs, withWarningOff } from "../shared/demo-start-args.mjs";
|
|
57
57
|
import { bundleVerdict } from "../shared/bundle-freshness.mjs";
|
|
58
58
|
const argv = process.argv.slice(2);
|
|
59
59
|
const flag = (n, d = null) => { const i = argv.indexOf(n); return i >= 0 ? argv[i + 1] : d; };
|
|
@@ -168,6 +168,14 @@ if (!sampleDirs.length || !isFrozen(sampleDir)) {
|
|
|
168
168
|
// rest are published. An unreadable file inside one sample used to throw out of the copy here and take
|
|
169
169
|
// every demo down with a stack trace (measured on a published beta, 2026-09-11).
|
|
170
170
|
const failures = ALL ? inventory.unusable.map((u) => ({ name: u.name, why: u.why })) : [];
|
|
171
|
+
// A STOP WHILE THE SAMPLES ARE COPIED OR PUBLISHED still takes the copies with it. The copies go on exit
|
|
172
|
+
// (driver/demo-container.mjs), and a signal's default ends the process without one. Taken off again once
|
|
173
|
+
// the copies are gone, before the portal's own handlers are installed.
|
|
174
|
+
const stopEarly = [["SIGINT", 2], ["SIGTERM", 15], ["SIGHUP", 1]].map(([sig, no]) => {
|
|
175
|
+
const h = () => process.exit(128 + no);
|
|
176
|
+
process.once(sig, h);
|
|
177
|
+
return [sig, h];
|
|
178
|
+
});
|
|
171
179
|
const samples = [];
|
|
172
180
|
for (const dir of sampleDirs) {
|
|
173
181
|
const r = prepareSample(dir, { repoRoot: REPO });
|
|
@@ -250,6 +258,14 @@ if (existsSync(poolRoot) && !statSync(poolRoot).isDirectory()) die(`demo: ${pool
|
|
|
250
258
|
|
|
251
259
|
// ── 3. replay ────────────────────────────────────────────────────────────────────────────────────────
|
|
252
260
|
console.log(`\n ${BRAND.name} ${BRAND.product.toLowerCase()} — demo\n`);
|
|
261
|
+
// THE LABEL, FIRST. The reader is about to look at a document that reads like advice about a real mark. It
|
|
262
|
+
// is not, and the demo says so before anything else rather than in a footnote nobody reaches. It printed
|
|
263
|
+
// after the replay, below the engine's own diagnostics and a Node warning, so the first thing a newcomer
|
|
264
|
+
// read was internal tallies (measured on the published beta, 2026-09-19).
|
|
265
|
+
console.log(" Real engine output for the fictional mark VENQORI, captured against Clarivate Compumark.");
|
|
266
|
+
console.log(" Replaying it needs no account, no key and no network.");
|
|
267
|
+
console.log(" Every number, band and citation below was produced by that real run and is being");
|
|
268
|
+
console.log(" re-rendered from its artifacts. It is an example, not advice.\n");
|
|
253
269
|
console.log(samples.length === 1
|
|
254
270
|
? ` sample: ${samples[0].dir}`
|
|
255
271
|
: ` samples: ${samples.length}${failures.length ? ` of ${shipped}` : ""} — ${samples.map((x) => x.name).join(", ")}`);
|
|
@@ -268,26 +284,42 @@ const { republishRun } = await import(pathToFileURL(join(REPO, "driver", "publis
|
|
|
268
284
|
// The failures are collected and reported together at the end, and the process exits non-zero, because a
|
|
269
285
|
// demo that came up missing a quarter of itself is not a success however good the three look.
|
|
270
286
|
const results = [];
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
287
|
+
// THE PUBLISHER'S OWN DIAGNOSTICS GO TO A FILE. It prints its tallies as it works (`[record-links] …`,
|
|
288
|
+
// `knockout receipts: …`), which is right for an operator watching a delivery and wrong as the first
|
|
289
|
+
// screen a newcomer reads. The replay's output is written to `replay.log` in the folder this demo made,
|
|
290
|
+
// and restored when the replay ends, so everything below prints as before. A failure is not lost: it is
|
|
291
|
+
// collected here and reported after the list.
|
|
292
|
+
const replayLog = join(flag("--pool") ? poolRoot : demoBase, "replay.log");
|
|
293
|
+
const toLog = (chunk, encoding, cb) => {
|
|
294
|
+
try { appendFileSync(replayLog, chunk, typeof encoding === "string" ? encoding : undefined); } catch { /* a log that cannot be written must not stop the replay */ }
|
|
295
|
+
if (typeof encoding === "function") encoding(); else if (typeof cb === "function") cb();
|
|
296
|
+
return true;
|
|
297
|
+
};
|
|
298
|
+
const screen = { out: process.stdout.write, err: process.stderr.write };
|
|
299
|
+
process.stdout.write = toLog;
|
|
300
|
+
process.stderr.write = toLog;
|
|
301
|
+
try {
|
|
302
|
+
for (const s0 of samples) {
|
|
303
|
+
try {
|
|
304
|
+
// poolUrl "" on purpose: the report's own link block is for a deployment that serves the pool at a
|
|
305
|
+
// public URL. This one is served from this process, at a port picked below.
|
|
306
|
+
results.push({ ...s0, published: await republishRun({ runId: s0.meta.runId, meta: s0.meta, pool: poolRoot, poolUrl: "", runDir: join(s0.publishFrom, "run") }) });
|
|
307
|
+
} catch (e) {
|
|
308
|
+
failures.push({ name: s0.name, why: String(e?.message ?? e) });
|
|
309
|
+
}
|
|
278
310
|
}
|
|
311
|
+
} finally {
|
|
312
|
+
process.stdout.write = screen.out;
|
|
313
|
+
process.stderr.write = screen.err;
|
|
279
314
|
}
|
|
315
|
+
// PUBLISHED, SO THE COPIES THEY WERE PUBLISHED FROM GO NOW, whatever comes next.
|
|
316
|
+
releaseDemoCopies();
|
|
317
|
+
for (const [sig, h] of stopEarly) process.off(sig, h);
|
|
280
318
|
if (!results.length) {
|
|
281
319
|
die(`demo: no demo could be replayed.`, "", ...failures.map((f) => ` ${f.name}: ${f.why}`));
|
|
282
320
|
}
|
|
283
321
|
const published = results[0].published;
|
|
284
322
|
|
|
285
|
-
// THE LABEL. The reader is about to look at a document that reads like advice about a real mark. It is
|
|
286
|
-
// not, and the demo says so before the browser opens rather than in a footnote nobody reaches.
|
|
287
|
-
console.log(" Real engine output for the fictional mark VENQORI, captured against Clarivate Compumark.");
|
|
288
|
-
console.log(" Replaying it needs no account, no key and no network.");
|
|
289
|
-
console.log(" Every number, band and citation below was produced by that real run and is being");
|
|
290
|
-
console.log(" re-rendered from its artifacts. It is an example, not advice.\n");
|
|
291
323
|
// NAMES THE POPULATION. This printed "13 finding(s)" beside a report showing
|
|
292
324
|
// twelve, in the first sentence a reader meets. The number is not wrong — it comes from the audit
|
|
293
325
|
// spine (`driver/publish/audit-from-spine.mjs`, `counts.findings`), which counts every finding the run
|
|
@@ -296,10 +328,8 @@ console.log(" re-rendered from its artifacts. It is an example, not advice.\n")
|
|
|
296
328
|
// EACH LANE'S PUBLISHER REPORTS ITS OWN NUMBER, AND THEY ARE NOT THE SAME NUMBER.
|
|
297
329
|
//
|
|
298
330
|
// The clearance branch returns `counts.findings` — every finding in the run's audit spine. The knockout
|
|
299
|
-
// branch returns no `counts` at all; it returns
|
|
300
|
-
//
|
|
301
|
-
// is printed in its own words rather than mapped onto the clearance sentence. They happen to agree on
|
|
302
|
-
// the demo in this tree, which is one member of a class and proves nothing about the metric.
|
|
331
|
+
// branch returns no `counts` at all; it returns the names it screened and the rating each was given, so
|
|
332
|
+
// its line states those rather than a count mapped onto the clearance sentence.
|
|
303
333
|
//
|
|
304
334
|
// The third branch is the point: a lane whose publisher reports no count says so. This line printed a
|
|
305
335
|
// bare "?" to the knockout — a could-not-look wearing the costume of a number.
|
|
@@ -311,9 +341,13 @@ const spineOf = (pub) =>
|
|
|
311
341
|
Number.isFinite(pub.counts?.findings)
|
|
312
342
|
? `${pub.counts.findings} finding(s) recorded in the run's audit trail; the report shows
|
|
313
343
|
the ones it retains`
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
344
|
+
// A KNOCKOUT STATES WHAT IT SCREENED AND HOW EACH NAME WAS RATED, in the words its own report opens
|
|
345
|
+
// with ("One name screened: VENQORI, rated Low."). It used to count citations traced to held evidence,
|
|
346
|
+
// which a knockout's register-only findings never carry, so the line read "0 finding(s)" and a stranger
|
|
347
|
+
// took it as "the demo found nothing". Chosen 2026-09-19.
|
|
348
|
+
: Array.isArray(pub.reports) && pub.reports.length
|
|
349
|
+
? `${pub.reports.length} name${pub.reports.length === 1 ? "" : "s"} screened: ${pub.reports
|
|
350
|
+
.map((r) => (r.band ? `${r.mark}, rated ${r.band}` : r.mark)).join("; ")}; the report shows why`
|
|
317
351
|
: `this lane's publisher reported no finding count — the report itself is the record`;
|
|
318
352
|
for (const r of results) console.log(` published: ${r.published.runId}\n ${r.name} — ${spineOf(r.published)}`);
|
|
319
353
|
// "ONE PER PRODUCT" ONLY WHEN IT IS TRUE: with a sample missing, the count below says how many of how many.
|
|
@@ -407,6 +441,10 @@ console.log("");
|
|
|
407
441
|
// made, it starts from here as before.
|
|
408
442
|
const programRoot = ensureDemoProgram({ base: demoBase, say: (line) => console.log(line) });
|
|
409
443
|
const startFrom = programRoot ?? REPO;
|
|
444
|
+
// The services inherit the demo's Node flag through NODE_OPTIONS, so none of them prints the SQLite warning
|
|
445
|
+
// either. Set on this process's own environment, which both branches below hand on; the reader's own
|
|
446
|
+
// NODE_OPTIONS is kept.
|
|
447
|
+
process.env.NODE_OPTIONS = withWarningOff(process.env.NODE_OPTIONS);
|
|
410
448
|
const child = spawn(process.execPath, [join(startFrom, "bin", "start.mjs"), ...startArgs], {
|
|
411
449
|
cwd: startFrom, stdio: ["ignore", "inherit", "inherit"],
|
|
412
450
|
env: programRoot ? demoProgramEnv(process.env) : process.env,
|
package/bin/onboard.mjs
CHANGED
|
@@ -3344,8 +3344,22 @@ export async function runCheck() {
|
|
|
3344
3344
|
sock.once("error", (e) => done(e.code === "ECONNREFUSED" ? false : null));
|
|
3345
3345
|
});
|
|
3346
3346
|
} catch { /* stays null — nobody could ask */ }
|
|
3347
|
+
// WHOSE LISTENER. Only this install's own evidence counts: its unit running (which binds this very
|
|
3348
|
+
// port), or a live start of this install whose record names the port. Anything else on the port is
|
|
3349
|
+
// somebody's process, and on a shared box usually another account's door.
|
|
3350
|
+
let doorOwnListener = null;
|
|
3351
|
+
if (doorListening === true) {
|
|
3352
|
+
if (doorActive === true) doorOwnListener = true;
|
|
3353
|
+
else {
|
|
3354
|
+
try {
|
|
3355
|
+
const { clientDoorPort: portOf } = await import(pathToFileURL(join(REPO, "shared", "client-door.mjs")).href);
|
|
3356
|
+
const { readRunning } = await import(pathToFileURL(join(REPO, "shared", "running-start.mjs")).href);
|
|
3357
|
+
if (readRunning().some((r) => r?.ports?.client === portOf(doorEnv))) doorOwnListener = true;
|
|
3358
|
+
} catch { /* stays null — an unread record is not a claim either way */ }
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3347
3361
|
const door = doorState({ env: doorEnv, unitDir, exists: existsSync, active: doorActive,
|
|
3348
|
-
listening: doorListening, activeState: doorActiveState, subState: doorSubState });
|
|
3362
|
+
listening: doorListening, ownListener: doorOwnListener, activeState: doorActiveState, subState: doorSubState });
|
|
3349
3363
|
// EVERY COMMAND THROUGH `invoke`. Doctor's own guard runs every command doctor
|
|
3350
3364
|
// prints from a directory that is not the install; a literal `clearotron start` in that text is
|
|
3351
3365
|
// `command not found` for a reader with no shim, and the guard caught exactly that the moment this
|
|
@@ -4272,7 +4286,7 @@ try {
|
|
|
4272
4286
|
// Enter yield the empty string, `present("")` is false, and askValue loops with "A value is needed
|
|
4273
4287
|
// here." — so a reader taking the header at its word ("Enter takes the default in brackets") on the
|
|
4274
4288
|
// one prompt that advertises empty as an answer could not leave it. Driven on the merged tree:
|
|
4275
|
-
// 60 enters, killed at the cap, every cycle this prompt. Same defect class as
|
|
4289
|
+
// 60 enters, killed at the cap, every cycle this prompt. Same defect class as the
|
|
4276
4290
|
// engine-menu loop, arriving one commit after it was fixed, in a prompt this branch's own sibling
|
|
4277
4291
|
// added.
|
|
4278
4292
|
//
|
package/bin/start.mjs
CHANGED
|
@@ -1610,9 +1610,12 @@ if (isMain) {
|
|
|
1610
1610
|
// — `clearotron demo` hands over to this — so fixing the player alone left the defect where it was.
|
|
1611
1611
|
// ONE SAMPLE AT A TIME: one whose files cannot be read is left out and named below, and the others
|
|
1612
1612
|
// seed. Copied in one call, a single unreadable file emptied the whole archive.
|
|
1613
|
-
const { publishContainer, seedDemoRuns } = await import("../driver/demo-container.mjs");
|
|
1613
|
+
const { publishContainer, seedDemoRuns, releaseDemoCopies } = await import("../driver/demo-container.mjs");
|
|
1614
1614
|
const container = publishContainer(join(REPO, "demo"), { repoRoot: REPO });
|
|
1615
|
-
|
|
1615
|
+
let seed;
|
|
1616
|
+
// THE COPY GOES AS SOON AS THE POOL IS SEEDED FROM IT, and on the way out if seeding throws.
|
|
1617
|
+
try { seed = await seedPool({ pool: paths.pool, examplesDir: container.dir, republish: republishRun }); }
|
|
1618
|
+
finally { releaseDemoCopies(); }
|
|
1616
1619
|
// AND AS RUNS, so the assistant this demo's connect line wires has them to list, brief and open. Under
|
|
1617
1620
|
// the demo's own workspace only: nothing of it reaches an install started afterwards. Their report
|
|
1618
1621
|
// links are stamped with this portal's address, the one the Open line prints.
|
|
@@ -2565,7 +2568,7 @@ if (isMain) {
|
|
|
2565
2568
|
// THE HEADINGS COME WITH THE PAIR, from the composer. Nothing is written here: the page prints these
|
|
2566
2569
|
// same two words above these same two commands, and a second author is how the two surfaces drift.
|
|
2567
2570
|
if (connect.variants) {
|
|
2568
|
-
for (const v of connect.variants) { say(` ${v.heading}`); say(` ${v.text}`); say(""); }
|
|
2571
|
+
for (const v of connect.variants) { say(` ${v.heading}`); say(` ${v.text}`); if (v.hint) say(` ${v.hint}`); say(""); }
|
|
2569
2572
|
} else {
|
|
2570
2573
|
say(` ${connect.command}`);
|
|
2571
2574
|
say("");
|
package/build-info.json
CHANGED
package/docs/CLIENT-MCP.md
CHANGED
|
@@ -46,10 +46,10 @@ uses). Enrolment is therefore the portal's: no second credential to mint, rotate
|
|
|
46
46
|
access revokes this with it. **Off unless `CLIENT_MCP_ACCOUNT_ACCESS=1`.**
|
|
47
47
|
|
|
48
48
|
**Who turns that on. The installer, since 2026-09-03** — ruling, settled
|
|
49
|
-
point 2. `render-units.mjs --apply` and `
|
|
49
|
+
point 2. `render-units.mjs --apply` and `clearotron start --background` both write the settings this
|
|
50
50
|
door refuses to start without and then place and enable `clearotron-client-mcp.service`. The settings
|
|
51
51
|
come from one authority, `enablePlan` in `shared/client-door.mjs`, which is also what
|
|
52
|
-
`
|
|
52
|
+
`clearotron connect` calls.
|
|
53
53
|
|
|
54
54
|
**This supersedes the 2026-08-31 ruling** *"On demand is fine"*, under which nothing at install and no
|
|
55
55
|
rebuild's enable list could start this unit, because starting it WAS the consent that opened
|
|
@@ -57,10 +57,10 @@ access for each company's people. The owner changed the posture knowingly: **the
|
|
|
57
57
|
gate, not whether a process runs.** A door with no key issued refuses everything, which is the same
|
|
58
58
|
protection by a mechanism that does not depend on a reader finding a verb.
|
|
59
59
|
|
|
60
|
-
**`
|
|
60
|
+
**`clearotron disconnect` therefore revokes a person, not a service** (Q3). It writes the caller's key
|
|
61
61
|
ids to the denylist and strikes them from the record; it does not stop the unit and does not touch
|
|
62
62
|
`CLIENT_MCP_ACCOUNT_ACCESS`, which is the whole install's setting. Cutting everyone off is
|
|
63
|
-
`
|
|
63
|
+
`clearotron disconnect --everyone`, which states how many keys and how many people that is before
|
|
64
64
|
acting — and does not stop the service either.
|
|
65
65
|
|
|
66
66
|
An `account` principal reaches **eighteen** tools, for its own companies only — everything carrying
|
|
@@ -117,8 +117,8 @@ Four things about it are worth knowing before you offer it:
|
|
|
117
117
|
- **The caller cannot choose the model.** The tier is cost and method both, and it is the one argument on
|
|
118
118
|
the one tool that spends. Express the change with `instructions`.
|
|
119
119
|
- **Nothing bounds the spend, by ruling.** `start_run` is stamped `clientPrincipal: true` at the
|
|
120
|
-
chokepoint so `runCaps.dailyRuns` bites it; a what-if job carries no such stamp, because
|
|
121
|
-
|
|
120
|
+
chokepoint so `runCaps.dailyRuns` bites it; a what-if job carries no such stamp, because spend controls
|
|
121
|
+
were deliberately left out of it. Every experiment records what it spent; no door
|
|
122
122
|
refuses the next one. Concurrency IS bounded — `CLEAROTRON_WHATIF_MAX_CONCURRENT`, default 1 — because
|
|
123
123
|
letting a free experiment occupy the box while a paid clearance waits is a different question from
|
|
124
124
|
spend, and the ruling did not touch it.
|
package/docs/DELIVERY.md
CHANGED
|
@@ -284,12 +284,12 @@ So the checks that run are exactly those whose whole input is model-authored tex
|
|
|
284
284
|
**permission-prose**, **scope-numbers-in-prose**, **counting-consistency**,
|
|
285
285
|
**wipo-designation-language**, **prescription-prose**. Every other clearance check is listed in the
|
|
286
286
|
receipt under `notApplicable` with the reason its input does not exist here
|
|
287
|
-
(`KNOCKOUT_ABSENT_BY_DESIGN` in `driver/predelivery-lint.mjs`). That is a third state
|
|
287
|
+
(`KNOCKOUT_ABSENT_BY_DESIGN` in `driver/predelivery-lint.mjs`). That is a third state,
|
|
288
288
|
and it is the point: a check that cannot apply is recorded as absent, never as a silent pass, and
|
|
289
289
|
never as a `pass:false` that would project to the lawyer as a defect.
|
|
290
290
|
|
|
291
|
-
**Why the lane cannot simply skip the lint.** The lint receipt IS the workbook's QC record
|
|
292
|
-
|
|
291
|
+
**Why the lane cannot simply skip the lint.** The lint receipt IS the workbook's QC record,
|
|
292
|
+
so a lane that writes no`_driver/predelivery-lint.json` produces an *empty* QC record
|
|
293
293
|
rather than a deliberately-absent one — and an empty record reads as "not evaluated", never
|
|
294
294
|
"passed". And since recorded defects reach the reviewing lawyer only through the projected
|
|
295
295
|
machine-check sheet on the audit workbook, a lane with nothing to project is a lane whose
|
package/docs/ONBOARDING.md
CHANGED
|
@@ -21,7 +21,7 @@ installer.
|
|
|
21
21
|
## The command
|
|
22
22
|
|
|
23
23
|
```
|
|
24
|
-
|
|
24
|
+
clearotron brandowner add <key> --name "<legal name>" [options]
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
`<key>` is the bundle's filename and its identity everywhere else. It is validated on the way in, so a
|
package/docs/PORTAL.md
CHANGED
|
@@ -99,7 +99,7 @@ There were two ways in and only one of them proved anything: the auth-proxy edge
|
|
|
99
99
|
and handed every caller the same synthetic address. **Both switches are deleted**, along with
|
|
100
100
|
`PORTAL_DEV_EMAIL`. Local mode replaces them with a real sign-in.
|
|
101
101
|
|
|
102
|
-
**Use `
|
|
102
|
+
**Use `clearotron start`.** It is the documented way to run this product on one machine
|
|
103
103
|
([INSTALL.md](../INSTALL.md) §6) and it assembles everything below for you: the portal, the MCP face the Start button
|
|
104
104
|
calls, a minted ops key, the grants file, the saved-search store and the data-plane paths — one command,
|
|
105
105
|
one URL, `Ctrl-C` stops both processes. Nothing here needs an `*_AUTH_DISABLED` variable, and the
|
|
@@ -143,7 +143,7 @@ its dev bypass to provide that**:`TRADEMARK_MCP_AUTH_MODE=token` runs it with a
|
|
|
143
143
|
access key and no auth proxy — loopback only, and refused outright alongside
|
|
144
144
|
`TRADEMARK_MCP_AUTH_DISABLED`, which authenticates nobody. Mint the key with
|
|
145
145
|
`mint-token.mjs --scope ops --sub portal --verbs start_run,stop_run --accounts foxglade`, or let
|
|
146
|
-
`
|
|
146
|
+
`clearotron start` mint one in memory at every start and never write it down.
|
|
147
147
|
|
|
148
148
|
## Putting your own login provider in front
|
|
149
149
|
|
|
@@ -177,7 +177,7 @@ the model.
|
|
|
177
177
|
1. Put the proxy in front of the portal's port and make it require sign-in. The portal must not be
|
|
178
178
|
reachable except through it — an origin anyone can reach directly is an origin with no door.
|
|
179
179
|
2. Set `PORTAL_AUTH_MODE=auth-proxy` and the four values above in `.env`.
|
|
180
|
-
3. `
|
|
180
|
+
3. `clearotron doctor` — the **Portal door** section reports which door is configured and which of
|
|
181
181
|
the four values are present, by name. It never prints their values.
|
|
182
182
|
4. Sign in through the proxy once and confirm the portal shows your address rather than a passphrase
|
|
183
183
|
box.
|
package/docs/RELEASES.md
CHANGED
|
@@ -16,7 +16,7 @@ npm install -g clearotron@beta # newest — cut when there is something wort
|
|
|
16
16
|
| | `latest` (stable) | `beta` |
|
|
17
17
|
|---|---|---|
|
|
18
18
|
| **Version looks like** | `0.2.0` | `0.2.1-beta.4` |
|
|
19
|
-
| **Cut when** | a beta has passed a full clearance run and a from-scratch install by somebody who has never seen the product, and the
|
|
19
|
+
| **Cut when** | a beta has passed a full clearance run and a from-scratch install by somebody who has never seen the product, and the maintainers decide to ship it | when a change lands that is worth testing, or while a stable is being prepared |
|
|
20
20
|
| **Promises** | it installed and ran a real clearance end to end before it was published | it built, and the automated suite passed |
|
|
21
21
|
| **Use it if** | you are running this for real work | you want a fix that landed today, or you are helping test |
|
|
22
22
|
|
package/docs/SECURITY.md
CHANGED
|
@@ -123,7 +123,7 @@ what the mechanism guarantees.*
|
|
|
123
123
|
- One OPERATOR issuance path: `mint-token.mjs` (prints once, stores nothing; `sub` names the
|
|
124
124
|
principal in every audit line; the `jti` printed at mint time is the revocation handle). Two
|
|
125
125
|
automatic minters sit beside it on the same `mintToken`: the clearance publisher mints the report
|
|
126
|
-
link's run-bound `user` token at publish, and `
|
|
126
|
+
link's run-bound `user` token at publish, and `clearotron start` mints the portal's verb-scoped,
|
|
127
127
|
company-capped ops token in memory at every start. Neither prints, and neither is written down.
|
|
128
128
|
- **Revocation**: denylist file checked on every verification; missing file = nothing revoked (the
|
|
129
129
|
denylist can never take all auth down). **Rotation**: two-secret window, flag-day-free.
|
|
@@ -176,11 +176,11 @@ timeout shot, warm patch, backoff), the lane-wedge re-dispatch, and the rate-lim
|
|
|
176
176
|
|---|---|---|
|
|
177
177
|
| `CLEAROTRON_AI` | `anthropic-agent` (default) \| `openai-agent` | Compute engine (a registered provider adapter). `anthropic-agent` = `claude -p`; `openai-agent` = `codex exec` (both off-gateway, same normalized contract). An **unregistered** value — a typo, or the gateway-runtime adapter removed in the extraction — **fails loud**: no silent wrong-provider run. Production default is unchanged. |
|
|
178
178
|
| `CLEAROTRON_AI_BILLING` | `subscription` (default) \| `api-key` \| `cloud` | Billing mode for the selected engine. ONE variable for both, and only the LIVE engine's setting is read — it fills each engine's billing knob, and the engine that is not selected is never consulted. **`anthropic-agent`:** subscription **deletes `ANTHROPIC_API_KEY` from the child env** so `claude -p` uses OAuth subscription credentials (a present key would override them); `api-key` keeps the key — the scale setting and standing fallback; **`cloud`** bills Claude per use through the reader's own cloud account, named by the Claude program's own switch — `CLAUDE_CODE_USE_VERTEX`, `CLAUDE_CODE_USE_FOUNDRY` or `CLAUDE_CODE_USE_BEDROCK`, or `ANTHROPIC_BASE_URL` alone for a gateway (see the credentials table below) — and deletes `ANTHROPIC_API_KEY` as subscription does. **`openai-agent`:** subscription seeds `auth.json` into the per-run `CODEX_HOME` from `CLEAROTRON_OPENAI_AUTH_FILE` (default `~/.codex/auth.json`) and strips API keys; `api-key` keeps `CODEX_API_KEY`; `cloud` is refused. **Fail-loud, never a guess, never the subscription in its place:** `api-key` with no `ANTHROPIC_API_KEY` / `CODEX_API_KEY` throws; on `anthropic-agent`, `cloud` with two switches on, or with none and no `ANTHROPIC_BASE_URL`, throws, and so does a switch left on under `subscription` or `api-key`, because the program bills that cloud whatever this says; a value that is none of the three throws on both engines. `clearotron start` and `clearotron doctor` report each refusal against the settings the services read, naming a value that is not a billing mode by its setting and never quoting it, and the runner refuses the search when it is ordered, before anything is spent. The resolved mode is stamped on every stage telemetry row (`engine`, `authMode`, `apiBilled`, and `cloud`, null when no cloud bills), and the attempt row also carries `providerReported`, the program's own word for who served the turn. |
|
|
179
|
-
| `CLEAROTRON_CLAUDE_PATH` · `CLEAROTRON_CODEX_PATH` | `claude` / `codex` on `PATH`, then the copy Clearotron installed | The engine binary to spawn, ONE PER ENGINE: `CLEAROTRON_CLAUDE_PATH` under `anthropic-agent`, `CLEAROTRON_CODEX_PATH` under `openai-agent`. Only the live engine's is read, and a machine that runs both sets both — they were one variable until it met a machine needing two different paths. A path set here must be **absolute**: stage subprocesses run with cwd set to the run directory, so a relative path does not resolve there, and `
|
|
180
|
-
| `CLEAROTRON_OPENAI_AUTH_FILE` | `~/.codex/auth.json` | The credentials file seeded into the per-run `CODEX_HOME` under subscription billing. Was exempted from the August 2026 rename as an `openai-agent` internal rather than an install-surface name;
|
|
179
|
+
| `CLEAROTRON_CLAUDE_PATH` · `CLEAROTRON_CODEX_PATH` | `claude` / `codex` on `PATH`, then the copy Clearotron installed | The engine binary to spawn, ONE PER ENGINE: `CLEAROTRON_CLAUDE_PATH` under `anthropic-agent`, `CLEAROTRON_CODEX_PATH` under `openai-agent`. Only the live engine's is read, and a machine that runs both sets both — they were one variable until it met a machine needing two different paths. A path set here must be **absolute**: stage subprocesses run with cwd set to the run directory, so a relative path does not resolve there, and `clearotron doctor` refuses one. **Unset**, or set to the bare word `claude` / `codex` (which means the same), the engine uses the program on `PATH` when the machine has one, and otherwise the copy Clearotron installed in `~/.local/share/clearotron/engines`: `clearotron install` offers to put the chosen engine's program there (`@anthropic-ai/claude-code` or `@openai/codex`, this platform's build only), and `clearotron update` refreshes it. Nothing is bundled into the package. A value naming anything else is used as given and never falls back. `clearotron doctor` says which copy it found, and its version when npm installed that copy. |
|
|
180
|
+
| `CLEAROTRON_OPENAI_AUTH_FILE` | `~/.codex/auth.json` | The credentials file seeded into the per-run `CODEX_HOME` under subscription billing. Was exempted from the August 2026 rename as an `openai-agent` internal rather than an install-surface name; a decision of 2026-09-04 **reversed that exemption** and renamed the whole namespace, so it carries the house prefix like everything else. One spelling, no exceptions. |
|
|
181
181
|
| `CLEAROTRON_OPENAI_MODEL_JUDGMENT` / `CLEAROTRON_OPENAI_MODEL_SWEEP` / `CLEAROTRON_OPENAI_MODEL_CHEAP` | `gpt-5.6-sol` / `gpt-5.6-terra` / `gpt-5.6-luna`. | Maps the driver's abstract `opus`/`sonnet`/`haiku` tiers onto the codex ladder, the way the anthropic engine maps onto its own (ruling 2026-09-02, superseding the single-model mapping). A tier that cannot hold its stage contract announces itself at that stage — `missing:negative-results`, `no_coverage_status_row`, `named_band_missing` in `_driver/<stage>.jsonl` — not at delivery. |
|
|
182
182
|
| ↳ why they are three, and what the old measurement still says | superseded 2026-09-02, not erased | The three defaulted to `sol` alone on a MEASUREMENT, never a placeholder: on 2026-08-11/12, with `SWEEP=gpt-5.6-terra` / `CHEAP=gpt-5.6-luna`, every codex clearance died on structural-output gates — `missing:negative-results`, `no_coverage_status_row`, `named_band_missing` — over 2 scenarios × 3 stage families, **byte-identically on retry**, so no retry budget rescued it. That measurement stands for the code and the codex CLI **of that date**, and it is why the judgment tier keeps `sol`. Three weeks of stage-contract work and several codex minor versions sit between it and the 2026-09-02 ruling that split the three. **The symptom to match against this cause is unchanged**: those same three gates, as a fail row in`_driver/<stage>.jsonl`, inside the first few dispatches of a clearance and identical on every retry — a model incapable of a contract is not flaky at it. It reads as an engine bug when the only fault is this configuration. A cheap-tier experiment still belongs in the three constants in `driver/engine/openai-agent.mjs` where a reviewer sees it, not in an untraceable env line. |
|
|
183
|
-
| `CLEAROTRON_DATABASE` | **REQUIRED — no default.** `corsearch` \| `clarivate` \| `signa` \| `euipo` \| `uspto-local` \| `free-tier` | Active register provider — one per run, set in the environment the deploy carries, in every environment including production. There is **no default**: unset resolves to `null` and every use of it throws, so a run refuses at start rather than calling a vendor nobody chose (
|
|
183
|
+
| `CLEAROTRON_DATABASE` | **REQUIRED — no default.** `corsearch` \| `clarivate` \| `signa` \| `euipo` \| `uspto-local` \| `free-tier` | Active register provider — one per run, set in the environment the deploy carries, in every environment including production. There is **no default**: unset resolves to `null` and every use of it throws, so a run refuses at start rather than calling a vendor nobody chose (the removed `corsearch` fallback named a vendor the deployment did not choose). Three are paid global sweeps; `euipo` (EU) and `uspto-local` (US) are free single-office sources, and `free-tier` composes those two as one register. Choosing a free value makes every territory outside its coverage a disclosed *deferred* row. Unknown ids throw loudly, and the gather layer throws at stage time for any provider without a built MCP server. |
|
|
184
184
|
| `CLEAROTRON_CODEX_SANDBOX_BYPASS` | unset | `1` runs `codex exec` with its own sandbox helper bypassed, for hosts where that helper cannot spawn. It removes a defence: with it set, the run dir is writable by the seat and only the deny-hook stands between a stage and `_driver/`. Set it because the host forces it, never for convenience. |
|
|
185
185
|
|
|
186
186
|
## Environment variable reference
|
|
@@ -225,7 +225,7 @@ deployment may override (verify live values per deployment).
|
|
|
225
225
|
| `CLEAROTRON_RUN_LOCK_POLL_MS` | 15000 | Run-slot acquire poll cadence. |
|
|
226
226
|
| `CLEAROTRON_ADMISSION_BUDGET_MS` | 7200000 (2 h) | Runner stops claiming new jobs after this per activation; leftovers re-trigger a fresh activation. |
|
|
227
227
|
| `CLEAROTRON_QUEUE_SCAN_MS` | 10000 (min 1000) | Mid-drain re-scan for newly arrived jobs. |
|
|
228
|
-
| `CLEAROTRON_WHATIF_MAX_CONCURRENT` | 1 (min 1) | How many queued what-ifs the runner drains at once. Deliberately NOT a run-slot: an experiment that took one from `CLEAROTRON_MAX_CONCURRENT_RUNS` could block an admitted paid clearance rather than merely share the box with it. This is a concurrency bound and not a spend control —
|
|
228
|
+
| `CLEAROTRON_WHATIF_MAX_CONCURRENT` | 1 (min 1) | How many queued what-ifs the runner drains at once. Deliberately NOT a run-slot: an experiment that took one from `CLEAROTRON_MAX_CONCURRENT_RUNS` could block an admitted paid clearance rather than merely share the box with it. This is a concurrency bound and not a spend control — spend controls were deliberately left out when what-if opened to each company's people (2026-08-27), and nothing here refuses the next experiment. |
|
|
229
229
|
| `CLEAROTRON_STOP_GRACE_MS` | 60000 | Grace after first SIGTERM before exit(1). |
|
|
230
230
|
| `CLEAROTRON_MAX_CLAIM_AGE_MS` | 172800000 (48 h; 0 disables) | Hard ceiling on a claim's age (from the `.pid` sidecar mtime) — beyond it, re-claim regardless of liveness. |
|
|
231
231
|
| `CLEAROTRON_KNOCKOUT_VARIANT_CAP` | unset (⇒ the lane's own cap) | Ceiling on variants a knockout screens per name. Set only to bound an unusually wide batch; absent means the lane decides. |
|
|
@@ -97,7 +97,7 @@ product doc.
|
|
|
97
97
|
| **Allowlist** (`{version, grants:[{email, customer}]}`) | T2 | git + PR on the `CLIENT_ACCESS_MAP` file | see §2 row 4 | LIVE, file-only; surfaced read-only at `admin.access` |
|
|
98
98
|
| **Ops tokens** (scope ops/user, verbs, companies, TTL) | T4 | `mint-token.mjs` CLI; jti denylist file | operator-held tokens | LIVE, CLI |
|
|
99
99
|
|
|
100
|
-
## 4b. The install surface names
|
|
100
|
+
## 4b. The install surface names
|
|
101
101
|
|
|
102
102
|
The variables a **user or installer** ever types carry the product’s own prefix. They are listed
|
|
103
103
|
by name in §5 below and in the upgrade table in INSTALL.md.
|
|
@@ -123,14 +123,14 @@ has taken it.
|
|
|
123
123
|
|
|
124
124
|
**`CLEAROTRON_JX_LANES` was held back from the August 2026 rename** — it was RETIRED 2026-07-27
|
|
125
125
|
(`pipeline.mjs` `RETIRED_ENV`, `jx-units.mjs`) and nothing reads it, so renaming a dead name looked like
|
|
126
|
-
handing an operator a name that warns about nothing.
|
|
126
|
+
handing an operator a name that warns about nothing. A decision of 2026-09-04 reversed that: the
|
|
127
127
|
whole namespace carries one prefix, dead names included, because a tree spelled two ways costs more
|
|
128
128
|
than a retired row spelled consistently. The per-lane `CLEAROTRON_NATIVE_LANGUAGE_<code>` switch in the same row **is** live and is
|
|
129
|
-
fail-OPEN: unset means ON (§5.5
|
|
129
|
+
fail-OPEN: unset means ON (§5.5).
|
|
130
130
|
|
|
131
131
|
Variables outside the install surface were left alone by the August rename — `CLEAROTRON_ENGINE_MAX_BUFFER`
|
|
132
|
-
and its siblings were never in that window. **That separate decision was taken on 2026-09-04**: the
|
|
133
|
-
|
|
132
|
+
and its siblings were never in that window. **That separate decision was taken on 2026-09-04**: the rename
|
|
133
|
+
became global and pre-cut, so the internals carry the house prefix too and the public tree never
|
|
134
134
|
shows the old namespace. No compatibility layer, no alias reading, no migration — greenfield, and our own
|
|
135
135
|
boxes rebuild.
|
|
136
136
|
|
|
@@ -207,10 +207,10 @@ move once the same window has been read across more runs.
|
|
|
207
207
|
`CLEAROTRON_BAND_TRUTH_GATE` (**never disable in prod — restores the fabrication**),
|
|
208
208
|
`CLEAROTRON_FRAME_REOPEN` (+`CLEAROTRON_FRAME_REOPEN_MAX`=1, `CLEAROTRON_REOPEN_MAX_FETCH`=150),
|
|
209
209
|
`CLEAROTRON_REGISTER_GAP_CLAMP`, `CLEAROTRON_RECALL_PROBES`, `CLEAROTRON_RECALL_TRIPWIRE`, `CLEAROTRON_WARM_RETRY`,
|
|
210
|
-
`CLEAROTRON_MODEL_WIRE_CHECK` (
|
|
210
|
+
`CLEAROTRON_MODEL_WIRE_CHECK` (fails a turn whose
|
|
211
211
|
provider reports a different model FAMILY than the driver asked for; disarming it silences the refusal
|
|
212
212
|
and never the record: `modelActual`/`modelMismatch` keep landing on every dispatch row), `CLEAROTRON_FORM_REPAIR`
|
|
213
|
-
(
|
|
213
|
+
(repairs a form-class stage failure inside the dispatch, up to twice; disarmed, the defect
|
|
214
214
|
falls through to the retry ladder exactly as it did before, visibly, and is never swallowed as
|
|
215
215
|
"validated fine"). Policy knob: `CLEAROTRON_UNREACHABLE_SENIOR`
|
|
216
216
|
(open-item|clamp). Enumerate: `CLEAROTRON_ENUMERATE_CEILING` (600/OR-stack),
|
|
@@ -359,7 +359,7 @@ systemd, writes no heartbeat, and must keep saying "waiting to start" rather tha
|
|
|
359
359
|
repo** — `git grep process.env.CLIENT_ACCESS` here returns nothing, so they are governed here and
|
|
360
360
|
never appear in the audit).
|
|
361
361
|
|
|
362
|
-
**Which identity source the portal runs
|
|
362
|
+
**Which identity source the portal runs — T4, and it is chosen by name, never inferred.**
|
|
363
363
|
`PORTAL_AUTH_MODE` selects the door: unset or `auth-proxy` (the default for a hosted deployment) means
|
|
364
364
|
any login system in front that authenticates in the browser and forwards a verifiable JWT per request.
|
|
365
365
|
**Any OIDC or JWT proxy is a choice per deployment** — for example Cloudflare Access, which is not a
|
|
@@ -367,7 +367,7 @@ special case in the code; `local` means one address and one passphrase on loopba
|
|
|
367
367
|
exactly the same thing** — normalised where the mode is read rather than by an alias row, because
|
|
368
368
|
`shared/env-aliases.mjs` maps variable NAMES and there is no value-alias mechanism.
|
|
369
369
|
|
|
370
|
-
**Bringing your own login provider
|
|
370
|
+
**Bringing your own login provider — T4.**`PORTAL_OIDC_ISSUER`,
|
|
371
371
|
`PORTAL_JWKS_URL`, `PORTAL_EMAIL_CLAIM` and `PORTAL_AUTH_HEADER` are the portal-side spelling of the four
|
|
372
372
|
values the staff MCP face already reads as `TRADEMARK_MCP_OIDC_ISSUER`, `TRADEMARK_MCP_JWKS_URL`,
|
|
373
373
|
`TRADEMARK_MCP_EMAIL_CLAIM` and `TRADEMARK_MCP_AUTH_HEADER`. `makeAccessVerifier` has always accepted them; the portal
|
|
@@ -432,7 +432,7 @@ synthetic identity would answer before the mandatory key ever ran. The mode is l
|
|
|
432
432
|
travels in a header or the query string), requires `TRADEMARK_MCP_ALLOWED_HOSTS` exactly as the
|
|
433
433
|
authenticated door does, and mirrors its mandatory `CLEAROTRON_ACCESS_FILE`.
|
|
434
434
|
|
|
435
|
-
**The local install sets all of the above itself.**`
|
|
435
|
+
**The local install sets all of the above itself.**`clearotron start` (`bin/start.mjs`) is a supervisor:
|
|
436
436
|
it resolves one set of ports, derives `PORTAL_MCP_URL` and `TRADEMARK_MCP_ALLOWED_HOSTS` from them, mints
|
|
437
437
|
the ops key in memory, and hands each child an explicit environment carrying `CLEAROTRON_NO_ENV_FILE=1`. So
|
|
438
438
|
exactly one process in that tree reads `<repo>/.env` — the supervisor — and nothing a laptop runs needs
|
|
@@ -73,7 +73,7 @@ properties below, each of which was learned the expensive way:
|
|
|
73
73
|
mid-run feeds one expensive run two different skill versions. **Call
|
|
74
74
|
check that nothing is in flight and abort if anything is** — read every queue this
|
|
75
75
|
deployment would drain plus the run-slot locks, and refuse on any of them. `--override
|
|
76
|
-
"<reason>"` is the
|
|
76
|
+
"<reason>"` is the operator's deliberate exception; the reason is printed into the deploy output, and a
|
|
77
77
|
blank one exits 2 rather than passing. This step used to be a line of prose asking a human to
|
|
78
78
|
check, which is not a guard on the night it matters. Then **stop the four trigger units** for
|
|
79
79
|
the deploy window and install an **EXIT trap that restarts them on every exit path**, success or
|
|
@@ -184,7 +184,7 @@ rather than stage compute. That gateway is no longer part of the product, so the
|
|
|
184
184
|
to probe and the file went with them. Nothing invoked it: not `package.json`, not `bin/`, not
|
|
185
185
|
`scripts/`, not a unit, not CI. It was run by hand, and it spent real money when it was.
|
|
186
186
|
|
|
187
|
-
What replaces each half: the free path check is `
|
|
187
|
+
What replaces each half: the free path check is `clearotron doctor` plus the runner's own preflights
|
|
188
188
|
(`preflightEngineBinary`, `preflightCredentials`, `preflightDeploymentUrls`), each of which refuses by
|
|
189
189
|
name before a run dir exists. The billable half has no replacement and needs none — stage compute is
|
|
190
190
|
exercised by real runs and the A/B harness, which is what its own note already said.
|
|
@@ -289,7 +289,7 @@ Copy it, point `CLEAROTRON_ACCESS_FILE` at your copy, sign in as one of its addr
|
|
|
289
289
|
exactly that organisation's companies. Then delete it and write your own — it names nobody real, which also
|
|
290
290
|
means it grants nothing you have.
|
|
291
291
|
|
|
292
|
-
`
|
|
292
|
+
`clearotron start` (§6) writes an empty roster (`{"tenants": {}}`) into its state directory: your own staff
|
|
293
293
|
address is admitted, nobody else is enrolled, and enforcement is already on.
|
|
294
294
|
|
|
295
295
|
### Ops tokens — a credential for the verbs that spend
|
|
@@ -269,7 +269,7 @@ write nothing and 7 that do:
|
|
|
269
269
|
`what_if_run` → one sandboxed stage; `what_if_result` → collect a queued one) — **executing** is
|
|
270
270
|
local stdio only, never remote, for any principal. The confirmation token is a deliberate-action
|
|
271
271
|
handshake, not a crypto boundary; the security boundary is ops scope + local-only execution.
|
|
272
|
-
Since
|
|
272
|
+
Since 2026-08-27 an `account` principal reaches all three, and `what_if_run` on that
|
|
273
273
|
path ENQUEUES into `<runDir>/_experiments/_queue/` rather than shelling — `driver/whatif-worker.mjs`,
|
|
274
274
|
drained by the runner, is what spawns the sandbox. Because the token is unsigned, a call on that path must
|
|
275
275
|
also name its `runId` so the grant check fires, and the enqueue refuses a token naming another run.
|
|
@@ -88,7 +88,7 @@ These are the things a well-meaning refactor breaks. Each is enforced somewhere;
|
|
|
88
88
|
|
|
89
89
|
Mechanically a config change ([04](04-configuration-reference.md#model-tiers-and-resolution)); two
|
|
90
90
|
traps and one law. Traps: an alias not registered in the engine's model map now **refuses the
|
|
91
|
-
dispatch by name** (
|
|
91
|
+
dispatch by name** (it used to run sonnet silently and log the alias you asked for, which is
|
|
92
92
|
the `fable` lesson turned into an error), and `CLEAROTRON_SYNTHESIS_MODEL` is read at module load (fine
|
|
93
93
|
for the oneshot service, stale in long-lived processes). The law: any grade-moving
|
|
94
94
|
change — family, effort, tier remap — ships only through the paid A/B against the reference
|
|
@@ -221,7 +221,7 @@ Four realities to respect:
|
|
|
221
221
|
|
|
222
222
|
- **Exclusion is by filename convention only.** Anything named `*.test.mjs` runs in CI; billable or
|
|
223
223
|
manual harnesses must not match the glob (historical one-off proofs with hard-coded dev paths are
|
|
224
|
-
not kept; the last billable hand-run harness, `selftest.mjs`, was deleted
|
|
224
|
+
not kept; the last billable hand-run harness, `selftest.mjs`, was deleted).
|
|
225
225
|
- **The `||=` env guards leak**: a shell exporting a real register credential or
|
|
226
226
|
`CLEAROTRON_PLAN_DISPATCH=on` is *not* overridden by the harness — run the suite in a clean env.
|
|
227
227
|
(CI is safe.)
|
|
@@ -83,7 +83,7 @@ are no root units; everything is `systemd --user`.
|
|
|
83
83
|
are stripped), and **`account`** (a signed-in person on the client face — the runs of the
|
|
84
84
|
companies they are granted, reached either by the CF sign-in with no token, or by a per-person API
|
|
85
85
|
key, `scope: account`, whose companies are re-read from the grants file on every request rather than
|
|
86
|
-
baked into it. Wider *reach* than a report link and, since
|
|
86
|
+
baked into it. Wider *reach* than a report link and, since a decision of 2026-08-27, more
|
|
87
87
|
*depth* too: the audit chain — the audit trail, the reasoning narrative, the record artifacts, a
|
|
88
88
|
register axis, and the `get_run` / `trace` / `decision_timeline` decision walk. Model identity
|
|
89
89
|
and billed counts stay sealed (`get_telemetry`, `get_provider_usage`), as does the reviewers'
|