clearotron 0.4.0 → 0.4.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/THIRD-PARTY-NOTICES.md +2 -2
  2. package/bin/README.md +1 -1
  3. package/bin/clearotron.mjs +2 -0
  4. package/bin/mcp.mjs +80 -0
  5. package/bin/start.mjs +14 -1
  6. package/build-info.json +2 -2
  7. package/demo/README.md +1 -1
  8. package/docs/SECURITY-OWASP.md +21 -13
  9. package/docs/SECURITY.md +20 -14
  10. package/docs/architecture/04-configuration-reference.md +3 -3
  11. package/docs/architecture/05-config-governance.md +1 -1
  12. package/docs/architecture/06-operations-runbook.md +5 -4
  13. package/docs/architecture/08-development-guide.md +3 -1
  14. package/docs/architecture/09-security-and-data.md +16 -11
  15. package/driver/CHANGELOG.md +29 -0
  16. package/driver/binding-layers.mjs +28 -6
  17. package/driver/citation-census.json +3 -3
  18. package/driver/common-law-coverage-status.mjs +52 -0
  19. package/driver/common-law-receipts.mjs +1 -1
  20. package/driver/compare.mjs +1 -1
  21. package/driver/connotation-search.mjs +5 -5
  22. package/driver/contract-audit.mjs +6 -6
  23. package/driver/contract-dictation-registry.mjs +1 -1
  24. package/driver/contract-e3-backlog.mjs +18 -17
  25. package/driver/contract-vocabulary.mjs +145 -132
  26. package/driver/declination-call.mjs +5 -1
  27. package/driver/degraded-parts.mjs +10 -3
  28. package/driver/dev-portal.mjs +2 -1
  29. package/driver/dispatch-record.mjs +19 -0
  30. package/driver/doubt-closure-tool.mjs +2 -2
  31. package/driver/driver.config.mjs +39 -6
  32. package/driver/engagement-receipt.mjs +1 -1
  33. package/driver/engine/CONTRACT.md +41 -1
  34. package/driver/engine/anthropic-agent.mjs +56 -4
  35. package/driver/engine/mcp/codex-config.mjs +8 -3
  36. package/driver/engine/mcp/coverage-server.mjs +1 -1
  37. package/driver/engine/mcp/gather-config.mjs +1 -1
  38. package/driver/engine/mcp/recording-server.mjs +32 -5
  39. package/driver/engine/mcp/stdio-server.mjs +45 -7
  40. package/driver/engine/mcp/supplemental.mjs +26 -1
  41. package/driver/engine/openai-agent.mjs +113 -20
  42. package/driver/engine/session-record.mjs +223 -0
  43. package/driver/enqueue-schema.mjs +2 -2
  44. package/driver/findings-model.mjs +4 -4
  45. package/driver/flag-snapshot.mjs +2 -1
  46. package/driver/form-neighbourhood.mjs +7 -7
  47. package/driver/frame-diff-model.mjs +3 -3
  48. package/driver/framework.mjs +22 -0
  49. package/driver/gateway.mjs +96 -14
  50. package/driver/jx.mjs +1 -1
  51. package/driver/knockout-review-record.mjs +55 -1
  52. package/driver/named-band.mjs +4 -0
  53. package/driver/owner-screen.mjs +7 -4
  54. package/driver/package.json +1 -1
  55. package/driver/phase0.mjs +13 -2
  56. package/driver/pipeline-knockout.mjs +15 -13
  57. package/driver/pipeline.mjs +132 -48
  58. package/driver/portal-families.mjs +3 -3
  59. package/driver/portal-service.mjs +6 -5
  60. package/driver/predelivery-lint.mjs +19 -19
  61. package/driver/products.mjs +1 -1
  62. package/driver/profiles.mjs +4 -3
  63. package/driver/publish/index.mjs +1 -1
  64. package/driver/publish/knockout.mjs +5 -4
  65. package/driver/publish/office-record-links.mjs +17 -2
  66. package/driver/publish/publish-inputs.mjs +3 -3
  67. package/driver/publish/render-knockout.mjs +17 -5
  68. package/driver/publish/render.mjs +4 -2
  69. package/driver/publish/xlsx.mjs +1 -1
  70. package/driver/record-fold.mjs +177 -0
  71. package/driver/register-count.mjs +20 -3
  72. package/driver/register-coverage.mjs +1 -1
  73. package/driver/register-plan.mjs +83 -18
  74. package/driver/register-records.mjs +7 -0
  75. package/driver/registry-fidelity.mjs +2 -2
  76. package/driver/repairs.mjs +1 -1
  77. package/driver/report-card-record.mjs +4 -4
  78. package/driver/result-noun-fields.mjs +3 -3
  79. package/driver/scope-facts.mjs +11 -3
  80. package/driver/scope-rules.mjs +2 -2
  81. package/driver/search-policy.mjs +20 -1
  82. package/driver/skills/clearance-common-law/SKILL.md +22 -22
  83. package/driver/skills/clearance-register/digest.md +3 -3
  84. package/driver/skills/clearance-register/register-recipes.md +11 -11
  85. package/driver/skills/clearance-register/status-rules.md +1 -1
  86. package/driver/skills/clearance-register/unit.md +1 -1
  87. package/driver/skills/clearance-search/firm-wide-reasoning.md +3 -2
  88. package/driver/skills/clearance-search/phase2-execution.md +6 -5
  89. package/driver/skills/clearance-search/synthesis-rules.md +4 -4
  90. package/driver/skills/clearance-search/worked-examples.md +39 -39
  91. package/driver/skills/clearance-variants/SKILL.md +16 -16
  92. package/driver/skills/frame-diff/SKILL.md +2 -2
  93. package/driver/stage-context.mjs +36 -1
  94. package/driver/stages-knockout.mjs +6 -5
  95. package/driver/stages.mjs +30 -30
  96. package/driver/suite-census.json +259 -55
  97. package/driver/synthesis-record.mjs +5 -0
  98. package/driver/unit-inventory.mjs +17 -28
  99. package/driver/verify.mjs +7 -7
  100. package/mcp-server/CHANGELOG.md +8 -0
  101. package/mcp-server/CONNECT.md +9 -3
  102. package/mcp-server/demo-mode.mjs +10 -0
  103. package/mcp-server/lib/evidence.mjs +8 -8
  104. package/mcp-server/lib/knockout.mjs +1 -1
  105. package/mcp-server/lib/runs.mjs +7 -1
  106. package/mcp-server/lib/scrub.mjs +1 -1
  107. package/mcp-server/package.json +2 -2
  108. package/mcp-server/packs/README.md +1 -1
  109. package/mcp-server/server.mjs +16 -2
  110. package/package.json +4 -3
  111. package/portal-ui/package.json +3 -3
  112. package/providers/_shared/count.mjs +9 -2
  113. package/providers/_shared/enumerate.mjs +125 -41
  114. package/providers/_shared/execute-plan.mjs +15 -4
  115. package/providers/corsearch/src/capabilities.js +1 -1
  116. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  117. package/providers/oauth-mcp-bridge/package.json +2 -2
  118. package/providers/signa/src/capabilities.js +32 -2
  119. package/providers/signa/src/core.js +262 -36
  120. package/scripts/band-shape-probe.mjs +2 -2
  121. package/scripts/deprecate-below.mjs +28 -4
  122. package/scripts/e2e-first-time.mjs +17 -2
  123. package/scripts/env-audit.mjs +3 -2
  124. package/scripts/freeze-example-run.mjs +10 -10
  125. package/scripts/mcp-registry-listing.mjs +199 -0
  126. package/scripts/mint-plugin-folder.mjs +65 -0
  127. package/scripts/release-version.mjs +44 -0
  128. package/scripts/score.mjs +1 -1
  129. package/shared/register-floor.mjs +19 -0
  130. package/shared/scope.mjs +18 -2
  131. package/shared/trigger-lane.mjs +1 -1
  132. package/skills/clearotron-ops/SKILL.md +81 -24
@@ -110,7 +110,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
110
110
  SOFTWARE.
111
111
  ```
112
112
 
113
- ## @modelcontextprotocol/sdk@1.30.1
113
+ ## @modelcontextprotocol/sdk@1.32.1
114
114
 
115
115
  - **Licence declared:** `MIT`
116
116
  - **Repository:** https://github.com/modelcontextprotocol/typescript-sdk
@@ -4896,7 +4896,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
4896
4896
  SOFTWARE.**
4897
4897
  ```
4898
4898
 
4899
- ## proxy-addr@2.0.7
4899
+ ## proxy-addr@2.0.8
4900
4900
 
4901
4901
  - **Licence declared:** `MIT`
4902
4902
  - **Repository:** jshttp/proxy-addr
package/bin/README.md CHANGED
@@ -20,7 +20,7 @@ and is what you want when you are working in this tree.
20
20
  | `signa-sync.mjs` | — | `node bin/signa-sync.mjs` | One vendor read (`GET /v1/offices`, paged). Needs `SIGNA_API_KEY`. |
21
21
 
22
22
  `example.mjs` replays `demo` through the ordinary publisher into `~/trademark-demo/pool` and
23
- serves it on `127.0.0.1:18900`. Every path the demo pool could collide with is checked by realpath and by
23
+ serves it on `127.0.0.1:18860`. Every path the demo pool could collide with is checked by realpath and by
24
24
  containment before anything is written, because a demo runs on deployed machines too.
25
25
 
26
26
  `onboard.mjs` validates each answer through the door the engine itself uses before it persists anything,
@@ -65,6 +65,7 @@ export const VERBS = {
65
65
  passphrase: ["bin/passphrase.mjs"],
66
66
  sync: ["bin/uspto-sync.mjs"],
67
67
  update: ["bin/update.mjs"],
68
+ mcp: ["bin/mcp.mjs"],
68
69
  };
69
70
 
70
71
  export const SUMMARY = {
@@ -87,6 +88,7 @@ export const SUMMARY = {
87
88
  passphrase: "report or RESET the portal's local sign-in — the recovery for a lost passphrase",
88
89
  sync: "build or update the free US register index (a large download, and hours of ingest)",
89
90
  update: "bring this install up to date — and REFUSE to do it over the top of your own configuration",
91
+ mcp: "serve this install's searches to an assistant over MCP, or the demo's where there is no install",
90
92
  };
91
93
 
92
94
  /**
package/bin/mcp.mjs ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
4
+ //
5
+ // mcp.mjs — `clearotron mcp`: the MCP server over stdio, started from the package itself.
6
+ //
7
+ // An install's own connect lines run the server from that install's copy (shared/stdio-connect.mjs), so
8
+ // an assistant never keeps an old server after an upgrade, and they stay as they are. This verb is for a
9
+ // machine with no install, and for a listing that launches the published package with `npx`.
10
+ //
11
+ // WHICH RUNS IT READS. An install's, wherever one is found; the demo's four sample runs where none is.
12
+ // Standard output belongs to the protocol from its first byte, so nothing here prints to it.
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { parseEnv } from "node:util";
17
+ import { isEntrypoint } from "../shared/is-entrypoint.mjs";
18
+ import { demoMode } from "../mcp-server/demo-mode.mjs";
19
+
20
+ /** The two settings the install's own connect lines hand the server: where runs are, and where reports are. */
21
+ export const INSTALL_FOLDERS = Object.freeze(["CLEAROTRON_WORK_DIR", "CLEAROTRON_REPORTS_DIR"]);
22
+
23
+ /** The demo's own base, and where it keeps its runs and its access log: the layout `installPaths` gives it. */
24
+ export const demoBase = (home = homedir()) => join(home, "trademark-demo");
25
+ export const demoWorkspace = (home = homedir()) => join(demoBase(home), "workspace");
26
+
27
+ /**
28
+ * WHAT COUNTS AS AN INSTALL, and so whether this server reads an install's runs or the demo's. PURE.
29
+ *
30
+ * An install is any one of these:
31
+ * - a work folder or a reports folder named in this process's environment, in the settings file a
32
+ * packaged install writes (`~/.config/clearotron/.env`), or in the file a background install's
33
+ * services read (`~/.env`);
34
+ * - either of those files carrying any Clearotron setting at all, which is an install configured and
35
+ * not yet pointed at its folders: the server then reads its default and names what is missing;
36
+ * - the default workspace, `~/trademark/workspace`, on disk.
37
+ * The demo writes neither file and keeps its runs under its own base, so a machine that has only run the
38
+ * demo still reads as no install. An install never gets the demo: a misconfigured one is refused by name.
39
+ *
40
+ * Returns the folders to hand the server (only what is set, and never over the environment's own value),
41
+ * or `demo: true` where there is no install.
42
+ */
43
+ export function mcpTarget({ env = process.env, home = homedir(), exists = existsSync, read = (p) => readFileSync(p, "utf8") } = {}) {
44
+ const parsed = (p) => { try { return parseEnv(read(p)); } catch { return null; } };
45
+ const settings = parsed(join(home, ".config", "clearotron", ".env"));
46
+ const services = parsed(join(home, ".env"));
47
+ const set = (v) => (typeof v === "string" && v.trim() ? v.trim() : null);
48
+ const folders = {};
49
+ for (const k of INSTALL_FOLDERS) {
50
+ const v = set(env[k]) ?? set(settings?.[k]) ?? set(services?.[k]);
51
+ if (v) folders[k] = v;
52
+ }
53
+ const configures = (file) => Boolean(file) && Object.keys(file).some((k) => /^(CLEAROTRON|TRADEMARK|PORTAL)_/.test(k));
54
+ const install = Object.keys(folders).length > 0 || configures(settings) || configures(services)
55
+ || exists(join(home, "trademark", "workspace"));
56
+ return install ? { demo: false, env: folders } : { demo: true, env: {} };
57
+ }
58
+
59
+ async function main() {
60
+ const target = mcpTarget();
61
+ if (target.demo) {
62
+ // The four sample runs, copied where the connector walks, under the demo's own base: nothing is
63
+ // written anywhere an install reads (driver/demo-container.mjs).
64
+ const { seedDemoRuns } = await import("../driver/demo-container.mjs");
65
+ const workspace = demoWorkspace();
66
+ seedDemoRuns({ workspace, examplesDir: join(import.meta.dirname, "..", "demo") });
67
+ process.env.CLEAROTRON_WORK_DIR = workspace;
68
+ // The door's access log too, which would otherwise open the install's own telemetry folder.
69
+ if (!set(process.env.TRADEMARK_MCP_AUDIT_LOG)) process.env.TRADEMARK_MCP_AUDIT_LOG = join(demoBase(), "telemetry", "trademark-mcp-access.jsonl");
70
+ demoMode.on = true;
71
+ } else {
72
+ for (const [k, v] of Object.entries(target.env)) if (!set(process.env[k])) process.env[k] = v;
73
+ }
74
+ // The server starts through the entry that refuses an old Node in one plain line before it loads.
75
+ await import("../mcp-server/serve.mjs");
76
+ }
77
+
78
+ const set = (v) => typeof v === "string" && v.trim() !== "";
79
+
80
+ if (isEntrypoint(import.meta.url)) await main();
package/bin/start.mjs CHANGED
@@ -2445,6 +2445,18 @@ if (isMain) {
2445
2445
  const clientDoor = adoptedClientDoor
2446
2446
  ? null
2447
2447
  : start("the client door", "mcp-server/http-server-client.mjs", envs.client, { fatal: false });
2448
+ // ── THE BANNER NAMES THIS DOOR ONLY ONCE IT ANSWERS ─────────────────────────────────────────────────
2449
+ //
2450
+ // The engine door and the portal are each awaited above before anything is printed. This door was
2451
+ // not: the banner below was composed the moment it was spawned, while it was still loading, and
2452
+ // announced an address that refused connections. Measured 2026-10-09, in a container with no network
2453
+ // running as root and on a host as a normal user: a request sent as the banner printed got no
2454
+ // connection, and the door's first answer came about half a second later. Its own output said nothing
2455
+ // was wrong, because nothing was; it had not finished starting.
2456
+ //
2457
+ // Same probe and same budget as the other two, and non-fatal like the door itself. A door that exits,
2458
+ // or does not answer within the budget, takes the NOT RUNNING branch below and the portal keeps serving.
2459
+ const clientDoorAnswered = clientDoor ? await healthy(`http://${HOST}:${ports.client}/healthz`, clientDoor) : false;
2448
2460
 
2449
2461
  // ── 6. one URL ─────────────────────────────────────────────────────────────────────────────────────
2450
2462
 
@@ -2471,7 +2483,8 @@ if (isMain) {
2471
2483
  // AN ADOPTED DOOR IS RUNNING. Without this the banner reported "NOT RUNNING …
2472
2484
  // its output above says why" about a door that is up and serving, and pointed the reader at output
2473
2485
  // that does not exist — the one sentence on this screen a reader would act on, and false.
2474
- const doorRunning = adoptedClientDoor || clientDoor?.child?.exitCode === null;
2486
+ // AND A SPAWNED DOOR IS RUNNING ONLY ONCE IT HAS ANSWERED: alive is not listening (see the probe above).
2487
+ const doorRunning = adoptedClientDoor || (clientDoor?.child?.exitCode === null && clientDoorAnswered);
2475
2488
  if (doorRunning) {
2476
2489
  say(` Client door http://${HOST}:${ports.client}/mcp — a client's assistant connects here.`);
2477
2490
  if (adoptedClientDoor)
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "9b8ab0f275a16e9b586425c3574c94b5974e225b",
3
- "version": "0.4.0"
2
+ "commit": "0ac7d328864dbb4446ea2dd472f64acff14b7a4f",
3
+ "version": "0.4.1-beta.1"
4
4
  }
package/demo/README.md CHANGED
@@ -16,7 +16,7 @@ carries no `report.md` at all, because for that lane the markdown is an output o
16
16
  an input to it.
17
17
 
18
18
  Each directory is named by the product that produced it — the id in the run's own frozen search policy,
19
- not a label chosen afterwards. `clearotron demo` replays the first; `clearotron demo --product <id>`
19
+ not a label chosen afterwards. `clearotron demo` replays all four; `clearotron demo --product <id>`
20
20
  replays a named one; `clearotron demo --run-dir <dir>` replays any frozen run from anywhere.
21
21
 
22
22
  ## The marks are invented; the data behind them is real
@@ -45,10 +45,11 @@ shows the engine's internals to a company's people.
45
45
  **What Clearotron does.** Every read passes one authorization check, and a person sees only what their
46
46
  grant names. A key bound to one run reads that run and writes nothing. The copy of a report a company
47
47
  receives drops the staff-only notes. No real company's data is in this repository; run data lives
48
- in folders the operator owns. CI scans the tree and the built bundle for secrets.
48
+ in folders the operator owns. CI scans the tree and the built bundle for secrets. A report published
49
+ from 0.4.0 on carries its fonts inside itself, and opening it contacts no other server.
49
50
 
50
51
  **Where.** [SECURITY.md](SECURITY.md) (the access model), `shared/scope.mjs`, `driver/portal-report.mjs`,
51
- `.gitleaks.toml`, `.github/workflows/ci.yml`.
52
+ `.gitleaks.toml`, `.github/workflows/ci.yml`, `scripts/report-offline-render-check.mjs`.
52
53
 
53
54
  **Not covered.** A company's clearances are kept until the operator deletes them: Clearotron sets no
54
55
  retention period, because how long they are held is the operator's policy.
@@ -57,14 +58,15 @@ retention period, because how long they are held is the operator's policy.
57
58
 
58
59
  **Here.** A compromised npm package, GitHub Action or engine program.
59
60
 
60
- **What Clearotron does.** Dependabot updates the npm packages and the GitHub Actions. Each release is
61
- published to npm through trusted publishing, with provenance that ties the package to the commit and the
62
- build that produced it. A stable release is not published while a code-scanning alert is open. Setup
61
+ **What Clearotron does.** Dependabot updates the npm packages and the GitHub Actions. Every GitHub Action is pinned to a commit
62
+ hash. Each release is published to npm through trusted publishing, with provenance that ties the package
63
+ to the commit and the build that produced it. Each release carries a software bill of materials. On an install from a git
64
+ checkout, `clearotron update` installs with install scripts off. A stable release is not published while a code-scanning alert is open. Setup
63
65
  installs, and `clearotron doctor` accepts, only an engine program at or above the version this release
64
66
  needs.
65
67
 
66
- **Where.** `.github/dependabot.yml`, `.github/workflows/release.yml`,
67
- `scripts/release-code-scanning-check.mjs`, `driver/driver.config.mjs` (`ENGINE_BINARIES`, each program's
68
+ **Where.** `.github/dependabot.yml`, `.github/workflows/`, `.github/workflows/release.yml`,
69
+ `scripts/release-sbom.mjs`, `bin/update.mjs`, `scripts/release-code-scanning-check.mjs`, `driver/driver.config.mjs` (`ENGINE_BINARIES`, each program's
68
70
  `floor`).
69
71
 
70
72
  ### LLM04:2025 Data and Model Poisoning
@@ -99,12 +101,14 @@ The common-law search asks the research service to run a search program, on that
99
101
 
100
102
  **What Clearotron does.** Tools are granted per stage, by name. The case-law stage is the exception: it is granted every tool its two case-law services offer. A Claude
101
103
  stage is offered no command tool.
102
- Register searches beyond the fixed plan go through a proposal that code checks and runs. A key issued for
103
- automation can be limited to named actions. A what-if requested by a company's people is queued for a
104
+ Register searches beyond the fixed plan go through a proposal that code checks and runs. A new key issued for
105
+ automation must name the actions it may take; keys issued before that rule keep working, and each use of
106
+ one is recorded. No key for automation reaches a what-if over the network. A what-if requested by a company's people is queued for a
104
107
  separate process rather than run by the door that received it.
105
108
 
106
109
  **Where.** `driver/engine/mcp/gather-config.mjs`, `driver/engine/anthropic-agent.mjs`,
107
- `driver/engine/mcp/supplemental.mjs`, `shared/scope.mjs`, `driver/whatif-worker.mjs`.
110
+ `driver/engine/mcp/supplemental.mjs`, `shared/scope.mjs`, `driver/whatif-worker.mjs`,
111
+ `mcp-server/mint-token.mjs`, `mcp-server/server.mjs`.
108
112
 
109
113
  **Not covered.** As ASI02: the Claude program also offers a stage built-in tools that act without asking.
110
114
 
@@ -189,7 +193,7 @@ remove them.
189
193
  **What Clearotron does.** The AI program starts with a named list of settings, and the key that signs
190
194
  access keys is not on it. The worker that starts the AI program does not hold that key either, whether
191
195
  the install runs in a terminal or under systemd. Access keys are scoped to a run, a company or named
192
- actions, and one check enforces the scope. On Claude, a stage's file tools can read nothing outside its run folder, its
196
+ actions, and one check enforces the scope. A new key for automation must name its actions. On Claude, a stage's file tools can read nothing outside its run folder, its
193
197
  instruction folders and any folder the machine's own Claude settings add. On Codex with its sandbox on, a stage's commands can read nothing outside its run folder, its instruction folders, the temporary
194
198
  folders, and the system and program files a command needs to run.
195
199
 
@@ -276,10 +280,12 @@ could not run is listed in the report.
276
280
 
277
281
  **What Clearotron does.** No agent outlives its stage. Each stage is one process under a watchdog and a
278
282
  hard time limit, and the driver stops its whole process group. The driver records every program it
279
- starts. On the Claude engine a stage cannot write into the instructions it runs from.
283
+ starts. On the Claude engine a stage cannot write into the instructions it runs from. The systemd units
284
+ an install can run in the background start with `NoNewPrivileges`, so nothing a stage starts there can
285
+ gain more than the install's account holds.
280
286
 
281
287
  **Where.** `driver/engine/anthropic-agent.mjs`, `driver/engine/common.mjs`,
282
- `driver/engine/child-record.mjs`, `driver/engine/deny-authority-write.mjs`.
288
+ `driver/engine/child-record.mjs`, `driver/engine/deny-authority-write.mjs`, `driver/systemd/`.
283
289
 
284
290
  **Not covered.** As LLM04: on Codex, no check refuses a stage's write inside its run folder.
285
291
 
@@ -296,3 +302,5 @@ Measured on 2026-09-23 with the GitHub API.
296
302
  | Private vulnerability reporting | On |
297
303
  | npm provenance | Every release, through trusted publishing |
298
304
  | Required checks on `main` | Six; force pushes and branch deletion refused |
305
+ | Actions pinned to a commit hash | All 42, read from the workflow files on 2026-09-24 |
306
+ | Software bill of materials | CycloneDX, attached to every release, read from the release workflow on 2026-09-24 |
package/docs/SECURITY.md CHANGED
@@ -12,7 +12,7 @@ Each risk on OWASP's two lists for AI systems, and what Clearotron does about it
12
12
 
13
13
  | Surface | Trust | Guard |
14
14
  |---|---|---|
15
- | stdio MCP (`mcp-server/server.mjs`) | local/full ("ops") | OS user boundary — run it AS the operator account; it is the only surface on which `what_if_run` EXECUTES (`visibleTools` keeps what-if out of the HTTP listing for ops, but the CallTool chokepoint gates on `authorize()` alone, which admits it for any ops token not `--verbs`-scoped) |
15
+ | stdio MCP (`mcp-server/server.mjs`) | local/full ("ops") | OS user boundary — run it AS the operator account; it is the only surface on which `what_if_run` EXECUTES (`visibleTools` keeps what-if out of the HTTP listing for ops, and the CallTool chokepoint refuses any tool the listing hides, so no ops token reaches it over HTTP) |
16
16
  | Client MCP (`mcp-server/http-server-client.mjs`) | a company's signed-in person / their access key | `what_if_run` from an `account` principal ENQUEUES rather than executes (ruling 2026-08-27) — it never imports the engine, and `driver/whatif-worker.mjs` spawns the sandbox from an OS service process. A confirmation token is unsigned, so the call must ALSO name its `runId`: the grant check keys on it, and `whatIfEnqueue` refuses a token naming a different run. The `model` argument is refused on this face. |
17
17
  | HTTP MCP (`mcp-server/http-server.mjs`) | authenticated remote | auth-BEFORE-data; fail-closed construction; inner scoped tokens |
18
18
  | Run-bound keys | a report recipient an operator issues one to | a `user` key from `mint-token.mjs`, read-only and bound to one run; the plain-language report tools (`clientSafe`) only |
@@ -98,9 +98,9 @@ with access to everything.
98
98
  on (`get_coverage`, `search`, `diff_artifact`, `run_changes`, what-if), which are denied by the
99
99
  default that an undecided tool is denied.
100
100
  - HMAC-signed tokens (`v1.<payload>.<sig>`, node:crypto only). `authorize()` is the single
101
- enforcement point for every tool call; `visibleTools()` additionally hides what a session may not
102
- call — but it is hygiene on the tool LISTING, not a second gate, and it is the only thing that
103
- reads `local`.
101
+ enforcement point for every tool call; `visibleTools()` decides what a session is offered, and a
102
+ tool it hides is refused by name at the call, after `authorize()`. `authorize()` never sees `local`,
103
+ and `visibleTools()` is the only per-call gate that uses it.
104
104
  - **User tokens cannot**: enumerate runs, cross to another run (explicit mismatching runId refused;
105
105
  omitted runId pinned), reach any write/spend tool, or read internal artifacts — `read_artifact`
106
106
  is name-gated to the report alone (`USER_ARTIFACTS`), and `list_findings` to the curated
@@ -110,13 +110,13 @@ with access to everything.
110
110
  rides inside a delivered PDF and can be forwarded to anyone, and the ruling was about people the
111
111
  operator enrolled. Both sets are gated at the read_artifact tool AND at the Resources surface
112
112
  (`resources/list` / `resources/read`), kind for kind — two doors to the same bytes, one rule.
113
- - **Ops tokens are least-privilege**: an optional `verbs[]` allowlist restricts write tools per
114
- principal (an intake connector physically cannot `stop_run`). `what_if_*` is filtered out of the
115
- HTTP tool LISTING for every OPS scope (`visibleTools`, keyed on `local` — the `local` test governs
116
- the ops branch only, and an `account` principal returns above it). That is hygiene, not a wall:
117
- `authorize()` never sees `local` and treats what-if as an ordinary ops write verb, so an ops token
118
- minted without `--verbs` can still call it over HTTP — which is why every HTTP ops token should be
119
- minted verb-scoped.
113
+ - **Ops tokens are least-privilege**: a new ops token must name its `verbs[]`, the write tools it may
114
+ call (an intake connector physically cannot `stop_run`), and `mint-token` refuses one without
115
+ `--verbs`. Tokens issued before that rule keep working, and the audit line marks every call they
116
+ make. `what_if_plan` and `what_if_run` are left out of the HTTP tool listing for every ops scope
117
+ (`visibleTools`, keyed on `local`), and a tool the listing hides is refused at the call, so no ops
118
+ token plans or runs a what-if over HTTP. An `account` principal returns above that branch: its
119
+ what-if is queued for a separate process, never run by the door.
120
120
 
121
121
  ## Token lifecycle
122
122
 
@@ -127,15 +127,21 @@ what the mechanism guarantees.*
127
127
  principal in every audit line; the `jti` printed at mint time is the revocation handle). Three other callers share the same `mintToken`: `clearotron start` mints the portal's verb-scoped,
128
128
  company-capped ops token in memory at every start, and neither prints nor stores it; `clearotron connect`
129
129
  and the portal's connect screen each mint a person's key for their own assistant.
130
- - **Revocation**: denylist file checked on every verification; missing file = nothing revoked (the
131
- denylist can never take all auth down). **Rotation**: two-secret window, flag-day-free.
130
+ - **Revocation**: a denylist file is checked on every verification, always. With
131
+ `TRADEMARK_MCP_TOKEN_DENYLIST` unset it is `~/.config/clearotron/token-denylist`, the file every revoker
132
+ writes in that case, and that file being absent means nothing has been revoked. A list named in the
133
+ setting that is missing or unreadable, or a default list that exists but cannot be read, refuses every
134
+ key. **Rotation**: two-secret window, flag-day-free.
132
135
  - **Rate limits**: per-identity bucket on every request plus a separate lower per-principal bucket
133
136
  for ops sessions.
134
137
 
135
138
  ## Audit
136
139
 
137
140
  Every HTTP tool call appends one line to an append-only JSONL (`TRADEMARK_MCP_AUDIT_LOG`) when the call
138
- finishes: `{ts, email, sub, method, tool, runId, status, door}`, where `status` is the outcome. It is
141
+ finishes: `{ts, email, sub, kind, method, tool, runId, status, door}`, where `status` is the outcome, plus `query` (its
142
+ first 120 characters) on a search, `uri` on a resource read and `transport` on the local route, and for an
143
+ ops token `namesVerbs`, which says whether the token names its tools (never which tools, and never the
144
+ token). It is
139
145
  written after scope resolution, so it names the principal. A request refused before any tool runs is
140
146
  written too, with the status `refused`. Writing it is best-effort and never blocks a request.
141
147
 
@@ -156,7 +156,8 @@ the attempt row records the id the program reports it served.
156
156
  **The CLI must be new enough for the model.** Each release carries its own list of accepted models,
157
157
  and one that predates a model refuses it outright while the tier alias goes on serving the previous
158
158
  generation — a 400 at the first turn, or a run that quietly used an older model. Setup installs
159
- `2.1.280` or newer for this reason; a copy already on the machine is used as it is, so `doctor`'s
159
+ `2.1.293` or newer for this reason, the first release whose `haiku` reaches Claude Haiku 5.5 (`opus` and
160
+ `sonnet` reached 5.5 from `2.1.284`); a copy already on the machine is used as it is, so `doctor`'s
160
161
  version is the one to read before assuming which model a tier reaches.
161
162
 
162
163
  **Anything else throws.** There is no regex fall-through to sonnet and no cross-provider
@@ -190,7 +191,7 @@ timeout shot, warm patch, backoff), the lane-wedge re-dispatch, and the rate-lim
190
191
  | `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. |
191
192
  | `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. |
192
193
  | `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. |
193
- | `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. |
194
+ | `CLEAROTRON_OPENAI_MODEL_JUDGMENT` / `CLEAROTRON_OPENAI_MODEL_SWEEP` / `CLEAROTRON_OPENAI_MODEL_CHEAP` | `gpt-6.1-sol` / `gpt-6.1-sol` / `gpt-6-luna` (from 2026-10-09; before that `gpt-5.6-sol` / `gpt-5.6-terra` / `gpt-5.6-luna`). Codex `0.159.0` or newer is required on a ChatGPT sign-in: older releases refuse `gpt-6.1-sol` with a 400. | 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. |
194
195
  | ↳ 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. |
195
196
  | `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. |
196
197
  | `CLEAROTRON_CODEX_SANDBOX_BYPASS` | unset | `1` runs `codex exec` with its own sandbox bypassed, for hosts where that sandbox cannot run (setup and `doctor --probe-engine` report it). It removes a defence: with it set, a stage's commands run with every permission of the account Clearotron runs as, not only in the run folder. Codex has no guard against a stage writing into the run's `_driver/` folder, with the sandbox or without it. Set it because the host forces it, never for convenience. |
@@ -300,7 +301,6 @@ because the rule is about what PRODUCT CODE reads, not about what a run reads.
300
301
 
301
302
  | Var | Default | Meaning |
302
303
  |---|---|---|
303
- | `DEMO_PORT` | `18900` | Port `npx clearotron demo` serves the replayed report on. `--port` overrides it. |
304
304
  | `CLEAROTRON_DEMO` | unset | `1` puts this install in the DEMO posture. **Ordering is real**: the four products are listed and orderable, the form, the plan and the confirmation are the product's own, and the confirmation resolves to a finished report that already exists rather than dispatching — no engine turn, no register call, no queue entry, no run directory (ruling 2026-08-31, superseding the greyed-control ruling of the same day). A product the demo carries no finished report for refuses and names which one. It also re-aims two boot warnings written for an operator of a real deployment at the visitor who is not one, from one place (`driver/demo-posture.mjs`). **Set by `npx clearotron demo`, not by an operator** — it is passed explicitly to the two processes that have a reason to know (the portal and the MCP door; the worker is not told, because a demo never queues anything for it to drain), and those run with `CLEAROTRON_NO_ENV_FILE=1`, so a stray `.env` can neither put a live install into demo mode nor take a demo out of one. Anything but the literal `1` is not a demo. Replaces `PORTAL_DEMO`, which named only one of the processes that has to know. |
305
305
  | `CLEAROTRON_TEST_FIXTURE_PROFILES` | unset | `1` makes the profile loader return the three suite fixtures, which are refused from every roster otherwise. Set by `scripts/test-run.mjs`, never by an operator; an explicit `includeTestFixtures` argument beats it. Effect class `harness`; the full contract is its row in `.env.example`. |
306
306
  | `CLEAROTRON_DEMO_PROFILES` | unset | `1` makes the profile loader return the bundled demo company, which a fresh install does not resolve (ruling 2026-09-08). Set by `clearotron demo`, `start --demo` and the suite runner, never by an operator. The gate is on the bundled layer, so a deployment's own configured store keeps its `demoData` companies either way. Effect class `harness`; the full contract is its row in `.env.example`. |
@@ -161,7 +161,7 @@ structural, or dev seam); [dev] = dev/test seam, never set in prod.
161
161
  `CLEAROTRON_AI` (anthropic-agent | openai-agent; default anthropic-agent), `CLEAROTRON_AI_BILLING`
162
162
  (subscription|api-key|cloud; cloud is Claude only), `CLEAROTRON_CODEX_PATH`,
163
163
  `CLEAROTRON_OPENAI_AUTH_FILE`, `CLEAROTRON_OPENAI_MODEL_JUDGMENT` / `CLEAROTRON_OPENAI_MODEL_SWEEP` /
164
- `CLEAROTRON_OPENAI_MODEL_CHEAP` (all gpt-5.6-sol),
164
+ `CLEAROTRON_OPENAI_MODEL_CHEAP` (gpt-6.1-sol / gpt-6.1-sol / gpt-6-luna),
165
165
  `CLEAROTRON_CLAUDE_PATH` (claude on PATH, then the copy Clearotron installed),
166
166
  `CLEAROTRON_SYNTHESIS_MODEL` (opus), `CLEAROTRON_KNOCKOUT_MODEL` (opus),
167
167
  `CLEAROTRON_MAX_BUDGET_USD` (unset).
@@ -294,16 +294,17 @@ node mcp-server/mint-token.mjs --scope ops --sub <principal-name> \
294
294
 
295
295
  - `--sub` names the principal, and it rides into every audit line. Two integrations holding ops
296
296
  tokens are then distinguishable in the log; one shared token makes them permanently indistinguishable.
297
- - `--verbs` is a least-privilege allowlist of write tools. A connector minted without `stop_run`
297
+ - `--verbs` is required for an ops token: it lists the write tools the token may call. A connector minted without `stop_run`
298
298
  cannot call it — the check sits at the one chokepoint every tool call passes, not in each tool.
299
299
  - `--accounts` caps the token to a set of company keys, so a trial integration can start demo
300
300
  searches and never a real company's.
301
301
  - The token is printed once and stored nowhere. Losing it means minting another.
302
302
 
303
303
  **Revoking one.** Every mint prints a `jti`. Write that line into the file named by
304
- `TRADEMARK_MCP_TOKEN_DENYLIST` (one `jti` per line, `#` comments allowed) and the next verification
305
- refuses the token. A **missing** denylist file means nothing is revoked, never everything — a denylist
306
- must not be able to take all authentication down.
304
+ `TRADEMARK_MCP_TOKEN_DENYLIST`, or into `~/.config/clearotron/token-denylist` when the setting is unset
305
+ (one `jti` per line, `#` comments allowed), and the next verification refuses the token. A named list
306
+ that is missing or unreadable refuses every key, and so does a default list that exists but cannot be
307
+ read. An absent default list means nothing has been revoked.
307
308
 
308
309
  **Rotating the signing secret.** Verification accepts `TRADEMARK_MCP_TOKEN_SECRET` or
309
310
  `TRADEMARK_MCP_TOKEN_SECRET_PREVIOUS`; minting always signs with the current one. Move the old value
@@ -10,7 +10,9 @@ How to work on the driver without breaking the properties that make it trustwort
10
10
 
11
11
  - **Node ≥ 22** (every workspace `package.json` declares it; CI pins 22.23.2). The repo is an npm
12
12
  workspace — `npm ci` at the root installs `driver`, `mcp-server`, `providers/oauth-mcp-bridge`
13
- and `portal-ui` together. Production deploys install with `--omit=dev --ignore-scripts`.
13
+ and `portal-ui` together. An install from a git checkout updates with `npm ci --ignore-scripts` and
14
+ keeps development packages, because the portal is rebuilt from them. A packaged install updates
15
+ through `npm install --global`.
14
16
  - **Tests**: `npm test` in the driver dir runs the *contributor* tier
15
17
  (`scripts/test-run.mjs` → `scripts/test-fast.mjs` over `test/*.test.mjs`), which leaves out the
16
18
  slowest end-to-end files so a first run finishes in minutes. `npm run test:full` is the
@@ -105,9 +105,12 @@ are no root units; everything is `systemd --user`.
105
105
  The API-key door is a fourth process (loopback :18812) with **no Access in front** — the trade is
106
106
  explicit: a mandatory key replaces the browser sign-in, and the mode refuses to start if anything
107
107
  that would weaken that (the dev auth-disable knob, a missing signing secret) is also set.
108
- - The staff unit is systemd-hardened (`NoNewPrivileges`, `ProtectSystem=strict` with only the
109
- audit-log path writable, `PrivateTmp`); both HTTP faces enforce host allowlists
110
- (DNS-rebinding protection), per-identity rate limits, body caps, and auth-before-body-read.
108
+ - The remote deployment's staff unit (`mcp-server/remote/`) is systemd-hardened (`NoNewPrivileges`,
109
+ `ProtectSystem=strict` with only the audit-log path writable, `PrivateTmp`). The portal, worker and two
110
+ assistant-door units an install runs (`driver/systemd/`) set `NoNewPrivileges`, `RestrictSUIDSGID`,
111
+ `LockPersonality` and `UMask=0007`, and nothing that limits where they write: they can write wherever
112
+ the install's account can. Both HTTP faces enforce host allowlists (DNS-rebinding protection),
113
+ per-identity rate limits, body caps, and auth-before-body-read.
111
114
  - **Outbound** traffic is enumerated in [What leaves the machine](#what-leaves-the-machine) below.
112
115
  No outbound service is given filesystem access: register, research, and case-law calls carry query
113
116
  arguments only, and the pool and run dirs are never exposed to them.
@@ -165,17 +168,17 @@ CLI's built-in `WebFetch` (`engine/mcp/gather-config.mjs`), or the driver's stan
165
168
  moment of the call, so no list here can be complete, and a deployment that needs one must enforce it at
166
169
  the network layer.
167
170
 
168
- **A published report reaches two font CDNs.** The report HTML links stylesheets on `api.fontshare.com`
169
- and `fonts.googleapis.com`, with the font files on `cdn.fontshare.com` and `fonts.gstatic.com`
170
- (`publish/render.mjs`, `publish/render-knockout.mjs`; the served CSP admits exactly
171
- those hosts at `portal-static.mjs`). The reader's browser fetches them when the report
172
- opens. The request carries no matter data. Registry links in the report resolve only when a reader
171
+ **A report carries its own fonts.** A report published from 0.4.0 on holds Plus Jakarta Sans and Fira
172
+ Code inside itself (`shared/fonts/`), and opening it contacts no other server, from disk, from an email
173
+ or in the portal. CI records every request a rendered report makes and fails on one that leaves
174
+ (`scripts/report-offline-render-check.mjs`). A report published before 0.4.0 still links its two font
175
+ services when opened from disk. The request carries no matter data. Registry links in the report resolve only when a reader
173
176
  clicks one.
174
177
 
175
178
  **The demo contacts nothing.** `bin/example.mjs` re-publishes artifacts already on disk: no `fetch` in its
176
179
  module graph, no MCP server, no engine binary. Its two subprocesses are a `git rev-parse` build stamp
177
- and the browser opener, and the portal binds `127.0.0.1`. The report it opens loads the two font
178
- stylesheets above.
180
+ and the browser opener, and the portal binds `127.0.0.1`. The report it opens loads nothing from
181
+ another server.
179
182
 
180
183
  ## Secrets
181
184
 
@@ -228,7 +231,9 @@ today — flag for the buyer's compliance review.
228
231
  and the engine-local gather MCP servers add none: each is ~130 lines over a shared 120-line stdio
229
232
  scaffolding (`driver/engine/mcp/stdio-server.mjs`) with no MCP SDK. The artifacts read layer does
230
233
  use the SDK (`@modelcontextprotocol/sdk`, `jose`). Small surface by design.
231
- - Deploy installs with `--omit=dev --ignore-scripts`. If your deploy then rebuilds a native addon,
234
+ - On an install from a git checkout, `clearotron update` installs with `npm ci --ignore-scripts`, so no
235
+ package's install script runs on the host. Development packages are kept there, because the portal is
236
+ rebuilt from them. If your deploy then rebuilds a native addon,
232
237
  make that a named, reviewed step: it is the only place a lifecycle script should run on the host.
233
238
  - Run a skill scanner over the skills tree at deploy time and abort on high/critical findings. Two
234
239
  things to check on any new host: that the scanner is actually installed (a scanner that is absent
@@ -1,5 +1,34 @@
1
1
  # clearotron-driver
2
2
 
3
+ ## 0.4.1-beta.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Fixed: A report repaired just before delivery keeps its coverage notes, such as a script that was not searched, in the report and the audit workbook.
8
+ - Fixed: A search too broad for the register to count is no longer reported as one that could not be run. The report shows it as too large to read, with the register's own figure.
9
+ - Fixed: On Signa, a crowded search over several spellings now counts each spelling alone, so a rare spelling is no longer reported as crowded.
10
+ - New: `npx clearotron mcp` starts Clearotron's MCP server for an assistant; with no install it answers from the demo's sample runs, and says so.
11
+ - Fixed: The address `clearotron start` and `clearotron demo` print for a client's assistant now answers as soon as it is printed.
12
+ - Fixed: The audit workbook's coverage summary names each area in words, not the engine's identifiers.
13
+
14
+ ## 0.4.1-beta.0
15
+
16
+ ### Patch Changes
17
+
18
+ - Fixed: A connection library is updated to close a reported weakness in how sign-in details could be sent.
19
+ - Fixed: A clearance started from a connected assistant under the Generic default now appears on Clearances when you choose Generic. Before, it showed only under All companies.
20
+ - Fixed: A register count or record that could not be fetched no longer shows the program's error message to a client. This covers the knockout workbook's count notes, the knockout report and a clearance finding card. Each shows the line the report already uses for something left open.
21
+ - Fixed: A knockout's completion message and email now give its rating, or the highest rating when several names were screened. They no longer use the term 'worst band'.
22
+ - Fixed: A client whose mark is written in another script, such as Greek, Cyrillic or Chinese, now receives the link to the report. Before, the email arrived without one.
23
+ - Fixed: In rare cases, a report repaired just before delivery listed one conflict as two findings. Its last finding also lost its full write-up. The conflict now stays one finding, and every finding keeps its write-up.
24
+ - Fixed: Reports no longer say a search "did not finish and must be re-run" when a name's non-Latin form was in fact searched. They no longer make a conclusion depend on reviewing records the search had already read in full. Knockout reports now list every territory you ordered.
25
+ - Fixed: The audit workbook no longer warns that the depth rules were applied to nothing on a clearance whose narrative is written in sections. Those rules govern per-finding write-ups, and such a narrative has none.
26
+ - Fixed: An order for the European Union now also searches each member state's own trade mark register, at no extra search cost. A national registration — in France, for example — is now found by an EU search.
27
+ - Before you upgrade: if you installed Codex yourself, update it to 0.159.0 or newer first. Older copies cannot use GPT-6.1 Sol, and searches on the OpenAI engine would stop at their first step.
28
+ - New: Searches now use the latest models: Claude Haiku 5.5, Sonnet 5.5 and Opus 5.5, and OpenAI's GPT-6.1 Sol and GPT-6 Luna. Installing or updating Clearotron brings the Claude and Codex programs up to versions that can use them.
29
+ - Fixed: Two different names written in Greek, Cyrillic or Chinese characters, or differing only in an accented letter, can now be screened in one knockout. Before, they were refused as duplicates.
30
+ - Fixed: Findings rated medium risk or higher are now checked for the owner's actual use in the same way on every company's risk ladder. That same search looks for the owner's record of oppositions and disputes, so "Likely to enforce" carries a rating instead of "not checked".
31
+
3
32
  ## 0.4.0
4
33
 
5
34
  ### Minor Changes
@@ -100,19 +100,39 @@ export function bindingLayersFor(territory) {
100
100
  layers.push({ layer: "national", office: BX_OFFICE,
101
101
  why: `${t} has no separate national register — Benelux (BX) is the national register for it` });
102
102
  } else if (t === WO_OFFICE) {
103
- // The international register ORDERED AS a territory. Same shape as the EU below: there is no
104
- // national layer to miss, because the Madrid register IS the order. Without this a matter listing
103
+ // The international register ORDERED AS a territory. There is no national layer to miss, because
104
+ // the Madrid register IS the order. Without this a matter listing
105
105
  // "International" (territory-codes.mjs normalizes it to WO) discloses a missing national register
106
106
  // that does not exist — a limitation on a register the plan searched, which is the kind a reader
107
107
  // learns to skim.
108
108
  layers.push({ layer: "international", office: WO_OFFICE, why: "the international register, ordered directly" });
109
109
  return Object.freeze(layers.map(Object.freeze));
110
- } else if (t === EU_OFFICE || t === "EU") {
111
- // The EU ordered AS a territory. There is no national layer to miss; the EU register IS the order.
112
- layers.push({ layer: "regional", office: EU_OFFICE, why: "the EU-wide register, ordered directly" });
110
+ } else if (t === EU_OFFICE) {
111
+ // The EU-wide register named by its OFFICE code, which is how a plan that is already resolved
112
+ // carries it. It names that register, not the territory: resolving a France plan's ["FR", "EM",
113
+ // "WO"] again must not turn it into a search of every member state. An EU ORDER arrives as "EU"
114
+ // (normalizeTerritory folds "European Union", EUTM and EUIPO there) and takes the branch below.
115
+ layers.push({ layer: "regional", office: EU_OFFICE, why: "the EU-wide register, named by its office code" });
113
116
  layers.push({ layer: "international", office: WO_OFFICE,
114
117
  why: "international registrations designating the EU bind it without a separate EU filing" });
115
118
  return Object.freeze(layers.map(Object.freeze));
119
+ } else if (t === "EU") {
120
+ // The EU ordered AS a territory: the EU-wide register, EVERY member state's national register, and
121
+ // international registrations designating the EU or a member. A national right blocks use in its own
122
+ // member state without appearing in the EU-wide register, so an EU order that searched only EUIPO and
123
+ // Madrid cleared none of the countries inside it — measured 2026-10-09 on both live providers: an
124
+ // EU-scoped search returns no member state's national filings, and listing the members in the same
125
+ // request reaches them. The owner's ruling of the same day: an EU order searches the national
126
+ // registers too, on every provider. Benelux is one national register for three members, listed once.
127
+ layers.push({ layer: "regional", office: EU_OFFICE, why: "the EU-wide register, ordered directly" });
128
+ for (const office of new Set(EU_MEMBERS.map((m) => (BENELUX_MEMBERS.includes(m) ? BX_OFFICE : m)))) {
129
+ layers.push({ layer: "national", office, why: office === BX_OFFICE
130
+ ? "Benelux (BX) is the national register of three EU members"
131
+ : `a national right in ${office} blocks use there without appearing in the EU-wide register` });
132
+ }
133
+ layers.push({ layer: "international", office: WO_OFFICE,
134
+ why: "an international registration designating the EU or a member state binds it without a separate filing" });
135
+ return Object.freeze(layers.map(Object.freeze));
116
136
  } else {
117
137
  layers.push({ layer: "national", office: t, why: `the national register of ${t}` });
118
138
  }
@@ -215,7 +235,9 @@ export function unsearchedLayerReason(report) {
215
235
  regional: "EU-wide rights",
216
236
  international: "international registrations designating it",
217
237
  };
218
- const missing = report.unsearched.map((u) => names[u.layer] ?? u.layer);
238
+ // One name per KIND of register: an EU order carries a national layer per member state, and a provider
239
+ // that reaches only some of them would otherwise list "the national register" once per member missed.
240
+ const missing = [...new Set(report.unsearched.map((u) => names[u.layer] ?? u.layer))];
219
241
  const list = missing.length === 1 ? missing[0]
220
242
  : `${missing.slice(0, -1).join(", ")} and ${missing[missing.length - 1]}`;
221
243
  return `Not a complete clearance for ${report.territory}: ${list} ${missing.length === 1 ? "was" : "were"} `
@@ -1,5 +1,5 @@
1
1
  {
2
- "citations": 465,
3
- "files": 3308,
4
- "checkable": 129
2
+ "citations": 257,
3
+ "files": 3469,
4
+ "checkable": 245
5
5
  }