@arnilo/prism 0.11.0 → 0.12.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +28 -27
  3. package/dist/cli-dev.js +1 -1
  4. package/dist/cli-init.js +4 -4
  5. package/dist/cli-provider-add.js +2 -2
  6. package/dist/field-evidence.d.ts +53 -0
  7. package/dist/field-evidence.js +143 -0
  8. package/dist/index.d.ts +3 -1
  9. package/dist/index.js +2 -1
  10. package/dist/run-bundle.js +2 -2
  11. package/docs/acp-agent.md +2 -2
  12. package/docs/ag-ui.md +1 -1
  13. package/docs/attention-compiler.md +1 -1
  14. package/docs/cli-rpc.md +2 -2
  15. package/docs/coding-tools.md +3 -3
  16. package/docs/compaction-observational-memory.md +13 -3
  17. package/docs/computer-use-linux.md +2 -2
  18. package/docs/core.md +9 -10
  19. package/docs/dev-inspector.md +1 -1
  20. package/docs/disaster-recovery.md +1 -1
  21. package/docs/document-extraction.md +140 -0
  22. package/docs/evaluations.md +1 -1
  23. package/docs/execution-timeline.md +30 -0
  24. package/docs/guardrails.md +53 -0
  25. package/docs/history/130-cyclic-workflows-primitive-review.md +293 -0
  26. package/docs/history/132-document-ingestion-primitive-review.md +124 -0
  27. package/docs/history/README.md +2 -0
  28. package/docs/history/migrate-to-0.12.0.md +57 -0
  29. package/docs/history/retire-node-runtime.md +59 -0
  30. package/docs/hooks.md +1 -1
  31. package/docs/host-compositions.md +2 -2
  32. package/docs/host-security.md +1 -1
  33. package/docs/index.md +30 -25
  34. package/docs/live-testing.md +1 -1
  35. package/docs/mcp-tools.md +2 -2
  36. package/docs/messaging-channels.md +1 -1
  37. package/docs/migrate-to-0.7.md +1 -1
  38. package/docs/migrate-to-0.8.md +2 -2
  39. package/docs/migrate-to-0.9.md +1 -1
  40. package/docs/migration.md +2 -2
  41. package/docs/model-registry.md +1 -1
  42. package/docs/model-routing.md +21 -0
  43. package/docs/multi-agent-patterns.md +77 -4
  44. package/docs/obscura.md +1 -1
  45. package/docs/openapi-tools.md +1 -1
  46. package/docs/operations.md +2 -2
  47. package/docs/peer-dependencies.md +18 -17
  48. package/docs/performance.md +32 -32
  49. package/docs/prompt-registry.md +1 -1
  50. package/docs/provider-packages.md +23 -21
  51. package/docs/providers/decisions.md +134 -0
  52. package/docs/providers/laya.md +116 -0
  53. package/docs/providers/typesafe.md +148 -0
  54. package/docs/rag.md +17 -5
  55. package/docs/release-and-install.md +119 -113
  56. package/docs/run-bundle.md +4 -0
  57. package/docs/runs-and-usage.md +1 -1
  58. package/docs/signal-channel.md +1 -1
  59. package/docs/sqlite-persistence.md +5 -5
  60. package/docs/synapta-integration-review.md +238 -0
  61. package/docs/telegram-channel.md +1 -1
  62. package/docs/testing.md +33 -14
  63. package/docs/wiki.md +12 -11
  64. package/docs/work-sandbox.md +2 -0
  65. package/docs/work-tools.md +3 -3
  66. package/docs/workflows.md +213 -11
  67. package/package.json +22 -21
  68. package/templates/business-worker/README.md.tmpl +3 -3
  69. package/templates/business-worker/package.json.tmpl +4 -5
  70. package/templates/business-worker/src/tests/agent.test.ts.tmpl +1 -1
  71. package/templates/business-worker/tsconfig.json.tmpl +2 -1
  72. package/templates/deep-research/README.md.tmpl +4 -4
  73. package/templates/deep-research/package.json.tmpl +4 -5
  74. package/templates/deep-research/src/tests/research.test.ts.tmpl +1 -1
  75. package/templates/deep-research/tsconfig.json.tmpl +2 -1
  76. package/templates/init/README.md.tmpl +3 -3
  77. package/templates/init/optional/evals-example.ts.tmpl +1 -1
  78. package/templates/init/optional/workflows-example.ts.tmpl +1 -1
  79. package/templates/init/package.json.tmpl +4 -5
  80. package/templates/init/src/tests/agent.test.ts.tmpl +1 -1
  81. package/templates/init/tsconfig.json.tmpl +2 -1
  82. package/templates/personal-assistant/README.md.tmpl +3 -3
  83. package/templates/personal-assistant/package.json.tmpl +4 -5
  84. package/templates/personal-assistant/src/tests/agent.test.ts.tmpl +1 -1
  85. package/templates/personal-assistant/tsconfig.json.tmpl +2 -1
  86. package/templates/provider/README.md.tmpl +2 -2
  87. package/templates/provider/package.json.tmpl +3 -4
  88. package/templates/provider/src/tests/provider.test.ts.tmpl +1 -1
  89. package/templates/provider/tsconfig.json.tmpl +1 -1
package/docs/core.md CHANGED
@@ -5,20 +5,19 @@ The `@arnilo/prism-core` family package unifies Prism's privileged runtime, sess
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install @arnilo/prism @arnilo/prism-core
8
+ bun add @arnilo/prism @arnilo/prism-core
9
9
  ```
10
10
 
11
- For database persistence and distributed event streams, install the required optional peer dependencies:
11
+ SQLite persistence uses the Bun runtime's built-in `bun:sqlite`. No package to install.
12
12
 
13
- ```bash
14
- # SQLite sessions & prompt storage
15
- npm install better-sqlite3
13
+ For PostgreSQL and distributed event streams, install the optional peer:
16
14
 
15
+ ```bash
17
16
  # PostgreSQL sessions, enterprise persistence & prompt storage
18
- npm install pg
17
+ bun add pg
19
18
 
20
19
  # NATS JetStream distributed event source
21
- npm install @nats-io/jetstream @nats-io/transport-node
20
+ bun add @nats-io/jetstream @nats-io/transport-node
22
21
  ```
23
22
 
24
23
  Every peer below is optional and fails closed at first use; the [optional peer dependencies](peer-dependencies.md) matrix lists the exact ranges, pins, and which of them reach the network.
@@ -31,12 +30,12 @@ Every peer below is optional and fails closed at first use; the [optional peer d
31
30
  | `@arnilo/prism-core/runtime/supervisor` | Agent-to-Agent (A2A) protocol server, client, event source, and multi-agent supervisor | — |
32
31
  | `@arnilo/prism-core/runtime/workflows` | Multi-step DAG workflow coordinator, saga recovery, checkpoints, and loop nodes | — |
33
32
  | `@arnilo/prism-core/sessions/codecs` | Checkpoint, cursor, feedback, and search serialization codecs | — |
34
- | `@arnilo/prism-core/sessions/sqlite` | SQLite session store, leases, lifecycle, and schema migrations | `better-sqlite3` |
33
+ | `@arnilo/prism-core/sessions/sqlite` | SQLite session store, leases, lifecycle, and schema migrations | — (`bun:sqlite`, built in) |
35
34
  | `@arnilo/prism-core/sessions/postgres` | PostgreSQL session store, event source, and migrations | `pg` |
36
35
  | `@arnilo/prism-core/sessions/nats` | NATS JetStream distributed event source | `@nats-io/jetstream`, `@nats-io/transport-node` |
37
36
  | `@arnilo/prism-core/governance/policy` | Capability admission, tool execution approvals, audit log exporter, and OPA evaluator | — |
38
37
  | `@arnilo/prism-core/governance/evals` | Offline evaluation runs, scorers, judges, threshold assertions, and trace curation | — |
39
- | `@arnilo/prism-core/governance/prompts` | Versioned prompt registry, promotion gating, rollback, and storage | `better-sqlite3`, `pg` |
38
+ | `@arnilo/prism-core/governance/prompts` | Versioned prompt registry, promotion gating, rollback, and storage | `pg` (SQLite is `bun:sqlite`) |
40
39
  | `@arnilo/prism-core/governance/model-router` | Cost- and latency-aware model routing, token reservations, and failover | — |
41
40
  | `@arnilo/prism-core/governance/observability` | OpenTelemetry instrumentation and event tracing | `@opentelemetry/api` |
42
41
  | `@arnilo/prism-core/credentials/node` | Keyring-backed encrypted credential store, scrypt envelope encryption, OAuth2 PKCE providers, and OIDC identity verification | `@napi-rs/keyring` (bundled) |
@@ -81,7 +80,7 @@ const validator = createJsonSchemaToolArgumentValidator();
81
80
 
82
81
  ## Security & Import Isolation
83
82
 
84
- - Subpaths never load database drivers (`pg`, `better-sqlite3`) unless the specific database subpath is imported.
83
+ - Subpaths never load the `pg` driver unless the specific database subpath is imported. SQLite uses the runtime's `bun:sqlite`, not a package.
85
84
  - All database and network drivers fail closed with clear actionable error messages when peers are omitted.
86
85
  - Root `@arnilo/prism` remains dependency-free contracts and CLI runner.
87
86
  - Messaging channels are `@arnilo/prism-channels` (`/telegram`, `/signal`), not a `@arnilo/prism-core` subpath.
@@ -17,7 +17,7 @@ Use it when iterating on prompts in a local Prism host and you want a inspectabl
17
17
  ### Quickstart — `prism dev` (plan 040 Task 4)
18
18
 
19
19
  ```bash
20
- npm install --save-dev @arnilo/prism-coding-tools/dev
20
+ bun add --dev @arnilo/prism-coding-tools/dev
21
21
  cd my-agent && npm run dev # → prism dev → http://127.0.0.1:4311
22
22
  ```
23
23
 
@@ -29,7 +29,7 @@ This page is the operator runbook for backup, restore, migration rollback, point
29
29
  ```sh
30
30
  # Protected drill (standard tools only, orchestrated by the script):
31
31
  PRISM_PITR_URL=postgresql://user:***@localhost:55436/postgres \
32
- node scripts/phase27-dr.test.mjs \
32
+ bun scripts/phase27-dr.test.mjs \
33
33
  --source "$PRISM_TEST_POSTGRES_URL" \
34
34
  --target postgresql://user:***@localhost:55432/prism_dr_target \
35
35
  --confirm-target prism_dr_restore
@@ -0,0 +1,140 @@
1
+ # Document extraction (`@arnilo/prism-work/document-extraction`)
2
+
3
+ > **Optional peer install:** `bun add @arnilo/prism-work @firecrawl/anydoc@0.2.4`. Default hosts skip it. See [Optional peer dependencies](peer-dependencies.md).
4
+
5
+ ## What it does
6
+
7
+ Converts in-memory Office, OpenDocument, RTF, EPUB, CSV, and text-PDF bytes to Markdown with a host-selected optional peer. Scanned PDFs throw `NeedsOcrError` unless the host injects a Docling runner. This subpath does not call Firecrawl Parse and does not read an API key.
8
+
9
+ ## When to use it
10
+
11
+ Use when a host needs structured Markdown from a document byte buffer and can install `@firecrawl/anydoc`. Do not use it for the coding read tool (`document-reader`), for editable OOXML (`documents`), or for decimal-safe sheet ingest (`sheets`). Image OCR runs only when the host passes a runner and sets `ocr: true` on that call.
12
+
13
+ ## Inputs / request
14
+
15
+ `createDocumentExtractor(options?)` loads the peer once. `extract({ bytes, filename?, signal? })` converts one buffer.
16
+
17
+ | Option | Meaning | Default | Ceiling |
18
+ | --- | --- | --- | --- |
19
+ | `maxBytes` | Input cap. Oversize buffers refuse before the native call | 32 MiB | 512 MiB |
20
+ | `maxTextBytes` | Markdown cap. Oversize results refuse; they are not truncated | 2 MiB | 64 MiB |
21
+ | `maxPages` | OCR page cap. Checked before the runner when anydoc reported a count, and again on the worker result | 32 | 256 |
22
+ | `ocrTimeoutMs` | Wall time the host runner must enforce. This module cannot kill a process | 120 s | 600 s |
23
+ | `maxStderrBytes` | Worker stderr cap. Oversize or truncated stderr fails with no Markdown | 1 MiB | 4 MiB |
24
+ | `ocr` | Host runner. Absent: scanned PDFs throw `NeedsOcrError` | none | |
25
+
26
+ Format comes from content. `filename` is used only when content has no signature and its basename ends with `.csv`. A path in `filename` is not opened. `extract({ ocr: true })` selects image OCR only; a text PDF that anydoc converts still skips the runner.
27
+
28
+ ## Outputs / response / events
29
+
30
+ Success: `{ markdown, format, ocrUsed }`. `pages` is set only when Docling reported it. `ocrUsed` is false for anydoc. Markdown is untrusted data; label it at the wiki or RAG boundary that stores it.
31
+
32
+ Errors: `DocumentExtractionError` (`code: "ERR_PRISM_DOCUMENT_EXTRACTION"`) with `reason`:
33
+
34
+ | `reason` | When |
35
+ | --- | --- |
36
+ | `needsOcr` | `NeedsOcrError`. `pages` (1-indexed) and `pageCount`. No Markdown |
37
+ | `unsupported` | No signature, and the name is not CSV |
38
+ | `encrypted` / `malformed` / `resourceLimit` / `missingPart` / `io` | Peer refused. No fallback |
39
+ | `hosted` | Peer reported a hosted-OCR failure. This API never requests hosted OCR |
40
+ | `missingPeer` | `@firecrawl/anydoc` is not installed, at creation |
41
+ | `inputLimit` / `outputLimit` | Cap exceeded. No text returned |
42
+ | `busy` | One conversion is in flight and another is already waiting |
43
+
44
+ Invalid caps throw `RangeError` at creation. An aborted `signal` throws `AbortError`.
45
+
46
+ ## Request/response example
47
+
48
+ ```json
49
+ {
50
+ "format": "docx",
51
+ "ocrUsed": false,
52
+ "markdown": "Hello Prism DOCX\n"
53
+ }
54
+ ```
55
+
56
+ ## Implementation example
57
+
58
+ ```ts
59
+ import { createDocumentExtractor, NeedsOcrError } from "@arnilo/prism-work/document-extraction";
60
+
61
+ const extractor = await createDocumentExtractor();
62
+ try {
63
+ const { markdown, format } = await extractor.extract({ bytes, filename, signal });
64
+ // markdown is untrusted. Do not grant it tool authority.
65
+ } catch (error) {
66
+ if (error instanceof NeedsOcrError) {
67
+ // pages and pageCount only. No partial Markdown.
68
+ }
69
+ throw error;
70
+ }
71
+ ```
72
+
73
+ Host OCR runner. Prism does not spawn this. Kill the child on abort; do not pass the process env through.
74
+
75
+ ```ts
76
+ import { spawn } from "node:child_process";
77
+ import { createDocumentExtractor, doclingOcrArgs, type OcrWorkerResult } from "@arnilo/prism-work/document-extraction";
78
+
79
+ const extractor = await createDocumentExtractor({
80
+ ocr: (request) =>
81
+ new Promise<OcrWorkerResult>((resolve, reject) => {
82
+ const child = spawn(python, doclingOcrArgs(request, artifacts), {
83
+ env: { PATH: process.env.PATH ?? "", HF_HUB_OFFLINE: "1", TRANSFORMERS_OFFLINE: "1", HF_HUB_DISABLE_TELEMETRY: "1" },
84
+ stdio: ["pipe", "pipe", "pipe"],
85
+ });
86
+ const stdout: Buffer[] = [];
87
+ const stderr: Buffer[] = [];
88
+ const kill = () => child.kill("SIGKILL");
89
+ request.signal?.addEventListener("abort", kill, { once: true });
90
+ child.stdout.on("data", (chunk: Buffer) => stdout.push(chunk));
91
+ child.stderr.on("data", (chunk: Buffer) => stderr.push(chunk));
92
+ child.on("error", reject);
93
+ child.on("close", (exitCode) => {
94
+ resolve({ exitCode, stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr), aborted: request.signal?.aborted });
95
+ });
96
+ child.stdin.end(request.bytes);
97
+ }),
98
+ });
99
+ const scanned = await extractor.extract({ bytes: pdfBytes });
100
+ const image = await extractor.extract({ bytes: pngBytes, ocr: true });
101
+ ```
102
+
103
+ Wiki and RAG stay in the memory package. This adapter does not import it. Set `ocrImages` on both sides or images keep the wiki stub.
104
+
105
+ ```ts
106
+ import { createDocumentExtractor, createDocumentIngest } from "@arnilo/prism-work/document-extraction";
107
+ import { createWikiExtension } from "@arnilo/prism-memory/wiki";
108
+ import { replaceDocument } from "@arnilo/prism-memory/rag";
109
+
110
+ const extractor = await createDocumentExtractor({ ocr });
111
+ const ingest = createDocumentIngest(extractor, { ocrImages: true });
112
+ const wiki = createWikiExtension({ extractDocument: ingest.extractDocument, ocrImages: ingest.ocrImages });
113
+ await replaceDocument({ uri, loader, parser: ingest.parser, store, scope, sourceId });
114
+ // ingest.stats() is counts and milliseconds only. Markdown is untrusted at the wiki and RAG boundary.
115
+ ```
116
+
117
+ ## Extension and configuration notes
118
+
119
+ - Peer version is pinned to `0.2.4`. The published wrapper accepts a third `options` argument that can upload the whole file. This module never passes it.
120
+ - Docling is not an npm dependency. The package ships `docling/ocr.py`. The host pins `docling==2.130.0` and CPU torch (`https://download.pytorch.org/whl/cpu`; default PyPI torch pulls CUDA wheels), prefetches models (~1.4 GiB, about 150 s), and injects a runner. `doclingOcrArgs(request, artifactsPath)` builds the helper argv. Bytes go on stdin. A URL artifact path is rejected.
121
+ - The runner must use a scrubbed env, close stdin after the bytes, kill the worker on abort and on stdout/stderr caps, and not leave the process running. `WorkSandbox.execFile` has no stdin, so it is not this runner. One OCR job in flight per extractor, same slot as anydoc. A third call fails with `busy`.
122
+ - The helper accepts only `ConversionStatus.SUCCESS`. Partial, timeout, missing models, and over-page results are errors. Empty Markdown with a page count is valid. PDF and PNG Markdown are not byte-identical.
123
+ - Native packages: darwin x64/arm64, linux x64/arm64 gnu and musl, win32 x64. No FreeBSD, Android, riscv, or Windows ARM build.
124
+ - Importing `/documents`, `/sheets`, `/connectors`, or `/document-reader` does not load this peer.
125
+
126
+ ## Security and performance notes
127
+
128
+ - Local conversion only. No `process.env` read, no hosted OCR, no process spawn, no macro execution, no filename filesystem read.
129
+ - Upstream error text is not copied into `Error.message`. A peer message can contain document bytes.
130
+ - One conversion in flight per extractor, plus one waiter. A third call fails with `busy`. Two extractors can run at once; share one if memory matters.
131
+ - `AbortSignal` is checked before the native call and after it returns. It does not preempt the native call. The slot stays held until that call returns, then the result is discarded if the signal aborted.
132
+ - Caps are checked on the input length before native code runs, and on UTF-8 output length after. Defaults match the document-reader defaults. Raising them toward the hard ceiling is host-owned: native work on a large file cannot be cancelled.
133
+ - Small anydoc fixtures (under 10 KiB) on Bun 1.4.2 finished in a few milliseconds with process VmHWM under 64 MiB. The regression gate is 500 ms and 128 MiB for that class of input.
134
+ - After model prefetch, one scanned page was about 5 s cold and 2 s warm, with VmHWM about 1.8–2 GiB. Two converters in one process reached about 3 GiB. Keep one worker. Budget for inputs under 100 KiB: under 30 s and 4 GiB VmHWM. Prefetch itself is host-owned and is not part of that budget.
135
+
136
+ ## Related APIs
137
+
138
+ - [Document reader](document-reader.md): literal text for the coding read tool, including an opt-in Mistral OCR parser. Separate from this subpath.
139
+ - [Documents, spreadsheets, and presentations](documents.md): editable OOXML model. Not a Markdown converter.
140
+ - [Optional peer dependencies](peer-dependencies.md): install line for `@firecrawl/anydoc`.
@@ -323,7 +323,7 @@ if (records.some((record) => record.status !== "scored" || record.score !== 1))
323
323
 
324
324
  ```sh
325
325
  # Protected run (requires a disposable PostgreSQL instance):
326
- PRISM_TEST_POSTGRES_URL=postgresql://... node --test scripts/phase27-erp-journey.test.mjs
326
+ PRISM_TEST_POSTGRES_URL=postgresql://... bun test scripts/phase27-erp-journey.test.mjs
327
327
  ```
328
328
 
329
329
  The journey reuses the two-replica failover worker (`scripts/phase27-ha-worker.mjs`) and asserts the comprehensive DR drill evidence (`docs/_evidence/phase27-dr-evidence.json`) is present and not stale. Local substitutes are labelled in the journey evidence and never converted into production claims: an in-memory WORM/SIEM sink (host owns the immutable store in production), in-memory saga checkpoint/lease stores (saga durability is proven in its own suite), and a logical pg-client backup/restore of the ERP tables (comprehensive PITR is in the DR drill evidence). Passing this protected journey **does not** satisfy the 0.3.0 live-service matrix.
@@ -211,6 +211,36 @@ const timeline = projectWorkflowTimeline(workflowEvents, {
211
211
 
212
212
  See runnable host demo in `examples/execution-timeline.ts` for offline workflow timeline projection, cockpit summary, and Mermaid diagram export.
213
213
 
214
+ ### Host step loops and ledger persistence
215
+
216
+ When the host owns the step loop (one business step per `session.run()`), correlate its own
217
+ `stepId` / `actionId` / `attemptId` with the Prism `sessionId` / `runId` in the host ledger envelope:
218
+ Prism's events already carry those ids, so the timeline needs no extra correlation field. Project one
219
+ incremental folder per step:
220
+
221
+ ```ts
222
+ const subscription = session.subscribe(); // before run — see ordering note below
223
+ const folder = createTimelineFolder({ content: "metadata" });
224
+ const consume = (async () => {
225
+ for await (const event of subscription) folder.push(event);
226
+ })();
227
+ const result = await session.run(prompt); // result.sessionId / result.runId
228
+ await consume;
229
+ const timeline = folder.snapshot(); // per-step frozen projection
230
+ ```
231
+
232
+ `subscribe()` registers synchronously, while `session.run()` emits `agent_started` before it returns —
233
+ so start the subscription *before* the run, or the folder misses the run identity and `timeline.runId`
234
+ stays empty. Persist the frozen timeline (default `metadata` policy) into the host ledger as
235
+ newline-delimited JSON. Keep any legacy prose trace as display-only data beside the structural record:
236
+ budgets, turns and stop reasons come from the typed fields, never from parsing prose. External host
237
+ evidence (commits, verifications) is host authority, not a Prism effect — attach it to the ledger
238
+ envelope with an explicit host marker instead of adding a timeline step.
239
+
240
+ Runnable: [`examples/host-step-loop-timeline.ts`](../examples/host-step-loop-timeline.ts) — eight steps,
241
+ one tool round persisting a `sha256:` argument hash instead of raw args, and external commit evidence
242
+ on the last step.
243
+
214
244
  ### Stop reasons
215
245
 
216
246
  Run-level `stopReason` mirrors `agent_finished.finishReason` when the loop stopped on a ceiling or a host turn policy (`"host_policy"`); `status` reads `finished:<stopReason>` for those runs and `succeeded` for a natural end. `stopDetail` carries the host's `turnPolicy.stop` reason, bounded to 256 bytes and redacted at the runtime boundary. See [Runs and usage ledger § Clean stops and stop reasons](runs-and-usage.md#clean-stops-and-stop-reasons).
@@ -149,6 +149,59 @@ An explicit citation is `[evidence:<ref>]`, immediately after its claim (within
149
149
 
150
150
  With `onViolation: "block"`, the standard `GuardrailError` has `reason: "claim_ungrounded"` and bounded metadata `{ claim, contentIndex, start, end }` — never a full response body. `"flag"` returns `action: "allow"` plus that same metadata and `violation: true`, so the response stays visible while the normal `guardrail_decision` event and run ledger preserve the flag. Strict mode treats every standalone number (including dates, percentages, and versions) as a claim; use the option only where that law is wanted.
151
151
 
152
+ ## Field-evidence provenance
153
+
154
+ `createFieldEvidenceGuardrail(options)` is a deterministic `tool_input` guardrail for one host-selected
155
+ tool. It verifies that each required field carries a typed source reference and that the claimed
156
+ value's revision and normalized value match the host's evidence set. It performs no retrieval, calls
157
+ no model, and grants no authority: retrieval, grants, digest freeze, and commit-time validation stay
158
+ with the host.
159
+
160
+ ```ts
161
+ import { createFieldEvidenceGuardrail } from "@arnilo/prism";
162
+
163
+ const provenance = createFieldEvidenceGuardrail({
164
+ toolName: "synapta:proposal",
165
+ required: ["amount", "vendor.id"],
166
+ evidence: ({ toolCallId }) => hostLedger.evidenceFor(toolCallId), // current rows only
167
+ normalize: (value) => (typeof value === "string" ? Number(value.replace(/[$,]/g, "")) : value),
168
+ });
169
+
170
+ const agent = createAgent({ model, provider, guardrails: { toolInput: [provenance] } });
171
+ ```
172
+
173
+ A required field must hold a provenance envelope:
174
+
175
+ ```json
176
+ {
177
+ "amount": { "value": 1250, "source": "tool:invoice_fetch", "path": "invoice.total", "revision": 7 }
178
+ }
179
+ ```
180
+
181
+ Evidence rows are `{ source, path, value, revision? }` — the source's current value. `source` and
182
+ `path` are matched literally; `revision` must match exactly (a claim without a revision cannot match
183
+ an evidence row that has one, and vice versa). After `normalize`, values compare exactly
184
+ (`Object.is`), so a normalizer that returns objects never matches. `required` supports dot paths
185
+ (`vendor.id`), bounded to 256 entries and 256 chars per path; evidence is capped at 4096 rows per call
186
+ and a larger set fails closed instead of truncating.
187
+
188
+ | Violation | Meaning |
189
+ | --- | --- |
190
+ | `missing_field` | The required path is absent from the call arguments. |
191
+ | `malformed_claim` | The path holds a plain value or an incomplete envelope (missing `value`, `source`, or `path`; invalid `revision`). |
192
+ | `missing_evidence` | The host evidence source returned no valid rows for this call. |
193
+ | `unknown_source` | No evidence row matches the claimed `source` + `path` (invented field or wrong object). |
194
+ | `stale_revision` | Matching rows exist but none carries the claimed revision. |
195
+ | `value_mismatch` | A fresh row exists but its normalized value differs. |
196
+ | `evidence_over_limit` | More than 4096 valid evidence rows; fails closed instead of truncating. |
197
+
198
+ A violation blocks the call before lookup, permission, validation, or execution: the decision is
199
+ `{ action: "block", reason: "field_evidence", metadata: { field, violation } }`, the blocked
200
+ `ToolResult` stays neutral, and the `guardrail_decision` event carries only the field path and code —
201
+ never the claimed value, evidence value, or any commit/approval semantics. Calls to other tools are
202
+ allowed untouched. The host decides which fields require evidence and supplies it per call; the
203
+ guardrail never widens, rewrites, or commits anything.
204
+
152
205
  ## Extension and configuration notes
153
206
 
154
207
  Guardrails are callbacks supplied by the host. Prism does not discover, load, retry, or persist callback code. `createSecureAgent()` keeps configured guardrails and only appends run-level checks; it never lets a run remove secure defaults. Custom loops receive guarded `LoopContext.generate()` and `LoopContext.dispatchToolCall()`; host code that directly calls a provider or `ToolDefinition.execute()` is outside the runtime boundary. Guardrail packs follow the same rule: they are host-supplied config, compiled in memory per session, never discovered from disk. Their compiled identity and pack-owned state persist only inside an opt-in durable checkpoint (`persistSessionState`, see above) and nowhere else.