clearotron 0.3.2-beta.1 → 0.3.2-beta.3
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 +29 -0
- package/INSTALL.md +2 -0
- package/README.md +15 -10
- package/bin/onboard.mjs +4 -4
- package/build-info.json +2 -2
- package/docs/CLIENT-MCP.md +54 -54
- package/docs/DELIVERY.md +16 -15
- package/docs/E2E.md +8 -8
- package/docs/GLOSSARY.md +2 -2
- package/docs/INTAKE.md +11 -11
- package/docs/ONBOARDING.md +17 -16
- package/docs/PORTAL.md +18 -16
- package/docs/README.md +10 -10
- package/docs/SECURITY.md +20 -20
- package/docs/architecture/01-product-overview.md +17 -16
- package/docs/architecture/02-architecture.md +6 -6
- package/docs/architecture/03-run-lifecycle.md +7 -7
- package/docs/architecture/04-configuration-reference.md +15 -13
- package/docs/architecture/05-config-governance.md +39 -30
- package/docs/architecture/05-customer-profiles.md +35 -35
- package/docs/architecture/06-operations-runbook.md +20 -20
- package/docs/architecture/07-quality-and-audit.md +17 -17
- package/docs/architecture/08-development-guide.md +3 -3
- package/docs/architecture/09-security-and-data.md +27 -27
- package/docs/architecture/README.md +1 -1
- package/docs/branding.md +8 -3
- package/docs/configuration.md +18 -18
- package/docs/writing-standard.md +3 -3
- package/driver/CHANGELOG.md +33 -0
- package/driver/driver.config.mjs +19 -3
- package/driver/package.json +1 -1
- package/driver/portal-service.mjs +34 -4
- package/driver/suite-census.json +118 -46
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/package.json +1 -1
- package/mcp-server/packs/client/CONNECT.md +6 -6
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-BsbasHjM.js → index-Bki5jT_N.js} +3376 -2971
- package/portal-ui/dist/assets/{index-DNQpLYZF.css → index-De2RFLbT.css} +972 -205
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/signa/src/core.js +29 -1
- package/scripts/ask-ai-render-check.mjs +287 -105
- package/scripts/release-note-required.mjs +16 -2
- package/scripts/release-pre-gate.mjs +111 -0
- package/scripts/settings-render-check.mjs +575 -0
- package/shared/brand.mjs +29 -0
- package/shared/connect-clients.mjs +99 -47
- package/shared/names-in-force.mjs +1 -0
- package/shared/stdio-connect.mjs +16 -2
- package/shared/writing-standard-classes.mjs +34 -3
|
@@ -21,8 +21,8 @@ Configuration lives in three layers, resolved in this order:
|
|
|
21
21
|
2. **The stage table** → `stages.mjs` — the single source of truth for what each pipeline stage
|
|
22
22
|
runs: model tier, reasoning effort, timeouts, skills, output file, validator, message. Per-stage
|
|
23
23
|
behaviour changes happen here, nowhere else.
|
|
24
|
-
3. **
|
|
25
|
-
per run. Documented in [05 —
|
|
24
|
+
3. **Company profiles** → `profiles/` — per-company knowledge and delivery configuration, frozen
|
|
25
|
+
per run. Documented in [05 — Company profiles](05-customer-profiles.md).
|
|
26
26
|
|
|
27
27
|
**Resolution semantics matter.** Every value on the `config` object is a **getter**, read at access
|
|
28
28
|
time, so all of them honour a mid-process env flip — the file's own NOTE beside `config` says why:
|
|
@@ -93,8 +93,8 @@ does after the stage's full retry ladder fails ([03 §5](03-run-lifecycle.md#5--
|
|
|
93
93
|
Notes that keep maintainers out of trouble (all verified against code):
|
|
94
94
|
|
|
95
95
|
- **`skillReads` on a stage entry is declarative metadata only.** The live skill reads are the
|
|
96
|
-
`reads([...])` call inside each stage's `message()`, which resolves per-
|
|
97
|
-
worked-examples files via the profile. Do not "reconcile" the two — that breaks per-
|
|
96
|
+
`reads([...])` call inside each stage's `message()`, which resolves per-company framework and
|
|
97
|
+
worked-examples files via the profile. Do not "reconcile" the two — that breaks per-company
|
|
98
98
|
framework selection (guarded by `test/profiles.test.mjs`).
|
|
99
99
|
- **Stage messages embed verbatim machine contracts** (`ESCALATE:` lines, `- ord:` stamps, closed
|
|
100
100
|
enums, band `state` values). Code parses these strings; rewording a message can break the parser.
|
|
@@ -193,13 +193,13 @@ deployment may override (verify live values per deployment).
|
|
|
193
193
|
| `CLEAROTRON_SKILLS_STORE_STRICT` | unset (warn) | Hard mode for the run door's doctrine-store check (`driver/skills-store-provenance.mjs`). The door classifies the store `CLEAROTRON_INSTRUCTIONS_DIR` names into the publication gate's three outcomes — **pass**, **fail** (uncommitted changes under the served path, or a HEAD not contained in the main line), **blocked** (could not be determined: git absent, an unreadable ancestor, no main-line ref, or a store no commit tracks). A store affirmatively outside any checkout passes: the walk to the filesystem root must complete with every step readable, so "no git evidence" can never be inferred from a directory this process cannot see. Unset, a non-pass prints one `[preflight]` line to stderr and stamps a `skills-store` event into the run's `_driver/run.jsonl`, and the run proceeds. Set (`1`; `0`/`off`/`false`/`no`/empty do not arm it), any non-pass **refuses the run before any spend** — `blocked` included, because a store whose state nobody can name is not a store anybody can reproduce. |
|
|
194
194
|
| `CLEAROTRON_SKILLS_STORE_MAIN_BRANCH` | `main` | Which branch name the check above treats as the main line. A deployment whose config store is on `master` sets this; otherwise that store has no main-line ref to be measured against and every run reports `blocked`. Only the branch name — the check prefers `origin/<name>` and falls back to the local ref. |
|
|
195
195
|
| `CLEAROTRON_MIN_FREE_DISK_MB` | `500` | Free space the run door requires on the filesystem holding `CLEAROTRON_WORK_DIR`, in MB. Below it a run is **refused** before anything is written — a disk that fills mid-run surfaces as a *missing artifact* at some later stage, which is indistinguishable from an engine or provider fault. Not a sizing estimate: one real delivered run measured 5.85 MB, so 500 MB is the line below which the filesystem is in trouble for reasons of its own.`0` disables the check; a non-numeric value **throws** rather than silently disabling it. A disk that cannot be measured is reported and the run proceeds — never read as room. |
|
|
196
|
-
| `CLEAROTRON_REPORTS_DIR` | **none — set it** | Publish pool (web-served). **No default since: unset refuses and names the variable.** It read`/srv/trademark-archive` — a deployed server's real
|
|
196
|
+
| `CLEAROTRON_REPORTS_DIR` | **none — set it** | Publish pool (web-served). **No default since: unset refuses and names the variable.** It read`/srv/trademark-archive` — a deployed server's real archive — so a forgotten export published into somebody else's clearances, and two entry points already carried hand-written defences against exactly that (`bin/onboard.mjs`, `bin/example.mjs`). Same shape as `CLEAROTRON_DATABASE` and `scripts/purge-runs.mjs`: guessing wrong is expensive, so it does not guess. Read-only surfaces (flag snapshot, status page, MCP options) degrade to "no pool" instead of throwing; anything that writes refuses. `driver/production-pool-guard.mjs` still names `/srv/trademark-archive` on purpose — that constant is a fact about where the archive is, not a default. |
|
|
197
197
|
| `CLEAROTRON_REPORTS_URL` | **none — set it** | Pool base URL used in notification links. No placeholder default: unset ⇒ the link is omitted and the runner logs `deployment config MISSING` at activation. It does not gate the queue (a missing hostname costs a link, not the deliverable), so treat that log line as the alarm. |
|
|
198
198
|
| `CLEAROTRON_ACCESS_DOMAIN` | unset (note omitted) | Identity domain named in the delivery email's access note ("sign in with a `<domain>` account"). Unset ⇒ the note is omitted rather than naming the wrong domain. |
|
|
199
199
|
| `CLEAROTRON_RUN_LOCK_DIR` | `<workspaceRoot>/prelim-run-locks` | Run-slot lock dir (turn locks under `…/turns`). |
|
|
200
200
|
| `CLEAROTRON_OUTBOX_DIR` | `<workspaceRoot>/prelim-outbox` | Delivery outbox (`<runId>.pending` wake markers). |
|
|
201
201
|
| `CLEAROTRON_OAUTH_BRIDGE` | module-relative `providers/oauth-mcp-bridge/bridge.mjs` | Case-law MCP bridge script. (Portable since the module-relative default; set explicitly only for a bridge outside the repo tree.) |
|
|
202
|
-
| `CLEAROTRON_REGISTER_CALL_LOG` | `~/trademark/telemetry/register-calls.jsonl`, or the existing file wherever it already is | Billing-grade provider-call ledger, shared by whichever ONE register provider is wired — not a vendor artifact. Every read site derives the default from`homedir()` at call time (2026-07-19: two sites had hardcoded a literal
|
|
202
|
+
| `CLEAROTRON_REGISTER_CALL_LOG` | `~/trademark/telemetry/register-calls.jsonl`, or the existing file wherever it already is | Billing-grade provider-call ledger, shared by whichever ONE register provider is wired — not a vendor artifact. Every read site derives the default from`homedir()` at call time (2026-07-19: two sites had hardcoded a literal home directory, splitting the ledger under any other service account — guarded by `test/deployment-hostnames.test.mjs`). |
|
|
203
203
|
| `CLEAROTRON_REGISTER_RECORD_LOG` | **runtime-injected per run**: `<runDir>/_driver/register-record-bodies.jsonl` | Citation-fidelity log: the BODY of every fetched official record. ** moved it INTO the run** — created with the run, unioned into the run's`_records/`, archived and purged with it. There is no retention setting and no cleanup job, because it no longer grows on the box: held globally it reached 432 MB in 61 days on production and needed a rotation timer on every install. **Do not set this by hand** — a fixed value pins every run's bodies to one file and restores the problem. A box upgraded across still holds its old global file; nothing writes or reads it, the driver names it once per process on stderr, and archiving it is one`mv`. An empty log cannot read as verified: the run's successful `record_fetch` rows in the (still global) call ledger are compared against the assembled record set, and a gap is reported as a failure. |
|
|
204
204
|
| `CORSEARCH_CALL_LOG` / `CORSEARCH_RECORD_LOG` | — | **Deprecated, honoured for one release.** These are the names these two variables carried before the rename. Unset on every deployed box (all three ran the homedir default), so what actually protects an upgrade is the filename fallback: a`corsearch-calls.jsonl` / `corsearch-records.jsonl` already on disk keeps being read where it sits. Resolution order is in `providers/_shared/ledger-path.mjs`. |
|
|
205
205
|
| `CLEAROTRON_BAND_RUN_DIR` | set per dispatch | The run dir the band MCP server writes into, injected per stage — unset means the server has no run to write to and says so rather than guessing one. |
|
|
@@ -219,7 +219,7 @@ deployment may override (verify live values per deployment).
|
|
|
219
219
|
| `CLEAROTRON_RUN_LOCK_POLL_MS` | 15000 | Run-slot acquire poll cadence. |
|
|
220
220
|
| `CLEAROTRON_ADMISSION_BUDGET_MS` | 7200000 (2 h) | Runner stops claiming new jobs after this per activation; leftovers re-trigger a fresh activation. |
|
|
221
221
|
| `CLEAROTRON_QUEUE_SCAN_MS` | 10000 (min 1000) | Mid-drain re-scan for newly arrived jobs. |
|
|
222
|
-
| `CLEAROTRON_WHATIF_MAX_CONCURRENT` | 1 (min 1) | How many
|
|
222
|
+
| `CLEAROTRON_WHATIF_MAX_CONCURRENT` | 1 (min 1) | How many queued what-ifs the runner drains at once. Deliberately NOT a run-slot: an experiment that took one from `CLEAROTRON_MAX_CONCURRENT_RUNS` could block an admitted paid clearance rather than merely share the box with it. This is a concurrency bound and not a spend control — the owner ruled spend controls out when he opened what-if to each company's people (2026-08-27), and nothing here refuses the next experiment. |
|
|
223
223
|
| `CLEAROTRON_STOP_GRACE_MS` | 60000 | Grace after first SIGTERM before exit(1). |
|
|
224
224
|
| `CLEAROTRON_MAX_CLAIM_AGE_MS` | 172800000 (48 h; 0 disables) | Hard ceiling on a claim's age (from the `.pid` sidecar mtime) — beyond it, re-claim regardless of liveness. |
|
|
225
225
|
| `CLEAROTRON_KNOCKOUT_VARIANT_CAP` | unset (⇒ the lane's own cap) | Ceiling on variants a knockout screens per name. Set only to bound an unusually wide batch; absent means the lane decides. |
|
|
@@ -284,8 +284,9 @@ because the rule is about what PRODUCT CODE reads, not about what a run reads.
|
|
|
284
284
|
| `DEMO_PORT` | `18900` | Port `npx clearotron demo` serves the replayed report on. `--port` overrides it. |
|
|
285
285
|
| `CLEAROTRON_DEMO` | unset | `1` puts this install in the DEMO posture. **Ordering is real**: the four products are listed and orderable, the form, the plan and the confirmation are the product's own, and the confirmation resolves to a finished report that already exists rather than dispatching — no engine turn, no register call, no queue entry, no run directory (ruling 2026-08-31, superseding the greyed-control ruling of the same day). A product the demo carries no finished report for refuses and names which one. It also re-aims two boot warnings written for an operator of a real deployment at the visitor who is not one, from one place (`driver/demo-posture.mjs`). **Set by `npx clearotron demo`, not by an operator** — it is passed explicitly to the two processes that have a reason to know (the portal and the MCP door; the worker is not told, because a demo never queues anything for it to drain), and those run with `CLEAROTRON_NO_ENV_FILE=1`, so a stray `.env` can neither put a live install into demo mode nor take a demo out of one. Anything but the literal `1` is not a demo. Replaces `PORTAL_DEMO`, which named only one of the processes that has to know. |
|
|
286
286
|
| `CLEAROTRON_TEST_FIXTURE_PROFILES` | unset | `1` makes the profile loader return the three suite fixtures, which are refused from every roster otherwise. Set by `scripts/test-run.mjs`, never by an operator; an explicit `includeTestFixtures` argument beats it. Effect class `harness`; the full contract is its row in `.env.example`. |
|
|
287
|
-
| `CLEAROTRON_DEMO_PROFILES` | unset | `1` makes the profile loader return the bundled demo
|
|
287
|
+
| `CLEAROTRON_DEMO_PROFILES` | unset | `1` makes the profile loader return the bundled demo company, which a fresh install does not resolve (ruling 2026-09-08). Set by `clearotron demo`, `start --demo` and the suite runner, never by an operator. The gate is on the bundled layer, so a deployment's own configured store keeps its `demoData` companies either way. Effect class `harness`; the full contract is its row in `.env.example`. |
|
|
288
288
|
| `CLEAROTRON_ORGANISATION_NAME` | unset | Your organisation's name, written quoted by `npx clearotron install`, which asks for it and not for a sign-in address. The first `clearotron start` files it as the first organisation in the grants file (`CLEAROTRON_ACCESS_FILE`) when that file holds none; from then on the grants file holds the name and renaming is an edit there. The portal also reads it, to name the organisation on its top bar and on its sign-in refusal page. Unset ⇒ no organisation is invented. `clearotron start --organisation <name>` supplies it for one start, and a demo never reads it. Effect class `deployment`; the full contract is its row in `.env.example`. |
|
|
289
|
+
| `CLEAROTRON_ADMINISTRATOR_CONTACT` | unset | Who a signed-in person contacts to change the address, the permissions or the companies on their sign-in: a mail address (with or without `mailto:`) or an http(s) address. The portal reads it where it reads the organisation name and sends it on the same `/me` answer; Preferences then links the words "Clearotron administrator" to it. Unset, or any value that is neither a mail nor a web address, ⇒ the words are plain text. Effect class `deployment`; the full contract is its row in `.env.example`. |
|
|
289
290
|
| `PORTAL_LOCAL_CREDENTIAL` | `~/.cordillera/portal-local-credential.json` | Where local sign-in keeps its passphrase DIGEST. `clearotron start` points it inside the install's own base directory for a new install, so it mints its own passphrase instead of adopting a digest another install left; an install that has been signing in with the shared file keeps it. `npx clearotron demo` always points it inside the demo's own base directory and mints a new passphrase there on every start, so a demo never inherits a digest minted for another address, and removing the demo stays one `rm -rf`. |
|
|
290
291
|
| `PORTAL_LOCAL_PASSPHRASE` | unset | **NEVER set this in a file.** An internal one-shot handoff, not an operator control: on a first FOREGROUND start the supervisor mints the passphrase and hands it to the portal it spawns *at the spawn call*, so the closing summary can print the value beside the address rather than sending a first-time reader back into eleven startup log lines for the one value in this product that cannot be read back. It is deliberately absent from the composed child environments, because that composition is what `--background` writes into the units' env file — a passphrase there would be a permanent plaintext copy on disk and the product's own sentence, "it is stored only as a digest", would stop being true. Setting it in any env file recreates exactly that. Lost passphrase: `clearotron passphrase --reset`. |
|
|
291
292
|
| `PORTAL_URL` | `http://127.0.0.1:18802`, or built from `PORTAL_SERVICE_HOST`/`PORTAL_SERVICE_PORT` | Where the deploy tick's live-surface check expects to reach the portal. |
|
|
@@ -296,7 +297,8 @@ because the rule is about what PRODUCT CODE reads, not about what a run reads.
|
|
|
296
297
|
| `CLEAROTRON_UPDATER_STAMP` | `_updater-identity.json` beside the update script, in the directory the updater runs from | The full path of the file in which the updater that deploys this box records which copy of itself ran. The updater writes it and deploy health reads it under this one name, so a box that moves the stamp sets it once for both. On a box with no updater unit, setting it says an updater exists elsewhere and is to be judged. Effect class `deployment`. |
|
|
297
298
|
| `CLEAROTRON_CUT_REF` | `HEAD` | Which ref the cut decision reads the version from. **Read only by the release workflow, never set on a deployment.** The jobs that ask about `main` set it to `origin/main` explicitly, because their checkout is pinned to the run's own ref and `HEAD` there is that ref rather than the branch they are deciding about. A job that asks the wrong subject gets a confident wrong answer. |
|
|
298
299
|
| `CLEAROTRON_RELEASE_WAIT_MS` | 25 minutes | How long a requested cut waits for the version pull request to merge itself before giving up. **Read only by the release workflow.** A rehearsal sets it to `0` so the wiring is exercised without holding a runner. Giving up is a quiet success by design — the scheduled run underneath catches a cut whose wait expired — so this budget failing shows up as a slow job rather than a red one. |
|
|
299
|
-
| `
|
|
300
|
+
| `ACTIONS_APPROVE_TOKEN` | unset | A GitHub token with Actions read and write, used to approve the version pull request's parked CI run so a cut does not wait for a person. **Read only by the release workflow, never set on a deployment.** The built-in token cannot approve a run — GitHub blocks self-approval — so this is a second, separate credential. Unset is the ordinary case and not an error: the script names the absent token and exits successfully, and the version run waits for a person as it did before. Actions write is broader than approval alone — it also dispatches workflows, cancels any run in the repository and deletes run logs. |
|
|
301
|
+
| `CLEAROTRON_AGENT_MCP_URL` | unset (⇒ `null`) | The API-key MCP door advertised to a signed-in person. Null until that door is deployed, and the UI keeps its honest empty state rather than inventing a URL. |
|
|
300
302
|
| `CLEAROTRON_AGENTS` | derived | Comma-separated agent ids for `scripts/purge-runs.mjs` to sweep. |
|
|
301
303
|
| `CLEAROTRON_E2E_DIR` | **none — the script refuses without it** | The config repo's `e2e/` directory. There is one suite and it is not in this repo (ruling 2026-08-07), so the comparison script names the variable rather than defaulting anywhere. |
|
|
302
304
|
| `CLEAROTRON_E2E_EXPECT_DEMO_ROSTER` | unset | `1` makes the live-surface check REQUIRE the bundled demo roster. For a box that is meant to ship the demos; off elsewhere, so a real deployment is not failed for lacking them. |
|
|
@@ -323,14 +325,14 @@ cannot be read as one list.
|
|
|
323
325
|
| `PERPLEXITY_API_KEY` | Marketplace grid research server. |
|
|
324
326
|
| `EUIPO_CLIENT_ID` / `EUIPO_CLIENT_SECRET` / `EUIPO_ENVIRONMENT` | EUIPO OAuth2 client-credentials; environment defaults to **sandbox** — production requires an explicit set. |
|
|
325
327
|
| `CLEAROTRON_MCP_URL` | Staff "Ask your AI" connector base, baked into the report at render — there is one report, and `portal-report.mjs` strips the block for non-staff readers at serve time. Fails closed → link omitted when unset. |
|
|
326
|
-
| `CLEAROTRON_CLIENT_MCP_URL` |
|
|
327
|
-
| `CLIENT_MCP_ACCOUNT_ACCESS` | `1` admits the signed-in
|
|
328
|
+
| `CLEAROTRON_CLIENT_MCP_URL` | The client connector's "Ask your AI" base address, served LIVE by the portal's `/portal/api/mcp-access` (the Use-your-AI screen) — set it on the portal unit, or the screen shows its (correct) empty state. The render no longer reads it: the baked block is the staff connector above. Fails closed → `{url:null}` when unset. **Never pin a placeholder value:** the fail-closed branch keys on the var being EMPTY, so a placeholder host defeats the guard and hands out a dead connector address. |
|
|
329
|
+
| `CLIENT_MCP_ACCOUNT_ACCESS` | `1` admits the signed-in principal from outside your staff (kind `account`) on the client MCP surface: no token, scoped to the companies the CF-verified email is granted. Off by default. Set WITHOUT `CLEAROTRON_ACCESS_FILE` ⇒ fatal start (`accountsForEmail` answers `"*"` with no guest list, and an unscoped wildcard must never be admitted). See `docs/CLIENT-MCP.md` — enabling this for a company with no `runCaps.dailyRuns` lets it start paid searches all day. |
|
|
328
330
|
|
|
329
331
|
### Scrub guard
|
|
330
332
|
|
|
331
333
|
| Var | Default | Meaning |
|
|
332
334
|
|---|---|---|
|
|
333
|
-
| `CLEAROTRON_IDENTIFIER_BLOCKLIST` | unset (⇒ sentinels) | Path to `identifier-blocklist.json` in the
|
|
335
|
+
| `CLEAROTRON_IDENTIFIER_BLOCKLIST` | unset (⇒ sentinels) | Path to `identifier-blocklist.json` in the config store: every real company and mark this repo has carried, paired with the demo twin that replaced it. Read by the identifier guard and the publication scan, never on a run path. **Unset is a supported mode**, not a degraded one — the guard runs on synthetic sentinels built into it, which exercise every branch of the matcher and identify nobody; that is how the public repository runs it. Set but unreadable, malformed, or below the size floor ⇒ **throws** (a truncated table reads as a smaller blocklist, and a smaller blocklist reads as a cleaner repo). The table is kept out of the product repo because the list of names *is* the thing the guard protects. |
|
|
334
336
|
|
|
335
337
|
### A/B, telemetry, debug
|
|
336
338
|
|
|
@@ -341,7 +343,7 @@ cannot be read as one list.
|
|
|
341
343
|
| `CLEAROTRON_STAGE_THINKING` | unset (⇒ each stage's declared tier) | Per-stage thinking-tier override, `<stage>=<tier>[,…]` (e.g. `register-digest=high`) — the A/B instrument, so a suite arm needs no code fork or redeploy between runs. Thinking only: models are deliberately not overridable here, so one arm can never move two variables. **The env override is a dev/test instrument**; a permanent change edits the tier in `stages.mjs` and ships. Unknown stage or tier **throws** — `effortFor()` falls back to `medium`, so a typo would otherwise run a stage at a tier nobody chose and every number measured against it would be wrong. Read per call, so an arm can flip mid-process. |
|
|
342
344
|
| `CLEAROTRON_AZURE_MODEL` | `azure-openai/gpt-5.4` | Target of the `azure` alias — a legacy catalogue entry no engine can run (see model tiers above). |
|
|
343
345
|
| `CLEAROTRON_DUMP_JSON` | unset | Dump each attempt's raw engine envelope to `_driver/<stage>.attempt<N>.rawjson.json`. Opt-in: any value except `0`/`off`/`false`/`no`/empty arms it. |
|
|
344
|
-
| `CLEAROTRON_DISPATCH_RECORD` | **on** | Write the verbatim message of every stage dispatch to `_driver/<stage>.attempt<N>[.repair<M>].dispatch.txt`, with `{file, sha, bytes, chars, kind}` on the attempt row. **Default ON** — `0`/`off`/`false`/`no` disarms it. Unlike `CLEAROTRON_DUMP_JSON` beside it, this is opt-OUT: the question it answers ("was the model given this?") is asked *after* the run that raised it, so a flag someone had to remember would be off on exactly the run that needed it. The files carry
|
|
346
|
+
| `CLEAROTRON_DISPATCH_RECORD` | **on** | Write the verbatim message of every stage dispatch to `_driver/<stage>.attempt<N>[.repair<M>].dispatch.txt`, with `{file, sha, bytes, chars, kind}` on the attempt row. **Default ON** — `0`/`off`/`false`/`no` disarms it. Unlike `CLEAROTRON_DUMP_JSON` beside it, this is opt-OUT: the question it answers ("was the model given this?") is asked *after* the run that raised it, so a flag someone had to remember would be off on exactly the run that needed it. The files carry the company's identity verbatim and are deliberately not in the artifact table. |
|
|
345
347
|
| `CLEAROTRON_GATHER_SESSION_KEY` / `CLEAROTRON_GATHER_AGENT` / `CLEAROTRON_GATHER_SESSION_ID` | set per stage | Telemetry attribution into the provider-call ledger (set by the gather config; not operator-set). |
|
|
346
348
|
| `CLEAROTRON_RECORD_AXIS` | set per dispatch (unset ⇒ the stage is not fanned out) | Binds one fan-out turn of a recording stage to the single member it may write. `stageOnce` suffixes a fan-out stage's label with its axis, the gather config resolves `<stage>:<axis>` back to the base stage's tool group, and this carries the axis to the recording server. A call whose payload names a different member than the turn is bound to is REFUSED, so a seat cannot write into a sibling's file — without the binding every turn of the fan-out would record over member one. Set by the driver; not operator-set. |
|
|
347
349
|
| `PORTAL_READ_MODEL` | `claude-sonnet-5` | The model the portal's own compose-read turn uses. Distinct from the pipeline's tiers: this is a portal surface, not a stage. |
|
|
@@ -22,16 +22,16 @@
|
|
|
22
22
|
|
|
23
23
|
| Tier | Who changes it | Through what | Examples |
|
|
24
24
|
|---|---|---|---|
|
|
25
|
-
| **T1 —
|
|
26
|
-
| **T2 — Staff/ops UI** |
|
|
25
|
+
| **T1 — Company** | A company's own people (self-service) | Agent conversation → job spec; the portal | mark, classes, deadline, own profile fields, flags |
|
|
26
|
+
| **T2 — Staff/ops UI** | The operator's staff | the portal's company settings screens, profile-service | company profiles, project overlays, saved searches, run curation |
|
|
27
27
|
| **T3 — Operator env** | Ops, on the box | the EnvironmentFile + restart/next-activation | hostnames, models, caps, timeouts, feature gates |
|
|
28
28
|
| **T4 — Backend-only** | Ops, deliberately | env secrets, unit files, code defaults | credentials, token secrets, data-plane paths, dev seams |
|
|
29
|
-
| **T5 — Edge auth** | Ops, in the edge console | the auth proxy's own console, off-box (reference:
|
|
29
|
+
| **T5 — Edge auth** | Ops, in the edge console | the auth proxy's own console, off-box (reference: Cloudflare Zero Trust) | ingress routes, access apps + policies |
|
|
30
30
|
|
|
31
|
-
The UI mapping, as the `portal-ui` nav is actually structured: T2 → **`brand.profile`** ("
|
|
32
|
-
|
|
33
|
-
("
|
|
34
|
-
**read-only** in **`admin.config`** ("
|
|
31
|
+
The UI mapping, as the `portal-ui` nav is actually structured: T2 → **`brand.profile`** ("Profile",
|
|
32
|
+
everything specific to one company) + **`brand.projects`** + **`brand.searches`**
|
|
33
|
+
("Search templates"), plus **`admin.access`** ("People & access") for who may sign in; T3 →
|
|
34
|
+
**read-only** in **`admin.config`** ("Installation settings"): which engine is running the searches and who is
|
|
35
35
|
billed for them, and every provider a search depends on with a configured-or-missing state — secrets,
|
|
36
36
|
paths and switch names deliberately excluded, visible to staff, changed only on the backend.
|
|
37
37
|
|
|
@@ -55,8 +55,8 @@ recorded.
|
|
|
55
55
|
|---|---|---|---|
|
|
56
56
|
| 1 | The EnvironmentFile (`%h/.env`) | secrets + T3 operator env for the generic units | ops, by hand. **Keep it dedicated to this product where possible** — sharing one env file with another stack couples rotation and backup blast-radius across products |
|
|
57
57
|
| 2 | systemd user units (`driver/systemd/`, `mcp-server/remote/`) | ports, CF Access mirrors, paths. Two kinds — **generic** (defer to the EnvironmentFile; safe to sync verbatim) vs **template** (placeholders in-repo, real identity-edge values in the live copies; merge by hand after a diff — banner-marked). Copying a template over a live unit replaces working auth with placeholders that *look* configured, which is why the two kinds are distinguished at all | this repo's deploy |
|
|
58
|
-
| 3 | The config store (separate git repo) |
|
|
59
|
-
| 4 |
|
|
58
|
+
| 3 | The config store (separate git repo) | company profiles, project overlays, frameworks/skills | profile-service + portal git auto-commit; frameworks by git edit (deliberate) |
|
|
59
|
+
| 4 | The allowlist file (`CLIENT_ACCESS_MAP` JSON) | per-email → company grants | git + PR (today it lives in the integrator's repo — a candidate to move into the config store so the product owns all its config) |
|
|
60
60
|
| 5 | Web-server / edge routing (reverse-proxy block or tunnel ingress; this deployment: Caddy + Cloudflare) | which hostname reaches which loopback port; the pool's file-server root | ops |
|
|
61
61
|
| 6 | Data roots (`CLEAROTRON_REPORTS_DIR`, shares) | published reports, quality flags | services |
|
|
62
62
|
| 7 | **Edge console** (off-box; reference: Cloudflare) | ingress routes + access apps and who they admit | ops — **not recoverable from the box**; keep an exported note of apps/AUDs in the deployment's ops repo |
|
|
@@ -75,7 +75,7 @@ fills the same three roles, and the product requires none of them by name.
|
|
|
75
75
|
`MCP_ALLOWED_EMAIL_DOMAINS` appear in unit files as **mirrors of the dashboard**, not a second
|
|
76
76
|
setup. They must match the edge; rotation touches both. The CF-gated loopback services
|
|
77
77
|
deliberately do NOT load the shared EnvironmentFile so one app's AUD can't shadow another's
|
|
78
|
-
(client-access
|
|
78
|
+
(`client-access`/`client-mcp` refuse to start if the client AUD == the staff AUD).
|
|
79
79
|
3. **Rendered links (T3, env):** `CLEAROTRON_REPORTS_URL`, `CLEAROTRON_MCP_URL`, `CLEAROTRON_CLIENT_MCP_URL`,
|
|
80
80
|
`CLEAROTRON_ACCESS_DOMAIN` are neither of the above — they are what gets *printed into reports and
|
|
81
81
|
emails*. Wrong values render dead links; unset values are omitted.
|
|
@@ -88,18 +88,18 @@ product doc.
|
|
|
88
88
|
|
|
89
89
|
| Surface | Tier | Managed via | Storage | State today |
|
|
90
90
|
|---|---|---|---|---|
|
|
91
|
-
| **Job spec** (per matter): id, forwarder(+email/domain), markName/marks[], classes\|goods/use, ref, profileKey, projectKey, searchLevel/recipeKey, deliveryRoute, customer(+Unknown), deliverableSpec, commercialFlexibility, priorUse, dupOverride, deadline, brief | T1 | agent conversation → `start_run` MCP verb (ops token) → queue; validated by `enqueue-schema.mjs` | queue → run dir | LIVE (conversational; portal `run/plan`+`run` API exists) |
|
|
92
|
-
| **
|
|
93
|
-
| **Project overlays** (8 overlayable keys) | T2 | profile-service UI | config store `profiles/projects/<cust>/` | LIVE. The project form deliberately withholds `defaultProduct` and both `delivery` sub-keys — for the first the sparse save path has no `""` ⇒ clear branch, so the control could only ever be turned on; for the second the engine replaces `delivery` wholesale, so a partial overlay would silently drop the
|
|
91
|
+
| **Job spec** (per matter): id, forwarder(+email/domain), markName/marks[], classes\|goods/use, ref, profileKey, projectKey, searchLevel/recipeKey, deliveryRoute, `customer`(+Unknown), deliverableSpec, commercialFlexibility, priorUse, dupOverride, deadline, brief | T1 | agent conversation → `start_run` MCP verb (ops token) → queue; validated by `enqueue-schema.mjs` | queue → run dir | LIVE (conversational; portal `run/plan`+`run` API exists) |
|
|
92
|
+
| **Company profile** (17 keys — identity/rating/provenance: name, matchDomains, selfExclusionOwners, frameworkPath, workedExamplesPath, allowedRecipes, jxPolicy, runCaps, demoData (`true` marks the record as demo data; a real clearance is refused at the runner's admission wall); overlayable: platforms, defaultClasses, defaultJurisdictions, marketplaceDensity, delivery, riskAppetite, industry, defaultProduct) | T2 (staff) + T1 (a company's people edit its own via portal §C) | profile-service UI (staff, `/profiles/*`); the portal (own profile) | config store, git auto-commit | LIVE. Merge law: project **replaces** every overlayable key except `platforms`, which **unions** (the company's floor is never subtractable) |
|
|
93
|
+
| **Project overlays** (8 overlayable keys) | T2 | profile-service UI | config store `profiles/projects/<cust>/` | LIVE. The project form deliberately withholds `defaultProduct` and both `delivery` sub-keys — for the first the sparse save path has no `""` ⇒ clear branch, so the control could only ever be turned on; for the second the engine replaces `delivery` wholesale, so a partial overlay would silently drop the company's other sub-keys. Both are company-level controls until the server side changes |
|
|
94
94
|
| **Frameworks / skills** (risk-framework-<key>.md + .manifest.json, worked examples, SKILL.md) | T2 (senior-lawyer content) | git edits in the config store (deliberate — the prose deck is the rating authority) | config store `skills/prelim-search/` | LIVE via git; no UI by design |
|
|
95
95
|
| **Recipes / saved searches** (base level + component toggles + emailTable/defaultDeadlineDays/standingInstructions) | T2 | recipe-service UI | `<recipesDir>/<cust>/<slug>.json`, git | **DARK** — code complete, no unit deployed. A saved search is honoured wherever it resolves (the `CLEAROTRON_RECIPES_MODE` door was retired 2026-07-27) |
|
|
96
96
|
| **Run curation** (archive folds, republish, index regen) | T2 | `pool-admin.mjs` CLI only | pool `archive-tags.json` | LIVE, CLI-only |
|
|
97
|
-
| **
|
|
98
|
-
| **Ops tokens** (scope ops/user, verbs,
|
|
97
|
+
| **Allowlist** (`{version, grants:[{email, customer}]}`) | T2 | git + PR on the `CLIENT_ACCESS_MAP` file | see §2 row 4 | LIVE, file-only; surfaced read-only at `admin.access` |
|
|
98
|
+
| **Ops tokens** (scope ops/user, verbs, companies, TTL) | T4 | `mint-token.mjs` CLI; jti denylist file | operator-held tokens | LIVE, CLI |
|
|
99
99
|
|
|
100
100
|
## 4b. The install surface names ()
|
|
101
101
|
|
|
102
|
-
The variables a **
|
|
102
|
+
The variables a **user or installer** ever types carry the product’s own prefix. They are listed
|
|
103
103
|
by name in §5 below and in the upgrade table in INSTALL.md.
|
|
104
104
|
`CLEAROTRON` is the internal codename of the first product this engine shipped and means nothing to a reader
|
|
105
105
|
who has not read the code. Vendor credentials keep the vendor’s name (`SIGNA_API_KEY`,
|
|
@@ -152,8 +152,9 @@ structural, or dev seam); [dev] = dev/test seam, never set in prod.
|
|
|
152
152
|
| `CLEAROTRON_MCP_URL` | fail-closed omit | Staff "Ask your AI" connector base |
|
|
153
153
|
| `CLEAROTRON_CLIENT_MCP_URL` | fail-closed omit | Client connector base |
|
|
154
154
|
| `CLEAROTRON_ACCESS_DOMAIN` | omit note | Identity domain in the delivery email access note |
|
|
155
|
-
| `CLEAROTRON_BOX` | none — required (unset or unrecognised ⇒ the unit-inventory line fails and names this variable) | Which deployment this is (`prod` \| `test`), for `scripts/live-surface-check.mjs`'s unit inventory. Self-declared, never inferred from the
|
|
156
|
-
| `CLEAROTRON_BRAND_NAME` / `CLEAROTRON_BRAND_TAGLINE` / `CLEAROTRON_BRAND_PRODUCT` | reference-
|
|
155
|
+
| `CLEAROTRON_BOX` | none — required (unset or unrecognised ⇒ the unit-inventory line fails and names this variable) | Which deployment this is (`prod` \| `test`), for `scripts/live-surface-check.mjs`'s unit inventory. Self-declared, never inferred from the login name. Without a recognised value, the half that looks for a unit declared here and not running cannot run, because a guess would report every other deployment's units missing; so the line fails instead of passing with that half unrun |
|
|
156
|
+
| `CLEAROTRON_BRAND_NAME` / `CLEAROTRON_BRAND_TAGLINE` / `CLEAROTRON_BRAND_PRODUCT` | reference-deployment literals in `shared/brand.mjs` | Installation brand seam (single-sourced) |
|
|
157
|
+
| `CLEAROTRON_ADMINISTRATOR_CONTACT` | unset ⇒ plain text, no link | The administrator contact Preferences links "Clearotron administrator" to — a mail or http(s) address, read in `shared/brand.mjs`; any other value reads as unset |
|
|
157
158
|
|
|
158
159
|
### 5.2 Engine & models — T3
|
|
159
160
|
|
|
@@ -230,7 +231,7 @@ Per-lane `CLEAROTRON_NATIVE_LANGUAGE_<XX>` — `zh`, `ja` and `ko` all work (`LA
|
|
|
230
231
|
`driver/jx-lanes.mjs`); default on, set `0` to kill one lane — plus `CLEAROTRON_JX_SERP_DEADLINE_MS`.
|
|
231
232
|
`CLEAROTRON_JX_SERP_GRID`, `CLEAROTRON_JX_NATIVEREAD` and `CLEAROTRON_JX_CONSUME` were **deleted by item 8**
|
|
232
233
|
under ADR-0002: each was off here and on in production, so the shipped default described a
|
|
233
|
-
configuration nobody ran. The slices now run on conditions a
|
|
234
|
+
configuration nobody ran. The slices now run on conditions a requester can already see.
|
|
234
235
|
|
|
235
236
|
`CLEAROTRON_KNOCKOUT_MODE`, `CLEAROTRON_JX_LANES` and `CLEAROTRON_RECIPES_MODE` were **RETIRED 2026-07-27** and
|
|
236
237
|
have no reader. Depth availability is decided by `BUILT` (`driver/search-policy.mjs`) and the wired
|
|
@@ -260,13 +261,13 @@ bodies to one file and restores the unbounded growth the move removed),
|
|
|
260
261
|
`PROFILE_REPO_ROOT`, `RECIPE_REPO_ROOT`, `CLEAROTRON_OAUTH_BRIDGE` (module-relative),
|
|
261
262
|
`OAUTH_BRIDGE_CREDS_DIR`, `OAUTH_BRIDGE_CLIENT_NAME`.
|
|
262
263
|
|
|
263
|
-
`PROFILE_DIR` **left this list.** The
|
|
264
|
+
`PROFILE_DIR` **left this list.** The company store is named once, by
|
|
264
265
|
`CLEAROTRON_CUSTOMERS_DIR`, and resolved through `shared/customer-store.mjs` for the settings
|
|
265
266
|
surface and the runs alike. It is not accepted as a fallback: a box setting only the retired name
|
|
266
267
|
would otherwise pull the settings surface onto a second store, which is the split that issue
|
|
267
268
|
closed. It still appears in `env-set-in-production.txt` because production is measured, not
|
|
268
269
|
edited — that box runs pre-rebuild code and genuinely still sets it.
|
|
269
|
-
| `CLEAROTRON_JX_SUBCLASS_DB` | unset ⇒ the similar-group lookup REFUSES by name | Path to `similar-groups.db`, built by `node providers/jx-subclass/load-public.mjs` from the committed `public/` tables. Not committed (the office sources permit redistributing the data, not their prose), so a deployment builds it. Unset or missing is a refusal, never an empty answer: `node:sqlite` creates an empty file on open and every lookup over it would read as "no similar groups" — a false clear in the offices a Western
|
|
270
|
+
| `CLEAROTRON_JX_SUBCLASS_DB` | unset ⇒ the similar-group lookup REFUSES by name | Path to `similar-groups.db`, built by `node providers/jx-subclass/load-public.mjs` from the committed `public/` tables. Not committed (the office sources permit redistributing the data, not their prose), so a deployment builds it. Unset or missing is a refusal, never an empty answer: `node:sqlite` creates an empty file on open and every lookup over it would read as "no similar groups" — a false clear in the offices a Western company can least check |
|
|
270
271
|
|
|
271
272
|
### 5.7 Delivery & comms — T3/T4
|
|
272
273
|
|
|
@@ -300,10 +301,10 @@ set), `TRADEMARK_MCP_TOKEN_SECRET` (+`TRADEMARK_MCP_TOKEN_SECRET_PREVIOUS` rotat
|
|
|
300
301
|
only**; the portal refuses to start without it — see [docs/SECURITY.md](../SECURITY.md)).
|
|
301
302
|
|
|
302
303
|
**Scrub guard (test-time only — never read on a run path, and outside the five tiers above).**
|
|
303
|
-
`CLEAROTRON_IDENTIFIER_BLOCKLIST` points at `identifier-blocklist.json` in the
|
|
304
|
-
retired
|
|
305
|
-
of names *is* what the guard protects. Unset is a SUPPORTED mode, not a degraded one —
|
|
306
|
-
on synthetic sentinels built into the guard, which is how the public repository runs
|
|
304
|
+
`CLEAROTRON_IDENTIFIER_BLOCKLIST` points at `identifier-blocklist.json` in the config store: the
|
|
305
|
+
retired roster of real companies and marks the scrub guard matches on, kept out of the product repo
|
|
306
|
+
because the list of names *is* what the guard protects. Unset is a SUPPORTED mode, not a degraded one —
|
|
307
|
+
the guard runs on synthetic sentinels built into the guard, which is how the public repository runs
|
|
307
308
|
it. Set-but-unreadable, malformed, or below the size floor **throws**: a truncated table reads as a
|
|
308
309
|
smaller blocklist, and a smaller blocklist reads as a cleaner repo. **Whether the real table is required
|
|
309
310
|
is the CALLER's declaration, never the environment's**: `publication-scan.mjs` asks for it in its own
|
|
@@ -386,7 +387,7 @@ to be written out.
|
|
|
386
387
|
| recipe service | `RECIPE_OIDC_ISSUER` | `RECIPE_JWKS_URL` | `RECIPE_EMAIL_CLAIM` | `RECIPE_AUTH_HEADER` |
|
|
387
388
|
| client MCP origin | `CLIENT_MCP_OIDC_ISSUER` | `CLIENT_MCP_JWKS_URL` | `CLIENT_MCP_EMAIL_CLAIM` | `CLIENT_MCP_AUTH_HEADER` |
|
|
388
389
|
|
|
389
|
-
Each is T4, each is optional, and each unset falls back exactly as the two older sets do — the client
|
|
390
|
+
Each is T4, each is optional, and each unset falls back exactly as the two older sets do — the client MCP
|
|
390
391
|
row to the staff face's matching value first (`TRADEMARK_MCP_OIDC_ISSUER`, `TRADEMARK_MCP_JWKS_URL`,
|
|
391
392
|
`TRADEMARK_MCP_EMAIL_CLAIM`, `TRADEMARK_MCP_AUTH_HEADER`), then to the shapes derived from
|
|
392
393
|
`CF_ACCESS_TEAM` for whichever JWT-fronting proxy the deployment runs.
|
|
@@ -419,7 +420,7 @@ replayed as the other. First start in local mode mints a passphrase and prints i
|
|
|
419
420
|
byte-for-byte the behaviour that shipped before the name existed, dev bypass included; `token` requires
|
|
420
421
|
a valid HMAC-signed scoped access key on **every** request and runs no auth proxy at all. An
|
|
421
422
|
unrecognised value is fatal. `token` is the far end of the local install's Start button — the portal
|
|
422
|
-
holds a verb-scoped,
|
|
423
|
+
holds a verb-scoped, company-capped ops key and this door demands it — and it is **not** a bypass:
|
|
423
424
|
`lib/http-handler.mjs` refuses outright to build it alongside `TRADEMARK_MCP_AUTH_DISABLED`, because a
|
|
424
425
|
synthetic identity would answer before the mandatory key ever ran. The mode is loopback-only (an ops key
|
|
425
426
|
travels in a header or the query string), requires `TRADEMARK_MCP_ALLOWED_HOSTS` exactly as the
|
|
@@ -495,7 +496,15 @@ origin; all portal config lives server-side in portal-service.
|
|
|
495
496
|
|
|
496
497
|
These are read by the release workflow and by nothing a deployment runs. They are listed here because a
|
|
497
498
|
name absent from this register is a name nobody can look up, not because an operator has any reason to set
|
|
498
|
-
one — and setting
|
|
499
|
+
one — and setting any of them on a box does nothing at all.
|
|
500
|
+
|
|
501
|
+
`ACTIONS_APPROVE_TOKEN` (unset) — a GitHub token with Actions read and write, used to approve the
|
|
502
|
+
version pull request's parked CI run so that a cut does not wait for someone to click. That run is
|
|
503
|
+
authored by the repository's own Actions bot and GitHub parks bot-authored runs; the built-in token
|
|
504
|
+
cannot release one, because self-approval is blocked deliberately. Unset is the ordinary case: the
|
|
505
|
+
script names the absent token and exits successfully, and the run waits for a person as before.
|
|
506
|
+
Actions write is wider than approving — it also dispatches workflows, cancels any run in the
|
|
507
|
+
repository and deletes run logs — so it is worth rotating on the same schedule as a deploy key.
|
|
499
508
|
|
|
500
509
|
`CLEAROTRON_CUT_REF` (default `HEAD`) — which ref the cut decision reads the version from. The jobs that
|
|
501
510
|
ask about `main` set it to `origin/main` explicitly, because their checkout is pinned to the run's own ref
|
|
@@ -515,8 +524,8 @@ change. The product's known mirror classes:
|
|
|
515
524
|
|
|
516
525
|
| Value class | Copies (by design) | Break mode if they diverge |
|
|
517
526
|
|---|---|---|
|
|
518
|
-
| CF Access team / AUD / domain gates | edge (dashboard) + each fronted unit's inline env | staff or
|
|
519
|
-
| `TRADEMARK_MCP_TOKEN_SECRET` | EnvironmentFile + any integrator-hosted artifacts-MCP env block | run-bound
|
|
527
|
+
| CF Access team / AUD / domain gates | edge (dashboard) + each fronted unit's inline env | staff or company lockout, service by service |
|
|
528
|
+
| `TRADEMARK_MCP_TOKEN_SECRET` | EnvironmentFile + any integrator-hosted artifacts-MCP env block | run-bound `user` tokens fail verification |
|
|
520
529
|
| Register-provider credentials | EnvironmentFile + integrator plugin config (when both consume the provider) | one consumer silently unauthenticated |
|
|
521
530
|
| Profiles/skills store paths | EnvironmentFile + service unit files (+ integrator MCP env) | roster-mismatch class (the PR #14 incident) |
|
|
522
531
|
| Pool root | code default + units + web-server file root (+ integrator MCP env) | reports publish where nothing serves |
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
# 05 —
|
|
1
|
+
# 05 — Company Profiles
|
|
2
2
|
|
|
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
|
-
> This chapter describes the profile *mechanism*. Real
|
|
6
|
-
> via `CLEAROTRON_CUSTOMERS_DIR` and no
|
|
7
|
-
>
|
|
5
|
+
> This chapter describes the profile *mechanism*. Real company bundles load from an external store
|
|
6
|
+
> via `CLEAROTRON_CUSTOMERS_DIR` and no real company is named here; the package ships `generic` and the
|
|
7
|
+
> demo company, and the repository holds three further synthetic profiles for the test suite — see
|
|
8
8
|
> [`driver/profiles/`](../../driver/profiles/).
|
|
9
9
|
|
|
10
|
-
One engine, never forked — three layers. The reasoning core is shared by every
|
|
10
|
+
One engine, never forked — three layers. The reasoning core is shared by every company; the company's
|
|
11
11
|
context sharpens judgment without ever overriding it; the delivery layer shapes presentation only.
|
|
12
|
-
The profile system is where that principle is *enforced*, not just stated: every per-
|
|
12
|
+
The profile system is where that principle is *enforced*, not just stated: every per-company knob is
|
|
13
13
|
either consumed by named code or rejected at load, and configuration that attempts to move a legal
|
|
14
14
|
rating is refused by pattern guards and by stage-level firewalls.
|
|
15
15
|
|
|
16
16
|
## The bundle
|
|
17
17
|
|
|
18
|
-
A
|
|
19
|
-
(`<key>.context.md`), a per-
|
|
18
|
+
A company = one git-owned JSON file `profiles/<key>.json`, plus optionally: a prose context pack
|
|
19
|
+
(`<key>.context.md`), a per-company rating framework pair in `skills/prelim-search/`
|
|
20
20
|
(`risk-framework-<key>.md` + its `.manifest.json`, plus worked examples), and per-engagement
|
|
21
21
|
project overlays under `profiles/projects/<key>/`.
|
|
22
22
|
|
|
@@ -27,25 +27,25 @@ that reads it in `FIELD_CONSUMERS`, and CI asserts the manifest and the key list
|
|
|
27
27
|
| Key | Layer | What it drives |
|
|
28
28
|
|---|---|---|
|
|
29
29
|
| `name` (required) | identity | Anchor for the self-exclusion gate (`applicantMatchesProfile` — word-boundary containment, so "Company" matches "Company Corporation" but not "Companyish Ltd") |
|
|
30
|
-
| `matchDomains[]` | identity | Forwarder-domain fallback resolution; cross-file overlap is a load error (directory order must never decide a
|
|
30
|
+
| `matchDomains[]` | identity | Forwarder-domain fallback resolution; cross-file overlap is a load error (directory order must never decide a company) |
|
|
31
31
|
| `platforms[]` (required) | structural | The store domains the marketplace grid sweeps, verbatim — dictated into the worker spec and enforced by the receipts join. Bare domains only, no `"web"` (the general-web cell is implicit), no duplicates: a one-character slip bricks every run under the profile, so load guards are strict |
|
|
32
32
|
| `defaultClasses[]` / `defaultJurisdictions[]` | structural | Matter-frame defaults when the request names none |
|
|
33
|
-
| `selfExclusionOwners[]` | structural | Never flag the
|
|
33
|
+
| `selfExclusionOwners[]` | structural | Never flag the company against itself — seeded only when the job's applicant *is* the profile's company |
|
|
34
34
|
| `marketplaceDensity` | structural | `sparse` (default) or `dense` — selects the grid cell budget (98 vs 16 cells/call, both calibrated from real truncation incidents) |
|
|
35
35
|
| `industry` | context | Sector context in the matter frame — context, never a rule |
|
|
36
36
|
| `riskAppetite` | context | Prose posture only; reaches **curation stages only**, never synthesis |
|
|
37
37
|
| `delivery` | delivery | `{email: "summary", privileged: bool, style: prose, template: closed-enum}` — presentation only. `email` is inert as of 2026-07-28 (every run gets a cover note); `"table"` is accepted-but-retired so stored profiles keep loading |
|
|
38
|
-
| `frameworkPath` | rating authority | The
|
|
39
|
-
| `workedExamplesPath` | context | Per-
|
|
38
|
+
| `frameworkPath` | rating authority | The company's own risk framework deck; absent ⇒ the Generic default rates the matter ("nothing in between") |
|
|
39
|
+
| `workedExamplesPath` | context | Per-company synthesis depth target |
|
|
40
40
|
| `defaultProduct` | entitlement | Which search runs when a request names none (`search-policy.mjs`). May be unset — a clearance that names no product is then named by its own resolved territories |
|
|
41
|
-
| `allowedRecipes[]` | entitlement | The closed menu of searches this
|
|
41
|
+
| `allowedRecipes[]` | entitlement | The closed menu of searches this company may trigger; absent ⇒ everything allowed. Non-empty when present |
|
|
42
42
|
| `jxPolicy` | entitlement | The native-language deepening posture (declared lanes, escalation, provider stance) — policy, never capability. Frozen into the run sidecar so a resume keeps it |
|
|
43
|
-
| `runCaps` | admission | Per-
|
|
43
|
+
| `runCaps` | admission | Per-company admission caps `{maxQueued?, dailyRuns?, monthlyRuns?}`, integers 1–10000, at least one set. Enforced at the runner's claim chokepoint for **both** intake doors (email and portal) — the run-slot cap does not bound people who can trigger their own searches. **Two behaviours it is worth knowing before you rely on a cap** — see below |
|
|
44
44
|
|
|
45
45
|
**Two `runCaps` behaviours to know before relying on a cap** (`checkRunCaps`, `driver/runner.mjs`):
|
|
46
46
|
**a cap written onto `generic` does nothing** — it is exempt by design, so a long-lived credential
|
|
47
|
-
pointed there spends without limit; and a
|
|
48
|
-
`DEFAULT_CLIENT_DAILY_RUNS`, not unlimited.
|
|
47
|
+
pointed there spends without limit; and a run a company's own people start with no
|
|
48
|
+
`dailyRuns` gets `DEFAULT_CLIENT_DAILY_RUNS`, not unlimited.
|
|
49
49
|
|
|
50
50
|
**Derived, never stored** (storing either is its own louder load error): the grid floor
|
|
51
51
|
`minCellsPerVariant = platforms.length + 1` and `batchSize = gridCellBudget / floor`.
|
|
@@ -56,7 +56,7 @@ never sees it): curated background facts, standing concerns, prior-matter learni
|
|
|
56
56
|
conditionals are rejected at load; the fix is to rephrase the rule as a concern or question. It
|
|
57
57
|
feeds `report-overview` as context that "NEVER changes a band" and never reaches synthesis.
|
|
58
58
|
|
|
59
|
-
**The framework layer** (`framework.mjs`) is where per-
|
|
59
|
+
**The framework layer** (`framework.mjs`) is where per-company rating vocabulary lives. The
|
|
60
60
|
framework itself is a prose deck the model reasons *with*; the code-side manifest
|
|
61
61
|
(`<framework>.manifest.json`) carries **vocabulary and order only** — 2–8 ordered bands, each
|
|
62
62
|
`{label, tone}`, no digits in labels ("a numbered band is a score in disguise"), and structurally
|
|
@@ -64,8 +64,8 @@ framework itself is a prose deck the model reasons *with*; the code-side manifes
|
|
|
64
64
|
CI-linted, every profile's framework selection must resolve to a manifest, and bands-shaped decks
|
|
65
65
|
are checked to carry no legacy scoring machinery (`test/framework-lint.test.mjs`).
|
|
66
66
|
|
|
67
|
-
> **Per-
|
|
68
|
-
> "doctrine-lockstep" check keeping rating tables byte-identical across
|
|
67
|
+
> **Per-company decks legitimately diverge, and no test forbids it.** There is no
|
|
68
|
+
> "doctrine-lockstep" check keeping rating tables byte-identical across company frameworks: the
|
|
69
69
|
> framework in force is what *rates* the matter, so divergence is the point. The integrity guarantee
|
|
70
70
|
> is elsewhere — closed manifests (vocabulary only), the anti-rule guards below, and the
|
|
71
71
|
> framework-lint suite (`test/framework-lint.test.mjs`). Code comments in `stages.mjs` still
|
|
@@ -76,19 +76,19 @@ split 8/8 and every future field must choose a side (`PROJECT_KEYS ∪ CUSTOMER_
|
|
|
76
76
|
KNOWN_PROFILE_KEYS`, asserted by test). **Overlayable (8):** platforms, defaultClasses,
|
|
77
77
|
defaultJurisdictions, marketplaceDensity, delivery, riskAppetite, industry, defaultProduct — a
|
|
78
78
|
distinct engagement legitimately runs a different product and different marketplaces.
|
|
79
|
-
**
|
|
80
|
-
allowedRecipes, jxPolicy, runCaps — identity, rating authority and entitlement stay whole-
|
|
81
|
-
and a project that could widen its own caps would hollow out the
|
|
82
|
-
merges project →
|
|
79
|
+
**Company-only (8):** name, matchDomains, selfExclusionOwners, frameworkPath, workedExamplesPath,
|
|
80
|
+
allowedRecipes, jxPolicy, runCaps — identity, rating authority and entitlement stay whole-company,
|
|
81
|
+
and a project that could widen its own caps would hollow out the company's. Effective resolution
|
|
82
|
+
merges project → company → generic with a per-field `origins` map frozen into the run.
|
|
83
83
|
|
|
84
84
|
## Resolution and the frozen sidecar
|
|
85
85
|
|
|
86
86
|
```mermaid
|
|
87
87
|
flowchart TD
|
|
88
88
|
J["job arrives (queue file)"] --> PK{"job.profileKey<br/>names a roster key?"}
|
|
89
|
-
PK -- yes --> P1["that
|
|
89
|
+
PK -- yes --> P1["that company wins<br/>(intake AI resolved it)"]
|
|
90
90
|
PK -- no --> FD{"forwarderDomain matches<br/>a profile's matchDomains?"}
|
|
91
|
-
FD -- yes --> P2["domain-matched
|
|
91
|
+
FD -- yes --> P2["domain-matched company"]
|
|
92
92
|
FD -- no --> P3["generic (required fallback)"]
|
|
93
93
|
P1 --> PROJ{"job.projectKey?"}
|
|
94
94
|
P2 --> PROJ
|
|
@@ -110,10 +110,10 @@ Three rules make this trustworthy:
|
|
|
110
110
|
profiles/ edit mid-run can never change a live run's floor or platforms; resume/experiment
|
|
111
111
|
and every validator read the same immutable file; a corrupt sidecar crashes loudly. `profileSha`
|
|
112
112
|
(canonical-JSON sha256) makes "which profile/framework rated this run" verifiable by recompute.
|
|
113
|
-
A mid-run late-bind of the
|
|
113
|
+
A mid-run late-bind of the company re-classifies findings only — it never re-resolves the
|
|
114
114
|
profile.
|
|
115
|
-
- **An unbound run can never be presented
|
|
116
|
-
delivery (no
|
|
115
|
+
- **An unbound run can never be presented as any company's.** No key + no domain ⇒ generic ⇒ neutral
|
|
116
|
+
delivery (no review table, no privileged header) — pinned by test.
|
|
117
117
|
|
|
118
118
|
## The guardrails
|
|
119
119
|
|
|
@@ -124,7 +124,7 @@ Three rules make this trustworthy:
|
|
|
124
124
|
| **Anti-threshold guard** | `riskAppetite` (and context pack, and delivery style) reject numeric/threshold/rule shapes at load — percentages, comparison operators, "threshold", level/composite cutoffs, imperative ratings | `profiles.mjs` |
|
|
125
125
|
| **The D1 firewall** | Stage messages decide which stage *sees* what: synthesis gets framework + worked examples and **no** appetite/pack/style; the three curation stages get them labelled "emphasis only / NEVER changes a band" | `stages.mjs` |
|
|
126
126
|
| **Framework manifest constraints** | Vocabulary + order only; no digit labels; closed keys; 2–8 bands; deck⇄manifest lint | `framework.mjs`, `test/framework-lint.test.mjs` |
|
|
127
|
-
| **Freeze completeness** | A configured field must be carried by `freezeProfile` or it is silently never applied — the exact 2026-06-19 bug (frameworkPath dropped from the freeze quietly rated two
|
|
127
|
+
| **Freeze completeness** | A configured field must be carried by `freezeProfile` or it is silently never applied — the exact 2026-06-19 bug (frameworkPath dropped from the freeze quietly rated two companies under the Generic default); regression-pinned | `pipeline.mjs`, `test/framework-freeze.test.mjs` |
|
|
128
128
|
|
|
129
129
|
Honest boundary: the regex guards are conservative-reject and *necessary, not sufficient* — a
|
|
130
130
|
pure-prose rule can pass them; the evaluation layer (reference library / review) is the catch for
|
|
@@ -137,7 +137,7 @@ A small loopback HTTP service (`profile-service.mjs`, systemd user unit, `PROFIL
|
|
|
137
137
|
which carries the deployment's own directories and is therefore part of a deployment rather than of
|
|
138
138
|
this source tree.
|
|
139
139
|
|
|
140
|
-
- **Endpoints**: roster list, per-
|
|
140
|
+
- **Endpoints**: roster list, per-company view (config + derived values + framework box), validate
|
|
141
141
|
(server-side dry run), save (validated **auto-commit** — creates or updates the JSON + context
|
|
142
142
|
pack and commits authored as the signed-in identity), health (unauthenticated liveness), plus the
|
|
143
143
|
project endpoints.
|
|
@@ -150,13 +150,13 @@ this source tree.
|
|
|
150
150
|
(a body-supplied author is ignored); every write re-runs the *same* load-time validators the
|
|
151
151
|
driver uses, so the UI can never persist a profile the driver would reject; and
|
|
152
152
|
`frameworkPath`/`workedExamplesPath` are **code-owned** — the on-disk value always wins because a
|
|
153
|
-
2026-07-04 UI save once silently stripped both fields and flipped two
|
|
153
|
+
2026-07-04 UI save once silently stripped both fields and flipped two companies to the Generic default
|
|
154
154
|
framework (`preserveCodeOwned`, `profile-service.mjs`).
|
|
155
155
|
- **Governance posture**: UI saves are validated auto-commits with no PR gate. Recovery is
|
|
156
156
|
`git revert` plus the audit log (`profiles/_audit.log`, git-tracked). In-flight runs are
|
|
157
157
|
unaffected (sidecar freeze); changes ship to the next run.
|
|
158
158
|
|
|
159
|
-
## Onboarding a new
|
|
159
|
+
## Onboarding a new company — the runbook
|
|
160
160
|
|
|
161
161
|
**Path A — git PR** (the bespoke-engagement default):
|
|
162
162
|
|
|
@@ -173,7 +173,7 @@ this source tree.
|
|
|
173
173
|
report reads right.
|
|
174
174
|
5. Merge + deploy.
|
|
175
175
|
|
|
176
|
-
**Path B — the config UI**: sign in through CF Access → "+ New
|
|
176
|
+
**Path B — the config UI**: sign in through CF Access → "+ New company" → fill the form →
|
|
177
177
|
"Check first" (dry-run validate) → "Save" (validated auto-commit + audit line).
|
|
178
178
|
|
|
179
179
|
**Either path, afterwards:**
|
|
@@ -181,9 +181,9 @@ this source tree.
|
|
|
181
181
|
- **Routing**: intake stamps `job.profileKey` (the primary selector); optionally add
|
|
182
182
|
`matchDomains` for forwarder-based fallback. A profile with empty `matchDomains` is reachable by
|
|
183
183
|
profileKey only.
|
|
184
|
-
- **Per-
|
|
184
|
+
- **Per-company framework** (optional; git-only, legal-team work — the UI cannot set it): add the
|
|
185
185
|
deck + manifest + worked examples under `skills/prelim-search/`, set the two paths in the profile
|
|
186
|
-
JSON via git. Until then the
|
|
186
|
+
JSON via git. Until then the company rates under the Generic default.
|
|
187
187
|
- **Per-engagement overlay** (optional): `profiles/projects/<key>/<slug>.json` with the 8
|
|
188
188
|
overlayable keys; intake stamps `job.projectKey` to select it.
|
|
189
189
|
|
|
@@ -85,7 +85,7 @@ properties below, each of which was learned the expensive way:
|
|
|
85
85
|
them; silent auto-promotion twice resurrected intentionally deleted files. A skill relocated into
|
|
86
86
|
the driver tree is relocation, not drift.
|
|
87
87
|
4. **Driver block** — install dependencies, copy the seven unit files + `daemon-reload`,
|
|
88
|
-
install/enable/restart the sibling services (profile-service
|
|
88
|
+
install/enable/restart the sibling services (`profile-service`, `client-access`, `client-mcp`), and
|
|
89
89
|
**pre-create the queue dirs and the outbox**: an inotify `.path` watch on a directory that does
|
|
90
90
|
not exist never fires.
|
|
91
91
|
5. **Restart the gateway last**, with no live run and the triggers stopped, then re-enable the four
|
|
@@ -260,37 +260,37 @@ pre-change telemetry directory), because the resolver reads the file that is the
|
|
|
260
260
|
## Access control and instance isolation
|
|
261
261
|
|
|
262
262
|
Three capabilities you meet once the engine produces reports other people want to see. Each is off,
|
|
263
|
-
or
|
|
263
|
+
or shared by everyone, until you configure it.
|
|
264
264
|
|
|
265
|
-
###
|
|
265
|
+
### Organisation grants — who may see which runs
|
|
266
266
|
|
|
267
|
-
`CLEAROTRON_ACCESS_FILE` names one JSON file: the guest list. It maps **
|
|
268
|
-
**
|
|
269
|
-
`profileKey` — the
|
|
267
|
+
`CLEAROTRON_ACCESS_FILE` names one JSON file: the guest list. It maps **organisations** (`tenants`) to the
|
|
268
|
+
**companies** (`accounts`) they may see, and people within an organisation to a subset of it. A company
|
|
269
|
+
key is a `profileKey` — the company bundle a run froze at start (§4) — so a grant is expressed in the same
|
|
270
270
|
vocabulary as the runs it filters.
|
|
271
271
|
|
|
272
272
|
**Unset means enforcement is off for the MCP faces** — whoever gets through your door sees every run.
|
|
273
273
|
**It is not a valid state for the portal, which refuses to start without a guest list**; the access
|
|
274
|
-
model is stated once, in [docs/SECURITY.md](../SECURITY.md). Setting the variable turns
|
|
274
|
+
model is stated once, in [docs/SECURITY.md](../SECURITY.md). Setting the variable turns company scoping
|
|
275
275
|
on for **every face at once** — the portal, the MCP read face, the client face, and ops tokens. There is
|
|
276
276
|
no half-enforced state and no per-surface switch to forget.
|
|
277
277
|
|
|
278
|
-
Resolution: the signed-in email is matched against each
|
|
279
|
-
a `*@domain` wildcard; a user value of `"*"` means the
|
|
280
|
-
several
|
|
281
|
-
signs in and sees an empty world. That is deliberate. Signing in is not being enrolled.
|
|
278
|
+
Resolution: the signed-in email is matched against each organisation's `users` map, by exact address or
|
|
279
|
+
by a `*@domain` wildcard; a user value of `"*"` means the organisation's whole grant; an address
|
|
280
|
+
appearing in several organisations gets the union. An authenticated address matching nothing is granted
|
|
281
|
+
nothing — it signs in and sees an empty world. That is deliberate. Signing in is not being enrolled.
|
|
282
282
|
|
|
283
283
|
**A file that is set but unreadable throws rather than failing open.** A missing path, malformed JSON,
|
|
284
284
|
or a file with no `tenants` object stops the process and names the reason. Keep that in mind when you
|
|
285
285
|
move the file: a guest list you configured and then broke must never resolve to "admit everybody".
|
|
286
286
|
|
|
287
|
-
`examples/grants.example.json` is a working file over the synthetic demo
|
|
287
|
+
`examples/grants.example.json` is a working file over the synthetic demo companies this repo ships.
|
|
288
288
|
Copy it, point `CLEAROTRON_ACCESS_FILE` at your copy, sign in as one of its addresses, and you see
|
|
289
|
-
exactly that
|
|
289
|
+
exactly that organisation's companies. Then delete it and write your own — it names nobody real, which also
|
|
290
290
|
means it grants nothing you have.
|
|
291
291
|
|
|
292
292
|
`npx clearotron start` (§6) writes an empty roster (`{"tenants": {}}`) into its state directory: your own staff
|
|
293
|
-
address is admitted,
|
|
293
|
+
address is admitted, nobody else is enrolled, and enforcement is already on.
|
|
294
294
|
|
|
295
295
|
### Ops tokens — a credential for the verbs that spend
|
|
296
296
|
|
|
@@ -308,8 +308,8 @@ node mcp-server/mint-token.mjs --scope ops --sub <principal-name> \
|
|
|
308
308
|
tokens are then distinguishable in the log; one shared token makes them permanently indistinguishable.
|
|
309
309
|
- `--verbs` is a least-privilege allowlist of write tools. A connector minted without `stop_run`
|
|
310
310
|
cannot call it — the check sits at the one chokepoint every tool call passes, not in each tool.
|
|
311
|
-
- `--accounts` caps the token to a set of
|
|
312
|
-
searches and never a real
|
|
311
|
+
- `--accounts` caps the token to a set of company keys, so a trial integration can start demo
|
|
312
|
+
searches and never a real company's.
|
|
313
313
|
- The token is printed once and stored nowhere. Losing it means minting another.
|
|
314
314
|
|
|
315
315
|
**Revoking one.** Every mint prints a `jti`. Write that line into the file named by
|
|
@@ -338,11 +338,11 @@ That makes the environment the whole isolation boundary, so set it to make a tes
|
|
|
338
338
|
| Axis | Variables | Why it is structural |
|
|
339
339
|
|---|---|---|
|
|
340
340
|
| Data plane | `CLEAROTRON_REPORTS_DIR`, `CLEAROTRON_WORK_DIR`, `CLEAROTRON_QUEUE_DIR`, `CLEAROTRON_OUTBOX_DIR` | separate directories mean a test run cannot publish into the live archive even by mistake |
|
|
341
|
-
|
|
|
341
|
+
| Company configs | `CLEAROTRON_CUSTOMERS_DIR`, `CLEAROTRON_INSTRUCTIONS_DIR` **unset** on the test instance | it then resolves the repo's synthetic demo companies and cannot read a real bundle |
|
|
342
342
|
| Engine | the running engine's binary variable (`CLEAROTRON_CLAUDE_PATH` / `CLEAROTRON_CODEX_PATH`) pointed at `driver/test/mock-claude.mjs` | the mock engine needs no provider credentials, so a test instance can hold none — the strongest form of "spends nothing" |
|
|
343
343
|
| Ports | give the test instance its own for the portal, MCP face and profile service | a default that collides with a live service turns a dry run into a probe of the live one, and it looks like it worked |
|
|
344
344
|
| Auth | `TRADEMARK_MCP_AUTH_DISABLED=1` with `TRADEMARK_MCP_DEV=1` is **loopback-only, enforced in code** | the switch cannot be used to open a remote face |
|
|
345
345
|
|
|
346
|
-
The strongest isolation is still two instances with their own pool and their own config store.
|
|
347
|
-
grants above are for the case where one instance must be shared safely — demos, trials, per-
|
|
348
|
-
scoping inside one
|
|
346
|
+
The strongest isolation is still two instances with their own pool and their own config store. Organisation
|
|
347
|
+
grants above are for the case where one instance must be shared safely — demos, trials, per-person
|
|
348
|
+
scoping inside one organisation — not a substitute for this.
|