clearotron 0.4.0-beta.2 → 0.4.0

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 (57) hide show
  1. package/THIRD-PARTY-NOTICES.md +5 -5
  2. package/build-info.json +2 -2
  3. package/driver/CHANGELOG.md +90 -0
  4. package/driver/ask-ledger.mjs +2 -2
  5. package/driver/coverage-form.mjs +6 -0
  6. package/driver/coverage-ledger.mjs +14 -2
  7. package/driver/driver.config.mjs +24 -7
  8. package/driver/engine/mcp/band-server.mjs +3 -3
  9. package/driver/engine/mcp/stdio-server.mjs +12 -1
  10. package/driver/gateway.mjs +27 -3
  11. package/driver/package.json +1 -1
  12. package/driver/pipeline-knockout.mjs +41 -10
  13. package/driver/pipeline.mjs +104 -12
  14. package/driver/progress.mjs +6 -1
  15. package/driver/provider-usage.mjs +16 -0
  16. package/driver/publish/index.mjs +11 -5
  17. package/driver/publish/knockout.mjs +16 -3
  18. package/driver/publish/render-knockout.mjs +38 -11
  19. package/driver/record-carry.mjs +17 -1
  20. package/driver/reference-strip-signatures.mjs +11 -1
  21. package/driver/register-plan.mjs +7 -3
  22. package/driver/register-served.mjs +91 -0
  23. package/driver/remedy-accounting.mjs +38 -9
  24. package/driver/reviewer-open-points.mjs +20 -23
  25. package/driver/score-redaction.mjs +416 -20
  26. package/driver/screen-gate.mjs +3 -3
  27. package/driver/skills/clearance-register/SKILL.md +0 -8
  28. package/driver/skills/clearance-register/digest.md +1 -1
  29. package/driver/skills/clearance-register/providers/corsearch.md +1 -1
  30. package/driver/skills/clearance-register/register-recipes.md +3 -9
  31. package/driver/skills/clearance-search/SKILL.md +2 -2
  32. package/driver/skills/clearance-search/phase2-execution.md +1 -1
  33. package/driver/skills/knockout-assess/SKILL.md +2 -0
  34. package/driver/stages-knockout.mjs +22 -0
  35. package/driver/suite-census.json +152 -26
  36. package/driver/unit-inventory.mjs +38 -43
  37. package/mcp-server/CHANGELOG.md +8 -0
  38. package/mcp-server/package.json +1 -1
  39. package/node_modules/brace-expansion/index.js +78 -22
  40. package/node_modules/brace-expansion/package.json +1 -1
  41. package/node_modules/readdir-glob/node_modules/brace-expansion/index.js +78 -22
  42. package/node_modules/readdir-glob/node_modules/brace-expansion/package.json +1 -1
  43. package/package.json +1 -1
  44. package/portal-ui/package.json +2 -2
  45. package/providers/_shared/term-shape.mjs +1 -1
  46. package/providers/jx/src/core.js +0 -1
  47. package/providers/jx/src/judge.js +0 -1
  48. package/providers/jx/src/nativeread.js +0 -1
  49. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  50. package/providers/oauth-mcp-bridge/package.json +1 -1
  51. package/providers/signa/src/core.js +125 -17
  52. package/scripts/e2e-first-time.mjs +12 -4
  53. package/scripts/e2e-scenario-ops.mjs +25 -2
  54. package/scripts/e2e.mjs +246 -24
  55. package/scripts/mint-reference-strip-backlog.mjs +36 -2
  56. package/scripts/score.mjs +102 -29
  57. package/shared/identifier-scan.mjs +31 -1
@@ -159,9 +159,9 @@ export const UNIT_INVENTORY = Object.freeze([
159
159
  // stamped and says where its verdict lives, and driver/updater-identity.mjs makes that verdict from
160
160
  // the digests the updater records about itself.
161
161
  attribution: "stamp",
162
- measured: "2026-09-08, the test deployment: the timer and its service are installed for that deployment's own user, the timer is enabled and ACTIVE, and it last fired within the hour — `systemctl --user list-timers`. The service's own state says nothing about this: it is `inactive` between runs and `inactive` when the timer has been stopped. The timer's ActiveState says the timer itself is still up. It is NOT a next elapse and does not promise one: `active` means UP, never SCHEDULED. Proving something will still fire takes a real next elapse, which `systemctl list-timers --all` supplies and `show -p ActiveState` cannot. Measured in testing 2026-09-09: a timer reading `ActiveState=active` whose `OnCalendar` names a date already past, so `list-timers` reports its next fire as null and it will never run again.",
162
+ measured: "2026-09-08, read on the test deployment: the timer and its service are installed, the timer is enabled and ACTIVE, and it last fired within the hour — `systemctl --user list-timers`. The service's own state says nothing about this: it is `inactive` between runs and `inactive` when the timer has been stopped. The timer's ActiveState says the timer itself is still up. It is NOT a next elapse and does not promise one: `active` means UP, never SCHEDULED. Proving something will still fire takes a real next elapse, which `systemctl list-timers --all` supplies and `show -p ActiveState` cannot. Measured in testing 2026-09-09: a timer reading `ActiveState=active` whose `OnCalendar` names a date already past, so `list-timers` reports its next fire as null and it will never run again.",
163
163
  tracked: ["clearotron-deploy.service", "clearotron-deploy.timer"],
164
- note: "the install's own updater. Placed on the test deployment, where it pulls hourly; production takes it when the owner asks.",
164
+ note: "the install's own updater.",
165
165
  // IT WAS AN ORPHAN, AND THE CONDITION IT NAMED HAS BEEN MET. This entry carried an `orphanReason`
166
166
  // saying it had been shipped and installed nowhere, and that `runsOn` would gain "test" the day a
167
167
  // deploy placed it. It has: the timer is installed, enabled and firing, and `runsOn` says so. The prose
@@ -212,10 +212,8 @@ export const UNIT_INVENTORY = Object.freeze([
212
212
  // prose references in driver/systemd/render-units.mjs also describe this file as though it were in
213
213
  // the tree; they are stale today either way.
214
214
  unit: "profile-service", runsOn: [],
215
- measured: "2026-09-08: production read by account — the deployment account carries six clearotron-* "
216
- + "unit files and the doctrine-sync timer, the legacy account carries five units belonging to the "
217
- + "other product plus unloadable residue, and no unit of THIS name is under either. Names and enable "
218
- + "state only; no unit contents were read.",
215
+ measured: "2026-09-08, read on production: no unit of this name is present. Names and enable state only; "
216
+ + "no unit contents were read.",
219
217
  tracked: null,
220
218
  untrackedReason: "the repository has never carried this file — `git log --all` on the path is empty. "
221
219
  + "It was described as deploying from a copy held on the deployment itself, in the same "
@@ -226,9 +224,9 @@ export const UNIT_INVENTORY = Object.freeze([
226
224
  // it loaded no EnvironmentFile and so had no `${VAR}` systemd could expand. The owner's one-config-
227
225
  // per-server-box ruling gives it `EnvironmentFile=%h/.env` like every other service, which makes the
228
226
  // checkout path an ordinary systemd expansion and leaves no placeholder to render.
229
- note: "NOT RUNNING ANYWHERE, measured 2026-09-08. This row said LIVE ON PRODUCTION for as long as it "
230
- + "existed and no enumeration ever supported it. Was a TEMPLATE unit carrying identity-provider "
231
- + "values inline; generic since the product installed its own units.",
227
+ note: "Was a TEMPLATE unit carrying identity-provider values inline; generic since the product "
228
+ + "installed its own units. It claimed a box for as long as it existed and no reading ever "
229
+ + "supported it — which is what the dated measurement beside it exists to prevent.",
232
230
  orphanReason: "CLAIMED A DEPLOYMENT IT WAS NEVER MEASURED ON, which is a FOURTH kind of orphan and "
233
231
  + "the only one that was ever a wrong claim rather than a waiting decision: the other three run "
234
232
  + "under another name, were deliberately switched off, or have simply never been installed. This "
@@ -245,7 +243,7 @@ export const UNIT_INVENTORY = Object.freeze([
245
243
  // REPORTED, never a fault, for exactly this case. It becomes `["prod"]` in the change that DEPLOYS
246
244
  // it, against a fresh enumeration, which is the ratchet working rather than being edited around.
247
245
  unit: "clearotron-portal", runsOn: ["test"], tracked: ["clearotron-portal.service"],
248
- measured: "2026-09-07, test box, `systemctl --user show` with a working user bus: ActiveState=active, UnitFileState=enabled, fragment under ~/.config/systemd/user/.",
246
+ measured: "2026-09-07, read on the test deployment with a working user bus: ActiveState=active, UnitFileState=enabled, its fragment under the account's own unit directory.",
249
247
  note: "NEW, SHIPPED BUT NOT YET DEPLOYED. The portal had NO unit at all — it "
250
248
  + "and the MCP face are the two children bin/start.mjs supervises, and neither ran under systemd, "
251
249
  + "so a hosted deployment kept them alive with a hand launcher and `clearotron start` had nothing "
@@ -262,7 +260,7 @@ export const UNIT_INVENTORY = Object.freeze([
262
260
  // Same as clearotron-portal above: shipped, running nowhere, declared as an orphan rather than
263
261
  // asserting a production state no enumeration has ever shown.
264
262
  unit: "clearotron-mcp-face", runsOn: ["test"], tracked: ["clearotron-mcp-face.service"],
265
- measured: "2026-09-07, test box, `systemctl --user show` with a working user bus: ActiveState=active, UnitFileState=enabled, fragment under ~/.config/systemd/user/.",
263
+ measured: "2026-09-07, read on the test deployment with a working user bus: ActiveState=active, UnitFileState=enabled, its fragment under the account's own unit directory.",
266
264
  note: "NEW, SHIPPED BUT NOT YET DEPLOYED. The engine door the portal calls over "
267
265
  + "MCP; the other of the two processes that had no unit.",
268
266
  orphanReason: "THE ENGINE DOOR THE PORTAL CALLS, shipped with no box carrying it yet. Same kind as "
@@ -291,7 +289,7 @@ export const UNIT_INVENTORY = Object.freeze([
291
289
  // door beside it: it is in SERVER_INSTALL_SET, and it leaves this list on the day a measured
292
290
  // enumeration of a box shows it running.
293
291
  unit: "clearotron-client-mcp", runsOn: ["test"], tracked: ["clearotron-client-mcp.service"],
294
- measured: "2026-09-07, test box, `systemctl --user show` with a working user bus: ActiveState=active, UnitFileState=enabled, fragment under ~/.config/systemd/user/.",
292
+ measured: "2026-09-07, read on the test deployment with a working user bus: ActiveState=active, UnitFileState=enabled, its fragment under the account's own unit directory.",
295
293
  note: "INSTALLED WITH THE PRODUCT since 2026-09-03. The client door "
296
294
  + "— the one surface that accepts an account-scoped key, and a separate process from the engine "
297
295
  + "door, which refuses one outright.",
@@ -309,10 +307,9 @@ export const UNIT_INVENTORY = Object.freeze([
309
307
  // Written as part of retiring the path-watcher posture, because retiring `prelim-driver.*` without
310
308
  // it leaves a hosted box healthy-looking and draining nothing.
311
309
  unit: "clearotron-worker", runsOn: ["test"], tracked: ["clearotron-worker.service"],
312
- measured: "2026-09-07, test box, `systemctl --user show` with a working user bus: ActiveState=active, UnitFileState=enabled, fragment under ~/.config/systemd/user/.",
310
+ measured: "2026-09-07, read on the test deployment with a working user bus: ActiveState=active, UnitFileState=enabled, its fragment under the account's own unit directory.",
313
311
  note: "NEW. The drain, as one plain service unit invoking the entrypoint "
314
- + "directly (owner ruling 2026-08-31). Ships tracked, runs nowhere yet — production takes it at "
315
- + "its rebuild, which is the same event that lets the retired units' files leave the tree.",
312
+ + "directly (owner ruling 2026-08-31). Ships tracked and runs nowhere yet.",
316
313
  orphanReason: "AWAITING A BOX, not an orphan by design. It is the hosted posture's drainer, and the "
317
314
  + "hosted posture is an opt-in step no install performs. It must NOT be "
318
315
  + "started beside `clearotron start`, which supervises its own worker — the two postures are "
@@ -320,10 +317,10 @@ export const UNIT_INVENTORY = Object.freeze([
320
317
  },
321
318
  {
322
319
  unit: "trademark-portal", runsOn: [],
323
- orphanReason: "THE PRE-RENAME PORTAL. The deployment account runs `clearotron-portal`; this name is "
320
+ orphanReason: "THE PRE-RENAME PORTAL. Production runs `clearotron-portal`; this name is "
324
321
  + "what that service used to be called, and the 2026-09-08 reading finds no unit of this name under "
325
322
  + "any account. It ran on production until the rename and the entry was never re-measured after it.",
326
- measured: "2026-09-08: production read by account — the deployment account carries six clearotron-* unit files and the doctrine-sync timer, the legacy account carries five units of the other product plus unloadable residue, and no unit of THIS name is under either. Names and enable state only. This line previously carried a 2026-09-07 reading of the test deployment — LoadState=not-found there, with the production claim left alone because production had not been measured. It has been now, and this is that reading.", tracked: null,
323
+ measured: "2026-09-08, read on production: no unit of this name is present. Names and enable state only. This line previously carried a 2026-09-07 reading of the test deployment — LoadState=not-found there, with the production claim left alone because production had not been measured. It has been now, and this is that reading.", tracked: null,
327
324
  untrackedReason: "the deployed copy carries real Cloudflare Access team/AUD/domain values inline. "
328
325
  + "A tracked file would be the placeholder TEMPLATE, merged by hand after a "
329
326
  + "diff — writing one carelessly replaces working auth with placeholders that look configured.",
@@ -335,10 +332,10 @@ export const UNIT_INVENTORY = Object.freeze([
335
332
  },
336
333
  {
337
334
  unit: "trademark-ops-mcp", runsOn: [],
338
- orphanReason: "THE PRE-RENAME OPERATOR DOOR. The deployment account runs `clearotron-mcp-face`; the "
335
+ orphanReason: "THE PRE-RENAME OPERATOR DOOR. Production runs `clearotron-mcp-face`; the "
339
336
  + "2026-09-08 reading finds no unit of this name under any account. Same rename as the portal it "
340
337
  + "serves, and the same claim carried across it unmeasured.",
341
- measured: "2026-09-08: production read by account — the deployment account carries six clearotron-* unit files and the doctrine-sync timer, the legacy account carries five units of the other product plus unloadable residue, and no unit of THIS name is under either. Names and enable state only. This line previously carried a 2026-09-07 reading of the test deployment — LoadState=not-found there, with the production claim left alone because production had not been measured. It has been now, and this is that reading.", tracked: null,
338
+ measured: "2026-09-08, read on production: no unit of this name is present. Names and enable state only. This line previously carried a 2026-09-07 reading of the test deployment — LoadState=not-found there, with the production claim left alone because production had not been measured. It has been now, and this is that reading.", tracked: null,
342
339
  untrackedReason: "same CF Access inline-values shape as the portal it serves.",
343
340
  },
344
341
  {
@@ -346,22 +343,20 @@ export const UNIT_INVENTORY = Object.freeze([
346
343
  orphanReason: "THE PRE-RENAME CLIENT DOOR. `clearotron-client-mcp` runs on both the production and "
347
344
  + "the test deployments; the 2026-09-08 reading finds no unit of THIS name under any account. The "
348
345
  + "tracked template stays because the repository still ships it — see the note.",
349
- measured: "2026-09-08: production read by account — the deployment account carries six clearotron-* unit files and the doctrine-sync timer, the legacy account carries five units of the other product plus unloadable residue, and no unit of THIS name is under either. Names and enable state only. This line previously carried a 2026-09-07 reading of the test deployment — LoadState=not-found there, with the production claim left alone because production had not been measured. It has been now, and this is that reading.", tracked: ["client-mcp.service"],
346
+ measured: "2026-09-08, read on production: no unit of this name is present. Names and enable state only. This line previously carried a 2026-09-07 reading of the test deployment — LoadState=not-found there, with the production claim left alone because production had not been measured. It has been now, and this is that reading.", tracked: ["client-mcp.service"],
350
347
  note: "TRACKED, in mcp-server/remote/ — this entry said it had no file, and the file was there. It is "
351
348
  + "a banner-marked TEMPLATE, so the live copy differing from it is the arrangement, not drift. "
352
- + "#1147 ADDED THE TEST BOX (2026-08-18): the client surface ran nowhere but production, so the test "
353
- + "instance exercised none of it and #764 sat merged-awaiting-e2e with no round able to settle it. "
354
- + "The issue reported that no unit shipped; a unit did ship, in mcp-server/remote/ rather than "
355
- + "driver/systemd/ — the same one-directory blind spot this file's own header records. The test "
356
- + "unit runs the same binary in dev mode on its own port; see the template for why that port is "
357
- + "not the default.",
349
+ + "It was reported as shipping no unit; a unit did ship, in mcp-server/remote/ rather than "
350
+ + "driver/systemd/ — the same one-directory blind spot this file's own header records. A "
351
+ + "deployment unit runs the same binary in dev mode on its own port; see the template for why "
352
+ + "that port is not the default.",
358
353
  },
359
354
  {
360
355
  unit: "client-mcp-apikey", runsOn: ["test"],
361
- measured: "2026-09-08, read by account: enabled and present on the TEST deployment, beside the "
362
- + "renamed `clearotron-client-mcp`, and INACTIVE — enabled with no running process. Absent from "
363
- + "production under any account. The entry declared production and the reading puts it on the "
364
- + "other deployment — the claim was not stale, it named the wrong machine.",
356
+ measured: "2026-09-08, read on the test deployment: enabled and present, beside the renamed "
357
+ + "`clearotron-client-mcp`, and INACTIVE — enabled with no running process. Absent from "
358
+ + "production. The entry declared production and the reading puts it on the other deployment "
359
+ + "— the claim was not stale, it named the wrong machine.",
365
360
  tracked: ["client-mcp-apikey.service"],
366
361
  // THE SEPARATE QUESTION, ANSWERED. The line above used to end by saying that why it runs beside the
367
362
  // renamed door was a question for somebody else. It is a survivor of the rename, not a posture
@@ -401,7 +396,7 @@ export const UNIT_INVENTORY = Object.freeze([
401
396
  orphanReason: "A PRE-RENAME NAME OF THE RETIRED DEPLOYMENT, and the one of them whose tracked file "
402
397
  + "is GENERIC rather than a template — so the file is ready to be compared against a live copy the "
403
398
  + "day something runs it, and nothing does. Read as absent under every account on 2026-09-08.",
404
- measured: "2026-09-08: production read by account — no unit of this name is under the deployment account or the legacy one. Names and enable state only; no unit contents were read.", tracked: ["trademark-artifacts-http.service"],
399
+ measured: "2026-09-08, read on production: no unit of this name is present. Names and enable state only; no unit contents were read.", tracked: ["trademark-artifacts-http.service"],
405
400
  note: "TRACKED, in mcp-server/remote/. GENERIC, not a template: it carries no banner and no "
406
401
  + "placeholder, and defers its CF Access values to the EnvironmentFile — so its live copy is "
407
402
  + "expected to MATCH the tracked file, and a difference is real drift.",
@@ -455,11 +450,11 @@ export const UNIT_INVENTORY = Object.freeze([
455
450
  },
456
451
  {
457
452
  unit: "client-access", runsOn: [],
458
- orphanReason: "NOT THIS REPOSITORY'S SERVICE, and no longer running under this name: the legacy "
459
- + "account holds it only as `client-access.service.retired-20260904`, a suffix systemd never reads. "
453
+ orphanReason: "NOT THIS REPOSITORY'S SERVICE, and no longer running under this name: production holds it "
454
+ + "only under a retired-suffixed filename, a suffix systemd never reads. "
460
455
  + "Declared here so a unit sharing a machine with ours is not invisible, and now declared as gone.",
461
- measured: "2026-09-08: present on the legacy account ONLY as a retired-suffixed filename, which "
462
- + "systemd does not load. No loadable unit of this name under any account.", tracked: null,
456
+ measured: "2026-09-08, read on production: present ONLY as a retired-suffixed filename, which systemd "
457
+ + "does not load. No loadable unit of this name.", tracked: null,
463
458
  untrackedReason: "it is not this repo's service. Its ExecStart runs a script from a different "
464
459
  + "product's checkout on the same box, not under this clone — so there is no code here to "
465
460
  + "template, and the old reason (CF Access values inline) would send the next reader hunting for a "
@@ -524,12 +519,12 @@ export const UNIT_INVENTORY = Object.freeze([
524
519
  + "flag was ever captured, so stopping it dropped nothing. The decision owed, if any, is whether the "
525
520
  + "feedback machinery is removed rather than parked; driver/run-activity.mjs still carries a note "
526
521
  + "addressed to whoever picks that up, and it is still unclaimed.",
527
- note: "STOPPED. Production ran it every 30 minutes against an input nothing writes to any more"
528
- + "; the owner's word came 2026-08-20 and it was disabled the same evening.",
522
+ note: "STOPPED against an input nothing writes to any more; the owner's word came 2026-08-20 "
523
+ + "and it was disabled the same evening.",
529
524
  },
530
525
  // ── ANOTHER PRODUCT'S UNITS ARE NOT DECLARED HERE, AND THAT IS THE RULE (ruling 594, 2026-09-27) ──
531
526
  //
532
- // Five entries stood here describing a different product's services on the production box — a gateway, a
527
+ // Five entries stood here describing a different product's services on production — a gateway, a
533
528
  // notify adapter and three bridges — each with prose about how that box actually runs them. The owner's
534
529
  // rule: deployment-specific entries have no place in public open-source code, and the health check needs
535
530
  // its own services only.
@@ -549,15 +544,15 @@ export const UNIT_INVENTORY = Object.freeze([
549
544
  // deployments. Not here. This file ships.
550
545
  {
551
546
  unit: "trademark-test-deploy", runsOn: [],
552
- measured: "2026-09-07, test box: LoadState=not-found. The hourly deploy runs as clearotron-deploy.timer, which has its own entry; this name is what that one used to be called.", tracked: null,
547
+ measured: "2026-09-07, read on the test deployment: LoadState=not-found. The hourly deploy runs as clearotron-deploy.timer, which has its own entry; this name is what that one used to be called.", tracked: null,
553
548
  // AN EMPTY runsOn OWES A REASON, and this one is a supersession rather than an orphan: the hourly
554
- // deploy still runs on the test box, under the name `clearotron-deploy`, which has its own entry and
549
+ // deploy still runs on the test deployment, under the name `clearotron-deploy`, which has its own entry
555
550
  // its own measurement. This name is what that unit used to be called. It is kept rather than deleted
556
551
  // because the unit FILE is still tracked and an inventory of what ships should account for every
557
552
  // tracked file — deleting the entry would make the file unaccounted for, which is the ratchet this
558
553
  // file exists to keep shut.
559
554
  orphanReason: "SUPERSEDED, NOT RETIRED — the hourly deploy of the test instance runs as `clearotron-deploy`, measured active there on 2026-09-07. This entry is the pre-rename name for the same job, so it runs on no box under THIS name and the boxes moved to the entry that replaced it. The tracked file stays until production is rebuilt, which is the same disposition the other retired units carry.",
560
- untrackedReason: "the hourly --ff-only deploy of the TEST instance. It exists on the test box only "
555
+ untrackedReason: "the hourly --ff-only deploy of the TEST instance. It exists on that deployment only "
561
556
  + "and by design: production never auto-deploys, so a tracked file shipped to both would be a unit "
562
557
  + "production must be trusted never to enable.",
563
558
  },
@@ -565,7 +560,7 @@ export const UNIT_INVENTORY = Object.freeze([
565
560
  // row was right until the moment it was wrong. That nightly rotation was retired in code by:
566
561
  // bin/register-ledger-prune.mjs is deleted, record bodies now live in each run's own directory and
567
562
  // are purged with the run, so there is no global file left to rotate. The row stayed here on
568
- // purpose while the TIMER was still installed on the test box — dropping it first would have made
563
+ // purpose while the TIMER was still installed on the test deployment — dropping it first would have
569
564
  // an installed unit invisible to the very check that exists to find installed units nothing
570
565
  // describes. The timer was disabled and stopped on 2026-08-13, four hours before it would have
571
566
  // fired against the deleted script, and this row went with it. Re-listing it would re-declare a
@@ -728,7 +723,7 @@ export function unitInventoryVerdict({
728
723
  // units shipped WITH entries, those entries kept `runsOn: []`, and the pre-rename names kept the
729
724
  // boxes. Every arm scoped to CHECKED_UNITS — which filters on a NON-EMPTY runsOn — therefore asked
730
725
  // systemd about sixteen names that mostly named nothing and reported "0 active; 16 at rest", while
731
- // four services ran under names no entry claimed. Measured on the test box 2026-09-07.
726
+ // four services ran under names no entry claimed. Measured on the test deployment, 2026-09-07.
732
727
  //
733
728
  // This needs NO knowledge of which box it is running on, which is what makes it the right shape: an
734
729
  // entry saying a unit runs on no box is contradicted by that unit running HERE, wherever here is.
@@ -1,5 +1,13 @@
1
1
  # trademark-artifacts-mcp
2
2
 
3
+ ## 0.4.0
4
+
5
+ No changes in this release.
6
+
7
+ ## 0.4.0-beta.3
8
+
9
+ No changes in this release.
10
+
3
11
  ## 0.4.0-beta.2
4
12
 
5
13
  No changes in this release.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-artifacts-mcp",
3
- "version": "0.4.0-beta.2",
3
+ "version": "0.4.0",
4
4
  "license": "AGPL-3.0-only",
5
5
  "private": true,
6
6
  "description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
@@ -23,6 +23,26 @@ var EXPANSION_MAX = 100000
23
23
  // characters) so legitimate input is unaffected.
24
24
  var EXPANSION_MAX_LENGTH = 4000000
25
25
 
26
+ // `expand` recurses once per level of brace *nesting* - both when expanding a
27
+ // set's comma members and when re-wrapping a set whose body is a single part.
28
+ // The CVE-2026-14257 fix made the *tail* iterative (recursion on `m.post`, one
29
+ // level per chained group), which left nesting depth unbounded: about 3,100
30
+ // levels of `{{{...a,b...}}}` - only ~6KB of input - exhausted the native stack
31
+ // and crashed the process. `EXPANSION_MAX_DEPTH` bounds how deep the parser
32
+ // will follow nesting. It sits far above any realistic pattern and well below
33
+ // the depth at which the stack runs out.
34
+ var EXPANSION_MAX_DEPTH = 1000
35
+
36
+ // Bash keeps a quirk where a brace group followed by a comma set still expands
37
+ // (`{a},b}`). The parser implements it by rewriting the string and restarting
38
+ // the scan, absorbing one `}` per pass. `n` trailing braces therefore cost `n`
39
+ // full passes over a string that itself grows by one `escClose` sentinel each
40
+ // time - quadratic in `n`, with a ~26x constant from the sentinel's length.
41
+ // 128KB of `'{a}' + '}'.repeat(n) + ',z}'` blocked the event loop for 27
42
+ // seconds to produce two results. `EXPANSION_MAX_REWRITES` bounds how many
43
+ // times the scan may restart. Real `{a},b}` input needs a handful.
44
+ var EXPANSION_MAX_REWRITES = 1000
45
+
26
46
  function numeric(str) {
27
47
  return parseInt(str, 10) == str
28
48
  ? parseInt(str, 10)
@@ -46,34 +66,54 @@ function unescapeBraces(str) {
46
66
  }
47
67
 
48
68
 
69
+ // Like `target.push(...items)` but doesn't overflow the stack
70
+ function pushAll(target, items) {
71
+ for (var i = 0; i < items.length; i++) {
72
+ target.push(items[i]);
73
+ }
74
+ }
75
+
49
76
  // Basically just str.split(","), but handling cases
50
77
  // where we have nested braced sections, which should be
51
78
  // treated as individual members, like {a,{b,c},d}
52
79
  function parseCommaParts(str) {
53
- if (!str)
54
- return [''];
55
-
56
80
  var parts = [];
57
- var m = balanced('{', '}', str);
58
81
 
59
- if (!m)
60
- return str.split(',');
82
+ // Walk the brace groups iteratively. Recursing on `post` once per group let a
83
+ // chain of them exhaust the stack - the parsing-side counterpart to
84
+ // the `expand` overflow fixed for CVE-2026-14257, and not something `max` or
85
+ // `maxLength` can bound, since it happens before expansion.
86
+ //
87
+ // The part the next chunk continues
88
+ var carry = '';
61
89
 
62
- var pre = m.pre;
63
- var body = m.body;
64
- var post = m.post;
65
- var p = pre.split(',');
90
+ for (;;) {
91
+ var m = balanced('{', '}', str);
66
92
 
67
- p[p.length-1] += '{' + body + '}';
68
- var postParts = parseCommaParts(post);
69
- if (post.length) {
70
- p[p.length-1] += postParts.shift();
71
- p.push.apply(p, postParts);
72
- }
93
+ if (!m) {
94
+ var tail = str.split(',');
95
+ tail[0] = carry + tail[0];
96
+ pushAll(parts, tail);
97
+ return parts;
98
+ }
73
99
 
74
- parts.push.apply(parts, p);
100
+ var pre = m.pre;
101
+ var body = m.body;
102
+ var post = m.post;
103
+ var p = pre.split(',');
75
104
 
76
- return parts;
105
+ p[0] = carry + p[0];
106
+ p[p.length-1] += '{' + body + '}';
107
+
108
+ if (!post.length) {
109
+ pushAll(parts, p);
110
+ return parts;
111
+ }
112
+
113
+ carry = p.pop();
114
+ pushAll(parts, p);
115
+ str = post;
116
+ }
77
117
  }
78
118
 
79
119
  function expandTop(str, options) {
@@ -83,6 +123,8 @@ function expandTop(str, options) {
83
123
  options = options || {};
84
124
  var max = options.max == null ? EXPANSION_MAX : options.max;
85
125
  var maxLength = options.maxLength == null ? EXPANSION_MAX_LENGTH : options.maxLength;
126
+ var maxDepth = options.maxDepth == null ? EXPANSION_MAX_DEPTH : options.maxDepth;
127
+ var maxRewrites = options.maxRewrites == null ? EXPANSION_MAX_REWRITES : options.maxRewrites;
86
128
 
87
129
  // I don't know why Bash 4.3 does this, but it does.
88
130
  // Anything starting with {} will have the first two bytes preserved
@@ -94,7 +136,7 @@ function expandTop(str, options) {
94
136
  str = '\\{\\}' + str.substr(2);
95
137
  }
96
138
 
97
- return expand(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
139
+ return expand(escapeBraces(str), max, maxLength, maxDepth, 0, maxRewrites, true).map(unescapeBraces);
98
140
  }
99
141
 
100
142
  function identity(e) {
@@ -218,8 +260,18 @@ function expand(
218
260
  str,
219
261
  max,
220
262
  maxLength,
263
+ maxDepth,
264
+ depth,
265
+ maxRewrites,
221
266
  isTop
222
267
  ) {
268
+ // Too deeply nested to keep following: treat the rest as literal, the same
269
+ // way a group that cannot expand is already handled. Truncating rather than
270
+ // throwing keeps expansion total, matching `max` and `maxLength`.
271
+ if (depth > maxDepth) {
272
+ return [str];
273
+ }
274
+
223
275
  // Consume the string's top-level brace groups left to right, threading a
224
276
  // running set of combined prefixes (`acc`). Expanding the tail iteratively -
225
277
  // rather than recursing on `m.post` once per group - keeps the native stack
@@ -240,6 +292,9 @@ function expand(
240
292
  // `accBase[a]` records how much of `acc[a]` predates the current run;
241
293
  // `combine` treats an expansion as empty when it adds nothing past that.
242
294
  var accBase = [0]
295
+ // How many times the `{a},b}` rewrite below has restarted the scan. Each pass
296
+ // re-reads the whole string, so leaving this unbounded is quadratic.
297
+ var rewrites = 0
243
298
  var dropEmpties = false
244
299
  var firstGroup = true
245
300
  var nextBase
@@ -271,7 +326,8 @@ function expand(
271
326
  var isOptions = m.body.indexOf(',') >= 0;
272
327
  if (!isSequence && !isOptions) {
273
328
  // {a},b}
274
- if (m.post.match(/,(?!,).*\}/)) {
329
+ if (rewrites < maxRewrites && m.post.match(/,(?!,).*\}/)) {
330
+ rewrites++;
275
331
  str = m.pre + '{' + m.body + escClose + m.post;
276
332
  // The rewritten string is expanded as if it were a fresh top-level one,
277
333
  // so start a new empty-drop run: anchor the baseline at what `acc`
@@ -310,7 +366,7 @@ function expand(
310
366
  var n = parseCommaParts(m.body);
311
367
  if (n.length === 1 && n[0] !== undefined) {
312
368
  // x{{a,b}}y ==> x{a}y x{b}y
313
- n = expand(n[0], max, maxLength, false).map(embrace);
369
+ n = expand(n[0], max, maxLength, maxDepth, depth + 1, maxRewrites, false).map(embrace);
314
370
  //XXX is this necessary? Can't seem to hit it in tests.
315
371
  /* c8 ignore start */
316
372
  if (n.length === 1) {
@@ -349,7 +405,7 @@ function expand(
349
405
  values = []
350
406
  var valuesLength = 0
351
407
  outer: for (var j = 0; j < n.length; j++) {
352
- var expanded = expand(n[j], max, maxLength, false)
408
+ var expanded = expand(n[j], max, maxLength, maxDepth, depth + 1, maxRewrites, false)
353
409
  for (var k = 0; k < expanded.length; k++) {
354
410
  var v = expanded[k]
355
411
  if (dropsEmpties && !v) continue
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "brace-expansion",
3
3
  "description": "Brace expansion as known from sh/bash",
4
- "version": "1.1.18",
4
+ "version": "1.1.21",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git://github.com/juliangruber/brace-expansion.git"
@@ -22,6 +22,26 @@ var EXPANSION_MAX = 100000
22
22
  // characters) so legitimate input is unaffected.
23
23
  var EXPANSION_MAX_LENGTH = 4000000
24
24
 
25
+ // `expand` recurses once per level of brace *nesting* - both when expanding a
26
+ // set's comma members and when re-wrapping a set whose body is a single part.
27
+ // The CVE-2026-14257 fix made the *tail* iterative (recursion on `m.post`, one
28
+ // level per chained group), which left nesting depth unbounded: about 3,100
29
+ // levels of `{{{...a,b...}}}` - only ~6KB of input - exhausted the native stack
30
+ // and crashed the process. `EXPANSION_MAX_DEPTH` bounds how deep the parser
31
+ // will follow nesting. It sits far above any realistic pattern and well below
32
+ // the depth at which the stack runs out.
33
+ var EXPANSION_MAX_DEPTH = 1000
34
+
35
+ // Bash keeps a quirk where a brace group followed by a comma set still expands
36
+ // (`{a},b}`). The parser implements it by rewriting the string and restarting
37
+ // the scan, absorbing one `}` per pass. `n` trailing braces therefore cost `n`
38
+ // full passes over a string that itself grows by one `escClose` sentinel each
39
+ // time - quadratic in `n`, with a ~26x constant from the sentinel's length.
40
+ // 128KB of `'{a}' + '}'.repeat(n) + ',z}'` blocked the event loop for 27
41
+ // seconds to produce two results. `EXPANSION_MAX_REWRITES` bounds how many
42
+ // times the scan may restart. Real `{a},b}` input needs a handful.
43
+ var EXPANSION_MAX_REWRITES = 1000
44
+
25
45
  function numeric(str) {
26
46
  return parseInt(str, 10) == str
27
47
  ? parseInt(str, 10)
@@ -45,34 +65,54 @@ function unescapeBraces(str) {
45
65
  }
46
66
 
47
67
 
68
+ // Like `target.push(...items)` but doesn't overflow the stack
69
+ function pushAll(target, items) {
70
+ for (var i = 0; i < items.length; i++) {
71
+ target.push(items[i]);
72
+ }
73
+ }
74
+
48
75
  // Basically just str.split(","), but handling cases
49
76
  // where we have nested braced sections, which should be
50
77
  // treated as individual members, like {a,{b,c},d}
51
78
  function parseCommaParts(str) {
52
- if (!str)
53
- return [''];
54
-
55
79
  var parts = [];
56
- var m = balanced('{', '}', str);
57
80
 
58
- if (!m)
59
- return str.split(',');
81
+ // Walk the brace groups iteratively. Recursing on `post` once per group let a
82
+ // chain of them exhaust the stack - the parsing-side counterpart to
83
+ // the `expand` overflow fixed for CVE-2026-14257, and not something `max` or
84
+ // `maxLength` can bound, since it happens before expansion.
85
+ //
86
+ // The part the next chunk continues
87
+ var carry = '';
60
88
 
61
- var pre = m.pre;
62
- var body = m.body;
63
- var post = m.post;
64
- var p = pre.split(',');
89
+ for (;;) {
90
+ var m = balanced('{', '}', str);
65
91
 
66
- p[p.length-1] += '{' + body + '}';
67
- var postParts = parseCommaParts(post);
68
- if (post.length) {
69
- p[p.length-1] += postParts.shift();
70
- p.push.apply(p, postParts);
71
- }
92
+ if (!m) {
93
+ var tail = str.split(',');
94
+ tail[0] = carry + tail[0];
95
+ pushAll(parts, tail);
96
+ return parts;
97
+ }
98
+
99
+ var pre = m.pre;
100
+ var body = m.body;
101
+ var post = m.post;
102
+ var p = pre.split(',');
103
+
104
+ p[0] = carry + p[0];
105
+ p[p.length-1] += '{' + body + '}';
72
106
 
73
- parts.push.apply(parts, p);
107
+ if (!post.length) {
108
+ pushAll(parts, p);
109
+ return parts;
110
+ }
74
111
 
75
- return parts;
112
+ carry = p.pop();
113
+ pushAll(parts, p);
114
+ str = post;
115
+ }
76
116
  }
77
117
 
78
118
  function expandTop(str, options) {
@@ -82,6 +122,8 @@ function expandTop(str, options) {
82
122
  options = options || {};
83
123
  var max = options.max == null ? EXPANSION_MAX : options.max;
84
124
  var maxLength = options.maxLength == null ? EXPANSION_MAX_LENGTH : options.maxLength;
125
+ var maxDepth = options.maxDepth == null ? EXPANSION_MAX_DEPTH : options.maxDepth;
126
+ var maxRewrites = options.maxRewrites == null ? EXPANSION_MAX_REWRITES : options.maxRewrites;
85
127
 
86
128
  // I don't know why Bash 4.3 does this, but it does.
87
129
  // Anything starting with {} will have the first two bytes preserved
@@ -93,7 +135,7 @@ function expandTop(str, options) {
93
135
  str = '\\{\\}' + str.substr(2);
94
136
  }
95
137
 
96
- return expand(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
138
+ return expand(escapeBraces(str), max, maxLength, maxDepth, 0, maxRewrites, true).map(unescapeBraces);
97
139
  }
98
140
 
99
141
  function embrace(str) {
@@ -205,8 +247,18 @@ function expand(
205
247
  str,
206
248
  max,
207
249
  maxLength,
250
+ maxDepth,
251
+ depth,
252
+ maxRewrites,
208
253
  isTop
209
254
  ) {
255
+ // Too deeply nested to keep following: treat the rest as literal, the same
256
+ // way a group that cannot expand is already handled. Truncating rather than
257
+ // throwing keeps expansion total, matching `max` and `maxLength`.
258
+ if (depth > maxDepth) {
259
+ return [str];
260
+ }
261
+
210
262
  // Consume the string's top-level brace groups left to right, threading a
211
263
  // running set of combined prefixes (`acc`). Expanding the tail iteratively -
212
264
  // rather than recursing on `m.post` once per group - keeps the native stack
@@ -219,6 +271,9 @@ function expand(
219
271
  // comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
220
272
  // is on the final strings, so it is applied to whichever `combine` produces
221
273
  // them (the one with no brace set left in the tail).
274
+ // How many times the `{a},b}` rewrite below has restarted the scan. Each pass
275
+ // re-reads the whole string, so leaving this unbounded is quadratic.
276
+ var rewrites = 0
222
277
  var dropEmpties = false
223
278
  var firstGroup = true
224
279
 
@@ -254,7 +309,8 @@ function expand(
254
309
  var isOptions = m.body.indexOf(',') >= 0;
255
310
  if (!isSequence && !isOptions) {
256
311
  // {a},b}
257
- if (m.post.match(/,(?!,).*\}/)) {
312
+ if (rewrites < maxRewrites && m.post.match(/,(?!,).*\}/)) {
313
+ rewrites++;
258
314
  str = m.pre + '{' + m.body + escClose + m.post;
259
315
  isTop = true;
260
316
  continue;
@@ -282,7 +338,7 @@ function expand(
282
338
  var n = parseCommaParts(m.body);
283
339
  if (n.length === 1 && n[0] !== undefined) {
284
340
  // x{{a,b}}y ==> x{a}y x{b}y
285
- n = expand(n[0], max, maxLength, false).map(embrace);
341
+ n = expand(n[0], max, maxLength, maxDepth, depth + 1, maxRewrites, false).map(embrace);
286
342
  //XXX is this necessary? Can't seem to hit it in tests.
287
343
  /* c8 ignore start */
288
344
  if (n.length === 1) {
@@ -316,7 +372,7 @@ function expand(
316
372
  values = []
317
373
  var valuesLength = 0
318
374
  outer: for (var j = 0; j < n.length; j++) {
319
- var expanded = expand(n[j], max, maxLength, false)
375
+ var expanded = expand(n[j], max, maxLength, maxDepth, depth + 1, maxRewrites, false)
320
376
  for (var k = 0; k < expanded.length; k++) {
321
377
  var v = expanded[k]
322
378
  if (dropsEmpties && !v) continue
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "brace-expansion",
3
3
  "description": "Brace expansion as known from sh/bash",
4
- "version": "2.1.4",
4
+ "version": "2.1.7",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git://github.com/juliangruber/brace-expansion.git"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clearotron",
3
3
  "type": "module",
4
- "version": "0.4.0-beta.2",
4
+ "version": "0.4.0",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
7
7
  "type": "git",
@@ -2,7 +2,7 @@
2
2
  "name": "portal-ui",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.4.0-beta.2",
5
+ "version": "0.4.0",
6
6
  "license": "AGPL-3.0-only",
7
7
  "description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
8
8
  "engines": {
@@ -24,6 +24,6 @@
24
24
  "@types/react-dom": "^19.3.0",
25
25
  "@vitejs/plugin-react": "^6.1.1",
26
26
  "typescript": "^7.0.2",
27
- "vite": "^8.3.0"
27
+ "vite": "^8.3.1"
28
28
  }
29
29
  }