clearotron 0.2.4 → 0.3.0-beta.1
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 +13 -2
- package/CONTRIBUTING.md +1 -1
- package/INSTALL.md +62 -38
- package/bin/brandowner.mjs +18 -169
- package/bin/clearotron.mjs +3 -1
- package/bin/connect.mjs +28 -19
- package/bin/disconnect.mjs +3 -3
- package/bin/example.mjs +7 -7
- package/bin/framework-preflight.mjs +49 -0
- package/bin/grant.mjs +151 -93
- package/bin/onboard.mjs +220 -138
- package/bin/start.mjs +146 -137
- package/bin/stop.mjs +2 -2
- package/bin/update.mjs +1 -1
- package/build-info.json +2 -2
- package/docs/CLIENT-MCP.md +2 -2
- package/docs/E2E.md +12 -2
- package/docs/ONBOARDING.md +1 -1
- package/docs/PORTAL.md +14 -13
- package/docs/SECURITY.md +23 -24
- package/docs/architecture/04-configuration-reference.md +12 -5
- package/docs/architecture/05-config-governance.md +7 -7
- package/docs/architecture/07-quality-and-audit.md +1 -1
- package/docs/architecture/08-development-guide.md +5 -0
- package/docs/configuration.md +118 -0
- package/docs/decisions/0004-documentation-structure.md +2 -2
- package/docs/decisions/0006-what-the-public-repository-carries.md +2 -2
- package/driver/CHANGELOG.md +70 -0
- package/driver/ask-ledger.mjs +2 -2
- package/driver/cancel.mjs +27 -0
- package/driver/case-law-sources.mjs +3 -3
- package/driver/company-bundle.mjs +261 -0
- package/driver/compare.mjs +1 -1
- package/driver/compose-read.mjs +2 -2
- package/driver/config-inventory.mjs +2 -2
- package/driver/contract-audit.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +1 -1
- package/driver/contract-vocabulary.mjs +1 -1
- package/driver/declination-call.mjs +1 -1
- package/driver/deliver-trigger.sh +3 -3
- package/driver/dev-portal.mjs +3 -1
- package/driver/digest-queue.mjs +1 -1
- package/driver/disposition-tool.mjs +1 -1
- package/driver/doc-constants.mjs +1 -1
- package/driver/drain-posture.mjs +2 -2
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +44 -9
- package/driver/effective-scope.mjs +30 -1
- package/driver/effort-model.mjs +6 -6
- package/driver/engine/CONTRACT.md +2 -2
- package/driver/engine/anthropic-agent.mjs +11 -11
- package/driver/engine/jx-turn.mjs +1 -1
- package/driver/engine/mcp/gather-config.mjs +29 -4
- package/driver/engine/mcp/recording-server.mjs +73 -1
- package/driver/engine/openai-agent.mjs +1 -1
- package/driver/engine/probe.mjs +28 -4
- package/driver/enqueue-schema.mjs +23 -3
- package/driver/findings-model.mjs +2 -2
- package/driver/flag-snapshot.mjs +2 -2
- package/driver/floor-duty.mjs +2 -2
- package/driver/frame-diff-model.mjs +1 -1
- package/driver/framework-preflight.mjs +143 -0
- package/driver/gateway.mjs +9 -1
- package/driver/hit-list.mjs +1 -1
- package/driver/jx-lanes.mjs +1 -1
- package/driver/jx.mjs +1 -1
- package/driver/knockout-assess-record.mjs +1 -1
- package/driver/knockout-review-record.mjs +435 -0
- package/driver/order-probe.mjs +1 -1
- package/driver/outbox-backoff.mjs +2 -2
- package/driver/owner-use-check.mjs +2 -2
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +105 -8
- package/driver/pipeline.mjs +81 -30
- package/driver/plain-register.mjs +77 -3
- package/driver/portal-access.mjs +141 -74
- package/driver/portal-config-view.mjs +59 -70
- package/driver/portal-report.mjs +4 -4
- package/driver/portal-service.mjs +348 -141
- package/driver/portal-upstream.mjs +105 -17
- package/driver/predelivery-lint.mjs +43 -18
- package/driver/product-rows.mjs +1 -1
- package/driver/products.mjs +1 -1
- package/driver/profile-page.html +30 -5
- package/driver/profile-service.mjs +197 -26
- package/driver/profiles.mjs +48 -1
- package/driver/publish/index.mjs +31 -13
- package/driver/publish/knockout.mjs +9 -5
- package/driver/publish/office-record-links.mjs +189 -0
- package/driver/publish/parse.mjs +3 -3
- package/driver/publish/publish-inputs.mjs +26 -0
- package/driver/publish/render-knockout.mjs +42 -42
- package/driver/publish/render.mjs +29 -4
- package/driver/publish/report-data.mjs +2 -2
- package/driver/publish/seed-pool.mjs +1 -1
- package/driver/publish/templates/report.css +8 -8
- package/driver/publish/xlsx.mjs +49 -7
- package/driver/queue-watch-verdict.mjs +2 -2
- package/driver/recipe-service.mjs +1 -1
- package/driver/record-carry.mjs +1 -1
- package/driver/reference-score.mjs +1 -1
- package/driver/reference-strip-signatures.mjs +1 -1
- package/driver/register-availability.mjs +4 -3
- package/driver/register-count.mjs +3 -3
- package/driver/register-records.mjs +1 -1
- package/driver/repair-composers.mjs +1 -1
- package/driver/repairs.mjs +3 -3
- package/driver/replay-archive.mjs +1 -1
- package/driver/report-card-record.mjs +1 -1
- package/driver/result-noun-fields.mjs +5 -0
- package/driver/roster-verdict.mjs +48 -5
- package/driver/run-activity.mjs +1 -1
- package/driver/run-requirements.mjs +18 -5
- package/driver/runner.mjs +24 -15
- package/driver/search-policy.mjs +8 -8
- package/driver/senior-rights.mjs +1 -1
- package/driver/skills/prelim-search/delivery-contract.md +1 -1
- package/driver/skills/prelim-search/risk-framework-triage.md +10 -7
- package/driver/stages-knockout.mjs +72 -6
- package/driver/stages.mjs +10 -10
- package/driver/suite-census.json +238 -70
- package/driver/synthesis-record.mjs +2 -2
- package/driver/systemd/clearotron-client-mcp.service +3 -3
- package/driver/systemd/clearotron-deploy.service +2 -2
- package/driver/systemd/clearotron-mcp-face.service +1 -1
- package/driver/systemd/clearotron-portal.service +3 -3
- package/driver/systemd/clearotron-worker.service +5 -5
- package/driver/systemd/install-census.mjs +1 -1
- package/driver/systemd/render-units.mjs +9 -9
- package/driver/terminal-clamp.mjs +1 -1
- package/driver/trigger-cap.mjs +18 -2
- package/driver/unit-inventory.mjs +8 -8
- package/driver/usage-ledger.mjs +5 -3
- package/driver/verify-knockout.mjs +7 -7
- package/driver/verify.mjs +5 -5
- package/driver/whatif-memo-run.mjs +1 -1
- package/driver/whatif-queue.mjs +3 -3
- package/driver/whatif-worker.mjs +2 -2
- package/examples/README.md +1 -1
- package/examples/grants.example.json +25 -24
- package/mcp-server/CHANGELOG.md +10 -0
- package/mcp-server/http-server.mjs +3 -3
- package/mcp-server/key-socket.mjs +1 -1
- package/mcp-server/lib/audit-view.mjs +3 -3
- package/mcp-server/lib/brief.mjs +3 -3
- package/mcp-server/lib/driver.mjs +1 -1
- package/mcp-server/lib/events.mjs +1 -1
- package/mcp-server/lib/http-handler.mjs +2 -2
- package/mcp-server/lib/instructions.mjs +2 -2
- package/mcp-server/lib/knockout.mjs +1 -1
- package/mcp-server/lib/ops.mjs +6 -3
- package/mcp-server/lib/options.mjs +15 -4
- package/mcp-server/lib/plan.mjs +7 -6
- package/mcp-server/lib/runs.mjs +10 -0
- package/mcp-server/lib/whatif.mjs +5 -5
- package/mcp-server/package.json +1 -1
- package/mcp-server/packs/README.md +1 -1
- package/mcp-server/remote/client-mcp-apikey.service +1 -1
- package/mcp-server/remote/client-mcp.service +2 -2
- package/mcp-server/remote/trademark-artifacts-http.service +1 -1
- package/mcp-server/server.mjs +38 -24
- package/package.json +2 -2
- package/portal-ui/dist/assets/{index-KFAHMgdT.js → index-CWTHP0sH.js} +3471 -1901
- package/portal-ui/dist/assets/{index-1ziUJX1E.css → index-KpytsmNH.css} +79 -26
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/lane-probe.mjs +9 -3
- package/providers/jx-subclass/lookup.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/oauth-mcp-bridge/systemd/courtlistener-mcp.service +1 -1
- package/scripts/citation-drift-report.mjs +1 -1
- package/scripts/citation-line-check.mjs +2 -2
- package/scripts/drive-env-check.mjs +1 -1
- package/scripts/e2e.mjs +5 -5
- package/scripts/env-audit.mjs +13 -1
- package/scripts/headless-page.mjs +5 -5
- package/scripts/live-surface-check.mjs +26 -2
- package/scripts/mint-names-in-force.mjs +19 -5
- package/scripts/mint-reference-strip-backlog.mjs +1 -1
- package/scripts/mint-suite-census.mjs +37 -10
- package/scripts/pack-publishable.mjs +1 -1
- package/scripts/preinstall-node-check.mjs +1 -1
- package/scripts/release-await-cut.mjs +3 -3
- package/scripts/release-cut-decision.mjs +1 -1
- package/scripts/release-dist-tag.mjs +1 -1
- package/scripts/release-install-check.mjs +1 -1
- package/scripts/release-notes-lint.mjs +1 -1
- package/scripts/release-publish-guard.mjs +1 -1
- package/scripts/release-version-pr-checks.mjs +2 -2
- package/scripts/release-version.mjs +61 -5
- package/scripts/render-brand-banner.mjs +1 -1
- package/scripts/render-check.mjs +2 -2
- package/scripts/repo-writes.mjs +1 -1
- package/scripts/report-frame-check.mjs +1 -1
- package/scripts/report-screenshot.mjs +2 -2
- package/scripts/retire-bare-refs.mjs +1 -1
- package/scripts/revisit-render-check.mjs +1 -1
- package/scripts/score.mjs +1 -1
- package/scripts/strip-titles-and-attributions.mjs +389 -0
- package/scripts/strip-tracker-citations.mjs +122 -5
- package/scripts/test-run.mjs +4 -4
- package/scripts/third-party-notices.mjs +1 -1
- package/scripts/verify-publishable.mjs +1 -1
- package/shared/access-audience.mjs +2 -2
- package/shared/anon-overlay.mjs +1 -1
- package/shared/brand.mjs +15 -1
- package/shared/bundle-freshness.mjs +1 -1
- package/shared/bundle-rebuild.mjs +1 -1
- package/shared/checkout-move.mjs +2 -2
- package/shared/client-door.mjs +8 -8
- package/shared/connect-clients.mjs +7 -7
- package/shared/connector-signin-probe.mjs +1 -1
- package/shared/env-aliases.mjs +1 -1
- package/shared/env-local.mjs +5 -5
- package/shared/grants-edit.mjs +76 -0
- package/shared/install-auth.mjs +1 -1
- package/shared/listen.mjs +3 -3
- package/shared/mcp-challenge.mjs +1 -1
- package/shared/names-in-force.mjs +2 -1
- package/shared/onboarding-store.mjs +19 -2
- package/shared/reference-guard-classes.mjs +44 -2
- package/shared/register-selection.mjs +1 -1
- package/shared/scope.mjs +223 -56
- package/shared/secret-file.mjs +1 -1
- package/shared/server-units.mjs +1 -1
- package/shared/staff-domain.mjs +45 -78
- package/shared/summary-blocks.mjs +2 -2
- package/shared/systemd-failure.mjs +3 -3
- package/shared/tracked-files.mjs +1 -1
- package/shared/trigger-lane.mjs +1 -1
- package/shared/tty-style.mjs +1 -1
- package/shared/usage-block.mjs +1 -1
- package/shared/vacuous-pass.mjs +1 -1
- package/shared/verb-shim.mjs +1 -1
package/.env.example
CHANGED
|
@@ -356,7 +356,7 @@ SIGNA_FIXTURES_DIR= # canned signa payloads — the $0 seam for that
|
|
|
356
356
|
# sets one. This name has no default — unset, the lane refuses. Moving it there would spend that
|
|
357
357
|
# header's credibility, which every other row in that file depends on, to buy a tidier taxonomy.
|
|
358
358
|
# `.env.example` promises "what you decide at install", and a must-set variable fits deciding-at-install
|
|
359
|
-
# better than it fits never-set-one.
|
|
359
|
+
# better than it fits never-set-one. Ruling, 2026-08-30.
|
|
360
360
|
#
|
|
361
361
|
# GIVING THE LANE A DEFAULT WAS CONSIDERED AND REFUSED: refuse-by-name-when-unset is the honest design —
|
|
362
362
|
# the same family as the pool root, where an unset value THROWS rather than quietly resolving somewhere
|
|
@@ -525,7 +525,7 @@ CLEAROTRON_WHATSAPP_OPERATOR_COPY=
|
|
|
525
525
|
CLEAROTRON_TEST_FIXTURE_PROFILES=
|
|
526
526
|
|
|
527
527
|
# `1` makes the profile loader return the BUNDLED demo account (demo-brand-owner, marked `demoData`).
|
|
528
|
-
# A fresh install resolves `generic` alone —
|
|
528
|
+
# A fresh install resolves `generic` alone — ruling 2026-09-08, so nobody has to clean demo
|
|
529
529
|
# material out of an environment they just made — and the demo brings its own account when it runs.
|
|
530
530
|
# Set by `clearotron demo`, by `clearotron start --demo`, and by scripts/test-run.mjs.
|
|
531
531
|
#
|
|
@@ -535,3 +535,14 @@ CLEAROTRON_TEST_FIXTURE_PROFILES=
|
|
|
535
535
|
# Read by driver/profiles.mjs.
|
|
536
536
|
# effect: harness
|
|
537
537
|
CLEAROTRON_DEMO_PROFILES=
|
|
538
|
+
|
|
539
|
+
# ── The organisation setup names ──────────────────────────────────────────────────────────────────
|
|
540
|
+
#
|
|
541
|
+
# Your organisation's name. `npm run setup` asks for it directly after the sign-in address and writes it
|
|
542
|
+
# here, quoted, so a name holding a `#` survives. The first `clearotron start` files it as the first
|
|
543
|
+
# organisation in the grants file (CLEAROTRON_ACCESS_FILE) when that file holds none; from then on the
|
|
544
|
+
# grants file holds the name, renaming is an edit there, and this is not read. Unset or empty, no
|
|
545
|
+
# organisation is invented. `clearotron start --organisation <name>` supplies it for one start, and a
|
|
546
|
+
# demo never reads it. Read by bin/start.mjs.
|
|
547
|
+
# effect: deployment
|
|
548
|
+
CLEAROTRON_ORGANISATION_NAME=
|
package/CONTRIBUTING.md
CHANGED
|
@@ -80,7 +80,7 @@ product and will not be published. Its absence weakens nothing you can run — t
|
|
|
80
80
|
the machinery, and the scenario suite covers the answers. The validation ladder, from the $0 offline
|
|
81
81
|
suite to a paid live run, is [docs/E2E.md](docs/E2E.md).
|
|
82
82
|
|
|
83
|
-
**Some comments cite issues you cannot open** —
|
|
83
|
+
**Some comments cite issues you cannot open** — `` and the like, against the private tracker this
|
|
84
84
|
repository was cut from. They are being removed rather than preserved.
|
|
85
85
|
[ADR-0005](docs/decisions/0005-comments-carry-reasoning.md) is the ruling and states what a new comment
|
|
86
86
|
has to meet; read it there rather than from a summary here. New work cites this repository's issues
|
package/INSTALL.md
CHANGED
|
@@ -168,7 +168,7 @@ you want the second one** — the first assumes access to the repository, which
|
|
|
168
168
|
|
|
169
169
|
**This is the development tree, and it is for contributors.** If you want to run a clearance, take one
|
|
170
170
|
of the two routes above instead. A clone is not a smaller version of the package — it carries the
|
|
171
|
-
fixtures and sample
|
|
171
|
+
fixtures and sample companies the project develops against, and those load as real records. They
|
|
172
172
|
are not yours and they are not in the package, which excludes them deliberately.
|
|
173
173
|
|
|
174
174
|
```
|
|
@@ -381,7 +381,7 @@ CLARIVATE_API_KEY=... # the credential for the provider named
|
|
|
381
381
|
# EUIPO_CLIENT_ID=... / EUIPO_CLIENT_SECRET=...
|
|
382
382
|
EUIPO_ENVIRONMENT=production # REQUIRED with euipo — sandbox | production are SEPARATE
|
|
383
383
|
# deployments over different corpora. There is no default:
|
|
384
|
-
# unset, the adapter refuses by name
|
|
384
|
+
# unset, the adapter refuses by name.
|
|
385
385
|
|
|
386
386
|
# ── Web research — REQUIRED for all four products (see §1) ─────────────
|
|
387
387
|
PERPLEXITY_API_KEY=...
|
|
@@ -445,33 +445,36 @@ staleness thresholds that decide when an index is too old to trust are in
|
|
|
445
445
|
|
|
446
446
|
### The four things, and what contains what
|
|
447
447
|
|
|
448
|
-
Read this before the rest of the section.
|
|
449
|
-
|
|
450
|
-
which names nothing here at all. That confusion is real and it belongs to the documentation, not to
|
|
451
|
-
the reader.
|
|
448
|
+
Read this before the rest of the section. Everything is one tree, and a person is given access to
|
|
449
|
+
points on it.
|
|
452
450
|
|
|
453
|
-
| What it is |
|
|
451
|
+
| What it is | What the product calls it | Where it lives | What creates it |
|
|
454
452
|
|---|---|---|---|
|
|
455
|
-
|
|
|
456
|
-
| A
|
|
457
|
-
| One engagement under that
|
|
458
|
-
|
|
|
459
|
-
|
|
460
|
-
Nesting, in one line: **
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
453
|
+
| A group of people and the companies they clear for — a firm, a brand team, one customer of 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` |
|
|
454
|
+
| 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 customer store, keyed by an account key, and listed under exactly one organisation | the portal's `+ New company`, or `npx clearotron brandowner add <key>` |
|
|
455
|
+
| One engagement under that company — its classes, jurisdictions, platforms | **project** | inside that company's bundle | `npx clearotron project add` |
|
|
456
|
+
| 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` |
|
|
457
|
+
|
|
458
|
+
Nesting, in one line: **an organisation contains companies; a company contains projects; a person is
|
|
459
|
+
given access to points on that tree — the whole install, an organisation, or one company — and sees
|
|
460
|
+
everything below them.**
|
|
461
|
+
|
|
462
|
+
Three consequences worth stating, because each has surprised someone:
|
|
463
|
+
|
|
464
|
+
- **A company belongs to exactly one organisation.** The guest list is refused at load if a company is
|
|
465
|
+
listed under two; another organisation's people are given access to it where it lives.
|
|
466
|
+
- **A person holds two switches, and nothing else is a permission.** **Run clearances** starts and
|
|
467
|
+
stops them; **Manage** adds people, adds companies and changes settings. Viewing is not a permission:
|
|
468
|
+
access is viewing. A person with no entry under `people` sees what their access covers and starts
|
|
469
|
+
nothing.
|
|
467
470
|
- **A key grants no reach of its own.** `npx clearotron key issue` mints the identity a person's assistant
|
|
468
|
-
presents; what that identity may see is decided by
|
|
469
|
-
|
|
471
|
+
presents; what that identity may see and do is decided by the guest list at the moment of each call.
|
|
472
|
+
Enrol first, issue second — a key for someone with no access reaches nothing, and is not an error
|
|
473
|
+
anywhere.
|
|
470
474
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
onto it is a product decision that has not been taken.
|
|
475
|
+
**The file keeps its words.** The screens say organisation, company and person; `grants.json`, the wire
|
|
476
|
+
and the command line keep `tenant` and `account`, and the CLI verb stays `brandowner`. Moving them would
|
|
477
|
+
break every file and script written against them.
|
|
475
478
|
|
|
476
479
|
|
|
477
480
|
A clearance run is shaped by a **customer profile** — a small JSON file that declares that customer's
|
|
@@ -741,10 +744,12 @@ You sign in as `<your-username>@localhost` unless you say otherwise:
|
|
|
741
744
|
npx clearotron start --user you@example.com
|
|
742
745
|
```
|
|
743
746
|
|
|
744
|
-
The address is written to `.env`, so it is asked for once. It is also the
|
|
745
|
-
install:
|
|
746
|
-
|
|
747
|
-
|
|
747
|
+
The address is written to `.env`, so it is asked for once. It is also the first person on this
|
|
748
|
+
install: the first start writes it into the grants file (`CLEAROTRON_ACCESS_FILE`, §8) with access to
|
|
749
|
+
everything and both permissions, Run clearances and Manage. Setup asks for your organisation's name
|
|
750
|
+
directly after the address, and the same start files it there as your first organisation. The address
|
|
751
|
+
admits nobody else at its domain; enrolling anyone else is that same file, exactly as on a hosted
|
|
752
|
+
instance.
|
|
748
753
|
|
|
749
754
|
**No authentication is switched off to make this work, and none can be.** Both doors prove who the
|
|
750
755
|
caller is — the portal by passphrase and a signed session cookie, the engine door by a mandatory
|
|
@@ -888,13 +893,13 @@ set:
|
|
|
888
893
|
|
|
889
894
|
### The client connector's ingress is not integrator-supplied
|
|
890
895
|
|
|
891
|
-
The client connector is the door a
|
|
896
|
+
The client connector is the door a company's assistant talks to, and it
|
|
892
897
|
is **part of the product** rather than something a deployment invents. A reader who reaches the
|
|
893
898
|
Use-your-AI page and finds Connect buttons that do nothing has been failed by the install, not by
|
|
894
899
|
their own integration work.
|
|
895
900
|
|
|
896
|
-
**Two shapes, and the line between them is what your assistant can do — not where it runs.**
|
|
897
|
-
|
|
901
|
+
**Two shapes, and the line between them is what your assistant can do — not where it runs.** Ruling
|
|
902
|
+
2026-09-03, on the vendor's own documented behaviour:
|
|
898
903
|
|
|
899
904
|
1. **An assistant that can launch a local process** — Claude Code, Claude Desktop's local config,
|
|
900
905
|
Codex CLI, an agent that runs commands. It spawns the server over stdio. No address, no key, no
|
|
@@ -1005,11 +1010,23 @@ infer it from an install step. The operational side — issuing and rotating gra
|
|
|
1005
1010
|
What belongs here is only what you set at install time.
|
|
1006
1011
|
|
|
1007
1012
|
**The guest list.** `CLEAROTRON_ACCESS_FILE` turns account scoping on for **every face at once** — the
|
|
1008
|
-
portal, the MCP read face, and the client connector. `npx clearotron start` (§6) writes
|
|
1009
|
-
|
|
1013
|
+
portal, the MCP read face, and the client connector. `npx clearotron start` (§6) writes one into its
|
|
1014
|
+
state directory the first time it runs: you, with access to everything, your organisation if setup was
|
|
1015
|
+
told its name, and nobody else yet.
|
|
1010
1016
|
[examples/grants.example.json](examples/grants.example.json) is a runnable guest list over the demo
|
|
1011
1017
|
clients.
|
|
1012
1018
|
|
|
1019
|
+
**Giving someone access.** `npx clearotron grant add` writes the same file the portal's People page
|
|
1020
|
+
writes:
|
|
1021
|
+
|
|
1022
|
+
```
|
|
1023
|
+
npx clearotron grant add <email> --tenant <organisation> --accounts <key,key|*> [--run] [--manage]
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
`--accounts '*'` is the whole organisation, including companies filed under it later. `--run` lets the
|
|
1027
|
+
person start and stop clearances; `--manage` lets them add people and companies and change settings.
|
|
1028
|
+
With neither, they can see what their access covers and start nothing.
|
|
1029
|
+
|
|
1013
1030
|
**Keys for people.** `npx clearotron grant` enrols someone; it decides what they may see and issues
|
|
1014
1031
|
nothing. The key their assistant actually presents comes from a different verb:
|
|
1015
1032
|
|
|
@@ -1032,10 +1049,17 @@ Said plainly rather than dressed as a verb, because the distinction costs real t
|
|
|
1032
1049
|
key issue` mints ACCOUNT keys only**, and `npx clearotron grant` mints nothing at all. This page
|
|
1033
1050
|
previously sent readers to `grant` for an ops token, which is why the sentence is now this long.
|
|
1034
1051
|
|
|
1035
|
-
**When you have to re-mint one.**
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1052
|
+
**When you have to re-mint one.** Not on the ordinary path any more, and this paragraph used to say
|
|
1053
|
+
otherwise. The portal takes its ops credential afresh at the start of each call, capped to the company
|
|
1054
|
+
roster as it stands, so a company created after the portal started can run a clearance and stop one
|
|
1055
|
+
without anybody re-minting anything.
|
|
1056
|
+
|
|
1057
|
+
The pinned credential minted at startup is still the fallback, and it is where this matters. If the
|
|
1058
|
+
signing secret cannot be read or the company store cannot be listed, the portal uses that one rather than
|
|
1059
|
+
widening the cap to get the call through — so its account list is frozen at the moment it was minted, and
|
|
1060
|
+
a company created since is refused at the door with `your grant [...] does not include account "..."`.
|
|
1061
|
+
The portal says so in its log when it happens. That is when you re-mint by hand, with the new list, and
|
|
1062
|
+
update the environment.
|
|
1039
1063
|
|
|
1040
1064
|
### Putting a surface behind your identity provider
|
|
1041
1065
|
|
package/bin/brandowner.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
//
|
|
13
13
|
// ── THE FRAMEWORK IS SET HERE, AND THAT NEEDED A NEW DOOR ─────────────────────────────────────────
|
|
14
14
|
//
|
|
15
|
-
//
|
|
15
|
+
// Ruling, 2026-08-29: the risk framework stays MANDATORY at onboarding, with a DEFAULT BACKUP so
|
|
16
16
|
// onboarding is never blocked. This sets `frameworkPath` ALWAYS — the client's own when supplied, the
|
|
17
17
|
// Generic default otherwise — and says out loud which one it used.
|
|
18
18
|
//
|
|
@@ -59,180 +59,22 @@ import { assertProfileKey, profileStoreResolution, CONTEXT_PACK_FILE, validatePr
|
|
|
59
59
|
import { DEFAULT_FRAMEWORK, loadFrameworkManifest } from "../driver/framework.mjs";
|
|
60
60
|
import { defaultWriteProfile } from "../driver/profile-service.mjs";
|
|
61
61
|
import { config } from "../driver/driver.config.mjs";
|
|
62
|
+
import { preflightFramework, formatPreflight } from "../driver/framework-preflight.mjs";
|
|
62
63
|
import { makeCommittableAudit, commitWithAuditRow, makeStoreCommit, resolveStoreRepoRoot } from "../shared/store-in-repo.mjs";
|
|
63
64
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
64
65
|
// Shared with `project add`, which asks an identical question of the same variable — see the
|
|
65
66
|
// module header for why this is not two copies.
|
|
66
67
|
import { Refusal, storeForAdd } from "../shared/onboarding-store.mjs";
|
|
67
68
|
|
|
68
|
-
// ──
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* Broken means someone TRIED to select one and it does not resolve, and falling back there would rate a
|
|
76
|
-
* client's matters under a framework nobody chose while the output said everything was fine.
|
|
77
|
-
*
|
|
78
|
-
* The profile validator checks the SHAPE of this string only (`skills/prelim-search/<file>.md`, no
|
|
79
|
-
* escape) and never whether the file is there — so a shape-valid path to a document that does not exist
|
|
80
|
-
* validates cleanly and fails at rating time, which is the wrong place to find out.
|
|
81
|
-
*/
|
|
82
|
-
export function resolveFramework(requested, {
|
|
83
|
-
resolveSkill = (rel) => config.resolveSkillPath(rel),
|
|
84
|
-
loadManifest = loadFrameworkManifest,
|
|
85
|
-
} = {}) {
|
|
86
|
-
if (requested == null || String(requested).trim() === "")
|
|
87
|
-
return { path: DEFAULT_FRAMEWORK, source: "default" };
|
|
88
|
-
|
|
89
|
-
const path = String(requested).trim();
|
|
90
|
-
// Shape first, and by the profile validator's own rule rather than a second copy of it: a path that
|
|
91
|
-
// would be refused at load must be refused here, in the same words, before anything is written.
|
|
92
|
-
if (!/^skills\/prelim-search\/[^/]+\.md$/.test(path) || path.includes(".."))
|
|
93
|
-
throw new Refusal(
|
|
94
|
-
`--framework must name a document of the form "skills/prelim-search/<file>.md" (got ${JSON.stringify(path)}). `
|
|
95
|
-
+ `A profile selects a SHIPPED framework, never an arbitrary path.`);
|
|
96
|
-
|
|
97
|
-
// RESOLVED THE WAY THE RATING STAGE RESOLVES IT, never by joining the repo root. `skills/...` paths
|
|
98
|
-
// are relative to the DRIVER's skills directory, and a deployment may serve them from a doctrine
|
|
99
|
-
// overlay (CLEAROTRON_INSTRUCTIONS_DIR) instead. Joining the repo root would have looked in a
|
|
100
|
-
// directory that does not exist, refused every valid framework on every install, and — worse in the
|
|
101
|
-
// other direction — been blind to the overlay a deployment actually reads.
|
|
102
|
-
const deck = resolveSkill(path);
|
|
103
|
-
if (!existsSync(deck))
|
|
104
|
-
throw new Refusal(
|
|
105
|
-
`--framework names ${path}, and there is no such document on this install (resolved to ${deck}). `
|
|
106
|
-
+ `Refusing rather than rating this brand owner's matters under the Generic default: a framework `
|
|
107
|
-
+ `somebody chose and that does not resolve is a mistake, not an absence.`);
|
|
108
|
-
|
|
109
|
-
// The manifest is DERIVED from the deck path, never a separate knob — and it is what the validators,
|
|
110
|
-
// the renderer and the profile page read to know the framework's band vocabulary. A deck whose
|
|
111
|
-
// manifest will not load produces a customer whose page cannot state their own ladder.
|
|
112
|
-
//
|
|
113
|
-
// ASKED, NOT RE-CHECKED. An `existsSync` on the manifest here would be a second opinion about the
|
|
114
|
-
// same fact: `loadFrameworkManifest` already refuses a missing sidecar by name
|
|
115
|
-
// (`framework_manifest_missing:<path>`) and is the exact read the rating stage makes. Two checks
|
|
116
|
-
// means two messages to keep in step, and the one this file could write would be the one that goes
|
|
117
|
-
// stale.
|
|
118
|
-
let manifest;
|
|
119
|
-
try {
|
|
120
|
-
manifest = loadManifest(resolveSkill, path);
|
|
121
|
-
} catch (e) {
|
|
122
|
-
throw new Refusal(
|
|
123
|
-
`--framework names ${path}, and the framework will not load: ${String(e?.message ?? e)}. `
|
|
124
|
-
+ `Refusing rather than falling back: a framework somebody chose and that does not resolve is a `
|
|
125
|
-
+ `mistake, not an absence.`);
|
|
126
|
-
}
|
|
127
|
-
return { path, source: "supplied", manifest };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ── the profile this writes ────────────────────────────────────────────────────────────────────────
|
|
131
|
-
/**
|
|
132
|
-
* Which marketplaces this brand owner's searches cover — supplied, or the Generic default, SAID OUT LOUD.
|
|
133
|
-
*
|
|
134
|
-
* A customer bundle is a COMPLETE document in this design, not an overlay on generic: every shipped
|
|
135
|
-
* profile carries its own `platforms`, and the loader requires a non-empty array on every file. The
|
|
136
|
-
* command had no way to supply one and set none, so every bundle it wrote failed to load on this field
|
|
137
|
-
* as well as on the `key` field above — two independent invalidities, and onboarding could not produce
|
|
138
|
-
* a loadable brand owner at all.
|
|
139
|
-
*
|
|
140
|
-
* Defaulting rather than refusing, and naming it, is this command's own established idiom: the same
|
|
141
|
-
* ruling governs the framework one function down. Which marketplaces a client's clearance searches is
|
|
142
|
-
* not a detail to decide silently, so an operator who supplies nothing is TOLD what they got and can
|
|
143
|
-
* refine it in the portal.
|
|
144
|
-
*/
|
|
145
|
-
export function resolvePlatforms(supplied, roster) {
|
|
146
|
-
if (supplied?.length) return { platforms: supplied, source: "supplied" };
|
|
147
|
-
const house = roster?.get?.("generic")?.platforms ?? [];
|
|
148
|
-
if (!house.length)
|
|
149
|
-
throw new Refusal(
|
|
150
|
-
"no --platforms was given and the Generic default carries none, so there is nothing to onboard this "
|
|
151
|
-
+ "brand owner with. Pass --platforms, or repair the generic profile in the store.");
|
|
152
|
-
return { platforms: [...house], source: "house default" };
|
|
153
|
-
}
|
|
69
|
+
// ── what a valid company bundle is ─────────────────────────────────────────────────────────────────
|
|
70
|
+
// MOVED TO driver/company-bundle.mjs when the browser became the second door. Re-exported here because
|
|
71
|
+
// this file was the only home they ever had and callers import them from it; the definition is now in
|
|
72
|
+
// one place and both doors ask it, rather than two doors agreeing by inspection.
|
|
73
|
+
import {
|
|
74
|
+
resolveFramework, resolvePlatforms, buildProfile, rosterAsItStands, assertRosterAccepts,
|
|
75
|
+
} from "../driver/company-bundle.mjs";
|
|
154
76
|
|
|
155
|
-
export
|
|
156
|
-
// NO `key` IN THE DOCUMENT. The loader derives it from the FILENAME and injects it — readProfilesLayer
|
|
157
|
-
// composes `{ key, ...p }` — so a `key` written here is redundant on the way in and fatal on the way
|
|
158
|
-
// out: it is not in KNOWN_PROFILE_KEYS, and the deny-unknown-key gate hard-fails the whole roster over
|
|
159
|
-
// it. Every bundle this command wrote carried one, so the first successful onboarding made the store
|
|
160
|
-
// unloadable and the next command to read profiles threw. The parameter stays — the filename and the
|
|
161
|
-
// roster checks are addressed by key — it simply does not travel into the file.
|
|
162
|
-
const profile = { name, platforms };
|
|
163
|
-
if (domains?.length) profile.matchDomains = domains;
|
|
164
|
-
if (industry) profile.industry = industry;
|
|
165
|
-
// ALWAYS SET, per the ruling. `frameworkFor` would fall back to the same value if this were absent —
|
|
166
|
-
// but "the tool sets it" is the point of the setup, and an explicit selection is what makes the
|
|
167
|
-
// receipt below mean anything.
|
|
168
|
-
profile.frameworkPath = framework.path;
|
|
169
|
-
return profile;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ── the proposed roster, validated whole ───────────────────────────────────────────────────────────
|
|
173
|
-
/**
|
|
174
|
-
* Validate the candidate the way the deployment will read it: as one member of the roster, not alone.
|
|
175
|
-
*
|
|
176
|
-
* `loadProfiles` composes every profile in the store and throws on the FIRST cross-profile conflict it
|
|
177
|
-
* finds — a domain claimed twice, most of all. That throw is not scoped to the offending file: it stops
|
|
178
|
-
* the roster loading at all. So the only honest check is over the proposed state, which is what
|
|
179
|
-
* bin/grant.mjs does with `accessView` for exactly the same reason.
|
|
180
|
-
*/
|
|
181
|
-
/**
|
|
182
|
-
* The roster as it stands, where AN EMPTY STORE IS AN EMPTY ROSTER — F42.
|
|
183
|
-
*
|
|
184
|
-
* `loadProfiles` with an explicit `dir` reads that directory ALONE and refuses a store with no
|
|
185
|
-
* `generic.json`, which is right for what that refusal is for: `generic` is the universal fallback every
|
|
186
|
-
* unprofiled job resolves to, and a RUN against a store without it would silently reprofile a client.
|
|
187
|
-
* The explicit-dir form deliberately has no fall-through, because the fixtures that build a roster
|
|
188
|
-
* assert on precisely that roster and layering the bundled set underneath would widen three set-level
|
|
189
|
-
* guards until none still tested its own name.
|
|
190
|
-
*
|
|
191
|
-
* BUT THIS COMMAND IS NOT RUNNING A CLEARANCE. It reads the store to answer one question — does this key
|
|
192
|
-
* or one of its domains already exist — and on a fresh install the honest answer is "no, there is
|
|
193
|
-
* nothing here yet". Instead it stack-traced on the FIRST day-one command a new operator types, with a
|
|
194
|
-
* refusal about a file they had never heard of and did not need: the runtime resolves `generic` from the
|
|
195
|
-
* product's bundled set through the OVERLAY path, so a deployment store never needs its own copy.
|
|
196
|
-
*
|
|
197
|
-
* So the missing-generic refusal is caught BY NAME and answered as the empty roster it describes. Caught
|
|
198
|
-
* by name rather than broadly, because every other thing that loader throws — an overlapping domain, an
|
|
199
|
-
* unknown key, an unreadable store — is a real refusal this command must still relay.
|
|
200
|
-
*/
|
|
201
|
-
export function rosterAsItStands(store, loadProfiles) {
|
|
202
|
-
try { return loadProfiles({ dir: store, force: true }); }
|
|
203
|
-
catch (e) {
|
|
204
|
-
if (/generic\.json is REQUIRED/.test(String(e?.message ?? ""))) return new Map();
|
|
205
|
-
throw e;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export function assertRosterAccepts({ store, key, profile, loadProfiles }) {
|
|
210
|
-
// THE LOADER'S OWN VALIDATOR, over the CANDIDATE — the discipline `project add` already applies to an
|
|
211
|
-
// overlay, in the same words, for the same reason: whatever the tree would refuse at load is refused
|
|
212
|
-
// here, before anything is written. Everything below this line reads the store AS IT STANDS, which is
|
|
213
|
-
// why none of it could ever see a bad field in the file about to be added: the candidate never met the
|
|
214
|
-
// loader until the next command did, and by then the write had landed.
|
|
215
|
-
const v = validateProfileEdit(key, profile);
|
|
216
|
-
if (!v.ok)
|
|
217
|
-
throw new Refusal(`the brand owner bundle is not valid, so nothing was written:\n ${v.errors.join("\n ")}`);
|
|
218
|
-
|
|
219
|
-
const existing = rosterAsItStands(store, loadProfiles);
|
|
220
|
-
if (existing.has(key))
|
|
221
|
-
throw new Refusal(
|
|
222
|
-
`a brand owner "${key}" already exists in ${store}. This command creates; it does not overwrite an `
|
|
223
|
-
+ `existing bundle. Edit it in the portal, or remove the file deliberately first.`);
|
|
224
|
-
|
|
225
|
-
for (const d of profile.matchDomains ?? []) {
|
|
226
|
-
const dl = String(d).toLowerCase();
|
|
227
|
-
for (const [otherKey, other] of existing) {
|
|
228
|
-
if ((other.matchDomains ?? []).some((o) => String(o).toLowerCase() === dl))
|
|
229
|
-
throw new Refusal(
|
|
230
|
-
`domain "${dl}" is already claimed by the brand owner "${otherKey}". Two owners claiming one `
|
|
231
|
-
+ `domain makes the WHOLE roster refuse to load on the next start — not just this bundle — so `
|
|
232
|
-
+ `nothing has been written.`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
77
|
+
export { resolveFramework, resolvePlatforms, buildProfile, rosterAsItStands, assertRosterAccepts };
|
|
236
78
|
|
|
237
79
|
|
|
238
80
|
// ── argument parsing ───────────────────────────────────────────────────────────────────────────────
|
|
@@ -338,6 +180,13 @@ export async function add(argv, {
|
|
|
338
180
|
if (contextPack) out(`would create ${join(store, CONTEXT_PACK_FILE(args.key))}`);
|
|
339
181
|
out(frameworkLine);
|
|
340
182
|
out(platformsLine);
|
|
183
|
+
// THE SAME REPORT `clearotron framework` PRINTS, from the same module. A dry run already proved the
|
|
184
|
+
// framework LOADS; that is a smaller question than the one the operator has, because a deck whose
|
|
185
|
+
// manifest parses can still name bands the deck never defines, and the profile screen answers that by
|
|
186
|
+
// quietly omitting the box. Two checks of one property drift; this is one check with two doors.
|
|
187
|
+
out("");
|
|
188
|
+
out(formatPreflight(preflightFramework(framework.path)));
|
|
189
|
+
out("");
|
|
341
190
|
out(`nothing was written (--dry-run)`);
|
|
342
191
|
return { written: false, store, profile, framework };
|
|
343
192
|
}
|
|
@@ -407,7 +256,7 @@ export async function add(argv, {
|
|
|
407
256
|
if (gap.uncovered.length) {
|
|
408
257
|
out("");
|
|
409
258
|
out(` ⚠ NOT YET STARTABLE — ${triggerCapWarning(gap)}`);
|
|
410
|
-
out(` The portal will
|
|
259
|
+
out(` The portal will still start a clearance for ${args.key}: it re-takes its credential on each call. This matters when it cannot — then it uses the token above and the door refuses.`);
|
|
411
260
|
}
|
|
412
261
|
}
|
|
413
262
|
} catch (e) {
|
package/bin/clearotron.mjs
CHANGED
|
@@ -58,6 +58,7 @@ export const VERBS = {
|
|
|
58
58
|
grant: ["bin/grant.mjs"],
|
|
59
59
|
key: ["bin/key.mjs"],
|
|
60
60
|
brandowner: ["bin/brandowner.mjs"],
|
|
61
|
+
framework: ["bin/framework-preflight.mjs"],
|
|
61
62
|
project: ["bin/project.mjs"],
|
|
62
63
|
passphrase: ["bin/passphrase.mjs"],
|
|
63
64
|
sync: ["bin/uspto-sync.mjs"],
|
|
@@ -79,6 +80,7 @@ export const SUMMARY = {
|
|
|
79
80
|
grant: "enrol a client, or list who may see what",
|
|
80
81
|
key: "issue the key a person's own assistant presents — after you have enrolled them with `grant`",
|
|
81
82
|
brandowner: "onboard a brand owner — create its bundle and set the risk framework its matters are rated under",
|
|
83
|
+
framework: "check a risk framework deck and its manifest before a matter is rated under it. Writes nothing",
|
|
82
84
|
project: "add an engagement under a brand owner — the classes, jurisdictions and platforms it searches",
|
|
83
85
|
passphrase: "report or RESET the portal's local sign-in — the recovery for a lost passphrase",
|
|
84
86
|
sync: "build or update the free US register index (a large download, and hours of ingest)",
|
|
@@ -202,7 +204,7 @@ const [verb, ...rest] = process.argv.slice(2);
|
|
|
202
204
|
process.exit(code ?? 0);
|
|
203
205
|
});
|
|
204
206
|
|
|
205
|
-
// ── A SIGNAL MUST REACH THE CHILD, BECAUSE THIS IS THE ONLY PID A READER CAN SEE
|
|
207
|
+
// ── A SIGNAL MUST REACH THE CHILD, BECAUSE THIS IS THE ONLY PID A READER CAN SEE ─────────────────────
|
|
206
208
|
//
|
|
207
209
|
// The header above says this file "spawns and forwards, and does not look at what it is forwarding".
|
|
208
210
|
// That was written about ARGUMENTS and read as though it covered signals, which it did not.
|
package/bin/connect.mjs
CHANGED
|
@@ -48,13 +48,13 @@ import { createServer } from "node:net";
|
|
|
48
48
|
import { CONNECT_CLIENTS, clientById, whatItNeeds } from "../shared/connect-clients.mjs";
|
|
49
49
|
import { stdioConnectFor, STDIO_SHAPES } from "../shared/stdio-connect.mjs";
|
|
50
50
|
import { defaultDenylistPath, clientDoorAddress, clientDoorPort, clientDoorState, enablePlan, applyEnablePlan, describeChange, recordConnectKey, CLIENT_DOOR_UNIT } from "../shared/client-door.mjs";
|
|
51
|
-
import { mintToken, tokenId,
|
|
51
|
+
import { mintToken, tokenId, resolvePerson, loadGrants } from "../shared/scope.mjs";
|
|
52
52
|
import { envFrom } from "../shared/env-aliases.mjs";
|
|
53
53
|
import { atomicWrite } from "../driver/progress.mjs";
|
|
54
54
|
// — F40. SERVER_INSTALL_SET is what `bin/start.mjs` re-exports as
|
|
55
55
|
// BACKGROUND_UNITS; taken from shared/ so this verb does not reach into another bin/ entry point.
|
|
56
56
|
import { SERVER_INSTALL_SET, unitHealthVerdict } from "../shared/server-units.mjs";
|
|
57
|
-
import { checkoutMove, movePosture, describeMove, describeConflict } from "../shared/checkout-move.mjs";
|
|
57
|
+
import { checkoutMove, movePosture, describeMove, describeConflict } from "../shared/checkout-move.mjs";
|
|
58
58
|
import { unitEnvironment, unitValue, couldNotDetermine } from "../driver/unit-environment.mjs";
|
|
59
59
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
60
60
|
import { looksLikeBusFailure, systemdSaid, userBusEnv, CAPTURE_STDERR,
|
|
@@ -62,13 +62,13 @@ import { looksLikeBusFailure, systemdSaid, userBusEnv, CAPTURE_STDERR,
|
|
|
62
62
|
|
|
63
63
|
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
64
64
|
// `userBusEnv` and `busRemedy` MOVED TO shared/systemd-failure.mjs, unchanged, because
|
|
65
|
-
// `start --background` needs the same answers and had none
|
|
65
|
+
// `start --background` needs the same answers and had none. Their reasoning went
|
|
66
66
|
// with them; this file imports them and passes `userBusEnv()` at every `systemctl` call it makes,
|
|
67
67
|
// `showUnit` included — which is the property that makes deriving the bus safe here and is why `start`
|
|
68
68
|
// does not do it at two of its five.
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* What a half-finished connect has ALREADY written by the time `step` failed
|
|
71
|
+
* What a half-finished connect has ALREADY written by the time `step` failed.
|
|
72
72
|
*
|
|
73
73
|
* The reader's problem is not only that a step failed; it is not knowing whether to run `connect` again,
|
|
74
74
|
* run `disconnect` first, or leave it alone. A message that names a command and stops leaves a
|
|
@@ -95,7 +95,7 @@ function alreadyApplied(step) {
|
|
|
95
95
|
/**
|
|
96
96
|
* A systemd failure, said in the reader's terms — the REASON first, then the remedy that fits it.
|
|
97
97
|
*
|
|
98
|
-
* TWO DEFECTS, ONE SITE
|
|
98
|
+
* TWO DEFECTS, ONE SITE. The calls ran with `stdio: "ignore"`, so systemd's own
|
|
99
99
|
* explanation was thrown away before anyone could read it and the whole output was `connect: Command
|
|
100
100
|
* failed: systemctl --user daemon-reload`. And the bus remedy was appended to EVERY failure, so a unit
|
|
101
101
|
* that would not start — a bound port, a bad ExecStart — told the reader to export XDG_RUNTIME_DIR: a
|
|
@@ -108,7 +108,7 @@ function alreadyApplied(step) {
|
|
|
108
108
|
/**
|
|
109
109
|
* Does this failure say the SESSION BUS is missing, rather than anything about the unit?
|
|
110
110
|
*
|
|
111
|
-
* ONE AUTHORITY, because two readers now ask it (
|
|
111
|
+
* ONE AUTHORITY, because two readers now ask it (criterion 3). The failure text
|
|
112
112
|
* below offers the bus remedy on a yes, and the health reader refuses to translate a yes into "the door
|
|
113
113
|
* is not open" — that mistranslation is the defect, and a second copy of this test is how the two would
|
|
114
114
|
* come to disagree about which failures are bus failures.
|
|
@@ -172,7 +172,7 @@ function runningEnv() {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
|
-
* Which tree each installed unit's LIVE process is executing
|
|
175
|
+
* Which tree each installed unit's LIVE process is executing.
|
|
176
176
|
*
|
|
177
177
|
* NOT the units' env file, which is the file this verb is about to write — after the write the two
|
|
178
178
|
* agree and the drift is invisible. The running processes are the only witnesses, and they are
|
|
@@ -282,11 +282,11 @@ function portOwnerOf(port, bound) {
|
|
|
282
282
|
/**
|
|
283
283
|
* Read properties off a user unit — the ONE place this file asks systemd anything read-only.
|
|
284
284
|
*
|
|
285
|
-
* ── WHY IT EXISTS (
|
|
285
|
+
* ── WHY IT EXISTS (criterion 3) ──────────────────────────────────────────────────────────────────────
|
|
286
286
|
*
|
|
287
287
|
* Two readers here called `systemctl --user show` directly and neither did what the WRITERS in this
|
|
288
|
-
* same file already do: neither passed `userBusEnv()`, so both failed in exactly the shell
|
|
289
|
-
*
|
|
288
|
+
* same file already do: neither passed `userBusEnv()`, so both failed in exactly the shell the
|
|
289
|
+
* refusal report was filed about, and neither captured stderr, so systemd's own "Failed to connect to bus" went
|
|
290
290
|
* straight past the reader with no remedy beside it.
|
|
291
291
|
*
|
|
292
292
|
* The writers were fixed and these were missed, which made the product WORSE on this path rather than
|
|
@@ -370,7 +370,7 @@ export function unitIsHealthy(name, { show = showUnit, pause = settle } = {}) {
|
|
|
370
370
|
pause(3000);
|
|
371
371
|
const { fields: f, error } = show(name, ["ActiveState", "SubState", "NRestarts"]);
|
|
372
372
|
if (error) {
|
|
373
|
-
// ── A BUS THAT COULD NOT BE REACHED IS NOT A DOOR THAT IS DOWN
|
|
373
|
+
// ── A BUS THAT COULD NOT BE REACHED IS NOT A DOOR THAT IS DOWN ────────────────────────────────
|
|
374
374
|
//
|
|
375
375
|
// Returning false here is what made the operator worse off than before that fix.
|
|
376
376
|
// The caller renders false as "the door is not open, so no key was issued" — a confident, wrong,
|
|
@@ -396,11 +396,20 @@ function enableTheDoor({ have, identity, client = null, dryRun, portFree, portOw
|
|
|
396
396
|
const reach = { publicAddress: have.publicAddress, reachabilityKnown: envKnown };
|
|
397
397
|
// Resolved HERE because it reads a file, and handed to the pure planner as a fact. A door this verb
|
|
398
398
|
// opens for an identity that is granted nothing is a credential with no reach.
|
|
399
|
+
//
|
|
400
|
+
// ENROLLED MEANS A PERSON WITH ACCESS SOMEWHERE, asked of the resolver the door itself reads. The
|
|
401
|
+
// flat company list could not say it: a person whose only reach is their organisation's Generic holds
|
|
402
|
+
// no company, and read as enrolled in nothing. No grants file answers "*", as it always has.
|
|
399
403
|
let granted;
|
|
400
|
-
try {
|
|
404
|
+
try {
|
|
405
|
+
const grants = loadGrants();
|
|
406
|
+
const person = grants ? resolvePerson(identity, grants) : null;
|
|
407
|
+
granted = !grants ? "*" : !person ? [] : person.accounts === "*" ? "*"
|
|
408
|
+
: [...person.accounts, ...(person.genericOrgs.length ? ["generic"] : [])];
|
|
409
|
+
} catch { granted = undefined; }
|
|
401
410
|
// The env file the UNIT reads, not this shell — found by the drive: a shell-exported secret passed
|
|
402
411
|
// every plan check while the door died at birth reading an env file that lacked it.
|
|
403
|
-
// ── THE PLAN CHECKED THE FILE AND THE MINT READ THE SHELL
|
|
412
|
+
// ── THE PLAN CHECKED THE FILE AND THE MINT READ THE SHELL ─────────────────────────────────────────
|
|
404
413
|
//
|
|
405
414
|
// `unitEnvHasSecret` below asks the UNIT'S ENV FILE, which is the right question for the DOOR: a
|
|
406
415
|
// process reads its environment at start, so a secret that is only in this shell is one the door
|
|
@@ -425,7 +434,7 @@ function enableTheDoor({ have, identity, client = null, dryRun, portFree, portOw
|
|
|
425
434
|
portOwner: (p) => (p === have.port ? (portOwner ?? (portFree ? "free" : "stranger")) : "stranger") });
|
|
426
435
|
if (!plan.possible) return { ok: false, blockers: plan.blockers, fix: plan.fix };
|
|
427
436
|
|
|
428
|
-
// ── A MACHINE-WIDE SETTING IS NOT THIS VERB'S TO MOVE IN SILENCE
|
|
437
|
+
// ── A MACHINE-WIDE SETTING IS NOT THIS VERB'S TO MOVE IN SILENCE ───────────────────────────────────
|
|
429
438
|
//
|
|
430
439
|
// `plan.settings` carries CLEAROTRON_CHECKOUT_DIR set to THIS checkout, and `setEnvValue` replaces
|
|
431
440
|
// rather than preserves — so running this from a worktree repoints every unit's ExecStart and the
|
|
@@ -500,7 +509,7 @@ function enableTheDoor({ have, identity, client = null, dryRun, portFree, portOw
|
|
|
500
509
|
recordNote = "This key's id could not be read, so it was NOT recorded — `clearotron disconnect` cannot revoke it; it dies only at its own expiry.";
|
|
501
510
|
} else {
|
|
502
511
|
try {
|
|
503
|
-
// ── THE TRUE CAUSE, NOT WHATEVER THREW
|
|
512
|
+
// ── THE TRUE CAUSE, NOT WHATEVER THREW ─────────────────────────────────────────────────────
|
|
504
513
|
//
|
|
505
514
|
// With no grants file configured this reached `atomicWrite(undefined, …)` and the reader got a
|
|
506
515
|
// raw Node type error inside a client-facing sentence — a live key, outside `disconnect`'s
|
|
@@ -561,7 +570,7 @@ async function render(offer, have, { dryRun, running, allowMove = false }) {
|
|
|
561
570
|
return 0;
|
|
562
571
|
}
|
|
563
572
|
|
|
564
|
-
// ── AN ADDRESS ROUTE ALWAYS MINTS NOW (
|
|
573
|
+
// ── AN ADDRESS ROUTE ALWAYS MINTS NOW (ruling 2026-09-03,) ─────────────
|
|
565
574
|
//
|
|
566
575
|
// This used to be `if (offer.enables)` — mint only when the row said the door still had to be turned
|
|
567
576
|
// on. Under settled point 2 the door auto-starts, so no row asks for that any more and every offer
|
|
@@ -586,7 +595,7 @@ async function render(offer, have, { dryRun, running, allowMove = false }) {
|
|
|
586
595
|
const bound = await portIsFree(have.port);
|
|
587
596
|
const r = enableTheDoor({ have, identity, client: offer.client.id, dryRun, env: running.env,
|
|
588
597
|
envKnown: running.known, portFree: bound, portOwner: portOwnerOf(have.port, bound), allowMove });
|
|
589
|
-
// SAID BEFORE THE OUTCOME, whichever way it goes
|
|
598
|
+
// SAID BEFORE THE OUTCOME, whichever way it goes: a reader who is about to be
|
|
590
599
|
// told the door is open needs to have already read that the deployment moved trees to open it.
|
|
591
600
|
for (const line of r.moveSays ?? []) say(line);
|
|
592
601
|
if (!r.ok) {
|
|
@@ -708,12 +717,12 @@ async function main() {
|
|
|
708
717
|
return await render(whatItNeeds(chosen, have), have, { dryRun, running, allowMove: argv.includes("--allow-checkout-move") });
|
|
709
718
|
}
|
|
710
719
|
|
|
711
|
-
// THE DISPATCH RUNS ONLY WHEN THIS FILE IS THE COMMAND
|
|
720
|
+
// THE DISPATCH RUNS ONLY WHEN THIS FILE IS THE COMMAND. Without the guard, importing
|
|
712
721
|
// this module to drive one of its message helpers RUNS THE WHOLE VERB — the arm for the half-applied
|
|
713
722
|
// failure path opened an interactive prompt and hung the suite. `bin/clearotron.mjs` carries the same
|
|
714
723
|
// guard for the same reason, written up there: "importing it to read the verb table would DISPATCH".
|
|
715
724
|
//
|
|
716
|
-
// Ten other files under bin/ still lack it. That is a class, filed
|
|
725
|
+
// Ten other files under bin/ still lack it. That is a class, filed separately rather than
|
|
717
726
|
// swept in here — each one needs verifying that it still runs as a command.
|
|
718
727
|
if (isEntrypoint(import.meta.url)) {
|
|
719
728
|
main().then((code) => process.exit(code ?? 0), (e) => { console.error(`connect: ${e.message}`); process.exit(2); });
|