clearotron 0.3.0-beta.10 → 0.3.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -13
- package/INSTALL.md +6 -110
- package/README.md +21 -31
- package/bin/connect.mjs +26 -71
- package/bin/disconnect.mjs +11 -16
- package/bin/example.mjs +2 -14
- package/bin/key.mjs +5 -23
- package/bin/onboard.mjs +44 -191
- package/bin/start.mjs +33 -197
- package/bin/status.mjs +3 -23
- package/bin/stop.mjs +8 -23
- package/bin/update.mjs +6 -38
- package/build-info.json +2 -2
- package/demo/full-country-search/run/status.json +1 -1
- package/demo/global-preliminary-search/run/status.json +1 -1
- package/demo/knockout-search/run/email-body.md +1 -1
- package/demo/knockout-search/run/status.json +2 -2
- package/demo/multi-country-focus-search/run/status.json +1 -1
- package/docs/architecture/04-configuration-reference.md +1 -2
- package/driver/CHANGELOG.md +8 -107
- package/driver/compose-read.mjs +2 -21
- package/driver/contract-e3-backlog.mjs +1 -1
- package/driver/demo-container.mjs +2 -65
- package/driver/driver.config.mjs +0 -5
- package/driver/engine/anthropic-agent.mjs +17 -34
- package/driver/engine/jx-turn.mjs +1 -4
- package/driver/engine/openai-agent.mjs +2 -60
- package/driver/enqueue-schema.mjs +1 -3
- package/driver/gateway.mjs +15 -88
- package/driver/package.json +1 -1
- package/driver/portal-local-auth.mjs +3 -35
- package/driver/portal-service.mjs +22 -91
- package/driver/portal-upstream.mjs +1 -20
- package/driver/profile-service.mjs +3 -31
- package/driver/publish/index.mjs +9 -12
- package/driver/publish/knockout.mjs +2 -4
- package/driver/publish/office-record-links.mjs +21 -56
- package/driver/recipe-service.mjs +5 -14
- package/driver/record-origins.mjs +0 -14
- package/driver/search-policy.mjs +2 -7
- package/driver/suite-census.json +46 -154
- package/driver/systemd/render-units.mjs +13 -7
- package/mcp-server/CHANGELOG.md +2 -36
- package/mcp-server/CONNECT.md +2 -3
- package/mcp-server/lib/driver.mjs +0 -2
- package/mcp-server/lib/knockout.mjs +2 -2
- package/mcp-server/lib/options.mjs +5 -20
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-CwPAS0we.js → index-CcFjgM78.js} +383 -525
- package/portal-ui/dist/assets/{index-Cv-E_agg.css → index-CsCuPshD.css} +86 -199
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/clarivate/src/core.js +0 -5
- package/providers/oauth-mcp-bridge/CHANGELOG.md +0 -32
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/e2e-unread-terminals.mjs +1 -1
- package/scripts/e2e.mjs +20 -197
- package/scripts/engine-probe.mjs +2 -2
- package/scripts/env-classify.mjs +0 -3
- package/scripts/revisit-render-check.mjs +4 -22
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/access-audience.mjs +6 -7
- package/shared/brand.mjs +3 -7
- package/shared/client-door.mjs +0 -39
- package/shared/connect-clients.mjs +321 -290
- package/shared/env-file-merge.mjs +0 -24
- package/shared/invocation.mjs +2 -49
- package/shared/names-in-force.mjs +0 -2
- package/shared/stdio-connect.mjs +14 -95
- package/shared/store-in-repo.mjs +2 -50
- package/shared/verb-shim.mjs +1 -10
- package/shared/permanent-install.mjs +0 -237
- package/shared/running-start.mjs +0 -73
- package/shared/wsl.mjs +0 -23
package/.env.example
CHANGED
|
@@ -399,15 +399,6 @@ CLEAROTRON_DEMO=
|
|
|
399
399
|
# effect: deployment
|
|
400
400
|
# CLEAROTRON_INVOKED_AS=
|
|
401
401
|
|
|
402
|
-
# Marks an install that has already moved itself out of npm's npx cache. `npx clearotron install` first
|
|
403
|
-
# installs the same version under ~/.local and then runs that copy's install, with this set to 1, so the
|
|
404
|
-
# copy does not try to move itself a second time. Unset, an install running from npx's cache moves.
|
|
405
|
-
# Set by bin/onboard.mjs on the one process it starts, never by an operator, so the row is left
|
|
406
|
-
# commented. It is listed for the reason CLEAROTRON_DEMO is: the catalogue is about what an operator can
|
|
407
|
-
# FIND. Read by bin/onboard.mjs.
|
|
408
|
-
# effect: deployment
|
|
409
|
-
# CLEAROTRON_RELOCATED=
|
|
410
|
-
|
|
411
402
|
# How many client what-ifs the runner drains at once (default 1). A what-if re-runs ONE stage of a run
|
|
412
403
|
# that already exists, in a sandbox, at a client's request — the owner opened it to clients on
|
|
413
404
|
# 2026-08-27 and ruled SPEND controls out with it ("ignore the call spend"). This is not one.
|
|
@@ -561,9 +552,8 @@ CLEAROTRON_DEMO_PROFILES=
|
|
|
561
552
|
# Your organisation's name. `npm run setup` asks for it (and, on a local install, not for a sign-in
|
|
562
553
|
# address) and writes it here, quoted, so a name holding a `#` survives. The first `clearotron start` files it as the first
|
|
563
554
|
# organisation in the grants file (CLEAROTRON_ACCESS_FILE) when that file holds none; from then on the
|
|
564
|
-
# grants file holds the name
|
|
565
|
-
# organisation
|
|
566
|
-
#
|
|
567
|
-
# it. Read by bin/start.mjs and shared/brand.mjs.
|
|
555
|
+
# grants file holds the name, renaming is an edit there, and this is not read. Unset or empty, no
|
|
556
|
+
# organisation is invented. `clearotron start --organisation <name>` supplies it for one start, and a
|
|
557
|
+
# demo never reads it. Read by bin/start.mjs.
|
|
568
558
|
# effect: deployment
|
|
569
559
|
CLEAROTRON_ORGANISATION_NAME=
|
package/INSTALL.md
CHANGED
|
@@ -64,15 +64,6 @@ run is [mcp-server/CONNECT.md](mcp-server/CONNECT.md), and why something is the
|
|
|
64
64
|
npx clearotron install
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
Run from `npx`, the install first installs Clearotron under `~/.local`, as `npm install -g --prefix
|
|
68
|
-
~/.local` would, and finishes from there. That way the `clearotron` command and your assistant's connection
|
|
69
|
-
do not point into npm's temporary cache, which npm replaces on an update and deletes when it cleans up.
|
|
70
|
-
Later, the `update` command updates that copy in place.
|
|
71
|
-
|
|
72
|
-
On WSL, a program on the Windows side can hold a port that WSL reports as free, and the browser reaches
|
|
73
|
-
it first. VS Code's Remote-SSH port forwarding is the common case. If the page that opens is not this
|
|
74
|
-
install's sign-in, run the same command again with `--port 28802`, or any free number.
|
|
75
|
-
|
|
76
67
|
A *hosted* deployment needs Linux for one further thing, the systemd outbox trigger —
|
|
77
68
|
[driver/systemd/README.md](driver/systemd/README.md).
|
|
78
69
|
- **A reasoning CLI on your `PATH`, signed in.** This is the prerequisite people miss. Every stage runs
|
|
@@ -295,71 +286,6 @@ keeps working throughout.
|
|
|
295
286
|
provider, so it is safe to run on any machine that has the repository. "Safe anywhere" is about spend,
|
|
296
287
|
not about where it will run — on a packaged tree it refuses, as above.
|
|
297
288
|
|
|
298
|
-
## 2a. Removing it
|
|
299
|
-
|
|
300
|
-
An install is not one directory, so removing it is not one command. Everything it writes is listed here.
|
|
301
|
-
Read the list before you delete anything: one of these directories holds your reports.
|
|
302
|
-
|
|
303
|
-
**Stop it first.** If the product is running in a terminal, Ctrl-C in that terminal. If you installed the
|
|
304
|
-
background services, `clearotron stop` stops them and removes their unit files. `clearotron status` says
|
|
305
|
-
which of the two you have.
|
|
306
|
-
|
|
307
|
-
**The program.** Installed globally, `npm rm -g clearotron` removes it. Installed by `clearotron install`
|
|
308
|
-
(which is also what a demo run from `npx` leaves behind), it lives in two places under `~/.local`:
|
|
309
|
-
|
|
310
|
-
```
|
|
311
|
-
~/.local/lib/node_modules/clearotron
|
|
312
|
-
~/.local/bin/clearotron
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
**The settings, and the keys.** `~/.config/clearotron/.env` holds this install's signing secrets, so
|
|
316
|
-
deleting it invalidates every key it ever issued. Beside it: `~/.config/clearotron/token-denylist`, the
|
|
317
|
-
list of keys you revoked, and `~/.config/clearotron/running`, which records a product running in a
|
|
318
|
-
terminal and is rewritten on every start.
|
|
319
|
-
|
|
320
|
-
**Your work.** The install keeps it all under one directory, `~/trademark` by default — wherever
|
|
321
|
-
`CLEAROTRON_REPORTS_DIR`'s parent points if you moved it:
|
|
322
|
-
|
|
323
|
-
```
|
|
324
|
-
~/trademark/pool THE REPORTS. Every clearance this install delivered.
|
|
325
|
-
~/trademark/workspace the working files of each run, including unfinished ones
|
|
326
|
-
~/trademark/queue work ordered and not yet run
|
|
327
|
-
~/trademark/outbox what the product prepared to send
|
|
328
|
-
~/trademark/locks which runs are in flight
|
|
329
|
-
~/trademark/config your companies, their projects and your saved searches
|
|
330
|
-
~/trademark/config/recipes the saved searches themselves
|
|
331
|
-
~/trademark/config/profiles the company profiles
|
|
332
|
-
~/trademark/grants.json who may use this install
|
|
333
|
-
~/trademark/portal-audit.log what the portal was asked to do
|
|
334
|
-
~/trademark/portal-local-credential.json the sign-in this install minted for you
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
**Keep the pool if you want the reports.** They are finished documents and nothing else reads them: move
|
|
338
|
-
`~/trademark/pool` somewhere of your own and delete the rest. Deleting the pool deletes the clearances.
|
|
339
|
-
|
|
340
|
-
**The demo, if you ran one.** `~/trademark-demo` holds the demo's own copy of everything above, including
|
|
341
|
-
its own copy of the program under `~/trademark-demo/program`. It is one directory to remove and it shares
|
|
342
|
-
nothing with an install.
|
|
343
|
-
|
|
344
|
-
**The background services, if you installed them.** `clearotron stop` removes the four unit files; if you
|
|
345
|
-
would rather see them, they are in `~/.config/systemd/user`:
|
|
346
|
-
|
|
347
|
-
```
|
|
348
|
-
clearotron-portal.service
|
|
349
|
-
clearotron-mcp-face.service
|
|
350
|
-
clearotron-worker.service
|
|
351
|
-
clearotron-client-mcp.service
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
**And `~/.env`, which only a background install writes.** A service inherits nothing from the terminal
|
|
355
|
-
that installed it, so `clearotron start --background` writes everything those services need into `~/.env`,
|
|
356
|
-
mode 600 — your register credential, your research key and the engine's settings among it. It is not the
|
|
357
|
-
same file as `~/.config/clearotron/.env`, which configures the product when you run it yourself. Delete
|
|
358
|
-
both, or you leave a file of credentials in your home for services that no longer exist.
|
|
359
|
-
|
|
360
|
-
Nothing else on the machine is ours: no system-wide files, no services outside your own user, and nothing
|
|
361
|
-
in `/etc`, `/usr` or `/var`.
|
|
362
|
-
|
|
363
289
|
## 3. Configuration (environment)
|
|
364
290
|
|
|
365
291
|
Runtime configuration is by environment variable, and there are two ways to supply them.
|
|
@@ -565,25 +491,6 @@ matches, the neutral Generic default applies.
|
|
|
565
491
|
- **Your real customers live outside the repo.** Point `CLEAROTRON_CUSTOMERS_DIR` at your own private
|
|
566
492
|
config store and the engine loads *those* accounts instead. **Same engine, different config path** —
|
|
567
493
|
the code carries no customer identities.
|
|
568
|
-
|
|
569
|
-
Two things go with it, and both are refusals rather than preferences:
|
|
570
|
-
|
|
571
|
-
- **`PROFILE_REPO_ROOT` moves too.** The customer directory has to sit inside the repository that
|
|
572
|
-
variable names, because editing a profile is a commit. Point one somewhere new and leave the other
|
|
573
|
-
behind and the portal and the profile service both refuse to start, naming both variables.
|
|
574
|
-
- **That repository needs a `user.name` and a `user.email` of its own.** Saves are committed under
|
|
575
|
-
the name of whoever asked for them, but git also records who *made* the commit, and it will not
|
|
576
|
-
commit at all without one. A service account usually has no global git identity, so a store created
|
|
577
|
-
by hand needs its own:
|
|
578
|
-
|
|
579
|
-
```bash
|
|
580
|
-
git init -b main /srv/clearotron-store
|
|
581
|
-
git -C /srv/clearotron-store config user.name "clearotron local install"
|
|
582
|
-
git -C /srv/clearotron-store config user.email "clearotron@example.com"
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
`clearotron start` does this for the store it creates. A store you make yourself does not get it,
|
|
586
|
-
and the symptom is the first save failing at a commit rather than anything about profiles.
|
|
587
494
|
- **Run data is external too.** Published reports, audits, and per-run state go to the archive pool at
|
|
588
495
|
`CLEAROTRON_REPORTS_DIR`. Nothing customer-specific is committed to the repository.
|
|
589
496
|
|
|
@@ -823,13 +730,9 @@ Use the demo to see what this system produces. Use `npx clearotron start` to run
|
|
|
823
730
|
|
|
824
731
|
- Generates `PORTAL_SECRET` and `TRADEMARK_MCP_TOKEN_SECRET` and **appends** them to `~/.config/clearotron/.env` at
|
|
825
732
|
mode 600. Append, never rewrite: that file also holds the credentials `npx clearotron install` collected.
|
|
826
|
-
It also records where saved searches are kept, `CLEAROTRON_RECIPES_DIR` and `RECIPE_REPO_ROOT`, so
|
|
827
|
-
`clearotron doctor` and a connected assistant read the same saved searches as the portal.
|
|
828
733
|
- Creates `~/trademark/` — `pool/`, `workspace/`, `queue/`, `outbox/`, `locks/`, an empty grants file,
|
|
829
734
|
and a small git repository for saved searches. Same base directory `npx clearotron install` uses, so whichever
|
|
830
735
|
of the two you ran first, the other finds the same install. Move it with `npx clearotron start --base <dir>`.
|
|
831
|
-
That does not move anything the env file already names: the saved-search lines above, and the data
|
|
832
|
-
directories `npx clearotron install` wrote, keep pointing at the old place until you edit them.
|
|
833
736
|
- Mints your sign-in passphrase and **prints it once**. Write it down. It is stored as a scrypt digest in
|
|
834
737
|
`~/trademark/portal-local-credential.json`, nothing can read it back, and no later start reprints it. To
|
|
835
738
|
get a new one, run `clearotron passphrase --reset`. An install that has been signing in with
|
|
@@ -1000,19 +903,12 @@ their own integration work.
|
|
|
1000
903
|
**Two shapes, and the line between them is what your assistant can do — not where it runs.** Ruling
|
|
1001
904
|
2026-09-03, on the vendor's own documented behaviour:
|
|
1002
905
|
|
|
1003
|
-
1. **
|
|
1004
|
-
Codex
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
2. **
|
|
1008
|
-
|
|
1009
|
-
Always. Claude (app, web, Cowork and mobile), ChatGPT on the web, Perplexity and other agents only
|
|
1010
|
-
ever connect this way; Claude Code and Codex can connect either way. `npx clearotron connect
|
|
1011
|
-
--where elsewhere` makes the key and prints the steps for the assistant you name.
|
|
1012
|
-
|
|
1013
|
-
Without `--where`, `connect` asks when both answers are possible, and `--client <name>` on its own keeps
|
|
1014
|
-
the answer that assistant had before: a key for Claude, the local line for Claude Code and Codex.
|
|
1015
|
-
`npx clearotron disconnect` takes the same `--where`.
|
|
906
|
+
1. **An assistant that can launch a local process** — Claude Code, Claude Desktop's local config,
|
|
907
|
+
Codex CLI, an agent that runs commands. It spawns the server over stdio. No address, no key, no
|
|
908
|
+
network, no ingress. `npx clearotron start` prints the one line to paste, and `npx clearotron
|
|
909
|
+
connect` hands it over per assistant.
|
|
910
|
+
2. **Everything else, wherever it appears to run** — Cowork, ChatGPT, claude.ai, Perplexity, the mobile
|
|
911
|
+
apps. These need **a publicly reachable HTTPS address**, plus a key. Always.
|
|
1016
912
|
|
|
1017
913
|
**A loopback address is never an answer for shape 2, and that is not about your network.** A remote MCP
|
|
1018
914
|
connector is reached **from the vendor's cloud**, never from the reader's device. Anthropic's own help
|
package/README.md
CHANGED
|
@@ -19,62 +19,51 @@ no gateway, no platform, and nothing about your matters reaches us.
|
|
|
19
19
|
|
|
20
20
|
## Install
|
|
21
21
|
|
|
22
|
-
**See it work first, with nothing
|
|
22
|
+
**See it work first, with nothing checked out.**
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
npx clearotron demo
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
That fetches the published package — it will ask once before downloading — then replays finished
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
That fetches the published package — it will ask once before downloading — then replays a finished
|
|
29
|
+
clearance into a local pool and opens the report in your browser. No account, no credentials, no network
|
|
30
|
+
calls to us.
|
|
31
31
|
|
|
32
32
|
**Then install it.**
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
|
|
35
|
+
npm install -g clearotron
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
Node 22.13 or newer, on macOS or Linux.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
real clearance needs WSL2.** Native Windows clearances are planned for a later release. Until then the
|
|
43
|
-
engine does not run on native Windows: it resolves the reasoning CLI the POSIX way, and a clearance
|
|
44
|
-
started there refuses at preflight.
|
|
45
|
-
|
|
46
|
-
`npm install -g clearotron` also works where npm's global directory is yours to write. On a Linux Node
|
|
47
|
-
from the distribution or NodeSource that directory is `/usr`, owned by root, and npm refuses with
|
|
48
|
-
`EACCES`. Do not answer that with `sudo`; install under your home instead:
|
|
49
|
-
`npm install -g clearotron --prefix ~/.local`.
|
|
38
|
+
Node 22.13 or newer, on macOS or Linux. That puts `clearotron` on your `PATH`; every command below
|
|
39
|
+
works in that short form. **On Windows the demo above runs natively; a real clearance needs WSL2.**
|
|
40
|
+
Native Windows clearances are planned for a later release. Until then the engine does not run on native
|
|
41
|
+
Windows: it resolves the reasoning CLI the POSIX way, and a clearance started there refuses at preflight.
|
|
50
42
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
43
|
+
That command gives you the **stable** release — the one that has run a real clearance end to end before it
|
|
44
|
+
was published. If you want the newest code instead, a beta is published whenever there is something worth
|
|
45
|
+
testing — deliberately, days apart, not on every merge:
|
|
54
46
|
|
|
55
47
|
```bash
|
|
56
|
-
|
|
48
|
+
npm install -g clearotron@beta
|
|
57
49
|
```
|
|
58
50
|
|
|
59
51
|
What each channel promises, and when a stable is cut: [docs/RELEASES.md](docs/RELEASES.md). If you are not
|
|
60
52
|
sure, the first command is the one you want.
|
|
61
53
|
|
|
62
|
-
Removing it later: **[INSTALL.md §2a](INSTALL.md#2a-removing-it)** lists every path an install writes, and
|
|
63
|
-
says which one holds your reports so you can keep them deliberately.
|
|
64
|
-
|
|
65
54
|
## Quick start
|
|
66
55
|
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
Check the install before it does anything. `doctor` only reads — it writes nothing, calls nobody, and
|
|
57
|
+
names whatever is still missing:
|
|
69
58
|
|
|
70
59
|
```bash
|
|
71
|
-
clearotron doctor
|
|
60
|
+
npx clearotron doctor
|
|
72
61
|
```
|
|
73
62
|
|
|
74
|
-
Then start the product and open the portal
|
|
63
|
+
Then start the product and open the portal it prints:
|
|
75
64
|
|
|
76
65
|
```bash
|
|
77
|
-
clearotron start
|
|
66
|
+
npx clearotron start
|
|
78
67
|
```
|
|
79
68
|
|
|
80
69
|
That is the portal a brand owner uses. Ordering a clearance is the same screen — describe it in a
|
|
@@ -93,10 +82,11 @@ rights-holders behind them by jurisdiction:
|
|
|
93
82
|
|
|
94
83
|

|
|
95
84
|
|
|
96
|
-
Then run your own
|
|
85
|
+
Then run your own. `install` asks one question at a time and checks each credential before it saves it:
|
|
97
86
|
|
|
98
87
|
```bash
|
|
99
|
-
clearotron
|
|
88
|
+
npx clearotron install
|
|
89
|
+
npx clearotron run --job my-job.json
|
|
100
90
|
```
|
|
101
91
|
|
|
102
92
|
## How it fits together
|
package/bin/connect.mjs
CHANGED
|
@@ -45,9 +45,8 @@ import { homedir, userInfo } from "node:os";
|
|
|
45
45
|
import { fileURLToPath } from "node:url";
|
|
46
46
|
import { execFileSync } from "node:child_process";
|
|
47
47
|
import { createServer } from "node:net";
|
|
48
|
-
import { CONNECT_CLIENTS,
|
|
48
|
+
import { CONNECT_CLIENTS, clientById, whatItNeeds } from "../shared/connect-clients.mjs";
|
|
49
49
|
import { stdioConnectFor, STDIO_SHAPES } from "../shared/stdio-connect.mjs";
|
|
50
|
-
import { isWsl } from "../shared/wsl.mjs";
|
|
51
50
|
import { defaultDenylistPath, clientDoorAddress, clientDoorPort, clientDoorState, enablePlan, applyEnablePlan, describeChange, recordConnectKey, CLIENT_DOOR_UNIT } from "../shared/client-door.mjs";
|
|
52
51
|
import { mintToken, tokenId, resolvePerson, loadGrants } from "../shared/scope.mjs";
|
|
53
52
|
import { envFrom } from "../shared/env-aliases.mjs";
|
|
@@ -206,8 +205,7 @@ function deploymentHas(env = process.env) {
|
|
|
206
205
|
return {
|
|
207
206
|
// EVERY SHAPE, RESOLVED ONCE. A row picks its own; nothing here knows a client's name.
|
|
208
207
|
stdioRoutes: Object.fromEntries(Object.keys(STDIO_SHAPES).map((shape) =>
|
|
209
|
-
[shape, stdioConnectFor(shape, { workDir: env.CLEAROTRON_WORK_DIR || null
|
|
210
|
-
wsl: isWsl({ env }),
|
|
208
|
+
[shape, stdioConnectFor(shape, { workDir: env.CLEAROTRON_WORK_DIR || null })])),
|
|
211
209
|
// WHERE THE DOOR BINDS — not an address handed to any assistant. It is the loopback address the
|
|
212
210
|
// unit listens on, and `enablePlan` needs it to write the unit. It used to be passed to the
|
|
213
211
|
// resolver as `localAddress` and served to Cowork as somewhere to connect, which is the false
|
|
@@ -557,12 +555,17 @@ async function render(offer, have, { dryRun, running, allowMove = false }) {
|
|
|
557
555
|
return 1;
|
|
558
556
|
}
|
|
559
557
|
|
|
560
|
-
if (offer.route === "disk") {
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
|
|
564
|
-
|
|
558
|
+
if (offer.route === "disk" || (offer.route === "either" && offer.command)) {
|
|
559
|
+
// A COMMAND AND A CONFIG BLOCK ARE NOT THE SAME INSTRUCTION, and saying "run this" over a TOML
|
|
560
|
+
// block is how a reader pastes four lines into a shell. The shape says which it is.
|
|
561
|
+
const s = offer.stdio;
|
|
562
|
+
say(s?.kind === "config"
|
|
563
|
+
? ` Add this to ${s.where}:`
|
|
564
|
+
: " Run this once, on this machine:");
|
|
565
565
|
say("");
|
|
566
|
+
for (const line of String(offer.command).split("\n")) say(` ${line}`);
|
|
567
|
+
say("");
|
|
568
|
+
if (s?.after) { say(` ${s.after}`); say(""); }
|
|
566
569
|
say(` ${offer.note}`);
|
|
567
570
|
return 0;
|
|
568
571
|
}
|
|
@@ -618,40 +621,22 @@ async function render(offer, have, { dryRun, running, allowMove = false }) {
|
|
|
618
621
|
}
|
|
619
622
|
|
|
620
623
|
say(` Address: ${offer.address}`);
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* The offer's own steps, numbered, with each copy printed under the step that hands it over.
|
|
633
|
-
*
|
|
634
|
-
* ── AND WHERE TO PUT THEM ( — F35) ───────────────────────────────────────
|
|
635
|
-
*
|
|
636
|
-
* The owner was left with two strings and no destination: *"I don't know how to connect it in Claude
|
|
637
|
-
* Cowork with those details."* The steps were DEFINED IN THE PRODUCT the whole time and this verb simply
|
|
638
|
-
* never printed them. Nothing is authored here; a second set of instructions written at the CLI would
|
|
639
|
-
* drift from the page's, which is the defect connect-clients-are-data exists against. A secret copy is
|
|
640
|
-
* printed with the key this press minted put in its slot — the page does the same substitution into the
|
|
641
|
-
* clipboard — and with no key (a dry run) the step prints and its copy does not.
|
|
642
|
-
*/
|
|
643
|
-
function printSteps(offer, key) {
|
|
624
|
+
if (key) say(` Key: ${key}`); // printed once, stored nowhere
|
|
625
|
+
// ── AND WHERE TO PUT THEM ( — F35) ───────────────────────────────────────
|
|
626
|
+
//
|
|
627
|
+
// The owner was left with two strings and no destination: *"I don't know how to connect it in Claude
|
|
628
|
+
// Cowork with those details."* The steps were DEFINED IN THE PRODUCT the whole time — `withSteps`
|
|
629
|
+
// computes them for every offer, interpolating this install's own address and operator — and this
|
|
630
|
+
// verb simply never printed them. Nothing new is authored here; a second set of instructions written
|
|
631
|
+
// at the CLI would drift from the page's, which is the defect connect-clients-are-data exists against.
|
|
644
632
|
if (offer.steps?.length) {
|
|
645
633
|
say("");
|
|
646
634
|
say(` In ${offer.client?.name ?? "your assistant"}:`);
|
|
647
|
-
offer.steps.forEach((step, n) => {
|
|
648
|
-
say(` ${n + 1}. ${plainStep(step.text)}`);
|
|
649
|
-
const c = step.copy;
|
|
650
|
-
const text = !c ? null : c.kind === "secret" ? (key ? c.template.split(c.slot).join(key) : null) : c.text;
|
|
651
|
-
if (text) { say(""); for (const line of text.split("\n")) say(` ${line}`); say(""); }
|
|
652
|
-
if (step.hint) say(` ${plainStep(step.hint)}`);
|
|
653
|
-
});
|
|
635
|
+
offer.steps.forEach((step, n) => say(` ${n + 1}. ${step}`));
|
|
654
636
|
}
|
|
637
|
+
say("");
|
|
638
|
+
say(` ${offer.note}`);
|
|
639
|
+
return 0;
|
|
655
640
|
}
|
|
656
641
|
|
|
657
642
|
async function main() {
|
|
@@ -669,9 +654,6 @@ async function main() {
|
|
|
669
654
|
say(" not about you.");
|
|
670
655
|
say("");
|
|
671
656
|
say(" --client <name> skip the question (see --list for the names)");
|
|
672
|
-
say(" --where here your assistant runs on this machine: it starts the software itself");
|
|
673
|
-
say(" --where elsewhere your assistant runs somewhere else and reaches this install over the");
|
|
674
|
-
say(" internet, with a key made for you now");
|
|
675
657
|
say(" --list the assistants this build knows");
|
|
676
658
|
say(" --dry-run say what would change, change nothing");
|
|
677
659
|
say(" --allow-checkout-move");
|
|
@@ -681,7 +663,7 @@ async function main() {
|
|
|
681
663
|
say("");
|
|
682
664
|
return 0;
|
|
683
665
|
}
|
|
684
|
-
const known = new Set(["--client", "--
|
|
666
|
+
const known = new Set(["--client", "--list", "--dry-run", "--allow-checkout-move", "--help", "-h"]);
|
|
685
667
|
const unknown = argv.filter((a) => a.startsWith("--") && !known.has(a));
|
|
686
668
|
if (unknown.length) {
|
|
687
669
|
console.error(`connect: unrecognised flag(s): ${unknown.join(", ")}`);
|
|
@@ -689,12 +671,6 @@ async function main() {
|
|
|
689
671
|
process.exit(2);
|
|
690
672
|
}
|
|
691
673
|
const dryRun = argv.includes("--dry-run");
|
|
692
|
-
const w = argv.indexOf("--where");
|
|
693
|
-
if (w >= 0 && !Object.hasOwn(WHERE_FLAG, argv[w + 1] ?? "")) {
|
|
694
|
-
console.error(`connect: --where takes one of: ${Object.keys(WHERE_FLAG).join(", ")}`);
|
|
695
|
-
process.exit(2);
|
|
696
|
-
}
|
|
697
|
-
let route = w >= 0 ? WHERE_FLAG[argv[w + 1]] : null;
|
|
698
674
|
// F40 — reason about the RUNNING product from the units' own environment, not this CLI's env file.
|
|
699
675
|
const running = runningEnv();
|
|
700
676
|
const have = deploymentHas(running.env);
|
|
@@ -736,30 +712,9 @@ async function main() {
|
|
|
736
712
|
chosen = CONNECT_CLIENTS[Number(answer) - 1] ?? clientById(answer);
|
|
737
713
|
} finally { rl.close(); }
|
|
738
714
|
if (!chosen) { console.error("connect: not one of the listed assistants."); process.exit(2); }
|
|
739
|
-
// THE SECOND QUESTION, ASKED ONLY WHEN IT HAS TWO ANSWERS — the page's own, in the terminal's words.
|
|
740
|
-
// Where only one route is served here, asking would offer a choice whose other half cannot work.
|
|
741
|
-
const both = ["disk", "public-http"].filter((r) => whatItNeeds(chosen, have, r)?.served);
|
|
742
|
-
if (!route && both.length > 1) {
|
|
743
|
-
say("");
|
|
744
|
-
say(" Where does it run?");
|
|
745
|
-
say("");
|
|
746
|
-
say(" 1) On this machine — it starts the software itself, nothing to open up");
|
|
747
|
-
say(" 2) Somewhere else — it reaches this install over the internet, with a key made for you now");
|
|
748
|
-
say("");
|
|
749
|
-
const rl2 = createInterface({ input: stdin, output: stdout });
|
|
750
|
-
try {
|
|
751
|
-
const answer = (await rl2.question(" 1-2: ")).trim();
|
|
752
|
-
route = answer === "1" ? "disk" : answer === "2" ? "public-http" : null;
|
|
753
|
-
} finally { rl2.close(); }
|
|
754
|
-
if (!route) { console.error("connect: answer 1 or 2."); process.exit(2); }
|
|
755
|
-
}
|
|
756
|
-
route ??= both[0] ?? chosen.lead;
|
|
757
715
|
}
|
|
758
716
|
|
|
759
|
-
|
|
760
|
-
// `--client cowork` still mints a key and `--client codex` still prints a settings block.
|
|
761
|
-
route ??= leadRouteFor(argv[i + 1]);
|
|
762
|
-
return await render(whatItNeeds(chosen, have, route), have, { dryRun, running, allowMove: argv.includes("--allow-checkout-move") });
|
|
717
|
+
return await render(whatItNeeds(chosen, have), have, { dryRun, running, allowMove: argv.includes("--allow-checkout-move") });
|
|
763
718
|
}
|
|
764
719
|
|
|
765
720
|
// THE DISPATCH RUNS ONLY WHEN THIS FILE IS THE COMMAND. Without the guard, importing
|
package/bin/disconnect.mjs
CHANGED
|
@@ -57,7 +57,7 @@ import { stdin, stdout } from "node:process";
|
|
|
57
57
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from "node:fs";
|
|
58
58
|
import { join, dirname } from "node:path";
|
|
59
59
|
import { homedir, userInfo } from "node:os";
|
|
60
|
-
import { CONNECT_CLIENTS,
|
|
60
|
+
import { CONNECT_CLIENTS, clientById } from "../shared/connect-clients.mjs";
|
|
61
61
|
import { defaultDenylistPath, disablePlan, revokeEveryonePlan, applyDisablePlan, describeClosure, recordedKeysFor, removeRecordedKeys } from "../shared/client-door.mjs";
|
|
62
62
|
import { loadGrants } from "../shared/scope.mjs";
|
|
63
63
|
import { envFrom } from "../shared/env-aliases.mjs";
|
|
@@ -83,8 +83,6 @@ async function main() {
|
|
|
83
83
|
say(" `clearotron grant` manages that.");
|
|
84
84
|
say("");
|
|
85
85
|
say(" --client <name> which assistant you connected (see --list for the names)");
|
|
86
|
-
say(" --where here|elsewhere");
|
|
87
|
-
say(" how you connected it: on this machine, or over the internet with a key");
|
|
88
86
|
say(" --list the assistants this build knows");
|
|
89
87
|
say(" --everyone the admin act: revoke EVERY issued key on this install. It says");
|
|
90
88
|
say(" how many keys and how many people that is before doing it.");
|
|
@@ -92,7 +90,7 @@ async function main() {
|
|
|
92
90
|
say("");
|
|
93
91
|
return 0;
|
|
94
92
|
}
|
|
95
|
-
const known = new Set(["--client", "--
|
|
93
|
+
const known = new Set(["--client", "--list", "--dry-run", "--everyone", "--help", "-h"]);
|
|
96
94
|
const unknown = argv.filter((a) => a.startsWith("--") && !known.has(a));
|
|
97
95
|
if (unknown.length) {
|
|
98
96
|
console.error(`disconnect: unrecognised flag(s): ${unknown.join(", ")}`);
|
|
@@ -139,21 +137,18 @@ async function main() {
|
|
|
139
137
|
say(` ${chosen.name}`);
|
|
140
138
|
say("");
|
|
141
139
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
if (w >= 0 && !Object.hasOwn(WHERE_FLAG, argv[w + 1] ?? "")) {
|
|
147
|
-
console.error(`disconnect: --where takes one of: ${Object.keys(WHERE_FLAG).join(", ")}`);
|
|
148
|
-
process.exit(2);
|
|
149
|
-
}
|
|
150
|
-
const route = w >= 0 ? WHERE_FLAG[argv[w + 1]] : (i >= 0 ? leadRouteFor(argv[i + 1]) : chosen.lead);
|
|
151
|
-
if (route === "disk") {
|
|
140
|
+
// The row's own property decides the side. `accepts: "stdio"` never touched this install; "either"
|
|
141
|
+
// only opens the door when its stdio route was missing, and on THIS box (the one disconnect runs on)
|
|
142
|
+
// the stdio route resolves, so its connect handed over a command too.
|
|
143
|
+
if (chosen.accepts === "stdio" || chosen.accepts === "either") {
|
|
152
144
|
say(" Connecting this assistant changed nothing on this install — it runs the software itself,");
|
|
153
145
|
say(" from the configuration you added on its side. To disconnect it, remove that entry in the");
|
|
154
146
|
say(" assistant's own settings.");
|
|
155
|
-
|
|
156
|
-
|
|
147
|
+
if (chosen.accepts === "either") {
|
|
148
|
+
say("");
|
|
149
|
+
say(" If you connected it by address instead, the door and key are shared — disconnect the");
|
|
150
|
+
say(" assistant you named when the door was opened, and the closure covers this one too.");
|
|
151
|
+
}
|
|
157
152
|
return 0;
|
|
158
153
|
}
|
|
159
154
|
|
package/bin/example.mjs
CHANGED
|
@@ -47,7 +47,6 @@ import { spawn } from "node:child_process";
|
|
|
47
47
|
import { BRAND } from "../shared/brand.mjs"; // — the installer's own name, from the tenant seam
|
|
48
48
|
import { envFrom } from "../shared/env-aliases.mjs"; // — resolves EITHER spelling; names the retired one because that is the live-writable half
|
|
49
49
|
import { isFrozen, demoChildren, publishSource } from "../driver/demo-container.mjs"; // — one definition of what a frozen demo is, for the player AND the gate
|
|
50
|
-
import { ensureDemoProgram, demoProgramEnv } from "../shared/permanent-install.mjs";
|
|
51
50
|
|
|
52
51
|
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
53
52
|
|
|
@@ -370,19 +369,8 @@ console.log(` Removing this demo later is one directory: ${removeDirectory(dem
|
|
|
370
369
|
strayFromAnOlderDemo();
|
|
371
370
|
console.log("");
|
|
372
371
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
// Started from npm's cache, the supervisor printed every command it gives the reader as `cd <npm's cache>
|
|
376
|
-
// && npx clearotron …`: the passphrase reset, the key, the free-port hint, `start --background`. Each one
|
|
377
|
-
// failed once npm cleaned that cache (measured on a published beta, 2026-09-11). So the copy the demo
|
|
378
|
-
// keeps in `<base>/program` is laid down first and the supervisor is started from it, which also puts the
|
|
379
|
-
// portal and the doors on a program that outlives the cache. Everywhere else, and when the copy cannot be
|
|
380
|
-
// made, it starts from here as before.
|
|
381
|
-
const programRoot = ensureDemoProgram({ base: demoBase, say: (line) => console.log(line) });
|
|
382
|
-
const startFrom = programRoot ?? REPO;
|
|
383
|
-
const child = spawn(process.execPath, [join(startFrom, "bin", "start.mjs"), ...startArgs], {
|
|
384
|
-
cwd: startFrom, stdio: ["ignore", "inherit", "inherit"],
|
|
385
|
-
env: programRoot ? demoProgramEnv(process.env) : process.env,
|
|
372
|
+
const child = spawn(process.execPath, [join(REPO, "bin", "start.mjs"), ...startArgs], {
|
|
373
|
+
cwd: REPO, stdio: ["ignore", "inherit", "inherit"],
|
|
386
374
|
});
|
|
387
375
|
child.on("error", (e) => die(`demo: could not start the portal: ${String(e?.message ?? e)}`));
|
|
388
376
|
// Its exit code is the demo's. A supervisor that swallowed a child's refusal would report a demo that
|
package/bin/key.mjs
CHANGED
|
@@ -29,9 +29,7 @@
|
|
|
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
31
|
import { existsSync, readFileSync } from "node:fs";
|
|
32
|
-
import { defaultGrantsPath
|
|
33
|
-
import { demoTokenSecretPath } from "../shared/client-door.mjs";
|
|
34
|
-
import { resolvePerson } from "../shared/scope.mjs"; // the door's own reading of the guest list, so one answer serves both
|
|
32
|
+
import { defaultGrantsPath } from "./start.mjs";
|
|
35
33
|
import { mintFromOptions } from "../mcp-server/mint-token.mjs";
|
|
36
34
|
|
|
37
35
|
const argv = process.argv.slice(2);
|
|
@@ -46,7 +44,6 @@ const USAGE = `usage: ${p}clearotron key issue <email> [options]
|
|
|
46
44
|
|
|
47
45
|
--accounts a,b cap the key to these account keys as well (omit = whatever their grant allows)
|
|
48
46
|
--ttl-days <n> how long it is valid (default 90)
|
|
49
|
-
--base <dir> the install or demo to issue for, when it is not the one set up in ~/trademark
|
|
50
47
|
|
|
51
48
|
The token is printed ONCE on stdout and stored nowhere — possession is the credential. Everything
|
|
52
49
|
else goes to stderr, including the jti that revokes it.`;
|
|
@@ -68,17 +65,6 @@ const email = positional[0];
|
|
|
68
65
|
if (!email) die(`who is the key for? Give the person's email.\n\n${USAGE}`);
|
|
69
66
|
if (!email.includes("@")) die(`"${email}" is not an email address — the subject of an account key is the identity their assistant presents, and the grants file is keyed on it.`);
|
|
70
67
|
|
|
71
|
-
// `--base` NAMES THE INSTALL, as it does for `start` and `passphrase`: its guest list, and — for a demo,
|
|
72
|
-
// which keeps its signing secret in its own base rather than in any settings file — that secret. Without
|
|
73
|
-
// it the verb read the install's settings and refused to issue a key for a running demo.
|
|
74
|
-
const baseAt = rest.indexOf("--base");
|
|
75
|
-
const base = baseAt >= 0 ? rest[baseAt + 1] : null;
|
|
76
|
-
if (baseAt >= 0 && (!base || base.startsWith("--"))) die(`--base needs a directory.\n\n${USAGE}`);
|
|
77
|
-
if (base) {
|
|
78
|
-
const secretFile = demoTokenSecretPath(base);
|
|
79
|
-
if (existsSync(secretFile)) process.env.TRADEMARK_MCP_TOKEN_SECRET = readFileSync(secretFile, "utf8").trim();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
68
|
const ttlDays = Number(flag("--ttl-days") ?? 90);
|
|
83
69
|
const accounts = flag("--accounts") ? flag("--accounts").split(",").map((s) => s.trim()).filter(Boolean) : null;
|
|
84
70
|
|
|
@@ -107,19 +93,15 @@ for (const line of minted.notes) console.error(line);
|
|
|
107
93
|
// inert. STDERR, so the token stays alone on stdout and `key issue ... > token.txt` keeps working —
|
|
108
94
|
// that split is deliberate and this must not undo it.
|
|
109
95
|
try {
|
|
110
|
-
const rosterPath =
|
|
96
|
+
const rosterPath = defaultGrantsPath();
|
|
111
97
|
if (!existsSync(rosterPath)) {
|
|
112
98
|
console.error(`\nNOTE: no guest list at ${rosterPath} yet, so nothing grants ${email} anything and this key will be refused at the door. \`clearotron start\` writes the list; then: clearotron grant add ${email} --tenant <name> --accounts <brand-owner-key>`);
|
|
113
99
|
} else {
|
|
114
|
-
// THE DOOR'S OWN RESOLVER DECIDES, not a second reading of the file. This walked `tenants[].users`
|
|
115
|
-
// alone, and the demo's guest list grants its account through the top-level `people` map — so the
|
|
116
|
-
// demo's own key came with a note saying the door would refuse it, over a key the door accepts
|
|
117
|
-
// (driven 2026-09-11). One reader for one question.
|
|
118
100
|
const roster = JSON.parse(readFileSync(rosterPath, "utf8"));
|
|
119
|
-
const
|
|
120
|
-
const listed =
|
|
101
|
+
const tenants = roster?.tenants && typeof roster.tenants === "object" ? roster.tenants : {};
|
|
102
|
+
const listed = Object.values(tenants).some((t) => t?.users && Object.prototype.hasOwnProperty.call(t.users, email));
|
|
121
103
|
if (!listed)
|
|
122
|
-
console.error(`\nNOTE: ${email} is
|
|
104
|
+
console.error(`\nNOTE: ${email} is on no tenant in ${rosterPath}, so this key resolves to no accounts and the door will refuse it. Grant them access with: clearotron grant add ${email} --tenant <name> --accounts <brand-owner-key>`);
|
|
123
105
|
}
|
|
124
106
|
} catch (e) {
|
|
125
107
|
// A ROSTER THIS COMMAND CANNOT READ IS NOT A ROSTER SAYING THE SUBJECT IS ABSENT. Said as what it is,
|