clearotron 0.3.0-beta.1 → 0.3.0-beta.10
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 +26 -5
- package/CONTRIBUTING.md +1 -1
- package/INSTALL.md +122 -16
- package/README.md +31 -20
- package/bin/brandowner.mjs +94 -1
- package/bin/connect.mjs +71 -26
- package/bin/disconnect.mjs +16 -11
- package/bin/example.mjs +14 -2
- package/bin/key.mjs +23 -5
- package/bin/onboard.mjs +346 -98
- package/bin/passphrase.mjs +23 -4
- package/bin/start.mjs +363 -60
- package/bin/status.mjs +23 -3
- package/bin/stop.mjs +23 -8
- package/bin/update.mjs +38 -6
- 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 +6 -2
- package/docs/architecture/05-config-governance.md +3 -2
- package/driver/CHANGELOG.md +199 -5
- package/driver/compose-read.mjs +21 -2
- package/driver/contract-e3-backlog.mjs +1 -1
- package/driver/demo-container.mjs +65 -2
- package/driver/demo-posture.mjs +59 -8
- package/driver/driver.config.mjs +41 -7
- package/driver/engine/anthropic-agent.mjs +34 -17
- package/driver/engine/child-record.mjs +93 -0
- package/driver/engine/jx-turn.mjs +4 -1
- package/driver/engine/openai-agent.mjs +60 -2
- package/driver/enqueue-schema.mjs +3 -1
- package/driver/findings-model.mjs +6 -1
- package/driver/gateway.mjs +88 -15
- package/driver/knockout-assess-record.mjs +6 -3
- package/driver/package.json +1 -1
- package/driver/portal-local-auth.mjs +132 -5
- package/driver/portal-service.mjs +156 -51
- package/driver/portal-upstream.mjs +20 -1
- package/driver/profile-service.mjs +31 -3
- package/driver/publish/index.mjs +12 -9
- package/driver/publish/knockout.mjs +14 -1
- package/driver/publish/office-record-links.mjs +111 -25
- package/driver/publish/render-knockout.mjs +31 -11
- package/driver/publish/xlsx.mjs +33 -1
- package/driver/recipe-service.mjs +14 -5
- package/driver/record-origins.mjs +14 -0
- package/driver/register-records.mjs +6 -0
- package/driver/run-requirements.mjs +24 -1
- package/driver/runner.mjs +10 -5
- package/driver/search-policy.mjs +7 -2
- package/driver/skills/knockout-assess/SKILL.md +1 -1
- package/driver/suite-census.json +326 -74
- package/driver/systemd/render-units.mjs +7 -13
- package/driver/unit-inventory.mjs +109 -5
- package/driver/updater-identity.mjs +178 -0
- package/driver/usage-ledger.mjs +5 -5
- package/mcp-server/CHANGELOG.md +42 -2
- package/mcp-server/CONNECT.md +3 -2
- package/mcp-server/http-server.mjs +16 -13
- package/mcp-server/lib/driver.mjs +9 -0
- package/mcp-server/lib/knockout.mjs +14 -2
- package/mcp-server/lib/ops.mjs +38 -16
- package/mcp-server/lib/options.mjs +20 -5
- package/mcp-server/package.json +2 -2
- package/mcp-server/server.mjs +1 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-KpytsmNH.css → index-Cv-E_agg.css} +206 -88
- package/portal-ui/dist/assets/{index-CWTHP0sH.js → index-CwPAS0we.js} +556 -399
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +3 -3
- package/providers/clarivate/src/core.js +5 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +36 -0
- package/providers/oauth-mcp-bridge/package.json +2 -2
- package/scripts/changelog-plain-language.mjs +7 -30
- package/scripts/e2e-unread-terminals.mjs +1 -1
- package/scripts/e2e.mjs +210 -22
- package/scripts/engine-probe.mjs +2 -2
- package/scripts/env-audit.mjs +10 -3
- package/scripts/env-classify.mjs +208 -12
- package/scripts/live-surface-check.mjs +74 -2
- package/scripts/plain-language-rules.mjs +103 -0
- package/scripts/release-note-required.mjs +118 -24
- package/scripts/release-notes-lint.mjs +22 -43
- package/scripts/release-version.mjs +59 -0
- package/scripts/revisit-render-check.mjs +28 -6
- package/scripts/text-difference.mjs +22 -0
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/access-audience.mjs +7 -6
- package/shared/brand.mjs +7 -3
- package/shared/client-door.mjs +39 -0
- package/shared/connect-clients.mjs +290 -321
- package/shared/env-file-merge.mjs +24 -0
- package/shared/env-local.mjs +25 -2
- package/shared/invocation.mjs +49 -2
- package/shared/names-in-force.mjs +3 -0
- package/shared/permanent-install.mjs +237 -0
- package/shared/reap-on-exit.mjs +27 -14
- package/shared/running-start.mjs +73 -0
- package/shared/stdio-connect.mjs +95 -14
- package/shared/store-in-repo.mjs +197 -2
- package/shared/verb-shim.mjs +10 -1
- package/shared/withheld-paths-access.mjs +6 -6
- package/shared/wsl.mjs +23 -0
- package/shared/yes-no-echo.mjs +35 -0
package/.env.example
CHANGED
|
@@ -388,6 +388,26 @@ CLEAROTRON_SUITE_TELEMETRY_DIR=
|
|
|
388
388
|
# effect: deployment
|
|
389
389
|
CLEAROTRON_DEMO=
|
|
390
390
|
|
|
391
|
+
# How the reader typed this command, handed from the dispatcher to each verb it runs. `clearotron`
|
|
392
|
+
# runs every verb as a process of its own, so a verb's own argv[1] is `bin/<verb>.mjs` however the
|
|
393
|
+
# reader started it. The dispatcher passes its own argv[1] down in this name, and every command a verb
|
|
394
|
+
# prints for the reader to type next is spelled from it: `clearotron …` after a global install,
|
|
395
|
+
# `npx clearotron …` otherwise. Unset, a verb reads its own argv[1].
|
|
396
|
+
# Set by bin/clearotron.mjs on every verb it runs, never by an operator, so the row is left commented.
|
|
397
|
+
# It is listed for the reason CLEAROTRON_DEMO is: the catalogue is about what an operator can FIND.
|
|
398
|
+
# Read by shared/invocation.mjs.
|
|
399
|
+
# effect: deployment
|
|
400
|
+
# CLEAROTRON_INVOKED_AS=
|
|
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
|
+
|
|
391
411
|
# How many client what-ifs the runner drains at once (default 1). A what-if re-runs ONE stage of a run
|
|
392
412
|
# that already exists, in a sandbox, at a client's request — the owner opened it to clients on
|
|
393
413
|
# 2026-08-27 and ruled SPEND controls out with it ("ignore the call spend"). This is not one.
|
|
@@ -538,11 +558,12 @@ CLEAROTRON_DEMO_PROFILES=
|
|
|
538
558
|
|
|
539
559
|
# ── The organisation setup names ──────────────────────────────────────────────────────────────────
|
|
540
560
|
#
|
|
541
|
-
# Your organisation's name. `npm run setup` asks for it
|
|
542
|
-
# here, quoted, so a name holding a `#` survives. The first `clearotron start` files it as the first
|
|
561
|
+
# Your organisation's name. `npm run setup` asks for it (and, on a local install, not for a sign-in
|
|
562
|
+
# address) and writes it here, quoted, so a name holding a `#` survives. The first `clearotron start` files it as the first
|
|
543
563
|
# organisation in the grants file (CLEAROTRON_ACCESS_FILE) when that file holds none; from then on the
|
|
544
|
-
# grants file holds the name
|
|
545
|
-
# organisation
|
|
546
|
-
#
|
|
564
|
+
# grants file holds the name and renaming is an edit there. The portal also reads it, to name the
|
|
565
|
+
# organisation on its top bar and on its sign-in refusal page. Unset or empty, no organisation is
|
|
566
|
+
# invented. `clearotron start --organisation <name>` supplies it for one start, and a demo never reads
|
|
567
|
+
# it. Read by bin/start.mjs and shared/brand.mjs.
|
|
547
568
|
# effect: deployment
|
|
548
569
|
CLEAROTRON_ORGANISATION_NAME=
|
package/CONTRIBUTING.md
CHANGED
|
@@ -12,7 +12,7 @@ the rest — with the file that owns each.
|
|
|
12
12
|
|
|
13
13
|
## What you can run, with nothing but a clone
|
|
14
14
|
|
|
15
|
-
**Node 22 is a hard floor.** `package.json` declares it, `.nvmrc` pins it, and the free US register
|
|
15
|
+
**Node 22.13 is a hard floor.** `package.json` declares it, `.nvmrc` pins it, and the free US register
|
|
16
16
|
runs on `node:sqlite`. Node 20 will fail in ways that look like your change.
|
|
17
17
|
|
|
18
18
|
```bash
|
package/INSTALL.md
CHANGED
|
@@ -48,7 +48,8 @@ run is [mcp-server/CONNECT.md](mcp-server/CONNECT.md), and why something is the
|
|
|
48
48
|
- **macOS, Linux, or native Windows for the demo; WSL2 for a clearance.** `npx clearotron
|
|
49
49
|
demo` runs anywhere Node does, native Windows included. A real clearance does not: the engine resolves
|
|
50
50
|
the reasoning CLI the POSIX way, so a native-Windows clearance refuses at preflight even with the CLI
|
|
51
|
-
on `PATH`.
|
|
51
|
+
on `PATH`. Native Windows clearances are planned for a later release. Until then, on Windows,
|
|
52
|
+
`wsl --install -d Ubuntu`, then `wsl -d Ubuntu`, and work through this page
|
|
52
53
|
from **inside** that distribution. Name it: plain `wsl` can open a minimal image with no apt, no
|
|
53
54
|
curl and no bash, and everything below assumes Ubuntu. A fresh Ubuntu has no Node at all, and
|
|
54
55
|
apt's package is below the floor above, so `npx` answers "not found" before anything of ours runs.
|
|
@@ -63,6 +64,15 @@ run is [mcp-server/CONNECT.md](mcp-server/CONNECT.md), and why something is the
|
|
|
63
64
|
npx clearotron install
|
|
64
65
|
```
|
|
65
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
|
+
|
|
66
76
|
A *hosted* deployment needs Linux for one further thing, the systemd outbox trigger —
|
|
67
77
|
[driver/systemd/README.md](driver/systemd/README.md).
|
|
68
78
|
- **A reasoning CLI on your `PATH`, signed in.** This is the prerequisite people miss. Every stage runs
|
|
@@ -285,6 +295,71 @@ keeps working throughout.
|
|
|
285
295
|
provider, so it is safe to run on any machine that has the repository. "Safe anywhere" is about spend,
|
|
286
296
|
not about where it will run — on a packaged tree it refuses, as above.
|
|
287
297
|
|
|
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
|
+
|
|
288
363
|
## 3. Configuration (environment)
|
|
289
364
|
|
|
290
365
|
Runtime configuration is by environment variable, and there are two ways to supply them.
|
|
@@ -490,6 +565,25 @@ matches, the neutral Generic default applies.
|
|
|
490
565
|
- **Your real customers live outside the repo.** Point `CLEAROTRON_CUSTOMERS_DIR` at your own private
|
|
491
566
|
config store and the engine loads *those* accounts instead. **Same engine, different config path** —
|
|
492
567
|
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.
|
|
493
587
|
- **Run data is external too.** Published reports, audits, and per-run state go to the archive pool at
|
|
494
588
|
`CLEAROTRON_REPORTS_DIR`. Nothing customer-specific is committed to the repository.
|
|
495
589
|
|
|
@@ -729,12 +823,17 @@ Use the demo to see what this system produces. Use `npx clearotron start` to run
|
|
|
729
823
|
|
|
730
824
|
- Generates `PORTAL_SECRET` and `TRADEMARK_MCP_TOKEN_SECRET` and **appends** them to `~/.config/clearotron/.env` at
|
|
731
825
|
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.
|
|
732
828
|
- Creates `~/trademark/` — `pool/`, `workspace/`, `queue/`, `outbox/`, `locks/`, an empty grants file,
|
|
733
829
|
and a small git repository for saved searches. Same base directory `npx clearotron install` uses, so whichever
|
|
734
830
|
of the two you ran first, the other finds the same install. Move it with `npx clearotron start --base <dir>`.
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
+
- Mints your sign-in passphrase and **prints it once**. Write it down. It is stored as a scrypt digest in
|
|
834
|
+
`~/trademark/portal-local-credential.json`, nothing can read it back, and no later start reprints it. To
|
|
835
|
+
get a new one, run `clearotron passphrase --reset`. An install that has been signing in with
|
|
836
|
+
`~/.cordillera/portal-local-credential.json`, which earlier versions shared between installs, keeps it.
|
|
738
837
|
|
|
739
838
|
### Who you are
|
|
740
839
|
|
|
@@ -744,12 +843,12 @@ You sign in as `<your-username>@localhost` unless you say otherwise:
|
|
|
744
843
|
npx clearotron start --user you@example.com
|
|
745
844
|
```
|
|
746
845
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
instance.
|
|
846
|
+
Setup does not ask for the address: it writes the local-account form to `.env` and shows it once, in
|
|
847
|
+
its summary, as the address that signs in. An address already in `.env` is kept. It is also the first
|
|
848
|
+
person on this install: the first start writes it into the grants file (`CLEAROTRON_ACCESS_FILE`, §8)
|
|
849
|
+
with access to everything and both permissions, Run clearances and Manage. Setup asks for your
|
|
850
|
+
organisation's name, and the same start files it there as your first organisation. The address admits
|
|
851
|
+
nobody else at its domain; enrolling anyone else is that same file, exactly as on a hosted instance.
|
|
753
852
|
|
|
754
853
|
**No authentication is switched off to make this work, and none can be.** Both doors prove who the
|
|
755
854
|
caller is — the portal by passphrase and a signed session cookie, the engine door by a mandatory
|
|
@@ -901,12 +1000,19 @@ their own integration work.
|
|
|
901
1000
|
**Two shapes, and the line between them is what your assistant can do — not where it runs.** Ruling
|
|
902
1001
|
2026-09-03, on the vendor's own documented behaviour:
|
|
903
1002
|
|
|
904
|
-
1. **
|
|
905
|
-
Codex
|
|
906
|
-
network, no ingress. `npx clearotron start` prints the one line to
|
|
907
|
-
connect` hands it over per assistant.
|
|
908
|
-
2. **
|
|
909
|
-
|
|
1003
|
+
1. **Clearotron is installed on the machine the assistant runs on** — Claude's desktop app, Claude Code,
|
|
1004
|
+
Codex, the ChatGPT desktop app, an agent that runs commands. The assistant spawns the server over
|
|
1005
|
+
stdio. No address, no key, no network, no ingress. `npx clearotron start` prints the one line to
|
|
1006
|
+
paste, and `npx clearotron connect --where here` hands it over per assistant.
|
|
1007
|
+
2. **Clearotron is running somewhere else** — a server, a cloud machine, anywhere the assistant is
|
|
1008
|
+
not. These need **a publicly reachable HTTPS address**, plus a key made for the person connecting.
|
|
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`.
|
|
910
1016
|
|
|
911
1017
|
**A loopback address is never an answer for shape 2, and that is not about your network.** A remote MCP
|
|
912
1018
|
connector is reached **from the vendor's cloud**, never from the reader's device. Anthropic's own help
|
package/README.md
CHANGED
|
@@ -19,50 +19,62 @@ 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 installed.**
|
|
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
|
|
29
|
-
|
|
30
|
-
calls to us.
|
|
28
|
+
That fetches the published package — it will ask once before downloading — then replays finished
|
|
29
|
+
clearances into a local portal and prints the portal's address and the passphrase to sign in with. Open
|
|
30
|
+
the address in your browser. No account, no credentials, no network calls to us.
|
|
31
31
|
|
|
32
32
|
**Then install it.**
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
|
|
35
|
+
npx clearotron install
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
Node 22.13 or newer, on macOS or Linux.
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
Node 22.13 or newer, on macOS or Linux. It needs no root: it puts the program under `~/.local`, with the
|
|
39
|
+
`clearotron` command in `~/.local/bin`, then asks one question at a time and checks each credential
|
|
40
|
+
before it saves it. With `~/.local/bin` on your `PATH`, every command below works in the short form;
|
|
41
|
+
otherwise use the full path `install` prints at the end. **On Windows the demo above runs natively; a
|
|
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`.
|
|
41
50
|
|
|
42
|
-
|
|
43
|
-
was published. If you want the newest code instead, a beta is published whenever there is
|
|
44
|
-
testing — deliberately, days apart, not on every merge:
|
|
51
|
+
`npx clearotron install` gives you the **stable** release — the one that has run a real clearance end to
|
|
52
|
+
end before it was published. If you want the newest code instead, a beta is published whenever there is
|
|
53
|
+
something worth testing — deliberately, days apart, not on every merge:
|
|
45
54
|
|
|
46
55
|
```bash
|
|
47
|
-
|
|
56
|
+
npx clearotron@beta install
|
|
48
57
|
```
|
|
49
58
|
|
|
50
59
|
What each channel promises, and when a stable is cut: [docs/RELEASES.md](docs/RELEASES.md). If you are not
|
|
51
60
|
sure, the first command is the one you want.
|
|
52
61
|
|
|
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
|
+
|
|
53
65
|
## Quick start
|
|
54
66
|
|
|
55
|
-
|
|
56
|
-
names whatever is still missing:
|
|
67
|
+
With it installed, check what it found before it does anything. `doctor` only reads — it writes nothing,
|
|
68
|
+
calls nobody, and names whatever is still missing:
|
|
57
69
|
|
|
58
70
|
```bash
|
|
59
|
-
|
|
71
|
+
clearotron doctor
|
|
60
72
|
```
|
|
61
73
|
|
|
62
|
-
Then start the product and open the portal it prints:
|
|
74
|
+
Then start the product and open the portal address it prints:
|
|
63
75
|
|
|
64
76
|
```bash
|
|
65
|
-
|
|
77
|
+
clearotron start
|
|
66
78
|
```
|
|
67
79
|
|
|
68
80
|
That is the portal a brand owner uses. Ordering a clearance is the same screen — describe it in a
|
|
@@ -81,11 +93,10 @@ rights-holders behind them by jurisdiction:
|
|
|
81
93
|
|
|
82
94
|

|
|
83
95
|
|
|
84
|
-
Then run your own
|
|
96
|
+
Then run your own: order it in the portal, or hand the engine a job file:
|
|
85
97
|
|
|
86
98
|
```bash
|
|
87
|
-
|
|
88
|
-
npx clearotron run --job my-job.json
|
|
99
|
+
clearotron run --job my-job.json
|
|
89
100
|
```
|
|
90
101
|
|
|
91
102
|
## How it fits together
|
package/bin/brandowner.mjs
CHANGED
|
@@ -119,6 +119,16 @@ const USAGE = `
|
|
|
119
119
|
--context a file whose contents become this owner's context pack
|
|
120
120
|
--dry-run say exactly what would be written, and write nothing
|
|
121
121
|
|
|
122
|
+
clearotron brandowner framework <key> <path>
|
|
123
|
+
|
|
124
|
+
Point an existing company at a risk framework, as skills/prelim-search/<file>.md.
|
|
125
|
+
The deck is checked before anything is written: a path that does not resolve, or a
|
|
126
|
+
manifest that will not load, is refused and the company is left exactly as it was.
|
|
127
|
+
|
|
128
|
+
This is the route a company created in the browser gets a framework by. The browser
|
|
129
|
+
deliberately sets no framework — frameworkPath is code-owned and the profile screen
|
|
130
|
+
shows it read-only — so without this verb there was no supported route at all.
|
|
131
|
+
|
|
122
132
|
Exit codes: 0 written and recorded · 1 refused, nothing written · 2 usage
|
|
123
133
|
3 written but NOT recorded — the bundle is written and the store has no record of it
|
|
124
134
|
`;
|
|
@@ -266,11 +276,94 @@ export async function add(argv, {
|
|
|
266
276
|
return { written: true, store, profile, framework, commit, commitError };
|
|
267
277
|
}
|
|
268
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Point an existing company at a risk framework.
|
|
281
|
+
*
|
|
282
|
+
* THE LINT IS `resolveFramework`, NOT A SECOND OPINION. That function already refuses a path of the
|
|
283
|
+
* wrong shape, one that does not resolve on this install, and one whose manifest will not load — in the
|
|
284
|
+
* words the create path uses. A second check here would be a second set of messages to keep in step, and
|
|
285
|
+
* the one this file could write is the one that would go stale.
|
|
286
|
+
*
|
|
287
|
+
* IT REFUSES BEFORE IT WRITES. A framework somebody chose that does not resolve is a mistake, not an
|
|
288
|
+
* absence, and the company is left exactly as it was — which matters more here than on a create, because
|
|
289
|
+
* there is an existing company whose rating this would change.
|
|
290
|
+
*
|
|
291
|
+
* THE WHOLE PROFILE IS REWRITTEN, one field changed. `defaultWriteProfile` is the one writer, so this
|
|
292
|
+
* cannot invent a second shape of profile file; the read-modify-write is deliberate and the alternative
|
|
293
|
+
* — patching a key in place — is how two writers come to disagree about what a bundle contains.
|
|
294
|
+
*/
|
|
295
|
+
export async function framework(argv, {
|
|
296
|
+
resolution = profileStoreResolution(),
|
|
297
|
+
out = console.log,
|
|
298
|
+
} = {}) {
|
|
299
|
+
const [key, path] = argv;
|
|
300
|
+
if (!key) throw new Refusal(`this command needs a company key.${USAGE}`);
|
|
301
|
+
if (!path) throw new Refusal(`this command needs a framework path, as skills/prelim-search/<file>.md.${USAGE}`);
|
|
302
|
+
try { assertProfileKey(key); }
|
|
303
|
+
catch (e) { throw new Refusal(e?.message ?? String(e)); }
|
|
304
|
+
|
|
305
|
+
const store = storeForAdd(resolution);
|
|
306
|
+
const file = join(store, `${key}.json`);
|
|
307
|
+
// AN ABSENT COMPANY IS NAMED, not created. `add` is the verb that creates; a typo here would
|
|
308
|
+
// otherwise write a bundle carrying nothing but a framework path.
|
|
309
|
+
if (!existsSync(file))
|
|
310
|
+
throw new Refusal(`no company "${key}" in ${store} — "brandowner add ${key} --name ..." creates one.`);
|
|
311
|
+
|
|
312
|
+
// Refuses here, before anything is read for writing.
|
|
313
|
+
const resolved = resolveFramework(path);
|
|
314
|
+
|
|
315
|
+
const profile = JSON.parse(readFileSync(file, "utf8"));
|
|
316
|
+
const was = profile.frameworkPath ?? null;
|
|
317
|
+
if (was === resolved.path) {
|
|
318
|
+
out(` ${key} already rates under ${resolved.path} — nothing to change.`);
|
|
319
|
+
return { written: false, store, framework: resolved };
|
|
320
|
+
}
|
|
321
|
+
// THE CONTEXT PACK IS READ AND HANDED BACK, because omitting it is not "leave it alone".
|
|
322
|
+
// `defaultWriteProfile` reads an absent pack as "this company has none" and REMOVES the sibling file.
|
|
323
|
+
// `add` never meets that branch: it always passes the pack it was given. This verb is the first caller
|
|
324
|
+
// that rewrites a company which already exists, so it is the first one that can reach it — and setting
|
|
325
|
+
// a framework would have deleted the company's context pack, and committed the deletion under a
|
|
326
|
+
// message about the framework. Found in review, driven before the fix: a store holding acme.json and
|
|
327
|
+
// acme.context.md kept only acme.json.
|
|
328
|
+
//
|
|
329
|
+
// Re-writing the pack is deliberate rather than clever. It lands in the commit's file list, git sees
|
|
330
|
+
// no change in a pack that was already stored the way this writer stores it, and the commit still
|
|
331
|
+
// carries only the profile. NOT byte-for-byte in every case: `defaultWriteProfile` trims and ends
|
|
332
|
+
// with one newline, so a pack hand-edited with blank lines around its text comes back without them.
|
|
333
|
+
// The words are untouched, which is what the arm below checks. A pack holding nothing but whitespace
|
|
334
|
+
// is still removed, which is what every other reader of this store already means by an empty pack.
|
|
335
|
+
const packPath = join(store, CONTEXT_PACK_FILE(key));
|
|
336
|
+
const contextPack = existsSync(packPath) ? readFileSync(packPath, "utf8") : "";
|
|
337
|
+
const { files } = defaultWriteProfile({
|
|
338
|
+
profileDir: store, key, profile: { ...profile, frameworkPath: resolved.path }, contextPack,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const repoRoot = resolveStoreRepoRoot({ names: ["CLEAROTRON_CUSTOMERS_DIR"], fallback: store }).root;
|
|
342
|
+
const audit = makeCommittableAudit({ auditPath: join(store, "audit.jsonl"), repoRoot });
|
|
343
|
+
const gitCommit = makeStoreCommit({ repoRoot, what: "customers" });
|
|
344
|
+
let by = "unknown";
|
|
345
|
+
try { by = userInfo().username || "unknown"; } catch { /* no passwd entry — the row still gets written */ }
|
|
346
|
+
const { commit, commitError } = commitWithAuditRow({
|
|
347
|
+
audit, gitCommit, files, by,
|
|
348
|
+
message: `company ${key} rates under ${resolved.path}${was ? ` (was ${was})` : ""}`,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
out(` ${key} now rates under ${resolved.path}${was ? ` — was ${was}` : " — it had none of its own"}.`);
|
|
352
|
+
if (commit) out(` recorded ${commit}`);
|
|
353
|
+
if (commitError) out(` WROTE THE CHANGE BUT DID NOT RECORD IT: ${commitError} — the audit line is on disk; fix the store's git state`);
|
|
354
|
+
return { written: true, store, framework: resolved, was, commit, commitError };
|
|
355
|
+
}
|
|
356
|
+
|
|
269
357
|
async function main() {
|
|
270
358
|
const [sub, ...rest] = process.argv.slice(2);
|
|
271
359
|
if (!sub || sub === "--help" || sub === "-h" || sub === "help") { console.log(USAGE); process.exit(sub ? 0 : 1); }
|
|
272
|
-
if (sub !== "add") { console.error(`brandowner: no such action "${sub}". One of: add`); process.exit(2); }
|
|
360
|
+
if (sub !== "add" && sub !== "framework") { console.error(`brandowner: no such action "${sub}". One of: add, framework`); process.exit(2); }
|
|
273
361
|
try {
|
|
362
|
+
if (sub === "framework") {
|
|
363
|
+
const r = await framework(rest);
|
|
364
|
+
if (r?.commitError) process.exit(3);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
274
367
|
// WROTE-BUT-DID-NOT-RECORD IS NOT SUCCESS. The write stands — rolling a written bundle back
|
|
275
368
|
// would lose the operator's work over a git fault they can fix — but a scripted onboarding
|
|
276
369
|
// reading exit 0 concludes the store recorded it, and the store did not. Exit 3 says both
|
package/bin/connect.mjs
CHANGED
|
@@ -45,8 +45,9 @@ 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, clientById, whatItNeeds } from "../shared/connect-clients.mjs";
|
|
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
|
+
import { isWsl } from "../shared/wsl.mjs";
|
|
50
51
|
import { defaultDenylistPath, clientDoorAddress, clientDoorPort, clientDoorState, enablePlan, applyEnablePlan, describeChange, recordConnectKey, CLIENT_DOOR_UNIT } from "../shared/client-door.mjs";
|
|
51
52
|
import { mintToken, tokenId, resolvePerson, loadGrants } from "../shared/scope.mjs";
|
|
52
53
|
import { envFrom } from "../shared/env-aliases.mjs";
|
|
@@ -205,7 +206,8 @@ function deploymentHas(env = process.env) {
|
|
|
205
206
|
return {
|
|
206
207
|
// EVERY SHAPE, RESOLVED ONCE. A row picks its own; nothing here knows a client's name.
|
|
207
208
|
stdioRoutes: Object.fromEntries(Object.keys(STDIO_SHAPES).map((shape) =>
|
|
208
|
-
[shape, stdioConnectFor(shape, { workDir: env.CLEAROTRON_WORK_DIR || null })])),
|
|
209
|
+
[shape, stdioConnectFor(shape, { workDir: env.CLEAROTRON_WORK_DIR || null, reportsDir: env.CLEAROTRON_REPORTS_DIR || null })])),
|
|
210
|
+
wsl: isWsl({ env }),
|
|
209
211
|
// WHERE THE DOOR BINDS — not an address handed to any assistant. It is the loopback address the
|
|
210
212
|
// unit listens on, and `enablePlan` needs it to write the unit. It used to be passed to the
|
|
211
213
|
// resolver as `localAddress` and served to Cowork as somewhere to connect, which is the false
|
|
@@ -555,17 +557,12 @@ async function render(offer, have, { dryRun, running, allowMove = false }) {
|
|
|
555
557
|
return 1;
|
|
556
558
|
}
|
|
557
559
|
|
|
558
|
-
if (offer.route === "disk"
|
|
559
|
-
//
|
|
560
|
-
// block is how a reader pastes four lines into a shell
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
? ` Add this to ${s.where}:`
|
|
564
|
-
: " Run this once, on this machine:");
|
|
560
|
+
if (offer.route === "disk") {
|
|
561
|
+
// THE ROW'S OWN STEPS, and nothing written here. A command and a settings block are not the same
|
|
562
|
+
// instruction — saying "run this" over a TOML block is how a reader pastes four lines into a shell —
|
|
563
|
+
// and the steps already say which each one is and where it goes.
|
|
564
|
+
printSteps(offer, null);
|
|
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(""); }
|
|
569
566
|
say(` ${offer.note}`);
|
|
570
567
|
return 0;
|
|
571
568
|
}
|
|
@@ -621,22 +618,40 @@ async function render(offer, have, { dryRun, running, allowMove = false }) {
|
|
|
621
618
|
}
|
|
622
619
|
|
|
623
620
|
say(` Address: ${offer.address}`);
|
|
624
|
-
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
621
|
+
// Printed once, stored nowhere — and only here when no step hands it over. A step whose copy carries
|
|
622
|
+
// the key prints it inside the line the reader pastes, which is the one place it is needed.
|
|
623
|
+
const keyInSteps = (offer.steps ?? []).some((s) => s.copy?.kind === "secret");
|
|
624
|
+
if (key && !keyInSteps) say(` Key: ${key}`);
|
|
625
|
+
printSteps(offer, key);
|
|
626
|
+
say("");
|
|
627
|
+
say(` ${offer.note}`);
|
|
628
|
+
return 0;
|
|
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) {
|
|
632
644
|
if (offer.steps?.length) {
|
|
633
645
|
say("");
|
|
634
646
|
say(` In ${offer.client?.name ?? "your assistant"}:`);
|
|
635
|
-
offer.steps.forEach((step, n) =>
|
|
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
|
+
});
|
|
636
654
|
}
|
|
637
|
-
say("");
|
|
638
|
-
say(` ${offer.note}`);
|
|
639
|
-
return 0;
|
|
640
655
|
}
|
|
641
656
|
|
|
642
657
|
async function main() {
|
|
@@ -654,6 +669,9 @@ async function main() {
|
|
|
654
669
|
say(" not about you.");
|
|
655
670
|
say("");
|
|
656
671
|
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");
|
|
657
675
|
say(" --list the assistants this build knows");
|
|
658
676
|
say(" --dry-run say what would change, change nothing");
|
|
659
677
|
say(" --allow-checkout-move");
|
|
@@ -663,7 +681,7 @@ async function main() {
|
|
|
663
681
|
say("");
|
|
664
682
|
return 0;
|
|
665
683
|
}
|
|
666
|
-
const known = new Set(["--client", "--list", "--dry-run", "--allow-checkout-move", "--help", "-h"]);
|
|
684
|
+
const known = new Set(["--client", "--where", "--list", "--dry-run", "--allow-checkout-move", "--help", "-h"]);
|
|
667
685
|
const unknown = argv.filter((a) => a.startsWith("--") && !known.has(a));
|
|
668
686
|
if (unknown.length) {
|
|
669
687
|
console.error(`connect: unrecognised flag(s): ${unknown.join(", ")}`);
|
|
@@ -671,6 +689,12 @@ async function main() {
|
|
|
671
689
|
process.exit(2);
|
|
672
690
|
}
|
|
673
691
|
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;
|
|
674
698
|
// F40 — reason about the RUNNING product from the units' own environment, not this CLI's env file.
|
|
675
699
|
const running = runningEnv();
|
|
676
700
|
const have = deploymentHas(running.env);
|
|
@@ -712,9 +736,30 @@ async function main() {
|
|
|
712
736
|
chosen = CONNECT_CLIENTS[Number(answer) - 1] ?? clientById(answer);
|
|
713
737
|
} finally { rl.close(); }
|
|
714
738
|
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;
|
|
715
757
|
}
|
|
716
758
|
|
|
717
|
-
|
|
759
|
+
// NAMED WITHOUT --where: the route this id had before every row took both, so a scripted
|
|
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") });
|
|
718
763
|
}
|
|
719
764
|
|
|
720
765
|
// THE DISPATCH RUNS ONLY WHEN THIS FILE IS THE COMMAND. Without the guard, importing
|