@arnilo/prism 0.5.6 → 0.6.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 (64) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +10 -10
  3. package/dist/agent-approval.js +7 -6
  4. package/dist/agent-loops.js +51 -12
  5. package/dist/agent-session/session.d.ts +1 -0
  6. package/dist/agent-session/session.js +20 -2
  7. package/dist/agent-tool-dispatch.js +5 -4
  8. package/dist/content.d.ts +3 -16
  9. package/dist/content.js +9 -99
  10. package/dist/context-budget.d.ts +12 -1
  11. package/dist/context-budget.js +42 -19
  12. package/dist/contracts-core/agent.d.ts +11 -0
  13. package/dist/contracts-core/agent.js +4 -1
  14. package/dist/index.d.ts +4 -4
  15. package/dist/index.js +3 -3
  16. package/dist/input.d.ts +6 -0
  17. package/dist/input.js +12 -1
  18. package/dist/media-types.d.ts +34 -0
  19. package/dist/media-types.js +158 -0
  20. package/dist/pinned-fetch.d.ts +2 -2
  21. package/dist/pinned-fetch.js +11 -12
  22. package/dist/redaction.js +74 -1
  23. package/dist/session-stores.d.ts +11 -0
  24. package/dist/session-stores.js +23 -8
  25. package/docs/acp.md +1 -1
  26. package/docs/ag-ui.md +4 -2
  27. package/docs/agent-events.md +2 -0
  28. package/docs/agent-loops.md +1 -1
  29. package/docs/agent-session-runtime.md +3 -1
  30. package/docs/browser-automation.md +5 -2
  31. package/docs/contributing.md +37 -0
  32. package/docs/core.md +2 -0
  33. package/docs/document-reader.md +2 -0
  34. package/docs/documents.md +1 -1
  35. package/docs/graft.md +3 -1
  36. package/docs/history/release-handoffs.md +33 -0
  37. package/docs/host-security.md +2 -2
  38. package/docs/index.md +27 -14
  39. package/docs/input-and-prompt-assembly.md +4 -4
  40. package/docs/language-intelligence.md +1 -1
  41. package/docs/migrate-to-0.5.md +7 -2
  42. package/docs/migrate-to-0.6.md +89 -0
  43. package/docs/migration.md +30 -0
  44. package/docs/model-registry.md +1 -1
  45. package/docs/multimodal-content.md +1 -1
  46. package/docs/obscura.md +3 -1
  47. package/docs/options-index.md +286 -0
  48. package/docs/peer-dependencies.md +94 -0
  49. package/docs/performance.md +34 -2
  50. package/docs/ponytail.md +2 -0
  51. package/docs/postgres-persistence.md +3 -1
  52. package/docs/provider-conformance.md +1 -1
  53. package/docs/provider-packages.md +21 -21
  54. package/docs/provider-primitives.md +2 -1
  55. package/docs/providers/ai-sdk.md +5 -2
  56. package/docs/public-contracts.md +2 -2
  57. package/docs/release-and-install.md +75 -55
  58. package/docs/server.md +1 -1
  59. package/docs/session-stores.md +3 -1
  60. package/docs/sqlite-persistence.md +2 -0
  61. package/docs/testing.md +38 -0
  62. package/docs/tools.md +1 -1
  63. package/docs/wiki.md +1 -1
  64. package/package.json +5 -5
package/dist/redaction.js CHANGED
@@ -3,6 +3,18 @@ const REDACTED = "[REDACTED]";
3
3
  // Depth bound matching agent-run-state.ts; hostile deep structures yield a placeholder
4
4
  // instead of a stack overflow.
5
5
  const MAX_REDACT_DEPTH = 32;
6
+ // Plan 070 Task 9: single-pass fast path. One left-to-right alternation replaces every
7
+ // occurrence of every needle in a single scan, instead of one split/join pass per needle.
8
+ // It is only equivalent to the ordered reduce below when no needle occurrence can overlap
9
+ // another needle's occurrence or a produced placeholder, so `singlePassMatcher` returns
10
+ // null for such sets and the loop stays authoritative. Below this length the set check
11
+ // costs more than the passes it saves (measured crossover ~4 KB, so the fast path only
12
+ // engages with a wide margin).
13
+ const SINGLE_PASS_MIN_CHARS = 16 * 1024;
14
+ // ponytail: bound the fast path to this needle count. The set check is O(k²) and the
15
+ // alternation compile grows with k, so the loop is no slower beyond it. Raise if a host
16
+ // redacts with much larger secret sets.
17
+ const SINGLE_PASS_MAX_NEEDLES = 32;
6
18
  export function createSecretRedactor(secrets) {
7
19
  return { redact: (value) => redactSecrets(value, secrets) };
8
20
  }
@@ -35,7 +47,18 @@ export function redactSecrets(value, secrets) {
35
47
  const needles = secrets.filter((secret) => Boolean(secret));
36
48
  if (needles.length === 0)
37
49
  return value;
38
- const redactString = (text) => needles.reduce((current, secret) => current.split(secret).join(REDACTED), text);
50
+ // Decided lazily on the first large string, and only once per call: a redaction of many
51
+ // small strings never pays the set check and never regresses against the loop.
52
+ let singlePass;
53
+ const redactString = (text) => {
54
+ if (text.length >= SINGLE_PASS_MIN_CHARS) {
55
+ if (singlePass === undefined)
56
+ singlePass = singlePassMatcher(needles);
57
+ if (singlePass)
58
+ return text.replace(singlePass, REDACTED);
59
+ }
60
+ return needles.reduce((current, secret) => current.split(secret).join(REDACTED), text);
61
+ };
39
62
  const redactKey = (key) => {
40
63
  if (typeof key === "string")
41
64
  return redactString(key);
@@ -90,6 +113,56 @@ export function redactSecrets(value, secrets) {
90
113
  };
91
114
  return redact(value);
92
115
  }
116
+ function escapeRegExpLiteral(literal) {
117
+ return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
118
+ }
119
+ /**
120
+ * Compiled single-pass matcher for `needles`, or null when the set is not provably
121
+ * equivalent to the ordered reduce/split/join in `redactSecrets`. Equivalence holds when
122
+ * no needle occurrence can overlap another needle's occurrence (overlap would make the
123
+ * result depend on which needle is mentioned first rather than on position) and no needle
124
+ * can occur inside or across the edges of a produced "[REDACTED]" placeholder (a later
125
+ * pass would then redact text the single scan never sees). Both checks are conservative:
126
+ * a false negative only costs the fast path.
127
+ */
128
+ function singlePassMatcher(needles) {
129
+ if (needles.length < 2 || needles.length > SINGLE_PASS_MAX_NEEDLES)
130
+ return null;
131
+ for (const needle of needles) {
132
+ if (needleTouchesPlaceholder(needle))
133
+ return null;
134
+ }
135
+ for (const left of needles) {
136
+ for (const right of needles) {
137
+ if (left !== right && needlesOverlap(left, right))
138
+ return null;
139
+ }
140
+ }
141
+ return new RegExp(needles.map(escapeRegExpLiteral).join("|"), "g");
142
+ }
143
+ /** A placeholder-relative occurrence: needle inside "[REDACTED]", or a needle prefix equal
144
+ * to a placeholder suffix / needle suffix equal to a placeholder prefix (a match spanning
145
+ * the placeholder's edge that the ordered passes would create). */
146
+ function needleTouchesPlaceholder(needle) {
147
+ if (REDACTED.includes(needle))
148
+ return true;
149
+ for (let n = 1; n < needle.length; n += 1) {
150
+ if (REDACTED.endsWith(needle.slice(0, n)) || REDACTED.startsWith(needle.slice(n)))
151
+ return true;
152
+ }
153
+ return false;
154
+ }
155
+ /** One occurrence of `left` overlapping one of `right`: containment either way, or a proper
156
+ * suffix of `left` equal to a proper prefix of `right` (callers check both directions). */
157
+ function needlesOverlap(left, right) {
158
+ if (right.includes(left))
159
+ return true;
160
+ for (let n = 1; n < left.length && n < right.length; n += 1) {
161
+ if (right.startsWith(left.slice(left.length - n)))
162
+ return true;
163
+ }
164
+ return false;
165
+ }
93
166
  export function errorToErrorInfo(error, secrets = []) {
94
167
  const code = readErrorCode(error);
95
168
  const retry = readRetryAfterMs(error);
@@ -28,5 +28,16 @@ export type MemorySessionSearchMode = "linear" | "unsupported";
28
28
  export interface CreateMemorySessionStoreOptions {
29
29
  /** Default `"linear"`: capped in-process scan. `"unsupported"`: typed throw. */
30
30
  readonly sessionSearchMode?: MemorySessionSearchMode;
31
+ /**
32
+ * Optional overrides for the capped in-process scan. Hosts with a small session set can
33
+ * raise the caps (bounded by the contract `HARD_MAX_SESSION_SEARCH_LINEAR_*` values);
34
+ * defaults are the contract `DEFAULT_MAX_SESSION_SEARCH_LINEAR_*` caps. Values below 1
35
+ * or above the hard cap fail store construction closed with a `TypeError`.
36
+ */
37
+ readonly search?: {
38
+ readonly maxLinearSessions?: number;
39
+ readonly maxLinearEntries?: number;
40
+ readonly maxLinearBytes?: number;
41
+ };
31
42
  }
32
43
  export declare function createMemorySessionStore(initialEntries?: readonly SessionEntry[], options?: CreateMemorySessionStoreOptions): SessionStore;
@@ -1,4 +1,4 @@
1
- import { DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, resolveSessionSearchQuery, SESSION_APPEND_CONFLICT_CODE, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SessionAppendConflictError, SessionSearchUnsupportedError, } from "./contracts.js";
1
+ import { DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS, resolveSessionSearchQuery, SESSION_APPEND_CONFLICT_CODE, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SessionAppendConflictError, SessionSearchUnsupportedError, } from "./contracts.js";
2
2
  import { createId } from "./ids.js";
3
3
  export function createSessionEntry(options) {
4
4
  const { createId, now, ...entry } = options;
@@ -96,12 +96,27 @@ function rebuildSessionContextCore(entries, options = {}) {
96
96
  }
97
97
  return { leafId: branch.at(-1)?.id, entries: branch, messages, summaries };
98
98
  }
99
+ function resolveLinearSearchCaps(search) {
100
+ return {
101
+ sessions: assertLinearCap(search?.maxLinearSessions, "maxLinearSessions", DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS),
102
+ entries: assertLinearCap(search?.maxLinearEntries, "maxLinearEntries", DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES),
103
+ bytes: assertLinearCap(search?.maxLinearBytes, "maxLinearBytes", DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES),
104
+ };
105
+ }
106
+ function assertLinearCap(value, name, fallback, hardMax) {
107
+ const cap = value ?? fallback;
108
+ if (!Number.isSafeInteger(cap) || cap < 1 || cap > hardMax) {
109
+ throw new TypeError(`CreateMemorySessionStoreOptions.search.${name} must be a safe integer from 1 to ${hardMax}`);
110
+ }
111
+ return cap;
112
+ }
99
113
  export function createMemorySessionStore(initialEntries = [], options = {}) {
100
114
  const byId = new Map();
101
115
  const bySession = new Map();
102
116
  const leafBySession = new Map();
103
117
  const idempotencySeen = new Set();
104
118
  const mode = options.sessionSearchMode ?? "linear";
119
+ const searchCaps = resolveLinearSearchCaps(options.search);
105
120
  for (const entry of initialEntries)
106
121
  add(entry);
107
122
  return {
@@ -118,7 +133,7 @@ export function createMemorySessionStore(initialEntries = [], options = {}) {
118
133
  async searchSessions(query) {
119
134
  if (mode === "unsupported")
120
135
  throw new SessionSearchUnsupportedError();
121
- return searchMemorySessionsLinear(bySession, leafBySession, query);
136
+ return searchMemorySessionsLinear(bySession, leafBySession, query, searchCaps);
122
137
  },
123
138
  };
124
139
  function add(entry, options) {
@@ -160,7 +175,7 @@ export function createMemorySessionStore(initialEntries = [], options = {}) {
160
175
  leafBySession.set(entry.sessionId, entry.id);
161
176
  }
162
177
  }
163
- function searchMemorySessionsLinear(bySession, leafBySession, query) {
178
+ function searchMemorySessionsLinear(bySession, leafBySession, query, caps) {
164
179
  const q = resolveSessionSearchQuery(query);
165
180
  q.signal?.throwIfAborted();
166
181
  let sessionsScanned = 0;
@@ -168,11 +183,11 @@ function searchMemorySessionsLinear(bySession, leafBySession, query) {
168
183
  let bytesScanned = 0;
169
184
  const matches = [];
170
185
  for (const [sessionId, entries] of bySession) {
171
- if (sessionsScanned >= DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS)
186
+ if (sessionsScanned >= caps.sessions)
172
187
  break;
173
- if (entriesScanned >= DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES)
188
+ if (entriesScanned >= caps.entries)
174
189
  break;
175
- if (bytesScanned >= DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES)
190
+ if (bytesScanned >= caps.bytes)
176
191
  break;
177
192
  q.signal?.throwIfAborted();
178
193
  sessionsScanned += 1;
@@ -190,9 +205,9 @@ function searchMemorySessionsLinear(bySession, leafBySession, query) {
190
205
  let matchedModel = false;
191
206
  let snippetSource;
192
207
  for (const entry of entries) {
193
- if (entriesScanned >= DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES)
208
+ if (entriesScanned >= caps.entries)
194
209
  break;
195
- if (bytesScanned >= DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES)
210
+ if (bytesScanned >= caps.bytes)
196
211
  break;
197
212
  entriesScanned += 1;
198
213
  const text = entrySearchText(entry);
package/docs/acp.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## What it does
4
4
 
5
- `@arnilo/prism-ag-ui/acp` (stable ACP **v1**, `@agentclientprotocol/sdk@1.3.0` root exports only) exposes two adapters:
5
+ `@arnilo/prism-ag-ui/acp` (stable ACP **v1**, `@agentclientprotocol/sdk@1.4.0` root exports only) exposes two adapters:
6
6
 
7
7
  - `createPrismAcpAgent(options)` — serves ACP as an **agent**: an editor/AI client connects through the SDK transport and drives host-owned Prism sessions with `session/new`, `session/load`, `session/resume`, `session/prompt`, `session/set_mode`, `session/set_config_option`, `session/list`, `session/delete`, `session/close`, and `session/cancel`. The agent is a thin protocol adapter: every capability, decision, and byte cap is wired from host seams, and there is **no second policy engine** on the agent side.
8
8
  - `createAcpEventMapper(options)` — maps a Prism `AgentEvent` stream (or `CoWorkEvent`) to ACP `SessionUpdate`s for hosts that stream through their own transport.
package/docs/ag-ui.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # Frontend interoperability (AG-UI and ACP)
2
2
 
3
+ > **Required peer install:** `zod` (the pinned ACP SDK peers it) — see [Optional peer dependencies](peer-dependencies.md).
4
+
3
5
  ## What it does
4
6
 
5
7
  `@arnilo/prism-ag-ui` is an optional, framework-free protocol adapter over Prism's existing redacted `AgentEvent`, session, durable-run, and persistence seams.
6
8
 
7
9
  - Root export maps Prism events to AG-UI `@ag-ui/core` **0.0.59** events and offers `createAgUiHandler()` (`Request` → SSE `Response`), compatible `createPersistenceAgUiReplay()` pages, distributed `createAgentEventSourceAgUiReplay()` follow, and explicit `createAgUiMcpAdapter()` / `createAgUiMcpAppHandler()` / `createAgUiA2AAdapter()` protocol handshakes.
8
- - `@arnilo/prism-ag-ui/acp` is the stable ACP **v1** sibling: `createAcpEventMapper()` and `createPrismAcpAgent()` over `@agentclientprotocol/sdk` **1.3.0** root exports. ACP is a protocol adapter — sessions, modes, MCP, fs/terminal, lifecycle mapping, and caps live on the host seams. See [ACP coding-host interop](acp.md) for the full reference; this page covers AG-UI only.
10
+ - `@arnilo/prism-ag-ui/acp` is the stable ACP **v1** sibling: `createAcpEventMapper()` and `createPrismAcpAgent()` over `@agentclientprotocol/sdk` **1.4.0** root exports. ACP is a protocol adapter — sessions, modes, MCP, fs/terminal, lifecycle mapping, and caps live on the host seams. See [ACP coding-host interop](acp.md) for the full reference; this page covers AG-UI only.
9
11
  - Core remains protocol-free. `resumeAgentRunStream()` / `AgentRunLifecycle.resumeStream()` are generic durable-resume streams shared by adapters.
10
12
 
11
13
  ## When to use it
@@ -38,7 +40,7 @@ npm install @arnilo/prism @arnilo/prism-ag-ui
38
40
  | `projection` | Explicit safe tool/state/messages/activity/reasoning/raw/custom/interrupt projection. Omit each callback for default deny. Prefer `composeAgUiProjections(createMessagesFromSessionProjection(...), createStateFromStoreProjection(...), createActivityFromToolProgressProjection(), host)` for standard families. |
39
41
  | `a2ui` | Opt-in A2UI painting middleware (`{ catalogId, mode, renderToolName?, allowedCatalogIds?, limits? }`). Detects `a2ui_operations` tool results and/or streams from `render_a2ui` args; paints `a2ui-surface` activity events. Absent = inert. |
40
42
  | `capabilities` | Optional host declaration narrowed to implemented SSE/projector/lifecycle features; read `handler.capabilities`. |
41
- | `redactor`, `limits` | Host redaction and narrowing-only finite caps. |
43
+ | `redactor`, `limits` | Host redaction and narrowing-only finite caps (`AgUiLimitOptions`, with its A2UI variant). |
42
44
 
43
45
  The handler accepts only `POST` JSON validated with official AG-UI `RunAgentInputSchema`. Every aggregate is bounded before a callback runs. With no `input.project`, it preserves compatibility: final text user message only; non-empty state or frontend tools fail before authorization/session lookup. With a projector, all current roles/history, context, state, forwarded props, multimodal parts, parent lineage, and tool-result continuations are available as untrusted input. The projector must apply Prism media URL/SSRF/MIME policy before forwarding media. Start a run with no `resume` and no `?cursor=`; replay supplies `?cursor=`.
44
46
 
@@ -1,5 +1,7 @@
1
1
  # Agent events
2
2
 
3
+ > **Optional peer install:** `@nats-io/jetstream` + `@nats-io/transport-node` for the JetStream event source — see [Optional peer dependencies](peer-dependencies.md).
4
+
3
5
  ## What it does
4
6
 
5
7
  `AgentEvent` is the single observable stream every `AgentSession` run emits. Subscribers receive normalized, redacted, in-order events covering agent lifecycle, assistant message streaming, delegated-agent activity, tool execution, queue updates, subscriber overflow, compaction, retry, artifact validation/refinement, and terminal errors. The stream is in-memory, live-only, and bounded per subscriber by `SubscribeOptions`; there is no durable queue, no background work, and no extra dependency.
@@ -233,7 +233,7 @@ await session.run(input, { loop: twoShotLoop });
233
233
  - `ArtifactValidation.errors[].message` may echo model text — `artifact_*` event payloads flow through the same `redactAgentEvent` path as other `AgentEvent`s (see [Agent events](agent-events.md)).
234
234
  - `generateValidateReviseLoop` makes at most `1 + maxRevisions + maxToolRounds` provider turns when bounded tools are enabled (otherwise `maxRevisions + 1`); it cannot loop forever. Each revision costs one provider turn plus one store append.
235
235
  - Bounded artifact tool calls run sequentially through `dispatchToolCall` (permission + validation + execute); their assistant call and result are persisted before the next provider request. `singleShotLoop` retains its bounded parallel worker pool and original call-order transcript behavior.
236
- - In a parallel single-shot batch, the worker pool stops claiming calls after the first dispatch error or abort, waits for every already-claimed worker with `Promise.allSettled`, appends no buffered tool-result rows for a failed batch, then rethrows the first failure. Already-claimed side effects may finish and are not rolled back; successful batches still append results in original call order. The round-level `chargeToolRound` approval gate runs before workers, so approval suspension starts no worker.
236
+ - In a parallel single-shot batch, the worker pool stops claiming calls after the first dispatch error or abort, waits for every already-claimed worker with `Promise.allSettled`, then persists rows in call order before rethrowing the first failure: real results for calls that finished, an error row carrying the failure for the call that threw, and a `tool_call_not_dispatched` row for calls the batch never started. A stopped batch therefore never ends the run with `tool_call` ids that have no `tool_result` (providers reject such a history on the next turn); run-level suspension errors (`AgentRunSuspended`, `ERR_PRISM_DELEGATION_SUSPENDED`, `ERR_PRISM_LOOP_*`) are the exception — their resume machinery appends the real result, so the failed call gets no synthetic row. Already-claimed side effects may finish and are not rolled back; successful batches still append results in original call order. The round-level `chargeToolRound` approval gate runs before workers, so approval suspension starts no worker.
237
237
  - The loop is a plain object/factory; no class hierarchy, no background work, no extra dependencies. `LoopContext` is a single object literal of bound arrows built once per run.
238
238
  - The host-domain-free boundary is guarded by tests: `src/` imports no host-domain package, and the `Artifact*`/`AgentLoop*`/`LoopContext` contracts contain no `workflow`/`node`/`step` field names. Hosts supply their own schema; no host domain type is imported by `src/`.
239
239
 
@@ -45,7 +45,7 @@ createAgentSession(config: AgentSessionConfig & { agent: Agent }): AgentSession
45
45
  string | Message | readonly Message[]
46
46
  ```
47
47
 
48
- `AgentSessionConfig.store` overrides `AgentConfig.store`; otherwise the session gets a private memory store. `AgentSessionConfig.leafId` selects the branch leaf to resume from.
48
+ `AgentSessionConfig.store` overrides `AgentConfig.store`; otherwise the session gets a private memory store. `AgentSessionConfig.leafId` selects the branch leaf to resume from. `AgentSessionConfig.snapshotCacheTtlMs` tunes the in-memory branch cache behind `session.snapshot()`: default `DEFAULT_SNAPSHOT_CACHE_TTL_MS` (1000 ms), `0` disables caching so every read rebuilds from the store, maximum `HARD_MAX_SNAPSHOT_CACHE_TTL_MS` (30 s); values outside `0..hard` fail session construction with `TypeError`. The cache is invalidated by any new leaf or mutation, so the TTL only bounds reuse of an unchanged branch.
49
49
 
50
50
  `AgentConfig.limits` sets run ceilings; `RunOptions.limits` may only narrow configured agent values (`null` counts as no cap, so a configured finite ceiling still wins). Limits cover turns, provider attempts, tool rounds/calls, wall time, request/response bytes, tokens, and optional single-currency cost. Policy axes accept `null` (0.5.4) to disable the axis; request/response bytes reject `null` and stay process-hard at 64 MiB. A breach emits one `run_limit_exceeded` event and throws `AgentRunError` with `result.limit`; see [Runs and usage ledger](runs-and-usage.md#run-limits).
51
51
 
@@ -55,6 +55,8 @@ string | Message | readonly Message[]
55
55
 
56
56
  ## Outputs / response / events
57
57
 
58
+ `session.fork(options?)` / `session.clone(options?)` take `AgentSessionForkOptions` / `AgentSessionCloneOptions` (leaf id, new session id, metadata, and store overrides), and `session.steer(input, options?)` takes `SteerOptions`. See [Public contracts](public-contracts.md) for the field tables, and the [options index](options-index.md) for every session option surface.
59
+
58
60
  `session.run()` / `session.prompt()` resolve to an `AgentRunResult` with `sessionId`, `runId`, `status`, `text`, `content`, optional `message`/`usage`/`leafId`, and terminal `error`/`abortReason` when applicable. Callers may ignore the return value. Failed and aborted runs still emit their terminal events, then reject with `AgentRunError` whose `.result` carries the same shape.
59
61
 
60
62
  `session.stream(input, options?)` subscribes first, starts exactly one run, yields only that run's events, and terminates when the run succeeds, fails, or aborts. Early consumer return aborts the owned run and releases the session. `SubscribeOptions.maxQueuedEvents` / `overflow` may be passed alongside `RunOptions`.
@@ -1,5 +1,7 @@
1
1
  # Browser automation
2
2
 
3
+ > **Optional peer install:** `playwright-core@1.63.0` (exact pin) — see [Optional peer dependencies](peer-dependencies.md).
4
+
3
5
  ## What it does
4
6
 
5
7
  The `@arnilo/prism-web-tools/browser` subpath exposes six exclusive model-facing tools—`browser_open`, `browser_snapshot`, `browser_act`, `browser_close`, `browser_evaluate`, and `browser_observe`—over a host-supplied Playwright `Browser`. Prism creates one non-persistent `BrowserContext` per run, serializes actions, returns bounded AI-mode accessibility snapshots with snapshot-scoped refs, enforces egress/side-effect/upload/download/screenshot policy, and closes context/pages/listeners/quarantined downloads on close, abort, or manager disposal. Since 0.1.4 the package also rides playwright-core's existing CDP transport for bounded page evaluation, console/network observation, and network/emulation control on Chromium hosts — zero new dependencies, Prism still never launches or downloads browsers.
@@ -100,8 +102,9 @@ await browser.close();
100
102
 
101
103
  ## Extension and configuration notes
102
104
 
103
- - Compatibility line: `playwright-core@1.61.0` optional peer. Hosts pin browser binaries/images; Prism package install downloads nothing.
104
- - Default/hard caps: pages 4/16; actions 100/256; queued actions 16/64; snapshot refs 2k/10k; depth 30/100; snapshot bytes 256 KiB/2 MiB; navigation 30s/120s; action 10s/60s; wait 30s/120s; run wall 20min/30min; popups 4/16; dialogs 16/64; close grace 5s/30s; network requests 1k/10k; redirects/request 10/32; WebSockets 8/32; screenshots 16/64 with 16/64 megapixels and 10 MiB/32 MiB encoded; uploads 8/32 files, 16 MiB/64 MiB each, 64 MiB/256 MiB aggregate; downloads 8/32 files, 32 MiB/256 MiB each, 64 MiB/512 MiB aggregate.
105
+ - Compatibility line: `playwright-core@1.63.0` optional peer. Hosts pin browser binaries/images; Prism package install downloads nothing.
106
+ - Default/hard caps: pages 4/16; actions 100/256; queued actions 16/64; snapshot refs 2k/10k; depth 30/100; snapshot bytes 256 KiB/2 MiB; navigation 30s/120s; action 10s/60s; wait 30s/120s; run wall 20min/30min; idle run TTL 0 (off)/30min; popups 4/16; dialogs 16/64; close grace 5s/30s; network requests 1k/10k; redirects/request 10/32; WebSockets 8/32; screenshots 16/64 with 16/64 megapixels and 10 MiB/32 MiB encoded; uploads 8/32 files, 16 MiB/64 MiB each, 64 MiB/256 MiB aggregate; downloads 8/32 files, 32 MiB/256 MiB each, 64 MiB/512 MiB aggregate.
107
+ - Optional `idleRunTtlMs` (default `0` = off, hard-capped at the run wall-time cap) arms one unref'd manager-scoped sweep interval. A run with no queued action and no activity for the TTL is disposed exactly like `manager.closeRun(runId)` — context and pages closed — so later calls fail with `ERR_PRISM_BROWSER_STATE` and the host can `open()` it again. Any operation (open/snapshot/act/evaluate/observe) resets the clock, in-flight work is never reaped mid-action, and the reaper is cleared by `manager.close()`. Hosts that already close runs explicitly can leave it off; enabling it bounds contexts leaked by abandoned runs.
105
108
  - Contexts use `serviceWorkers: "block"` and install `BrowserContext.route()` for every visible HTTP(S)/WebSocket request. `acceptDownloads` is enabled only when `downloads` is configured.
106
109
  - `networkPolicy` defaults to `requireContainedProxy: true` (fail closed). Hosts must supply `containedProxyAttestation: { proxyEndpoint, denyDirectEgress: true }`. Private/loopback/link-local, `file`/`data`/`blob`/`javascript`/`devtools` schemes are denied by default. Playwright routing is defense in depth — production DNS/private egress is a host firewall/proxy.
107
110
  - Uploads require absolute paths under `uploads.roots` (realpath-contained; symlink escapes rejected). Downloads stream into `downloads.quarantine` with SHA-256/MIME/name metadata; `download_release` requires host `approveRelease`. Screenshots return bounded `ImageContent`.
@@ -0,0 +1,37 @@
1
+ # Contribution quality budgets
2
+
3
+ ## What it does
4
+
5
+ Records the code-quality ceilings a change must not raise, and the procedure for lowering them. Three budgets exist today: the non-null assertion allowance per directory, the public export surface per package, and the benchmark/timing envelopes. All live in `scripts/budgets.json` and are enforced by in-chain gates that run as part of `npm test`.
6
+
7
+ ## When to use it
8
+
9
+ Before a change that adds a `!` non-null assertion, adds a public export, or moves code between directories; and after any sweep that removes them — the recorded numbers are lowered in the same change, never later.
10
+
11
+ ## Non-null assertions
12
+
13
+ `style/noNonNullAssertion` is an **error** repo-wide in `biome.json`. Directories that still carry legacy sites are switched back to `off` per directory through `overrides`, and `scripts/budgets.json` → `nonNullAssertions` records how many sites remain in each of them.
14
+
15
+ Rules:
16
+
17
+ - New code does not add `!`. Narrow the value once — a local `const` behind an explicit guard — or capture the seam in a helper (the durable-session and elicitation seams are the models). Do not trade the assertion for an `as` cast or a `??` placeholder.
18
+ - Keep the allowlist and the budget rows identical: the gate asserts that the `biome.json` allowlist directories and the `nonNullAssertions.byPath` keys describe the same set, so there is no ambiguity about which row an override corresponds to.
19
+ - A sweep lowers the directory's `byPath` number and the `ceiling` in the same change. Raising a number needs a reason in the `$comment`.
20
+ - When a directory reaches zero sites, delete its `overrides` entry and its `byPath` row: the gate fails on a stale zero row rather than letting the allowance rot.
21
+ - The clusters swept in that pass — `src/agent-approval.ts`, `src/agent-loops.ts`, `src/agent-tool-dispatch.ts`, `packages/prism-coding-tools/src/agent/{glob-match,delete,git*}.ts`, `packages/prism-coding-tools/src/agent/language/framing.ts`, and `packages/prism-coding-tools/src/security/{sandbox-tar,sandbox-fs-operations}.ts` — are re-enabled as errors by the last override in `biome.json` (the last matching override wins), so they cannot regress. The gate rejects a rename that would silently drop that enforcement.
22
+ - Measure locally with one pass: `node_modules/.bin/biome lint --only=style/noNonNullAssertion --reporter=json .`. `--only` reports the rule inside allowlisted directories too, which is what makes the counting gate possible; the gate also fails when a site appears outside the allowlist.
23
+
24
+ ## Public export surface
25
+
26
+ `scripts/budgets.json` → `exportCounts` records a ceiling per package. Growth fails the gate and names the package and the exact delta. Prefer re-exporting an existing symbol (or documenting the host-side composition) over widening a package's surface; moving an existing internal helper between modules does not change the count, because the counter dedupes by name.
27
+
28
+ ## Timing envelopes
29
+
30
+ Benchmark medians and p95 ceilings in `scripts/budgets.json` are non-flaky sanity bounds, not portable SLOs; the startup gate additionally compares a machine-relative ratio so external CPU load cannot fail the suite. `scripts/benchmark.mjs` produces the evidence-of-record numbers.
31
+
32
+ ## Related APIs
33
+
34
+ - `scripts/budget-gates.mjs`: `measureNonNullAssertions()`, `evaluateNonNullBudget()`, `measureExportCounts()`, `checkExportBudget()`, and the startup helpers.
35
+ - `scripts/budget-gate.test.mjs`: the in-chain gate, including the negative fixtures that prove each failure mode.
36
+ - `scripts/run-all-tests.mjs`: the stages `npm test` runs (`gate suites` includes the budget gate).
37
+ - [Release and install](release-and-install.md): the release gates that re-assert these budgets before publication.
package/docs/core.md CHANGED
@@ -21,6 +21,8 @@ npm install pg
21
21
  npm install @nats-io/jetstream @nats-io/transport-node
22
22
  ```
23
23
 
24
+ 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.
25
+
24
26
  ## Subpaths Map
25
27
 
26
28
  | Subpath | Description | Optional Peers |
@@ -1,5 +1,7 @@
1
1
  # Document reader (`@arnilo/prism-coding-tools/document-reader`)
2
2
 
3
+ > **Optional peer install:** `pdf-parse` and/or `mammoth` — see [Optional peer dependencies](peer-dependencies.md).
4
+
3
5
  ## What it does
4
6
 
5
7
  Optional bounded literal-text extraction for PDF and DOCX files, consumed by the coding `read` tool (plan 018 closeout `doc-reader`, 0.1.6). `createDocumentReader()` returns a `DocumentReader` that the host wires into `createReadTool(cwd, { documentReader })`; the read tool then extracts text from supported documents instead of falling back to the raw text page.
package/docs/documents.md CHANGED
@@ -35,7 +35,7 @@ Do **not** use this package for collaborative real-time editing (OT/CRDT), macro
35
35
  | `createPatchHistory` | `(initialModel: DocumentModel) => PatchHistory` | Creates an interactive undo/redo history manager for host editing workflows. |
36
36
  | `renderPreviewBlocks` | `(model: DocumentModel, options?: PreviewBlocksOptions) => PreviewBlock[]` | Emits framework-neutral structured blocks (document outlines, bounded sheet grid chunks, slide summaries). |
37
37
  | `renderPreviewHtml` | `(model: DocumentModel, options?: PreviewHtmlOptions) => string` | Emits safe, bounded HTML fragments with all entities escaped and external URLs neutralized. |
38
- | `getDocumentModelSchema`| `(options: GetDocumentModelSchemaOptions) => Record<string, unknown>` | Retrieves full Draft-07 JSON Schema or a self-contained sliced sub-schema with resolved `$defs`. |
38
+ | `documentModelSchema` | `(kind: DocumentKind, slice?: string \| readonly string[]) => JsonSchema` | Retrieves the Draft-07 JSON Schema for a document kind, or a self-contained sliced sub-schema with resolved `$defs` (`docModelSchema` / `sheetModelSchema` / `deckModelSchema` expose the unsliced schemas). |
39
39
  | `validateDocumentModel`| `(model: unknown) => asserts model is DocumentModel` | Validates arbitrary JSON objects against Draft-07 document schemas and structural invariants. |
40
40
 
41
41
  ### Capacity Limits and Defaults
package/docs/graft.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Graft context-graph integration
2
2
 
3
+ > **Optional peer install:** `@nanonets/graft` — see [Optional peer dependencies](peer-dependencies.md).
4
+
3
5
  ## What it does
4
6
 
5
7
  `@arnilo/prism-memory/graft` is an optional subpath that wires [nanonets/graft](https://github.com/nanonets/graft) — a repository context-graph CLI (`graft/` directory, INDEX.md orientation, symbol-level wiring graph) — into Prism contribution contracts.
@@ -14,7 +16,7 @@ Use it when a host wants agents to locate code by architecture, callers, and cou
14
16
  - `"push"` — per-turn retrieval pack (pointers only) + first-turn orientation, injected automatically.
15
17
  - `"both"` — everything.
16
18
 
17
- Install optional peer `@nanonets/graft@^0.16.0` **or** pass `packageRoot`/`cliPath` explicitly. Pair with progressive disclosure: the `graft` skill body stays small; tool schemas carry the details. Graft complements indexed code search (`repository_search`): graph/semantic locators vs literal search — neither replaces the other.
19
+ Install optional peer `@nanonets/graft@^0.16.0 || ^0.18.0` **or** pass `packageRoot`/`cliPath` explicitly. Both floors are smoke-tested by the offline peer-contract suite (`resolveGraftCli` bin discovery + packaged manifest); the range lists exactly the two released lines Prism validates, and `0.17` is absent because upstream never published one. Pair with progressive disclosure: the `graft` skill body stays small; tool schemas carry the details. Graft complements indexed code search (`repository_search`): graph/semantic locators vs literal search — neither replaces the other.
18
20
 
19
21
  Zero-code alternative (L0): hosts can skip this package entirely and let agents call `graft <command> --json` through their shell tool, optionally seeding context with graft's own generated instruction files. This package exists for native-tool ergonomics, budgeted subprocesses, session persistence, and push mode.
20
22
 
@@ -2,6 +2,39 @@
2
2
 
3
3
  Operator publish handoffs per release line, kept verbatim. Not read on the hot path.
4
4
 
5
+ ### 0.6.0 publish handoff (plan 071 Task 16)
6
+
7
+
8
+ **Decision: GO when the operator prerequisites below are recorded.** Release **0.6.0** is the first published cut after **0.5.6**: the 0.5.7 cut was never published and is **superseded** by this one (registry preflight `node scripts/release.mjs check --lockstep --version 0.6.0` reports all **10/10 packages available**, so there is no published 0.5.7 to replace or deprecate). The graph is **10 publishable manifests** at exact **0.6.0** with internal ranges `^0.6.0`: root `@arnilo/prism` plus 9 workspace packages (3 `prism-*` family packages, 6 capability packages, 19 provider adapter subpaths inside the providers family).
9
+
10
+ Host-visible delta (full detail in [migrate-to-0.6.md](../../docs/migrate-to-0.6.md)): the runtime floor moves to **Node `>=22`** (Node 20 is upstream EOL since 2026-04-30) and the never-published 0.5.7 content ships here — third-party floors (`pg ^8.23`, `playwright-core 1.63.0`, `@ai-sdk/provider 4.0.13`, `@agentclientprotocol/sdk` exact `1.4.0`, `@office-open/* 0.14.5`, `zod ^4.6.2`), the removed `@arnilo/prism-office` `playwright-core` peer, five additive host knobs, and the durable-tool-round / strict-provider tool-result fixes. No import path, store schema, event shape, or public signature was removed: the compat baselines were regenerated and reviewed as **+70 public names, zero removals** (32 declaration-site moves from the module splits).
11
+
12
+ Evidence recorded for the tree under publication: `scripts/release-evidence.json` — **42 surfaces, 11 pass, 31 protected with reasons, `blocked: false`** (`test:postgres durable conformance` is a real pass, count 91, env **name** only); `npm test` 5/5 stages (core count 3716, skip 33); combined coverage core 92.13% lines against the 60/70/75 gate with all nine workspace suites above their lines thresholds; `security:threat-suites` 83/83; `npm audit --audit-level=moderate` 0; SBOM regenerated (`npm sbom --sbom-format spdx > security-artifacts/sbom.spdx.json`, 172 packages, 10 licenses, `verify-sbom` clean); tracked-source secret scan 2166 files, 0 findings.
13
+
14
+ ```bash
15
+ # Operator prerequisites (each a named blocked gate — none may be skipped):
16
+ # 1. protected live-canary matrix green (live-canaries.yml, canary-report.json retained)
17
+ # 2. PostgreSQL protected suite green (test:postgres) and CodeQL SAST green on the release commit
18
+ # 3. npm OIDC trusted publishing identity authenticated (NPM_TOKEN with id-token, provenance)
19
+ # 4. branch protection: the compatibility leg is named node22-compat (renamed from node20-compat)
20
+
21
+ git diff --check
22
+ npm ci
23
+ # sdk:ready phases, as .github/workflows/release.yml runs them (env scoped to release:gate only):
24
+ npm run typecheck && npm run lint && npm run format:check
25
+ npm test && npm run test:coverage && npm run pack:dry-run
26
+ PRISM_TEST_POSTGRES_URL=... npm run release:gate
27
+ npm run security:threat-suites
28
+
29
+ # Sign the release on the clean tagged tree (operator GPG key):
30
+ git tag -s v0.6.0 -m "0.6.0"
31
+ node scripts/release.mjs publish --lockstep --version 0.6.0
32
+
33
+ # First-party package tags: push in batches of <=3 per push (tag-push storms; VENT 26-08-29).
34
+ ```
35
+
36
+ Rollback pins the previous published line — `@arnilo/prism@0.5.6` and its siblings, exact pins per package. **Never pin 0.5.7: it does not exist on the registry.** Persisted shapes are unchanged across 0.5.6 → 0.6.0, so a pin rollback loses only the Node floor, the peer floors, and the new knobs.
37
+
5
38
  ### 0.3.2 independent workflow patch (plan 045)
6
39
 
7
40
 
@@ -152,13 +152,13 @@ Wire those values where they matter: provider adapters receive the resolved cred
152
152
  - `@arnilo/prism-core/runtime/server` exposes no agent/workflow by default and requires `authorize()` for every matched operation. Derive complete tenant/account/user ownership from validated host identity, never request JSON. Workflow active identity and cancellation compare exact ownership; a tenant-only scope intentionally cannot cancel a checkpoint/run carrying account or user identity. The artifact review service (`createArtifactService`) requires authenticated identity + thread ownership on every attach/revise/compare/approve/reject/download, resolves concurrent reviewers via checkpoint CAS (no lost approvals), rejects local filesystem paths in `uri`/citations, redacts records before persist and on response, and serves downloads only through signed expiring links that are reauthorized against the token's ownership per request. When a blob store is wired (`bodies: ArtifactBodyStore`, 0.0.28), delivery links additionally resolve through `bodies.presign`; the reference `createS3ArtifactBodyStore` verifies ownership on every operation, verifies size/SHA-256/MIME on put and get (fail closed), refuses delete under legal hold (host `isHeld` callback), keeps credentials host-resolved, and never discloses bucket/path/key in errors, telemetry, or artifact records. Pass the current explicitly revised workflow definition so recursive hash mismatch fails before abort or durable mutation. Configure exact host/origin allow-lists where needed, wire redaction before execution, retain tool/workflow policy checks, and adapt the Web handler behind host TLS/rate limits. Disconnect abort is default; persistent reconnect/status belongs to durable workflow checkpoints, not an invented in-memory agent result cache. Outbound lifecycle webhooks (`createWebhookNotifier`, 0.3.2) share the same boundary posture: host-registered public HTTPS (or explicitly opted-in loopback HTTP) targets only, private/metadata literals rejected at registration, every attempt DNS-pinned and redirect-free through core `pinnedFetch`, redaction before HMAC signing, and the key held by the host only. Pass a known-secret `SecretRedactor`.
153
153
  - Coding tools from `@arnilo/prism-coding-tools/agent` accept an optional `ExecutionPolicy` checked inside each tool before side effects; shared policy propagation includes `createReadOnlyTools()`. They enforce finite text-scan/image/edit/write/shell limits, repository list/search depth/entry/match/scan/time caps, structured Git path/ref/message/output/patch/worktree caps, named-check concurrency/output caps, a 600-second default shell wall time, and a 64 MiB default total-output ceiling. Opt-in `createGitTools()` uses argument arrays with hooks/credential prompts/external diff disabled, requires host `commitIdentity` for commits, and never pushes or opens PRs. Successful truncated shell output leaves a host-owned exclusive `0600` temp file; delete `metadata.fullOutputPath` after use. Error/abort/timeout/overflow removes unpublished spills. Custom read/edit/shell/repository backends must honor supplied caps/signals. Use `@arnilo/prism-coding-tools/security` for path roots, command rules, identity-scoped approval caching, required `workspaceMode` on `createSandboxCodingComposition()` / `createSandboxCodingTools()`, and the optional `createDockerSandbox()` reference adapter. **Host mode is never contained execution** (every isolation capability false). Sandbox mode reports isolation only from validated adapter capability metadata: `composition.capabilities` carries the frozen `SandboxCapabilities` object (`workspaceCoherent`, `filesystemIsolated`, `networkIsolated`, `processIsolated`, `privilegeIsolated`, `egressRestricted`); the deprecated `containmentClaim` is a conservative projection and must never be used alone. Authorize security-sensitive actions from the individual capabilities the policy actually needs — e.g. require `filesystemIsolated` before hosting untrusted coding tasks, and `egressRestricted` before any network-capable run. Mixed wiring requires `allowMixedWorkspaceWiring` and still reports no isolation. Limits alone are not containment: construct the Docker adapter (absolute CLI, digest-pinned image, network none by default) or an equivalent host sandbox before treating coding execution as production-safe. Docker daemon/image trust, egress firewall/proxy, and artifact retention remain host-owned.
154
154
  - Allow-list egress (0.0.26, `@arnilo/prism-coding-tools/security`): `createEgressPolicy()` is deny-all with exact host/port/protocol rules and frozen `npm-registry`/`github` presets; `createAllowListEgressProxy()` is an HTTP forward proxy + CONNECT tunnel that pins DNS answers and verifies the connected address (rebinding defense), denies private/link-local/metadata ranges unless a rule opts in, re-validates every redirect hop against policy, and cuts oversized/slow transfers at frozen byte/time caps. TLS passes through without interception. Every allow/deny writes an audit record with no secrets. The proxy is inert until `start()`; `reloadPolicy()` is the only rule change path. `composeEgressSandboxNetwork(proxy.attestation(), name)` records validated attestation as `prism.egress.*` container labels — evidence, not enforcement: the host must restrict the Docker network so the proxy is the only reachable path, and `denyDirectEgress: true` is a claim the host makes true by topology. The proxy is not a firewall and cannot stop a container whose network reaches the internet directly.
155
- - Optional `@arnilo/prism-web-tools/browser` requires a host-supplied Playwright Browser (`playwright-core@1.61.0` peer). Import is inert. One non-persistent context belongs to one run; actions serialize; refs are snapshot-scoped; CSS/evaluate/CDP/persistent profiles are denied. Context routing + `serviceWorkers: "block"` deny file/data/blob/devtools/private/loopback by default and require contained-proxy attestation for external egress (Playwright routing is defense in depth, not DNS containment). Uploads are realpath-rooted; downloads quarantine with hash/MIME until host `approveRelease`; screenshots return bounded `ImageContent`. Observation vs mutation/high-impact actions map to `ExecutionPolicy`. Treat snapshot/page text as untrusted external content. Close contexts with `browser_close` or `manager.closeRun(runId)` on abort/terminal. Browser control endpoint, binary/image pin, and real egress firewall/proxy remain host-owned. Shared sandbox: `createSharedSandboxBrowserOptions()` + `assertBrowserSandboxNetwork()`.
155
+ - Optional `@arnilo/prism-web-tools/browser` requires a host-supplied Playwright Browser (`playwright-core@1.63.0` peer). Import is inert. One non-persistent context belongs to one run; actions serialize; refs are snapshot-scoped; CSS/evaluate/CDP/persistent profiles are denied. Context routing + `serviceWorkers: "block"` deny file/data/blob/devtools/private/loopback by default and require contained-proxy attestation for external egress (Playwright routing is defense in depth, not DNS containment). Uploads are realpath-rooted; downloads quarantine with hash/MIME until host `approveRelease`; screenshots return bounded `ImageContent`. Observation vs mutation/high-impact actions map to `ExecutionPolicy`. Treat snapshot/page text as untrusted external content. Close contexts with `browser_close` or `manager.closeRun(runId)` on abort/terminal. Browser control endpoint, binary/image pin, and real egress firewall/proxy remain host-owned. Shared sandbox: `createSharedSandboxBrowserOptions()` + `assertBrowserSandboxNetwork()`.
156
156
  - Browser verified-state checkpoints (0.0.14, `createBrowserCheckpointLedger()`) store URL + domain-state hash + host data refs only — never serialized browser internals (cookies/storage/contexts). After any resume/interruption the ledger fails closed (`assertVerifiedBeforeSideEffect`) until the host reloads + verifies, so side effects never replay on stale state.
157
157
  - Device adapters (0.0.14, `resolveDevicePolicy`/`assertDeviceAdmit`) are deny-by-default: admission fails closed without explicit `enabled`, an explicit sandbox, approval (when required), an under-budget session count, and shared `RunLimits`. Stream chunks over the frozen cap are dropped with a marker; telemetry is redacted before emit/persist. No vendor voice/desktop package ships in 0.0.14 (demand-gated 0.1.x); device adapters cannot broaden consent/memory/network/file/browser/connector/tool permissions (gate 8).
158
158
  - Optional `@arnilo/prism-memory/wiki` tools treat agent-supplied input as untrusted at the first-party `.wiki/` filesystem boundary. `wiki_read_page` enforces lexical containment (`path.relative` with separator-aware `..`/absolute checks) plus `fs.realpath` containment for the wiki root and every successfully read file, so sibling-prefix (`.wiki-evil`), `..`, absolute, alternate-separator, and symlink escapes are denied before content is returned; missing contained pages report `found: false` while denied paths throw an access-denied error (never mapped to not-found). `wiki_record_insight` rejects empty titles/content, caps titles at 200 characters and content at 65,536 bytes, and collapses control characters and newlines in titles to single-line display text before any page/frontmatter/index/log write, so titles cannot inject Markdown headings, index entries, or log entries; slugs are allow-listed to `[a-z0-9-_]` with a non-empty fallback. See [LLM Wiki](wiki.md).
159
159
  - `@arnilo/prism-core/credentials/node` rejects oversized/malformed envelopes and excessive scrypt work before KDF allocation, uses async scrypt, and requires restrictive existing/new Unix vault modes. Keep vault ownership and parent-directory access host-controlled; review before `chmod 600`, never auto-weaken a file policy. Keychain calls use abort-aware native async work with finite timeout/payload caps and sanitized errors. OS prompts, service availability, and whether a native backend promptly honors cancellation remain host/platform boundaries; no plaintext fallback is attempted.
160
160
  - LLM compaction always sends finite summary `maxTokens`, retains bounded deltas/events, and bounds/redacts provider/factory/policy error detail. Observational-memory workers cap turns, calls, arguments, results, transcript, and surfaced errors; unknown tools fail before execution, while invalid results can only be rejected after a host tool returns and may therefore follow side effects. Pass all known provider/credential/tool secrets into compaction/runtime options; exact replacement is not secret discovery.
161
- - Default remote-media loading resolves every DNS answer, rejects the hostname if any address is non-public, and pins one validated address through the request. Explicit `allowedHostnames` can trust private destinations. A host-supplied `fetch` owns DNS/rebinding/proxy/redirect safety; a custom `requestUrl` must connect to its supplied validated address.
161
+ - Default remote-media loading resolves every DNS answer, rejects the hostname if any address is non-public, and pins one validated address through the request. Explicit `allowedHostnames` can trust private destinations; `allowedCidrs` trusts IP ranges instead (checked after the hostname/denied-name lists, applied to literals and resolved answers, and bypassing only the private-IP block — metadata-style names and malformed ranges fail closed). A host-supplied `fetch` owns DNS/rebinding/proxy/redirect safety; a custom `requestUrl` must connect to its supplied validated address.
162
162
  - Permission checks happen before tool validation and before `tool.execute()`. Middleware cannot grant permission by renaming a tool.
163
163
  - Session stores and ledgers receive redacted values when a redactor is active, but durable storage remains host-owned. Enforce tenant/account/user ownership, retention, legal hold, and quotas via `ProductionPersistenceStore.lifecycle` (or host-equivalent DB controls). Hold always blocks delete.
164
164
  - `@arnilo/prism-core/enterprise/postgres` request paths require exact tenant scope plus principal for work/router state, use bound SQL values, and retain no prompts, connector request bodies, raw provider results, tokens, or credentials. Configure TLS/credential rotation/connection limits with the host `pg` pool. Run checksum/catalog migration setup with a controlled migration principal; keep request-path SQL least-privilege (`USAGE`, `SELECT`, `INSERT`, `UPDATE`, `DELETE` on six state tables) and do not grant request workers `CREATE`, `ALTER`, `DROP`, `TRUNCATE`, `GRANT`, or `COPY PROGRAM`. Back up and restore-test the schema; run bounded owner-scoped `state.cleanup()` from an authorized host job. `unknown` connector outcomes require reconciliation and must never auto-replay.
package/docs/index.md CHANGED
@@ -2,8 +2,16 @@
2
2
 
3
3
  Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credentials, storage, and behavior; Prism supplies contracts, registries, events, and replaceable runtime primitives.
4
4
 
5
- ## Current line (0.5.6)
6
-
5
+ ## Current line (0.6.0)
6
+
7
+ - **Node 22 floor**: `engines.node` is `>=22` in all ten publishable packages, the `node20-compat` CI leg becomes `node22-compat`, and `@types/node` moves to `^22.20.0` (plan 071; Node 20 is upstream EOL since 2026-04-30).
8
+ - **Folded 0.5.7 content**: the 0.5.7 cut was never published — its durable-tool-round and strict-tool-result fixes, host knobs, peer/options truth, and dependency floors ship in 0.6.0 (migration guide below).
9
+ - **Release-truth gates**: one forward-claim version-literal gate (manifests, internal ranges, lockfile, version constant, index banner, workflow tags), a workflow-liveness gate (every script target and action reference resolves, actions SHA-pinned), and a load-tolerant startup budget ratio (plan 071).
10
+ - **Self-describing coverage failures**: a failing coverage child prints its redacted output tail and records `status`/`exitCode`/`tail` on its artifact row (plan 071).
11
+ - **Durable tool rounds**: concurrent tool dispatch persists successful sibling results — plus synthetic errors for failed and never-dispatched calls — before a round fails or aborts, so no `tool_use` is left unanswered (plan 070).
12
+ - **Strict-provider tool results**: a content-less `ToolResult` folds to the non-empty `(tool completed with no output)` payload instead of an empty one (plan 070).
13
+ - **Host-tunable knobs**: context-budget `tokenEstimator`, `snapshotCacheTtlMs`, memory-session search caps, `SsrfPolicy.allowedCidrs`, and browser `idleRunTtlMs` (plan 070).
14
+ - **Peer and options truth**: the optional peer-dependency matrix and the configuration options index (both linked below) cover every third-party peer and public option surface (plan 070).
7
15
  - **Trusted extension activation**: `activateKernel(kernel)` returns ready-to-spread `AgentConfig` contributions; CLI loads allow-listed `--extension` packages (plan 069).
8
16
  - **Wiki ingest**: `/wiki-ingest` + `ingestWikiSource` stage text/file/image/PDF (and URLs via a host `fetchUrl` hook) into `raw/ingest/` with an OKF filing brief (plan 069).
9
17
  - **Graft graph commands**: `/graft-init`, `/graft-build`, `/graft-build-deep` (host-configured `deepModel`, key env-only) (plan 069).
@@ -11,13 +19,14 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
11
19
  - **Tool-result fold**: content-only `ToolResult`s fold into `tool_result.result` (0.5.3).
12
20
  - **Stream token coalesce**: adjacent text/thinking deltas merge on persist (0.5.2).
13
21
  - **Provider request construction**: kernel session/cache/thinking defaults; hosts overlay (0.5.1).
14
- - **10 publishable packages** at current **0.5.6** lockstep — inventory below.
22
+ - **10 publishable packages** at current **0.6.0** lockstep — inventory below.
15
23
 
16
24
  ## Public contracts
17
25
 
18
26
  - [Public contracts](public-contracts.md): canonical message, agent, tool, store, resource, credential, and event shapes.
19
27
  - [Coding tools, sandboxing, and personas](coding-tools.md): `@arnilo/prism-coding-tools` family subpaths — agent, security, document-reader, openapi, computer-use-linux, dev, personas.
20
28
  - [Core runtime, sessions, and governance](core.md): `@arnilo/prism-core` family subpaths — runtime, sessions, governance, credentials, enterprise, work, validation.
29
+ - [Configuration options index](options-index.md): every public `*Options`/`*Limits`/`*Config` surface mapped to the doc page that owns its fields.
21
30
 
22
31
  ## Identity and governance
23
32
 
@@ -56,7 +65,7 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
56
65
  - [SQLite persistence](sqlite-persistence.md): optional `better-sqlite3` adapter with FTS search and verified migrations.
57
66
  - [PostgreSQL persistence](postgres-persistence.md): optional pooled `pg` adapter with advisory-locked migrations and live conformance.
58
67
  - [Enterprise PostgreSQL state](enterprise-postgres-state.md): durable governance/router/ERP state, outbox/inbox messaging, approval records.
59
- - [Migration guide](migration.md): current 0.5.x migration cuts with replacement tables and rollback notes.
68
+ - [Migration guide](migration.md): the era index of migration cuts with replacement tables and rollback notes.
60
69
  - [Node JSONL session store](node-jsonl-session-store.md): development-only JSONL adapter, single-process, no cross-process safety.
61
70
 
62
71
  ## Provider and model connection
@@ -135,6 +144,7 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
135
144
  - [Configuration and manifests](configuration-and-manifests.md): layered JSON config merge with data-only manifest validation.
136
145
  - [Node filesystem config loader](node-filesystem-config.md): explicitly read caller-named JSON config files in Node.
137
146
  - [Resource loading](resource-loading.md): decode text/JSON/binary through caller-provided loaders; RAG bridge.
147
+ - [Optional peer dependencies](peer-dependencies.md): every third-party peer a package declares, the subpath it unlocks, its install line, pin rationale, and which peers touch the network.
138
148
 
139
149
  ## Server/API
140
150
 
@@ -165,6 +175,8 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
165
175
 
166
176
  ## Testing and examples
167
177
 
178
+ - [Test layout and isolation](testing.md): the five `npm test` stages, scratch-root rule, and the tracked-fixture isolation gate.
179
+ - [Contribution quality budgets](contributing.md): the non-null assertion allowance, export-surface ceilings, and the rule that keeps them shrinking.
168
180
  - [Live and end-to-end testing](live-testing.md): opt-in live matrix with skip-not-fail contract and credential scoping table.
169
181
  - Provider test doubles: `createMockProvider()` and provider event helpers are documented on the canonical Provider layer page above.
170
182
  - [Provider conformance](provider-conformance.md): network-free adapter assertions from `@arnilo/prism/testing/provider-conformance`.
@@ -185,6 +197,7 @@ Prism is a TypeScript/Node.js agent harness. Hosts own providers, tools, credent
185
197
  ## Release and install
186
198
 
187
199
  - [Release and install](release-and-install.md): install rules, package graph, and deterministic resumable publication.
200
+ - [Migrate 0.5 → 0.6](migrate-to-0.6.md): Node 22 floor, folded 0.5.7 host delta, third-party floors, and upgrade/rollback steps.
188
201
  - [Migrate 0.5](migrate-to-0.5.md): 0.4 → 0.5 migration guide with per-release sections and rollback.
189
202
  - [Documentation archive](history/README.md): frozen migration/history records — not read on the hot path.
190
203
  - [Review coverage archive](_evidence/): per-phase evidence freezes — audit trail, excluded from tarballs.
@@ -198,14 +211,14 @@ The generated inventory below derives from [`scripts/package-truth.json`](../scr
198
211
 
199
212
  | package | version | notes |
200
213
  | --- | --- | --- |
201
- | `@arnilo/prism` | 0.5.6 | core — runtime, CLI/RPC, templates, docs |
202
- | `@arnilo/prism-coding-tools` | 0.5.6 | family — /agent, /security, /document-reader, /openapi, /computer-use-linux, /dev, /caveman, /ponytail, /impeccable subpaths |
203
- | `@arnilo/prism-core` | 0.5.6 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /work, /validation subpaths |
204
- | `@arnilo/prism-providers` | 0.5.6 | family — all provider adapters as `/<adapter>` subpaths |
205
- | `@arnilo/prism-acp-agent` | 0.5.6 | capability — ACP adapter |
206
- | `@arnilo/prism-ag-ui` | 0.5.6 | capability — AG-UI/A2A/A2UI adapter |
207
- | `@arnilo/prism-mcp` | 0.5.6 | capability — MCP client/server/OAuth interop |
208
- | `@arnilo/prism-memory` | 0.5.6 | capability — memory plus /rag, /compaction/*, /graft, /wiki subpaths |
209
- | `@arnilo/prism-office` | 0.5.6 | capability — /documents, /sheets, /diagrams subpaths |
210
- | `@arnilo/prism-web-tools` | 0.5.6 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
214
+ | `@arnilo/prism` | 0.6.0 | core — runtime, CLI/RPC, templates, docs |
215
+ | `@arnilo/prism-coding-tools` | 0.6.0 | family — /agent, /security, /document-reader, /openapi, /computer-use-linux, /dev, /caveman, /ponytail, /impeccable subpaths |
216
+ | `@arnilo/prism-core` | 0.6.0 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /work, /validation subpaths |
217
+ | `@arnilo/prism-providers` | 0.6.0 | family — all provider adapters as `/<adapter>` subpaths |
218
+ | `@arnilo/prism-acp-agent` | 0.6.0 | capability — ACP adapter |
219
+ | `@arnilo/prism-ag-ui` | 0.6.0 | capability — AG-UI/A2A/A2UI adapter |
220
+ | `@arnilo/prism-mcp` | 0.6.0 | capability — MCP client/server/OAuth interop |
221
+ | `@arnilo/prism-memory` | 0.6.0 | capability — memory plus /rag, /compaction/*, /graft, /wiki subpaths |
222
+ | `@arnilo/prism-office` | 0.6.0 | capability — /documents, /sheets, /diagrams subpaths |
223
+ | `@arnilo/prism-web-tools` | 0.6.0 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
211
224
  <!-- generated:package-truth:inventory end -->
@@ -62,8 +62,8 @@ Useful exported types:
62
62
  - `InputAttachment`: already-loaded text/content blocks (including `audio`, `file`, and `document`) or an explicit URI loaded through a caller-provided `ResourceLoader`.
63
63
  - `PromptInstruction`: labeled system instruction text.
64
64
  - `DefaultPromptBuilder`: the default `PromptBuilder`; cache-aware by default and legacy-preserving when `inputLayout: "legacy"` is passed in its request.
65
- - `AssembleProviderInputOptions`: model, input, optional builders, context providers, selected skills, active tools, generic provider options, metadata, signal, and optional `contextBudget` (`maxInputTokens` / `maxInputBytes` / `reportOmissions`).
66
- - `applyContextBudget` / `getContextBudgetReport` / `resolveContextBudget`: deterministic eviction + omission report helpers (estimate = UTF-16 code units ÷ 4).
65
+ - `AssembleProviderInputOptions`: model, input, optional builders, context providers, selected skills, active tools, generic provider options, metadata, signal, and optional `contextBudget` (`maxInputTokens` / `maxInputBytes` / `reportOmissions` / `tokenEstimator`).
66
+ - `applyContextBudget` / `getContextBudgetReport` / `resolveContextBudget`: deterministic eviction + omission report helpers (estimate = UTF-16 code units ÷ 4, or the host's `tokenEstimator`).
67
67
  - `PromptTemplateOptions`: missing-variable behavior for `renderPromptTemplate()`.
68
68
 
69
69
  ## Outputs / response / events
@@ -88,10 +88,10 @@ In cache-aware mode, leading system instructions form the stable boundary before
88
88
  - History is prepended before current input.
89
89
  - Instructions and summaries are system messages; compacted branch summaries from `rebuildSessionContext()` use the same path.
90
90
  - Text attachments and explicit text resources are user messages; inline `audio`/`file`/`document` blocks pass through unchanged on attachments with `content`.
91
- - Tool results are tool messages containing `tool_result` content; the agent/session runtime uses this to feed dispatched tool results into the next provider turn, placing the assistant `tool_call` and the matching role `tool` `tool_result` before any final assistant content. Cache-aware layout keeps tool results before the current user suffix so it does not split tool transcripts.
91
+ - Tool results are tool messages containing `tool_result` content; the agent/session runtime uses this to feed dispatched tool results into the next provider turn, placing the assistant `tool_call` and the matching role `tool` `tool_result` before any final assistant content. Cache-aware layout keeps tool results before the current user suffix so it does not split tool transcripts. A result with no `value`, no `type:text` content, and no error carries the constant `EMPTY_TOOL_RESULT_TEXT` (`"(tool completed with no output)"`) as its `result`, so no provider route serializes an empty or absent tool payload.
92
92
  - Middleware runs only when `middleware` is supplied in the context.
93
93
  - `assembleProviderInput()` returns a `ProviderRequest` with the caller's model/tools/provider options/metadata/signal and composed messages/context. It stamps missing `sessionId`/`cacheKey` via `applyDefaultProviderRequestOptions` when `sessionId` is passed (agent sessions always pass `session.id`). It also calls `assertMessagesSupportModelCapabilities()` so unsupported `audio`/`file`/`document`/`image` blocks fail with `UnsupportedModalityError` when the model declares `capabilities.input`.
94
- - Optional `contextBudget` (at least one of `maxInputTokens` / `maxInputBytes`) runs after default message groups are built and before final flatten. Eviction drops droppable sections first (toolResults → history → summaries → context → skills → attachments; layout-aware). Within `history`, oldest messages drop first. Protected instructions + current user `input` (+ tools catalog) fail closed with `ContextBudgetError` if they alone exceed the budget. When `reportOmissions: true`, attach `ProviderRequest.metadata[CONTEXT_BUDGET_REPORT_METADATA_KEY]` and read via `getContextBudgetReport(request)` (kinds/ids/sizes only — no secrets). Raw session store entries are never deleted.
94
+ - Optional `contextBudget` (at least one of `maxInputTokens` / `maxInputBytes`) runs after default message groups are built and before final flatten. `tokenEstimator` replaces the built-in ÷4 heuristic for **eviction accounting only** — it never reaches billing, provider usage, or the wire, byte caps (`maxInputBytes`) stay estimator-independent and are always enforced, and an estimator that returns a non-finite or negative count (or is not a function) fails the assembly closed with `TypeError` instead of making eviction decisions unsound. Eviction drops droppable sections first (toolResults → history → summaries → context → skills → attachments; layout-aware). Within `history`, oldest messages drop first. Protected instructions + current user `input` (+ tools catalog) fail closed with `ContextBudgetError` if they alone exceed the budget. When `reportOmissions: true`, attach `ProviderRequest.metadata[CONTEXT_BUDGET_REPORT_METADATA_KEY]` and read via `getContextBudgetReport(request)` (kinds/ids/sizes only — no secrets). Raw session store entries are never deleted.
95
95
  - `renderPromptTemplate()` replaces top-level `{{name}}` variables with caller-supplied JSON-compatible values. Strings are inserted directly; numbers, booleans, `null`, arrays, and objects are stringified deterministically with sorted object keys. Missing variables throw by default or stay unchanged with `{ missing: "preserve" }`.
96
96
 
97
97
  ## Request/response example