clearotron 0.3.2-beta.11 → 0.3.2-beta.13
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 +1 -1
- package/CONTRIBUTING.md +6 -5
- package/INSTALL.md +3 -4
- package/README.md +2 -1
- package/bin/clearotron.mjs +14 -0
- package/bin/connect.mjs +68 -3
- package/bin/example.mjs +12 -1
- package/bin/key.mjs +6 -1
- package/bin/onboard.mjs +23 -3
- package/bin/passphrase.mjs +4 -2
- package/bin/start.mjs +18 -6
- 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 +29 -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/contract-vocabulary.mjs +5 -5
- 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/gather-config.mjs +1 -1
- package/driver/engine/mcp/recording-server.mjs +1 -1
- package/driver/engine/mcp/supplemental.mjs +22 -5
- package/driver/engine/openai-agent.mjs +2 -2
- package/driver/findings-model.mjs +21 -6
- package/driver/gateway.mjs +1 -1
- package/driver/knockout-next-step.mjs +72 -0
- package/driver/named-band.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +27 -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 +25 -3
- package/driver/portal-service.mjs +25 -13
- package/driver/predelivery-lint.mjs +9 -4
- package/driver/progress.mjs +37 -1
- package/driver/publish/render-knockout.mjs +14 -9
- package/driver/publish/render.mjs +6 -6
- package/driver/record-discard.mjs +1 -1
- package/driver/register-availability.mjs +1 -1
- package/driver/register-count.mjs +1 -1
- package/driver/register-digest-record.mjs +1 -1
- package/driver/register-plan.mjs +81 -11
- package/driver/report-card-record.mjs +2 -2
- package/driver/result-noun-fields.mjs +3 -1
- package/driver/roster-verdict.mjs +2 -2
- package/driver/search-policy.mjs +1 -1
- package/driver/skeptic-record.mjs +1 -1
- package/driver/stages-knockout.mjs +1 -1
- package/driver/stages.mjs +1 -1
- package/driver/suite-census.json +72 -24
- package/driver/systemd/README.md +1 -1
- package/driver/unit-inventory.mjs +2 -2
- package/driver/verify-knockout.mjs +0 -27
- 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/dist/assets/{index-CtvwLCti.css → index-7Lq-dXDV.css} +12 -9
- package/portal-ui/dist/assets/{index-DXSRxPV_.js → index-w8GFZftk.js} +110 -69
- package/portal-ui/dist/index.html +2 -2
- 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/ask-ai-render-check.mjs +26 -1
- package/scripts/citation-anchor-report.mjs +1 -1
- package/scripts/citation-line-check.mjs +3 -3
- package/scripts/dead-names.mjs +17 -16
- package/scripts/e2e.mjs +1 -1
- package/scripts/env-audit.mjs +7 -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/repo-writes.mjs +42 -0
- package/scripts/report-sections-render-check.mjs +245 -0
- package/scripts/report-theme-render-check.mjs +31 -3
- package/scripts/settings-render-check.mjs +15 -8
- package/scripts/strip-tracker-citations.mjs +4 -4
- package/scripts/test-run.mjs +108 -6
- package/shared/browser-temp-root.mjs +10 -3
- package/shared/client-door.mjs +38 -6
- package/shared/connect-clients.mjs +2 -0
- package/shared/identifier-scan.mjs +2 -2
- package/shared/invocation.mjs +1 -1
- package/shared/parent-watch.mjs +33 -0
- package/shared/reference-guard-classes.mjs +5 -3
- package/shared/running-start.mjs +14 -3
- package/shared/scope.mjs +22 -3
- package/shared/stdio-connect.mjs +39 -18
- package/shared/writing-standard-classes.mjs +2 -3
package/.env.example
CHANGED
|
@@ -578,7 +578,7 @@ TRADEMARK_MCP_KEY_SOCKET=
|
|
|
578
578
|
# names, read by code that ships.
|
|
579
579
|
|
|
580
580
|
# WHO the chat notice is addressed to: a JSON object of requester email or handle → number, e.g.
|
|
581
|
-
# {"
|
|
581
|
+
# {"robin@tenant.example":"+41...","jordan":"+41..."}. Before this, the notice went to a map keyed by
|
|
582
582
|
# AGENT id, and every user of a deployment shares one agent — so the operator was notified about work
|
|
583
583
|
# other people ordered and the requester was told nothing.
|
|
584
584
|
#
|
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
|
@@ -24,6 +24,7 @@ import { constants as SIG } from "node:os";
|
|
|
24
24
|
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
|
+
import { watchParent } from "../shared/parent-watch.mjs";
|
|
27
28
|
|
|
28
29
|
export const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
29
30
|
|
|
@@ -229,6 +230,19 @@ const [verb, ...rest] = process.argv.slice(2);
|
|
|
229
230
|
};
|
|
230
231
|
process.on(sig, forward);
|
|
231
232
|
}
|
|
233
|
+
|
|
234
|
+
// ── AND THE DEMO STOPS WHEN WHATEVER STARTED IT IS GONE ─────────────────────────────────────────────
|
|
235
|
+
//
|
|
236
|
+
// The forwarding above covers a signal sent to THIS pid. Through npx it is not this pid a reader holds:
|
|
237
|
+
// npm → `sh -c` → this launcher, and a TERM to npm dies at that `sh` (shared/parent-watch.mjs). This
|
|
238
|
+
// launcher is then reparented, and the demo runs on holding its three ports while the reader believes
|
|
239
|
+
// it stopped. So the demo takes its parent's death as a TERM, and stops everything it started.
|
|
240
|
+
//
|
|
241
|
+
// THE DEMO ONLY. A foreground `start` or a `run` outliving the shell that started it can be what the
|
|
242
|
+
// reader meant: `nohup clearotron start &` on a server they are about to log out of, or an hours-long
|
|
243
|
+
// clearance they will not babysit. The demo is a replay nobody leaves running on purpose, and its own
|
|
244
|
+
// banner already says it lasts only as long as the command that started it.
|
|
245
|
+
if (verb === "demo") watchParent(() => { try { child.kill("SIGTERM"); } catch { /* already gone */ } });
|
|
232
246
|
}
|
|
233
247
|
|
|
234
248
|
// RESOLVE BOTH SIDES THROUGH SYMLINKS. `npm install` puts a symlink at node_modules/.bin/clearotron, so
|
package/bin/connect.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
40
40
|
import { requireInteractive } from "../shared/invocation.mjs"; // — a prompt with nobody to answer it
|
|
41
41
|
import { stdin, stdout } from "node:process";
|
|
42
42
|
import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from "node:fs";
|
|
43
|
-
import { join, dirname } from "node:path";
|
|
43
|
+
import { join, dirname, resolve } from "node:path";
|
|
44
44
|
import { homedir, userInfo } from "node:os";
|
|
45
45
|
import { fileURLToPath } from "node:url";
|
|
46
46
|
import { execFileSync } from "node:child_process";
|
|
@@ -48,7 +48,11 @@ import { createServer } from "node:net";
|
|
|
48
48
|
import { CONNECT_CLIENTS, WHERE_FLAG, clientById, leadRouteFor, plainStep, whatItNeeds } from "../shared/connect-clients.mjs";
|
|
49
49
|
import { stdioConnectFor, STDIO_SHAPES } from "../shared/stdio-connect.mjs";
|
|
50
50
|
import { isWsl, wslTarget } from "../shared/wsl.mjs"; // — and which distribution a row should start the server in
|
|
51
|
-
import { defaultDenylistPath, clientDoorAddress, clientDoorPort, clientDoorState, enablePlan, applyEnablePlan, describeChange, recordConnectKey, CLIENT_DOOR_UNIT } from "../shared/client-door.mjs";
|
|
51
|
+
import { defaultDenylistPath, clientDoorAddress, clientDoorPort, clientDoorState, enablePlan, applyEnablePlan, describeChange, recordConnectKey, CLIENT_DOOR_UNIT, demoTokenSecretPath, keyPurposeLine, demoConnectCommand } from "../shared/client-door.mjs";
|
|
52
|
+
import { readRunning } from "../shared/running-start.mjs";
|
|
53
|
+
import { readLocalCredential, passphraseResetCommand, INSTALL_CREDENTIAL_FILE } from "../driver/portal-local-auth.mjs";
|
|
54
|
+
import { invocationPrefix } from "../shared/invocation.mjs";
|
|
55
|
+
import { BRAND } from "../shared/brand.mjs";
|
|
52
56
|
import { mintToken, tokenId, resolvePerson, loadGrants } from "../shared/scope.mjs";
|
|
53
57
|
import { envFrom } from "../shared/env-aliases.mjs";
|
|
54
58
|
import { atomicWrite } from "../driver/progress.mjs";
|
|
@@ -654,6 +658,59 @@ function printSteps(offer, key) {
|
|
|
654
658
|
}
|
|
655
659
|
}
|
|
656
660
|
|
|
661
|
+
/**
|
|
662
|
+
* ── A RUNNING DEMO, NAMED BY ITS FOLDER (owner, 2026-09-19) ─────────────────────────────────────────
|
|
663
|
+
*
|
|
664
|
+
* The demo is how a newcomer meets the product, and the next thing they ask an assistant is to connect.
|
|
665
|
+
* The route that works is the demo's own client door with an account key, and reaching it took a key
|
|
666
|
+
* issued by hand and an address read out of the startup log. This does both: it finds the demo that is
|
|
667
|
+
* running from that folder, mints a key with the demo's own secret for the demo's one user, and prints
|
|
668
|
+
* the address and the key, and nothing else is changed. No unit is written and no setting touched: the
|
|
669
|
+
* demo's door is already open, and everything of the demo goes when it stops.
|
|
670
|
+
*
|
|
671
|
+
* The address route's steps are not printed. They are written for a door behind a sign-in service,
|
|
672
|
+
* reached over the web, and a demo's door is on this machine and takes the key instead.
|
|
673
|
+
*/
|
|
674
|
+
async function connectDemo({ base, dryRun, clientName = null, running = readRunning() } = {}) {
|
|
675
|
+
const at = resolve(base);
|
|
676
|
+
const prefix = invocationPrefix();
|
|
677
|
+
say("");
|
|
678
|
+
if (clientName) { say(` ${clientName}`); say(""); }
|
|
679
|
+
const demo = running.find((r) => r.demo && resolve(String(r.base ?? "")) === at);
|
|
680
|
+
if (!demo) {
|
|
681
|
+
say(` Not available here — no demo is running from ${at}.`);
|
|
682
|
+
say(` What would change it: start it with \`${prefix}clearotron demo --base ${/\s/.test(at) ? `"${at}"` : at}\`, then run this again.`);
|
|
683
|
+
return 1;
|
|
684
|
+
}
|
|
685
|
+
if (!demo.ports?.client) {
|
|
686
|
+
say(` Not available here — the demo running from ${at} has no client door open.`);
|
|
687
|
+
say(" What would change it: stop the demo and start it again; its output says why the door did not start.");
|
|
688
|
+
return 1;
|
|
689
|
+
}
|
|
690
|
+
const address = `http://${!demo.host || demo.host === "0.0.0.0" ? "127.0.0.1" : demo.host}:${demo.ports.client}/mcp`;
|
|
691
|
+
const credentialPath = join(at, INSTALL_CREDENTIAL_FILE);
|
|
692
|
+
let email = "demo@localhost";
|
|
693
|
+
try { email = readLocalCredential(credentialPath)?.email || email; } catch { /* the demo's own user stands */ }
|
|
694
|
+
if (dryRun) {
|
|
695
|
+
say(" (dry run — nothing was changed)");
|
|
696
|
+
say(` Address: ${address}`);
|
|
697
|
+
return 0;
|
|
698
|
+
}
|
|
699
|
+
let secret = "";
|
|
700
|
+
try { secret = readFileSync(demoTokenSecretPath(at), "utf8").trim(); } catch { /* said below */ }
|
|
701
|
+
if (!secret) {
|
|
702
|
+
say(` Not available here — the demo running from ${at} keeps no key secret this command can read.`);
|
|
703
|
+
say(" What would change it: run this as the user who started the demo.");
|
|
704
|
+
return 1;
|
|
705
|
+
}
|
|
706
|
+
const key = withSecret(secret, () => mintToken({ scope: "account", sub: email, ttlSec: 90 * 24 * 3600 }));
|
|
707
|
+
say(` ${keyPurposeLine({ email, brand: BRAND.name, reset: passphraseResetCommand({ prefix, credentialPath }) })}`);
|
|
708
|
+
say("");
|
|
709
|
+
say(` Address: ${address}`);
|
|
710
|
+
say(` Key: ${key}`);
|
|
711
|
+
return 0;
|
|
712
|
+
}
|
|
713
|
+
|
|
657
714
|
async function main() {
|
|
658
715
|
const argv = process.argv.slice(2);
|
|
659
716
|
// `--help` IS ANSWERED, not rejected. Running this bare drops the reader into a question, so "run it
|
|
@@ -674,6 +731,7 @@ async function main() {
|
|
|
674
731
|
say(" internet, with a key made for you now");
|
|
675
732
|
say(" --list the assistants this build knows");
|
|
676
733
|
say(" --dry-run say what would change, change nothing");
|
|
734
|
+
say(" --base <dir> a running demo's folder: mint a key for its client door, and print both");
|
|
677
735
|
say(" --allow-checkout-move");
|
|
678
736
|
say(" write this checkout's path even though services are running from");
|
|
679
737
|
say(" another one. Every unit's ExecStart follows that value, so the ones");
|
|
@@ -681,7 +739,7 @@ async function main() {
|
|
|
681
739
|
say("");
|
|
682
740
|
return 0;
|
|
683
741
|
}
|
|
684
|
-
const known = new Set(["--client", "--where", "--list", "--dry-run", "--allow-checkout-move", "--help", "-h"]);
|
|
742
|
+
const known = new Set(["--client", "--where", "--list", "--dry-run", "--base", "--allow-checkout-move", "--help", "-h"]);
|
|
685
743
|
const unknown = argv.filter((a) => a.startsWith("--") && !known.has(a));
|
|
686
744
|
if (unknown.length) {
|
|
687
745
|
console.error(`connect: unrecognised flag(s): ${unknown.join(", ")}`);
|
|
@@ -689,6 +747,13 @@ async function main() {
|
|
|
689
747
|
process.exit(2);
|
|
690
748
|
}
|
|
691
749
|
const dryRun = argv.includes("--dry-run");
|
|
750
|
+
const b = argv.indexOf("--base");
|
|
751
|
+
if (b >= 0) {
|
|
752
|
+
const base = argv[b + 1];
|
|
753
|
+
if (!base || base.startsWith("--")) { console.error("connect: --base needs the demo's folder."); process.exit(2); }
|
|
754
|
+
const c = argv.indexOf("--client");
|
|
755
|
+
return await connectDemo({ base, dryRun, clientName: c >= 0 ? clientById(argv[c + 1])?.name ?? null : null });
|
|
756
|
+
}
|
|
692
757
|
const w = argv.indexOf("--where");
|
|
693
758
|
if (w >= 0 && !Object.hasOwn(WHERE_FLAG, argv[w + 1] ?? "")) {
|
|
694
759
|
console.error(`connect: --where takes one of: ${Object.keys(WHERE_FLAG).join(", ")}`);
|
package/bin/example.mjs
CHANGED
|
@@ -47,7 +47,7 @@ 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)), "..");
|
|
@@ -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 });
|
|
@@ -277,6 +285,9 @@ for (const s0 of samples) {
|
|
|
277
285
|
failures.push({ name: s0.name, why: String(e?.message ?? e) });
|
|
278
286
|
}
|
|
279
287
|
}
|
|
288
|
+
// PUBLISHED, SO THE COPIES THEY WERE PUBLISHED FROM GO NOW, whatever comes next.
|
|
289
|
+
releaseDemoCopies();
|
|
290
|
+
for (const [sig, h] of stopEarly) process.off(sig, h);
|
|
280
291
|
if (!results.length) {
|
|
281
292
|
die(`demo: no demo could be replayed.`, "", ...failures.map((f) => ` ${f.name}: ${f.why}`));
|
|
282
293
|
}
|
package/bin/key.mjs
CHANGED
|
@@ -28,9 +28,10 @@
|
|
|
28
28
|
// question would be two answers, and the wrong one would be the one nobody read.
|
|
29
29
|
import "../shared/env-local.mjs"; // side effect: apply the install's .env — FIRST, before anything reads process.env
|
|
30
30
|
import { invocationPrefix } from "../shared/invocation.mjs";
|
|
31
|
+
import { BRAND } from "../shared/brand.mjs";
|
|
31
32
|
import { existsSync, readFileSync } from "node:fs";
|
|
32
33
|
import { defaultGrantsPath, installPaths } from "./start.mjs";
|
|
33
|
-
import { demoTokenSecretPath } from "../shared/client-door.mjs";
|
|
34
|
+
import { demoTokenSecretPath, keyPurposeLine } from "../shared/client-door.mjs";
|
|
34
35
|
import { resolvePerson } from "../shared/scope.mjs"; // the door's own reading of the guest list, so one answer serves both
|
|
35
36
|
import { mintFromOptions } from "../mcp-server/mint-token.mjs";
|
|
36
37
|
|
|
@@ -96,6 +97,10 @@ try {
|
|
|
96
97
|
die(e.message);
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
// WHAT THIS KEY IS FOR, FIRST, in the owner's words (2026-09-19): a person who found this verb before the
|
|
101
|
+
// passphrase minted a key and could not sign in to the portal with it. Standard error, as every line here
|
|
102
|
+
// is, so the token stays alone on standard output.
|
|
103
|
+
console.error(keyPurposeLine({ email, brand: BRAND.name, reset: `${p}clearotron passphrase --reset` }));
|
|
99
104
|
for (const line of minted.notes) console.error(line);
|
|
100
105
|
|
|
101
106
|
// — found in review. A KEY FOR AN IDENTITY ON NO LIST IS A KEY THAT 403s. This command
|
package/bin/onboard.mjs
CHANGED
|
@@ -2818,6 +2818,9 @@ export async function runCheck() {
|
|
|
2818
2818
|
info(`the units are installed but their environment could not be read (${unitEnv?.why ?? "no reason given"}) — `
|
|
2819
2819
|
+ "the door verdicts below are withheld rather than guessed, because a failure to look is not a finding");
|
|
2820
2820
|
if (door.shape === "local") {
|
|
2821
|
+
// WHO THAT ONE USER IS, in the owner's words (2026-09-19), resolved as `start` resolves it.
|
|
2822
|
+
const oneUser = String(effectiveForService("PORTAL_LOCAL_USER")?.v || `${userInfo().username}@localhost`).trim().toLowerCase();
|
|
2823
|
+
say(` · Portal sign-in: one user, ${oneUser}, by passphrase.`);
|
|
2821
2824
|
say(` · local passphrase door (${typed}) — one operator, one passphrase, no identity provider`);
|
|
2822
2825
|
info(`a lost passphrase is recoverable: ${invocationPrefix()}clearotron passphrase --reset`);
|
|
2823
2826
|
} else if (door.shape === "fronted") {
|
|
@@ -3194,7 +3197,10 @@ export async function runCheck() {
|
|
|
3194
3197
|
try { const s = statSync(p); if (s.size > 0) { wrote = p; break; } } catch { /* not this one */ }
|
|
3195
3198
|
}
|
|
3196
3199
|
if (wrote) {
|
|
3197
|
-
|
|
3200
|
+
// WHAT WAS READ, AND NOTHING MORE. This said "is being written" from any non-empty file — a claim
|
|
3201
|
+
// about activity made from a file's size, printed over a log whose only line was a test's
|
|
3202
|
+
// (measured 2026-09-19). Reworded as the owner ruled, 2026-09-19.
|
|
3203
|
+
ok(`the client door's access log has entries: ${wrote}`);
|
|
3198
3204
|
} else {
|
|
3199
3205
|
// NOT-YET-WRITTEN IS NOT UNWRITABLE. A door nobody has called through has no log, and saying it
|
|
3200
3206
|
// is broken would be the same lie as calling an unreachable unit inactive.
|
|
@@ -3338,8 +3344,22 @@ export async function runCheck() {
|
|
|
3338
3344
|
sock.once("error", (e) => done(e.code === "ECONNREFUSED" ? false : null));
|
|
3339
3345
|
});
|
|
3340
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
|
+
}
|
|
3341
3361
|
const door = doorState({ env: doorEnv, unitDir, exists: existsSync, active: doorActive,
|
|
3342
|
-
listening: doorListening, activeState: doorActiveState, subState: doorSubState });
|
|
3362
|
+
listening: doorListening, ownListener: doorOwnListener, activeState: doorActiveState, subState: doorSubState });
|
|
3343
3363
|
// EVERY COMMAND THROUGH `invoke`. Doctor's own guard runs every command doctor
|
|
3344
3364
|
// prints from a directory that is not the install; a literal `clearotron start` in that text is
|
|
3345
3365
|
// `command not found` for a reader with no shim, and the guard caught exactly that the moment this
|
|
@@ -4266,7 +4286,7 @@ try {
|
|
|
4266
4286
|
// Enter yield the empty string, `present("")` is false, and askValue loops with "A value is needed
|
|
4267
4287
|
// here." — so a reader taking the header at its word ("Enter takes the default in brackets") on the
|
|
4268
4288
|
// one prompt that advertises empty as an answer could not leave it. Driven on the merged tree:
|
|
4269
|
-
// 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
|
|
4270
4290
|
// engine-menu loop, arriving one commit after it was fixed, in a prompt this branch's own sibling
|
|
4271
4291
|
// added.
|
|
4272
4292
|
//
|
package/bin/passphrase.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import { defaultInstallBase, establishCredential, installCredential, readLocalCr
|
|
|
32
32
|
// rather than hardcoded. A hardcoded `npx ` tells a global installer their install is somehow lesser;
|
|
33
33
|
// a hardcoded bare name sends an npx reader to `command not found`.
|
|
34
34
|
import { invocationPrefix } from "../shared/invocation.mjs";
|
|
35
|
+
import { BRAND } from "../shared/brand.mjs";
|
|
35
36
|
|
|
36
37
|
const P = invocationPrefix();
|
|
37
38
|
const USAGE = ` ${P}clearotron passphrase — report or reset the portal's local sign-in
|
|
@@ -117,8 +118,9 @@ catch (e) {
|
|
|
117
118
|
process.exit(1);
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
console.log(
|
|
121
|
+
// THE OWNER'S WORDS, 2026-09-19: what the value is for comes before it, and names the one user it signs in.
|
|
122
|
+
console.log(`\n New passphrase for this ${BRAND.name}'s one user, ${email}. Paste it on the portal's sign-in page:\n`);
|
|
122
123
|
console.log(` PASSPHRASE: ${minted.passphrase}\n`);
|
|
124
|
+
console.log(` The previous one no longer works.`);
|
|
123
125
|
console.log(` Write it down now. It is stored only as a digest, so this line is the only copy that will`);
|
|
124
126
|
console.log(` ever exist — re-run this command if you lose it.\n`);
|
package/bin/start.mjs
CHANGED
|
@@ -121,7 +121,7 @@ import { SERVER_INSTALL_SET, unitsToRestartOnRefresh, unitHealthVerdict } from "
|
|
|
121
121
|
// — the door --background now INSTALLS, and the one authority for the settings
|
|
122
122
|
// it refuses to start without. (Until 2026-09-03 this import read "the one unit --background may
|
|
123
123
|
// tolerate and never manage"; settled point 2 superseded that.)
|
|
124
|
-
import { defaultDenylistPath, denylistPathFor, denylistFor, ensureDenylistFile, CLIENT_DOOR_UNIT, enablePlan, clientDoorPort, demoTokenSecret, demoTokenSecretPath, keyIssueCommand } from "../shared/client-door.mjs"; // — one owner for the revocation list's path
|
|
124
|
+
import { defaultDenylistPath, denylistPathFor, denylistFor, ensureDenylistFile, CLIENT_DOOR_UNIT, enablePlan, clientDoorPort, demoTokenSecret, demoTokenSecretPath, keyIssueCommand, demoConnectCommand } from "../shared/client-door.mjs"; // — one owner for the revocation list's path
|
|
125
125
|
import { createServer } from "node:net";
|
|
126
126
|
import { listenErrorMessage, nextFreePort } from "../shared/listen.mjs";
|
|
127
127
|
import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -805,6 +805,10 @@ export function childEnv({ ports, paths, user, portalSecret, tokenSecret, opsTok
|
|
|
805
805
|
// DNS-rebinding protection is keyed off this list being non-empty, so it is derived from the same
|
|
806
806
|
// port the listener is given rather than left to whoever remembers.
|
|
807
807
|
TRADEMARK_MCP_ALLOWED_HOSTS: `${host}:${ports.mcp},localhost:${ports.mcp}`,
|
|
808
|
+
// WHERE THE OTHER DOOR IS, so a key refused here is told where it belongs (shared/scope.mjs
|
|
809
|
+
// otherDoor). Read by nothing else in this process.
|
|
810
|
+
CLIENT_MCP_HTTP_HOST: host,
|
|
811
|
+
CLIENT_MCP_HTTP_PORT: String(ports.client),
|
|
808
812
|
},
|
|
809
813
|
// The worker needs the install's PATHS and nothing else — no ports, no secrets, no door config. It
|
|
810
814
|
// talks to the queue and the pool, not to either listener.
|
|
@@ -830,6 +834,9 @@ export function childEnv({ ports, paths, user, portalSecret, tokenSecret, opsTok
|
|
|
830
834
|
// written in one place and an allow-list in another drift into a door that starts and turns every
|
|
831
835
|
// request away, which reads as a dead door rather than as a misconfiguration.
|
|
832
836
|
CLIENT_MCP_ALLOWED_HOSTS: `${host}:${ports.client},localhost:${ports.client}`,
|
|
837
|
+
// The staff door's place, for the mirror refusal (shared/scope.mjs otherDoor). Read by nothing else here.
|
|
838
|
+
TRADEMARK_MCP_HTTP_HOST: host,
|
|
839
|
+
TRADEMARK_MCP_HTTP_PORT: String(ports.mcp),
|
|
833
840
|
CLIENT_MCP_ACCOUNT_ACCESS: String(clientFence ?? "1"),
|
|
834
841
|
// TOKEN-ONLY, AND WITHOUT IT THE DOOR DOES NOT START. Found by driving it rather than by reading
|
|
835
842
|
// the diff: with this unset the door demands an OIDC audience plus a Cloudflare Access team or
|
|
@@ -1603,9 +1610,12 @@ if (isMain) {
|
|
|
1603
1610
|
// — `clearotron demo` hands over to this — so fixing the player alone left the defect where it was.
|
|
1604
1611
|
// ONE SAMPLE AT A TIME: one whose files cannot be read is left out and named below, and the others
|
|
1605
1612
|
// seed. Copied in one call, a single unreadable file emptied the whole archive.
|
|
1606
|
-
const { publishContainer, seedDemoRuns } = await import("../driver/demo-container.mjs");
|
|
1613
|
+
const { publishContainer, seedDemoRuns, releaseDemoCopies } = await import("../driver/demo-container.mjs");
|
|
1607
1614
|
const container = publishContainer(join(REPO, "demo"), { repoRoot: REPO });
|
|
1608
|
-
|
|
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(); }
|
|
1609
1619
|
// AND AS RUNS, so the assistant this demo's connect line wires has them to list, brief and open. Under
|
|
1610
1620
|
// the demo's own workspace only: nothing of it reaches an install started afterwards. Their report
|
|
1611
1621
|
// links are stamped with this portal's address, the one the Open line prints.
|
|
@@ -2421,7 +2431,8 @@ if (isMain) {
|
|
|
2421
2431
|
// The output used to name neither as a door, so an owner watching one of them start concluded MCP had
|
|
2422
2432
|
// not come up. Both are printed with who each is for, because "MCP is running" is ambiguous on a box
|
|
2423
2433
|
// that has two of them and the ambiguity is what cost the leg.
|
|
2424
|
-
|
|
2434
|
+
// THE CLIENT DOOR LEADS (owner, 2026-09-19). It is the one an assistant uses, and with the engine door
|
|
2435
|
+
// printed first a demo's reader handed the staff address to their assistant, which refused its key.
|
|
2425
2436
|
// THE SYNCHRONOUS TRUTH, NOT THE ASYNC FLAG ( — F26, review finding).
|
|
2426
2437
|
//
|
|
2427
2438
|
// `rec.alive` is flipped by the child's exit handler, which is async: reading it here asks "has the
|
|
@@ -2440,11 +2451,12 @@ if (isMain) {
|
|
|
2440
2451
|
if (adoptedClientDoor)
|
|
2441
2452
|
say(` Already running as ${CLIENT_DOOR_UNIT}; this start kept it, so existing keys still work.`);
|
|
2442
2453
|
else
|
|
2443
|
-
say(` It refuses every caller until a key is issued: ${keyIssueCommand({ prefix: invocationPrefix(), demo: DEMO, user, base: paths.base, defaultBase: join(homedir(), "trademark") })}`);
|
|
2454
|
+
say(` It refuses every caller until a key is issued: ${DEMO ? demoConnectCommand({ prefix: invocationPrefix(), base: paths.base }) : keyIssueCommand({ prefix: invocationPrefix(), demo: DEMO, user, base: paths.base, defaultBase: join(homedir(), "trademark") })}`);
|
|
2444
2455
|
} else {
|
|
2445
2456
|
say(` Client door NOT RUNNING on ${HOST}:${ports.client} — its output above says why. The portal and`);
|
|
2446
2457
|
say(" the engine door are unaffected; a client assistant cannot connect until it is up.");
|
|
2447
2458
|
}
|
|
2459
|
+
say(` Engine door http://${HOST}:${ports.mcp}/mcp — the portal's Start button calls this. Staff.`);
|
|
2448
2460
|
say("");
|
|
2449
2461
|
// ── WHAT `status` AND `stop` READ ABOUT THIS START ───────────────────────────────────────────────
|
|
2450
2462
|
//
|
|
@@ -2556,7 +2568,7 @@ if (isMain) {
|
|
|
2556
2568
|
// THE HEADINGS COME WITH THE PAIR, from the composer. Nothing is written here: the page prints these
|
|
2557
2569
|
// same two words above these same two commands, and a second author is how the two surfaces drift.
|
|
2558
2570
|
if (connect.variants) {
|
|
2559
|
-
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(""); }
|
|
2560
2572
|
} else {
|
|
2561
2573
|
say(` ${connect.command}`);
|
|
2562
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. |
|