clearotron 0.3.2-beta.10 → 0.3.2-beta.11

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 (43) hide show
  1. package/CONTRIBUTING.md +1 -0
  2. package/INSTALL.md +49 -46
  3. package/README.md +8 -7
  4. package/bin/onboard.mjs +44 -13
  5. package/bin/start.mjs +31 -6
  6. package/build-info.json +2 -2
  7. package/docs/architecture/05-config-governance.md +1 -1
  8. package/driver/CHANGELOG.md +400 -388
  9. package/driver/engine/mcp/recording-server.mjs +20 -0
  10. package/driver/package.json +1 -1
  11. package/driver/phase0.mjs +16 -7
  12. package/driver/pipeline.mjs +48 -1
  13. package/driver/portal-local-auth.mjs +14 -4
  14. package/driver/portal-service.mjs +12 -3
  15. package/driver/publish/attr.mjs +36 -0
  16. package/driver/publish/index.mjs +10 -10
  17. package/driver/publish/parse.mjs +1 -1
  18. package/driver/publish/render.mjs +9 -9
  19. package/driver/publish/xlsx.mjs +7 -1
  20. package/driver/queue-watch-verdict.mjs +1 -1
  21. package/driver/suite-census.json +59 -23
  22. package/driver/unit-inventory.mjs +7 -7
  23. package/mcp-server/CHANGELOG.md +23 -19
  24. package/mcp-server/lib/audit-view.mjs +4 -4
  25. package/mcp-server/lib/trace.mjs +1 -1
  26. package/mcp-server/package.json +1 -1
  27. package/package.json +2 -2
  28. package/portal-ui/package.json +1 -1
  29. package/providers/jx/README.md +2 -2
  30. package/providers/jx-subclass/README.md +1 -1
  31. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  32. package/providers/oauth-mcp-bridge/package.json +1 -1
  33. package/scripts/README.md +5 -10
  34. package/scripts/citation-line-check.mjs +2 -2
  35. package/scripts/e2e.mjs +8 -8
  36. package/scripts/generated-files-are-current.mjs +35 -4
  37. package/scripts/live-surface-check.mjs +17 -17
  38. package/scripts/release-code-scanning-check.mjs +114 -0
  39. package/scripts/release-visible-check.mjs +117 -0
  40. package/shared/invocation.mjs +2 -2
  41. package/shared/register-selection.mjs +4 -1
  42. package/shared/root-doc-commands.mjs +10 -4
  43. package/shared/scope.mjs +2 -2
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
 
package/bin/onboard.mjs CHANGED
@@ -1030,6 +1030,31 @@ export function installSizeLine(eng) {
1030
1030
  return `${Number.isFinite(eng?.installMB) ? `It takes about ${eng.installMB} MB. ` : ""}To remove it, delete that folder.`;
1031
1031
  }
1032
1032
 
1033
+ /**
1034
+ * Whether a login shell will put `dir` on the PATH: some profile this user's shells read names it.
1035
+ *
1036
+ * Setup promised "open a new login shell, and plain `clearotron start` works". That is Ubuntu's stock
1037
+ * `~/.profile` for an ordinary user, which adds `~/.local/bin` once it exists. It is not root's in a fresh
1038
+ * container, measured on ubuntu:24.04 by an outside install, and it is not macOS's default zsh. So the
1039
+ * promise is made only when a profile names the directory. A profile that adds it some other way reads as
1040
+ * "no", which costs a reader one line of advice and never sends them to a shell that will not find it.
1041
+ */
1042
+ const LOGIN_PROFILES = [".profile", ".bash_profile", ".bash_login", ".bashrc", ".zprofile", ".zshenv", ".zshrc", ".zlogin"];
1043
+ export function aLoginShellAdds(dir, { home = homedir(), read = (p) => readFileSync(p, "utf8") } = {}) {
1044
+ const d = String(dir ?? "");
1045
+ if (!d) return false;
1046
+ const rel = home && d.startsWith(`${home}/`) ? d.slice(home.length + 1) : null;
1047
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1048
+ const names = new RegExp(`(^|[^\\w.-])(${esc(d)}${rel ? `|(\\$HOME|\\$\\{HOME\\}|~)/${esc(rel)}` : ""})(?![\\w.-])`);
1049
+ for (const f of LOGIN_PROFILES) {
1050
+ let text;
1051
+ try { text = read(join(home, f)); } catch { continue; }
1052
+ // A COMMENTED-OUT LINE ADDS NOTHING, and a disabled PATH line is an ordinary state for a profile.
1053
+ if (String(text ?? "").split("\n").some((l) => !/^\s*#/.test(l) && names.test(l))) return true;
1054
+ }
1055
+ return false;
1056
+ }
1057
+
1033
1058
  /**
1034
1059
  * The engine menu, built from the driver's registry so the wizard cannot offer an adapter that does not
1035
1060
  * exist — or hide one that does. Same guarantee the register-provider list has.
@@ -1997,7 +2022,7 @@ export async function runCheck() {
1997
2022
  info(`this install's own is ${form.shim} — the commands below name it in full, so they reach THIS install`);
1998
2023
  } else if (form.form === "shim-path") {
1999
2024
  warn(`${form.dir} is not on this shell's PATH, so the bare \`clearotron\` will not resolve here`);
2000
- info(`add it with: export PATH="${form.dir}:$PATH" — or open a new login shell`);
2025
+ info(`add it with: export PATH="${form.dir}:$PATH"${aLoginShellAdds(form.dir) ? " — or open a new login shell" : ""}`);
2001
2026
  } else if (form.form === "npx-pinned") {
2002
2027
  info(`this is running from npm's npx cache, so the commands below name this version through npx (\`${form.prefix.trim()}\`), `
2003
2028
  + `which works from any directory and after npm cleans its cache; \`${invoke("install")}\` puts \`clearotron\` on your PATH`);
@@ -4029,20 +4054,25 @@ try {
4029
4054
  say("\n Register provider");
4030
4055
  say(" This decides which register gets searched, and which vendor gets billed. There is no default in");
4031
4056
  say(" the engine — it refuses to guess.");
4032
- const provider = await choose("Which register?", PROVIDERS.map((p) => ({ label: `${p.label} — ${p.cost}`, spec: p })), 0);
4057
+ // THE LAST ROW IS NOT A REGISTER, as on the AI question, and with the same words. Leaving the register
4058
+ // unset was reachable only by picking a vendor and skipping its credential, so a reader who meant "none
4059
+ // yet" had to pretend to choose one. Choosing it takes the branch a skipped credential reaches below.
4060
+ const provider = await choose("Which register?", [...PROVIDERS.map((p) => ({ label: `${p.label} — ${p.cost}`, spec: p })), { label: "None for now", spec: null }], 0);
4033
4061
  const spec = provider.spec;
4034
- say(`\n ${spec.label}: covers ${spec.covers}`);
4035
- for (const w of spec.warnings ?? []) warn(w);
4036
- if (spec.signup && !spec.credentials.every((k) => present(candidate[k]))) {
4037
- say("\n How to get a credential:");
4038
- for (const l of spec.signup) say(` ${l}`);
4039
- say("");
4062
+ if (spec) {
4063
+ say(`\n ${spec.label}: covers ${spec.covers}`);
4064
+ for (const w of spec.warnings ?? []) warn(w);
4065
+ if (spec.signup && !spec.credentials.every((k) => present(candidate[k]))) {
4066
+ say("\n How to get a credential:");
4067
+ for (const l of spec.signup) say(` ${l}`);
4068
+ say("");
4069
+ }
4040
4070
  }
4041
4071
  // ── — INSTALL MAY FINISH WITH NO REGISTER. Ruling, 2026-08-26 ──────────────────────
4042
4072
  //
4043
4073
  // Every row of PROVIDERS declares required credentials, and this prompt had no way out, so a reader
4044
- // with no vendor account could not reach the closing screen at all — the menu offers no "none" row,
4045
- // which means they arrive here without ever having chosen to supply a key.
4074
+ // with no vendor account could not reach the closing screen at all. The menu's "None for now" row is
4075
+ // the chosen way here; a skipped credential is the other.
4046
4076
  //
4047
4077
  // A REGISTER IS ALL-OR-NOTHING, WHICH IS WHY ONE SKIP ABANDONS THE WHOLE SELECTION. Half a credential
4048
4078
  // pair is not a working register; writing CLEAROTRON_DATABASE beside it would name an adapter that
@@ -4053,14 +4083,14 @@ try {
4053
4083
  // is single-valued with no default, a run already refuses by name when it is unset
4054
4084
  // (driver.config.mjs), and the Installation settings page already says a register is needed.
4055
4085
  // One register per install, any one of them sufficient, none a precondition for another.
4056
- let registerSelected = true;
4086
+ let registerSelected = spec != null;
4057
4087
  // What THIS step collected, so abandoning the selection can take it back. Measured: a register with
4058
4088
  // two required credentials — `euipo`, `free-tier` — let a reader supply the first and skip the second,
4059
4089
  // and the first was written to the .env with no CLEAROTRON_DATABASE beside it. `candidate` is
4060
4090
  // serialised wholesale at the write step, so anything left in it ships. A credential for a register
4061
4091
  // nobody selected is a secret persisted for a decision that was reversed.
4062
4092
  const collectedHere = [];
4063
- for (const k of spec.credentials) {
4093
+ for (const k of spec?.credentials ?? []) {
4064
4094
  if (present(candidate[k])) { ok(`${k} already adopted from your environment`); continue; }
4065
4095
  const v = await askValue(`${k}:`, { secret: true, skippable: true,
4066
4096
  skipped: `${k} not set, so ${spec.id} cannot be configured — this install will have NO register selected.` });
@@ -4581,7 +4611,8 @@ try {
4581
4611
  say(` \`clearotron\` is now installed at ${form.shim}, and ${form.dir} is not on this shell's`);
4582
4612
  say(" PATH yet — most login profiles add it only if it existed when the shell started. So:\n");
4583
4613
  say(` export PATH="${form.dir}:$PATH" # this terminal, now`);
4584
- say(" …or open a new login shell, and plain `clearotron start` works from anywhere.\n");
4614
+ if (aLoginShellAdds(form.dir)) say(" …or open a new login shell, and plain `clearotron start` works from anywhere.\n");
4615
+ else say("");
4585
4616
  } else if (form.form === "in-place") {
4586
4617
  say(" There is no `clearotron` on your PATH, so the commands above name the directory to run them");
4587
4618
  say(" from. Re-run the install to put the verb on your PATH.\n");
package/bin/start.mjs CHANGED
@@ -2322,7 +2322,7 @@ if (isMain) {
2322
2322
  // Captured HERE, immediately before the spawn, rather than beside the sentence that reads it: the
2323
2323
  // check has to sit on the other side of the thing that mints, and the only way to keep that true is
2324
2324
  // for it to be adjacent to the spawn where a reader can see why.
2325
- const { credentialPathFor: credentialPathBeforeStart, newPassphrase, passphraseResetCommand, demoCredentialToReplace, laterStartLines, readLocalCredential } = await import("../driver/portal-local-auth.mjs");
2325
+ const { credentialPathFor: credentialPathBeforeStart, newPassphrase, passphraseResetCommand, passphraseWithheldLines, demoCredentialToReplace, laterStartLines, readLocalCredential } = await import("../driver/portal-local-auth.mjs");
2326
2326
  // ASKED ABOUT THE FILE THE PORTAL WILL ACTUALLY USE, not the shared default. `credentialPathFor`
2327
2327
  // reads `PORTAL_LOCAL_CREDENTIAL`, and a demo sets it to a file inside its own base — but this call
2328
2328
  // was made against THIS process's environment, which never carries it. So on any box that already had
@@ -2486,16 +2486,25 @@ if (isMain) {
2486
2486
  // line is what a reader copies when they lose the passphrase — run as printed, the bare form resolved
2487
2487
  // the shared default and exited 1 saying no credential exists.
2488
2488
  const reset = passphraseResetCommand({ prefix: invocationPrefix(), credentialPath: envs.portal.PORTAL_LOCAL_CREDENTIAL ?? null });
2489
+ // ── ONLY A TERMINAL IS HANDED THE PASSPHRASE ──────────────────────────────────────────────────────
2490
+ //
2491
+ // The first-start box went to standard output whatever standard output was. Under a service manager,
2492
+ // `nohup` or `> start.log` that is a file, so the one value this product cannot read back landed in a
2493
+ // log, a moment after the portal's own line said it was being withheld for exactly that reason.
2494
+ // Measured on a published beta by an outside install. A redirected run gets the portal's own words
2495
+ // instead, from the composer the portal uses, and the way to mint one on a terminal.
2489
2496
  if (mintedPassphrase) {
2490
2497
  const rule = "─".repeat(66);
2491
2498
  say(` ┌${rule}┐`);
2492
2499
  say(` │ Open ${envs.url}`);
2493
2500
  say(` │ Sign in as ${user}`);
2494
- say(` │ Passphrase ${mintedPassphrase}`);
2495
- say(` │`);
2496
- say(` │ WRITE THE PASSPHRASE DOWN NOW. It is stored only as a digest, so`);
2497
- say(` │ nothing — not this product, not this terminal — can read it back.`);
2498
- say(` │ Lost it? ${reset}`);
2501
+ if (process.stdout.isTTY === true) {
2502
+ say(` │ Passphrase ${mintedPassphrase}`);
2503
+ say(` │`);
2504
+ say(` │ WRITE THE PASSPHRASE DOWN NOW. It is stored only as a digest, so`);
2505
+ say(` │ nothing — not this product, not this terminal — can read it back.`);
2506
+ say(` │ Lost it? ${reset}`);
2507
+ } else for (const line of passphraseWithheldLines({ stream: "stdout", resetCommand: reset }).flatMap((l) => (/^\s/.test(l) ? [l] : fitTo(64, l)))) say(` │ ${line}`);
2499
2508
  // THE HINT BELONGS IN THE BOX TOO, and this was the reader the whole sentence was written for. The
2500
2509
  // frame exists because a first-time reader skips the log wall and acts on it — so the one address
2501
2510
  // they copy was the one address with nothing beside it saying what to do when the page that opens
@@ -2604,6 +2613,22 @@ export function demoBaseIsTheReaders({ baseGiven = false, ownBase = false, base
2604
2613
  return !(ownBase && base && demoDefault && base === demoDefault); // handed over, and it IS the default
2605
2614
  }
2606
2615
 
2616
+ /**
2617
+ * A sentence broken at spaces into lines of at most `width` characters, for the framed box. The withheld
2618
+ * lines come from the portal's composer, written for a log line of any length; inside the frame they are
2619
+ * broken to the width of the lines they stand in for. A word longer than `width` keeps its own line. PURE.
2620
+ */
2621
+ export function fitTo(width, text) {
2622
+ const out = [];
2623
+ let line = "";
2624
+ for (const word of String(text ?? "").split(/\s+/).filter(Boolean)) {
2625
+ if (line && line.length + 1 + word.length > width) { out.push(line); line = word; }
2626
+ else line = line ? `${line} ${word}` : word;
2627
+ }
2628
+ if (line) out.push(line);
2629
+ return out;
2630
+ }
2631
+
2607
2632
  export function demoBaseResetTarget({ baseGiven = false, base = "", demoDefault = "" } = {}) {
2608
2633
  if (baseGiven) return null; // the reader chose it, so it is not the demo's to clear
2609
2634
  if (!base || !demoDefault) return null; // nothing to compare: say no
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "1c5ecbdefb9e379d1a7407aabfd3efc9384ffa44",
3
- "version": "0.3.2-beta.10"
2
+ "commit": "aa58558b3b09fbf5b1c83010291d340167d438d8",
3
+ "version": "0.3.2-beta.11"
4
4
  }
@@ -542,7 +542,7 @@ change. The product's known mirror classes:
542
542
  | CF Access team / AUD / domain gates | edge (dashboard) + each fronted unit's inline env | staff or company lockout, service by service |
543
543
  | `TRADEMARK_MCP_TOKEN_SECRET` | EnvironmentFile + any integrator-hosted artifacts-MCP env block | run-bound `user` tokens fail verification |
544
544
  | Register-provider credentials | EnvironmentFile + integrator plugin config (when both consume the provider) | one consumer silently unauthenticated |
545
- | Profiles/skills store paths | EnvironmentFile + service unit files (+ integrator MCP env) | roster-mismatch class (the PR #14 incident) |
545
+ | Profiles/skills store paths | EnvironmentFile + service unit files (+ integrator MCP env) | roster-mismatch class: a door serving the bundled demo roster instead of the real one |
546
546
  | Pool root | code default + units + web-server file root (+ integrator MCP env) | reports publish where nothing serves |
547
547
  | Public hostnames | EnvironmentFile (rendered links) + router + edge | dead links / dead routes |
548
548