clearotron 0.2.2 → 0.2.4
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 +64 -37
- package/.nvmrc +1 -1
- package/CONTRIBUTING.md +8 -4
- package/INSTALL.md +36 -14
- package/README.md +18 -22
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/bin/clearotron.mjs +22 -0
- package/bin/example.mjs +10 -5
- package/bin/onboard.mjs +370 -28
- package/bin/start.mjs +220 -12
- package/build-info.json +2 -2
- package/demo/README.md +1 -1
- package/demo/full-country-search/run/_driver/framework.json +1 -1
- package/demo/global-preliminary-search/run/_driver/framework.json +1 -1
- package/demo/knockout-search/run/_driver/framework.json +1 -1
- package/demo/multi-country-focus-search/run/_driver/framework.json +1 -1
- package/docs/E2E.md +5 -5
- package/docs/GLOSSARY.md +85 -0
- package/docs/PORTAL.md +2 -2
- package/docs/README.md +6 -4
- package/docs/SECURITY.md +17 -0
- package/docs/architecture/01-product-overview.md +21 -9
- package/docs/architecture/04-configuration-reference.md +2 -0
- package/docs/architecture/05-config-governance.md +19 -0
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/configuration.md +4 -3
- package/docs/decisions/0006-what-the-public-repository-carries.md +30 -5
- package/driver/CHANGELOG.md +90 -0
- package/driver/README.md +25 -6
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/config-staleness.mjs +1 -1
- package/driver/connotation-search.mjs +1 -1
- package/driver/contract-audit.mjs +5 -1
- package/driver/contract-e3-baseline.json +11 -11
- package/driver/dev-portal.mjs +2 -2
- package/driver/doubt-selection.mjs +1 -1
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +1 -1
- package/driver/effort-model.mjs +2 -2
- package/driver/engine/cli-version.mjs +103 -0
- package/driver/engine/probe.mjs +45 -5
- package/driver/enqueue-schema.mjs +8 -8
- package/driver/feedback-store.mjs +1 -1
- package/driver/findings-model.mjs +52 -2
- package/driver/flag-snapshot.mjs +20 -0
- package/driver/framework.mjs +1 -1
- package/driver/gateway.mjs +20 -3
- package/driver/outbox-backoff.mjs +1 -1
- package/driver/package.json +3 -3
- package/driver/party-facts.mjs +2 -2
- package/driver/phase0.mjs +1 -1
- package/driver/pipeline.mjs +211 -106
- package/driver/plain-register.mjs +134 -0
- package/driver/portal-access.mjs +1 -1
- package/driver/portal-config-view.mjs +70 -2
- package/driver/portal-families.mjs +1 -1
- package/driver/portal-local-auth.mjs +5 -1
- package/driver/portal-report.mjs +1 -1
- package/driver/portal-service.mjs +79 -9
- package/driver/portal-upstream.mjs +2 -2
- package/driver/predelivery-lint.mjs +151 -2
- package/driver/profile-page.html +10 -2
- package/driver/profile-service.mjs +4 -4
- package/driver/profiles/README.md +3 -3
- package/driver/profiles.mjs +144 -12
- package/driver/publish/index.mjs +2 -2
- package/driver/publish/knockout.mjs +7 -1
- package/driver/publish/render-knockout.mjs +453 -118
- package/driver/publish/render.mjs +111 -16
- package/driver/reference-strip-signatures.mjs +20 -0
- package/driver/search-policy.mjs +3 -3
- package/driver/skills/blind-frame/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +84 -0
- package/driver/skills/matter-frame/SKILL.md +6 -6
- package/driver/skills/matter-frame/watchlist-reference.md +2 -2
- package/driver/skills/prelim-common-law/SKILL.md +1 -1
- package/driver/skills/prelim-search/SKILL.md +1 -1
- package/driver/skills/prelim-search/report-prose.md +82 -0
- package/driver/skills/prelim-search/risk-framework-demo.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework-triage.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework.manifest.json +1 -1
- package/driver/skills/prelim-search/synthesis-rules.md +1 -1
- package/driver/skills/prelim-variants/SKILL.md +3 -3
- package/driver/stage-context.mjs +13 -0
- package/driver/stages.mjs +54 -7
- package/driver/suite-census.json +220 -58
- package/driver/systemd/clearotron-worker.service +3 -3
- package/driver/tokens.mjs +1 -1
- package/driver/unit-inventory.mjs +214 -27
- package/driver/verify.mjs +1 -1
- package/driver/whatif-memo-run.mjs +26 -4
- package/driver/whatif-memo.mjs +19 -1
- package/examples/README.md +2 -2
- package/examples/grants.example.json +6 -7
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/lib/whatif.mjs +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/packs/ops/EXAMPLES.md +2 -2
- package/mcp-server/server.mjs +1 -1
- package/package.json +31 -19
- package/portal-ui/dist/assets/{index-Ct3iQtZ2.css → index-1ziUJX1E.css} +43 -2
- package/portal-ui/dist/assets/{index-BUGgiYtF.js → index-KFAHMgdT.js} +289 -73
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/term-shape.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/uspto-local/src/index-store.js +2 -2
- package/scripts/added-reference-check.mjs +45 -89
- package/scripts/citation-line-check.mjs +121 -2
- package/scripts/e2e.mjs +2 -2
- package/scripts/env-classify.mjs +67 -3
- package/scripts/generated-files-are-current.mjs +125 -0
- package/scripts/live-surface-check.mjs +38 -1
- package/scripts/mint-names-in-force.mjs +4 -2
- package/scripts/mint-public-residue.mjs +74 -0
- package/scripts/mint-reference-strip-backlog.mjs +12 -1
- package/scripts/mint-suite-census.mjs +5 -2
- package/scripts/nightly-notice.mjs +0 -2
- package/scripts/preinstall-node-check.mjs +24 -0
- package/scripts/render-check.mjs +13 -2
- package/scripts/report-frame-check.mjs +8 -1
- package/scripts/report-screenshot.mjs +6 -3
- package/scripts/revisit-render-check.mjs +6 -3
- package/scripts/score.mjs +1 -1
- package/scripts/strip-tracker-citations.mjs +38 -7
- package/scripts/test-full.mjs +226 -0
- package/scripts/test-run.mjs +16 -0
- package/shared/README.md +2 -1
- package/shared/anon-overlay.mjs +3 -3
- package/shared/browser-temp-root.mjs +142 -0
- package/shared/checkout-move.mjs +1 -1
- package/shared/client-door.mjs +4 -3
- package/shared/customer-store.mjs +1 -1
- package/shared/identifier-scan.mjs +1 -1
- package/shared/install-auth.mjs +35 -0
- package/shared/invocation.mjs +33 -1
- package/shared/listen.mjs +55 -3
- package/shared/names-in-force.mjs +6 -1
- package/shared/node-floor.mjs +97 -0
- package/shared/os-advice.mjs +91 -0
- package/shared/reap-on-exit.mjs +42 -0
- package/shared/reference-guard-classes.mjs +351 -0
- package/shared/staff-domain.mjs +154 -0
- package/shared/store-in-repo.mjs +23 -2
- package/shared/suite-census.mjs +31 -3
- package/shared/withheld-paths-access.mjs +36 -18
- package/skills/clearotron-ops/SKILL.md +1 -1
- package/driver/recipes/README.md +0 -49
- package/driver/skills/prelim-search/risk-framework-aurora.manifest.json +0 -19
- package/driver/skills/prelim-search/risk-framework-aurora.md +0 -77
- package/driver/skills/prelim-search/risk-framework-zephyr.manifest.json +0 -14
- package/driver/skills/prelim-search/risk-framework-zephyr.md +0 -33
- package/driver/skills/prelim-search/worked-examples-aurora.md +0 -10
- package/driver/skills/prelim-search/worked-examples-zephyr.md +0 -10
- package/scripts/ai-page-render-check.mjs +0 -532
- package/scripts/clearances-render-check.mjs +0 -480
- package/scripts/composer-render-check.mjs +0 -1027
- package/scripts/home-render-check.mjs +0 -526
- package/scripts/portal-lifecycle-check.mjs +0 -639
package/bin/start.mjs
CHANGED
|
@@ -116,7 +116,7 @@ import { SERVER_INSTALL_SET, unitsToRestartOnRefresh, unitHealthVerdict } from "
|
|
|
116
116
|
// tolerate and never manage"; settled point 2 superseded that.)
|
|
117
117
|
import { defaultDenylistPath, denylistPathFor, denylistFor, ensureDenylistFile, CLIENT_DOOR_UNIT, enablePlan, clientDoorPort } from "../shared/client-door.mjs"; // — one owner for the revocation list's path
|
|
118
118
|
import { createServer } from "node:net";
|
|
119
|
-
import { listenErrorMessage } from "../shared/listen.mjs";
|
|
119
|
+
import { listenErrorMessage, nextFreePort } from "../shared/listen.mjs";
|
|
120
120
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
121
121
|
import { randomBytes } from "node:crypto";
|
|
122
122
|
import { invocationPrefix, invoke } from "../shared/invocation.mjs"; // — the banner names the verb
|
|
@@ -130,6 +130,11 @@ import { productIdentity } from "../shared/product-identity.mjs"; // AGPL §13
|
|
|
130
130
|
import { pinEnvAll } from "../shared/env-aliases.mjs"; // — a pin that names one spelling has set nothing that wins
|
|
131
131
|
import { BRAND } from "../shared/brand.mjs"; // — the installer's own name, from the tenant seam
|
|
132
132
|
import { rebuildIfStale } from "../shared/bundle-rebuild.mjs"; // never serve a bundle older than its sources
|
|
133
|
+
// ONE CLASSIFIER FOR WHAT A STAFF RULE ADMITS, shared with the setup wizard. Two copies of this
|
|
134
|
+
// judgement would be a wizard that asks about one rule and a launcher that writes another.
|
|
135
|
+
import { classifyStaffDomain, staffDomainRefusal, staffGrantSentence } from "../shared/staff-domain.mjs";
|
|
136
|
+
import { backgroundManager } from "../shared/os-advice.mjs";
|
|
137
|
+
import { frontingVariablesSet } from "../shared/install-auth.mjs"; // — one owner for what counts as a proxy in front of a door
|
|
133
138
|
|
|
134
139
|
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
135
140
|
const ENV_PATH = envLocalPath({ repoRoot: REPO }); // resolved, never composed: one resolver, so moving this file later is one line
|
|
@@ -366,6 +371,11 @@ export function installPaths(base) {
|
|
|
366
371
|
};
|
|
367
372
|
}
|
|
368
373
|
|
|
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
|
+
|
|
369
379
|
/**
|
|
370
380
|
* The one address that signs in, and the staff domain derived from it.
|
|
371
381
|
*
|
|
@@ -374,10 +384,61 @@ export function installPaths(base) {
|
|
|
374
384
|
* it safe here and would not be safe on a hosted instance. Without it the sign-in succeeds and every
|
|
375
385
|
* page 403s, because signing in is not being enrolled (portal-access.mjs decides that, and it is
|
|
376
386
|
* deliberately blind to which door you came through).
|
|
387
|
+
*
|
|
388
|
+
* ── AND THAT PARAGRAPH IS ONLY TRUE OF `<account>@localhost` ─────────────────────────────────────────
|
|
389
|
+
*
|
|
390
|
+
* "admits exactly that address and nobody else" is a property of the DOMAIN, not of the mode. It holds
|
|
391
|
+
* for `localhost`. It does not hold for a domain other people have addresses at: the rule this line
|
|
392
|
+
* produces from `alex@a-firm.example-tld` says *anyone at that domain*, the settings page then reports
|
|
393
|
+
* it back in those words, and on a deployment where identity arrives from an external login system the
|
|
394
|
+
* domain is the whole of the check. Nobody was asked, and there is nothing on the box recording that a
|
|
395
|
+
* grant was made.
|
|
396
|
+
*
|
|
397
|
+
* So the derivation now refuses the domains that can never be a staff rule — a webmail provider, a
|
|
398
|
+
* documentation domain — and hands anything wider than one machine back to the caller to state and
|
|
399
|
+
* confirm. `shared/staff-domain.mjs` is the one classifier, shared with the setup wizard, so the
|
|
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.
|
|
377
404
|
*/
|
|
378
405
|
export function staffDomainFor(email) {
|
|
379
406
|
const at = String(email ?? "").lastIndexOf("@");
|
|
380
|
-
|
|
407
|
+
const domain = at > 0 ? String(email).slice(at + 1).toLowerCase() : "";
|
|
408
|
+
if (!domain) return "";
|
|
409
|
+
const refusal = staffDomainRefusal(domain);
|
|
410
|
+
if (refusal) throw new StaffDomainRefused(domain, refusal);
|
|
411
|
+
return domain;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* What to say when the derived domain is a real one — wider than this machine, and never assumed.
|
|
416
|
+
*
|
|
417
|
+
* A REFUSAL RATHER THAN A PROMPT, and the reason is that this command has no reader. `start` runs from
|
|
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.`;
|
|
381
442
|
}
|
|
382
443
|
|
|
383
444
|
/**
|
|
@@ -777,6 +838,10 @@ if (isMain) {
|
|
|
777
838
|
// bound anywhere else in any mode. Sign-in is untouched — the demo signs in like any first start, and
|
|
778
839
|
// the portal mints and prints its passphrase exactly as it does for a real one.
|
|
779
840
|
const DEMO = argv.includes("--demo");
|
|
841
|
+
// THE DEMO BRINGS ITS OWN ACCOUNT. A fresh install resolves `generic` and nothing else (owner
|
|
842
|
+
// ruling, 2026-09-08), so the demo account is refused from the roster unless somebody asked for it.
|
|
843
|
+
// Asked here, once and visibly, rather than at each site that happens to read a roster.
|
|
844
|
+
if (DEMO) process.env.CLEAROTRON_DEMO_PROFILES ??= "1";
|
|
780
845
|
// ── `--port` MOVES EVERY DOOR IT OPENS (tracker issue 166) ───────────────────────────────────────
|
|
781
846
|
//
|
|
782
847
|
// It used to move ONE of the three. `resolvePorts` reads three independent variables with three
|
|
@@ -812,16 +877,47 @@ if (isMain) {
|
|
|
812
877
|
run: (cmd, args) => spawnSync(cmd, args, { cwd: REPO, stdio: "inherit", shell: process.platform === "win32" }).status ?? 1,
|
|
813
878
|
});
|
|
814
879
|
|
|
815
|
-
//
|
|
816
|
-
// the address never leaves the machine — `--user` is there for a reader who wants their real
|
|
817
|
-
// whatever is resolved here is written to `.env` so the question is never put twice.
|
|
880
|
+
// The ADDRESS is not asked for here: a local install has one user, this machine already knows their
|
|
881
|
+
// 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. What IS
|
|
883
|
+
// asked for, and used not to be, is the staff rule the address implies once it is a real one; that is
|
|
884
|
+
// the block below the address, and `clearotron install` is where the question is actually put.
|
|
818
885
|
let whoami = "user";
|
|
819
886
|
try { whoami = userInfo().username || "user"; } catch { /* a container with no passwd entry */ }
|
|
820
887
|
// In a demo the address is the demo's own and is never written anywhere: see the DEMO block above.
|
|
821
888
|
const user = String(flag("--user", DEMO ? "demo@localhost" : (process.env.PORTAL_LOCAL_USER || `${whoami}@localhost`))).trim().toLowerCase();
|
|
822
889
|
if (!user.includes("@") || user.indexOf("@") !== user.lastIndexOf("@"))
|
|
823
890
|
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.`);
|
|
824
|
-
|
|
891
|
+
// ── THE STAFF RULE THIS ADDRESS IMPLIES, STATED BEFORE IT IS WRITTEN ──────────────────────────────
|
|
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:
|
|
899
|
+
//
|
|
900
|
+
// `localhost` (or any bare hostname) — one machine, no second person, nothing to ask. Silent, as
|
|
901
|
+
// it has always been. This is the default path and the only
|
|
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.
|
|
907
|
+
//
|
|
908
|
+
// BEFORE THIS RUN CHANGES THE BOX, deliberately — it sits above the state-written divide further
|
|
909
|
+
// down, with the units gate, the auth-mode gate and the port probe. A refused start has written no
|
|
910
|
+
// `.env`, minted no secret and placed no unit, so the reader's only question — is my install
|
|
911
|
+
// half-made — has one answer.
|
|
912
|
+
let staffDomains = process.env.PORTAL_STAFF_DOMAINS;
|
|
913
|
+
if (!staffDomains) {
|
|
914
|
+
let derived = "";
|
|
915
|
+
try { derived = staffDomainFor(user); }
|
|
916
|
+
catch (e) { fatal(e.message); }
|
|
917
|
+
if (derived && classifyStaffDomain(derived) === "wide")
|
|
918
|
+
fatal(wideStaffDomainRefusal({ user, domain: derived, envPath: ENV_PATH }));
|
|
919
|
+
staffDomains = derived;
|
|
920
|
+
}
|
|
825
921
|
|
|
826
922
|
say("");
|
|
827
923
|
say(` ${BRAND.name} ${BRAND.product.toLowerCase()} — local install`);
|
|
@@ -868,7 +964,24 @@ if (isMain) {
|
|
|
868
964
|
// loop because both paths below have to act on it: the background path must RESTART the door it
|
|
869
965
|
// adopted, and the foreground path must not spawn a second one beside it.
|
|
870
966
|
let adoptedClientDoor = false;
|
|
871
|
-
|
|
967
|
+
// ── WHETHER ANYTHING OUTSIDE THIS PROCESS IS ADDRESSED TO THESE NUMBERS ────────────────────────
|
|
968
|
+
//
|
|
969
|
+
// A door may only be moved off a port nobody chose when nothing fronts it. A proxy, an Access team
|
|
970
|
+
// or an OIDC issuer means something external resolves to these numbers, and a door that quietly
|
|
971
|
+
// moved would be up and unreachable — which looks like success and is the worst of the three
|
|
972
|
+
// outcomes. The auth-mode half of this is already closed further up: a foreground start refuses
|
|
973
|
+
// outright when PORTAL_AUTH_MODE names a hosted door, and again when the units are installed and
|
|
974
|
+
// serving. What is left to check is the settings that can be present with the mode unset.
|
|
975
|
+
//
|
|
976
|
+
// PORTAL_AUTH_MODE IS ABSENT FROM THAT LIST ON PURPOSE, and only a rule in another file makes that
|
|
977
|
+
// safe: `driver/portal-service.mjs` refuses to start in auth-proxy without CF_ACCESS_TEAM or
|
|
978
|
+
// PORTAL_OIDC_ISSUER, so a portal fronted by the mode alone cannot come up at all. Relaxing that
|
|
979
|
+
// refusal without adding the mode here would let this under-report — which is why the list has one
|
|
980
|
+
// owner in `shared/install-auth.mjs` with an arm holding it to the doors themselves.
|
|
981
|
+
const fronted = frontingVariablesSet(process.env);
|
|
982
|
+
const claimedPorts = new Set([ports.portal, ports.mcp, ports.client]);
|
|
983
|
+
const movedDoors = [];
|
|
984
|
+
for (const [what, port, portVar, doorUnit = null, key = null] of [["portal", ports.portal, "PORTAL_SERVICE_PORT", null, "portal"], ["engine door", ports.mcp, "TRADEMARK_MCP_HTTP_PORT", null, "mcp"], ["client door", ports.client, "CLIENT_MCP_HTTP_PORT", CLIENT_DOOR_UNIT, "client"]]) {
|
|
872
985
|
// A --background REFRESH runs over its own healthy units, which hold these ports on purpose;
|
|
873
986
|
// systemd's restart is the handover. Probing would refuse the flag exactly once it has worked.
|
|
874
987
|
// The narrow carve above already proved every installed unit is ours.
|
|
@@ -893,8 +1006,38 @@ if (isMain) {
|
|
|
893
1006
|
// on a privileged port and from an address this host does not have, and names the way out of each;
|
|
894
1007
|
// the launcher having its own shorter sentence for one of the three would mean a user meets two
|
|
895
1008
|
// different answers to the same question depending on which door refused first.
|
|
1009
|
+
// ── A PORT NOBODY CHOSE IS MOVED RATHER THAN REFUSED (owner ruling, 2026-09-09) ─────────────
|
|
1010
|
+
//
|
|
1011
|
+
// Three conditions, and each is a different reason:
|
|
1012
|
+
// · the address is genuinely taken — anything else is not this case;
|
|
1013
|
+
// · the reader did not state this port, so no one is addressed to it. An explicitly set port is
|
|
1014
|
+
// a stated address and is refused exactly as before;
|
|
1015
|
+
// · nothing fronts these doors, so nothing outside resolves to the old number.
|
|
1016
|
+
// With any of those false the refusal below fires unchanged, which is the whole of the previous
|
|
1017
|
+
// behaviour kept intact underneath this.
|
|
1018
|
+
if (code === "EADDRINUSE" && key && !fronted.length && !String(process.env[portVar] ?? "").trim()) {
|
|
1019
|
+
const next = await nextFreePort(port, async (p) => (await probe(p)) === null, { claimed: claimedPorts });
|
|
1020
|
+
// NULL IS NOT A FALLBACK. Nothing free in range means the reader is told the truth about the
|
|
1021
|
+
// port they asked for, rather than sent to one this could not prove was free either.
|
|
1022
|
+
if (next) {
|
|
1023
|
+
claimedPorts.delete(port);
|
|
1024
|
+
claimedPorts.add(next);
|
|
1025
|
+
ports[key] = next;
|
|
1026
|
+
movedDoors.push({ what, portVar, from: port, to: next });
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
896
1030
|
if (code) fatal(listenErrorMessage({ code }, { what, host: HOST, port, portVar, portFile }));
|
|
897
1031
|
}
|
|
1032
|
+
// SAID OUT LOUD, EVERY TIME. A door that moved is at an address the reader did not ask for and will
|
|
1033
|
+
// not find by memory — and the portal's own URL is printed from `ports.portal` further down, so a
|
|
1034
|
+
// silent move would leave the two disagreeing with nothing explaining it.
|
|
1035
|
+
if (movedDoors.length) {
|
|
1036
|
+
say("");
|
|
1037
|
+
say(` ${movedDoors.length === 1 ? "One door was" : `${movedDoors.length} doors were`} already in use, so ${movedDoors.length === 1 ? "it" : "they"} moved:`);
|
|
1038
|
+
for (const d of movedDoors) say(` the ${d.what}: ${d.from} → ${d.to} (set ${d.portVar} to pin it)`);
|
|
1039
|
+
say(" Nothing outside this machine is addressed to these, so nothing else needed changing.");
|
|
1040
|
+
}
|
|
898
1041
|
|
|
899
1042
|
// ── 2. the two secrets, generated once and kept ────────────────────────────────────────────────────
|
|
900
1043
|
|
|
@@ -1574,10 +1717,40 @@ if (isMain) {
|
|
|
1574
1717
|
// sequence rather than a race between the terminal's signal and ours. It also means the group
|
|
1575
1718
|
// kill below reaches anything a child spawns, not just the child.
|
|
1576
1719
|
detached: true,
|
|
1577
|
-
|
|
1720
|
+
// — STDERR IS TEED RATHER THAN INHERITED, AND ONLY STDERR.
|
|
1721
|
+
//
|
|
1722
|
+
// These children announce a refusal as one FATAL line on stderr and exit 1. With `inherit` the
|
|
1723
|
+
// parent holds no copy, so the failure message below could only point at output it had never seen —
|
|
1724
|
+
// and on one report that output never reached the reader's terminal at all. It said "its own output
|
|
1725
|
+
// above says why" about a line that was not above, which is worse than saying nothing: it sends
|
|
1726
|
+
// somebody scrolling for a sentence they will not find.
|
|
1727
|
+
//
|
|
1728
|
+
// stdout stays inherited. It carries no refusal — measured: on a refusing door stdout is empty and
|
|
1729
|
+
// stderr holds the whole of it — and leaving it alone keeps the child's terminal detection and any
|
|
1730
|
+
// progress rendering intact. Piping a stream costs the child its TTY, so only the stream that has to
|
|
1731
|
+
// be read is piped.
|
|
1732
|
+
stdio: ["ignore", "inherit", "pipe"],
|
|
1578
1733
|
env: { ...process.env, ...env },
|
|
1579
1734
|
});
|
|
1580
|
-
|
|
1735
|
+
// The last lines of that child's stderr, forwarded on as they arrive so nothing is delayed or
|
|
1736
|
+
// swallowed, and kept so the failure can QUOTE them rather than refer to them.
|
|
1737
|
+
const tail = [];
|
|
1738
|
+
// A CHUNK IS NOT A LINE. `data` arrives on whatever boundary the pipe gives us, so a stream split
|
|
1739
|
+
// mid-sentence would push two half-lines and the quoted last words would reach the reader cut in
|
|
1740
|
+
// half — on precisely the message that exists because they could not see the original. The partial
|
|
1741
|
+
// remainder is held over and completed by the next chunk; whatever is left when the stream ends is
|
|
1742
|
+
// flushed, because a process that dies mid-line still said the thing it was saying.
|
|
1743
|
+
let pending = "";
|
|
1744
|
+
const keep = (line) => { if (line.trim()) tail.push(line); while (tail.length > 12) tail.shift(); };
|
|
1745
|
+
child.stderr?.setEncoding("utf8");
|
|
1746
|
+
child.stderr?.on("data", (chunk) => {
|
|
1747
|
+
process.stderr.write(chunk);
|
|
1748
|
+
const parts = (pending + String(chunk)).split("\n");
|
|
1749
|
+
pending = parts.pop() ?? "";
|
|
1750
|
+
for (const line of parts) keep(line);
|
|
1751
|
+
});
|
|
1752
|
+
child.stderr?.on("end", () => { keep(pending); pending = ""; });
|
|
1753
|
+
const rec = { name, script, child, alive: true, tail };
|
|
1581
1754
|
children.push(rec);
|
|
1582
1755
|
child.on("exit", (code, signal) => {
|
|
1583
1756
|
rec.alive = false;
|
|
@@ -1590,7 +1763,7 @@ if (isMain) {
|
|
|
1590
1763
|
+ ` but nothing is draining the queue now. Start one with:\n node ${script} --watch\n`);
|
|
1591
1764
|
return;
|
|
1592
1765
|
}
|
|
1593
|
-
err(
|
|
1766
|
+
err(childExitReport({ name, script, code, signal, tail }));
|
|
1594
1767
|
void shutdown(1);
|
|
1595
1768
|
});
|
|
1596
1769
|
child.on("error", (e) => {
|
|
@@ -1857,7 +2030,42 @@ if (isMain) {
|
|
|
1857
2030
|
// run". Saying it here, before the commands go by, is the whole fix.
|
|
1858
2031
|
say(" This terminal is now the product: it runs only while this command does, and Ctrl-C — or closing");
|
|
1859
2032
|
say(" the window — stops everything it started. So the commands above need a SECOND terminal.");
|
|
1860
|
-
|
|
1861
|
-
|
|
2033
|
+
// ── THE BACKGROUND ROUTE IS NOT OFFERED WHERE IT CANNOT WORK ────────────────────────────────────
|
|
2034
|
+
//
|
|
2035
|
+
// `--background` installs and enables service units. There are none on Windows, so both the offer
|
|
2036
|
+
// and the sentence naming what manages them were wrong there — a reader was told to run a flag that
|
|
2037
|
+
// cannot succeed and given a service manager that is not on the machine and cannot be put there.
|
|
2038
|
+
// Reported from a real run. Same rule as the engine refusal above: do not name a route this platform
|
|
2039
|
+
// does not have.
|
|
2040
|
+
const manager = backgroundManager();
|
|
2041
|
+
if (manager) {
|
|
2042
|
+
say(` To get your prompt back instead, stop this and run ${invoke("start")} --background`);
|
|
2043
|
+
say(` — same product, managed by ${manager}, and it survives logout.`);
|
|
2044
|
+
} else {
|
|
2045
|
+
say(" There is no background form on this platform: the product runs as long as this window does.");
|
|
2046
|
+
say(" Leave it open and use a second terminal for the commands above.");
|
|
2047
|
+
}
|
|
1862
2048
|
say("");
|
|
1863
2049
|
}
|
|
2050
|
+
|
|
2051
|
+
/**
|
|
2052
|
+
* What a reader is told when a child this install cannot run without exits.
|
|
2053
|
+
*
|
|
2054
|
+
* QUOTED, NOT POINTED AT. This used to end "its own output above says why" while the parent inherited
|
|
2055
|
+
* the child's stderr and therefore held no copy of it. On one report that output never reached the
|
|
2056
|
+
* reader's terminal at all, so the sentence sent somebody scrolling for a line that was not there —
|
|
2057
|
+
* worse than saying nothing, because it reads as a working instruction.
|
|
2058
|
+
*
|
|
2059
|
+
* An empty tail is reported as an empty tail. A child that exits silently is a finding about the child,
|
|
2060
|
+
* and printing a heading with nothing under it would hide exactly that.
|
|
2061
|
+
*
|
|
2062
|
+
* PURE, and exported, so this can be driven without starting a supervisor — the failure path of a
|
|
2063
|
+
* process manager is the one nobody exercises by hand.
|
|
2064
|
+
*/
|
|
2065
|
+
export function childExitReport({ name, script, code, signal, tail = [] }) {
|
|
2066
|
+
const how = signal ? `on ${signal}` : `with code ${code}`;
|
|
2067
|
+
const said = tail.length
|
|
2068
|
+
? `Its last line${tail.length > 1 ? "s" : ""}:\n${tail.map((l) => ` ${l}`).join("\n")}\n`
|
|
2069
|
+
: "It exited without printing anything, which is itself the thing to report.\n";
|
|
2070
|
+
return `\nstart: ${name} (${script}) exited ${how}. Stopping the rest.\n${said}`;
|
|
2071
|
+
}
|
package/build-info.json
CHANGED
package/demo/README.md
CHANGED
|
@@ -37,7 +37,7 @@ they are machine output with a fixed shape, described here instead:
|
|
|
37
37
|
the search policy, the receipts, the pre-delivery lint. These are what the report is assembled from
|
|
38
38
|
and what the audit surfaces read.
|
|
39
39
|
- `<product-id>/run/_records/` — the register records the run actually retrieved, one file per record,
|
|
40
|
-
named by register and number. Real
|
|
40
|
+
named by register and number. Real Clarivate Compumark records for a fictional mark.
|
|
41
41
|
|
|
42
42
|
`<product-id>/PROVENANCE.md` says how each run was frozen, what was dropped, and how to regenerate it.
|
|
43
43
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"framework_key": "house-default",
|
|
4
4
|
"title": "Generic default risk framework",
|
|
5
|
-
"source_deck": "
|
|
5
|
+
"source_deck": "Cordillera house risk framework, authored in-house and reviewed by counsel; 2026-08-31 revision",
|
|
6
6
|
"entity_label": "the company",
|
|
7
7
|
"bands": [
|
|
8
8
|
{
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"framework_key": "house-default",
|
|
4
4
|
"title": "Generic default risk framework",
|
|
5
|
-
"source_deck": "
|
|
5
|
+
"source_deck": "Cordillera house risk framework, authored in-house and reviewed by counsel; 2026-08-31 revision",
|
|
6
6
|
"entity_label": "the company",
|
|
7
7
|
"bands": [
|
|
8
8
|
{
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"framework_key": "house-triage",
|
|
4
4
|
"title": "House knockout triage ladder (Stage 0)",
|
|
5
|
-
"source_deck": "
|
|
5
|
+
"source_deck": "Cordillera house triage scale for knockout screening, authored in-house; five rating tiers",
|
|
6
6
|
"entity_label": "the client",
|
|
7
7
|
"bands": [
|
|
8
8
|
{
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"framework_key": "house-default",
|
|
4
4
|
"title": "Generic default risk framework",
|
|
5
|
-
"source_deck": "
|
|
5
|
+
"source_deck": "Cordillera house risk framework, authored in-house and reviewed by counsel; 2026-08-31 revision",
|
|
6
6
|
"entity_label": "the company",
|
|
7
7
|
"bands": [
|
|
8
8
|
{
|
package/docs/E2E.md
CHANGED
|
@@ -45,7 +45,7 @@ CLEAROTRON_DATABASE=corsearch # REQUIRED, no default (track
|
|
|
45
45
|
CORSEARCH_SESSION_KEY=dev-offline # the credential preflight wants it set; never fetches under the mock
|
|
46
46
|
CLEAROTRON_SATPROBE_CODESIDE=0 # the probe dials the provider; a mock run cannot
|
|
47
47
|
CLEAROTRON_BAND_TRUTH_GATE=0 # the gate evidences bands against the production call ledger
|
|
48
|
-
CLEAROTRON_CUSTOMERS_DIR= # unset ⇒ the
|
|
48
|
+
CLEAROTRON_CUSTOMERS_DIR= # unset ⇒ the demo customer bundled with the repo
|
|
49
49
|
# MCP face (optional): TRADEMARK_MCP_DEV=1 TRADEMARK_MCP_AUTH_DISABLED=1 + loopback host + an ABSOLUTE
|
|
50
50
|
# CLEAROTRON_ACCESS_FILE=/abs/path/grants.json. All four, or it refuses to start: with auth off and no
|
|
51
51
|
# grants file every token-less caller resolves to internal read-all across every customer.
|
|
@@ -59,8 +59,8 @@ and the refusal prints the absolute path to write.
|
|
|
59
59
|
Drive one run through the whole spine by hand:
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
|
-
node driver/enqueue.mjs --mark "
|
|
63
|
-
--forwarder ops --profile
|
|
62
|
+
node driver/enqueue.mjs --mark "NORTHWIND PROBE" --classes 9,41 --goods "water-quality monitoring software" \
|
|
63
|
+
--forwarder ops --profile demo-brand-owner
|
|
64
64
|
node driver/runner.mjs # claims, runs all stages on the mock, publishes, writes the packets
|
|
65
65
|
ls /home/you/trademark-dev/outbox # → <runId>.pending (+ failure/intake packets on the sad paths)
|
|
66
66
|
cat <archived run>/_driver/delivery.json
|
|
@@ -75,7 +75,7 @@ outbox packet kind — on a real filesystem with real process boundaries, for fr
|
|
|
75
75
|
this, from a clean `env -i` shell: enqueue → runner → `DELIVERED (verdict CLEAR)` → archived run dir
|
|
76
76
|
+ `<runId>.pending` outbox marker + `_driver/delivery.json` (runId, forwarder, subject,
|
|
77
77
|
`emailBodyHtml`, url, verdict — no profile field; that is `_driver/profile.json` beside it, the run's
|
|
78
|
-
frozen
|
|
78
|
+
frozen demo profile, and the file to open to prove which profile resolved).
|
|
79
79
|
|
|
80
80
|
### Tier 1b — the UI PORTAL (browse the dev instance; develop UI features against it)
|
|
81
81
|
|
|
@@ -105,7 +105,7 @@ so a dev instance beside a live one must be given its own (`PORTAL_PORT`, `PROFI
|
|
|
105
105
|
silently — each is a proxy to a port, and the port is all it knows. `/recipes/*` is the worse half:
|
|
106
106
|
its save endpoint writes and git-commits into whichever recipe store it reached.
|
|
107
107
|
|
|
108
|
-
A pass here looks like: index, run report, the
|
|
108
|
+
A pass here looks like: index, run report, the demo customer page and the profile-editor UI all
|
|
109
109
|
render against the Tier-1 pool; the `/profiles/*` proxy round-trips; traversal and non-loopback binds
|
|
110
110
|
are refused (unit-tested).
|
|
111
111
|
|
package/docs/GLOSSARY.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Glossary
|
|
2
|
+
|
|
3
|
+
Words this codebase uses in a particular way. They are here because they are already in the tree —
|
|
4
|
+
in file names, comments and test titles — and a contributor meeting one should not have to reverse
|
|
5
|
+
it out of the code. Product vocabulary a client would meet is in [`../README.md`](../README.md); the
|
|
6
|
+
tenant, account and project model is in [`../INSTALL.md`](../INSTALL.md) under "The four things, and
|
|
7
|
+
what contains what".
|
|
8
|
+
|
|
9
|
+
Nothing here is a rule. Each line says what the word points at, and names the file that owns it.
|
|
10
|
+
|
|
11
|
+
## The run
|
|
12
|
+
|
|
13
|
+
**Seat** — one model turn with one job, inside a stage that has several. The common-law grid runs
|
|
14
|
+
halves `a` and `b` and a meaning seat `m`; each has its own model tier and its own ledger, and one
|
|
15
|
+
seat failing is not the stage failing.
|
|
16
|
+
|
|
17
|
+
**Dispatch** — handing a seat its prompt and waiting for the turn. Recorded per attempt in
|
|
18
|
+
`_driver/<stage>.jsonl`, so a stage that ran three times has three dispatches and one result.
|
|
19
|
+
|
|
20
|
+
**Matter frame** — what the run understands the request to be: the mark, the goods, the sector, the
|
|
21
|
+
territories. Everything downstream derives from it, and it is told not to widen past the territories
|
|
22
|
+
the job named.
|
|
23
|
+
|
|
24
|
+
**Blind frame** — a second reading of the raw request that never sees the matter frame. It runs
|
|
25
|
+
beside the investigation as a non-fatal sibling, so a framing mistake shows up as a disagreement
|
|
26
|
+
rather than propagating quietly.
|
|
27
|
+
|
|
28
|
+
**Feedforward** — the reviewer's flags reaching the corrective pass as data rather than as prose.
|
|
29
|
+
`corrections-feedforward.mjs` decides which flags count as still open.
|
|
30
|
+
|
|
31
|
+
**Carry-through** — of every subject the run's findings surface, which ones reach none of the
|
|
32
|
+
delivered documents. A finding that is recorded and never carried is the thing this measures.
|
|
33
|
+
|
|
34
|
+
**Declination** — a record the run enumerated and then decided not to pursue, with the position of
|
|
35
|
+
its reason in `_driver/declination-spec.json`. A declination is a decision, not a gap.
|
|
36
|
+
|
|
37
|
+
**Refusal** — the product declining rather than failing. `terminalKind: "designed-refusal"`. Nothing
|
|
38
|
+
about a refusal is retried, parked or recovered; the remedy is always the operator's.
|
|
39
|
+
|
|
40
|
+
## What a run leaves behind
|
|
41
|
+
|
|
42
|
+
**Ledger** — an append-only record of what was covered, kept so that "unsearched" can never be
|
|
43
|
+
reported as "clean". The coverage ledger appears in the report as prose and as JSON, from one source.
|
|
44
|
+
|
|
45
|
+
**Receipt** — a record that something happened, kept for observability and deliberately not used as a
|
|
46
|
+
gate. A receipt read as a gate is a Goodhart problem, and the reasoning-integrity receipt says so on
|
|
47
|
+
its own face.
|
|
48
|
+
|
|
49
|
+
**Register-digest** — the condensed register result a downstream stage reads instead of the raw
|
|
50
|
+
records: the same evidence, at the size a model turn can hold.
|
|
51
|
+
|
|
52
|
+
**Hit list** — one line per enumerated record and the fate it was given. It is not a read: a record
|
|
53
|
+
can be on the hit list and never opened, and the sign-off condition is about documents actually read.
|
|
54
|
+
|
|
55
|
+
**Predelivery** — the checks that run after the documents exist and before anyone receives them
|
|
56
|
+
(`predelivery-lint.mjs`, `_driver/predelivery-lint.json`). A failing predelivery withholds the
|
|
57
|
+
artifact; it never annotates it.
|
|
58
|
+
|
|
59
|
+
**Courier** — delivery as a self-contained packet handed to the integrator. The engine does not send;
|
|
60
|
+
what it produces is carried.
|
|
61
|
+
|
|
62
|
+
**Drift** — a value that is mirrored in two places by design, and the two disagreeing. The catalogue
|
|
63
|
+
of what is mirrored, and which copy owns it, is in the architecture docs.
|
|
64
|
+
|
|
65
|
+
## Guards and tests
|
|
66
|
+
|
|
67
|
+
**Arm** — one `test(...)` block. A file has many; each is named for the property it holds, and a red
|
|
68
|
+
arm is read by its output rather than by its name.
|
|
69
|
+
|
|
70
|
+
**Plant** — deliberately breaking something to prove a check would catch it. A guard that has never
|
|
71
|
+
been shown failing has not been shown to work, so the hard version plants a NEW instance of the class
|
|
72
|
+
rather than the one the check was written against.
|
|
73
|
+
|
|
74
|
+
**Contract dictation** — the rule that a contract is stated once and everything else derives from it.
|
|
75
|
+
`contract-dictation.mjs` computes over a corpus; `scripts/contract-dictation-scan.mjs` builds that
|
|
76
|
+
corpus from the tracked tree, so a new authoring layer is caught structurally rather than because
|
|
77
|
+
somebody remembered to list it.
|
|
78
|
+
|
|
79
|
+
**Assert census** — counting the assertions that never executed. An assertion inside a branch nothing
|
|
80
|
+
reaches is a test that passes and guards nothing (`scripts/unexecuted-asserts.mjs`).
|
|
81
|
+
|
|
82
|
+
**Authority probe** — asking the live CLI whether it honours a boundary the driver sets, and whether
|
|
83
|
+
the turn survives being refused. CI has no CLI and no subscription, so it can only prove the decision
|
|
84
|
+
is made correctly; whether it is obeyed is a property of the real binary
|
|
85
|
+
(`scripts/authority-boundary-probe.mjs`).
|
package/docs/PORTAL.md
CHANGED
|
@@ -133,14 +133,14 @@ rather than quietly opening a passphrase door instead; and local mode refuses a
|
|
|
133
133
|
`PORTAL_SERVICE_HOST`, because off loopback the passphrase and the session cookie are on the wire in
|
|
134
134
|
clear. Put a TLS-terminating proxy in front if it has to be reachable.
|
|
135
135
|
|
|
136
|
-
Grants fixture: `{"tenants":{"demo":{"accounts":["
|
|
136
|
+
Grants fixture: `{"tenants":{"demo":{"accounts":["foxglade"],"users":{"cli@celta.example":["foxglade"]}}}}`.
|
|
137
137
|
|
|
138
138
|
The trigger lane needs the MCP HTTP face and an accounts-scoped ops token; without both, the run step
|
|
139
139
|
reports the trigger lane unwired and the plan step still works. **The face no longer has to be run in
|
|
140
140
|
its dev bypass to provide that**:`TRADEMARK_MCP_AUTH_MODE=token` runs it with a mandatory scoped
|
|
141
141
|
access key and no auth proxy — loopback only, and refused outright alongside
|
|
142
142
|
`TRADEMARK_MCP_AUTH_DISABLED`, which authenticates nobody. Mint the key with
|
|
143
|
-
`mint-token.mjs --scope ops --sub portal --verbs start_run,stop_run --accounts
|
|
143
|
+
`mint-token.mjs --scope ops --sub portal --verbs start_run,stop_run --accounts foxglade`, or let
|
|
144
144
|
`npx clearotron start` mint one in memory at every start and never write it down.
|
|
145
145
|
|
|
146
146
|
## Putting your own login provider in front
|
package/docs/README.md
CHANGED
|
@@ -38,6 +38,7 @@ the vendor flags approximate is UNKNOWN, never a number.
|
|
|
38
38
|
| [`PORTAL.md`](PORTAL.md) | The portal: what it serves, and who sees what |
|
|
39
39
|
| [`CLIENT-MCP.md`](CLIENT-MCP.md) | Publishing a connector your customers sign in to, and how their access is scoped. To connect *your own* app to *your own* runs, use [`../mcp-server/CONNECT.md`](../mcp-server/CONNECT.md) instead |
|
|
40
40
|
| [`E2E.md`](E2E.md) | Proving a deployment works end to end |
|
|
41
|
+
| [`GLOSSARY.md`](GLOSSARY.md) | The words this codebase uses in a particular way — one line each, for a contributor meeting them for the first time |
|
|
41
42
|
| [`SECURITY.md`](SECURITY.md) | The security envelope — what protects what, and where it is enforced in code. To report a vulnerability, use [`../SECURITY.md`](../SECURITY.md) |
|
|
42
43
|
|
|
43
44
|
Access control is stated once, in [`SECURITY.md`](SECURITY.md): who may see which runs, what an unset
|
|
@@ -76,10 +77,11 @@ The other half is structural: it fails on any undeclared identity inside a matte
|
|
|
76
77
|
which is the half that catches something new. Without the private table the guard runs on synthetic
|
|
77
78
|
sentinels: the machinery is exercised and there is nothing real to find.
|
|
78
79
|
|
|
79
|
-
**Demo clients are synthetic.**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
(`CLEAROTRON_CUSTOMERS_DIR`) and are never
|
|
80
|
+
**Demo clients are synthetic.** The published package carries a Generic default (`generic`) and one
|
|
81
|
+
demo brand owner. The repository holds three further invented accounts — gaming, functional drinks and
|
|
82
|
+
animal health — which exercise the per-client machinery and the test suite and are never published.
|
|
83
|
+
Real client bundles load at runtime from a private store (`CLEAROTRON_CUSTOMERS_DIR`) and are never
|
|
84
|
+
committed here.
|
|
83
85
|
|
|
84
86
|
**Real third-party names are deliberate.** Registers, marketplaces, regulators, research providers and
|
|
85
87
|
the parties to published decisions are named throughout the code, tests and fixtures — as facts about
|
package/docs/SECURITY.md
CHANGED
|
@@ -39,6 +39,23 @@ Exactly two roles exist: **staff**, admitted by an email-domain rule (`PORTAL_ST
|
|
|
39
39
|
created in the guest-list file by whoever administers the box — `npm run grant` is the editor for it —
|
|
40
40
|
never from a browser. The file is re-read per request, so a grant lands without a restart.
|
|
41
41
|
|
|
42
|
+
**A staff domain is a grant to everyone at that domain, so nothing derives one for you.** Every address
|
|
43
|
+
at a listed domain that gets past the sign-in door is staff, and staff sees every brand owner on the
|
|
44
|
+
instance. The install therefore derives `PORTAL_STAFF_DOMAINS` only from a sign-in address that names no
|
|
45
|
+
second person — the `<account>@localhost` form a single-user machine gets by default. Given a real
|
|
46
|
+
address it states the rule it would create, in the words the People & access screen will later use,
|
|
47
|
+
and refuses to write it: set `PORTAL_STAFF_DOMAINS` yourself, or answer the question `clearotron install`
|
|
48
|
+
asks. A webmail or shared provider is refused outright, because a rule built from one admits the public.
|
|
49
|
+
The People & access screen names the setting the rule came from and the file it is written in, so a
|
|
50
|
+
rule can be traced and undone by whoever finds it.
|
|
51
|
+
|
|
52
|
+
**That check is on the DERIVATION, not on the value.** A `PORTAL_STAFF_DOMAINS` you set yourself is
|
|
53
|
+
taken as written and never classified: `PORTAL_STAFF_DOMAINS=gmail.com` is accepted, and it admits
|
|
54
|
+
everyone at that provider who gets past the sign-in door. This is deliberate — a value someone typed is
|
|
55
|
+
a decision already taken, and the defect being fixed was a rule created with nobody asked — but it means
|
|
56
|
+
the protection is against the accident and not against the configuration. Read what you set, or set
|
|
57
|
+
nothing and answer the question the install asks.
|
|
58
|
+
|
|
42
59
|
|
|
43
60
|
- **On the proxy door**, every HTTP request re-validates a JWT from the fronting auth proxy. The
|
|
44
61
|
proxy is yours to choose — issuer, JWKS URL, claim and header are config
|
|
@@ -3,18 +3,30 @@
|
|
|
3
3
|
> Part of the architecture pack (`docs/architecture/`). The driver's module tree and the headless
|
|
4
4
|
> integrator contract are in [`driver/README.md`](../../driver/README.md).
|
|
5
5
|
|
|
6
|
-
The clearotron driver turns a
|
|
7
|
-
|
|
8
|
-
the
|
|
9
|
-
|
|
6
|
+
The clearotron driver turns a clearance request into a delivered preliminary trademark clearance
|
|
7
|
+
report. One matter in, one decision out — with the investigation, the reasoning, the challenge, and
|
|
8
|
+
the paper trail in between run by deterministic code that treats the model as a reasoning step,
|
|
9
|
+
never as the orchestrator.
|
|
10
10
|
|
|
11
11
|
## What it does
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
**Who this is for.** Anyone who needs to know whether a name is free to use and is willing to run the
|
|
14
|
+
search themselves: a lawyer, a brand team, or an individual clearing their own mark. You need three
|
|
15
|
+
self-serve accounts: a reasoning CLI, a register, and web research.
|
|
16
|
+
|
|
17
|
+
**What you get.** Four searches at different depths. A knockout screens up to eight names in minutes.
|
|
18
|
+
A global preliminary, a multi-country focus, or a full country search takes one name deeper, and the
|
|
19
|
+
full country search adds case law. Each delivers a written report with the records behind every
|
|
20
|
+
finding. What you do with it is your call.
|
|
21
|
+
|
|
22
|
+
The four searches are declared in `driver/products.mjs`, and that declaration is what the rest of the
|
|
23
|
+
system reads: a knockout screens up to eight names and skips case law, and only the full country
|
|
24
|
+
search turns case law on. A knockout takes 5 to 10 minutes; the other three take 1.5 to 2.5 hours.
|
|
25
|
+
|
|
26
|
+
Under any of them the system resolves the matter (mark, scope, deadline), investigates the trademark
|
|
27
|
+
registers and the live marketplace in the variations a searcher would try, reasons the risk in the
|
|
28
|
+
matter's own framework, challenges its own draft twice, and delivers a report with every factual
|
|
29
|
+
claim traceable to a fetched source record.
|
|
18
30
|
|
|
19
31
|
The driver is the machine that runs that whole distance. It is **not an agent**: it is a plain
|
|
20
32
|
Node.js process, launched by systemd, that executes a fixed pipeline of stages. Each stage that
|