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/bin/onboard.mjs
CHANGED
|
@@ -70,10 +70,9 @@ import { standFrom } from "../shared/invocation.mjs"; // is this tree one npm
|
|
|
70
70
|
import { installShim } from "../shared/verb-shim.mjs"; // — the verb goes on PATH
|
|
71
71
|
import { styleFor, banner } from "../shared/tty-style.mjs"; // — weight where the meaning is
|
|
72
72
|
import { bracketAsciiCells, BRAND } from "../shared/brand.mjs"; // F18 — the mark, from the geometry the SVG already uses
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
import { classifyStaffDomain, domainOfEmail, staffDomainRefusal, staffGrantSentence } from "../shared/staff-domain.mjs";
|
|
73
|
+
// THE REFUSALS ABOUT THE SIGN-IN ADDRESS ITSELF, shared with `bin/start.mjs`. Two copies would be a
|
|
74
|
+
// wizard that accepts an address the launcher then sends back.
|
|
75
|
+
import { addressRefusal } from "../shared/staff-domain.mjs";
|
|
77
76
|
import { join, dirname, isAbsolute, resolve } from "node:path";
|
|
78
77
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
79
78
|
import { delimiter } from "node:path";
|
|
@@ -95,14 +94,15 @@ import { resolveAuthMode } from "../driver/engine/auth.mjs";
|
|
|
95
94
|
import { isInsideCheckout } from "../shared/inside-checkout.mjs"; // — one copy of the rule, and it is testable
|
|
96
95
|
import { packagedBuild as sharedPackagedBuild } from "../shared/packaged-build.mjs"; // — one reader of build-info.json, reachable from the driver
|
|
97
96
|
import { processTable } from "../shared/process-table.mjs"; // — /proc is not the only box
|
|
98
|
-
import { programsFromAnotherCheckout } from "../shared/checkout-move.mjs";
|
|
97
|
+
import { programsFromAnotherCheckout } from "../shared/checkout-move.mjs";
|
|
99
98
|
import { entrypointOf } from "../driver/systemd/install-census.mjs"; // one ExecStart parser
|
|
100
99
|
import { overlayReport, renderOverlayReport } from "../shared/doctrine-overlay.mjs"; // — the doctor reports the overlay
|
|
101
100
|
import { engineInventory, engineMode, ENGINE_MODES } from "../driver/config-inventory.mjs"; //
|
|
102
|
-
import { probeEngineTurn, probeFailureText, PROBE_MODEL, PROBE_TIMEOUT_SEC } from "../driver/engine/probe.mjs";
|
|
101
|
+
import { probeEngineTurn, probeFailureText, PROBE_MODEL, PROBE_TIMEOUT_SEC, engineEnvKeys } from "../driver/engine/probe.mjs";
|
|
102
|
+
import { runRequiredNames, missingRequirements, REGISTER_ENV, ENGINE_ENV } from "../driver/run-requirements.mjs"; // the order-time gate's own question, asked here rather than restated
|
|
103
103
|
import { pinEnv, envFrom } from "../shared/env-aliases.mjs";
|
|
104
104
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-point test, all spellings
|
|
105
|
-
//
|
|
105
|
+
// one synopsis reader for every verb that prints one.
|
|
106
106
|
import { usageBlock } from "../shared/usage-block.mjs";
|
|
107
107
|
import { invoke } from "../shared/invocation.mjs"; // — name a command the reader can actually type
|
|
108
108
|
import { parseEnvFile } from "../driver/systemd/render-units.mjs"; // — ONE KEY=value reader; a second copy would drift from what systemd actually reads
|
|
@@ -120,8 +120,8 @@ function readIfPresent(path) {
|
|
|
120
120
|
|
|
121
121
|
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
122
122
|
const ENV_PATH = envLocalPath({ repoRoot: REPO }); // resolved, never composed: one resolver, so moving this file later is one line
|
|
123
|
-
// WHAT IS READ IS NOT ALWAYS WHERE THE NEXT WRITE GOES. An install configured before the move
|
|
124
|
-
//
|
|
123
|
+
// WHAT IS READ IS NOT ALWAYS WHERE THE NEXT WRITE GOES. An install configured before the move still
|
|
124
|
+
// has its file at the old path, and the loader still reads it — so every READ here asks
|
|
125
125
|
// the same resolver the loader asks, or this command reports "no configuration" over a file every other
|
|
126
126
|
// command is applying. Writes stay on ENV_PATH: a writer that followed the file backwards would keep an
|
|
127
127
|
// install in the directory npm replaces forever.
|
|
@@ -145,7 +145,7 @@ const PROBE_PROVIDERS = has("--probe-providers");
|
|
|
145
145
|
const PROBE_CONNECTOR = has("--probe-connector");
|
|
146
146
|
|
|
147
147
|
// ── output ───────────────────────────────────────────────────────────────────────────────────────────
|
|
148
|
-
// WEIGHT WHERE THE MEANING IS (
|
|
148
|
+
// WEIGHT WHERE THE MEANING IS (ruling 2026-08-31). Every line here used to
|
|
149
149
|
// be the same weight, which is why the passphrase and the coverage reason both disappeared into their
|
|
150
150
|
// surroundings — two defects on that issue that are formatting defects wearing other clothes.
|
|
151
151
|
//
|
|
@@ -175,7 +175,7 @@ const blocking = (s) => { blockers.push(s); say(` ${style.warn("!")} ${s}`); };
|
|
|
175
175
|
// ── A FOURTH STATE, AND THE COMPOSITE EXIT CONTRACT IT COMPLETES ────────────────────────────────────
|
|
176
176
|
//
|
|
177
177
|
// `inertSetting` is for configuration the operator SET that the build does not read: a retired
|
|
178
|
-
// spelling left behind by an upgrade
|
|
178
|
+
// spelling left behind by an upgrade, and anything else of that shape. It needed
|
|
179
179
|
// its own state because neither existing one is honest about it.
|
|
180
180
|
//
|
|
181
181
|
// `warn` prints `!` and keeps rc 0 — but rc 0 is the exact reading that let thirteen ignored
|
|
@@ -190,10 +190,10 @@ const blocking = (s) => { blockers.push(s); say(` ${style.warn("!")} ${s}`); };
|
|
|
190
190
|
// ── THE CONTRACT, STATED ONCE BECAUSE TWO ISSUES IN ONE BUNDLE BOTH CHANGE IT ───────────────────────
|
|
191
191
|
//
|
|
192
192
|
// rc 1 means: SOMETHING THE OPERATOR SET IS NOT DOING WHAT THEY THINK IT IS DOING.
|
|
193
|
-
// · `problem` — set wrongly. A credential present but unusable is this
|
|
193
|
+
// · `problem` — set wrongly. A credential present but unusable is this: the
|
|
194
194
|
// file exists, doctor called it enrolled, and the report then disclosed an outage
|
|
195
195
|
// that never happened.
|
|
196
|
-
// · `inertSetting` — set, and not read at all
|
|
196
|
+
// · `inertSetting` — set, and not read at all.
|
|
197
197
|
// rc 0 means: nothing set is being ignored or misapplied. An ABSENCE stays rc 0 — a fresh machine is
|
|
198
198
|
// not a broken one, and `blocking` exists so an absence that stops the product is still said out loud.
|
|
199
199
|
//
|
|
@@ -316,7 +316,7 @@ const prose = (...parts) => { for (const l of wrapProse(parts.join(" "), proseWi
|
|
|
316
316
|
* @returns {"no-sources"|"unbuilt"|"unversioned"|"guarded"|"tracked-unguarded"|"unmeasured"|"current"|"stale"}
|
|
317
317
|
*/
|
|
318
318
|
// `bundleFreshness` and its three readers moved to `shared/bundle-freshness.mjs` when `/portal/health`
|
|
319
|
-
// became a second caller
|
|
319
|
+
// became a second caller: health answered `ui: "built", ok: true` over a tree this
|
|
320
320
|
// command refuses at rc 1, because the two surfaces each had their own idea of what a usable bundle is.
|
|
321
321
|
// One definition, both readers, so they cannot disagree again.
|
|
322
322
|
|
|
@@ -565,7 +565,7 @@ export async function offerUsptoSync(dbPath, io, deps = {}) {
|
|
|
565
565
|
// sales-gated. It used to open with the free tier, and a test asserted that it must — an assertion that
|
|
566
566
|
// argued a case the ADR had already answered. The reasoning is in ADR-0001 and in providers/README.md,
|
|
567
567
|
// which is the canonical statement; this list carries no competing recommendation of its own.
|
|
568
|
-
// ── THE REGISTER SELECTION TABLE MOVED TO `shared/register-selection.mjs`
|
|
568
|
+
// ── THE REGISTER SELECTION TABLE MOVED TO `shared/register-selection.mjs` ───────────────────────────
|
|
569
569
|
//
|
|
570
570
|
// Re-exported here under the name every existing reader uses — this file's own call sites read
|
|
571
571
|
// `PROVIDERS`, and so does the run-requirements arm. It moved because the runner's intake wall and the
|
|
@@ -592,40 +592,47 @@ export const AMBIENT_KEYS = [
|
|
|
592
592
|
const present = (v) => typeof v === "string" && v.trim() !== "";
|
|
593
593
|
|
|
594
594
|
/**
|
|
595
|
-
* Ask who signs in, and
|
|
595
|
+
* Ask who signs in, and what their organisation is called.
|
|
596
596
|
*
|
|
597
|
-
* ── WHY
|
|
597
|
+
* ── WHY THE ADDRESS IS ASKED AT ALL ─────────────────────────────────────────────────────────────────
|
|
598
598
|
*
|
|
599
599
|
* Setup never asked for an address. `clearotron start` therefore took one from `--user`, from
|
|
600
|
-
* `PORTAL_LOCAL_USER`, or from the local account as `<account>@localhost
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
600
|
+
* `PORTAL_LOCAL_USER`, or from the local account as `<account>@localhost`. An outside install reached a
|
|
601
|
+
* state nobody chose that way: an assistant filled a documentation address into the environment file on
|
|
602
|
+
* the operator's behalf, because this wizard had no prompt for it. So the address is asked for here,
|
|
603
|
+
* where a person is definitionally present.
|
|
604
604
|
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
605
|
+
* WHAT IT NO LONGER DECIDES. The part after its `@` used to decide who was an administrator: everyone at
|
|
606
|
+
* that domain was staff. That rule is deleted, with the question about granting the domain and the
|
|
607
|
+
* setting it wrote. The address is the first person on the install — Run, Manage, access to everything —
|
|
608
|
+
* written as their own entry in the grants file by `clearotron start` (`installerGrants`), and it admits
|
|
609
|
+
* nobody else whatever its domain.
|
|
609
610
|
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
*
|
|
611
|
+
* TWO REFUSALS ABOUT THE ADDRESS ITSELF STAY: not a single email address, and a public or reserved
|
|
612
|
+
* domain (shared/staff-domain.mjs, shared with `clearotron start` so the two refuse the same addresses).
|
|
613
|
+
*
|
|
614
|
+
* THE ORGANISATION'S NAME, directly after the address and required. It is the organisation the install
|
|
615
|
+
* starts with and the label the portal shows for it; `clearotron start` files it in the grants file the
|
|
616
|
+
* first time that file holds none, and it is renamed there. WRITTEN QUOTED, because the loader reads
|
|
617
|
+
* `.env` with Node's `parseEnv`, which ends an unquoted value at `#` and trims it — measured: "Café #1
|
|
618
|
+
* Sàrl" written bare comes back "Café". Quoted it survives whole, except a double quote, which ends the
|
|
619
|
+
* value, so a name holding one is sent back.
|
|
614
620
|
*
|
|
615
621
|
* THE DEFAULT IS THE LOCAL ACCOUNT AND NOTHING ELSE — not the git author, not the hostname, not a shell
|
|
616
|
-
* variable.
|
|
622
|
+
* variable. The address that becomes the first person comes from the person, or from the one source that
|
|
617
623
|
* cannot name a second person.
|
|
618
624
|
*
|
|
619
|
-
* `io` IS INJECTED for the reason `offerUsptoSync`'s is: the
|
|
620
|
-
*
|
|
621
|
-
*
|
|
625
|
+
* `io` IS INJECTED for the reason `offerUsptoSync`'s is: the branches that matter are the answers that
|
|
626
|
+
* must be sent back, and closed over a terminal they are asserted nowhere. Returns the `.env` keys to
|
|
627
|
+
* write and nothing else.
|
|
622
628
|
*/
|
|
623
|
-
export async function askSignIn(io, { localAccount = "user"
|
|
624
|
-
const { askValue,
|
|
625
|
-
warn = () => {}, problem = () => {} } = io;
|
|
629
|
+
export async function askSignIn(io, { localAccount = "user" } = {}) {
|
|
630
|
+
const { askValue, ok = () => {}, problem = () => {} } = io;
|
|
626
631
|
const localDefault = `${localAccount}@localhost`;
|
|
627
|
-
prose("The portal admits one address on this install, and that address is
|
|
628
|
-
"
|
|
632
|
+
prose("The portal admits one address on this install, and that address is the first person on it:",
|
|
633
|
+
"access to everything, with both permissions — Run clearances and Manage. Enter accepts the",
|
|
634
|
+
"local-account form, which is this machine and nobody else.");
|
|
635
|
+
let address;
|
|
629
636
|
for (;;) {
|
|
630
637
|
const typed = String(await askValue("Sign-in address:", { def: localDefault })).trim().toLowerCase();
|
|
631
638
|
if (!typed.includes("@") || typed.indexOf("@") !== typed.lastIndexOf("@")) {
|
|
@@ -633,35 +640,27 @@ export async function askSignIn(io, { localAccount = "user", staffLabel = "Staff
|
|
|
633
640
|
+ "so this would sign in and then be denied at the door.");
|
|
634
641
|
continue;
|
|
635
642
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
+
// The refusal's own sentence, never a second copy: `clearotron start` prints these same words when it
|
|
644
|
+
// meets the same address, and two wordings of one refusal is how a reader comes to believe they have
|
|
645
|
+
// met two different problems.
|
|
646
|
+
const refusal = addressRefusal(typed);
|
|
647
|
+
if (refusal) { problem(refusal); continue; }
|
|
648
|
+
address = typed;
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
ok(`${address} — the first person on this install, with access to everything.`);
|
|
652
|
+
for (;;) {
|
|
653
|
+
const name = String(await askValue("Your organisation's name:") ?? "").trim();
|
|
654
|
+
if (!name) {
|
|
655
|
+
problem("A name is needed here: it is the organisation this install starts with. It can be renamed later.");
|
|
643
656
|
continue;
|
|
644
657
|
}
|
|
645
|
-
if (
|
|
646
|
-
|
|
647
|
-
|
|
658
|
+
if (name.includes('"')) {
|
|
659
|
+
problem(`${name} holds a double quote, and the settings file cannot keep one — it ends the value. Leave it out.`);
|
|
660
|
+
continue;
|
|
648
661
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
// admits people the reader has never met.
|
|
652
|
-
say("");
|
|
653
|
-
warn(`${typed} makes this a rule about ${domain}, not about you.`);
|
|
654
|
-
say(` ${staffGrantSentence(domain, { staffLabel })}`);
|
|
655
|
-
say("");
|
|
656
|
-
say(` Everyone at ${domain} who gets past this install's sign-in door would see every brand owner`);
|
|
657
|
-
say(" on it — every clearance, every report, every configuration. On a laptop that is only you,");
|
|
658
|
-
say(" because only one address can sign in. Behind a company login it is the whole domain.");
|
|
659
|
-
say("");
|
|
660
|
-
if (await confirm(`Grant ${domain} that, and write it down as PORTAL_STAFF_DOMAINS?`, false))
|
|
661
|
-
return { PORTAL_LOCAL_USER: typed, PORTAL_STAFF_DOMAINS: domain };
|
|
662
|
-
info(`nothing granted${envPath ? `, and nothing written to ${envPath}` : ""}. ${typed} would sign in `
|
|
663
|
-
+ "and every page would refuse it, because signing in is not being enrolled — so choose the "
|
|
664
|
-
+ "local-account form, or answer yes above.");
|
|
662
|
+
ok(`${name} — your organisation. \`clearotron start\` files it in the grants file, where it can be renamed.`);
|
|
663
|
+
return { PORTAL_LOCAL_USER: address, CLEAROTRON_ORGANISATION_NAME: `"${name}"` };
|
|
665
664
|
}
|
|
666
665
|
}
|
|
667
666
|
|
|
@@ -815,7 +814,7 @@ export function engineOptions() {
|
|
|
815
814
|
export function readEnvFile(path, { home = homedir() } = {}) {
|
|
816
815
|
if (!existsSync(path)) return {};
|
|
817
816
|
const env = {};
|
|
818
|
-
// NAMED, NOT INFERRED
|
|
817
|
+
// NAMED, NOT INFERRED. This passed `repoRoot: dirname(path)` and relied on the
|
|
819
818
|
// loader's resolution happening to land back on the same directory. It did — through the LEGACY
|
|
820
819
|
// fallback, and only while the reader had no `~/.config/clearotron/.env` of their own. With one, this
|
|
821
820
|
// function returned THAT file's contents for any path it was given, including a temporary fixture.
|
|
@@ -1128,7 +1127,7 @@ export async function runCheck() {
|
|
|
1128
1127
|
warn(`could not tell whether running programs are on the current tree: ${running.detail}`);
|
|
1129
1128
|
}
|
|
1130
1129
|
|
|
1131
|
-
// ── AND THE MORE DANGEROUS ANSWER: A DIFFERENT TREE, NOT AN OLDER ONE
|
|
1130
|
+
// ── AND THE MORE DANGEROUS ANSWER: A DIFFERENT TREE, NOT AN OLDER ONE ──────────────────────────────
|
|
1132
1131
|
//
|
|
1133
1132
|
// The block above asks whether a process predates this checkout's last move. This asks whether it is
|
|
1134
1133
|
// executing a DIFFERENT checkout entirely, which is what `clearotron connect` used to cause silently
|
|
@@ -1231,17 +1230,19 @@ export async function runCheck() {
|
|
|
1231
1230
|
home: homedir() })
|
|
1232
1231
|
: null;
|
|
1233
1232
|
|
|
1234
|
-
// ── AND THE DOOR SECTION IS THE THIRD SUCH SECTION
|
|
1233
|
+
// ── AND THE DOOR SECTION IS THE THIRD SUCH SECTION ──────────────────────────────────────────────
|
|
1235
1234
|
//
|
|
1236
1235
|
// The reading above was hoisted here from further down for this. It used to be resolved BELOW the
|
|
1237
1236
|
// door check, so the one section making the loudest claim about the running service — "NOBODY can
|
|
1238
1237
|
// use this portal" — was the one section that could not consult it.
|
|
1239
1238
|
//
|
|
1240
1239
|
// MEASURED 2026-09-06 on a healthy packaged install: `doctor` printed that ✗ and exited 1 on a box
|
|
1241
|
-
// where the local user signs in and `GET /portal/api/me`
|
|
1242
|
-
//
|
|
1243
|
-
// service
|
|
1244
|
-
// `PORTAL_AUTH_MODE`
|
|
1240
|
+
// where the local user signs in and `GET /portal/api/me` returned `{"role":"staff"}`. The units' file
|
|
1241
|
+
// carried `PORTAL_STAFF_DOMAINS=localhost` (a staff-domain rule, since deleted), the identity was
|
|
1242
|
+
// `<user>@localhost`, and the running service admitted it as staff. This command read the CLI's own
|
|
1243
|
+
// `.env`, where neither that name nor `PORTAL_AUTH_MODE` appeared — so the check was not ignoring the
|
|
1244
|
+
// auth mode, it never saw it. What admits a person now is their entry in the grants file, and the
|
|
1245
|
+
// units' file is still the only place that names which grants file.
|
|
1245
1246
|
//
|
|
1246
1247
|
// The disclosure was already here and one severity too quiet: a `·` saying "what THIS environment
|
|
1247
1248
|
// implies, not what the running service serves", directly above a `✗` phrased in the present
|
|
@@ -1460,7 +1461,13 @@ export async function runCheck() {
|
|
|
1460
1461
|
// The auth variables ride this list for the same reason CLEAROTRON_AI does: without them the
|
|
1461
1462
|
// probe bills the way the AMBIENT environment says rather than the way this .env says, and
|
|
1462
1463
|
// "proven" would name a lane no run takes.
|
|
1463
|
-
|
|
1464
|
+
//
|
|
1465
|
+
// THE CREDENTIALS RIDE IT TOO, and did not. This was a second copy of the list `applyEngineEnv`
|
|
1466
|
+
// applies, and it had dropped the API key and the headless token — so a token kept only in this
|
|
1467
|
+
// install's .env never reached the probe, which then reported the engine signed out while the same
|
|
1468
|
+
// token exported into the shell proved it. `engineEnvKeys` is the probe's own list: the copy that is
|
|
1469
|
+
// filled is now the copy that is applied.
|
|
1470
|
+
for (const k of engineEnvKeys()) {
|
|
1464
1471
|
const e = effective(k);
|
|
1465
1472
|
if (e) probeEnv[k] = e.v;
|
|
1466
1473
|
}
|
|
@@ -1516,7 +1523,7 @@ export async function runCheck() {
|
|
|
1516
1523
|
}
|
|
1517
1524
|
|
|
1518
1525
|
say("\n .env");
|
|
1519
|
-
// ── THE FILE MOVED, SO ASK THE SAME QUESTION THE LOADER ASKS
|
|
1526
|
+
// ── THE FILE MOVED, SO ASK THE SAME QUESTION THE LOADER ASKS ─────────────────────────────────────
|
|
1520
1527
|
//
|
|
1521
1528
|
// `.env` now resolves to `~/.config/clearotron/.env`, and an install configured before that ruling has
|
|
1522
1529
|
// it at the old path, where the loader still reads it. Doctor reporting "no .env" over a file every
|
|
@@ -1600,7 +1607,7 @@ export async function runCheck() {
|
|
|
1600
1607
|
const mod = await import(`../driver/profiles.mjs?doctor=${Date.now()}`);
|
|
1601
1608
|
const r = mod.profileStoreResolution();
|
|
1602
1609
|
const where = `profiles resolve from ${r.store}`;
|
|
1603
|
-
// ── ON A HOSTED BOX THE SERVICES' ANSWER COMES FIRST
|
|
1610
|
+
// ── ON A HOSTED BOX THE SERVICES' ANSWER COMES FIRST ────────────────────────────────────────────
|
|
1604
1611
|
//
|
|
1605
1612
|
// One run of this command reported the SAME variable as both set and unset, and concluded a
|
|
1606
1613
|
// production box was a demo install:
|
|
@@ -1672,7 +1679,7 @@ export async function runCheck() {
|
|
|
1672
1679
|
// The reader is told they have a customer and not told what they are running on. Both halves
|
|
1673
1680
|
// wrong from one list.
|
|
1674
1681
|
//
|
|
1675
|
-
// SINCE 2026-09-08 A FRESH INSTALL RESOLVES `generic` ALONE (
|
|
1682
|
+
// SINCE 2026-09-08 A FRESH INSTALL RESOLVES `generic` ALONE (ruling): nobody should have to
|
|
1676
1683
|
// clean demo material out of an environment they just created. So the demo branch below no longer
|
|
1677
1684
|
// fires on a plain install — it fires inside the demo, which asks for its own account. It is kept
|
|
1678
1685
|
// rather than deleted because it is still reachable, and a reader who meets the demo account
|
|
@@ -1727,7 +1734,7 @@ export async function runCheck() {
|
|
|
1727
1734
|
// a named disagreement instead of as a tenancy refusal a reader will read as a permissions problem.
|
|
1728
1735
|
try {
|
|
1729
1736
|
const cs = await import(`../shared/customer-store.mjs?doctor=${Date.now()}`);
|
|
1730
|
-
// ── ONE ENVIRONMENT ON BOTH SIDES, AND THE LINE SAYS WHICH
|
|
1737
|
+
// ── ONE ENVIRONMENT ON BOTH SIDES, AND THE LINE SAYS WHICH ────────────────────────────────────
|
|
1731
1738
|
//
|
|
1732
1739
|
// This compared a surface resolved from THIS PROCESS's environment against a roster resolved
|
|
1733
1740
|
// from this process's module load — so on a hosted box it certified agreement between two
|
|
@@ -1744,7 +1751,7 @@ export async function runCheck() {
|
|
|
1744
1751
|
+ `${split.surface}, runs: ${split.roster}. A brand owner in one and not the other is refused `
|
|
1745
1752
|
+ `by the surface with a tenancy message, which reads as a permissions problem and is not one.`);
|
|
1746
1753
|
} else {
|
|
1747
|
-
// ── WHAT THIS ✓ CERTIFIES, AND WHAT IT CANNOT
|
|
1754
|
+
// ── WHAT THIS ✓ CERTIFIES, AND WHAT IT CANNOT ──────────────────────────────────────────────
|
|
1748
1755
|
//
|
|
1749
1756
|
// Both sides derive from `CLEAROTRON_CUSTOMERS_DIR`, which is what 1923 settled — so this
|
|
1750
1757
|
// cannot catch two environments disagreeing, and reading it as "production is configured"
|
|
@@ -1784,7 +1791,7 @@ export async function runCheck() {
|
|
|
1784
1791
|
try {
|
|
1785
1792
|
const report = overlayReport({ baseRoot: config.skillsBaseDir, overlayRoot: config.skillsOverlayDir });
|
|
1786
1793
|
for (const line of renderOverlayReport(report, { indent: "" })) say(` ${line}`);
|
|
1787
|
-
// ── AND THE SERVICES MAY HAVE ONE THIS PROCESS CANNOT SEE
|
|
1794
|
+
// ── AND THE SERVICES MAY HAVE ONE THIS PROCESS CANNOT SEE ───────────────────────────────────────
|
|
1788
1795
|
//
|
|
1789
1796
|
// `config.skillsOverlayDir` resolves from this process's environment. On a production box that
|
|
1790
1797
|
// printed "none configured — this install overrides nothing" while CLEAROTRON_INSTRUCTIONS_DIR was
|
|
@@ -1869,6 +1876,32 @@ export async function runCheck() {
|
|
|
1869
1876
|
// doctor that hangs on a box with no route — so this reports ENROLMENT, and says plainly that a
|
|
1870
1877
|
// source which is enrolled can still fail at run time. The owner's own report says `CONNECTION_CLOSED`
|
|
1871
1878
|
// on exactly that case.
|
|
1879
|
+
// ── WILL A SEARCH RUN? — THE ORDER-TIME GATE'S OWN QUESTION, OF THE ENVIRONMENT A RUN WILL HAVE ──
|
|
1880
|
+
//
|
|
1881
|
+
// Every section above reads one requirement from YOUR shell and this command's .env. A search is
|
|
1882
|
+
// refused by a different reader: `orderTimeRefusal`, at order time, against the runner's environment —
|
|
1883
|
+
// a background unit's, on a box that has them. So this command could pass an install the run then
|
|
1884
|
+
// refused: it found `claude` on this shell's PATH while the unit, which has no such PATH, was refused
|
|
1885
|
+
// for an engine path it had never been given. Asking the gate's own authority of the units' environment
|
|
1886
|
+
// is what makes an install this command passes one that can run. `blocking`, not `problem`: an install
|
|
1887
|
+
// that is not configured yet is unfinished rather than broken, and the exit status keeps that contract.
|
|
1888
|
+
say("\n Will a search run?");
|
|
1889
|
+
if (!serviceKnown) {
|
|
1890
|
+
info("the units' environment could not be read, so what a search would be refused for is NOT checked here — a failure to look is not a clean result");
|
|
1891
|
+
} else {
|
|
1892
|
+
const tables = { registers: PROVIDERS, engines: ENGINE_BINARIES, defaultEngine: DEFAULT_ENGINE_ID };
|
|
1893
|
+
// Two passes: which credentials a run needs depends on the register and the engine it names.
|
|
1894
|
+
const view = {};
|
|
1895
|
+
const fill = (names) => { for (const n of names) { const e = effectiveForService(n); if (e) view[n] = e.v; } };
|
|
1896
|
+
fill([REGISTER_ENV, ENGINE_ENV]);
|
|
1897
|
+
fill(runRequiredNames(view, tables));
|
|
1898
|
+
const { atOrder } = missingRequirements(view, tables);
|
|
1899
|
+
if (atOrder.length) {
|
|
1900
|
+
blocking(`a search is refused until ${atOrder.length === 1 ? "this is" : "these are"} set in ${serviceEnvLabel}: ${atOrder.map((r) => r.name).join(", ")}`);
|
|
1901
|
+
for (const r of atOrder) info(` ${r.name} — ${r.why}`);
|
|
1902
|
+
} else ok(`nothing a search is refused for at order time is missing from ${serviceEnvLabel}`);
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1872
1905
|
say("\n Case law and other capabilities");
|
|
1873
1906
|
{
|
|
1874
1907
|
const { caseLawInventory } = await import("../driver/config-inventory.mjs");
|
|
@@ -1886,7 +1919,7 @@ export async function runCheck() {
|
|
|
1886
1919
|
// other search is unaffected — so it is a `warn`, which says it out loud and leaves the exit code
|
|
1887
1920
|
// alone. Nothing here can be misconfigured: there is no variable to get wrong.
|
|
1888
1921
|
//
|
|
1889
|
-
// ── AND A PRESENT-BUT-BROKEN CREDENTIAL IS NEITHER
|
|
1922
|
+
// ── AND A PRESENT-BUT-BROKEN CREDENTIAL IS NEITHER ─────────────────────────────────────────
|
|
1890
1923
|
//
|
|
1891
1924
|
// It is a MISCONFIGURATION: the operator enrolled, the file is there, and it cannot work. That
|
|
1892
1925
|
// exits non-zero under this command's contract, and it must not be filed under the absence rule
|
|
@@ -1991,7 +2024,7 @@ export async function runCheck() {
|
|
|
1991
2024
|
blocking(`the portal bundle could not be read — ${e.message} — so whether /portal can render is unknown`);
|
|
1992
2025
|
}
|
|
1993
2026
|
|
|
1994
|
-
// ── THE TRIGGER KEY, AND THE DEADLINE NOBODY WAS COUNTING
|
|
2027
|
+
// ── THE TRIGGER KEY, AND THE DEADLINE NOBODY WAS COUNTING ────────────────────────────────────────
|
|
1995
2028
|
//
|
|
1996
2029
|
// A `--background` install stores the portal's trigger key in `~/.env`, the file the units load, and
|
|
1997
2030
|
// it is minted with a thirty-day life. Nothing counted it down. Thirty days after an install, on a
|
|
@@ -2034,6 +2067,60 @@ export async function runCheck() {
|
|
|
2034
2067
|
} else {
|
|
2035
2068
|
ok(`the trigger key is good for ${posture.daysLeft} more day(s), until ${posture.expiresAt}`);
|
|
2036
2069
|
}
|
|
2070
|
+
|
|
2071
|
+
// ── AND WHICH COMPANIES IT CAN START ────────────────────────────────────────────────────────
|
|
2072
|
+
//
|
|
2073
|
+
// THE THIRD SURFACE, WHICH WAS NEVER WIRED. `triggerCapWarning` exists so the create command, the
|
|
2074
|
+
// portal's boot log and this command cannot say three different things about one gap — its own
|
|
2075
|
+
// comment says "three call sites" — and it had two. The command whose entire job is to report what
|
|
2076
|
+
// a machine is configured for said nothing at all about a cap that decides which companies can be
|
|
2077
|
+
// started. Three is true now, rather than aspirational.
|
|
2078
|
+
//
|
|
2079
|
+
// IT IS `info`, DELIBERATELY, AND THE SENTENCE IS WHY. The portal re-takes its engine credential at
|
|
2080
|
+
// the start of every call, so a stale cap refuses nothing on the ordinary path; the gap bites only
|
|
2081
|
+
// when it CANNOT re-mint. A `problem` here would exit non-zero on installs whose portal re-mints
|
|
2082
|
+
// perfectly well, which is a green install turned red for a refusal that will not happen — and it
|
|
2083
|
+
// would contradict the very sentence it prints. The level is worth revisiting the day the fallback
|
|
2084
|
+
// stops being the exception.
|
|
2085
|
+
//
|
|
2086
|
+
// THE ROSTER IS THE SERVICES', not this process's. A CLI is not started by the units'
|
|
2087
|
+
// EnvironmentFile, and the two answers disagreeing IS a defect this box has produced: a door
|
|
2088
|
+
// resolving the bundled demo roster while the configured store held one more company. Comparing
|
|
2089
|
+
// the units' token against a roster the units never see would report a gap that is not there, or
|
|
2090
|
+
// miss one that is. Which store was read is said, every time.
|
|
2091
|
+
if (posture.readable && posture.accountCapped) {
|
|
2092
|
+
const svcStore = (hosted && serviceKnown) ? (effectiveForService("CLEAROTRON_CUSTOMERS_DIR")?.v || null) : null;
|
|
2093
|
+
let roster = null, why = null;
|
|
2094
|
+
try {
|
|
2095
|
+
const { loadProfiles } = await import(`../driver/profiles.mjs?doctorcap=${Date.now()}`);
|
|
2096
|
+
const { rosterAsItStands } = await import("../driver/company-bundle.mjs");
|
|
2097
|
+
roster = [...rosterAsItStands(svcStore ?? undefined, loadProfiles).keys()];
|
|
2098
|
+
} catch (e) { why = String(e?.message ?? e); }
|
|
2099
|
+
|
|
2100
|
+
// AN EMPTY ROSTER IS NOT A COVERED ONE. `rosterAsItStands` answers an empty Map for a store with
|
|
2101
|
+
// no `generic.json`, and an empty roster leaves NOTHING uncovered — a tick over a could-not-look,
|
|
2102
|
+
// which is the one answer this command must never give.
|
|
2103
|
+
// ALREADY SAID ONCE, AND ONCE IS ENOUGH. A store that refuses to load is reported as a problem by
|
|
2104
|
+
// the roster check a few sections up, in its own words and with its own remedy. Repeating it here
|
|
2105
|
+
// as a second blocker put one fact on the screen three times — the problem, this line, and this
|
|
2106
|
+
// line again in the closing summary — which is how a reader learns to skim the section. Measured
|
|
2107
|
+
// by driving both, with an uncapped token as the control so the duplicate could be told from the
|
|
2108
|
+
// original. What this line adds is the CONSEQUENCE FOR THE CAP, which the roster check does not
|
|
2109
|
+
// know about, so it says that and defers.
|
|
2110
|
+
if (why) {
|
|
2111
|
+
info("whether the trigger key covers them cannot be checked until the roster above loads — not "
|
|
2112
|
+
+ "confirmed, and not refused");
|
|
2113
|
+
} else if (!roster.length) {
|
|
2114
|
+
blocking(`the trigger key names a company cap, and no companies could be read from `
|
|
2115
|
+
+ `${svcStore ?? "the store this process resolves"} — so nothing here says whether the cap fits`);
|
|
2116
|
+
} else {
|
|
2117
|
+
const { triggerCapGap, triggerCapWarning } = await import("../driver/trigger-cap.mjs");
|
|
2118
|
+
const gap = triggerCapGap({ accounts: posture.accounts, roster });
|
|
2119
|
+
if (gap.uncovered.length) info(triggerCapWarning(gap));
|
|
2120
|
+
else ok(`the trigger key covers all ${roster.length} company(ies) in `
|
|
2121
|
+
+ `${svcStore ?? "the store this process resolves"}`);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2037
2124
|
}
|
|
2038
2125
|
}
|
|
2039
2126
|
|
|
@@ -2061,7 +2148,7 @@ export async function runCheck() {
|
|
|
2061
2148
|
// The reading is still worth printing — it is what a hand-run process here would use, and it is
|
|
2062
2149
|
// what `clearotron start` would launch from. It is stated as that, and the sentence below says
|
|
2063
2150
|
// where the running answer lives.
|
|
2064
|
-
// NAMED, NOT CAVEATED
|
|
2151
|
+
// NAMED, NOT CAVEATED. This used to disclaim itself — "what THIS environment
|
|
2065
2152
|
// implies, not what the running service serves" — directly above a ✗ that asserted a lockout on the
|
|
2066
2153
|
// live box. The values now come from the file the service actually loads, so the line says which
|
|
2067
2154
|
// file that was and the verdicts below stand on it.
|
|
@@ -2093,68 +2180,66 @@ export async function runCheck() {
|
|
|
2093
2180
|
//
|
|
2094
2181
|
// The door is only half the question. The owner signed into the test portal and every action
|
|
2095
2182
|
// refused — submitting a search, saving a custom search, loading the Generic defaults — because his
|
|
2096
|
-
// identity
|
|
2097
|
-
//
|
|
2098
|
-
//
|
|
2099
|
-
//
|
|
2183
|
+
// identity held no access in the grants file. Three symptoms, one cause, and the product KNEW:
|
|
2184
|
+
// portal-service logs a warning at boot naming the identity and the remedy. It was invisible to
|
|
2185
|
+
// everyone who needed it, because the operator was in a browser and whoever restarted the service
|
|
2186
|
+
// checked ports, not the log.
|
|
2100
2187
|
//
|
|
2101
|
-
// ASKED OF `makePrincipal`, the function the door itself uses,
|
|
2102
|
-
// refusal it predicts. A
|
|
2103
|
-
// with
|
|
2188
|
+
// ASKED OF `makePrincipal`, the function the door itself uses, for every address the grants file
|
|
2189
|
+
// names — so this cannot drift from the refusal it predicts. A person is admitted by their own entry
|
|
2190
|
+
// and nothing else: an entry under `people` with access to everything admits with no tenant row at
|
|
2191
|
+
// all, and an entry holding only switches admits nobody, so counting rows in either section would be
|
|
2192
|
+
// a second opinion about who holds access, which is how a report comes to disagree with the thing it
|
|
2193
|
+
// reports on.
|
|
2104
2194
|
//
|
|
2105
|
-
// OFFLINE, and that is what makes it worth having here: if
|
|
2106
|
-
//
|
|
2107
|
-
//
|
|
2108
|
-
//
|
|
2195
|
+
// OFFLINE, and that is what makes it worth having here: if the grants file admits no address, then
|
|
2196
|
+
// NO identity can hold access — whoever signs in, whatever the door. That is knowable from the
|
|
2197
|
+
// filesystem, so `--check` can answer it without calling anybody, which is this command's whole
|
|
2198
|
+
// contract.
|
|
2109
2199
|
try {
|
|
2110
2200
|
const { makePrincipal } = await import("../driver/portal-access.mjs");
|
|
2111
|
-
// ASKED OF THE SERVICE'S OWN ENVIRONMENT
|
|
2112
|
-
//
|
|
2113
|
-
// staff on every request.
|
|
2114
|
-
const staffDomains = String(effectiveForService("PORTAL_STAFF_DOMAINS")?.v ?? "")
|
|
2115
|
-
.split(",").map((d) => d.trim()).filter(Boolean);
|
|
2201
|
+
// ASKED OF THE SERVICE'S OWN ENVIRONMENT. Reading this command's file here is what produced a hard
|
|
2202
|
+
// ✗ claiming nobody could use a portal that was admitting its operator on every request.
|
|
2116
2203
|
const grantsFile = effectiveForService("CLEAROTRON_ACCESS_FILE")?.v ?? "";
|
|
2117
2204
|
let grants = null, unreadable = null;
|
|
2118
2205
|
if (grantsFile) {
|
|
2119
2206
|
try { grants = JSON.parse(readFileSync(grantsFile, "utf8")); }
|
|
2120
2207
|
catch (e) { unreadable = String(e?.message ?? e).slice(0, 120); }
|
|
2121
2208
|
}
|
|
2122
|
-
const
|
|
2123
|
-
.
|
|
2209
|
+
const named = new Set([...Object.keys(grants?.people ?? {}),
|
|
2210
|
+
...Object.values(grants?.tenants ?? {}).flatMap((t) => Object.keys(t?.users ?? {}))]);
|
|
2211
|
+
const admitted = [...named].filter((address) => makePrincipal({ email: address, grants }));
|
|
2124
2212
|
|
|
2125
|
-
// A FAILURE TO LOOK IS NOT A LOCKOUT
|
|
2213
|
+
// A FAILURE TO LOOK IS NOT A LOCKOUT. On a hosted box whose unit environment
|
|
2126
2214
|
// could not be read, every name above resolves empty — which is indistinguishable from a box that
|
|
2127
2215
|
// has genuinely configured nothing, and would print the loudest ✗ in this command on no evidence.
|
|
2128
2216
|
if (!serviceKnown) {
|
|
2129
2217
|
info("who may use this portal is not judged here: the units' environment could not be read, so a "
|
|
2130
|
-
+ "
|
|
2218
|
+
+ "grants file configured there would be invisible to this check");
|
|
2131
2219
|
} else if (unreadable) {
|
|
2132
2220
|
problem(`the guest list at ${grantsFile} could not be read (${unreadable}) — the portal refuses `
|
|
2133
2221
|
+ `every request while that is true, and this is a failure to look rather than an empty list`);
|
|
2134
|
-
} else if (!
|
|
2222
|
+
} else if (!admitted.length) {
|
|
2135
2223
|
// PRESENT-AND-WRONG vs ABSENT. A configured portal that grants nobody is wrong: every page
|
|
2136
2224
|
// refuses and the symptom reads as a broken login. A box that has configured neither is a
|
|
2137
2225
|
// fresh install, which is loud but not a failure — the same rule the rest of this command uses.
|
|
2138
2226
|
const configured = Boolean(grantsFile) || Boolean(effectiveForService("PORTAL_AUTH_MODE")?.v);
|
|
2139
|
-
const sentence = `NOBODY can use this portal, per ${serviceEnvFile}:
|
|
2140
|
-
+
|
|
2227
|
+
const sentence = `NOBODY can use this portal, per ${serviceEnvFile}: the grants file gives nobody access`
|
|
2228
|
+
+ `${grantsFile ? ` (${grantsFile})` : " (CLEAROTRON_ACCESS_FILE is unset)"}. `
|
|
2141
2229
|
+ "Any identity that signs in is refused at the door on every page, which reads as a broken "
|
|
2142
2230
|
+ `login rather than as missing access. Fix with \`${invocationPrefix()}clearotron grant add\`, `
|
|
2143
|
-
+
|
|
2231
|
+
+ `or give the person who installed an entry under "people" in that file, with "everything": true.`;
|
|
2144
2232
|
if (configured) problem(sentence); else info(sentence);
|
|
2145
2233
|
} else {
|
|
2146
|
-
|
|
2147
|
-
if (staffDomains.length) who.push(`${staffDomains.length} staff domain(s): ${staffDomains.join(", ")}`);
|
|
2148
|
-
if (rows) who.push(`${rows} guest-list row(s)`);
|
|
2149
|
-
ok(`somebody can use this portal — ${who.join(", ")}`);
|
|
2234
|
+
ok(`somebody can use this portal — ${admitted.length} address(es) in the grants file have access`);
|
|
2150
2235
|
// AND THE ONE IDENTITY THIS BOX SIGNS IN, when the local door is what this environment implies.
|
|
2151
2236
|
// NAMES the address because it is this operator's own, on their own box, in a report they asked
|
|
2152
2237
|
// for — the same address `clearotron start` prints back at them.
|
|
2153
2238
|
const localUser = effectiveForService("PORTAL_LOCAL_USER")?.v ?? "";
|
|
2154
2239
|
if (door.shape === "local" && localUser) {
|
|
2155
|
-
if (makePrincipal({ email: localUser, grants
|
|
2156
|
-
else problem(` but ${localUser} — the identity this box's local sign-in produces —
|
|
2157
|
-
+
|
|
2240
|
+
if (makePrincipal({ email: localUser, grants })) ok(` and ${localUser} is one of them`);
|
|
2241
|
+
else problem(` but ${localUser} — the identity this box's local sign-in produces — has no access in `
|
|
2242
|
+
+ `${grantsFile}, so it signs in and is then refused on every page. Give it an entry under "people" there.`);
|
|
2158
2243
|
}
|
|
2159
2244
|
}
|
|
2160
2245
|
} catch (e) {
|
|
@@ -2518,8 +2603,8 @@ export async function runCheck() {
|
|
|
2518
2603
|
// rather than beside the unit, because the unit running and the address being reachable are
|
|
2519
2604
|
// different facts and the second is the one a client depends on.
|
|
2520
2605
|
const { clientDoorReachability } = await import(pathToFileURL(join(REPO, "shared", "client-door.mjs")).href);
|
|
2521
|
-
// FROM THE FILE THE UNITS LOAD, NOT THE SHELL THIS COMMAND WAS TYPED IN (the
|
|
2522
|
-
//
|
|
2606
|
+
// FROM THE FILE THE UNITS LOAD, NOT THE SHELL THIS COMMAND WAS TYPED IN (the same family,
|
|
2607
|
+
// met again here). On a hosted box the published address lives in the units' environment,
|
|
2523
2608
|
// and reading `process.env` reported "no client connector address is published — that is correct
|
|
2524
2609
|
// for a local install" about a deployment that publishes one. Every verdict below rests on this
|
|
2525
2610
|
// value, including the audience comparison, so a wrong reading here is not one wrong line.
|
|
@@ -2539,7 +2624,7 @@ export async function runCheck() {
|
|
|
2539
2624
|
// returns null when the header is absent — a looked-and-none answer, not a did-not-look — and
|
|
2540
2625
|
// the readers separate those, so a probe that omits the field reads as never-looked rather
|
|
2541
2626
|
// than silently as "no challenge".
|
|
2542
|
-
// THE REDIRECT TARGET TRAVELS WITH IT
|
|
2627
|
+
// THE REDIRECT TARGET TRAVELS WITH IT. This request is already made with
|
|
2543
2628
|
// `redirect: "manual"`, so the Location an unauthenticated caller is handed is right here —
|
|
2544
2629
|
// and it is where Cloudflare Access puts the audience. Reading it from THIS response rather
|
|
2545
2630
|
// than asking a second time keeps `doctor` to one request per address, and keeps both answers
|
|
@@ -2553,7 +2638,7 @@ export async function runCheck() {
|
|
|
2553
2638
|
else if (reach.state === "fail") problem(reach.message);
|
|
2554
2639
|
else info(reach.message);
|
|
2555
2640
|
|
|
2556
|
-
// ── AND IS IT THE AUDIENCE THIS INSTALL CHECKS AGAINST?
|
|
2641
|
+
// ── AND IS IT THE AUDIENCE THIS INSTALL CHECKS AGAINST? ───────────────────────────────────────
|
|
2557
2642
|
//
|
|
2558
2643
|
// The other half of the recreation trap, and the half with no symptom of its own. Deleting and
|
|
2559
2644
|
// recreating an Access application changes the audience; the existing warning fires on the
|
|
@@ -2577,7 +2662,7 @@ export async function runCheck() {
|
|
|
2577
2662
|
// unreachable; repeating it as a second failure teaches the reader that this section
|
|
2578
2663
|
// double-counts. It still states what was NOT established, which is the whole job.
|
|
2579
2664
|
else if (v.kind === "could-not-look") info(v.message);
|
|
2580
|
-
// NO ACCESS IN FRONT OF THE CLIENT DOOR IS A POSTURE, NOT A FAULT (
|
|
2665
|
+
// NO ACCESS IN FRONT OF THE CLIENT DOOR IS A POSTURE, NOT A FAULT (ruling 2026-09-08).
|
|
2581
2666
|
//
|
|
2582
2667
|
// This is the CLIENT connector's address, and how a client reaches it is the client's decision:
|
|
2583
2668
|
// "client access sitting behind OAuth is totally up to a client — plenty might just run it token
|
|
@@ -2602,7 +2687,7 @@ export async function runCheck() {
|
|
|
2602
2687
|
}
|
|
2603
2688
|
}
|
|
2604
2689
|
|
|
2605
|
-
// ── CAN AN ASSISTANT ACTUALLY SIGN IN? (
|
|
2690
|
+
// ── CAN AN ASSISTANT ACTUALLY SIGN IN? (opt-in) ───────────────────────────────────────────────
|
|
2606
2691
|
//
|
|
2607
2692
|
// The check above reads the CHALLENGE FORM, which is the first of two provider settings that decide
|
|
2608
2693
|
// this. The second — whether the vendor's own redirect address may register — has no symptom a
|
|
@@ -2799,7 +2884,7 @@ try {
|
|
|
2799
2884
|
// is DERIVED: the engines from ENGINE_BINARIES, the search vendors from the same adapter tables the
|
|
2800
2885
|
// prompts loop over, each wearing its own table row's where-to-get line. A hand-kept copy here would
|
|
2801
2886
|
// be the SERPAPI defect again, one screen earlier. Everything is skippable; the list says so.
|
|
2802
|
-
// THE BANNER — once, on entry, named (
|
|
2887
|
+
// THE BANNER — once, on entry, named (ruling 2026-08-31). Not on every
|
|
2803
2888
|
// command: a banner every verb prints is how a product becomes tiresome to use twice. It sits here
|
|
2804
2889
|
// rather than at the top of main() so that `--check`, which writes nothing and is run repeatedly,
|
|
2805
2890
|
// does not wear it either.
|
|
@@ -3052,8 +3137,8 @@ try {
|
|
|
3052
3137
|
// the run has started. So setup will not write an engine it has not exercised — and the menu's last
|
|
3053
3138
|
// row exists so that refusal always has somewhere to go.
|
|
3054
3139
|
say("");
|
|
3055
|
-
// —
|
|
3056
|
-
//
|
|
3140
|
+
// — the three lines this replaces were not understood by the reader they were written for.
|
|
3141
|
+
// They named `.env` and "stage failure" and "wearing the shape of a model
|
|
3057
3142
|
// fault" at a reader who is about to answer yes or no, and every one of those is our vocabulary.
|
|
3058
3143
|
//
|
|
3059
3144
|
// THE CHECK IS UNCHANGED AND THE REFUSAL IS UNCHANGED. He valued both, and the issue puts them out
|
|
@@ -3162,7 +3247,7 @@ try {
|
|
|
3162
3247
|
for (const l of spec.signup) say(` ${l}`);
|
|
3163
3248
|
say("");
|
|
3164
3249
|
}
|
|
3165
|
-
// ── — INSTALL MAY FINISH WITH NO REGISTER.
|
|
3250
|
+
// ── — INSTALL MAY FINISH WITH NO REGISTER. Ruling, 2026-08-26 ──────────────────────
|
|
3166
3251
|
//
|
|
3167
3252
|
// Every row of PROVIDERS declares required credentials, and this prompt had no way out, so a reader
|
|
3168
3253
|
// with no vendor account could not reach the closing screen at all — the menu offers no "none" row,
|
|
@@ -3248,8 +3333,8 @@ try {
|
|
|
3248
3333
|
}
|
|
3249
3334
|
}
|
|
3250
3335
|
if (registerSelected && !spec.validateEuipo && !spec.uspToLocalKey) {
|
|
3251
|
-
// —
|
|
3252
|
-
//
|
|
3336
|
+
// — decided: validate the register provider quickly where that is cheap, and where it is not,
|
|
3337
|
+
// set the fallback so a check that cannot be made says in ONE LINE why it
|
|
3253
3338
|
// cannot be checked AND what happens if it is wrong. The first half was already here; the second was
|
|
3254
3339
|
// not, so a reader was told about our billing scruple and nothing about their own risk.
|
|
3255
3340
|
//
|
|
@@ -3371,7 +3456,7 @@ try {
|
|
|
3371
3456
|
ok(`CLEAROTRON_REPORTS_URL=${trimmed}`);
|
|
3372
3457
|
}
|
|
3373
3458
|
|
|
3374
|
-
// 7a-bis ── THE ADDRESS CLIENTS REACH THE CONNECTOR AT (
|
|
3459
|
+
// 7a-bis ── THE ADDRESS CLIENTS REACH THE CONNECTOR AT (ruling 2026-09-03, Q2)
|
|
3375
3460
|
//
|
|
3376
3461
|
// *"The installer asks once, at install, on the box. It is a deployment setting, never a per-person
|
|
3377
3462
|
// one. Changing it later is editing that one setting and restarting."*
|
|
@@ -3501,21 +3586,18 @@ try {
|
|
|
3501
3586
|
say(" fallback — falls through by name, so an empty store is a working install. Your own customers");
|
|
3502
3587
|
say(" are added here by name; the bundled demo customers never show through into your roster.");
|
|
3503
3588
|
|
|
3504
|
-
// 7c ── WHO SIGNS IN, AND WHAT
|
|
3589
|
+
// 7c ── WHO SIGNS IN, AND WHAT THEIR ORGANISATION IS CALLED
|
|
3505
3590
|
//
|
|
3506
|
-
//
|
|
3507
|
-
//
|
|
3508
|
-
// documentation domain to a reader who had never been asked, is on `askSignIn` above.
|
|
3591
|
+
// The address is the first person on the install and the name is its first organisation. The whole
|
|
3592
|
+
// reasoning, including the domain rule this step no longer writes, is on `askSignIn` above.
|
|
3509
3593
|
section("Who signs in");
|
|
3510
3594
|
const localAccount = (() => {
|
|
3511
3595
|
try { return userInfo().username || "user"; } catch { return "user"; }
|
|
3512
3596
|
})();
|
|
3513
|
-
// ONE CALL SITE. The loop itself lives in `askSignIn` so that the
|
|
3514
|
-
//
|
|
3515
|
-
//
|
|
3516
|
-
Object.assign(candidate, await askSignIn(
|
|
3517
|
-
{ askValue, confirm, say, ok, info, warn, problem },
|
|
3518
|
-
{ localAccount, staffLabel: `${BRAND.name} staff`, envPath: ENV_PATH }));
|
|
3597
|
+
// ONE CALL SITE. The loop itself lives in `askSignIn` so that the branches that matter — an address
|
|
3598
|
+
// that must be sent back, a name that is missing — are reachable without a terminal. The same seam
|
|
3599
|
+
// and the same reason as `offerUsptoSync`.
|
|
3600
|
+
Object.assign(candidate, await askSignIn({ askValue, ok, problem }, { localAccount }));
|
|
3519
3601
|
|
|
3520
3602
|
// 8 ── the engine's own preflight over the whole candidate
|
|
3521
3603
|
//
|
|
@@ -3548,7 +3630,7 @@ try {
|
|
|
3548
3630
|
// 9 ── write, atomically
|
|
3549
3631
|
say("\n Writing configuration");
|
|
3550
3632
|
// THE DIRECTORY MAY NOT EXIST, and on a fresh machine it does not. `.env` now lives under
|
|
3551
|
-
// `~/.config/clearotron
|
|
3633
|
+
// `~/.config/clearotron/`, which nothing else creates — and the failure without
|
|
3552
3634
|
// this line lands on the temporary file below, so it reads as a permissions problem writing `.env`
|
|
3553
3635
|
// rather than a missing folder. Mode 700: the file inside is 600 and holds credentials, so a
|
|
3554
3636
|
// world-readable directory around it advertises that it is there.
|
|
@@ -3577,7 +3659,7 @@ try {
|
|
|
3577
3659
|
//
|
|
3578
3660
|
// The owner was given a command, typed it from his home directory, and got npm's error rather than
|
|
3579
3661
|
// ours: `npx` resolves a local package by walking UP from the current directory, so every command
|
|
3580
|
-
// this product prints worked only where the reader happened to be standing.
|
|
3662
|
+
// this product prints worked only where the reader happened to be standing. Ruling 2026-08-26:
|
|
3581
3663
|
// put the verb on PATH, as a per-user shim — `npm link` wants write access to `/usr` and refuses
|
|
3582
3664
|
// without root, which is why stopped at teaching the product to print `npx`.
|
|
3583
3665
|
//
|
|
@@ -3657,7 +3739,7 @@ try {
|
|
|
3657
3739
|
say(` ${style.dim(`\`${invocationPrefix()}clearotron run --job examples/job.euipo.json\` runs a first real clearance on the EU register.`)}`);
|
|
3658
3740
|
say(` ${style.dim("Each still works the old way too — `npm start`, `npm run example`, `node driver/pipeline.mjs`.")}\n`);
|
|
3659
3741
|
|
|
3660
|
-
// WHY THOSE LINES LOOK THE WAY THEY DO, when they are not the bare verb
|
|
3742
|
+
// WHY THOSE LINES LOOK THE WAY THEY DO, when they are not the bare verb.
|
|
3661
3743
|
//
|
|
3662
3744
|
// A login profile adds `~/.local/bin` to PATH only if the directory existed when the shell started,
|
|
3663
3745
|
// so the shim written seconds ago is usually absent from THIS terminal's PATH and arrives at the next
|