clearotron 0.2.4 → 0.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +13 -2
- package/CONTRIBUTING.md +1 -1
- package/INSTALL.md +62 -38
- package/bin/brandowner.mjs +18 -169
- package/bin/clearotron.mjs +3 -1
- package/bin/connect.mjs +28 -19
- package/bin/disconnect.mjs +3 -3
- package/bin/example.mjs +7 -7
- package/bin/framework-preflight.mjs +49 -0
- package/bin/grant.mjs +151 -93
- package/bin/onboard.mjs +220 -138
- package/bin/start.mjs +146 -137
- package/bin/stop.mjs +2 -2
- package/bin/update.mjs +1 -1
- package/build-info.json +2 -2
- package/docs/CLIENT-MCP.md +2 -2
- package/docs/E2E.md +12 -2
- package/docs/ONBOARDING.md +1 -1
- package/docs/PORTAL.md +14 -13
- package/docs/SECURITY.md +23 -24
- package/docs/architecture/04-configuration-reference.md +12 -5
- package/docs/architecture/05-config-governance.md +7 -7
- package/docs/architecture/07-quality-and-audit.md +1 -1
- package/docs/architecture/08-development-guide.md +5 -0
- package/docs/configuration.md +118 -0
- package/docs/decisions/0004-documentation-structure.md +2 -2
- package/docs/decisions/0006-what-the-public-repository-carries.md +2 -2
- package/driver/CHANGELOG.md +70 -0
- package/driver/ask-ledger.mjs +2 -2
- package/driver/cancel.mjs +27 -0
- package/driver/case-law-sources.mjs +3 -3
- package/driver/company-bundle.mjs +261 -0
- package/driver/compare.mjs +1 -1
- package/driver/compose-read.mjs +2 -2
- package/driver/config-inventory.mjs +2 -2
- package/driver/contract-audit.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +1 -1
- package/driver/contract-vocabulary.mjs +1 -1
- package/driver/declination-call.mjs +1 -1
- package/driver/deliver-trigger.sh +3 -3
- package/driver/dev-portal.mjs +3 -1
- package/driver/digest-queue.mjs +1 -1
- package/driver/disposition-tool.mjs +1 -1
- package/driver/doc-constants.mjs +1 -1
- package/driver/drain-posture.mjs +2 -2
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +44 -9
- package/driver/effective-scope.mjs +30 -1
- package/driver/effort-model.mjs +6 -6
- package/driver/engine/CONTRACT.md +2 -2
- package/driver/engine/anthropic-agent.mjs +11 -11
- package/driver/engine/jx-turn.mjs +1 -1
- package/driver/engine/mcp/gather-config.mjs +29 -4
- package/driver/engine/mcp/recording-server.mjs +73 -1
- package/driver/engine/openai-agent.mjs +1 -1
- package/driver/engine/probe.mjs +28 -4
- package/driver/enqueue-schema.mjs +23 -3
- package/driver/findings-model.mjs +2 -2
- package/driver/flag-snapshot.mjs +2 -2
- package/driver/floor-duty.mjs +2 -2
- package/driver/frame-diff-model.mjs +1 -1
- package/driver/framework-preflight.mjs +143 -0
- package/driver/gateway.mjs +9 -1
- package/driver/hit-list.mjs +1 -1
- package/driver/jx-lanes.mjs +1 -1
- package/driver/jx.mjs +1 -1
- package/driver/knockout-assess-record.mjs +1 -1
- package/driver/knockout-review-record.mjs +435 -0
- package/driver/order-probe.mjs +1 -1
- package/driver/outbox-backoff.mjs +2 -2
- package/driver/owner-use-check.mjs +2 -2
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +105 -8
- package/driver/pipeline.mjs +81 -30
- package/driver/plain-register.mjs +77 -3
- package/driver/portal-access.mjs +141 -74
- package/driver/portal-config-view.mjs +59 -70
- package/driver/portal-report.mjs +4 -4
- package/driver/portal-service.mjs +348 -141
- package/driver/portal-upstream.mjs +105 -17
- package/driver/predelivery-lint.mjs +43 -18
- package/driver/product-rows.mjs +1 -1
- package/driver/products.mjs +1 -1
- package/driver/profile-page.html +30 -5
- package/driver/profile-service.mjs +197 -26
- package/driver/profiles.mjs +48 -1
- package/driver/publish/index.mjs +31 -13
- package/driver/publish/knockout.mjs +9 -5
- package/driver/publish/office-record-links.mjs +189 -0
- package/driver/publish/parse.mjs +3 -3
- package/driver/publish/publish-inputs.mjs +26 -0
- package/driver/publish/render-knockout.mjs +42 -42
- package/driver/publish/render.mjs +29 -4
- package/driver/publish/report-data.mjs +2 -2
- package/driver/publish/seed-pool.mjs +1 -1
- package/driver/publish/templates/report.css +8 -8
- package/driver/publish/xlsx.mjs +49 -7
- package/driver/queue-watch-verdict.mjs +2 -2
- package/driver/recipe-service.mjs +1 -1
- package/driver/record-carry.mjs +1 -1
- package/driver/reference-score.mjs +1 -1
- package/driver/reference-strip-signatures.mjs +1 -1
- package/driver/register-availability.mjs +4 -3
- package/driver/register-count.mjs +3 -3
- package/driver/register-records.mjs +1 -1
- package/driver/repair-composers.mjs +1 -1
- package/driver/repairs.mjs +3 -3
- package/driver/replay-archive.mjs +1 -1
- package/driver/report-card-record.mjs +1 -1
- package/driver/result-noun-fields.mjs +5 -0
- package/driver/roster-verdict.mjs +48 -5
- package/driver/run-activity.mjs +1 -1
- package/driver/run-requirements.mjs +18 -5
- package/driver/runner.mjs +24 -15
- package/driver/search-policy.mjs +8 -8
- package/driver/senior-rights.mjs +1 -1
- package/driver/skills/prelim-search/delivery-contract.md +1 -1
- package/driver/skills/prelim-search/risk-framework-triage.md +10 -7
- package/driver/stages-knockout.mjs +72 -6
- package/driver/stages.mjs +10 -10
- package/driver/suite-census.json +238 -70
- package/driver/synthesis-record.mjs +2 -2
- package/driver/systemd/clearotron-client-mcp.service +3 -3
- package/driver/systemd/clearotron-deploy.service +2 -2
- package/driver/systemd/clearotron-mcp-face.service +1 -1
- package/driver/systemd/clearotron-portal.service +3 -3
- package/driver/systemd/clearotron-worker.service +5 -5
- package/driver/systemd/install-census.mjs +1 -1
- package/driver/systemd/render-units.mjs +9 -9
- package/driver/terminal-clamp.mjs +1 -1
- package/driver/trigger-cap.mjs +18 -2
- package/driver/unit-inventory.mjs +8 -8
- package/driver/usage-ledger.mjs +5 -3
- package/driver/verify-knockout.mjs +7 -7
- package/driver/verify.mjs +5 -5
- package/driver/whatif-memo-run.mjs +1 -1
- package/driver/whatif-queue.mjs +3 -3
- package/driver/whatif-worker.mjs +2 -2
- package/examples/README.md +1 -1
- package/examples/grants.example.json +25 -24
- package/mcp-server/CHANGELOG.md +10 -0
- package/mcp-server/http-server.mjs +3 -3
- package/mcp-server/key-socket.mjs +1 -1
- package/mcp-server/lib/audit-view.mjs +3 -3
- package/mcp-server/lib/brief.mjs +3 -3
- package/mcp-server/lib/driver.mjs +1 -1
- package/mcp-server/lib/events.mjs +1 -1
- package/mcp-server/lib/http-handler.mjs +2 -2
- package/mcp-server/lib/instructions.mjs +2 -2
- package/mcp-server/lib/knockout.mjs +1 -1
- package/mcp-server/lib/ops.mjs +6 -3
- package/mcp-server/lib/options.mjs +15 -4
- package/mcp-server/lib/plan.mjs +7 -6
- package/mcp-server/lib/runs.mjs +10 -0
- package/mcp-server/lib/whatif.mjs +5 -5
- package/mcp-server/package.json +1 -1
- package/mcp-server/packs/README.md +1 -1
- package/mcp-server/remote/client-mcp-apikey.service +1 -1
- package/mcp-server/remote/client-mcp.service +2 -2
- package/mcp-server/remote/trademark-artifacts-http.service +1 -1
- package/mcp-server/server.mjs +38 -24
- package/package.json +2 -2
- package/portal-ui/dist/assets/{index-KFAHMgdT.js → index-CWTHP0sH.js} +3471 -1901
- package/portal-ui/dist/assets/{index-1ziUJX1E.css → index-KpytsmNH.css} +79 -26
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/lane-probe.mjs +9 -3
- package/providers/jx-subclass/lookup.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/oauth-mcp-bridge/systemd/courtlistener-mcp.service +1 -1
- package/scripts/citation-drift-report.mjs +1 -1
- package/scripts/citation-line-check.mjs +2 -2
- package/scripts/drive-env-check.mjs +1 -1
- package/scripts/e2e.mjs +5 -5
- package/scripts/env-audit.mjs +13 -1
- package/scripts/headless-page.mjs +5 -5
- package/scripts/live-surface-check.mjs +26 -2
- package/scripts/mint-names-in-force.mjs +19 -5
- package/scripts/mint-reference-strip-backlog.mjs +1 -1
- package/scripts/mint-suite-census.mjs +37 -10
- package/scripts/pack-publishable.mjs +1 -1
- package/scripts/preinstall-node-check.mjs +1 -1
- package/scripts/release-await-cut.mjs +3 -3
- package/scripts/release-cut-decision.mjs +1 -1
- package/scripts/release-dist-tag.mjs +1 -1
- package/scripts/release-install-check.mjs +1 -1
- package/scripts/release-notes-lint.mjs +1 -1
- package/scripts/release-publish-guard.mjs +1 -1
- package/scripts/release-version-pr-checks.mjs +2 -2
- package/scripts/release-version.mjs +61 -5
- package/scripts/render-brand-banner.mjs +1 -1
- package/scripts/render-check.mjs +2 -2
- package/scripts/repo-writes.mjs +1 -1
- package/scripts/report-frame-check.mjs +1 -1
- package/scripts/report-screenshot.mjs +2 -2
- package/scripts/retire-bare-refs.mjs +1 -1
- package/scripts/revisit-render-check.mjs +1 -1
- package/scripts/score.mjs +1 -1
- package/scripts/strip-titles-and-attributions.mjs +389 -0
- package/scripts/strip-tracker-citations.mjs +122 -5
- package/scripts/test-run.mjs +4 -4
- package/scripts/third-party-notices.mjs +1 -1
- package/scripts/verify-publishable.mjs +1 -1
- package/shared/access-audience.mjs +2 -2
- package/shared/anon-overlay.mjs +1 -1
- package/shared/brand.mjs +15 -1
- package/shared/bundle-freshness.mjs +1 -1
- package/shared/bundle-rebuild.mjs +1 -1
- package/shared/checkout-move.mjs +2 -2
- package/shared/client-door.mjs +8 -8
- package/shared/connect-clients.mjs +7 -7
- package/shared/connector-signin-probe.mjs +1 -1
- package/shared/env-aliases.mjs +1 -1
- package/shared/env-local.mjs +5 -5
- package/shared/grants-edit.mjs +76 -0
- package/shared/install-auth.mjs +1 -1
- package/shared/listen.mjs +3 -3
- package/shared/mcp-challenge.mjs +1 -1
- package/shared/names-in-force.mjs +2 -1
- package/shared/onboarding-store.mjs +19 -2
- package/shared/reference-guard-classes.mjs +44 -2
- package/shared/register-selection.mjs +1 -1
- package/shared/scope.mjs +223 -56
- package/shared/secret-file.mjs +1 -1
- package/shared/server-units.mjs +1 -1
- package/shared/staff-domain.mjs +45 -78
- package/shared/summary-blocks.mjs +2 -2
- package/shared/systemd-failure.mjs +3 -3
- package/shared/tracked-files.mjs +1 -1
- package/shared/trigger-lane.mjs +1 -1
- package/shared/tty-style.mjs +1 -1
- package/shared/usage-block.mjs +1 -1
- package/shared/vacuous-pass.mjs +1 -1
- package/shared/verb-shim.mjs +1 -1
|
@@ -27,7 +27,7 @@ import { envFileRead } from "../shared/env-local.mjs"; // side effect: apply t
|
|
|
27
27
|
import { envFrom } from "../shared/env-aliases.mjs"; // — a refusal names the name in force
|
|
28
28
|
import { accessAudience, audienceLabel } from "../shared/access-audience.mjs"; // — F54; jose-free on purpose
|
|
29
29
|
import { doorPostureVerdict } from "./door-posture.mjs"; // — say when this door's mode came from another door's variables
|
|
30
|
-
// The local key door
|
|
30
|
+
// The local key door: a second listener on a unix socket, so a scoped access key has
|
|
31
31
|
// a path that no tunnel can forward to and the TCP door never learns about keys.
|
|
32
32
|
import { keyDoorRefusal, openKeyDoor, KEY_SOCKET_MODE } from "./key-socket.mjs";
|
|
33
33
|
import { demoPostureLine } from "../driver/demo-posture.mjs"; // — the two mis-aimed warnings answer from one place
|
|
@@ -221,7 +221,7 @@ if (isMain) {
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
// ── THE KEY PATH, ON THE POSTURE SURFACE
|
|
224
|
+
// ── THE KEY PATH, ON THE POSTURE SURFACE ──────────────────────────────────────────────────────────
|
|
225
225
|
//
|
|
226
226
|
// An operator must be able to see that a local key path exists, and with what permissions, WITHOUT
|
|
227
227
|
// reading a unit file — the acceptance asks for exactly that, and it is the same reasoning as the
|
|
@@ -318,7 +318,7 @@ if (isMain) {
|
|
|
318
318
|
onReady: ({ port: bound }) => log(`listening on http://${HOST}:${bound}/mcp — READ-ONLY staff surface, firmDomains=[${ALLOWED_DOMAINS.join(", ")}], ${door}`),
|
|
319
319
|
});
|
|
320
320
|
|
|
321
|
-
// ── THE SECOND DOOR: A LOCAL KEY PATH ON A UNIX SOCKET
|
|
321
|
+
// ── THE SECOND DOOR: A LOCAL KEY PATH ON A UNIX SOCKET ────────────────────────────────────────────
|
|
322
322
|
//
|
|
323
323
|
// One process, two transports, two handlers. The TCP door above is untouched and still never honours a
|
|
324
324
|
// key; this one takes a scoped access key and cannot be reached from any network. A tunnel forwards to
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
2
|
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
3
|
// key-socket.mjs — the local key door, on a unix socket, beside the TCP door that takes a proxy identity
|
|
4
|
-
|
|
4
|
+
//.
|
|
5
5
|
//
|
|
6
6
|
// ── THE PROBLEM, AND WHY THE OBVIOUS FIX IS NOT ONE ─────────────────────────────────────────────────
|
|
7
7
|
//
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
3
|
// lib/audit-view.mjs — the AUDIT CHAIN a client account may interrogate, projected.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// Ruling, 2026-08-27: "I don't see why we don't open it or just give it to clients. Ignore the call
|
|
6
6
|
// spend." shared/scope.mjs holds the line that ruling drew and which artifacts it opened; this file is the
|
|
7
7
|
// half that decides what the four STRUCTURED reads hand over — get_run, trace, decision_timeline,
|
|
8
8
|
// get_finding, and list_findings' raw block lists.
|
|
@@ -311,7 +311,7 @@ export function accountTimeline(result, { brandName = "The firm" } = {}) {
|
|
|
311
311
|
};
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
// ---- WHAT-IF (
|
|
314
|
+
// ---- WHAT-IF (ruling 2026-08-27) --------------------------------------------------------------
|
|
315
315
|
//
|
|
316
316
|
// The counterfactual is the second half of the same ruling, and it meets the same two seals. The PLAN
|
|
317
317
|
// prints what the change would cost, drawn from the prior run's telemetry — which is precisely the model
|
|
@@ -324,7 +324,7 @@ export function accountTimeline(result, { brandName = "The firm" } = {}) {
|
|
|
324
324
|
// those out and none is added here; this is the same cost/chain line the audit reads draw, applied to the
|
|
325
325
|
// one tool that would otherwise walk straight through it.
|
|
326
326
|
//
|
|
327
|
-
// THE MEMO KIND ADDS THREE, AND LEAVING THEM OUT BROKE IT SILENTLY
|
|
327
|
+
// THE MEMO KIND ADDS THREE, AND LEAVING THEM OUT BROKE IT SILENTLY. This list is
|
|
328
328
|
// default-deny, so a plan kind whose fields nobody added here arrives stripped rather than refused. A
|
|
329
329
|
// memo plan was composed correctly and reached a client missing `kind` (so it could not be told from a
|
|
330
330
|
// stage plan), `assumption` (so it did not say what it was about) and `parentUntouched` (so it did not
|
package/mcp-server/lib/brief.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// fallback. That fallback survives — for archived runs published before report-data.json existed — and it
|
|
14
14
|
// now says so in the brief instead of being indistinguishable from the real thing. The client-summary
|
|
15
15
|
// branch is deleted, and with it the `**Recommendation:** …` line that only it emitted: the deliverable
|
|
16
|
-
// carries prioritized facts and never advice (
|
|
16
|
+
// carries prioritized facts and never advice (ruling 2026-07-28) — the recipient is a lawyer who
|
|
17
17
|
// layers advice on top. The line is gone by construction, not suppressed.
|
|
18
18
|
//
|
|
19
19
|
// THE PRODUCT NAME IS DERIVED, NEVER STORED AND NEVER HARDCODED. Every run used to announce itself as
|
|
@@ -135,8 +135,8 @@ export function buildBrief(run) {
|
|
|
135
135
|
lines.push(`- **${m.name}** — ${band}.${d.url ? ` Report: ${d.url}` : ""}`);
|
|
136
136
|
for (const f of (m.findings ?? [])) {
|
|
137
137
|
const who = [f.name, f.owner].filter(Boolean).join(" — ");
|
|
138
|
-
// A PROMOTED REGISTER FILING SHOWS THE RATING AND THE READ THE SEARCH ACTUALLY MADE
|
|
139
|
-
//
|
|
138
|
+
// A PROMOTED REGISTER FILING SHOWS THE RATING AND THE READ THE SEARCH ACTUALLY MADE. This
|
|
139
|
+
// line used to print `net` alone, and for a register card `net` carried the
|
|
140
140
|
// stated "no rating of its own" — so the one hard legal right on a page was described here as
|
|
141
141
|
// unrated even on runs where the assessment had written a full read of that exact filing and
|
|
142
142
|
// the report was already printing it. The page and this briefing disagreed.
|
|
@@ -16,7 +16,7 @@ export {
|
|
|
16
16
|
|
|
17
17
|
// The KNOCKOUT lane's own run-dir table. `paths` above is the CLEARANCE table and has no entry for any
|
|
18
18
|
// file this lane writes, which is why every audit projection read a delivered knockout as a run with
|
|
19
|
-
// nothing on disk
|
|
19
|
+
// nothing on disk. Re-exported rather than re-derived for the reason this whole file
|
|
20
20
|
// exists: a second copy of a path table drifts, and the drift shows up as an artifact reported missing.
|
|
21
21
|
export { koPaths } from "../../driver/stages-knockout.mjs";
|
|
22
22
|
|
|
@@ -112,7 +112,7 @@ function classify(e, st, rat) {
|
|
|
112
112
|
// was never written, or was blank — were indistinguishable on a lawyer's timeline from a genuinely
|
|
113
113
|
// clean gate.
|
|
114
114
|
//
|
|
115
|
-
//
|
|
115
|
+
// RULING (2026-08-18): when the screening safety-net could not actually inspect anything, the
|
|
116
116
|
// client's progress view shows "Screening: incomplete — flagged for review". Clear wording appears
|
|
117
117
|
// ONLY when the gate genuinely ran and found nothing. `CLIENT_INCOMPLETE` below is that string
|
|
118
118
|
// VERBATIM and is not to be re-worded here — it is owner-approved client-visible text.
|
|
@@ -78,8 +78,8 @@ export function makeHttpHandler({ verify, limiter, opsLimiter = null, sessions,
|
|
|
78
78
|
if (tokenOnly && verify) throw new Error("makeHttpHandler: tokenOnly is for a door with no auth proxy in front — pass verify:null");
|
|
79
79
|
return async (req, res) => {
|
|
80
80
|
try {
|
|
81
|
-
// THE BASE IS A CONSTANT, AND WHAT THE `Host` HEADER IS USED FOR HERE IS: NOTHING
|
|
82
|
-
//
|
|
81
|
+
// THE BASE IS A CONSTANT, AND WHAT THE `Host` HEADER IS USED FOR HERE IS: NOTHING. Only
|
|
82
|
+
// `pathname` and `searchParams` are read below, so the base exists purely so a bare
|
|
83
83
|
// `req.url` parses as a path. Interpolating the caller's `Host` bought a crash and no behaviour:
|
|
84
84
|
// a value that is not a valid authority makes `new URL` throw, the outer catch answers 500 with a
|
|
85
85
|
// stack in the log, and every bit of that happens ABOVE the `authenticate FIRST` block — so an
|
|
@@ -44,7 +44,7 @@ import { fileURLToPath } from "node:url";
|
|
|
44
44
|
const SKILLS = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
|
|
45
45
|
const SKILL_DIR = Object.freeze({ client: "clearotron-client", account: "clearotron-account", ops: "clearotron-ops" });
|
|
46
46
|
|
|
47
|
-
// ── A PACK MAY BE MORE THAN ONE FILE
|
|
47
|
+
// ── A PACK MAY BE MORE THAN ONE FILE ──────────────────────────────────────────────────────────────
|
|
48
48
|
//
|
|
49
49
|
// The ops SKILL.md tells the assistant twice that delivery "comes back to you as outbox events (see
|
|
50
50
|
// COURIER.md)" — and this reader only ever opened SKILL.md, so from the assistant's side that document
|
|
@@ -105,7 +105,7 @@ function pack(audience) {
|
|
|
105
105
|
* engineering tool set neither pack describes". That premise held while ops meant OUR agents, briefed
|
|
106
106
|
* separately by the Claude Code plugin which installs the same packs as files.
|
|
107
107
|
*
|
|
108
|
-
*
|
|
108
|
+
* Ruling (2026-08-27, ruling 7): on a SELF-HOSTED install the customer IS ops. The person who owns
|
|
109
109
|
* the box connects over this same connector and is briefed with nothing, while
|
|
110
110
|
* `skills/clearotron-ops/SKILL.md` sits shipped and undelivered — SKILL_DIR has mapped it the whole
|
|
111
111
|
* time. So the premise is false for that deployment, and the exclusion went with it.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
2
|
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
-
// lib/knockout.mjs — the KNOCKOUT lane, projected into the audit tools' own shapes
|
|
3
|
+
// lib/knockout.mjs — the KNOCKOUT lane, projected into the audit tools' own shapes.
|
|
4
4
|
//
|
|
5
5
|
// THE DEFECT THIS CLOSES, and it is worth stating plainly because the failure mode was a confident wrong
|
|
6
6
|
// answer rather than an error. Every read-only tool whose job is to show HOW a search reached its answer
|
package/mcp-server/lib/ops.mjs
CHANGED
|
@@ -99,6 +99,9 @@ export function buildJob(args = {}, { scope } = {}) {
|
|
|
99
99
|
profileKey: args.profileKey || undefined,
|
|
100
100
|
// the project/engagement under the customer — its overlay rates the matter
|
|
101
101
|
projectKey: args.projectKey || undefined,
|
|
102
|
+
// which organisation's Generic — `authorize` stamps it on a Generic job from the verified person, and
|
|
103
|
+
// an ops caller is trusted to route it. Dropped here, a Generic run lost the organisation it belongs to.
|
|
104
|
+
tenant: args.tenant || undefined,
|
|
102
105
|
// per-run SCOPE — where the machinery points, as against the selectors above which choose WHICH
|
|
103
106
|
// machinery runs. Passed through verbatim: validateJob owns the vocabulary (shape, caps, dedupe,
|
|
104
107
|
// the bare-string normalization) so this door cannot drift from the CLI or the portal.
|
|
@@ -197,7 +200,7 @@ export const START_RUN_JOB_FIELDS = Object.freeze({
|
|
|
197
200
|
// and "I said nothing" are two different searches and only a positive instruction can tell them apart.
|
|
198
201
|
"geography",
|
|
199
202
|
"product", "recipeKey", "nativeLanguage", "caseLaw", "searchLevel", "deliveryRoute", "parentRunId",
|
|
200
|
-
"customer", "profileKey", "projectKey", "customerUnknown",
|
|
203
|
+
"customer", "profileKey", "projectKey", "customerUnknown", "tenant",
|
|
201
204
|
"upfrontInstructions", "brief", "rawRequest", "deliverableSpec", "commercialFlexibility",
|
|
202
205
|
"priorUse", "campaignShape", "deadline",
|
|
203
206
|
"dupOverride", "clientPrincipal", "enqueuedAt", "enqueuedVia",
|
|
@@ -208,7 +211,7 @@ export const START_RUN_JOB_FIELDS = Object.freeze({
|
|
|
208
211
|
notCarried: Object.freeze({
|
|
209
212
|
registerFixtures: "a run that reads canned register payloads instead of calling a register. This door "
|
|
210
213
|
+ "starts real work for staff and agents; a fixture round is composed as job files by the e2e harness, "
|
|
211
|
-
+ "which writes the field directly rather than asking this tool for it
|
|
214
|
+
+ "which writes the field directly rather than asking this tool for it.",
|
|
212
215
|
promptParts: "the requester's declaration that the prose rides as SIDECAR files. This door assembles from "
|
|
213
216
|
+ "structured tool input and writes no sidecars, so a job it built cannot be in that shape. Carrying it "
|
|
214
217
|
+ "would make the manifest claim an intake it did not use, and the manifest check would then report sidecars "
|
|
@@ -346,7 +349,7 @@ export function stopRun(args = {}, { scope } = {}) {
|
|
|
346
349
|
|
|
347
350
|
// ── — IMMEDIATE MODE: SENTINEL FIRST, THEN THE SIGNAL ─────────────────────
|
|
348
351
|
//
|
|
349
|
-
//
|
|
352
|
+
// Ruling, on his second encounter with the same wait: "a stop is a stop — maybe it should be
|
|
350
353
|
// a 'stop immediately or at next boundary to preserve data' kind of question when you press it."
|
|
351
354
|
// The boundary stop is unchanged and stays the default; this is the other half of the choice.
|
|
352
355
|
//
|
|
@@ -184,12 +184,20 @@ function accountKeyFor(args, scope) {
|
|
|
184
184
|
function accountFor(key, { scope, now }) {
|
|
185
185
|
if (!key) return null;
|
|
186
186
|
let profile = null;
|
|
187
|
-
|
|
187
|
+
// ON A MISS THE ROSTER IS READ AGAIN, the way start_run's check does it (driver/enqueue-schema.mjs).
|
|
188
|
+
// The cache behind `loadProfiles()` is filled once per process, so a company created in the portal
|
|
189
|
+
// after the door started was answered with no account at all. A hit costs nothing extra; a miss reads
|
|
190
|
+
// the store, and the new company's projects are then walked against that same fresh roster.
|
|
191
|
+
let fresh = null;
|
|
192
|
+
try {
|
|
193
|
+
profile = loadProfiles().get(key) ?? null;
|
|
194
|
+
if (!profile) { fresh = loadProfiles({ force: true }); profile = fresh.get(key) ?? null; }
|
|
195
|
+
} catch { return null; } // unreadable roster ⇒ say nothing
|
|
188
196
|
if (!profile || profile.key === "generic") return null;
|
|
189
197
|
|
|
190
198
|
let projects = [];
|
|
191
199
|
try {
|
|
192
|
-
for (const [, ov] of loadProjects()) {
|
|
200
|
+
for (const [, ov] of loadProjects(fresh ? { profiles: fresh, force: true } : {})) {
|
|
193
201
|
if (ov.archived || ov.customerKey !== key) continue;
|
|
194
202
|
projects.push({ key: ov.projectKey, name: ov.projectName });
|
|
195
203
|
}
|
|
@@ -268,7 +276,9 @@ function allowanceFor(profile, { scope, now }) {
|
|
|
268
276
|
// The queue dirs the runner drains — the ledger sits beside each of them (usage-ledger.mjs).
|
|
269
277
|
try { usage = accountUsage({ queueDirs: config.queueDirs, account: profile.key, now }); } catch { usage = null; }
|
|
270
278
|
const shared = {
|
|
271
|
-
|
|
279
|
+
// Capped means the daily allowance binds this session: an account session, except a person with
|
|
280
|
+
// access to everything, whose jobs are never stamped for the cap (shared/scope.mjs authorize).
|
|
281
|
+
capped: scope?.kind === "account" && scope?.everything !== true,
|
|
272
282
|
dailyRuns,
|
|
273
283
|
monthlyRuns: caps?.monthlyRuns ?? null,
|
|
274
284
|
maxQueued: caps?.maxQueued ?? null,
|
|
@@ -311,7 +321,8 @@ export function describeOptions(args = {}, { scope, now = Date.now() } = {}) {
|
|
|
311
321
|
const accountsGranted = !account && Array.isArray(granted) && granted.length
|
|
312
322
|
? granted.map((k) => {
|
|
313
323
|
let name = null;
|
|
314
|
-
|
|
324
|
+
// a miss re-reads the store, as accountFor does — a company granted since the door started has a name
|
|
325
|
+
try { name = (loadProfiles().get(k) ?? loadProfiles({ force: true }).get(k))?.name ?? null; } catch { /* roster unreadable ⇒ the key alone */ }
|
|
315
326
|
return { profileKey: k, name };
|
|
316
327
|
})
|
|
317
328
|
: null;
|
package/mcp-server/lib/plan.mjs
CHANGED
|
@@ -122,10 +122,11 @@ function allowanceFor(profile, { scope, now = Date.now() } = {}) {
|
|
|
122
122
|
dailyRunsEffective: dailyRuns,
|
|
123
123
|
monthlyRuns: caps?.monthlyRuns ?? null,
|
|
124
124
|
maxQueued: caps?.maxQueued ?? null,
|
|
125
|
-
// Only a
|
|
126
|
-
// bites jobs stamped clientPrincipal:true, and
|
|
127
|
-
//
|
|
128
|
-
|
|
125
|
+
// Only a session whose jobs are stamped for the cap is capped — the same positive-only rule the runner
|
|
126
|
+
// applies (checkRunCaps bites jobs stamped clientPrincipal:true, and the account door stamps every job
|
|
127
|
+
// except a person's with access to everything). Anyone else previewing sees the counts and is not
|
|
128
|
+
// blocked by them.
|
|
129
|
+
capped: scope?.kind === "account" && scope?.everything !== true,
|
|
129
130
|
};
|
|
130
131
|
if (!usage?.complete) {
|
|
131
132
|
return { ...shared, complete: false, today: null, thisMonth: null, queued: null, exhausted: false };
|
|
@@ -136,7 +137,7 @@ function allowanceFor(profile, { scope, now = Date.now() } = {}) {
|
|
|
136
137
|
...shared,
|
|
137
138
|
complete: true,
|
|
138
139
|
today: usage.today, thisMonth: usage.thisMonth, queued: usage.queued,
|
|
139
|
-
exhausted: scope?.kind === "account" && usage.today >= dailyRuns,
|
|
140
|
+
exhausted: scope?.kind === "account" && scope?.everything !== true && usage.today >= dailyRuns,
|
|
140
141
|
};
|
|
141
142
|
}
|
|
142
143
|
|
|
@@ -289,7 +290,7 @@ export function planRun(args = {}, { scope, now = Date.now() } = {}) {
|
|
|
289
290
|
// ── — AND WHAT THE REGISTER CANNOT REACH, on the door that commits ──────
|
|
290
291
|
//
|
|
291
292
|
// The same argument the coverage arm above makes, one rung further along. A worldwide search is
|
|
292
|
-
// ORDERABLE on a partial register now (
|
|
293
|
+
// ORDERABLE on a partial register now (ruling 2026-08-31), so it stops being a blocker and
|
|
293
294
|
// becomes something a requester has to be TOLD before they confirm. The portal says it twice — at
|
|
294
295
|
// the point of choosing and again in the review step — and `describe_options` says it on the menu.
|
|
295
296
|
// Without it here, an assistant can walk a client through the one door that spends and never
|
package/mcp-server/lib/runs.mjs
CHANGED
|
@@ -190,3 +190,13 @@ export function runAccountKey(run) {
|
|
|
190
190
|
return p.profileKey ?? p.key ?? null;
|
|
191
191
|
} catch { return null; }
|
|
192
192
|
}
|
|
193
|
+
|
|
194
|
+
// A Generic run's organisation, from the same frozen sidecar. Null for a company's run, for one filed
|
|
195
|
+
// before organisations existed, and for an unreadable sidecar — each of which the account gate treats as
|
|
196
|
+
// visible only to a full-grant session.
|
|
197
|
+
export function runOrganisation(run) {
|
|
198
|
+
try {
|
|
199
|
+
const p = JSON.parse(readFileSync(driverDir(run.runDir, "profile.json"), "utf8"));
|
|
200
|
+
return typeof p.organisation === "string" && p.organisation ? p.organisation : null;
|
|
201
|
+
} catch { return null; }
|
|
202
|
+
}
|
|
@@ -80,7 +80,7 @@ function completeness(stage) {
|
|
|
80
80
|
/** whatIfPlan — pure dry-run. run = resolved Run ({runId, slug, codename, agent, runDir, P, status, location}). */
|
|
81
81
|
export function whatIfPlan({ run, stage, axis = null, instructions = null, model = null, kind = "stage" }) {
|
|
82
82
|
if (!run) throw new Error("whatIfPlan: run is required");
|
|
83
|
-
// ── PLANNING A MEMO
|
|
83
|
+
// ── PLANNING A MEMO ────────────────────────────────────────────────────────────────────────────
|
|
84
84
|
//
|
|
85
85
|
// Without this branch the memo capability is COMPOSED AND UNREACHABLE. whatif-memo.mjs composes one,
|
|
86
86
|
// whatIfRefusal already admits `kind: "memo"` on a finished run, and decodeOp already validates a memo
|
|
@@ -179,7 +179,7 @@ export function decodeOp(confirmationToken, what = "whatIfRun") {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
/**
|
|
182
|
-
* whatIfEnqueue — the CLIENT path (
|
|
182
|
+
* whatIfEnqueue — the CLIENT path (ruling 2026-08-27). Queues the op for the worker instead of
|
|
183
183
|
* running it, because the remote surfaces never spawn the engine and this module's own lazy import of
|
|
184
184
|
* driver/pipeline.mjs is what keeps that true. Nothing below reaches runExperiment.
|
|
185
185
|
*
|
|
@@ -220,7 +220,7 @@ export async function whatIfRun({ confirmationToken } = {}, deps = {}) {
|
|
|
220
220
|
const run = resolveRun(runId);
|
|
221
221
|
if (!run) throw new Error(`whatIfRun: run "${runId}" not found.`);
|
|
222
222
|
|
|
223
|
-
// ── A MEMO IS NOT A STAGE RE-RUN, AND THIS IS THE DOOR IT WAS MISSING
|
|
223
|
+
// ── A MEMO IS NOT A STAGE RE-RUN, AND THIS IS THE DOOR IT WAS MISSING ────────────────────────────
|
|
224
224
|
//
|
|
225
225
|
// Every piece of the memo capability existed and nothing reached it. whatIfPlan mints the token,
|
|
226
226
|
// decodeOp validates it on its own terms, whatIfEnqueue queues it and answers the client
|
|
@@ -230,7 +230,7 @@ export async function whatIfRun({ confirmationToken } = {}, deps = {}) {
|
|
|
230
230
|
// 1. askArchivedRun (driver/whatif-memo-run.mjs, 323 lines with its own arms) was imported by
|
|
231
231
|
// nothing but its own test. Composed and completely unreachable.
|
|
232
232
|
// 2. The refusal below was called WITHOUT `kind`, so it defaulted to "stage" and refused every
|
|
233
|
-
// memo with "what-if runs on live runs only" — the exact sentence
|
|
233
|
+
// memo with "what-if runs on live runs only" — the exact sentence the fix was written
|
|
234
234
|
// to delete. whatIfEnqueue passes the kind (see its own note); this door did not.
|
|
235
235
|
//
|
|
236
236
|
// So a memo was accepted at the front door, PROMISED to the client, and killed in the worker where
|
|
@@ -273,7 +273,7 @@ export async function whatIfRun({ confirmationToken } = {}, deps = {}) {
|
|
|
273
273
|
if (deriveSlug(job) !== run.slug)
|
|
274
274
|
throw new Error(`whatIfRun: cannot reconstruct the job for ${runId} (derived slug "${deriveSlug(job)}" != "${run.slug}"). status.json lacks the original ref/markName.`);
|
|
275
275
|
|
|
276
|
-
// ── THE RATING AUTHORITY TRAVELS WITH THE JOB
|
|
276
|
+
// ── THE RATING AUTHORITY TRAVELS WITH THE JOB ──────────────────────────────────────────────────
|
|
277
277
|
//
|
|
278
278
|
// The reconstruction above carries six fields and resolveProfile keys on none of them. It reads
|
|
279
279
|
// `job.profileKey` first, then falls back to `job.forwarderDomain`; the job has `forwarder` but not
|
package/mcp-server/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-artifacts-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-beta.1",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
|
|
@@ -7,7 +7,7 @@ the per-host connection recipes, and worked example prompts. Three audiences, th
|
|
|
7
7
|
| Pack | Audience | Surface | Token |
|
|
8
8
|
|---|---|---|---|
|
|
9
9
|
| `client/` | report recipients (client legal teams) | plain-language read layer: `brief`, `list_findings`, `read_artifact` (the report — the only artifact a report link may read), one run | the run-scoped link embedded in their report ("Ask your AI") |
|
|
10
|
-
| `account/` | a customer's own assistant, across all of that customer's searches | the client layer, the evidence reads (`list_evidence`, `list_searches`, `get_search_coverage`), the audit chain (`read_artifact` over the chain artifacts, `list_findings` raw, `get_finding`, `get_run`, `trace`, `decision_timeline` —
|
|
10
|
+
| `account/` | a customer's own assistant, across all of that customer's searches | the client layer, the evidence reads (`list_evidence`, `list_searches`, `get_search_coverage`), the audit chain (`read_artifact` over the chain artifacts, `list_findings` raw, `get_finding`, `get_run`, `trace`, `decision_timeline` — ruling 2026-08-27), what-if as a QUEUED sandbox job (`what_if_plan`, `what_if_run`, `what_if_result`) and their own run lifecycle (`describe_options`, `plan_run`, `start_run`, `stop_run`) | their sign-in on the client surface, or an account API key (`mint-token.mjs --scope account`) — both refused unless that surface is started with `CLIENT_MCP_ACCOUNT_ACCESS=1` |
|
|
11
11
|
| `ops/` | integrator/operator agents that run searches and courier deliveries | `start_run` / `feed_context` / `stop_run`, the outbox courier verbs, triage reads | a verb-scoped ops token (`mint-token.mjs`, docs/architecture/06-operations-runbook.md) |
|
|
12
12
|
|
|
13
13
|
## Where the prompt text lives
|
|
@@ -51,7 +51,7 @@ RestartSec=10
|
|
|
51
51
|
# Hardening: this process only reads run-dirs + serves HTTP on loopback (mirrors client-mcp.service).
|
|
52
52
|
NoNewPrivileges=true
|
|
53
53
|
ProtectSystem=strict
|
|
54
|
-
#
|
|
54
|
+
# the telemetry default moved to %h/trademark/telemetry. BOTH are listed because resolution is
|
|
55
55
|
# by existence (providers/_shared/ledger-path.mjs, mcp-server/lib/audit.mjs): a box that already has
|
|
56
56
|
# a ledger or an access log under the old path keeps appending to it and needs it writable, while a
|
|
57
57
|
# fresh box writes to the new one. `-` on both so a missing directory does not fail the unit to
|
|
@@ -30,7 +30,7 @@ Environment=CLIENT_MCP_HTTP_HOST=127.0.0.1
|
|
|
30
30
|
# to avoid a port collision. The CF Tunnel route for clients-mcp.example.com must point at this port.
|
|
31
31
|
#
|
|
32
32
|
# ── AND ON A BOX THAT ALSO RUNS A TEST OR DEV INSTANCE, THE SECOND INSTANCE MUST NOT TAKE THIS. ────
|
|
33
|
-
# Measured 2026-08-18
|
|
33
|
+
# Measured 2026-08-18: the deployment box had 18811 and 18812 held by production client faces
|
|
34
34
|
# and no client face for the test instance at all. `http-server-client.mjs` DEFAULTS to 18811, so a
|
|
35
35
|
# second instance started without an explicit port does not merely collide — on a day production is
|
|
36
36
|
# down it SUCCEEDS, binds the port production is about to want, and a CLIENT surface ends up pointed at
|
|
@@ -59,7 +59,7 @@ RestartSec=10
|
|
|
59
59
|
# Hardening: this process only reads run-dirs + serves HTTP on loopback (mirrors trademark-artifacts-http).
|
|
60
60
|
NoNewPrivileges=true
|
|
61
61
|
ProtectSystem=strict
|
|
62
|
-
#
|
|
62
|
+
# the telemetry default moved to %h/trademark/telemetry. BOTH are listed because resolution is
|
|
63
63
|
# by existence (providers/_shared/ledger-path.mjs, mcp-server/lib/audit.mjs): a box that already has
|
|
64
64
|
# a ledger or an access log under the old path keeps appending to it and needs it writable, while a
|
|
65
65
|
# fresh box writes to the new one. `-` on both so a missing directory does not fail the unit to
|
|
@@ -34,7 +34,7 @@ RestartSec=10
|
|
|
34
34
|
# Hardening: this process only reads run-dirs + serves HTTP on loopback.
|
|
35
35
|
NoNewPrivileges=true
|
|
36
36
|
ProtectSystem=strict
|
|
37
|
-
#
|
|
37
|
+
# the telemetry default moved to %h/trademark/telemetry. BOTH are listed because resolution is
|
|
38
38
|
# by existence (providers/_shared/ledger-path.mjs, mcp-server/lib/audit.mjs): a box that already has
|
|
39
39
|
# a ledger or an access log under the old path keeps appending to it and needs it writable, while a
|
|
40
40
|
# fresh box writes to the new one. `-` on both so a missing directory does not fail the unit to
|
package/mcp-server/server.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import { join, basename } from "node:path";
|
|
|
30
30
|
import { driverDir } from "../shared/driver-dir.mjs"; //
|
|
31
31
|
import { fileURLToPath } from "node:url";
|
|
32
32
|
|
|
33
|
-
import { enumerateRuns, resolveRun, runAccountKey } from "./lib/runs.mjs";
|
|
33
|
+
import { enumerateRuns, resolveRun, runAccountKey, runOrganisation } from "./lib/runs.mjs";
|
|
34
34
|
import { ORDERABLE_PRODUCTS } from "../driver/search-policy.mjs";
|
|
35
35
|
import { PRODUCTS } from "../driver/products.mjs";
|
|
36
36
|
|
|
@@ -48,7 +48,7 @@ import { readEvents, projectTimeline } from "./lib/events.mjs";
|
|
|
48
48
|
import { tokenize, scoreLine } from "./lib/lexsearch.mjs";
|
|
49
49
|
import { artifactToStage, listArtifactVersions, assertDiffRefsSafe } from "./lib/artifacts.mjs";
|
|
50
50
|
import { authorize, visibleTools, USER_ARTIFACTS, ACCOUNT_ARTIFACTS, accountMayReadArtifact, assertAccountAccess, accountVisible, TOOL_SCOPES, readOnlyFor } from "./lib/scope.mjs";
|
|
51
|
-
// The AUDIT-CHAIN projections (
|
|
51
|
+
// The AUDIT-CHAIN projections (ruling 2026-08-27). Imported eagerly: it pulls only scrub.mjs, which
|
|
52
52
|
// this file already loads, so there is nothing here for a lazy import to save.
|
|
53
53
|
import { accountRun, accountTrace, accountTimeline, accountFinding, accountFindingList,
|
|
54
54
|
accountWhatIfPlan, accountWhatIfQueued, accountWhatIfResult, CLIENT_FAILURE_NOTE as clientFailureNote } from "./lib/audit-view.mjs";
|
|
@@ -56,7 +56,7 @@ import { scrubMarkdown, scrubBody, scrubFrontMatter, scrubCards } from "./lib/sc
|
|
|
56
56
|
import { evidenceRecords, searchLog, coverageStatement } from "./lib/evidence.mjs";
|
|
57
57
|
// The knockout lane's projections. Every audit tool below branches on `isKnockoutRun` because the
|
|
58
58
|
// clearance projections read artifacts this product does not write, and returned empty rather than saying
|
|
59
|
-
// so
|
|
59
|
+
// so.
|
|
60
60
|
import {
|
|
61
61
|
isKnockoutRun, knockoutDoc, knockoutArtifacts, knockoutArtifactPath, knockoutFindings,
|
|
62
62
|
knockoutEvidence, knockoutSearches, knockoutCoverage, traceKnockout, notProducedOnThisProduct,
|
|
@@ -89,7 +89,7 @@ function mustRun(runId) {
|
|
|
89
89
|
function artifactPath(run, name) {
|
|
90
90
|
const { P, runDir } = run;
|
|
91
91
|
if (name === "status.json") return join(runDir, "status.json");
|
|
92
|
-
// THE KNOCKOUT TABLE IS TERMINAL ON A KNOCKOUT, and both halves of that matter
|
|
92
|
+
// THE KNOCKOUT TABLE IS TERMINAL ON A KNOCKOUT, and both halves of that matter.
|
|
93
93
|
//
|
|
94
94
|
// Resolving FIRST is what fixes `report`: a knockout's report.md is written to the POOL and never into
|
|
95
95
|
// the run dir, so the clearance table returned the run dir's own `report` slot — a path that does not
|
|
@@ -214,9 +214,25 @@ const tools = {
|
|
|
214
214
|
// Each customer carries its PROJECTS (engagements) so intake can resolve a projectKey too. A bad
|
|
215
215
|
// project file must never blank the whole roster, so the project read is best-effort (its own loud failure
|
|
216
216
|
// surfaces at run time via loadProjects in the driver).
|
|
217
|
+
//
|
|
218
|
+
// READ FRESH ON EVERY CALL. `loadProfiles()` and `loadProjects()` answer from module caches with no
|
|
219
|
+
// expiry, filled by this process's first read — the boot line's — so a company or project created in
|
|
220
|
+
// the portal after the door started was missing here until the door restarted, while start_run, which
|
|
221
|
+
// re-reads on a miss (driver/enqueue-schema.mjs), already accepted it. This is the list an assistant
|
|
222
|
+
// resolves a customer against: a company it cannot see is a company it cannot pick. One read of the
|
|
223
|
+
// roster, handed to the project walk, so the two cannot come from different moments.
|
|
224
|
+
//
|
|
225
|
+
// A COMPANY FILE THAT CANNOT BE READ must not take every other company off this list. Re-reading made
|
|
226
|
+
// that possible: a bad file added after the door started used to go unread, because the door answered
|
|
227
|
+
// from its boot read, and now it would fail every call until fixed. So a failed re-read keeps the
|
|
228
|
+
// roster this process last read, and the reply says so. With no earlier read there is nothing to keep,
|
|
229
|
+
// and the call fails as it always did.
|
|
230
|
+
let roster, storeError = null;
|
|
231
|
+
try { roster = loadProfiles({ force: true }); }
|
|
232
|
+
catch (e) { storeError = String(e?.message ?? e); roster = loadProfiles(); }
|
|
217
233
|
let byCustomer = new Map();
|
|
218
234
|
try {
|
|
219
|
-
for (const [fq, ov] of loadProjects()) {
|
|
235
|
+
for (const [fq, ov] of loadProjects({ profiles: roster, force: true })) {
|
|
220
236
|
// An ARCHIVED project is not offered for new work: this list is what the intake AI resolves a
|
|
221
237
|
// projectKey against, so a name it cannot see is a name it cannot pick. Already-queued and
|
|
222
238
|
// finished runs are untouched — a run freezes its effective profile at admission.
|
|
@@ -226,7 +242,7 @@ const tools = {
|
|
|
226
242
|
byCustomer.set(ov.customerKey, list);
|
|
227
243
|
}
|
|
228
244
|
} catch { byCustomer = new Map(); }
|
|
229
|
-
const clients = [...
|
|
245
|
+
const clients = [...roster.values()]
|
|
230
246
|
.filter((p) => p.key !== "generic")
|
|
231
247
|
.map((p) => ({ key: p.key, name: p.name, industry: p.industry ?? null,
|
|
232
248
|
projects: (byCustomer.get(p.key) ?? []).sort((a, b) => a.key.localeCompare(b.key)) }))
|
|
@@ -235,6 +251,9 @@ const tools = {
|
|
|
235
251
|
_note: "Customer roster for intake resolution. Resolve by JUDGMENT — an explicit name, a misspelling of one of the keys below, or an implicit reference (\"our functional-beverage client\") all map to a key. Set the job's profileKey to the chosen customer key; OMIT it for a new/unknown customer (⇒ the neutral generic profile). If the request names a specific PROJECT/engagement under that customer (listed in `projects[]`), also set projectKey to that project's key; OMIT projectKey when no project is meant (⇒ the customer profile). CLARIFY if you cannot tell either. Never pick a profile from the sender's email domain.",
|
|
236
252
|
clients,
|
|
237
253
|
genericFallback: "generic",
|
|
254
|
+
// Staff-only tool (TOOL_SCOPES), so the reason may name the file: no client door sees this reply.
|
|
255
|
+
...(storeError ? { storeUnreadable: `The company store could not be re-read (${storeError}). This is the list as `
|
|
256
|
+
+ "last read, so a company added or changed since may be missing until that file is fixed." } : {}),
|
|
238
257
|
};
|
|
239
258
|
},
|
|
240
259
|
async describe_options(args, extra) {
|
|
@@ -482,7 +501,7 @@ const tools = {
|
|
|
482
501
|
const run = mustRun(runId);
|
|
483
502
|
const { whatIfPlan } = await import("./lib/whatif.mjs");
|
|
484
503
|
// `kind` DEFAULTS TO "stage", so every existing caller is unchanged. A memo is the other kind
|
|
485
|
-
|
|
504
|
+
//: a bounded re-read of a DELIVERED run's archived evidence under a stated
|
|
486
505
|
// assumption, which re-runs no stage and therefore cannot carry one.
|
|
487
506
|
const asked = kind === "memo" ? "memo" : "stage";
|
|
488
507
|
// A STAGE PLAN WITH NO STAGE IS A REFUSAL, NOT A THROW. `stage` cannot be schema-required any more:
|
|
@@ -575,7 +594,7 @@ const TOOL_DEFS = [
|
|
|
575
594
|
{ name: "plan_run", description: "FREE PREVIEW — resolve what a search WOULD do and return it, spending nothing and queueing nothing. Takes the SAME arguments as start_run. Use this BEFORE start_run whenever the requester has not already confirmed the specifics: it reports the PRODUCT that resolves and WHERE it came from (their request, the account default, a saved search, or the territories themselves), the territories and marketplaces that would ACTUALLY be searched, the turnaround, and any blockers as questions to put back. Show the result to the requester; then call start_run with the SAME arguments to actually run it. Nothing here reserves or holds anything.", inputSchema: { type: "object", properties: { markName: { type: "string", description: "The mark (single-mark form). REQUIRED unless marks[] is given." }, marks: { type: "array", items: { type: "object", properties: { name: { type: "string" }, classes: { type: "array", items: { type: "number" } }, ref: { type: "string" } }, required: ["name"] } }, classes: { type: "array", items: { type: "number" } }, goods: { type: "string" }, jurisdictions: { type: "array", items: { type: "string" } }, platforms: { type: "array", items: { type: "string" } }, customer: { type: "string" }, profileKey: { type: "string", description: "The customer ACCOUNT key (see list_profiles). REQUIRED for an accounts-scoped session (your grant names the keys — see describe_options)." }, projectKey: { type: "string" }, forwarder: { type: "string", description: "Requester/reply-routing key — same as start_run, so the preview describes the job that would actually be built. Account sessions may omit it — it is stamped from your verified identity." }, forwarderEmail: { type: "string" }, ref: { type: "string" }, product: { type: "string", enum: ORDERABLE_PRODUCTS, description: PRODUCT_DESC }, nativeLanguage: { type: "boolean", description: "OPTIONAL, and only on a multi-country-focus-search: the native-language investigation. Send TRUE or omit the field — FALSE IS REFUSED, not honoured: the toggle only ever added, so there is nothing for false to switch off, and accepting it would let you believe you had removed a reading that runs anyway. It runs AUTOMATICALLY on a full-country-search and is not part of the other two, so asking for it there is refused rather than accepted and ignored. It routes on territory — the scope must name one it covers." }, worldwide: { type: "boolean", description: "Search EVERYWHERE, and refuse to be narrowed by the account's own default territories. This is not the same as omitting jurisdictions, which means \"whatever the account says\" — the two used to be indistinguishable on this wire, which is how a request that bought everywhere ran an account's seven countries. Never send \"Worldwide\" as a jurisdictions entry." }, recipeKey: { type: "string" }, upfrontInstructions: { type: "string" }, deadline: { type: "string" }, deliveryRoute: { type: "string", enum: ["email", "portal"], description: "OPTIONAL: how the delivered packet leaves — email (default). \"portal\" is reserved for the portal delivery lane and CLARIFIES at admission until it ships (never a silent no-op) — so the preview BLOCKS it here rather than describing a job start_run could not run." }, commercialFlexibility: { type: "string" }, priorUse: { type: "string" }, campaignShape: { type: "string", description: "Campaign-shape FACTS from the request/client — how the mark will be deployed: standalone brand vs flavour/sub-brand under a NAMED house mark; seasonal/limited vs permanent; launch scale. Verbatim facts only — the engine records them on the matter frame instead of inferring a launch shape." }, customerUnknown: { type: "boolean" } }, required: ["forwarder"] } },
|
|
576
595
|
// ---- OPS write-face (requires an ops token; never exposed to a user/internal session) ----
|
|
577
596
|
{ name: "start_run", description: "OPS-ONLY. Enqueue a NEW clearance/search run the driver will pick up. Provide markName (or marks[] for a batch) + forwarder (and ideally classes/goods/customer/profileKey/ref). Returns the queue id + slug; the runId/codename is assigned when the runner claims it — poll list_runs (by markName) or run_changes for progress. Spends real money once it runs.", inputSchema: { type: "object", properties: { agent: { type: "string", description: "Which agent workspace's queue. Omit for the deployment default (CLEAROTRON_QUEUE_DIR or the default agent's queue — docs/INTAKE.md)." }, markName: { type: "string", description: "The mark (single-mark form). Required unless marks[] is provided." }, marks: { type: "array", items: { type: "object", properties: { name: { type: "string" }, classes: { type: "array", items: { type: "number" } }, ref: { type: "string" } }, required: ["name"] }, description: "Batch form: EVERY name of the batch in ONE job. Only a knockout-search reads more than one name at a time; a clearance reads one, and more is refused rather than truncated." }, classes: { type: "array", items: { type: "number" }, description: "Nice classes — whole numbers 1–45 (1–34 goods, 35–45 services). classes OR goods; either suffices." }, goods: { type: "string" }, jurisdictions: { type: "array", items: { type: "string" }, description: "WHERE the search points — and, for a clearance, WHICH search it is: one country is a full-country-search, a region or two-or-more countries a multi-country-focus-search. AUTHORITATIVE (the matter frame is told not to widen past them). Omit ⇒ the project/customer default territories; send worldwide:true to search everywhere instead. Names or codes both read; max 20." }, platforms: { type: "array", items: { type: "string" }, description: "OPTIONAL per-run SCOPE: extra marketplaces to sweep, as bare store domains e.g. [\"gnc.com\"]. ADDED to the account's own marketplaces — additive only, never a replacement (a client's platform list is a mandate). Max 10; \"web\" is implicit and must not be listed." }, customer: { type: "string" }, profileKey: { type: "string", description: "The customer ACCOUNT key (see list_profiles) whose profile rates this run; omit for the neutral generic profile. REQUIRED for an accounts-scoped session (your grant names the keys — see describe_options)." }, forwarder: { type: "string", description: "Requester/reply-routing key — rides the delivery packet so the integrator knows who gets the report (docs/DELIVERY.md). Account sessions may omit it — it is stamped from your verified identity." }, forwarderEmail: { type: "string" }, ref: { type: "string" }, provider: { type: "string" }, brief: { type: "string" }, upfrontInstructions: { type: "string" }, projectKey: { type: "string", description: "OPTIONAL (spec 62): the PROJECT/engagement key UNDER profileKey (see that customer's projects[] in list_profiles) whose overlay rates this run; omit to run on the customer profile. An unknown key clarifies at intake." }, product: { type: "string", enum: ORDERABLE_PRODUCTS, description: PRODUCT_DESC }, nativeLanguage: { type: "boolean", description: "OPTIONAL, and only on a multi-country-focus-search: the native-language investigation. Send TRUE or omit the field — FALSE IS REFUSED, not honoured: the toggle only ever added, so there is nothing for false to switch off, and accepting it would let you believe you had removed a reading that runs anyway. It runs AUTOMATICALLY on a full-country-search and is not part of the other two, so asking for it there is refused rather than accepted and ignored. It routes on territory — the scope must name one it covers." }, worldwide: { type: "boolean", description: "Search EVERYWHERE, and refuse to be narrowed by the account's own default territories. This is not the same as omitting jurisdictions, which means \"whatever the account says\" — the two used to be indistinguishable on this wire, which is how a request that bought everywhere ran an account's seven countries. Never send \"Worldwide\" as a jurisdictions entry." }, recipeKey: { type: "string", description: "OPTIONAL alternative selector: a saved search (recipe) slug for this customer. Mutually exclusive with product — a saved search already carries one." }, deliveryRoute: { type: "string", enum: ["email", "portal"], description: "OPTIONAL: how the delivered packet leaves — email (default). \"portal\" is reserved for the portal delivery lane and CLARIFIES at admission until it ships (never a silent no-op)." }, parentRunId: { type: "string", description: "OPTIONAL escalation lineage: the runId this run escalates from (e.g. a knockout HIGH mark → this clearotron)." }, deliverableSpec: { type: "string" }, commercialFlexibility: { type: "string" }, priorUse: { type: "string" }, campaignShape: { type: "string", description: "Campaign-shape FACTS from the request/client — how the mark will be deployed: standalone brand vs flavour/sub-brand under a NAMED house mark; seasonal/limited vs permanent; launch scale. Verbatim facts only — the engine records them on the matter frame instead of inferring a launch shape." }, deadline: { type: "string", description: "ISO-8601 — drives the deadline envelope." }, customerUnknown: { type: "boolean" }, dupOverride: { type: "boolean", description: "Requester-confirmed force-run past matter dedup." }, clientPrincipal: { type: "boolean", description: "Set by the client portal only: this run was started by a CLIENT and consumes that account's runCaps.dailyRuns allowance. Omit for staff, agent and email-door runs — absence means uncapped, and setting it can only restrict the run that carries it." }, rawRequest: { type: "string" }, forwarderDomain: { type: "string" }, msgId: { type: "string" }, conversationId: { type: "string" }, id: { type: "string" } }, required: ["forwarder"] } },
|
|
578
|
-
{ name: "stop_run", description: "OPS-ONLY. Cancel a run. Pass id to remove a not-yet-claimed queued job (a real cancel, no spend). Pass runId to file a cancel request on a started run. By default the cancel is COOPERATIVE — the sentinel is written and a turn already in flight is allowed to finish, which has no deadline: a reasoning turn can run for tens of minutes. Add immediate:true to also end that turn, so the run goes terminal in seconds.", inputSchema: { type: "object", properties: { agent: { type: "string" }, id: { type: "string", description: "A queued job id (from start_run) — dequeues before the runner claims it." }, runId: { type: "string", description: "A started run's runId — files a cancel sentinel." }, immediate: { type: "boolean", description: "Stop NOW instead of at the next step boundary
|
|
597
|
+
{ name: "stop_run", description: "OPS-ONLY. Cancel a run. Pass id to remove a not-yet-claimed queued job (a real cancel, no spend). Pass runId to file a cancel request on a started run. By default the cancel is COOPERATIVE — the sentinel is written and a turn already in flight is allowed to finish, which has no deadline: a reasoning turn can run for tens of minutes. Add immediate:true to also end that turn, so the run goes terminal in seconds.", inputSchema: { type: "object", properties: { agent: { type: "string" }, id: { type: "string", description: "A queued job id (from start_run) — dequeues before the runner claims it." }, runId: { type: "string", description: "A started run's runId — files a cancel sentinel." }, immediate: { type: "boolean", description: "Stop NOW instead of at the next step boundary. The cancel sentinel is written FIRST and then the run's own recorded engine turn is ended, in that order — the sentinel is what makes the record clean, so the terminal names the stage, the actor and the request time instead of leaving a run that reads as still running. The step in flight is lost; everything already recorded is kept. Falls back to the boundary stop, and SAYS SO in the answer, when there is no live turn to end — a run between steps, or one whose turn has already exited. Never send this expecting a guaranteed instant stop: the answer states which of the two actually happened."}, onBehalfOf: { type: "string", description: "The identifier of a human this caller has itself verified (#1378). RECORDED BESIDE the token's own principal, never instead of it — a stop by the portal for alice@x is written `portal:alice@x`, so an asserted name is never mistaken for a proved one. Bare identifiers only; anything else is dropped." } } } },
|
|
579
598
|
{ name: "server_info", description: "The AGPL §13 source offer: this server's name, version, licence, source repository and the COMMIT IT IS RUNNING. Callable by every session kind — the offer is owed to whoever interacts with the service. Reads no run and no account data.", inputSchema: { type: "object", properties: {} } },
|
|
580
599
|
{ name: "list_outbox_events", description: "Integrator discovery read (docs/DELIVERY.md): every pending outbox event — delivered markers plus the self-contained run-failed / intake-rejected / duplicate-skipped / late-bind-ack packets. Route each, then mark_sent (delivered) or ack_event (the rest).", inputSchema: { type: "object", properties: {} } },
|
|
581
600
|
{ name: "get_delivery_packet", description: "The run's send payload: _driver/delivery.json (subject, recipient routing, verbatim emailBodyHtml, whatsapp line) and/or failure.json, plus sendPending/.sent state. NOT exposed to client tokens.", inputSchema: { type: "object", properties: { runId: { type: "string" } }, required: ["runId"] } },
|
|
@@ -712,7 +731,7 @@ export function describeForAudience(def, kind) {
|
|
|
712
731
|
}
|
|
713
732
|
|
|
714
733
|
/**
|
|
715
|
-
* Attach MCP tool annotations
|
|
734
|
+
* Attach MCP tool annotations.
|
|
716
735
|
*
|
|
717
736
|
* No tool declared any, so a client could not tell `brief` from `start_run` and asked before every
|
|
718
737
|
* call. `readOnlyHint` is DERIVED from the scope table's `write` flag (`readOnlyFor`) rather than
|
|
@@ -757,7 +776,7 @@ export function attachHandlers(server, { scope = { kind: "ops", runId: null }, l
|
|
|
757
776
|
if (Array.isArray(scope?.accounts) && authedArgs?.runId != null) {
|
|
758
777
|
try {
|
|
759
778
|
const run = resolveRun(String(authedArgs.runId));
|
|
760
|
-
if (run) assertAccountAccess(scope, runAccountKey(run), `run "${authedArgs.runId}"
|
|
779
|
+
if (run) assertAccountAccess(scope, runAccountKey(run), `run "${authedArgs.runId}"`, runOrganisation(run));
|
|
761
780
|
} catch (e) {
|
|
762
781
|
log(`account deny ${name} [${scope.sub ?? scope.kind}]: ${e.message}`);
|
|
763
782
|
return { isError: true, content: [{ type: "text", text: `FORBIDDEN (${name}): ${e.message}` }] };
|
|
@@ -789,13 +808,13 @@ export function attachHandlers(server, { scope = { kind: "ops", runId: null }, l
|
|
|
789
808
|
const runs = scope.kind === "user"
|
|
790
809
|
? (() => { const r = scope.runId && resolveRun(scope.runId); return r ? [r] : []; })()
|
|
791
810
|
: enumerateRuns()
|
|
792
|
-
.filter((r) => !Array.isArray(scope?.accounts) || accountVisible(scope, runAccountKey(r)))
|
|
811
|
+
.filter((r) => !Array.isArray(scope?.accounts) || accountVisible(scope, runAccountKey(r), runOrganisation(r)))
|
|
793
812
|
.slice(0, 8).map((r) => ({ ...r, P: paths(r.runDir) }));
|
|
794
813
|
// A user (report-link) token sees ONLY the report (one report — never a second version by another
|
|
795
814
|
// name), never the internal KEY_ARTIFACTS (narrative/audit/run.jsonl/…) — the same gate authorize()
|
|
796
815
|
// puts on read_artifact.
|
|
797
816
|
//
|
|
798
|
-
// AND THE TWO CLIENT KINDS NO LONGER AGREE (
|
|
817
|
+
// AND THE TWO CLIENT KINDS NO LONGER AGREE (ruling 2026-08-27). They did while both read only
|
|
799
818
|
// the report; the account layer now reads the audit chain and the report link still does not. This
|
|
800
819
|
// is keyed on the KIND rather than on CLIENT_KINDS for exactly that reason — leaving it collapsed
|
|
801
820
|
// would have made the tool surface serve an account the audit chain while this door went on sealing
|
|
@@ -830,7 +849,7 @@ export function attachHandlers(server, { scope = { kind: "ops", runId: null }, l
|
|
|
830
849
|
throw new Error("forbidden: a client may only read the report");
|
|
831
850
|
const run = resolveRun(reqRunId);
|
|
832
851
|
if (!run) throw new Error(`run not found: ${m[1]}`);
|
|
833
|
-
if (Array.isArray(scope?.accounts)) assertAccountAccess(scope, runAccountKey(run), `run "${reqRunId}"
|
|
852
|
+
if (Array.isArray(scope?.accounts)) assertAccountAccess(scope, runAccountKey(run), `run "${reqRunId}"`, runOrganisation(run));
|
|
834
853
|
const path = artifactPath(run, reqArtifact);
|
|
835
854
|
if (!path || !existsSync(path)) throw new Error(`artifact not found: ${m[2]}`);
|
|
836
855
|
// CLIENT VIEW ( R1): the Resources surface is a second door to the same bytes — it applies the
|
|
@@ -875,7 +894,7 @@ export function presentForPrincipal(scope, name, result) {
|
|
|
875
894
|
const declared = TOOL_SCOPES[name]?.present ?? null;
|
|
876
895
|
if (declared === null) throw new UndeclaredPresentation(name);
|
|
877
896
|
|
|
878
|
-
// `project` — THE AUDIT CHAIN (
|
|
897
|
+
// `project` — THE AUDIT CHAIN (ruling 2026-08-27). lib/audit-view.mjs holds the allowlist over
|
|
879
898
|
// each result's structure and the prose transform; nothing about what travels is decided here.
|
|
880
899
|
//
|
|
881
900
|
// The four tools are accountSafe and NOT clientSafe, so a `user` (report-link) token never reaches this
|
|
@@ -949,32 +968,27 @@ export function filterByAccounts(scope, name, result) {
|
|
|
949
968
|
// one status-scan per call builds runId → account for the array filters
|
|
950
969
|
const accountOf = () => {
|
|
951
970
|
const map = new Map();
|
|
952
|
-
for (const r of enumerateRuns()) map.set(r.runId, runAccountKey(r));
|
|
971
|
+
for (const r of enumerateRuns()) map.set(r.runId, { account: runAccountKey(r), organisation: runOrganisation(r) });
|
|
953
972
|
return map;
|
|
954
973
|
};
|
|
974
|
+
const seen = (map, id) => { const o = id != null ? map.get(id) : null; return o != null && accountVisible(scope, o.account, o.organisation); };
|
|
955
975
|
if (name === "list_profiles" && result && Array.isArray(result.clients))
|
|
956
976
|
return { ...result, clients: result.clients.filter((c) => scope.accounts.includes(c.key)) };
|
|
957
977
|
if ((name === "list_runs" || name === "search_runs") && Array.isArray(result)) {
|
|
958
978
|
const map = accountOf();
|
|
959
|
-
return result.filter((r) =>
|
|
960
|
-
const id = r.runId ?? r.id ?? null;
|
|
961
|
-
return id != null && accountVisible(scope, map.get(id) ?? null);
|
|
962
|
-
});
|
|
979
|
+
return result.filter((r) => seen(map, r.runId ?? r.id ?? null));
|
|
963
980
|
}
|
|
964
981
|
// search_runs answers an OBJECT ({query, mode, scope, runsScanned, hits, truncated}) — the array
|
|
965
982
|
// guard above never matched it, so scoped sessions saw EVERY hit — a real cross-account content
|
|
966
983
|
// leak. Filter the hits by their run's account like the array shapes.
|
|
967
984
|
if (name === "search_runs" && result && Array.isArray(result.hits)) {
|
|
968
985
|
const map = accountOf();
|
|
969
|
-
const hits = result.hits.filter((h) =>
|
|
970
|
-
const id = h.runId ?? h.id ?? null;
|
|
971
|
-
return id != null && accountVisible(scope, map.get(id) ?? null);
|
|
972
|
-
});
|
|
986
|
+
const hits = result.hits.filter((h) => seen(map, h.runId ?? h.id ?? null));
|
|
973
987
|
return { ...result, hits, ...(typeof result.count === "number" ? { count: hits.length } : {}) };
|
|
974
988
|
}
|
|
975
989
|
if (name === "list_outbox_events" && result && Array.isArray(result.events)) {
|
|
976
990
|
const map = accountOf();
|
|
977
|
-
const events = result.events.filter((ev) =>
|
|
991
|
+
const events = result.events.filter((ev) => seen(map, ev.runId ?? null));
|
|
978
992
|
return { ...result, events, count: events.length };
|
|
979
993
|
}
|
|
980
994
|
return result;
|