modelrig 0.8.1 → 0.10.0
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/CHANGELOG.md +324 -0
- package/README.md +55 -29
- package/data/PROVENANCE.md +14 -0
- package/data/pricing-map.json +26 -0
- package/dist/adapters/attempt-meta-map.d.ts +8 -1
- package/dist/adapters/attempt-meta-map.d.ts.map +1 -1
- package/dist/adapters/attempt-meta-map.js +4 -0
- package/dist/adapters/build-adapters.js +2 -2
- package/dist/adapters/gemini.d.ts +28 -1
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +38 -9
- package/dist/adapters/grok.d.ts.map +1 -1
- package/dist/adapters/grok.js +53 -4
- package/dist/adapters/openai-compat.d.ts +8 -2
- package/dist/adapters/openai-compat.d.ts.map +1 -1
- package/dist/adapters/openai-compat.js +8 -1
- package/dist/adapters/openai-stream.d.ts +10 -2
- package/dist/adapters/openai-stream.d.ts.map +1 -1
- package/dist/adapters/openai-stream.js +5 -1
- package/dist/adapters/openai.d.ts +10 -1
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +57 -7
- package/dist/adapters/responses-api.d.ts +61 -0
- package/dist/adapters/responses-api.d.ts.map +1 -0
- package/dist/adapters/responses-api.js +124 -0
- package/dist/artifacts/artifact.d.ts +31 -12
- package/dist/artifacts/artifact.d.ts.map +1 -1
- package/dist/artifacts/artifact.js +218 -188
- package/dist/artifacts/index.d.ts +15 -5
- package/dist/artifacts/index.d.ts.map +1 -1
- package/dist/artifacts/index.js +19 -5
- package/dist/artifacts/run-context.d.ts +3 -24
- package/dist/artifacts/run-context.d.ts.map +1 -1
- package/dist/artifacts/run-context.js +3 -39
- package/dist/bakeoff/engine.d.ts +29 -0
- package/dist/bakeoff/engine.d.ts.map +1 -1
- package/dist/bakeoff/engine.js +47 -0
- package/dist/bakeoff/eval-cases-rig.d.ts +36 -0
- package/dist/bakeoff/eval-cases-rig.d.ts.map +1 -0
- package/dist/bakeoff/eval-cases-rig.js +27 -0
- package/dist/bakeoff/eval-cases.d.ts +19 -0
- package/dist/bakeoff/eval-cases.d.ts.map +1 -1
- package/dist/bakeoff/eval-cases.js +18 -1
- package/dist/bakeoff/golden-judge.d.ts +47 -0
- package/dist/bakeoff/golden-judge.d.ts.map +1 -0
- package/dist/bakeoff/golden-judge.js +49 -0
- package/dist/bakeoff/groundedness.d.ts +58 -0
- package/dist/bakeoff/groundedness.d.ts.map +1 -0
- package/dist/bakeoff/groundedness.js +165 -0
- package/dist/bakeoff/quality.d.ts +43 -0
- package/dist/bakeoff/quality.d.ts.map +1 -1
- package/dist/bakeoff/quality.js +31 -0
- package/dist/bakeoff/replay.d.ts +11 -0
- package/dist/bakeoff/replay.d.ts.map +1 -1
- package/dist/bakeoff/replay.js +12 -0
- package/dist/bakeoff/report.d.ts +7 -0
- package/dist/bakeoff/report.d.ts.map +1 -1
- package/dist/bakeoff/report.js +26 -0
- package/dist/cli-judge.d.ts +12 -0
- package/dist/cli-judge.d.ts.map +1 -0
- package/dist/cli-judge.js +48 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +22 -42
- package/dist/config.d.ts +85 -29
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +109 -47
- package/dist/detect/detector.d.ts +4 -2
- package/dist/detect/detector.d.ts.map +1 -1
- package/dist/detect/safe-harbor.d.ts +57 -0
- package/dist/detect/safe-harbor.d.ts.map +1 -0
- package/dist/detect/safe-harbor.js +278 -0
- package/dist/doctor-cli.d.ts +7 -0
- package/dist/doctor-cli.d.ts.map +1 -0
- package/dist/doctor-cli.js +67 -0
- package/dist/doctor-preflight.d.ts +52 -0
- package/dist/doctor-preflight.d.ts.map +1 -0
- package/dist/doctor-preflight.js +112 -0
- package/dist/feedback/coverage.d.ts +11 -8
- package/dist/feedback/coverage.d.ts.map +1 -1
- package/dist/feedback/coverage.js +29 -16
- package/dist/feedback/grade.d.ts +58 -0
- package/dist/feedback/grade.d.ts.map +1 -0
- package/dist/feedback/grade.js +67 -0
- package/dist/gateway/http-protocol.d.ts +20 -1
- package/dist/gateway/http-protocol.d.ts.map +1 -1
- package/dist/gateway/http-protocol.js +1 -1
- package/dist/gateway/serve-client.d.ts +99 -0
- package/dist/gateway/serve-client.d.ts.map +1 -0
- package/dist/gateway/serve-client.js +360 -0
- package/dist/index.d.ts +68 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +167 -46
- package/dist/init/observe-cli.js +1 -1
- package/dist/init/observe.d.ts +2 -2
- package/dist/init/observe.d.ts.map +1 -1
- package/dist/init/observe.js +11 -7
- package/dist/mask/arm.d.ts +112 -0
- package/dist/mask/arm.d.ts.map +1 -0
- package/dist/mask/arm.js +179 -0
- package/dist/mask/index.d.ts +2 -0
- package/dist/mask/index.d.ts.map +1 -1
- package/dist/mask/index.js +7 -1
- package/dist/mask/mask-policy-cli.d.ts +12 -4
- package/dist/mask/mask-policy-cli.d.ts.map +1 -1
- package/dist/mask/mask-policy-cli.js +34 -9
- package/dist/prompts/manifest.d.ts +128 -0
- package/dist/prompts/manifest.d.ts.map +1 -0
- package/dist/prompts/manifest.js +214 -0
- package/dist/prompts/prompt-fetch.d.ts +84 -0
- package/dist/prompts/prompt-fetch.d.ts.map +1 -0
- package/dist/prompts/prompt-fetch.js +120 -0
- package/dist/prompts/prompt-resolve.d.ts +76 -0
- package/dist/prompts/prompt-resolve.d.ts.map +1 -0
- package/dist/prompts/prompt-resolve.js +93 -0
- package/dist/prompts/slot-resolver.d.ts +91 -0
- package/dist/prompts/slot-resolver.d.ts.map +1 -0
- package/dist/prompts/slot-resolver.js +135 -0
- package/dist/prompts/tools-register.d.ts +60 -0
- package/dist/prompts/tools-register.d.ts.map +1 -0
- package/dist/prompts/tools-register.js +48 -0
- package/dist/prompts/version-hash.d.ts +29 -0
- package/dist/prompts/version-hash.d.ts.map +1 -0
- package/dist/prompts/version-hash.js +54 -0
- package/dist/prompts-export-cli.d.ts +21 -0
- package/dist/prompts-export-cli.d.ts.map +1 -0
- package/dist/prompts-export-cli.js +130 -0
- package/dist/query/grammar.d.ts +2 -2
- package/dist/query/grammar.d.ts.map +1 -1
- package/dist/query/grammar.js +3 -1
- package/dist/query/saved.d.ts +13 -0
- package/dist/query/saved.d.ts.map +1 -1
- package/dist/query/saved.js +67 -1
- package/dist/receipts/types.d.ts +1 -1
- package/dist/receipts/types.d.ts.map +1 -1
- package/dist/receipts/types.js +1 -0
- package/dist/registry/build.d.ts +43 -1
- package/dist/registry/build.d.ts.map +1 -1
- package/dist/registry/build.js +52 -0
- package/dist/registry/cache-policy.d.ts.map +1 -1
- package/dist/registry/cache-policy.js +22 -0
- package/dist/registry/pricing-shape.d.ts +67 -8
- package/dist/registry/pricing-shape.d.ts.map +1 -1
- package/dist/registry/pricing-shape.js +50 -13
- package/dist/registry/pricing.d.ts +6 -5
- package/dist/registry/pricing.d.ts.map +1 -1
- package/dist/registry/pricing.js +9 -5
- package/dist/registry/rows.d.ts.map +1 -1
- package/dist/registry/rows.js +1 -0
- package/dist/registry/select.d.ts +6 -1
- package/dist/registry/select.d.ts.map +1 -1
- package/dist/registry/select.js +11 -0
- package/dist/routes/call-config.d.ts +199 -0
- package/dist/routes/call-config.d.ts.map +1 -0
- package/dist/routes/call-config.js +343 -0
- package/dist/routes/destination-loader.d.ts +16 -0
- package/dist/routes/destination-loader.d.ts.map +1 -0
- package/dist/routes/destination-loader.js +29 -0
- package/dist/routes/draft.d.ts +7 -0
- package/dist/routes/draft.d.ts.map +1 -1
- package/dist/routes/draft.js +12 -0
- package/dist/routes/loader.d.ts.map +1 -1
- package/dist/routes/loader.js +31 -56
- package/dist/routes/objective-loader.d.ts +12 -0
- package/dist/routes/objective-loader.d.ts.map +1 -1
- package/dist/routes/objective-loader.js +82 -0
- package/dist/routes/output-loader.d.ts +42 -0
- package/dist/routes/output-loader.d.ts.map +1 -0
- package/dist/routes/output-loader.js +112 -0
- package/dist/routes/prompt-loader.d.ts +49 -0
- package/dist/routes/prompt-loader.d.ts.map +1 -0
- package/dist/routes/prompt-loader.js +113 -0
- package/dist/routes/resolved-bundle.d.ts +69 -0
- package/dist/routes/resolved-bundle.d.ts.map +1 -1
- package/dist/routes/resolved-bundle.js +76 -0
- package/dist/routes/screen-loader.d.ts +29 -1
- package/dist/routes/screen-loader.d.ts.map +1 -1
- package/dist/routes/screen-loader.js +59 -1
- package/dist/run/accept.d.ts +5 -0
- package/dist/run/accept.d.ts.map +1 -1
- package/dist/run/accept.js +2 -0
- package/dist/run/cost.d.ts +31 -2
- package/dist/run/cost.d.ts.map +1 -1
- package/dist/run/cost.js +57 -2
- package/dist/run/destination.d.ts +39 -0
- package/dist/run/destination.d.ts.map +1 -0
- package/dist/run/destination.js +45 -0
- package/dist/run/dispatch.d.ts +67 -16
- package/dist/run/dispatch.d.ts.map +1 -1
- package/dist/run/dispatch.js +133 -29
- package/dist/run/grounding.d.ts +28 -2
- package/dist/run/grounding.d.ts.map +1 -1
- package/dist/run/grounding.js +54 -10
- package/dist/run/raw-defaults.d.ts.map +1 -1
- package/dist/run/raw-defaults.js +6 -1
- package/dist/run/raw-finalize.d.ts +50 -0
- package/dist/run/raw-finalize.d.ts.map +1 -0
- package/dist/run/raw-finalize.js +266 -0
- package/dist/run/raw-knobs.d.ts +36 -0
- package/dist/run/raw-knobs.d.ts.map +1 -0
- package/dist/run/raw-knobs.js +99 -0
- package/dist/run/raw-request.d.ts +24 -0
- package/dist/run/raw-request.d.ts.map +1 -0
- package/dist/run/raw-request.js +69 -0
- package/dist/run/raw-schema.d.ts +76 -0
- package/dist/run/raw-schema.d.ts.map +1 -0
- package/dist/run/raw-schema.js +166 -0
- package/dist/run/raw-search-cost.d.ts +48 -0
- package/dist/run/raw-search-cost.d.ts.map +1 -0
- package/dist/run/raw-search-cost.js +78 -0
- package/dist/run/raw-support.d.ts +64 -6
- package/dist/run/raw-support.d.ts.map +1 -1
- package/dist/run/raw-support.js +84 -11
- package/dist/run/raw.d.ts +25 -26
- package/dist/run/raw.d.ts.map +1 -1
- package/dist/run/raw.js +30 -351
- package/dist/run/record-attempt.d.ts +12 -0
- package/dist/run/record-attempt.d.ts.map +1 -1
- package/dist/run/record-attempt.js +30 -12
- package/dist/run/repair-attempt.d.ts +68 -0
- package/dist/run/repair-attempt.d.ts.map +1 -0
- package/dist/run/repair-attempt.js +180 -0
- package/dist/run/resolve-destination.d.ts +49 -0
- package/dist/run/resolve-destination.d.ts.map +1 -0
- package/dist/run/resolve-destination.js +76 -0
- package/dist/run/routing-docs.d.ts.map +1 -1
- package/dist/run/routing-docs.js +10 -5
- package/dist/run/run.d.ts +30 -1
- package/dist/run/run.d.ts.map +1 -1
- package/dist/run/run.js +94 -172
- package/dist/run/telemetry-row.d.ts +92 -0
- package/dist/run/telemetry-row.d.ts.map +1 -0
- package/dist/run/telemetry-row.js +81 -0
- package/dist/run/validate.d.ts +12 -1
- package/dist/run/validate.d.ts.map +1 -1
- package/dist/run/validate.js +31 -0
- package/dist/run/web-search-pricing.d.ts +43 -0
- package/dist/run/web-search-pricing.d.ts.map +1 -0
- package/dist/run/web-search-pricing.js +56 -0
- package/dist/savings/odometer.d.ts +4 -0
- package/dist/savings/odometer.d.ts.map +1 -1
- package/dist/savings/odometer.js +20 -1
- package/dist/savings/swap-execute.d.ts +7 -0
- package/dist/savings/swap-execute.d.ts.map +1 -1
- package/dist/savings/swap-execute.js +30 -0
- package/dist/savings/swaps-client.d.ts +10 -0
- package/dist/savings/swaps-client.d.ts.map +1 -1
- package/dist/screen/fixtures/probe-corpora.d.ts +23 -0
- package/dist/screen/fixtures/probe-corpora.d.ts.map +1 -0
- package/dist/screen/fixtures/probe-corpora.js +69 -0
- package/dist/screen/index.d.ts +21 -0
- package/dist/screen/index.d.ts.map +1 -0
- package/dist/screen/index.js +64 -0
- package/dist/screen/model-screen.d.ts +111 -0
- package/dist/screen/model-screen.d.ts.map +1 -0
- package/dist/screen/model-screen.js +178 -0
- package/dist/screen/patterns.d.ts +51 -0
- package/dist/screen/patterns.d.ts.map +1 -0
- package/dist/screen/patterns.js +151 -0
- package/dist/screen/phi-gate.d.ts +52 -0
- package/dist/screen/phi-gate.d.ts.map +1 -0
- package/dist/screen/phi-gate.js +86 -0
- package/dist/screen/policy.d.ts +71 -0
- package/dist/screen/policy.d.ts.map +1 -0
- package/dist/screen/policy.js +334 -0
- package/dist/screen/probe.d.ts +118 -0
- package/dist/screen/probe.d.ts.map +1 -0
- package/dist/screen/probe.js +132 -0
- package/dist/screen/screen-cli.d.ts +16 -0
- package/dist/screen/screen-cli.d.ts.map +1 -0
- package/dist/screen/screen-cli.js +178 -0
- package/dist/screen/screen.d.ts +99 -0
- package/dist/screen/screen.d.ts.map +1 -0
- package/dist/screen/screen.js +0 -0
- package/dist/screen/sidecar.d.ts +69 -0
- package/dist/screen/sidecar.d.ts.map +1 -0
- package/dist/screen/sidecar.js +145 -0
- package/dist/screen/types.d.ts +43 -2
- package/dist/screen/types.d.ts.map +1 -1
- package/dist/settings/api-fetcher.d.ts +14 -3
- package/dist/settings/api-fetcher.d.ts.map +1 -1
- package/dist/settings/api-fetcher.js +61 -10
- package/dist/settings/grading-guide.d.ts +102 -0
- package/dist/settings/grading-guide.d.ts.map +1 -0
- package/dist/settings/grading-guide.js +23 -0
- package/dist/settings/index.d.ts +3 -2
- package/dist/settings/index.d.ts.map +1 -1
- package/dist/settings/index.js +2 -1
- package/dist/settings/task-settings.d.ts +44 -7
- package/dist/settings/task-settings.d.ts.map +1 -1
- package/dist/settings/task-settings.js +53 -12
- package/dist/status.d.ts +32 -20
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +51 -30
- package/dist/telemetry/artifact-store.d.ts +5 -6
- package/dist/telemetry/artifact-store.d.ts.map +1 -1
- package/dist/telemetry/artifact-store.js +21 -24
- package/dist/telemetry/attempt-meta.d.ts +76 -1
- package/dist/telemetry/attempt-meta.d.ts.map +1 -1
- package/dist/telemetry/attempt-meta.js +33 -1
- package/dist/telemetry/exporter-setup.d.ts +2 -0
- package/dist/telemetry/exporter-setup.d.ts.map +1 -1
- package/dist/telemetry/exporter-setup.js +6 -1
- package/dist/telemetry/exporter.d.ts +2 -2
- package/dist/telemetry/exporter.d.ts.map +1 -1
- package/dist/telemetry/exporter.js +22 -41
- package/dist/telemetry/grade-mappers.d.ts +25 -0
- package/dist/telemetry/grade-mappers.d.ts.map +1 -0
- package/dist/telemetry/grade-mappers.js +64 -0
- package/dist/telemetry/grades-sql.d.ts +56 -0
- package/dist/telemetry/grades-sql.d.ts.map +1 -0
- package/dist/telemetry/grades-sql.js +156 -0
- package/dist/telemetry/ingest-protocol.d.ts +55 -48
- package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
- package/dist/telemetry/ingest-protocol.js +48 -83
- package/dist/telemetry/org-identity.d.ts +44 -0
- package/dist/telemetry/org-identity.d.ts.map +1 -0
- package/dist/telemetry/org-identity.js +98 -0
- package/dist/telemetry/route-registration.d.ts +48 -0
- package/dist/telemetry/route-registration.d.ts.map +1 -0
- package/dist/telemetry/route-registration.js +77 -0
- package/dist/telemetry/sinks.d.ts +10 -1
- package/dist/telemetry/sinks.d.ts.map +1 -1
- package/dist/telemetry/sinks.js +52 -6
- package/dist/telemetry/store-migrations.d.ts.map +1 -1
- package/dist/telemetry/store-migrations.js +64 -0
- package/dist/telemetry/store.d.ts +9 -6
- package/dist/telemetry/store.d.ts.map +1 -1
- package/dist/telemetry/store.js +8 -50
- package/dist/tenancy/api-key-format.d.ts +45 -1
- package/dist/tenancy/api-key-format.d.ts.map +1 -1
- package/dist/tenancy/api-key-format.js +50 -1
- package/dist/types/provider.d.ts +23 -3
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/provider.js +26 -0
- package/dist/types/route.d.ts +154 -2
- package/dist/types/route.d.ts.map +1 -1
- package/dist/types/telemetry.d.ts +73 -48
- package/dist/types/telemetry.d.ts.map +1 -1
- package/dist/validate-cli.d.ts.map +1 -1
- package/dist/validate-cli.js +32 -0
- package/dist/verify/verify.d.ts +5 -0
- package/dist/verify/verify.d.ts.map +1 -1
- package/dist/verify/verify.js +2 -0
- package/dist/watch/evaluate.d.ts +84 -1
- package/dist/watch/evaluate.d.ts.map +1 -1
- package/dist/watch/evaluate.js +28 -0
- package/dist/watch/monitor.d.ts +8 -2
- package/dist/watch/monitor.d.ts.map +1 -1
- package/dist/watch/monitor.js +13 -5
- package/dist/watch/regret-monitor-cli.d.ts +16 -0
- package/dist/watch/regret-monitor-cli.d.ts.map +1 -0
- package/dist/watch/regret-monitor-cli.js +137 -0
- package/dist/watch/regret-monitor.d.ts +168 -0
- package/dist/watch/regret-monitor.d.ts.map +1 -0
- package/dist/watch/regret-monitor.js +304 -0
- package/dist/watch/upgrade.d.ts +62 -0
- package/dist/watch/upgrade.d.ts.map +1 -0
- package/dist/watch/upgrade.js +57 -0
- package/docs/agent-skill/SKILL.md +181 -9
- package/docs/how-it-fits.md +3 -2
- package/docs/instrumentation.md +219 -46
- package/docs/llms.txt +158 -35
- package/docs/managed-serving.md +150 -0
- package/docs/mcp-oracle.md +322 -0
- package/docs/migration-playbook.md +5 -4
- package/docs/quickstart.md +263 -89
- package/package.json +18 -4
- package/registry/leaderboard.json +74 -0
- package/registry/registry.json +232 -112
- package/screen.d.ts +16 -0
- package/templates/call-disposition-qa.json +77 -0
- package/templates/catalog-cleansing.json +77 -0
- package/templates/compliance-review.json +76 -0
- package/templates/cs-next-action.json +76 -0
- package/templates/document-extraction.json +78 -0
- package/templates/financial-classification.json +75 -0
- package/templates/lead-qualification.json +76 -0
- package/templates/medical-classification.json +71 -0
- package/templates/ticket-triage.json +76 -0
- package/dist/feedback/feedback.d.ts +0 -35
- package/dist/feedback/feedback.d.ts.map +0 -1
- package/dist/feedback/feedback.js +0 -39
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SC-3 — the T3 model-screen `Detector` (spec-sc3 §2). A `Detector` of the new
|
|
3
|
+
* `kind: "model"` that issues ONE structured-boolean screen call on an untrusted
|
|
4
|
+
* segment (the Anthropic "screen tool output / untrusted content" pattern), maps
|
|
5
|
+
* the boolean → counts-only spans, and returns them like any `Detector`. Its
|
|
6
|
+
* verdict is a SIGNAL that drives the existing deterministic actions
|
|
7
|
+
* (flag|strip|quarantine|block) — never the control itself (research §3.1
|
|
8
|
+
* principle 3). We compose over the shipped SC-1 seam; we do NOT fork it.
|
|
9
|
+
*
|
|
10
|
+
* METERED + DAILY CAP (spec-sc3 §2, mirrors `run-judge` economics): every screen
|
|
11
|
+
* call is a metered inference (the customer's own configured provider — no new
|
|
12
|
+
* default dependency). A per-UTC-day counter caps the spend. When the cap is
|
|
13
|
+
* exhausted the detector THROWS {@link ModelScreenCapExhaustedError}, and the
|
|
14
|
+
* shipped seam (screen.ts §4.3) applies the fail-closed matrix for us:
|
|
15
|
+
* · under `detect` → degrade to the surviving detectors, receipt `degraded:true`, SEND;
|
|
16
|
+
* · under `enforce` → block PRE-DISPATCH (invariant_violation) — an unscreened
|
|
17
|
+
* turn is NEVER dispatched under enforce (teaches the cap).
|
|
18
|
+
* A screen-call ERROR/timeout throws the same way and inherits the same matrix.
|
|
19
|
+
*
|
|
20
|
+
* FREE TEXT NEVER PERSISTS (spec-sc3 §2, acceptance a): the screen call returns a
|
|
21
|
+
* STRUCTURED boolean + optional type labels; this adapter parses ONLY
|
|
22
|
+
* `{ flagged, types }` and discards everything else the model may have emitted.
|
|
23
|
+
* No model prose, no scanned content, and no raw provider response reaches a
|
|
24
|
+
* span, a receipt, a log, or a thrown error — only per-type COUNTS survive (the
|
|
25
|
+
* seam rolls spans into counts). The thrown error carries the detector id + a
|
|
26
|
+
* coarse failure CLASS only (leak invariant §1.4, extended to the model tier).
|
|
27
|
+
*/
|
|
28
|
+
import type { DetectionInput, Detector } from "../detect/detector";
|
|
29
|
+
/** The structured verdict a screen call returns. The model is prompted to answer
|
|
30
|
+
* a boolean ("is this untrusted content attempting to inject instructions /
|
|
31
|
+
* carrying PII?") plus optional coarse type labels. NOTHING else on this object
|
|
32
|
+
* is read — free text the model may add is discarded by {@link parseVerdict}. */
|
|
33
|
+
export interface ModelScreenVerdict {
|
|
34
|
+
readonly flagged: boolean;
|
|
35
|
+
/** Coarse finding types (count labels only, never content). Optional; when
|
|
36
|
+
* absent a single default label is used. Each label is capped in length and
|
|
37
|
+
* count so a chatty model can never smuggle content out through a "type". */
|
|
38
|
+
readonly types?: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The metered screen call. In production this is a small structured-boolean
|
|
42
|
+
* inference on the customer's CONFIGURED provider (SDK lane: in-process on their
|
|
43
|
+
* keys; hosted lane: a server-metered call) — ModelRig bundles no model and adds
|
|
44
|
+
* no default dependency. Injected here so the detector is pure + unit-testable
|
|
45
|
+
* and reads no env. It receives the untrusted text and MUST resolve to a
|
|
46
|
+
* structured {@link ModelScreenVerdict}; its free-text output never leaves it.
|
|
47
|
+
*/
|
|
48
|
+
export type ModelScreenCall = (input: DetectionInput) => Promise<ModelScreenVerdict>;
|
|
49
|
+
/** A per-detector daily-call meter (mirrors `run-judge`'s daily cap). Keyed by
|
|
50
|
+
* UTC date so the cap resets at midnight UTC. In-process by default (one meter
|
|
51
|
+
* per SDK process, like the run-judge economics); injectable for tests and for a
|
|
52
|
+
* hosted lane that meters centrally. */
|
|
53
|
+
export interface DailyCapMeter {
|
|
54
|
+
/** Current call count for `dayKey` (a UTC `YYYY-MM-DD` string). */
|
|
55
|
+
count(dayKey: string): number;
|
|
56
|
+
/** Record one more call against `dayKey`. */
|
|
57
|
+
increment(dayKey: string): void;
|
|
58
|
+
}
|
|
59
|
+
/** The default in-process meter — a Map by UTC day. Old days are pruned lazily
|
|
60
|
+
* so the map never grows unbounded across a long-running process. */
|
|
61
|
+
export declare function createInProcessCapMeter(): DailyCapMeter;
|
|
62
|
+
export interface ModelScreenDetectorConfig {
|
|
63
|
+
/** Receipt id — `model:screen@<model>`. Normalized if a bare model is given. */
|
|
64
|
+
readonly id: string;
|
|
65
|
+
/** The model label the screen call runs on (for the id + receipt version). */
|
|
66
|
+
readonly model: string;
|
|
67
|
+
/** Metered screen calls allowed per UTC day before the tier is cap-exhausted
|
|
68
|
+
* (mirrors `run-judge`). Must be a positive integer. */
|
|
69
|
+
readonly dailyCap: number;
|
|
70
|
+
/** The structured-boolean screen call (customer's configured provider). */
|
|
71
|
+
readonly screenCall: ModelScreenCall;
|
|
72
|
+
/** Confidence stamped on a flagged span (0..1). Default 0.9 so a flag clears
|
|
73
|
+
* the default 0.8 policy threshold; a route may still raise `screen.threshold`
|
|
74
|
+
* to require the model tier plus a corroborating detector. */
|
|
75
|
+
readonly flagConfidence?: number;
|
|
76
|
+
/** Injectable clock (→ the UTC day key) so the cap boundary is testable. */
|
|
77
|
+
readonly now?: () => Date;
|
|
78
|
+
/** Injectable meter (defaults to an in-process per-day meter). */
|
|
79
|
+
readonly meter?: DailyCapMeter;
|
|
80
|
+
}
|
|
81
|
+
/** Normalize an id to the `model:screen@<model>` receipt shape. */
|
|
82
|
+
export declare function modelScreenReceiptId(idOrModel: string): string;
|
|
83
|
+
/** Thrown when the daily cap is exhausted. A subclass of Error so the shipped
|
|
84
|
+
* seam catches it exactly like any detector throw and applies the §4.3 matrix
|
|
85
|
+
* (degrade under detect, block pre-dispatch under enforce). Carries the detector
|
|
86
|
+
* id + a coarse reason ONLY — never content (leak invariant §1.4). */
|
|
87
|
+
export declare class ModelScreenCapExhaustedError extends Error {
|
|
88
|
+
readonly detectorId: string;
|
|
89
|
+
readonly reason: "daily_cap_exhausted";
|
|
90
|
+
constructor(detectorId: string, dailyCap: number);
|
|
91
|
+
}
|
|
92
|
+
/** Thrown when the screen call itself fails (error/timeout/malformed verdict).
|
|
93
|
+
* Coarse reason only — never the provider's response body or the scanned
|
|
94
|
+
* content. The seam turns it into the same fail-closed matrix. */
|
|
95
|
+
export declare class ModelScreenUnavailableError extends Error {
|
|
96
|
+
readonly detectorId: string;
|
|
97
|
+
readonly reason: "call_failed" | "malformed_verdict";
|
|
98
|
+
constructor(detectorId: string, reason: ModelScreenUnavailableError["reason"]);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Build the T3 model-screen `Detector`. `detect(input)`:
|
|
102
|
+
* 1. checks the per-UTC-day cap — over it ⇒ throw {@link ModelScreenCapExhaustedError};
|
|
103
|
+
* 2. otherwise records the call, issues the structured-boolean screen call;
|
|
104
|
+
* 3. parses ONLY `{flagged, types}` (free text discarded), and, when flagged,
|
|
105
|
+
* emits one counts-only span per type covering the segment. Confidence is
|
|
106
|
+
* stamped from `flagConfidence` — the RAW model score is never used or kept.
|
|
107
|
+
* Any call failure throws {@link ModelScreenUnavailableError}. Both throws are
|
|
108
|
+
* caught by the shipped seam, which applies the §4.3 fail-closed matrix.
|
|
109
|
+
*/
|
|
110
|
+
export declare function createModelScreenDetector(config: ModelScreenDetectorConfig): Detector;
|
|
111
|
+
//# sourceMappingURL=model-screen.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-screen.d.ts","sourceRoot":"","sources":["../../src/screen/model-screen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EACV,cAAc,EAEd,QAAQ,EACT,MAAM,oBAAoB,CAAC;AAE5B;;;iFAGiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;iFAE6E;IAC7E,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,cAAc,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;AAErF;;;wCAGwC;AACxC,MAAM,WAAW,aAAa;IAC5B,mEAAmE;IACnE,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,6CAA6C;IAC7C,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED;qEACqE;AACrE,wBAAgB,uBAAuB,IAAI,aAAa,CAavD;AAED,MAAM,WAAW,yBAAyB;IACxC,gFAAgF;IAChF,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;4DACwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;IACrC;;kEAE8D;IAC9D,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,4EAA4E;IAC5E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;CAChC;AAOD,mEAAmE;AACnE,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAI9D;AAED;;;sEAGsE;AACtE,qBAAa,4BAA6B,SAAQ,KAAK;IACrD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAG,qBAAqB,CAAU;gBACrC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CAKjD;AAED;;kEAEkE;AAClE,qBAAa,2BAA4B,SAAQ,KAAK;IACpD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,mBAAmB,CAAC;gBACzC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,2BAA2B,CAAC,QAAQ,CAAC;CAM9E;AAsCD;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,yBAAyB,GAAG,QAAQ,CA0CrF"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SC-3 — the T3 model-screen `Detector` (spec-sc3 §2). A `Detector` of the new
|
|
4
|
+
* `kind: "model"` that issues ONE structured-boolean screen call on an untrusted
|
|
5
|
+
* segment (the Anthropic "screen tool output / untrusted content" pattern), maps
|
|
6
|
+
* the boolean → counts-only spans, and returns them like any `Detector`. Its
|
|
7
|
+
* verdict is a SIGNAL that drives the existing deterministic actions
|
|
8
|
+
* (flag|strip|quarantine|block) — never the control itself (research §3.1
|
|
9
|
+
* principle 3). We compose over the shipped SC-1 seam; we do NOT fork it.
|
|
10
|
+
*
|
|
11
|
+
* METERED + DAILY CAP (spec-sc3 §2, mirrors `run-judge` economics): every screen
|
|
12
|
+
* call is a metered inference (the customer's own configured provider — no new
|
|
13
|
+
* default dependency). A per-UTC-day counter caps the spend. When the cap is
|
|
14
|
+
* exhausted the detector THROWS {@link ModelScreenCapExhaustedError}, and the
|
|
15
|
+
* shipped seam (screen.ts §4.3) applies the fail-closed matrix for us:
|
|
16
|
+
* · under `detect` → degrade to the surviving detectors, receipt `degraded:true`, SEND;
|
|
17
|
+
* · under `enforce` → block PRE-DISPATCH (invariant_violation) — an unscreened
|
|
18
|
+
* turn is NEVER dispatched under enforce (teaches the cap).
|
|
19
|
+
* A screen-call ERROR/timeout throws the same way and inherits the same matrix.
|
|
20
|
+
*
|
|
21
|
+
* FREE TEXT NEVER PERSISTS (spec-sc3 §2, acceptance a): the screen call returns a
|
|
22
|
+
* STRUCTURED boolean + optional type labels; this adapter parses ONLY
|
|
23
|
+
* `{ flagged, types }` and discards everything else the model may have emitted.
|
|
24
|
+
* No model prose, no scanned content, and no raw provider response reaches a
|
|
25
|
+
* span, a receipt, a log, or a thrown error — only per-type COUNTS survive (the
|
|
26
|
+
* seam rolls spans into counts). The thrown error carries the detector id + a
|
|
27
|
+
* coarse failure CLASS only (leak invariant §1.4, extended to the model tier).
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.ModelScreenUnavailableError = exports.ModelScreenCapExhaustedError = void 0;
|
|
31
|
+
exports.createInProcessCapMeter = createInProcessCapMeter;
|
|
32
|
+
exports.modelScreenReceiptId = modelScreenReceiptId;
|
|
33
|
+
exports.createModelScreenDetector = createModelScreenDetector;
|
|
34
|
+
/** The default in-process meter — a Map by UTC day. Old days are pruned lazily
|
|
35
|
+
* so the map never grows unbounded across a long-running process. */
|
|
36
|
+
function createInProcessCapMeter() {
|
|
37
|
+
const byDay = new Map();
|
|
38
|
+
return {
|
|
39
|
+
count(dayKey) {
|
|
40
|
+
return byDay.get(dayKey) ?? 0;
|
|
41
|
+
},
|
|
42
|
+
increment(dayKey) {
|
|
43
|
+
// Prune any day that is not the one being written (single-day retention:
|
|
44
|
+
// the cap is per UTC day, so only the current day's count is load-bearing).
|
|
45
|
+
for (const key of byDay.keys())
|
|
46
|
+
if (key !== dayKey)
|
|
47
|
+
byDay.delete(key);
|
|
48
|
+
byDay.set(dayKey, (byDay.get(dayKey) ?? 0) + 1);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const DEFAULT_FLAG_CONFIDENCE = 0.9;
|
|
53
|
+
const DEFAULT_TYPE = "injection:model_flag";
|
|
54
|
+
const MAX_TYPES = 8; // a verdict may not carry more than this many type labels
|
|
55
|
+
const MAX_TYPE_LEN = 64; // …nor a label longer than this — content can't ride a "type"
|
|
56
|
+
/** Normalize an id to the `model:screen@<model>` receipt shape. */
|
|
57
|
+
function modelScreenReceiptId(idOrModel) {
|
|
58
|
+
if (idOrModel.startsWith("model:screen@"))
|
|
59
|
+
return idOrModel;
|
|
60
|
+
if (idOrModel.startsWith("model:"))
|
|
61
|
+
return idOrModel;
|
|
62
|
+
return `model:screen@${idOrModel}`;
|
|
63
|
+
}
|
|
64
|
+
/** Thrown when the daily cap is exhausted. A subclass of Error so the shipped
|
|
65
|
+
* seam catches it exactly like any detector throw and applies the §4.3 matrix
|
|
66
|
+
* (degrade under detect, block pre-dispatch under enforce). Carries the detector
|
|
67
|
+
* id + a coarse reason ONLY — never content (leak invariant §1.4). */
|
|
68
|
+
class ModelScreenCapExhaustedError extends Error {
|
|
69
|
+
constructor(detectorId, dailyCap) {
|
|
70
|
+
super(`model screen "${detectorId}" is over its daily cap (${dailyCap}/day) — degrading (detect) / blocking pre-dispatch (enforce)`);
|
|
71
|
+
this.reason = "daily_cap_exhausted";
|
|
72
|
+
this.name = "ModelScreenCapExhaustedError";
|
|
73
|
+
this.detectorId = detectorId;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.ModelScreenCapExhaustedError = ModelScreenCapExhaustedError;
|
|
77
|
+
/** Thrown when the screen call itself fails (error/timeout/malformed verdict).
|
|
78
|
+
* Coarse reason only — never the provider's response body or the scanned
|
|
79
|
+
* content. The seam turns it into the same fail-closed matrix. */
|
|
80
|
+
class ModelScreenUnavailableError extends Error {
|
|
81
|
+
constructor(detectorId, reason) {
|
|
82
|
+
super(`model screen "${detectorId}" is unavailable (${reason})`);
|
|
83
|
+
this.name = "ModelScreenUnavailableError";
|
|
84
|
+
this.detectorId = detectorId;
|
|
85
|
+
this.reason = reason;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.ModelScreenUnavailableError = ModelScreenUnavailableError;
|
|
89
|
+
function isRecord(v) {
|
|
90
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Read ONLY `{ flagged, types }` from whatever the screen call returned —
|
|
94
|
+
* defensively, like the sidecar's `parseSpans`. A non-boolean `flagged` is a
|
|
95
|
+
* malformed verdict (fail closed, never a silent open). Every other field (and
|
|
96
|
+
* any echoed free text) is discarded here so it can never reach a span. Type
|
|
97
|
+
* labels are trimmed, de-duplicated, length-capped, and count-capped.
|
|
98
|
+
*/
|
|
99
|
+
function parseVerdict(raw, detectorId) {
|
|
100
|
+
if (!isRecord(raw) || typeof raw.flagged !== "boolean") {
|
|
101
|
+
throw new ModelScreenUnavailableError(detectorId, "malformed_verdict");
|
|
102
|
+
}
|
|
103
|
+
let types = [];
|
|
104
|
+
if (Array.isArray(raw.types)) {
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
for (const t of raw.types) {
|
|
107
|
+
if (typeof t !== "string")
|
|
108
|
+
continue;
|
|
109
|
+
const label = t.slice(0, MAX_TYPE_LEN);
|
|
110
|
+
if (label.length === 0 || seen.has(label))
|
|
111
|
+
continue;
|
|
112
|
+
seen.add(label);
|
|
113
|
+
types.push(label);
|
|
114
|
+
if (types.length >= MAX_TYPES)
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (types.length === 0)
|
|
119
|
+
types = [DEFAULT_TYPE];
|
|
120
|
+
return { flagged: raw.flagged, types };
|
|
121
|
+
}
|
|
122
|
+
/** The UTC `YYYY-MM-DD` day key the cap is bucketed by. */
|
|
123
|
+
function utcDayKey(now) {
|
|
124
|
+
return now.toISOString().slice(0, 10);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Build the T3 model-screen `Detector`. `detect(input)`:
|
|
128
|
+
* 1. checks the per-UTC-day cap — over it ⇒ throw {@link ModelScreenCapExhaustedError};
|
|
129
|
+
* 2. otherwise records the call, issues the structured-boolean screen call;
|
|
130
|
+
* 3. parses ONLY `{flagged, types}` (free text discarded), and, when flagged,
|
|
131
|
+
* emits one counts-only span per type covering the segment. Confidence is
|
|
132
|
+
* stamped from `flagConfidence` — the RAW model score is never used or kept.
|
|
133
|
+
* Any call failure throws {@link ModelScreenUnavailableError}. Both throws are
|
|
134
|
+
* caught by the shipped seam, which applies the §4.3 fail-closed matrix.
|
|
135
|
+
*/
|
|
136
|
+
function createModelScreenDetector(config) {
|
|
137
|
+
if (!Number.isInteger(config.dailyCap) || config.dailyCap <= 0) {
|
|
138
|
+
throw new Error(`model screen dailyCap must be a positive integer (got ${config.dailyCap})`);
|
|
139
|
+
}
|
|
140
|
+
const id = modelScreenReceiptId(config.id);
|
|
141
|
+
const confidence = config.flagConfidence ?? DEFAULT_FLAG_CONFIDENCE;
|
|
142
|
+
const now = config.now ?? (() => new Date());
|
|
143
|
+
const meter = config.meter ?? createInProcessCapMeter();
|
|
144
|
+
return {
|
|
145
|
+
id,
|
|
146
|
+
kind: "model",
|
|
147
|
+
async detect(input) {
|
|
148
|
+
// Trusted framing is the route author's own — never screened (mirrors the
|
|
149
|
+
// pattern detector), and never spends a metered call on it.
|
|
150
|
+
if (input.tier === "trusted")
|
|
151
|
+
return [];
|
|
152
|
+
const dayKey = utcDayKey(now());
|
|
153
|
+
if (meter.count(dayKey) >= config.dailyCap) {
|
|
154
|
+
// Cap exhausted — throw BEFORE spending a call. The seam degrades (detect)
|
|
155
|
+
// or blocks pre-dispatch (enforce). No content in the error.
|
|
156
|
+
throw new ModelScreenCapExhaustedError(id, config.dailyCap);
|
|
157
|
+
}
|
|
158
|
+
// Reserve the call against the cap before issuing it (a failed call still
|
|
159
|
+
// spent provider budget on the customer's keys — count it, like run-judge).
|
|
160
|
+
meter.increment(dayKey);
|
|
161
|
+
let raw;
|
|
162
|
+
try {
|
|
163
|
+
raw = await config.screenCall(input);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// NEVER let the provider error's message (which could echo the scanned
|
|
167
|
+
// content) escape — only the coarse reason travels.
|
|
168
|
+
throw new ModelScreenUnavailableError(id, "call_failed");
|
|
169
|
+
}
|
|
170
|
+
const { flagged, types } = parseVerdict(raw, id);
|
|
171
|
+
if (!flagged)
|
|
172
|
+
return [];
|
|
173
|
+
// One counts-only span per type over the whole segment. Offsets are into
|
|
174
|
+
// the exact string handed in; the substring is never read or kept.
|
|
175
|
+
return types.map((type) => ({ start: 0, end: input.text.length, type, confidence }));
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screening — T0 deterministic injection patterns + the optional T1 classifier
|
|
3
|
+
* adapter (WIRED 2026-08-25, SC-1a). Spec §3 (Detector) + §4 (T0/T1).
|
|
4
|
+
*
|
|
5
|
+
* An OWASP-derived pattern pack that implements the shared `Detector` interface
|
|
6
|
+
* (packages/modelrig/src/detect/detector.ts) with `kind: "deterministic"`,
|
|
7
|
+
* confidence 1 on a match.
|
|
8
|
+
*
|
|
9
|
+
* DOCTRINE (research §2.2): detection is a SIGNAL, not a control. These patterns
|
|
10
|
+
* are trivially evaded by paraphrase/encoding; they exist to DRIVE the
|
|
11
|
+
* deterministic actions (flag/strip/quarantine) and to populate a MEASURED
|
|
12
|
+
* false-positive rate, NEVER to claim prevention (claim-lint, spec §7).
|
|
13
|
+
*
|
|
14
|
+
* LEAK INVARIANT (spec §1.4): a detector emits SPANS (offsets + type +
|
|
15
|
+
* confidence) — never the matched substring. Nothing in this file returns,
|
|
16
|
+
* logs, or persists a matched value.
|
|
17
|
+
*/
|
|
18
|
+
import type { Detector } from "../detect/detector";
|
|
19
|
+
/** The registry id + version of the deterministic pack. Bumped when the pattern
|
|
20
|
+
* set changes so a receipt's `detectors[].version` pins what ran. */
|
|
21
|
+
export declare const PATTERNS_ID = "patterns@v1";
|
|
22
|
+
/**
|
|
23
|
+
* The T0 pattern detector. Runs only on untrusted/semi tiers — the template's
|
|
24
|
+
* own trusted text is never scanned (it is the route author's, by construction).
|
|
25
|
+
* Deterministic: confidence 1 on every match, versioned as `patterns@v1`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createPatternDetector(): Detector;
|
|
28
|
+
/** Thrown by {@link createClassifierDetector} when the optional peer dep or the
|
|
29
|
+
* local model file is absent — a TYPED signal the engine catches to degrade to
|
|
30
|
+
* T0 with `degraded: true` (spec §4.3), never a run-failing surprise. */
|
|
31
|
+
export declare class ScreenDetectorUnavailableError extends Error {
|
|
32
|
+
readonly detectorId: string;
|
|
33
|
+
constructor(detectorId: string, reason: string);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The optional T1 in-process classifier adapter (A3: OPTIONAL peer dependency;
|
|
37
|
+
* NO weights bundled; NO default npm dep). Absent peer dep OR absent model file
|
|
38
|
+
* ⇒ throws {@link ScreenDetectorUnavailableError} so the engine degrades to T0
|
|
39
|
+
* with a once-per-process notice and `degraded: true` on the receipt.
|
|
40
|
+
*
|
|
41
|
+
* ModelRig NEVER bundles or re-hosts weights — the customer fetches them with
|
|
42
|
+
* `modelrig screen pull <detector>` to a local cache and points `modelPath` at
|
|
43
|
+
* the result. The runtime peer (`onnxruntime-node` or a WASM transformers
|
|
44
|
+
* backend) is likewise the customer's to install.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createClassifierDetector(opts: {
|
|
47
|
+
readonly id: string;
|
|
48
|
+
readonly modelPath: string;
|
|
49
|
+
readonly runtime: "onnx" | "wasm";
|
|
50
|
+
}): Detector;
|
|
51
|
+
//# sourceMappingURL=patterns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patterns.d.ts","sourceRoot":"","sources":["../../src/screen/patterns.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAiC,MAAM,oBAAoB,CAAC;AAElF;qEACqE;AACrE,eAAO,MAAM,WAAW,gBAAgB,CAAC;AA+DzC;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,QAAQ,CAehD;AAED;;yEAEyE;AACzE,qBAAa,8BAA+B,SAAQ,KAAK;IACvD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAChB,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAK/C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;CACnC,GAAG,QAAQ,CAiCX"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Screening — T0 deterministic injection patterns + the optional T1 classifier
|
|
4
|
+
* adapter (WIRED 2026-08-25, SC-1a). Spec §3 (Detector) + §4 (T0/T1).
|
|
5
|
+
*
|
|
6
|
+
* An OWASP-derived pattern pack that implements the shared `Detector` interface
|
|
7
|
+
* (packages/modelrig/src/detect/detector.ts) with `kind: "deterministic"`,
|
|
8
|
+
* confidence 1 on a match.
|
|
9
|
+
*
|
|
10
|
+
* DOCTRINE (research §2.2): detection is a SIGNAL, not a control. These patterns
|
|
11
|
+
* are trivially evaded by paraphrase/encoding; they exist to DRIVE the
|
|
12
|
+
* deterministic actions (flag/strip/quarantine) and to populate a MEASURED
|
|
13
|
+
* false-positive rate, NEVER to claim prevention (claim-lint, spec §7).
|
|
14
|
+
*
|
|
15
|
+
* LEAK INVARIANT (spec §1.4): a detector emits SPANS (offsets + type +
|
|
16
|
+
* confidence) — never the matched substring. Nothing in this file returns,
|
|
17
|
+
* logs, or persists a matched value.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.ScreenDetectorUnavailableError = exports.PATTERNS_ID = void 0;
|
|
21
|
+
exports.createPatternDetector = createPatternDetector;
|
|
22
|
+
exports.createClassifierDetector = createClassifierDetector;
|
|
23
|
+
const node_fs_1 = require("node:fs");
|
|
24
|
+
/** The registry id + version of the deterministic pack. Bumped when the pattern
|
|
25
|
+
* set changes so a receipt's `detectors[].version` pins what ran. */
|
|
26
|
+
exports.PATTERNS_ID = "patterns@v1";
|
|
27
|
+
/** The T0 pack. Deliberately narrow, US-English-shaped (A8), and biased toward
|
|
28
|
+
* classic injection SHAPES over broad keyword nets so the measured FP rate stays
|
|
29
|
+
* honest. Each entry is a signal, not a guarantee. */
|
|
30
|
+
const INJECTION_PATTERNS = [
|
|
31
|
+
// "ignore the previous instructions" / "disregard all prior rules" family.
|
|
32
|
+
{
|
|
33
|
+
type: "injection:instruction_override",
|
|
34
|
+
re: /\b(?:ignore|disregard|forget|override)\b[^.?!\n]{0,40}\b(?:previous|prior|above|earlier|all|any)\b[^.?!\n]{0,20}\b(?:instruction|instructions|prompt|prompts|rule|rules|context|direction|directions)\b/gi,
|
|
35
|
+
},
|
|
36
|
+
// "you are now …", "new instructions:", "act as", "pretend to be" — role hijack.
|
|
37
|
+
{
|
|
38
|
+
type: "injection:role_override",
|
|
39
|
+
re: /\b(?:you\s+are\s+now|from\s+now\s+on|new\s+(?:instructions?|role|persona|system\s+prompt)|act\s+as(?:\s+if)?|pretend\s+to\s+be|roleplay\s+as|switch\s+to\s+developer\s+mode)\b/gi,
|
|
40
|
+
},
|
|
41
|
+
// An untrusted segment carrying its own "system:" / "assistant:" role label —
|
|
42
|
+
// the classic transcript-smuggling shape the raw lane's JSON-turn guard closes.
|
|
43
|
+
{
|
|
44
|
+
type: "injection:role_label",
|
|
45
|
+
re: /(?:^|\n)\s*(?:system|assistant|developer)\s*:/gi,
|
|
46
|
+
},
|
|
47
|
+
// Chat-template / instruction delimiters smuggled into data.
|
|
48
|
+
{
|
|
49
|
+
type: "injection:delimiter",
|
|
50
|
+
re: /<\/?(?:system|assistant|user|instructions?)>|\[\/?INST\]|<\|(?:im_start|im_end|system|user|assistant|endoftext)\|>/gi,
|
|
51
|
+
},
|
|
52
|
+
// "reveal your system prompt" / "print your instructions" — prompt-leak probe.
|
|
53
|
+
{
|
|
54
|
+
type: "injection:prompt_leak",
|
|
55
|
+
re: /\b(?:reveal|print|repeat|show|output|display|tell\s+me)\b[^.?!\n]{0,30}\b(?:system\s+prompt|your\s+instructions|initial\s+prompt|the\s+prompt\s+above|your\s+rules)\b/gi,
|
|
56
|
+
},
|
|
57
|
+
// "email/exfiltrate the api key/password/secret" — data-exfiltration steering.
|
|
58
|
+
{
|
|
59
|
+
type: "injection:exfiltration",
|
|
60
|
+
re: /\b(?:send|exfiltrate|post|email|upload|leak|forward|transmit)\b[^.?!\n]{0,40}\b(?:api[\s_-]?key|password|secret|secrets|token|credential|credentials|private\s+key)\b/gi,
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
/** Unicode tag plane (U+E0000–U+E007F) — invisible chars abused to smuggle
|
|
64
|
+
* instructions past a human reviewer. Detected as its own finding TYPE; the
|
|
65
|
+
* assembler strips them from delivered content, but their PRESENCE in untrusted
|
|
66
|
+
* input is itself a signal worth counting. */
|
|
67
|
+
const UNICODE_TAG_RE = /[\u{E0000}-\u{E007F}]+/gu;
|
|
68
|
+
/** Collect the spans one pattern matches in `text`. Offsets are into the exact
|
|
69
|
+
* string handed in; the matched substring is discarded immediately. */
|
|
70
|
+
function spansFor(type, re, text) {
|
|
71
|
+
const spans = [];
|
|
72
|
+
for (const m of text.matchAll(re)) {
|
|
73
|
+
const start = m.index ?? 0;
|
|
74
|
+
spans.push({ start, end: start + m[0].length, type, confidence: 1 });
|
|
75
|
+
}
|
|
76
|
+
return spans;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The T0 pattern detector. Runs only on untrusted/semi tiers — the template's
|
|
80
|
+
* own trusted text is never scanned (it is the route author's, by construction).
|
|
81
|
+
* Deterministic: confidence 1 on every match, versioned as `patterns@v1`.
|
|
82
|
+
*/
|
|
83
|
+
function createPatternDetector() {
|
|
84
|
+
return {
|
|
85
|
+
id: exports.PATTERNS_ID,
|
|
86
|
+
kind: "deterministic",
|
|
87
|
+
detect(input) {
|
|
88
|
+
// Trusted content is the route's own framing — never an injection source.
|
|
89
|
+
if (input.tier === "trusted")
|
|
90
|
+
return Promise.resolve([]);
|
|
91
|
+
const spans = [];
|
|
92
|
+
for (const p of INJECTION_PATTERNS)
|
|
93
|
+
spans.push(...spansFor(p.type, p.re, input.text));
|
|
94
|
+
spans.push(...spansFor("injection:unicode_tag", UNICODE_TAG_RE, input.text));
|
|
95
|
+
// Reading order keeps the (offsets-only) span list stable and testable.
|
|
96
|
+
spans.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
97
|
+
return Promise.resolve(spans);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Thrown by {@link createClassifierDetector} when the optional peer dep or the
|
|
102
|
+
* local model file is absent — a TYPED signal the engine catches to degrade to
|
|
103
|
+
* T0 with `degraded: true` (spec §4.3), never a run-failing surprise. */
|
|
104
|
+
class ScreenDetectorUnavailableError extends Error {
|
|
105
|
+
constructor(detectorId, reason) {
|
|
106
|
+
super(`screen detector "${detectorId}" is unavailable: ${reason}`);
|
|
107
|
+
this.name = "ScreenDetectorUnavailableError";
|
|
108
|
+
this.detectorId = detectorId;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.ScreenDetectorUnavailableError = ScreenDetectorUnavailableError;
|
|
112
|
+
/**
|
|
113
|
+
* The optional T1 in-process classifier adapter (A3: OPTIONAL peer dependency;
|
|
114
|
+
* NO weights bundled; NO default npm dep). Absent peer dep OR absent model file
|
|
115
|
+
* ⇒ throws {@link ScreenDetectorUnavailableError} so the engine degrades to T0
|
|
116
|
+
* with a once-per-process notice and `degraded: true` on the receipt.
|
|
117
|
+
*
|
|
118
|
+
* ModelRig NEVER bundles or re-hosts weights — the customer fetches them with
|
|
119
|
+
* `modelrig screen pull <detector>` to a local cache and points `modelPath` at
|
|
120
|
+
* the result. The runtime peer (`onnxruntime-node` or a WASM transformers
|
|
121
|
+
* backend) is likewise the customer's to install.
|
|
122
|
+
*/
|
|
123
|
+
function createClassifierDetector(opts) {
|
|
124
|
+
const peer = opts.runtime === "onnx" ? "onnxruntime-node" : "@xenova/transformers";
|
|
125
|
+
// A3: the peer is an OPTIONAL dependency resolved at RUNTIME. `require` is used
|
|
126
|
+
// (not a static import) precisely so a missing peer is a catchable throw, not a
|
|
127
|
+
// load-time crash / a bundled dependency.
|
|
128
|
+
let runtimeModule;
|
|
129
|
+
try {
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
131
|
+
runtimeModule = require(peer);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
throw new ScreenDetectorUnavailableError(opts.id, `optional peer dependency "${peer}" is not installed (install it and run \`modelrig screen pull ${opts.id}\`)`);
|
|
135
|
+
}
|
|
136
|
+
if (!(0, node_fs_1.existsSync)(opts.modelPath)) {
|
|
137
|
+
throw new ScreenDetectorUnavailableError(opts.id, `model file not found at "${opts.modelPath}" — fetch it with \`modelrig screen pull ${opts.id}\` (weights are never bundled, A3)`);
|
|
138
|
+
}
|
|
139
|
+
// The concrete inference wiring (tokenize → run → score → span) is delegated to
|
|
140
|
+
// the loaded runtime; this SC-1a scaffold proves the OPTIONAL-load contract and
|
|
141
|
+
// the fail-closed degrade path. A configured-and-present classifier tier is a
|
|
142
|
+
// Matt-env run (live weights); the deterministic T0 path is what ships.
|
|
143
|
+
void runtimeModule;
|
|
144
|
+
return {
|
|
145
|
+
id: opts.id,
|
|
146
|
+
kind: "classifier",
|
|
147
|
+
detect(_input) {
|
|
148
|
+
return Promise.resolve([]);
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SC-2 — the gated `phi` route posture (spec-sc2 §4). Deterministic, fail-closed,
|
|
3
|
+
* dependency-free. Runs at ROUTE LOAD (the same place `require:` / `mask:` /
|
|
4
|
+
* `screen:` are validated), so a mis-postured PHI route REFUSES TO LOAD with a
|
|
5
|
+
* teaching error rather than silently serving PHI on an unmasked, un-screened,
|
|
6
|
+
* content-retaining route.
|
|
7
|
+
*
|
|
8
|
+
* A route classified `phi` (route-level `classification: phi`) MUST satisfy ALL:
|
|
9
|
+
* 1. mask effective mode ≥ redact — i.e. mask rewrites (mode `mask` or `block`,
|
|
10
|
+
* CS-1); `off`/`detect` do NOT mask and fail the gate.
|
|
11
|
+
* 2. `require: zero_retention` OR a `sidecar` detector present in
|
|
12
|
+
* `screen.detectors` (the managed-custody + sidecar path).
|
|
13
|
+
*
|
|
14
|
+
* This does NOT claim de-identification (that is a banned claim, §6). It enforces
|
|
15
|
+
* that PHI never rides an unmasked, un-screened, content-retaining route. The
|
|
16
|
+
* gate is code-visible and lane-independent: it checks the mask mode, the
|
|
17
|
+
* `require` list, and the presence of a sidecar detector — all resolved at load.
|
|
18
|
+
* `modelrig validate` prints {@link describePhiPosture}: the resolved posture and
|
|
19
|
+
* the specific unmet requirement.
|
|
20
|
+
*/
|
|
21
|
+
import type { DataClassification } from "../artifacts/classification";
|
|
22
|
+
import type { ResolvedMaskPolicy } from "../mask/types";
|
|
23
|
+
import type { Requirement } from "../types/route";
|
|
24
|
+
import type { ResolvedScreenPolicy } from "./types";
|
|
25
|
+
/** The load-time inputs the gate reads — all already resolved by the loader. */
|
|
26
|
+
export interface PhiPostureInput {
|
|
27
|
+
/** The route-level `classification`, if any. The gate fires only for `phi`. */
|
|
28
|
+
readonly classification?: DataClassification;
|
|
29
|
+
readonly mask?: ResolvedMaskPolicy;
|
|
30
|
+
readonly screen?: ResolvedScreenPolicy;
|
|
31
|
+
readonly require: readonly Requirement[];
|
|
32
|
+
}
|
|
33
|
+
/** The resolved posture — what the gate saw and whether it passed. */
|
|
34
|
+
export interface PhiPosture {
|
|
35
|
+
readonly isPhi: boolean;
|
|
36
|
+
readonly maskRedacts: boolean;
|
|
37
|
+
readonly zeroRetention: boolean;
|
|
38
|
+
readonly sidecarPresent: boolean;
|
|
39
|
+
readonly satisfied: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** Resolve the posture for a route (whether or not it is `phi`). */
|
|
42
|
+
export declare function resolvePhiPosture(input: PhiPostureInput): PhiPosture;
|
|
43
|
+
/**
|
|
44
|
+
* Teaching-style problems for a mis-postured `phi` route (CE-2 fixHint pattern) —
|
|
45
|
+
* EMPTY when the route is not `phi` or the posture is satisfied. Each string
|
|
46
|
+
* names the SPECIFIC unmet requirement so the author knows exactly what to add.
|
|
47
|
+
*/
|
|
48
|
+
export declare function checkPhiPosture(input: PhiPostureInput): readonly string[];
|
|
49
|
+
/** A one-line human posture summary for `modelrig validate` (spec-sc2 §4): the
|
|
50
|
+
* resolved posture and, when unsatisfied, the unmet requirement. */
|
|
51
|
+
export declare function describePhiPosture(input: PhiPostureInput): string;
|
|
52
|
+
//# sourceMappingURL=phi-gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phi-gate.d.ts","sourceRoot":"","sources":["../../src/screen/phi-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAEpD,gFAAgF;AAChF,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,QAAQ,CAAC,cAAc,CAAC,EAAE,kBAAkB,CAAC;IAC7C,QAAQ,CAAC,IAAI,CAAC,EAAE,kBAAkB,CAAC;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,oBAAoB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;CAC1C;AAED,sEAAsE;AACtE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAcD,oEAAoE;AACpE,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,eAAe,GAAG,UAAU,CAYpE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,eAAe,GAAG,SAAS,MAAM,EAAE,CAkBzE;AAED;oEACoE;AACpE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAYjE"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SC-2 — the gated `phi` route posture (spec-sc2 §4). Deterministic, fail-closed,
|
|
4
|
+
* dependency-free. Runs at ROUTE LOAD (the same place `require:` / `mask:` /
|
|
5
|
+
* `screen:` are validated), so a mis-postured PHI route REFUSES TO LOAD with a
|
|
6
|
+
* teaching error rather than silently serving PHI on an unmasked, un-screened,
|
|
7
|
+
* content-retaining route.
|
|
8
|
+
*
|
|
9
|
+
* A route classified `phi` (route-level `classification: phi`) MUST satisfy ALL:
|
|
10
|
+
* 1. mask effective mode ≥ redact — i.e. mask rewrites (mode `mask` or `block`,
|
|
11
|
+
* CS-1); `off`/`detect` do NOT mask and fail the gate.
|
|
12
|
+
* 2. `require: zero_retention` OR a `sidecar` detector present in
|
|
13
|
+
* `screen.detectors` (the managed-custody + sidecar path).
|
|
14
|
+
*
|
|
15
|
+
* This does NOT claim de-identification (that is a banned claim, §6). It enforces
|
|
16
|
+
* that PHI never rides an unmasked, un-screened, content-retaining route. The
|
|
17
|
+
* gate is code-visible and lane-independent: it checks the mask mode, the
|
|
18
|
+
* `require` list, and the presence of a sidecar detector — all resolved at load.
|
|
19
|
+
* `modelrig validate` prints {@link describePhiPosture}: the resolved posture and
|
|
20
|
+
* the specific unmet requirement.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.resolvePhiPosture = resolvePhiPosture;
|
|
24
|
+
exports.checkPhiPosture = checkPhiPosture;
|
|
25
|
+
exports.describePhiPosture = describePhiPosture;
|
|
26
|
+
/** A mask policy "redacts" (rewrites content) iff its effective mode is `mask`
|
|
27
|
+
* or `block` — `off`/`detect` scan but do not rewrite (CS-1 §3). */
|
|
28
|
+
function maskRedacts(mask) {
|
|
29
|
+
return mask !== undefined && (mask.policy.mode === "mask" || mask.policy.mode === "block");
|
|
30
|
+
}
|
|
31
|
+
/** A sidecar detector is present iff the resolved screen policy carries a
|
|
32
|
+
* `sidecar:*` detector identity (built from `screen.sidecar` at resolve time). */
|
|
33
|
+
function hasSidecar(screen) {
|
|
34
|
+
return screen !== undefined && screen.detectorMeta.some((d) => d.id.startsWith("sidecar:"));
|
|
35
|
+
}
|
|
36
|
+
/** Resolve the posture for a route (whether or not it is `phi`). */
|
|
37
|
+
function resolvePhiPosture(input) {
|
|
38
|
+
const isPhi = input.classification === "phi";
|
|
39
|
+
const maskR = maskRedacts(input.mask);
|
|
40
|
+
const zdr = input.require.includes("zero_retention");
|
|
41
|
+
const sidecar = hasSidecar(input.screen);
|
|
42
|
+
return {
|
|
43
|
+
isPhi,
|
|
44
|
+
maskRedacts: maskR,
|
|
45
|
+
zeroRetention: zdr,
|
|
46
|
+
sidecarPresent: sidecar,
|
|
47
|
+
satisfied: maskR && (zdr || sidecar),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Teaching-style problems for a mis-postured `phi` route (CE-2 fixHint pattern) —
|
|
52
|
+
* EMPTY when the route is not `phi` or the posture is satisfied. Each string
|
|
53
|
+
* names the SPECIFIC unmet requirement so the author knows exactly what to add.
|
|
54
|
+
*/
|
|
55
|
+
function checkPhiPosture(input) {
|
|
56
|
+
const posture = resolvePhiPosture(input);
|
|
57
|
+
if (!posture.isPhi || posture.satisfied)
|
|
58
|
+
return [];
|
|
59
|
+
const problems = [];
|
|
60
|
+
if (!posture.maskRedacts) {
|
|
61
|
+
problems.push("a `phi`-classified route must mask its content — set `mask.mode: mask` (or `block`); " +
|
|
62
|
+
"`off`/`detect` do not redact. PHI must never ride an unmasked route.");
|
|
63
|
+
}
|
|
64
|
+
if (!posture.zeroRetention && !posture.sidecarPresent) {
|
|
65
|
+
problems.push("a `phi`-classified route must either `require: [zero_retention]` OR configure a `screen.sidecar` " +
|
|
66
|
+
"(a customer-run de-id detector, present in the resolved `screen.detectors` as `sidecar:<digest>`). " +
|
|
67
|
+
"PHI must never ride a content-retaining route without the sidecar.");
|
|
68
|
+
}
|
|
69
|
+
return problems;
|
|
70
|
+
}
|
|
71
|
+
/** A one-line human posture summary for `modelrig validate` (spec-sc2 §4): the
|
|
72
|
+
* resolved posture and, when unsatisfied, the unmet requirement. */
|
|
73
|
+
function describePhiPosture(input) {
|
|
74
|
+
const p = resolvePhiPosture(input);
|
|
75
|
+
if (!p.isPhi)
|
|
76
|
+
return "";
|
|
77
|
+
const parts = [
|
|
78
|
+
`mask≥redact=${p.maskRedacts ? "yes" : "NO"}`,
|
|
79
|
+
`zero_retention=${p.zeroRetention ? "yes" : "no"}`,
|
|
80
|
+
`sidecar=${p.sidecarPresent ? "yes" : "no"}`,
|
|
81
|
+
];
|
|
82
|
+
const verdict = p.satisfied
|
|
83
|
+
? "OK — PHI posture satisfied"
|
|
84
|
+
: `UNMET — ${checkPhiPosture(input).join(" ")}`;
|
|
85
|
+
return `phi posture [${parts.join(" · ")}] → ${verdict}`;
|
|
86
|
+
}
|