clearotron 0.3.2-beta.10 → 0.3.2-beta.12

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 (74) hide show
  1. package/.env.example +1 -1
  2. package/CONTRIBUTING.md +1 -0
  3. package/INSTALL.md +49 -46
  4. package/README.md +8 -7
  5. package/bin/clearotron.mjs +14 -0
  6. package/bin/connect.mjs +68 -3
  7. package/bin/key.mjs +6 -1
  8. package/bin/onboard.mjs +51 -14
  9. package/bin/passphrase.mjs +4 -2
  10. package/bin/start.mjs +43 -9
  11. package/build-info.json +2 -2
  12. package/docs/architecture/05-config-governance.md +1 -1
  13. package/driver/CHANGELOG.md +416 -388
  14. package/driver/contract-vocabulary.mjs +5 -5
  15. package/driver/engine/mcp/recording-server.mjs +21 -1
  16. package/driver/engine/mcp/supplemental.mjs +22 -5
  17. package/driver/knockout-next-step.mjs +72 -0
  18. package/driver/named-band.mjs +1 -1
  19. package/driver/package.json +1 -1
  20. package/driver/phase0.mjs +16 -7
  21. package/driver/pipeline-knockout.mjs +26 -0
  22. package/driver/pipeline.mjs +48 -1
  23. package/driver/portal-local-auth.mjs +14 -4
  24. package/driver/portal-report.mjs +21 -2
  25. package/driver/portal-service.mjs +20 -11
  26. package/driver/publish/attr.mjs +36 -0
  27. package/driver/publish/index.mjs +10 -10
  28. package/driver/publish/parse.mjs +1 -1
  29. package/driver/publish/render-knockout.mjs +14 -9
  30. package/driver/publish/render.mjs +9 -9
  31. package/driver/publish/xlsx.mjs +7 -1
  32. package/driver/queue-watch-verdict.mjs +1 -1
  33. package/driver/register-availability.mjs +1 -1
  34. package/driver/register-plan.mjs +81 -11
  35. package/driver/result-noun-fields.mjs +3 -1
  36. package/driver/stages-knockout.mjs +1 -1
  37. package/driver/suite-census.json +104 -38
  38. package/driver/unit-inventory.mjs +7 -7
  39. package/driver/verify-knockout.mjs +0 -27
  40. package/mcp-server/CHANGELOG.md +27 -19
  41. package/mcp-server/lib/audit-view.mjs +4 -4
  42. package/mcp-server/lib/trace.mjs +1 -1
  43. package/mcp-server/package.json +1 -1
  44. package/package.json +2 -2
  45. package/portal-ui/dist/assets/{index-CtvwLCti.css → index-7Lq-dXDV.css} +12 -9
  46. package/portal-ui/dist/assets/{index-DXSRxPV_.js → index-w8GFZftk.js} +110 -69
  47. package/portal-ui/dist/index.html +2 -2
  48. package/portal-ui/package.json +1 -1
  49. package/providers/jx/README.md +2 -2
  50. package/providers/jx-subclass/README.md +1 -1
  51. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  52. package/providers/oauth-mcp-bridge/package.json +1 -1
  53. package/scripts/README.md +5 -10
  54. package/scripts/ask-ai-render-check.mjs +26 -1
  55. package/scripts/citation-line-check.mjs +2 -2
  56. package/scripts/dead-names.mjs +17 -16
  57. package/scripts/e2e.mjs +8 -8
  58. package/scripts/env-audit.mjs +6 -0
  59. package/scripts/generated-files-are-current.mjs +35 -4
  60. package/scripts/live-surface-check.mjs +17 -17
  61. package/scripts/release-code-scanning-check.mjs +114 -0
  62. package/scripts/release-visible-check.mjs +117 -0
  63. package/scripts/repo-writes.mjs +42 -0
  64. package/scripts/report-sections-render-check.mjs +245 -0
  65. package/scripts/report-theme-render-check.mjs +31 -3
  66. package/scripts/settings-render-check.mjs +10 -7
  67. package/scripts/test-run.mjs +63 -4
  68. package/shared/client-door.mjs +20 -0
  69. package/shared/invocation.mjs +2 -2
  70. package/shared/parent-watch.mjs +33 -0
  71. package/shared/register-selection.mjs +4 -1
  72. package/shared/root-doc-commands.mjs +10 -4
  73. package/shared/running-start.mjs +14 -3
  74. package/shared/scope.mjs +24 -5
package/.env.example CHANGED
@@ -578,7 +578,7 @@ TRADEMARK_MCP_KEY_SOCKET=
578
578
  # names, read by code that ships.
579
579
 
580
580
  # WHO the chat notice is addressed to: a JSON object of requester email or handle → number, e.g.
581
- # {"lisa@tenant.example":"+41...","jordan":"+41..."}. Before this, the notice went to a map keyed by
581
+ # {"robin@tenant.example":"+41...","jordan":"+41..."}. Before this, the notice went to a map keyed by
582
582
  # AGENT id, and every user of a deployment shares one agent — so the operator was notified about work
583
583
  # other people ordered and the requester was told nothing.
584
584
  #
package/CONTRIBUTING.md CHANGED
@@ -17,6 +17,7 @@ runs on `node:sqlite`. Node 20 will fail in ways that look like your change.
17
17
 
18
18
  ```bash
19
19
  npm install
20
+ npm run build:ui # the browser bundle is not committed; the demo needs it
20
21
  npm test # the fast tier, offline, no network, no keys
21
22
  ```
22
23
 
package/INSTALL.md CHANGED
@@ -16,7 +16,7 @@ after §5 is needed to produce a report.
16
16
  | | Sections | For |
17
17
  |---|---|---|
18
18
  | **Installing** | §1 Prerequisites · §2 Install · §3 Configuration · §3a Free register route · §3b Paying through a cloud account · §4 Config store · §5 Run a clearance | Anyone |
19
- | **Operating** | §6 `npx clearotron start` · §7 The MCP server · §8 Access control and isolation | Running it as a service for other people |
19
+ | **Operating** | §6 Start the product · §7 The MCP server · §8 Access control and isolation | Running it as a service for other people |
20
20
  | **Reference** | §9 What an integrator supplies · §10 Licence | — |
21
21
 
22
22
  Before §3, decide which register you are using — it is the first real choice and the fastest route is
@@ -113,7 +113,7 @@ run is [mcp-server/CONNECT.md](mcp-server/CONNECT.md), and why something is the
113
113
  pay for Claude through your own Google Cloud, Microsoft Azure or Amazon Bedrock account (§3b).
114
114
 
115
115
  **Installed is not usable.** `npx clearotron install` proves the engine can complete a turn before it
116
- writes anything, and `npx clearotron doctor --probe-engine` re-proves it on a configured box. Both
116
+ writes anything, and `clearotron doctor --probe-engine` re-proves it on a configured box. Both
117
117
  spend one cheap turn; plain `doctor` spends nothing.
118
118
  - **A register credential**, and **`PERPLEXITY_API_KEY`**. Both are required for a real run and both
119
119
  fail closed at preflight — before a stage has spent, never at the grid after. The one exception is a
@@ -202,8 +202,8 @@ tarball as a dependency.** Not an unpacked archive; there is no step here that u
202
202
  mkdir ~/app && cd ~/app
203
203
  npm init -y
204
204
  npm install /path/to/clearotron-<version>.tgz
205
- npx clearotron doctor # reads; writes nothing, calls nobody
206
205
  npx clearotron install # the wizard
206
+ clearotron doctor # reads; writes nothing, calls nobody
207
207
  ```
208
208
 
209
209
  `npx` works from `~/app` because npm links a **dependency's** `bin` into the consuming project — the
@@ -264,7 +264,10 @@ there. On the repository route "the project" is the checkout; on the packaged ro
264
264
  you ran `npm init` in — and on that route `clearotron` IS a dependency, so `npx` finds it in
265
265
  `node_modules/.bin` exactly as the paragraph above describes.
266
266
 
267
- **`clearotron install` (§3) puts the short form on your `PATH`** and you can stop typing `npx` after it:
267
+ **`clearotron install` (§3) puts the short form on your `PATH`**, and every command after it in this
268
+ document uses that form. Use it rather than a fresh `npx clearotron`: `npx` resolves whichever version npm
269
+ picks at that moment — the stable, unless you name a channel — and does not hand over to the copy the
270
+ install placed. You can stop typing `npx` after it:
268
271
  it writes a small shim to `~/.local/bin/clearotron` pointing at this checkout. That directory needs no
269
272
  root — `npm link` would want npm's global prefix, which on a default install is `/usr` and refuses
270
273
  without it. Most login profiles add `~/.local/bin` to `PATH` only if it already existed when the shell
@@ -413,7 +416,7 @@ If you would rather not write this file by hand, `npx clearotron install` asks f
413
416
  these paths under it, checks what it can check without billing you before it writes anything, and never
414
417
  touches a path you did not name — an engine turn, an EUIPO token exchange, and an offered (not assumed)
415
418
  Perplexity ping. A paid register key is deliberately not probed: a call against a metered subscription is
416
- a charge you did not ask for, so that credential is written on your word. `npx clearotron doctor`
419
+ a charge you did not ask for, so that credential is written on your word. `clearotron doctor`
417
420
  reports what is set and what is missing, and writes nothing.
418
421
 
419
422
  Only the integrator-set knobs are shown — copy what you need:
@@ -597,9 +600,9 @@ points on it.
597
600
  | What it is | What the product calls it | Where it lives | What creates it |
598
601
  |---|---|---|---|
599
602
  | A group of people and the companies they clear for — a firm, a brand team, one company on a hosted install | **organisation** (`tenant` in `grants.json`) | a key under `tenants` in `grants.json`, with its `name` | setup creates the first; after that, a key you add to `grants.json` |
600
- | A company you do clearances for | **company** (`account` in `grants.json` and on the wire; the CLI calls it **brand owner**) | a bundle in the company store, keyed by an account key, and listed under exactly one organisation | the portal's `+ New company`, or `npx clearotron brandowner add <key>` |
601
- | One engagement under that company — its classes, jurisdictions, platforms | **project** | inside that company's bundle | `npx clearotron project add` |
602
- | Someone who may see some of it | **person** | `grants.json`: their access under each organisation's `users`, their two switches under `people` | the portal's People page, or `npx clearotron grant add` |
603
+ | A company you do clearances for | **company** (`account` in `grants.json` and on the wire; the CLI calls it **brand owner**) | a bundle in the company store, keyed by an account key, and listed under exactly one organisation | the portal's `+ New company`, or `clearotron brandowner add <key>` |
604
+ | One engagement under that company — its classes, jurisdictions, platforms | **project** | inside that company's bundle | `clearotron project add` |
605
+ | Someone who may see some of it | **person** | `grants.json`: their access under each organisation's `users`, their two switches under `people` | the portal's People page, or `clearotron grant add` |
603
606
 
604
607
  Nesting, in one line: **an organisation contains companies; a company contains projects; a person is
605
608
  given access to points on that tree — the whole install, an organisation, or one company — and sees
@@ -613,7 +616,7 @@ Three consequences worth stating, because each has surprised someone:
613
616
  stops them; **Manage** adds people, adds companies and changes settings. Viewing is not a permission:
614
617
  access is viewing. A person with no entry under `people` sees what their access covers and starts
615
618
  nothing.
616
- - **A key grants no reach of its own.** `npx clearotron key issue` mints the identity a person's assistant
619
+ - **A key grants no reach of its own.** `clearotron key issue` mints the identity a person's assistant
617
620
  presents; what that identity may see and do is decided by the guest list at the moment of each call.
618
621
  Enrol first, issue second — a key for someone with no access reaches nothing, and is not an error
619
622
  anywhere.
@@ -710,7 +713,7 @@ Copy or author the companies, context packs and project overlays you want; assum
710
713
  ## 5. Run a headless clearance report
711
714
 
712
715
  1. Make sure `CLEAROTRON_AI` names the engine you want (`anthropic-agent` or `openai-agent`), its CLI is
713
- authenticated, and your active register provider's credential is set. `npx clearotron doctor
716
+ authenticated, and your active register provider's credential is set. `clearotron doctor
714
717
  --probe-engine` answers all three, and the engine half of it by actually running a turn — an
715
718
  executable on `PATH` that is signed out passes every other check and fails at the first stage.
716
719
 
@@ -760,7 +763,7 @@ Copy or author the companies, context packs and project overlays you want; assum
760
763
  3. Run the pipeline:
761
764
 
762
765
  ```
763
- npx clearotron run --job job.json
766
+ clearotron run --job job.json
764
767
  ```
765
768
 
766
769
  **The example above is sized in minutes; a real clearance is sized in hours.**
@@ -834,7 +837,7 @@ A run stopped by a **provider rate limit** or parked for **automatic recovery**
834
837
  that is the systemd units in `driver/systemd/`. Everywhere else, run the watcher yourself:
835
838
 
836
839
  ```
837
- npx clearotron run-queue --watch
840
+ clearotron run-queue --watch
838
841
  ```
839
842
 
840
843
  That polls every 90 seconds for queued jobs and for parked runs whose window has elapsed, and it resumes
@@ -847,18 +850,18 @@ stops it, and anything still parked waits for the next time you start it.
847
850
  Everything above runs the engine from a job file. This is the product: a portal you sign in to, order a
848
851
  clearance from, and read the report in.
849
852
 
850
- **Plain `npx clearotron start` runs in the foreground and stops when this terminal closes** — Ctrl-C, a
853
+ **Plain `clearotron start` runs in the foreground and stops when this terminal closes** — Ctrl-C, a
851
854
  dropped SSH session, a shut laptop lid ending the session: the portal goes with it. That is the right
852
855
  shape for trying things. To keep it running when the window is gone:
853
856
 
854
857
  ```
855
- npx clearotron start --background # the same product, as user services that survive the terminal
856
- npx clearotron status # is it up, and on which ports
857
- npx clearotron stop # stop it and give the box back — plain `start` works again
858
+ clearotron start --background # the same product, as user services that survive the terminal
859
+ clearotron status # is it up, and on which ports
860
+ clearotron stop # stop it and give the box back — plain `start` works again
858
861
  ```
859
862
 
860
- `--background` never touches the assistant connector: `npx clearotron connect` opens that door
861
- and `npx clearotron disconnect` closes it, separately and on purpose.
863
+ `--background` never touches the assistant connector: `clearotron connect` opens that door
864
+ and `clearotron disconnect` closes it, separately and on purpose.
862
865
 
863
866
  **If anything else on this host already runs this product, set its ports first.** The portal (18802)
864
867
  and the engine door (18790) are **fixed defaults shared by every checkout on a machine**, so a second
@@ -866,14 +869,14 @@ instance collides with the first and `start` refuses rather than quietly moving.
866
869
  before the first start:
867
870
 
868
871
  ```
869
- PORTAL_SERVICE_PORT=18820 TRADEMARK_MCP_HTTP_PORT=18821 npx clearotron start
872
+ PORTAL_SERVICE_PORT=18820 TRADEMARK_MCP_HTTP_PORT=18821 clearotron start
870
873
  ```
871
874
 
872
875
  A test instance beside a live one needs more than two ports — §8, *Two instances on one machine*, is the
873
876
  whole boundary. On a host running nothing else, ignore this and carry on:
874
877
 
875
878
  ```
876
- npx clearotron start
879
+ clearotron start
877
880
  ```
878
881
 
879
882
  One command. It starts the portal and the engine door the portal's Start button calls, waits until both
@@ -881,14 +884,14 @@ answer, and prints one address to open. `Ctrl-C` stops both. The second run asks
881
884
 
882
885
  **This is not `npx clearotron demo`, and the two are not interchangeable.**
883
886
 
884
- | | `npx clearotron demo` | `npx clearotron start` |
887
+ | | `npx clearotron demo` | `clearotron start` |
885
888
  |---|---|---|
886
889
  | what it is | a finished report, replayed | the running product |
887
890
  | credentials | none | whatever a real run needs (§3) |
888
891
  | model calls | none | yes, once you order a clearance |
889
892
  | what you can do | read | sign in, configure, order, read |
890
893
 
891
- Use the demo to see what this system produces. Use `npx clearotron start` to run it.
894
+ Use the demo to see what this system produces. Use `clearotron start` to run it.
892
895
 
893
896
  ### What the first start does, once
894
897
 
@@ -898,7 +901,7 @@ Use the demo to see what this system produces. Use `npx clearotron start` to run
898
901
  `clearotron doctor` and a connected assistant read the same saved searches as the portal.
899
902
  - Creates `~/trademark/` — `pool/`, `workspace/`, `queue/`, `outbox/`, `locks/`, an empty grants file,
900
903
  and a small git repository for saved searches. Same base directory `npx clearotron install` uses, so whichever
901
- of the two you ran first, the other finds the same install. Move it with `npx clearotron start --base <dir>`.
904
+ of the two you ran first, the other finds the same install. Move it with `clearotron start --base <dir>`.
902
905
  That does not move anything the env file already names: the saved-search lines above, and the data
903
906
  directories `npx clearotron install` wrote, keep pointing at the old place until you edit them.
904
907
  - Mints your sign-in passphrase and **prints it once**. Write it down. It is stored as a scrypt digest in
@@ -911,7 +914,7 @@ Use the demo to see what this system produces. Use `npx clearotron start` to run
911
914
  You sign in as `<your-username>@localhost` unless you say otherwise:
912
915
 
913
916
  ```
914
- npx clearotron start --user you@example.com
917
+ clearotron start --user you@example.com
915
918
  ```
916
919
 
917
920
  Setup does not ask for the address: it writes the local-account form to `.env` and shows it once, in
@@ -923,7 +926,7 @@ nobody else at its domain; enrolling anyone else is that same file, exactly as o
923
926
 
924
927
  **No authentication is switched off to make this work, and none can be.** Both doors prove who the
925
928
  caller is — the portal by passphrase and a signed session cookie, the engine door by a mandatory
926
- access key that `npx clearotron start` mints in memory at every start and never writes down. The key is scoped to
929
+ access key that `clearotron start` mints in memory at every start and never writes down. The key is scoped to
927
930
  two verbs and capped to the companies this install knows about. The `*_AUTH_DISABLED` switches
928
931
  elsewhere in this repository are for something else and are written into the child environment as `0`.
929
932
 
@@ -941,7 +944,7 @@ no proxy in front is the one shape to avoid — the portal refuses to start with
941
944
 
942
945
  ### It drains its own queue
943
946
 
944
- `npx clearotron start` supervises a worker alongside the portal, so ordering a clearance from the portal
947
+ `clearotron start` supervises a worker alongside the portal, so ordering a clearance from the portal
945
948
  runs it.
946
949
 
947
950
  The consent did not move and did not weaken. The portal prices the run, quotes how long it takes, and
@@ -953,8 +956,8 @@ If you want the old separation — order here, drain deliberately over there —
953
956
  run the queue yourself:
954
957
 
955
958
  ```
956
- npx clearotron start --no-worker
957
- npx clearotron run-queue --watch
959
+ clearotron start --no-worker
960
+ clearotron run-queue --watch
958
961
  ```
959
962
 
960
963
  A queued job whose worker is not running says so on the portal rather than sitting at "Waiting to start".
@@ -982,7 +985,7 @@ systemctl --user daemon-reload && systemctl --user enable --now clearotron-deplo
982
985
 
983
986
  A busy box simply skips: the service exits clean when a run is live and the next firing tries again.
984
987
 
985
- **`npx clearotron doctor` reports how far behind this install is**, beside everything else it checks. It
988
+ **`clearotron doctor` reports how far behind this install is**, beside everything else it checks. It
986
989
  does not fetch, so the count is against your last fetch — which it says. An install that is not a git
987
990
  checkout, or a branch with no upstream, says that instead of reporting a number it cannot compute.
988
991
 
@@ -997,7 +1000,7 @@ yours to do, and now yours to know about.
997
1000
 
998
1001
  The portal is on 18802 and the engine door on 18790, both loopback, and both are **fixed defaults for
999
1002
  every checkout on the host** rather than per-install values. They move separately: the portal with
1000
- `npx clearotron start --port <n>` or `PORTAL_SERVICE_PORT=<n>`, the engine door with
1003
+ `clearotron start --port <n>` or `PORTAL_SERVICE_PORT=<n>`, the engine door with
1001
1004
  `TRADEMARK_MCP_HTTP_PORT=<n>`. A port already in use produces a sentence saying which port and which
1002
1005
  variable to change, before anything starts — it will not quietly pick another, because whatever sits in
1003
1006
  front of it is still addressed to the old one.
@@ -1073,17 +1076,17 @@ their own integration work.
1073
1076
 
1074
1077
  1. **Clearotron is installed on the machine the assistant runs on** — Claude's desktop app, Claude Code,
1075
1078
  Codex, the ChatGPT desktop app, an agent that runs commands. The assistant spawns the server over
1076
- stdio. No address, no key, no network, no ingress. `npx clearotron start` prints the one line to
1077
- paste, and `npx clearotron connect --where here` hands it over per assistant.
1079
+ stdio. No address, no key, no network, no ingress. `clearotron start` prints the one line to
1080
+ paste, and `clearotron connect --where here` hands it over per assistant.
1078
1081
  2. **Clearotron is running somewhere else** — a server, a cloud machine, anywhere the assistant is
1079
1082
  not. These need **a publicly reachable HTTPS address**, plus a key made for the person connecting.
1080
1083
  Always. Claude (app, web, Cowork and mobile), ChatGPT on the web, Perplexity and other agents only
1081
- ever connect this way; Claude Code and Codex can connect either way. `npx clearotron connect
1084
+ ever connect this way; Claude Code and Codex can connect either way. `clearotron connect
1082
1085
  --where elsewhere` makes the key and prints the steps for the assistant you name.
1083
1086
 
1084
1087
  Without `--where`, `connect` asks when both answers are possible, and `--client <name>` on its own keeps
1085
1088
  the answer that assistant had before: a key for Claude, the local line for Claude Code and Codex.
1086
- `npx clearotron disconnect` takes the same `--where`.
1089
+ `clearotron disconnect` takes the same `--where`.
1087
1090
 
1088
1091
  **A loopback address is never an answer for shape 2, and that is not about your network.** A remote MCP
1089
1092
  connector is reached **from the vendor's cloud**, never from the reader's device. Anthropic's own help
@@ -1091,7 +1094,7 @@ centre states it: *"Claude connects to your remote MCP server from Anthropic's c
1091
1094
  rather than from your local device. This is true across every Claude client, including claude.ai, Claude
1092
1095
  Desktop, Cowork, and the mobile apps … Your MCP server must be reachable over the public internet."*
1093
1096
  So an install behind `ssh -L` or an editor's port forward serves shape 1 perfectly and cannot serve
1094
- shape 2 at all, however the reader reaches the portal. `npx clearotron connect` says so plainly rather
1097
+ shape 2 at all, however the reader reaches the portal. `clearotron connect` says so plainly rather
1095
1098
  than printing an address that will be rejected.
1096
1099
 
1097
1100
  **The address is set once, at install.** `npx clearotron install` asks for it — *"the address companies'
@@ -1100,7 +1103,7 @@ Use-your-AI page, a report's Ask-your-AI control and `doctor` all read. Leave it
1100
1103
  install: every one of those surfaces then shows its honest empty state, which is correct for a machine
1101
1104
  with no public address. Changing it later is editing that one setting and restarting.
1102
1105
 
1103
- Whatever you provision, `npx clearotron doctor` will tell you whether the published address actually
1106
+ Whatever you provision, `clearotron doctor` will tell you whether the published address actually
1104
1107
  answers — being set is not the same as being reachable, and the page and the report both render from
1105
1108
  the value being present alone. An address that does not answer is reported as a problem with the
1106
1109
  reason, never as configured.
@@ -1168,7 +1171,7 @@ curl -sS -o /dev/null -w '%{http_code}\n' https://<your-host>/mcp
1168
1171
 
1169
1172
  Anything that comes back — including a 401 or a 405 — proves the route reaches the connector; a
1170
1173
  connection error or a login page does not. Put the address that answered into the installer's question
1171
- (or `CLEAROTRON_CLIENT_MCP_URL`), then `npx clearotron doctor` and read the connector line.
1174
+ (or `CLEAROTRON_CLIENT_MCP_URL`), then `clearotron doctor` and read the connector line.
1172
1175
 
1173
1176
  Smoke-test either face offline:
1174
1177
 
@@ -1191,28 +1194,28 @@ bills Claude through an API key or a cloud account, never a Claude subscription,
1191
1194
  require.
1192
1195
 
1193
1196
  **The guest list.** `CLEAROTRON_ACCESS_FILE` turns account scoping on for **every face at once** — the
1194
- portal, the MCP read face, and the client connector. `npx clearotron start` (§6) writes one into its
1197
+ portal, the MCP read face, and the client connector. `clearotron start` (§6) writes one into its
1195
1198
  state directory the first time it runs: you, with access to everything, your organisation if setup was
1196
1199
  told its name, and nobody else yet.
1197
1200
  [examples/grants.example.json](examples/grants.example.json) is a runnable guest list over the demo
1198
1201
  companies.
1199
1202
 
1200
- **Giving someone access.** `npx clearotron grant add` writes the same file the portal's People page
1203
+ **Giving someone access.** `clearotron grant add` writes the same file the portal's People page
1201
1204
  writes:
1202
1205
 
1203
1206
  ```
1204
- npx clearotron grant add <email> --tenant <organisation> --accounts <key,key|*> [--run] [--manage]
1207
+ clearotron grant add <email> --tenant <organisation> --accounts <key,key|*> [--run] [--manage]
1205
1208
  ```
1206
1209
 
1207
1210
  `--accounts '*'` is the whole organisation, including companies filed under it later. `--run` lets the
1208
1211
  person start and stop clearances; `--manage` lets them add people and companies and change settings.
1209
1212
  With neither, they can see what their access covers and start nothing.
1210
1213
 
1211
- **Keys for people.** `npx clearotron grant` enrols someone; it decides what they may see and issues
1214
+ **Keys for people.** `clearotron grant` enrols someone; it decides what they may see and issues
1212
1215
  nothing. The key their assistant actually presents comes from a different verb:
1213
1216
 
1214
1217
  ```
1215
- npx clearotron key issue <email> [--accounts a,b] [--ttl-days 90]
1218
+ clearotron key issue <email> [--accounts a,b] [--ttl-days 90]
1216
1219
  ```
1217
1220
 
1218
1221
  It is printed once, on stdout, and stored nowhere — possession is the credential. Enrol first: the key
@@ -1227,7 +1230,7 @@ node mcp-server/mint-token.mjs --scope ops --sub <name> --ttl-days 30 --verbs st
1227
1230
  ```
1228
1231
 
1229
1232
  Said plainly rather than dressed as a verb, because the distinction costs real time: **`npx clearotron
1230
- key issue` mints ACCOUNT keys only**, and `npx clearotron grant` mints nothing at all. This page
1233
+ key issue` mints ACCOUNT keys only**, and `clearotron grant` mints nothing at all. This page
1231
1234
  previously sent readers to `grant` for an ops token, which is why the sentence is now this long.
1232
1235
 
1233
1236
  **When you have to re-mint one.** Not on the ordinary path any more, and this paragraph used to say
@@ -1307,7 +1310,7 @@ https://claude.com/api/mcp/auth_callback
1307
1310
  https://chatgpt.com/connector_platform_oauth_redirect
1308
1311
  ```
1309
1312
 
1310
- `npx clearotron doctor --probe-connector` asks your own door whether each of those can register, with a
1313
+ `clearotron doctor --probe-connector` asks your own door whether each of those can register, with a
1311
1314
  localhost control first so a broken endpoint is never reported as a policy refusal. It is opt-in because
1312
1315
  each successful attempt creates a throwaway OAuth client on your account — every other `doctor` check
1313
1316
  writes nothing.
@@ -1318,7 +1321,7 @@ writes nothing.
1318
1321
  > whether the application was recreated before you change anything else.
1319
1322
 
1320
1323
  **Seeing what a company sees.** There is no "view as" screen. The documented route is a **client-scoped
1321
- connector key**: issue one for that company with `npx clearotron key issue`, point an assistant at the
1324
+ connector key**: issue one for that company with `clearotron key issue`, point an assistant at the
1322
1325
  client connector with it, and you get exactly that company's scope. Changing `PORTAL_LOCAL_USER` to
1323
1326
  impersonate someone is not the answer — local sign-in is one user by design, and the service refuses to
1324
1327
  start if the credential does not match the configured address, so you lose your own access and take the
@@ -1391,7 +1394,7 @@ the orchestrator, the provider adapters and the documentation.
1391
1394
 
1392
1395
  AGPL §13 matters if you run it as a network service: anyone who interacts with your instance is owed
1393
1396
  the source of **that** instance. Every network face answers with its own running commit — the portal's
1394
- About page, the MCP server's `server_info`, and `npx clearotron start --license`.
1397
+ About page, the MCP server's `server_info`, and `clearotron start --license`.
1395
1398
 
1396
1399
  **Everything §1 told you to bring is outside it.** Read this before you count the licence as your
1397
1400
  answer on any of them:
package/README.md CHANGED
@@ -43,8 +43,9 @@ npx clearotron install
43
43
 
44
44
  Node 22.13 or newer, on macOS or Linux. It needs no root: it puts the program under `~/.local`, with the
45
45
  `clearotron` command in `~/.local/bin`, then asks one question at a time and checks each credential
46
- before it saves it. With `~/.local/bin` on your `PATH`, every command below works in the short form;
47
- otherwise use the full path `install` prints at the end. **On Windows the demo above runs natively; a
46
+ before it saves it. Every command below uses the `clearotron` it installed — the same channel you
47
+ installed from — so if `~/.local/bin` is not on your `PATH` yet, type the full path,
48
+ `~/.local/bin/clearotron`, instead. **On Windows the demo above runs natively; a
48
49
  real clearance needs WSL2.** Native Windows clearances are planned for a later release. Until then the
49
50
  engine does not run on native Windows: it spawns each stage with POSIX path and process semantics, so
50
51
  a clearance is refused there before it starts.
@@ -71,13 +72,13 @@ With it installed, check what it found before it does anything. `doctor` only re
71
72
  calls nobody, and names whatever is still missing:
72
73
 
73
74
  ```bash
74
- npx clearotron doctor
75
+ clearotron doctor
75
76
  ```
76
77
 
77
78
  Then start the product and open the portal address it prints:
78
79
 
79
80
  ```bash
80
- npx clearotron start
81
+ clearotron start
81
82
  ```
82
83
 
83
84
  That is the portal everyone at your company uses. Ordering a clearance is the same screen — describe it
@@ -99,7 +100,7 @@ rights-holders behind them by jurisdiction:
99
100
  Then run your own: order it in the portal, or hand the engine a job file:
100
101
 
101
102
  ```bash
102
- npx clearotron run --job my-job.json
103
+ clearotron run --job my-job.json
103
104
  ```
104
105
 
105
106
  ## How it fits together
@@ -158,8 +159,8 @@ npx clearotron demo # replays finished clearances into a local portal
158
159
 
159
160
  From a clone the commands are `npx clearotron …`, run from that directory.
160
161
 
161
- `npm test` is the whole verification story for someone with no credentials, and it is the first thing
162
- [CONTRIBUTING.md](CONTRIBUTING.md) asks of a contributor.
162
+ `npm test` is the offline suite and the first thing [CONTRIBUTING.md](CONTRIBUTING.md) asks of a
163
+ contributor; `npm run test:full`, also free, is the merge gate.
163
164
 
164
165
  ## Project documents
165
166
 
@@ -24,6 +24,7 @@ import { constants as SIG } from "node:os";
24
24
  import { isEntrypoint } from "../shared/is-entrypoint.mjs";
25
25
  import { nodeFloorVerdict, nodeFloorRefusal } from "../shared/node-floor.mjs"; // — one floor, read from package.json
26
26
  import { invocationPrefix } from "../shared/invocation.mjs"; // — print a command the reader can type
27
+ import { watchParent } from "../shared/parent-watch.mjs";
27
28
 
28
29
  export const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
29
30
 
@@ -229,6 +230,19 @@ const [verb, ...rest] = process.argv.slice(2);
229
230
  };
230
231
  process.on(sig, forward);
231
232
  }
233
+
234
+ // ── AND THE DEMO STOPS WHEN WHATEVER STARTED IT IS GONE ─────────────────────────────────────────────
235
+ //
236
+ // The forwarding above covers a signal sent to THIS pid. Through npx it is not this pid a reader holds:
237
+ // npm → `sh -c` → this launcher, and a TERM to npm dies at that `sh` (shared/parent-watch.mjs). This
238
+ // launcher is then reparented, and the demo runs on holding its three ports while the reader believes
239
+ // it stopped. So the demo takes its parent's death as a TERM, and stops everything it started.
240
+ //
241
+ // THE DEMO ONLY. A foreground `start` or a `run` outliving the shell that started it can be what the
242
+ // reader meant: `nohup clearotron start &` on a server they are about to log out of, or an hours-long
243
+ // clearance they will not babysit. The demo is a replay nobody leaves running on purpose, and its own
244
+ // banner already says it lasts only as long as the command that started it.
245
+ if (verb === "demo") watchParent(() => { try { child.kill("SIGTERM"); } catch { /* already gone */ } });
232
246
  }
233
247
 
234
248
  // RESOLVE BOTH SIDES THROUGH SYMLINKS. `npm install` puts a symlink at node_modules/.bin/clearotron, so
package/bin/connect.mjs CHANGED
@@ -40,7 +40,7 @@ import { createInterface } from "node:readline/promises";
40
40
  import { requireInteractive } from "../shared/invocation.mjs"; // — a prompt with nobody to answer it
41
41
  import { stdin, stdout } from "node:process";
42
42
  import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from "node:fs";
43
- import { join, dirname } from "node:path";
43
+ import { join, dirname, resolve } from "node:path";
44
44
  import { homedir, userInfo } from "node:os";
45
45
  import { fileURLToPath } from "node:url";
46
46
  import { execFileSync } from "node:child_process";
@@ -48,7 +48,11 @@ import { createServer } from "node:net";
48
48
  import { CONNECT_CLIENTS, WHERE_FLAG, clientById, leadRouteFor, plainStep, whatItNeeds } from "../shared/connect-clients.mjs";
49
49
  import { stdioConnectFor, STDIO_SHAPES } from "../shared/stdio-connect.mjs";
50
50
  import { isWsl, wslTarget } from "../shared/wsl.mjs"; // — and which distribution a row should start the server in
51
- import { defaultDenylistPath, clientDoorAddress, clientDoorPort, clientDoorState, enablePlan, applyEnablePlan, describeChange, recordConnectKey, CLIENT_DOOR_UNIT } from "../shared/client-door.mjs";
51
+ import { defaultDenylistPath, clientDoorAddress, clientDoorPort, clientDoorState, enablePlan, applyEnablePlan, describeChange, recordConnectKey, CLIENT_DOOR_UNIT, demoTokenSecretPath, keyPurposeLine, demoConnectCommand } from "../shared/client-door.mjs";
52
+ import { readRunning } from "../shared/running-start.mjs";
53
+ import { readLocalCredential, passphraseResetCommand, INSTALL_CREDENTIAL_FILE } from "../driver/portal-local-auth.mjs";
54
+ import { invocationPrefix } from "../shared/invocation.mjs";
55
+ import { BRAND } from "../shared/brand.mjs";
52
56
  import { mintToken, tokenId, resolvePerson, loadGrants } from "../shared/scope.mjs";
53
57
  import { envFrom } from "../shared/env-aliases.mjs";
54
58
  import { atomicWrite } from "../driver/progress.mjs";
@@ -654,6 +658,59 @@ function printSteps(offer, key) {
654
658
  }
655
659
  }
656
660
 
661
+ /**
662
+ * ── A RUNNING DEMO, NAMED BY ITS FOLDER (owner, 2026-09-19) ─────────────────────────────────────────
663
+ *
664
+ * The demo is how a newcomer meets the product, and the next thing they ask an assistant is to connect.
665
+ * The route that works is the demo's own client door with an account key, and reaching it took a key
666
+ * issued by hand and an address read out of the startup log. This does both: it finds the demo that is
667
+ * running from that folder, mints a key with the demo's own secret for the demo's one user, and prints
668
+ * the address and the key, and nothing else is changed. No unit is written and no setting touched: the
669
+ * demo's door is already open, and everything of the demo goes when it stops.
670
+ *
671
+ * The address route's steps are not printed. They are written for a door behind a sign-in service,
672
+ * reached over the web, and a demo's door is on this machine and takes the key instead.
673
+ */
674
+ async function connectDemo({ base, dryRun, clientName = null, running = readRunning() } = {}) {
675
+ const at = resolve(base);
676
+ const prefix = invocationPrefix();
677
+ say("");
678
+ if (clientName) { say(` ${clientName}`); say(""); }
679
+ const demo = running.find((r) => r.demo && resolve(String(r.base ?? "")) === at);
680
+ if (!demo) {
681
+ say(` Not available here — no demo is running from ${at}.`);
682
+ say(` What would change it: start it with \`${prefix}clearotron demo --base ${/\s/.test(at) ? `"${at}"` : at}\`, then run this again.`);
683
+ return 1;
684
+ }
685
+ if (!demo.ports?.client) {
686
+ say(` Not available here — the demo running from ${at} has no client door open.`);
687
+ say(" What would change it: stop the demo and start it again; its output says why the door did not start.");
688
+ return 1;
689
+ }
690
+ const address = `http://${!demo.host || demo.host === "0.0.0.0" ? "127.0.0.1" : demo.host}:${demo.ports.client}/mcp`;
691
+ const credentialPath = join(at, INSTALL_CREDENTIAL_FILE);
692
+ let email = "demo@localhost";
693
+ try { email = readLocalCredential(credentialPath)?.email || email; } catch { /* the demo's own user stands */ }
694
+ if (dryRun) {
695
+ say(" (dry run — nothing was changed)");
696
+ say(` Address: ${address}`);
697
+ return 0;
698
+ }
699
+ let secret = "";
700
+ try { secret = readFileSync(demoTokenSecretPath(at), "utf8").trim(); } catch { /* said below */ }
701
+ if (!secret) {
702
+ say(` Not available here — the demo running from ${at} keeps no key secret this command can read.`);
703
+ say(" What would change it: run this as the user who started the demo.");
704
+ return 1;
705
+ }
706
+ const key = withSecret(secret, () => mintToken({ scope: "account", sub: email, ttlSec: 90 * 24 * 3600 }));
707
+ say(` ${keyPurposeLine({ email, brand: BRAND.name, reset: passphraseResetCommand({ prefix, credentialPath }) })}`);
708
+ say("");
709
+ say(` Address: ${address}`);
710
+ say(` Key: ${key}`);
711
+ return 0;
712
+ }
713
+
657
714
  async function main() {
658
715
  const argv = process.argv.slice(2);
659
716
  // `--help` IS ANSWERED, not rejected. Running this bare drops the reader into a question, so "run it
@@ -674,6 +731,7 @@ async function main() {
674
731
  say(" internet, with a key made for you now");
675
732
  say(" --list the assistants this build knows");
676
733
  say(" --dry-run say what would change, change nothing");
734
+ say(" --base <dir> a running demo's folder: mint a key for its client door, and print both");
677
735
  say(" --allow-checkout-move");
678
736
  say(" write this checkout's path even though services are running from");
679
737
  say(" another one. Every unit's ExecStart follows that value, so the ones");
@@ -681,7 +739,7 @@ async function main() {
681
739
  say("");
682
740
  return 0;
683
741
  }
684
- const known = new Set(["--client", "--where", "--list", "--dry-run", "--allow-checkout-move", "--help", "-h"]);
742
+ const known = new Set(["--client", "--where", "--list", "--dry-run", "--base", "--allow-checkout-move", "--help", "-h"]);
685
743
  const unknown = argv.filter((a) => a.startsWith("--") && !known.has(a));
686
744
  if (unknown.length) {
687
745
  console.error(`connect: unrecognised flag(s): ${unknown.join(", ")}`);
@@ -689,6 +747,13 @@ async function main() {
689
747
  process.exit(2);
690
748
  }
691
749
  const dryRun = argv.includes("--dry-run");
750
+ const b = argv.indexOf("--base");
751
+ if (b >= 0) {
752
+ const base = argv[b + 1];
753
+ if (!base || base.startsWith("--")) { console.error("connect: --base needs the demo's folder."); process.exit(2); }
754
+ const c = argv.indexOf("--client");
755
+ return await connectDemo({ base, dryRun, clientName: c >= 0 ? clientById(argv[c + 1])?.name ?? null : null });
756
+ }
692
757
  const w = argv.indexOf("--where");
693
758
  if (w >= 0 && !Object.hasOwn(WHERE_FLAG, argv[w + 1] ?? "")) {
694
759
  console.error(`connect: --where takes one of: ${Object.keys(WHERE_FLAG).join(", ")}`);
package/bin/key.mjs CHANGED
@@ -28,9 +28,10 @@
28
28
  // question would be two answers, and the wrong one would be the one nobody read.
29
29
  import "../shared/env-local.mjs"; // side effect: apply the install's .env — FIRST, before anything reads process.env
30
30
  import { invocationPrefix } from "../shared/invocation.mjs";
31
+ import { BRAND } from "../shared/brand.mjs";
31
32
  import { existsSync, readFileSync } from "node:fs";
32
33
  import { defaultGrantsPath, installPaths } from "./start.mjs";
33
- import { demoTokenSecretPath } from "../shared/client-door.mjs";
34
+ import { demoTokenSecretPath, keyPurposeLine } from "../shared/client-door.mjs";
34
35
  import { resolvePerson } from "../shared/scope.mjs"; // the door's own reading of the guest list, so one answer serves both
35
36
  import { mintFromOptions } from "../mcp-server/mint-token.mjs";
36
37
 
@@ -96,6 +97,10 @@ try {
96
97
  die(e.message);
97
98
  }
98
99
 
100
+ // WHAT THIS KEY IS FOR, FIRST, in the owner's words (2026-09-19): a person who found this verb before the
101
+ // passphrase minted a key and could not sign in to the portal with it. Standard error, as every line here
102
+ // is, so the token stays alone on standard output.
103
+ console.error(keyPurposeLine({ email, brand: BRAND.name, reset: `${p}clearotron passphrase --reset` }));
99
104
  for (const line of minted.notes) console.error(line);
100
105
 
101
106
  // — found in review. A KEY FOR AN IDENTITY ON NO LIST IS A KEY THAT 403s. This command