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/start.mjs
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
//
|
|
6
6
|
// npx clearotron start start the portal and the trigger lane; print one URL
|
|
7
7
|
// npx clearotron start --user you@host the one address that signs in (persisted; asked for once)
|
|
8
|
+
// npx clearotron start --organisation <name>
|
|
9
|
+
// your organisation's name, filed in a grants file that holds
|
|
10
|
+
// none yet (setup asks for it; unset, none is invented)
|
|
8
11
|
// npx clearotron start --base <dir> where this install keeps its data (default ~/trademark)
|
|
9
12
|
// npx clearotron start --port <n> the portal's port (default 18802); the engine door takes
|
|
10
13
|
// n+1 and the client door n+2 unless each is set explicitly
|
|
@@ -124,15 +127,19 @@ import { unitEnvPath } from "../shared/env-local.mjs"; // — the file the uni
|
|
|
124
127
|
import { homedir, userInfo } from "node:os";
|
|
125
128
|
import { dirname, join } from "node:path";
|
|
126
129
|
import { fileURLToPath } from "node:url";
|
|
127
|
-
import { usageBlock } from "../shared/usage-block.mjs";
|
|
130
|
+
import { usageBlock } from "../shared/usage-block.mjs";
|
|
128
131
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-point test, all spellings
|
|
129
132
|
import { productIdentity } from "../shared/product-identity.mjs"; // AGPL §13 — one answer, three surfaces
|
|
130
133
|
import { pinEnvAll } from "../shared/env-aliases.mjs"; // — a pin that names one spelling has set nothing that wins
|
|
131
134
|
import { BRAND } from "../shared/brand.mjs"; // — the installer's own name, from the tenant seam
|
|
132
135
|
import { rebuildIfStale } from "../shared/bundle-rebuild.mjs"; // never serve a bundle older than its sources
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
import {
|
|
136
|
+
// THE REFUSALS ABOUT THE SIGN-IN ADDRESS ITSELF, shared with the setup wizard so the two cannot send
|
|
137
|
+
// back different addresses.
|
|
138
|
+
import { addressRefusal } from "../shared/staff-domain.mjs";
|
|
139
|
+
// The grants file's editors, shared with `clearotron grant` and the portal's People page: the installer's
|
|
140
|
+
// entry is written through them, so this command cannot produce a shape of its own.
|
|
141
|
+
import { withPerson, withOrganisation } from "../shared/grants-edit.mjs";
|
|
142
|
+
import { assertGrantsShape, resolvePerson } from "../shared/scope.mjs";
|
|
136
143
|
import { backgroundManager } from "../shared/os-advice.mjs";
|
|
137
144
|
import { frontingVariablesSet } from "../shared/install-auth.mjs"; // — one owner for what counts as a proxy in front of a door
|
|
138
145
|
|
|
@@ -175,7 +182,7 @@ export function homeEnvUpdate(homeText, union) {
|
|
|
175
182
|
}
|
|
176
183
|
|
|
177
184
|
/**
|
|
178
|
-
* What a `--background` run has ALREADY DONE when systemd refuses to enable a unit
|
|
185
|
+
* What a `--background` run has ALREADY DONE when systemd refuses to enable a unit.
|
|
179
186
|
*
|
|
180
187
|
* THE READER'S QUESTION IS NOT THAT A STEP FAILED. It is whether they now have a half-installed
|
|
181
188
|
* deployment, and whether to run this again, undo it, or leave it alone. This step is the worst-placed
|
|
@@ -214,7 +221,7 @@ const HOST = "127.0.0.1";
|
|
|
214
221
|
// ── pure helpers (exported for driver/test/start-command.test.mjs) ───────────────────────────────────
|
|
215
222
|
|
|
216
223
|
/**
|
|
217
|
-
* WHOSE socket is on the client door's port
|
|
224
|
+
* WHOSE socket is on the client door's port.
|
|
218
225
|
*
|
|
219
226
|
* `clearotron stop` removes three units and DELIBERATELY leaves the client door running, because a
|
|
220
227
|
* product stop must not silently revoke an assistant's connection. `start` then manages that same door
|
|
@@ -294,7 +301,7 @@ export function resolvePorts(env = {}) {
|
|
|
294
301
|
}
|
|
295
302
|
|
|
296
303
|
/**
|
|
297
|
-
* Apply `--port <n>` to the three doors
|
|
304
|
+
* Apply `--port <n>` to the three doors.
|
|
298
305
|
*
|
|
299
306
|
* PURE, AND EXPORTED, because the inline version of this could only be tested by spawning a supervisor
|
|
300
307
|
* and reading `ss`. The defect it fixes was found by a reader watching a log line, which is exactly
|
|
@@ -371,74 +378,43 @@ export function installPaths(base) {
|
|
|
371
378
|
};
|
|
372
379
|
}
|
|
373
380
|
|
|
374
|
-
/** A staff rule this command will not build. Carries the sentence the operator reads, and why. */
|
|
375
|
-
export class StaffDomainRefused extends Error {
|
|
376
|
-
constructor(domain, message) { super(message); this.name = "StaffDomainRefused"; this.domain = domain; }
|
|
377
|
-
}
|
|
378
|
-
|
|
379
381
|
/**
|
|
380
|
-
* The
|
|
382
|
+
* The grants file after this start has made sure the person who installed can use the install.
|
|
381
383
|
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
* deliberately blind to which door you came through).
|
|
384
|
+
* THE PERSON WHO INSTALLS IS THE FIRST PERSON: Run, Manage, access to everything. That used to be derived
|
|
385
|
+
* from the part of their address after the `@` — a domain rule that admitted exactly one identity on
|
|
386
|
+
* `<account>@localhost` and a whole employer on anything else. The rule is deleted. The installer is an
|
|
387
|
+
* entry of their own under `people`, and nothing about a domain admits anyone.
|
|
387
388
|
*
|
|
388
|
-
*
|
|
389
|
+
* TWO CONDITIONS, DECIDED SEPARATELY:
|
|
389
390
|
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
391
|
+
* the installer's entry written when the file names nobody under `people` — a fresh file, and the
|
|
392
|
+
* `{"tenants":{}}` this command wrote before `people` existed, whose local user
|
|
393
|
+
* was admitted by the domain rule and would otherwise be locked out of their own
|
|
394
|
+
* install on the first start after an upgrade. A file that names anybody is
|
|
395
|
+
* somebody's decision, and it is left exactly as it is.
|
|
396
|
+
* the first organisation created when the file holds no organisation and a name is known. Absent a
|
|
397
|
+
* name, none is invented: a person with access to everything resolves without
|
|
398
|
+
* any organisation, and Generic then files under none, which is how every
|
|
399
|
+
* Generic run was filed before organisations existed.
|
|
396
400
|
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
* question the wizard asks and the rule this command builds cannot drift apart.
|
|
401
|
-
*
|
|
402
|
-
* An UNPARSEABLE address still returns `""`, and that is not the same answer as a refusal: nothing was
|
|
403
|
-
* classified, so nothing was decided. The caller already refuses a non-address one line earlier.
|
|
401
|
+
* PURE, so the policy can be DRIVEN rather than read — the reason `homeEnvUpdate` is extracted: the start
|
|
402
|
+
* that writes this binds ports and spawns children. `unadmitted` says the address resolves to no access
|
|
403
|
+
* in the result; the caller reports that rather than repairing it.
|
|
404
404
|
*/
|
|
405
|
-
export function
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
if (!
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
* a script, from `--background`, and from a service manager as often as from a terminal; a question
|
|
419
|
-
* asked there either hangs or is answered by whatever happens to be on stdin. Every other decision on
|
|
420
|
-
* this boundary already refuses instead — an unset access file, a multi-`@` identity, local mode off
|
|
421
|
-
* loopback — so this is the idiom the rest of the door speaks.
|
|
422
|
-
*
|
|
423
|
-
* The consent itself lives where a person is definitionally present: `clearotron install` asks for the
|
|
424
|
-
* address, shows this same sentence, and writes the answer down as an explicit `PORTAL_STAFF_DOMAINS`.
|
|
425
|
-
* After that the `||` above this call short-circuits and the derivation is never reached again.
|
|
426
|
-
*/
|
|
427
|
-
export function wideStaffDomainRefusal({ user, domain, envPath }) {
|
|
428
|
-
return `${user} would make every address at ${domain} an administrator of this install.\n`
|
|
429
|
-
+ "\n"
|
|
430
|
-
+ ` The rule that would be written is: ${staffGrantSentence(domain, { staffLabel: `${BRAND.name} staff` })}\n`
|
|
431
|
-
+ "\n"
|
|
432
|
-
+ " That is a grant to a group, and this command will not make one on your behalf. Pick the one\n"
|
|
433
|
-
+ " that is true here:\n"
|
|
434
|
-
+ "\n"
|
|
435
|
-
+ ` · only you use this machine — start without --user, or use ${String(user).slice(0, String(user).lastIndexOf("@"))}@localhost.\n`
|
|
436
|
-
+ ` The rule is then this machine and nobody else, and nothing is granted to anyone.\n`
|
|
437
|
-
+ ` · your colleagues at ${domain} should all be administrators — say so once, in writing:\n`
|
|
438
|
-
+ ` PORTAL_STAFF_DOMAINS=${domain}\n`
|
|
439
|
-
+ ` in ${envPath}, or in the environment. The settings page then names that file as where the\n`
|
|
440
|
-
+ " rule came from, so whoever reads it later can undo it.\n"
|
|
441
|
-
+ ` · run \`${invocationPrefix()}clearotron install\`, which asks for the address and this question with it.`;
|
|
405
|
+
export function installerGrants(existing, { user, organisation = null }) {
|
|
406
|
+
let grants = existing ?? { tenants: {} };
|
|
407
|
+
const changed = [];
|
|
408
|
+
if (!Object.keys(grants.people ?? {}).length) {
|
|
409
|
+
grants = withPerson(grants, { email: user, switches: { run: true, manage: true, everything: true } });
|
|
410
|
+
changed.push("person");
|
|
411
|
+
}
|
|
412
|
+
const name = String(organisation ?? "").trim();
|
|
413
|
+
if (name && !Object.keys(grants.tenants ?? {}).length) {
|
|
414
|
+
({ grants } = withOrganisation(grants, { name }));
|
|
415
|
+
changed.push("organisation");
|
|
416
|
+
}
|
|
417
|
+
return { grants, changed, unadmitted: resolvePerson(user, grants) === null };
|
|
442
418
|
}
|
|
443
419
|
|
|
444
420
|
/**
|
|
@@ -495,7 +471,7 @@ export const BACKGROUND_RETIRED = Object.freeze([]);
|
|
|
495
471
|
export const BACKGROUND_EXCLUDED = Object.freeze({
|
|
496
472
|
// ── `clearotron-client-mcp.service` LEFT THIS TABLE ON 2026-09-03, AND SAYING SO IS THE POINT ────
|
|
497
473
|
//
|
|
498
|
-
// It was the rebuild-seam gate of record (
|
|
474
|
+
// It was the rebuild-seam gate of record (rulings): starting the unit
|
|
499
475
|
// WAS the on-demand consent, because starting it turned on client-account access, so an enable list
|
|
500
476
|
// that included it would have made that consent meaningless. That reasoning was right under that
|
|
501
477
|
// ruling and the exclusion was not an oversight.
|
|
@@ -514,7 +490,7 @@ export const BACKGROUND_EXCLUDED = Object.freeze({
|
|
|
514
490
|
"profile-service.service": "the portal constructs the profile service IN-PROCESS (driver/portal-service.mjs); the standalone unit is the separate-editor deployment shape and running both double-serves the store",
|
|
515
491
|
});
|
|
516
492
|
|
|
517
|
-
export function childEnv({ ports, paths, user,
|
|
493
|
+
export function childEnv({ ports, paths, user, portalSecret, tokenSecret, opsToken, host = HOST, localWorker = false, demo = false, clientFence = null, env = process.env }) {
|
|
518
494
|
// ONE AUTHOR FOR THIS EXPRESSION. The hosted install path composes the same
|
|
519
495
|
// origin, and the near-miss is specific: this is an ORIGIN, the portal's client appends `/mcp`
|
|
520
496
|
// itself, and a second author writing the endpoint form produces a doubled path — a 404 at submit
|
|
@@ -577,7 +553,7 @@ export function childEnv({ ports, paths, user, staffDomains, portalSecret, token
|
|
|
577
553
|
worker: { ...shared },
|
|
578
554
|
// ── THE CLIENT DOOR, ON BOTH PATHS ( — F26) ──────────────────────────────
|
|
579
555
|
//
|
|
580
|
-
//
|
|
556
|
+
// Ruling, restated several times in session: START BOTH. It already held on the systemd path
|
|
581
557
|
// — the door is in SERVER_INSTALL_SET on the 2148 ruling that the door auto-starts and the
|
|
582
558
|
// PER-ACCOUNT KEY is the gate — and it did not hold on the foreground path, with nothing saying
|
|
583
559
|
// which of the two you were on. The owner spent the leg believing MCP had not started at all; it
|
|
@@ -628,7 +604,6 @@ export function childEnv({ ports, paths, user, staffDomains, portalSecret, token
|
|
|
628
604
|
...shared,
|
|
629
605
|
PORTAL_AUTH_MODE: "local",
|
|
630
606
|
PORTAL_LOCAL_USER: user,
|
|
631
|
-
PORTAL_STAFF_DOMAINS: staffDomains,
|
|
632
607
|
PORTAL_SECRET: portalSecret,
|
|
633
608
|
PORTAL_SERVICE_HOST: host,
|
|
634
609
|
PORTAL_SERVICE_PORT: String(ports.portal),
|
|
@@ -637,7 +612,7 @@ export function childEnv({ ports, paths, user, staffDomains, portalSecret, token
|
|
|
637
612
|
PORTAL_OPS_TOKEN: opsToken,
|
|
638
613
|
// — the demo runs THIS portal, not a second one. What that difference IS
|
|
639
614
|
// has changed: the products are orderable and the confirmation resolves to a report that already
|
|
640
|
-
// exists (
|
|
615
|
+
// exists (ruling 2026-08-31 14:47, superseding the greyed-control ruling of 14:44).
|
|
641
616
|
//
|
|
642
617
|
// The FLAG moved to `shared` and is `CLEAROTRON_DEMO`, because the
|
|
643
618
|
// portal is no longer the only process that has to know: the MCP door prints a boot warning aimed
|
|
@@ -687,7 +662,7 @@ if (isMain) {
|
|
|
687
662
|
const markStateWritten = () => { wroteState = true; };
|
|
688
663
|
// `stated` is a caller that has already said what stands, in terms this generic line cannot reach —
|
|
689
664
|
// which unit refused, which ones are up, what re-running does. Without it the systemd refusal printed
|
|
690
|
-
// both, and the pair read as two different answers to the reader's one question
|
|
665
|
+
// both, and the pair read as two different answers to the reader's one question.
|
|
691
666
|
const fatal = (msg, { stated = false } = {}) => {
|
|
692
667
|
err(`\nstart: ${msg}\n`);
|
|
693
668
|
if (wroteState && !stated) err(" This run had already written state (env file, data directories, grants, seeded example).\n Every one of those writes is idempotent — re-running `clearotron start` is safe and nothing needs undoing.\n");
|
|
@@ -749,7 +724,7 @@ if (isMain) {
|
|
|
749
724
|
// be the same silent downgrade refused to build.
|
|
750
725
|
// ── A BOX WITH THE SERVICES INSTALLED HAS ONE CONFIGURATION, AND IT IS NOT THIS ONE ───────────────
|
|
751
726
|
//
|
|
752
|
-
//
|
|
727
|
+
// Ruling,: "whatever is cleanest, simplest and industry-standard expected" —
|
|
753
728
|
// and yes to refusing outright. A box carrying the shipped units is a SERVER: its configuration is the
|
|
754
729
|
// units' `EnvironmentFile` (`%h/.env`), and this command's is `<repo>/.env`, the hand-run laptop file.
|
|
755
730
|
//
|
|
@@ -826,7 +801,7 @@ if (isMain) {
|
|
|
826
801
|
// product's own, and the confirmation resolves to a finished report that
|
|
827
802
|
// already exists — no engine turn, no register call, no queue entry, no run
|
|
828
803
|
// directory. A product the demo carries no report for refuses and names which.
|
|
829
|
-
// (
|
|
804
|
+
// (Ruling 2026-08-31 14:47, superseding the greyed-control ruling of
|
|
830
805
|
// 14:44 the same day: "a demo that shows four finished reports and a dead
|
|
831
806
|
// button demonstrates the output and hides the thing a buyer is deciding
|
|
832
807
|
// about". The greyed control was "a viewer creeping back in".)
|
|
@@ -838,11 +813,11 @@ if (isMain) {
|
|
|
838
813
|
// bound anywhere else in any mode. Sign-in is untouched — the demo signs in like any first start, and
|
|
839
814
|
// the portal mints and prints its passphrase exactly as it does for a real one.
|
|
840
815
|
const DEMO = argv.includes("--demo");
|
|
841
|
-
// THE DEMO BRINGS ITS OWN ACCOUNT. A fresh install resolves `generic` and nothing else (
|
|
842
|
-
//
|
|
816
|
+
// THE DEMO BRINGS ITS OWN ACCOUNT. A fresh install resolves `generic` and nothing else (ruling,
|
|
817
|
+
// 2026-09-08), so the demo account is refused from the roster unless somebody asked for it.
|
|
843
818
|
// Asked here, once and visibly, rather than at each site that happens to read a roster.
|
|
844
819
|
if (DEMO) process.env.CLEAROTRON_DEMO_PROFILES ??= "1";
|
|
845
|
-
// ── `--port` MOVES EVERY DOOR IT OPENS
|
|
820
|
+
// ── `--port` MOVES EVERY DOOR IT OPENS ───────────────────────────────────────────────────────────
|
|
846
821
|
//
|
|
847
822
|
// It used to move ONE of the three. `resolvePorts` reads three independent variables with three
|
|
848
823
|
// fixed defaults, and this line set only the portal's — so `demo --port 18860` put the portal on
|
|
@@ -865,7 +840,7 @@ if (isMain) {
|
|
|
865
840
|
Object.assign(ports, moved);
|
|
866
841
|
}
|
|
867
842
|
|
|
868
|
-
// ── A BUNDLE OLDER THAN ITS SOURCES IS REBUILT, NOT SERVED
|
|
843
|
+
// ── A BUNDLE OLDER THAN ITS SOURCES IS REBUILT, NOT SERVED ───────────────────────────────────────
|
|
869
844
|
//
|
|
870
845
|
// Only reachable on a source checkout: `portal-ui/dist` is untracked there, so a `git pull` that
|
|
871
846
|
// changed `portal-ui/src` leaves the bundle behind and every surface still reads healthy. A packaged
|
|
@@ -879,45 +854,32 @@ if (isMain) {
|
|
|
879
854
|
|
|
880
855
|
// The ADDRESS is not asked for here: a local install has one user, this machine already knows their
|
|
881
856
|
// name, and the address never leaves the machine — `--user` is there for a reader who wants their real
|
|
882
|
-
// one, and whatever is resolved here is written to `.env` so the question is never put twice.
|
|
883
|
-
//
|
|
884
|
-
//
|
|
857
|
+
// one, and whatever is resolved here is written to `.env` so the question is never put twice. It is the
|
|
858
|
+
// first person on the install (`installerGrants`, below); the part of it after the `@` decides nothing
|
|
859
|
+
// about anybody else.
|
|
885
860
|
let whoami = "user";
|
|
886
861
|
try { whoami = userInfo().username || "user"; } catch { /* a container with no passwd entry */ }
|
|
887
862
|
// In a demo the address is the demo's own and is never written anywhere: see the DEMO block above.
|
|
888
863
|
const user = String(flag("--user", DEMO ? "demo@localhost" : (process.env.PORTAL_LOCAL_USER || `${whoami}@localhost`))).trim().toLowerCase();
|
|
889
864
|
if (!user.includes("@") || user.indexOf("@") !== user.lastIndexOf("@"))
|
|
890
865
|
fatal(`--user "${user}" is not a single email address. It is the one identity that signs in here, and the portal refuses a multi-@ identity outright.`);
|
|
891
|
-
// ── THE
|
|
892
|
-
//
|
|
893
|
-
// An explicit `PORTAL_STAFF_DOMAINS` is a decision somebody already made in writing, and it wins
|
|
894
|
-
// untouched — including a domain the classifier would otherwise refuse, because an operator who
|
|
895
|
-
// typed it has said what they mean and this command does not overrule that.
|
|
896
|
-
//
|
|
897
|
-
// Absent one, the rule is DERIVED from the single address, and that derivation is the defect this
|
|
898
|
-
// block exists to close. Three outcomes:
|
|
866
|
+
// ── THE REFUSALS ABOUT THE ADDRESS ITSELF, the wizard's own (shared/staff-domain.mjs) ──────────────
|
|
899
867
|
//
|
|
900
|
-
//
|
|
901
|
-
//
|
|
902
|
-
// one a laptop ever reaches.
|
|
903
|
-
// a webmail or documentation domain — refused by `staffDomainFor` itself; there is no yes that
|
|
904
|
-
// makes it right.
|
|
905
|
-
// any other real domain — refused HERE, with the rule quoted in the words the
|
|
906
|
-
// settings page uses and the three ways out named.
|
|
868
|
+
// A public provider or a documentation domain is sent back in the words `clearotron install` uses for
|
|
869
|
+
// the same address, with where this one came from, because that is the value the reader changes.
|
|
907
870
|
//
|
|
908
871
|
// BEFORE THIS RUN CHANGES THE BOX, deliberately — it sits above the state-written divide further
|
|
909
872
|
// down, with the units gate, the auth-mode gate and the port probe. A refused start has written no
|
|
910
873
|
// `.env`, minted no secret and placed no unit, so the reader's only question — is my install
|
|
911
874
|
// half-made — has one answer.
|
|
912
|
-
|
|
913
|
-
if (
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
}
|
|
875
|
+
const refusal = addressRefusal(user);
|
|
876
|
+
if (refusal) fatal(`${refusal}\n This address came from ${flag("--user") ? "--user" : `PORTAL_LOCAL_USER, in the environment or ${ENV_PATH}`}.`);
|
|
877
|
+
// YOUR ORGANISATION'S NAME, which `clearotron install` asks for directly after the address and writes
|
|
878
|
+
// as CLEAROTRON_ORGANISATION_NAME. It is read only to file the first organisation into a grants file
|
|
879
|
+
// that holds none (`installerGrants`); after that the grants file is where the name lives, and renaming
|
|
880
|
+
// it is an edit there. A DEMO TAKES NONE FROM THE SETTING, for the reason it takes no address from it:
|
|
881
|
+
// the reader's real install must not decide what the demo shows.
|
|
882
|
+
const organisation = String(flag("--organisation", DEMO ? "" : (process.env.CLEAROTRON_ORGANISATION_NAME ?? "")) ?? "").trim();
|
|
921
883
|
|
|
922
884
|
say("");
|
|
923
885
|
say(` ${BRAND.name} ${BRAND.product.toLowerCase()} — local install`);
|
|
@@ -958,9 +920,9 @@ if (isMain) {
|
|
|
958
920
|
// down, which is what the paragraph above says this check is for.
|
|
959
921
|
// The file to send the reader to, MEASURED rather than composed — `envFileRead()` reports what
|
|
960
922
|
// shared/env-local.mjs actually did in this process, and answers null when it read nothing. Its own
|
|
961
|
-
// header carries the reasoning
|
|
923
|
+
// header carries the reasoning.
|
|
962
924
|
const portFile = envFileRead();
|
|
963
|
-
// Set when the probe met OUR OWN client door on its own port
|
|
925
|
+
// Set when the probe met OUR OWN client door on its own port. Carried out of this
|
|
964
926
|
// loop because both paths below have to act on it: the background path must RESTART the door it
|
|
965
927
|
// adopted, and the foreground path must not spawn a second one beside it.
|
|
966
928
|
let adoptedClientDoor = false;
|
|
@@ -987,7 +949,7 @@ if (isMain) {
|
|
|
987
949
|
// The narrow carve above already proved every installed unit is ours.
|
|
988
950
|
if (backgroundRefresh) break;
|
|
989
951
|
const code = await probe(port);
|
|
990
|
-
// ── OUR OWN DOOR IS NOT A COLLISION
|
|
952
|
+
// ── OUR OWN DOOR IS NOT A COLLISION ──────────────────────────────────────
|
|
991
953
|
//
|
|
992
954
|
// `stop` leaves this door up on purpose and says so; refusing here made `stop`'s own "plain
|
|
993
955
|
// `clearotron start` works in a terminal from here" a lie, and left `clearotron demo` unrunnable on
|
|
@@ -1006,7 +968,7 @@ if (isMain) {
|
|
|
1006
968
|
// on a privileged port and from an address this host does not have, and names the way out of each;
|
|
1007
969
|
// the launcher having its own shorter sentence for one of the three would mean a user meets two
|
|
1008
970
|
// different answers to the same question depending on which door refused first.
|
|
1009
|
-
// ── A PORT NOBODY CHOSE IS MOVED RATHER THAN REFUSED (
|
|
971
|
+
// ── A PORT NOBODY CHOSE IS MOVED RATHER THAN REFUSED (ruling, 2026-09-09) ─────────────
|
|
1010
972
|
//
|
|
1011
973
|
// Three conditions, and each is a different reason:
|
|
1012
974
|
// · the address is genuinely taken — anything else is not this case;
|
|
@@ -1092,13 +1054,60 @@ if (isMain) {
|
|
|
1092
1054
|
for (const d of [paths.pool, paths.workspace, paths.queue, paths.outbox, paths.locks, paths.recipes]) {
|
|
1093
1055
|
try { mkdirSync(d, { recursive: true }); } catch (e) { fatal(`could not create ${d} (${String(e?.message ?? e)}).`); }
|
|
1094
1056
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1057
|
+
// ── THE GRANTS FILE, AND THE PERSON WHO INSTALLED IN IT ─────────────────────────────────────────────
|
|
1058
|
+
//
|
|
1059
|
+
// Having no file is not legitimate — the portal refuses to start without one, because with no grants
|
|
1060
|
+
// file every admitted identity resolves to every account. `installerGrants` holds the policy for what
|
|
1061
|
+
// goes in it; this block reads, writes and says what happened.
|
|
1062
|
+
//
|
|
1063
|
+
// READ AND CHECKED FIRST, with the portal's own shape check: a file the portal will refuse is named
|
|
1064
|
+
// here, where the reader can still fix it, rather than as a child that exits after the doors are up.
|
|
1065
|
+
//
|
|
1066
|
+
// A DEMO WRITES ONLY ITS OWN FILE. The demo visitor is simply the first person on a box holding only
|
|
1067
|
+
// demo data, so it gets the installer's entry — in the demo's base and nowhere else. An environment
|
|
1068
|
+
// pointing CLEAROTRON_ACCESS_FILE elsewhere names a real install's roster, and a demo identity with
|
|
1069
|
+
// access to everything does not belong in it.
|
|
1070
|
+
{
|
|
1071
|
+
let existing = null;
|
|
1072
|
+
if (existsSync(paths.grants)) {
|
|
1073
|
+
try {
|
|
1074
|
+
existing = JSON.parse(readFileSync(paths.grants, "utf8"));
|
|
1075
|
+
assertGrantsShape(existing, paths.grants);
|
|
1076
|
+
} catch (e) {
|
|
1077
|
+
fatal(`the grants file at ${paths.grants} cannot be used: ${String(e?.message ?? e)}\n`
|
|
1078
|
+
+ " The portal refuses to start on it as well. Fix that entry by hand, or set CLEAROTRON_ACCESS_FILE to the file you mean.");
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
if (DEMO && paths.grants !== installPaths(paths.base).grants) {
|
|
1082
|
+
err(` WARNING: CLEAROTRON_ACCESS_FILE points this demo at ${paths.grants}, outside ${paths.base}. The demo `
|
|
1083
|
+
+ `writes nothing there, so ${user} has no access unless that file already gives it some`
|
|
1084
|
+
+ `${existing ? "" : " — and it does not exist, so the portal will refuse to start"}. `
|
|
1085
|
+
+ "Unset CLEAROTRON_ACCESS_FILE to run the demo on its own file.");
|
|
1086
|
+
} else {
|
|
1087
|
+
let seeded;
|
|
1088
|
+
try { seeded = installerGrants(existing, { user, organisation }); }
|
|
1089
|
+
catch (e) { fatal(`could not add ${user} to the grants file at ${paths.grants} (${String(e?.message ?? e)}).`); }
|
|
1090
|
+
if (!existing || seeded.changed.length) {
|
|
1091
|
+
// ATOMIC, because a --background refresh runs beside units that read this file per request, and a
|
|
1092
|
+
// reader catching half of it gets malformed JSON.
|
|
1093
|
+
const { atomicWrite } = await import("../driver/progress.mjs");
|
|
1094
|
+
try { atomicWrite(paths.grants, `${JSON.stringify(seeded.grants, null, 2)}\n`); }
|
|
1095
|
+
catch (e) { fatal(`could not write the grants file at ${paths.grants} (${String(e?.message ?? e)}).`); }
|
|
1096
|
+
}
|
|
1097
|
+
if (!existing) say(` created ${paths.grants} — ${user}: Run, Manage, access to everything`);
|
|
1098
|
+
else if (seeded.changed.includes("person"))
|
|
1099
|
+
say(` grants ${paths.grants} named nobody, so ${user} was added: Run, Manage, access to everything`);
|
|
1100
|
+
if (seeded.changed.includes("organisation")) say(` organisation "${organisation}", filed there — rename it there too`);
|
|
1101
|
+
else if (seeded.changed.includes("person") && !Object.keys(seeded.grants.tenants ?? {}).length)
|
|
1102
|
+
say(` no organisation named yet — \`${invocationPrefix()}clearotron start --organisation "<name>"\` files the first one`);
|
|
1103
|
+
// REPORTED, NOT REPAIRED. A file that names other people is somebody's decision, and adding this
|
|
1104
|
+
// address to it would give access to everything to an address nobody enrolled.
|
|
1105
|
+
if (seeded.unadmitted)
|
|
1106
|
+
err(` WARNING: ${paths.grants} gives ${user} no access, so it signs in and every page refuses it.\n`
|
|
1107
|
+
+ " The file already names other people, so this command leaves it as it is. Add under \"people\":\n"
|
|
1108
|
+
+ ` "${user}": { "run": true, "manage": true, "everything": true }\n`
|
|
1109
|
+
+ " or start with --user set to an address that file gives access.");
|
|
1110
|
+
}
|
|
1102
1111
|
}
|
|
1103
1112
|
// THE REVOCATION LIST, CREATED — not merely named (found in review).
|
|
1104
1113
|
//
|
|
@@ -1165,7 +1174,7 @@ if (isMain) {
|
|
|
1165
1174
|
// ONE sample ships today (a multi-country focus search). Three of the four products have no finished
|
|
1166
1175
|
// run anywhere to freeze — see — so this seeds what exists and picks the rest up unchanged when
|
|
1167
1176
|
// they are captured.
|
|
1168
|
-
// ── F23 — A REAL INSTALL STARTS EMPTY (
|
|
1177
|
+
// ── F23 — A REAL INSTALL STARTS EMPTY (RULING, 2026-09-04) ────────
|
|
1169
1178
|
//
|
|
1170
1179
|
// Owner, in session, on his first real start: "critical, it started and I still see a demo report in
|
|
1171
1180
|
// the actual product. Should not be there — should ONLY be in demo. Proper product should have no
|
|
@@ -1197,7 +1206,7 @@ if (isMain) {
|
|
|
1197
1206
|
// — `clearotron demo` hands over to this — so fixing the player alone left the defect where it was.
|
|
1198
1207
|
const { publishSource } = await import("../driver/demo-container.mjs");
|
|
1199
1208
|
const seed = await seedPool({ pool: paths.pool, examplesDir: publishSource(join(REPO, "demo"), { repoRoot: REPO }), republish: republishRun });
|
|
1200
|
-
// WHAT WAS ALREADY THERE IS SAID TOO
|
|
1209
|
+
// WHAT WAS ALREADY THERE IS SAID TOO. This branch used to run only when the pool
|
|
1201
1210
|
// was empty; it now tops a stale pool up to the package's set, so "seeded 1" on an upgrade is a fact
|
|
1202
1211
|
// about what was MISSING and says nothing on its own about how many are now listed.
|
|
1203
1212
|
if (seed.already?.length) {
|
|
@@ -1252,7 +1261,7 @@ if (isMain) {
|
|
|
1252
1261
|
// process's resolved environment, which is where `<repo>/.env` has already been applied, so a
|
|
1253
1262
|
// decision recorded in that file survives a start rather than being silently overwritten with "1".
|
|
1254
1263
|
const declaredFence = String(process.env.CLIENT_MCP_ACCOUNT_ACCESS ?? "").trim();
|
|
1255
|
-
const envs = childEnv({ ports, paths, user,
|
|
1264
|
+
const envs = childEnv({ ports, paths, user, portalSecret, tokenSecret, opsToken,
|
|
1256
1265
|
localWorker: wantWorker, demo: DEMO, clientFence: declaredFence || null });
|
|
1257
1266
|
|
|
1258
1267
|
// ── 3b. the configuration snapshot, so the portal can name this install's MODE ────────────────────
|
|
@@ -1300,8 +1309,8 @@ if (isMain) {
|
|
|
1300
1309
|
// the processes and thats it its done". The foreground supervisor dies with the terminal — his portal
|
|
1301
1310
|
// was gone between two of his own sessions and every screen spun. The honest fix on a machine with
|
|
1302
1311
|
// systemd is not a detach flag and a pid file; it is the same user units a server runs, installed for
|
|
1303
|
-
// exactly the pinned BACKGROUND_UNITS set (the census arm keeps that pin honest). Since
|
|
1304
|
-
//
|
|
1312
|
+
// exactly the pinned BACKGROUND_UNITS set (the census arm keeps that pin honest). Since the owner
|
|
1313
|
+
// settled point 2 that set INCLUDES the client door, which is why the block below writes the
|
|
1305
1314
|
// door's settings before any unit is placed.
|
|
1306
1315
|
//
|
|
1307
1316
|
// THE UNITS READ %h/.env, NOT THIS PROCESS'S ENV. Everything the foreground supervisor would have
|
|
@@ -1310,7 +1319,7 @@ if (isMain) {
|
|
|
1310
1319
|
// what makes re-running safe, and the same idempotence sentence the foreground fatal carries applies.
|
|
1311
1320
|
if (wantBackground) {
|
|
1312
1321
|
// ONE AUTHOR for this path — `driver/runner.mjs` names the same file in the same words since
|
|
1313
|
-
//
|
|
1322
|
+
// A later change moved the run-configuration refusal there, and two composers of one path
|
|
1314
1323
|
// fail quietly: a refusal that sends an operator to edit a file the units do not read.
|
|
1315
1324
|
const HOME_ENV = unitEnvPath();
|
|
1316
1325
|
const union = { ...envs.mcp, ...envs.portal, ...(envs.worker ?? {}),
|
|
@@ -1402,9 +1411,9 @@ if (isMain) {
|
|
|
1402
1411
|
};
|
|
1403
1412
|
const willRead = { ...already, ...union };
|
|
1404
1413
|
const miss = missingRequirements(willRead, RUN_TABLES);
|
|
1405
|
-
// ── — WHICH HALF OF `blocking` MAY REFUSE A START
|
|
1414
|
+
// ── — WHICH HALF OF `blocking` MAY REFUSE A START ─────────────────────────
|
|
1406
1415
|
//
|
|
1407
|
-
//
|
|
1416
|
+
// Ruling 2026-09-06, in session: "someone can install and select key later so it should
|
|
1408
1417
|
// still start." So the register, its credential, the engine and the engine's binary NO LONGER
|
|
1409
1418
|
// refuse here. They refuse AT ORDER TIME — `driver/runner.mjs`'s intake wall, before a stage
|
|
1410
1419
|
// dispatches and before anything is spent — and `doctor` and the portal report the box as
|
|
@@ -1426,7 +1435,7 @@ if (isMain) {
|
|
|
1426
1435
|
// of them, and offered `install` as the route. `install` REFUSES a non-terminal — "this is an
|
|
1427
1436
|
// interactive wizard and stdin is not a terminal", rc 2 — so the one reader who arrives here by a
|
|
1428
1437
|
// scripted or hosted install was handed a route they cannot take and no address for the route
|
|
1429
|
-
// they can. Same defect as naming a variable and not the file, one level up
|
|
1438
|
+
// they can. Same defect as naming a variable and not the file, one level up.
|
|
1430
1439
|
//
|
|
1431
1440
|
// BOTH FILES, and that is the difference from the port refusals, which say `~/.env` "is NOT read
|
|
1432
1441
|
// here". They are right: nothing in that file reaches a port decision. Here both are true. The
|
|
@@ -1456,7 +1465,7 @@ if (isMain) {
|
|
|
1456
1465
|
+ `client's first search, which is where this surfaced before: as a failed run and a notice saying `
|
|
1457
1466
|
+ `they had been notified.`);
|
|
1458
1467
|
}
|
|
1459
|
-
// ── THE BOX STARTS AND SAYS WHAT IT CANNOT DO YET
|
|
1468
|
+
// ── THE BOX STARTS AND SAYS WHAT IT CANNOT DO YET ──────────────────────
|
|
1460
1469
|
//
|
|
1461
1470
|
// An install that comes up unconfigured must not come up SILENTLY unconfigured — that is the
|
|
1462
1471
|
// failure one step along from the one being fixed: a reader who is told nothing concludes they are
|
|
@@ -1466,13 +1475,13 @@ if (isMain) {
|
|
|
1466
1475
|
say(` the doors and the portal are up, and every run is refused at order time, before`);
|
|
1467
1476
|
say(` anything is spent, naming what is missing.`);
|
|
1468
1477
|
for (const r of miss.atOrder) say(` ${r.name} — ${r.why}`);
|
|
1469
|
-
// ── THE SAME REMEDY THE REFUSAL USED TO CARRY, AND THE SAME TWO FILES
|
|
1478
|
+
// ── THE SAME REMEDY THE REFUSAL USED TO CARRY, AND THE SAME TWO FILES ──────────────────────
|
|
1470
1479
|
//
|
|
1471
1480
|
// 202's subject was a refusal on THESE values that named no file, on a product with two of them,
|
|
1472
1481
|
// and offered `install` — a wizard that refuses a non-terminal, so the one reader who arrives
|
|
1473
1482
|
// here by a scripted or hosted install was handed a route they cannot take.
|
|
1474
1483
|
//
|
|
1475
|
-
//
|
|
1484
|
+
// A later change moved that refusal to order time, so this screen is where these values are
|
|
1476
1485
|
// now named to an operator. The remedy travels with them RATHER THAN BEING DELETED WITH THE
|
|
1477
1486
|
// REFUSAL — 202's property is about the class ("a message about a value names the file that sets
|
|
1478
1487
|
// it"), not about which gate happened to print it, and the arms in
|
|
@@ -1518,7 +1527,7 @@ if (isMain) {
|
|
|
1518
1527
|
say(` ${ENV_PATH} — the CLI reads this one when you type a command in a shell.`);
|
|
1519
1528
|
say(` Editing one does not change the other. To change what the RUNNING product does, edit the`);
|
|
1520
1529
|
say(` first and restart the units.`);
|
|
1521
|
-
// PORTS ARE THE EXCEPTION, and leaving it unsaid is what
|
|
1530
|
+
// PORTS ARE THE EXCEPTION, and leaving it unsaid is what that gap was. The
|
|
1522
1531
|
// sentence above is true — a unit takes its port from the EnvironmentFile like everything else —
|
|
1523
1532
|
// but THIS command probes for a collision using the value it read from the CLI file, before any
|
|
1524
1533
|
// unit exists. A reader who has just been told to edit the first file, and whose next run refuses
|
|
@@ -1541,7 +1550,7 @@ if (isMain) {
|
|
|
1541
1550
|
writeFileSync(join(UNIT_DIR, u), renderUnit(text, { ...process.env, ...union }));
|
|
1542
1551
|
}
|
|
1543
1552
|
// STDERR IS CAPTURED, not discarded. `stdio: "ignore"` threw systemd's own explanation away before
|
|
1544
|
-
// anyone could read it, which is half of what
|
|
1553
|
+
// anyone could read it, which is half of what was fixed in `connect` and was never
|
|
1545
1554
|
// done here. The two-cause remedy below is right and stays; what was missing was the sentence
|
|
1546
1555
|
// systemd itself wrote.
|
|
1547
1556
|
//
|
|
@@ -1587,7 +1596,7 @@ if (isMain) {
|
|
|
1587
1596
|
//
|
|
1588
1597
|
// This loop ran uncaught. When systemd declined to enable a unit the whole output was
|
|
1589
1598
|
// `node:internal/errors:983`, a Node stack trace and a status code — no name for what failed, and no
|
|
1590
|
-
// statement of what had happened to the install
|
|
1599
|
+
// statement of what had happened to the install. It is the worst-placed refusal
|
|
1591
1600
|
// in this command: by here the env file, the data directories, the grants roster and the seeded
|
|
1592
1601
|
// example are written and every unit file is rendered, so the reader is left not knowing whether
|
|
1593
1602
|
// they have a half-installed deployment. Every other refusal in this command says whether anything
|
|
@@ -1620,7 +1629,7 @@ if (isMain) {
|
|
|
1620
1629
|
if (backgroundRefresh) for (const u of unitsToRestartOnRefresh(BACKGROUND_UNITS, unitTypeOf)) {
|
|
1621
1630
|
try { execFileSync("systemctl", ["--user", "restart", u], { stdio: "ignore" }); } catch { /* health check below reports it */ }
|
|
1622
1631
|
}
|
|
1623
|
-
// ── AN ADOPTED DOOR IS RESTARTED, OR IT KEEPS RUNNING THE OLD TREE
|
|
1632
|
+
// ── AN ADOPTED DOOR IS RESTARTED, OR IT KEEPS RUNNING THE OLD TREE ──────────────────────────────
|
|
1624
1633
|
//
|
|
1625
1634
|
// `enable --now` is a NO-OP on an already-active unit — the finding `unitsToRestartOnRefresh` exists
|
|
1626
1635
|
// for, measured on the test box as an unchanged MainPID. So a door this run adopted rather than
|
|
@@ -1910,7 +1919,7 @@ if (isMain) {
|
|
|
1910
1919
|
// supported state and a useful one; an install that refuses to come up at all because a door could
|
|
1911
1920
|
// not bind is not. The door's own refusal is loud and carries its remedy — measured and recorded as
|
|
1912
1921
|
// working — so a reader sees why in its output rather than losing the portal along with it.
|
|
1913
|
-
// ADOPTED, NOT RE-SPAWNED
|
|
1922
|
+
// ADOPTED, NOT RE-SPAWNED. On a box where `stop` left the door unit up, spawning a
|
|
1914
1923
|
// second door here would bind-fail against the first and print a refusal about a port the reader's
|
|
1915
1924
|
// assistant is correctly connected to. The unit IS the door; this path just does not add another.
|
|
1916
1925
|
const clientDoor = adoptedClientDoor
|
|
@@ -1937,7 +1946,7 @@ if (isMain) {
|
|
|
1937
1946
|
// is reaped, so it is the same fact without the race. The failure mode being avoided is this
|
|
1938
1947
|
// finding's own defect relocated into its failure path: a dead door announced as one a client's
|
|
1939
1948
|
// assistant connects to, which is worse than the silence F26 replaced.
|
|
1940
|
-
// AN ADOPTED DOOR IS RUNNING
|
|
1949
|
+
// AN ADOPTED DOOR IS RUNNING. Without this the banner reported "NOT RUNNING …
|
|
1941
1950
|
// its output above says why" about a door that is up and serving, and pointed the reader at output
|
|
1942
1951
|
// that does not exist — the one sentence on this screen a reader would act on, and false.
|
|
1943
1952
|
const doorRunning = adoptedClientDoor || clientDoor?.child?.exitCode === null;
|
package/bin/stop.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import { invoke } from "../shared/invocation.mjs";
|
|
|
27
27
|
const UNIT_DIR = join(homedir(), ".config", "systemd", "user");
|
|
28
28
|
const say = (s = "") => console.log(s);
|
|
29
29
|
// ON STDERR, because a refusal that scrolls past in the same stream as the success lines is a
|
|
30
|
-
// refusal a script cannot act on and a reader skims
|
|
30
|
+
// refusal a script cannot act on and a reader skims.
|
|
31
31
|
const err = (s = "") => console.error(s);
|
|
32
32
|
|
|
33
33
|
const argv = process.argv.slice(2);
|
|
@@ -49,7 +49,7 @@ for (const u of BACKGROUND_UNITS) {
|
|
|
49
49
|
const file = join(UNIT_DIR, u);
|
|
50
50
|
if (!existsSync(file)) continue;
|
|
51
51
|
found++;
|
|
52
|
-
// ── THE STOP IS VERIFIED, AND THE FILE GOES ONLY IF IT WORKED
|
|
52
|
+
// ── THE STOP IS VERIFIED, AND THE FILE GOES ONLY IF IT WORKED ────────────────────────────────────
|
|
53
53
|
//
|
|
54
54
|
// This was `catch { /* already down */ }` — a COMMENT standing in for a check. The comment guessed why
|
|
55
55
|
// the call failed, the next line deleted the unit file regardless, and the line after that announced
|
package/bin/update.mjs
CHANGED
|
@@ -314,7 +314,7 @@ export async function update(argv = process.argv.slice(2)) {
|
|
|
314
314
|
return installed;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
// ── THE BUNDLE THE PULL COULD NOT UPDATE
|
|
317
|
+
// ── THE BUNDLE THE PULL COULD NOT UPDATE ─────────────────────────────────────────────────────────
|
|
318
318
|
//
|
|
319
319
|
// `portal-ui/dist` is untracked on the public tree, so `git pull` above can never bring it forward.
|
|
320
320
|
// A pull that changed `portal-ui/src` therefore leaves the built bundle behind, and every surface
|
package/build-info.json
CHANGED