@arnilo/prism 0.3.0 → 0.3.2

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 (77) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +3 -1
  3. package/dist/agent-definitions.js +4 -1
  4. package/dist/agent-loops.js +45 -8
  5. package/dist/agent-session/helpers.js +2 -2
  6. package/dist/cache-helpers.d.ts +11 -0
  7. package/dist/cache-helpers.js +29 -5
  8. package/dist/cli-provider-add.js +2 -1
  9. package/dist/context-budget.js +9 -6
  10. package/dist/contracts-core/agent.d.ts +25 -2
  11. package/dist/contracts-core/provider.d.ts +2 -0
  12. package/dist/event-multiplexer.js +0 -4
  13. package/dist/index.d.ts +6 -4
  14. package/dist/index.js +5 -3
  15. package/dist/input.js +19 -11
  16. package/dist/node/session-store-jsonl.js +7 -3
  17. package/dist/providers/openai-compatible.js +2 -1
  18. package/dist/providers/openai-primitives.js +2 -1
  19. package/dist/providers/schema.d.ts +7 -0
  20. package/dist/providers/schema.js +25 -0
  21. package/dist/rpc.d.ts +4 -1
  22. package/dist/rpc.js +5 -1
  23. package/dist/testing/provider-conformance.d.ts +10 -0
  24. package/dist/testing/provider-conformance.js +37 -0
  25. package/dist/trim-trailing-slashes.d.ts +8 -0
  26. package/dist/trim-trailing-slashes.js +14 -0
  27. package/docs/0.1.0-readiness.md +1 -1
  28. package/docs/acp.md +1 -0
  29. package/docs/ag-ui.md +1 -0
  30. package/docs/agent-definitions.md +1 -1
  31. package/docs/agent-loops.md +3 -0
  32. package/docs/agent-session-runtime.md +1 -0
  33. package/docs/browser-automation.md +1 -0
  34. package/docs/coding-agent-tools.md +7 -1
  35. package/docs/compaction-and-retry.md +3 -0
  36. package/docs/compaction-observational-memory.md +47 -0
  37. package/docs/database-persistence.md +1 -1
  38. package/docs/extension-authoring.md +42 -0
  39. package/docs/graft.md +125 -0
  40. package/docs/host-security.md +3 -1
  41. package/docs/index.md +15 -13
  42. package/docs/input-and-prompt-assembly.md +11 -6
  43. package/docs/instruction-injection.md +1 -1
  44. package/docs/mcp-tools.md +1 -0
  45. package/docs/migration.md +10 -0
  46. package/docs/node-jsonl-session-store.md +1 -1
  47. package/docs/obscura.md +175 -0
  48. package/docs/observability.md +21 -1
  49. package/docs/performance.md +58 -4
  50. package/docs/ponytail.md +1 -1
  51. package/docs/provider-caching.md +13 -11
  52. package/docs/provider-conformance.md +6 -0
  53. package/docs/provider-packages.md +1 -1
  54. package/docs/provider-primitives.md +15 -2
  55. package/docs/providers/ai-sdk.md +1 -1
  56. package/docs/providers/anthropic.md +1 -1
  57. package/docs/providers/azure.md +1 -0
  58. package/docs/providers/bedrock.md +1 -0
  59. package/docs/providers/kimi.md +2 -1
  60. package/docs/providers/openai.md +19 -7
  61. package/docs/providers/opencode-go.md +3 -1
  62. package/docs/providers/openrouter.md +4 -3
  63. package/docs/providers/vertex.md +1 -0
  64. package/docs/public-contracts.md +3 -2
  65. package/docs/rag.md +55 -8
  66. package/docs/release-and-install.md +63 -7
  67. package/docs/server.md +1 -0
  68. package/docs/supervisors.md +10 -2
  69. package/docs/system-prompts.md +1 -1
  70. package/docs/tools.md +1 -1
  71. package/docs/web-tools.md +2 -0
  72. package/docs/wiki.md +154 -0
  73. package/docs/workflows.md +38 -4
  74. package/docs/working-and-semantic-memory.md +20 -0
  75. package/package.json +12 -5
  76. package/docs/api-page-template.md +0 -32
  77. package/docs/release-0.2.7-evidence.md +0 -514
@@ -1,3 +1,4 @@
1
+ import { canonicalizeJsonSchema } from "./schema.js";
1
2
  export function assertOpenAIChatMessage(message, path) {
2
3
  if (!message || typeof message !== "object") {
3
4
  throw new Error(`Invalid provider message at ${path}: expected object`);
@@ -46,7 +47,7 @@ export function serializeOpenAITool(tool) {
46
47
  function: {
47
48
  name: tool.name,
48
49
  description: tool.description,
49
- parameters: tool.parameters ?? { type: "object" },
50
+ parameters: canonicalizeJsonSchema(tool.parameters ?? { type: "object" }),
50
51
  },
51
52
  };
52
53
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Deterministic JSON Schema clone for tool/function parameters.
3
+ * Sorts object keys and unordered `required` names. Leaves semantic arrays
4
+ * (`prefixItems`, `examples`, `enum`, tuple `items`) in caller order.
5
+ * Does not resolve `$ref`, mutate input, or enforce schema bounds.
6
+ */
7
+ export declare function canonicalizeJsonSchema(value: unknown): unknown;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Deterministic JSON Schema clone for tool/function parameters.
3
+ * Sorts object keys and unordered `required` names. Leaves semantic arrays
4
+ * (`prefixItems`, `examples`, `enum`, tuple `items`) in caller order.
5
+ * Does not resolve `$ref`, mutate input, or enforce schema bounds.
6
+ */
7
+ export function canonicalizeJsonSchema(value) {
8
+ if (Array.isArray(value))
9
+ return value.map(canonicalizeJsonSchema);
10
+ if (!value || typeof value !== "object")
11
+ return value;
12
+ return Object.fromEntries(Object.entries(value)
13
+ .sort(([left], [right]) => compareKey(left, right))
14
+ .map(([key, item]) => [key, key === "required" ? canonicalizeRequired(item) : canonicalizeJsonSchema(item)]));
15
+ }
16
+ function canonicalizeRequired(item) {
17
+ if (Array.isArray(item) && item.every((entry) => typeof entry === "string")) {
18
+ return [...item].sort(compareKey);
19
+ }
20
+ return canonicalizeJsonSchema(item);
21
+ }
22
+ function compareKey(left, right) {
23
+ return left < right ? -1 : left > right ? 1 : 0;
24
+ }
25
+ //# sourceMappingURL=schema.js.map
package/dist/rpc.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Readable, Writable } from "node:stream";
2
- import type { AgentSession, CommandDefinition, InstructionInjector } from "./contracts.js";
2
+ import type { AgentSession, CommandDefinition, CommandDrivers, InstructionInjector } from "./contracts.js";
3
3
  import type { ContributionRegistry } from "./contributions.js";
4
4
  export type RpcCommandName = "prompt" | "steer" | "followUp" | "abort" | "state" | "messages" | "setModel" | "compact" | "switchSession" | "forkSession" | "cloneSession" | "checkout" | "command";
5
5
  export interface RpcRequest {
@@ -10,6 +10,9 @@ export interface RpcRequest {
10
10
  export interface RpcSessionFactory {
11
11
  createSession(id?: string): AgentSession;
12
12
  readonly commands?: readonly CommandDefinition[];
13
+ /** Host-opt-in driver capabilities forwarded to contributed commands on the
14
+ * `command` execution context. Absent ⇒ context shape unchanged. */
15
+ readonly drivers?: CommandDrivers;
13
16
  /** Optional registry for resolving `instructionInjectors` names in `prompt`/`followUp`
14
17
  * params (Phase 30). Names resolve fail-closed. */
15
18
  readonly instructionInjectors?: ContributionRegistry<InstructionInjector>;
package/dist/rpc.js CHANGED
@@ -9,6 +9,7 @@ export async function runRpcServer(options) {
9
9
  sessions: new Map([[first.id, first]]),
10
10
  commands: new Map((options.commands ?? []).map((command) => [command.name, command])),
11
11
  createSession: options.createSession,
12
+ ...(options.drivers ? { drivers: options.drivers } : {}),
12
13
  ...(options.instructionInjectors ? { instructionInjectors: options.instructionInjectors } : {}),
13
14
  };
14
15
  const activeRuns = new Map();
@@ -181,7 +182,10 @@ async function handleRequest(request, state, stdout, activeRuns) {
181
182
  if (!command)
182
183
  throw new Error(`Unknown command: ${name}`);
183
184
  const args = objectParam(request.params, "args") ?? {};
184
- const result = await command.execute(args, { sessionId: state.current.id });
185
+ const result = await command.execute(args,
186
+ // FEATURE-3 (plan 050 Task 5): host-opt-in drivers forwarded verbatim; the
187
+ // context carries no `drivers` key when the host supplied none.
188
+ state.drivers ? { sessionId: state.current.id, drivers: state.drivers } : { sessionId: state.current.id });
185
189
  write(stdout, { id: request.id, ok: true, result });
186
190
  break;
187
191
  }
@@ -38,6 +38,16 @@ export declare function assertProviderStreamConforms(options: ProviderStreamConf
38
38
  export declare function assertAbortIsObserved(options: ProviderAbortConformanceOptions): Promise<void>;
39
39
  export declare function assertToolCallDeltasReconstruct(events: readonly ProviderEvent[], expected: readonly ToolCallDeltaExpectation[]): readonly ToolCallContent[];
40
40
  export declare function assertSerializedRequestCoversContent(request: ProviderRequest, body: unknown, options?: SerializedContentCoverageOptions): void;
41
+ export declare function assertCanonicalToolParameters(serialized: unknown, original: unknown): void;
41
42
  export declare function assertProviderOwnedHeadersWin(captured: Headers, options: ProviderHeaderOwnershipConformanceOptions): void;
42
43
  export declare function assertNoSecretLeak(events: readonly ProviderEvent[], secrets: readonly string[]): void;
44
+ /**
45
+ * Implicit/none-cache providers must serialize no foreign cache fields: implicit
46
+ * caching works by byte-stable prefix reuse, not request payloads. `allowed` names
47
+ * fields the provider documents for that route (e.g. `cachedContent` via the host
48
+ * `extra.cachedContent` escape hatch on Gemini).
49
+ */
50
+ export declare function assertNoForeignCacheFields(body: unknown, allowed?: readonly string[]): void;
51
+ /** Provider construction and setup must perform zero network calls; discovery and streams are caller-gated. */
52
+ export declare function assertNoFetches(calls: readonly unknown[]): void;
43
53
  export declare function assertUsageAccounting(events: readonly ProviderEvent[], expected: Usage): Usage;
@@ -1,4 +1,5 @@
1
1
  import { reconstructToolCallDeltas } from "../provider-events.js";
2
+ import { canonicalizeJsonSchema } from "../providers/schema.js";
2
3
  export async function collectProviderEvents(provider, request) {
3
4
  const events = [];
4
5
  for await (const event of provider.generate(request))
@@ -63,6 +64,12 @@ export function assertSerializedRequestCoversContent(request, body, options = {}
63
64
  }
64
65
  }
65
66
  }
67
+ export function assertCanonicalToolParameters(serialized, original) {
68
+ const expected = canonicalizeJsonSchema(original ?? { type: "object" });
69
+ if (JSON.stringify(serialized) !== JSON.stringify(expected)) {
70
+ throw new Error("Tool parameters were not canonicalized");
71
+ }
72
+ }
66
73
  export function assertProviderOwnedHeadersWin(captured, options) {
67
74
  const ownedLower = {};
68
75
  for (const [name, expected] of Object.entries(options.owned))
@@ -89,6 +96,36 @@ export function assertNoSecretLeak(events, secrets) {
89
96
  throw new Error(`Secret leaked into provider events: ${secret.slice(0, 8)}...`);
90
97
  }
91
98
  }
99
+ /** Known cache wire fields across protocols; any of these in a request body is an explicit cache control. */
100
+ const CACHE_WIRE_FIELDS = [
101
+ "cache_control",
102
+ "prompt_cache_key",
103
+ "prompt_cache_retention",
104
+ "prompt_cache_options",
105
+ "prompt_cache_breakpoint",
106
+ "cachedContent",
107
+ "cachePoint",
108
+ ];
109
+ /**
110
+ * Implicit/none-cache providers must serialize no foreign cache fields: implicit
111
+ * caching works by byte-stable prefix reuse, not request payloads. `allowed` names
112
+ * fields the provider documents for that route (e.g. `cachedContent` via the host
113
+ * `extra.cachedContent` escape hatch on Gemini).
114
+ */
115
+ export function assertNoForeignCacheFields(body, allowed = []) {
116
+ const bodyText = JSON.stringify(body);
117
+ for (const field of CACHE_WIRE_FIELDS) {
118
+ if (allowed.includes(field))
119
+ continue;
120
+ if (bodyText.includes(field))
121
+ throw new Error(`Serialized request carries foreign cache field "${field}"`);
122
+ }
123
+ }
124
+ /** Provider construction and setup must perform zero network calls; discovery and streams are caller-gated. */
125
+ export function assertNoFetches(calls) {
126
+ if (calls.length > 0)
127
+ throw new Error(`Provider fetched ${calls.length} time(s) outside caller-gated discovery/stream`);
128
+ }
92
129
  export function assertUsageAccounting(events, expected) {
93
130
  const usage = [...events].reverse().find((event) => (event.type === "done" && event.usage) || event.type === "usage");
94
131
  const actual = usage?.type === "usage" ? usage.usage : usage?.usage;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Trims trailing "/" characters with a linear index scan.
3
+ *
4
+ * Shared replacement for `value.replace(/\/+$/, "")` (CodeQL js/polynomial-redos):
5
+ * no regex is evaluated, so hostile long inputs cannot backtrack. Semantics are
6
+ * identical — only trailing "/" characters (U+002F) are removed; "" and "/" stay "".
7
+ */
8
+ export declare function trimTrailingSlashes(value: string): string;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Trims trailing "/" characters with a linear index scan.
3
+ *
4
+ * Shared replacement for `value.replace(/\/+$/, "")` (CodeQL js/polynomial-redos):
5
+ * no regex is evaluated, so hostile long inputs cannot backtrack. Semantics are
6
+ * identical — only trailing "/" characters (U+002F) are removed; "" and "/" stay "".
7
+ */
8
+ export function trimTrailingSlashes(value) {
9
+ let end = value.length;
10
+ while (end > 0 && value.charCodeAt(end - 1) === 47)
11
+ end -= 1;
12
+ return value.slice(0, end);
13
+ }
14
+ //# sourceMappingURL=trim-trailing-slashes.js.map
@@ -20,7 +20,7 @@ Historical release lines (0.0.16 floor → 0.0.27 Phase 10 ACP interop → 0.1.0
20
20
  keep their per-phase evidence in the pages above; this page records the 0.2.6
21
21
  snapshot (plan 026) with the 0.1.x tables below as the historical record.
22
22
 
23
- ## Current line (0.3.0)
23
+ ## Current line (0.3.2)
24
24
 
25
25
  | Item | Status |
26
26
  |---|---|
package/docs/acp.md CHANGED
@@ -164,3 +164,4 @@ const agent = createPrismAcpAgent({
164
164
  - [Host security guide](host-security.md): fail-closed checklist rows for ACP boundaries (authorize, ownership, redaction, untrusted MCP).
165
165
  - [Migration guide](migration.md): 0.0.26 → 0.0.27 advertise/surface changes for hosts that parsed the old `initialize`.
166
166
  - [AG-UI adoption evaluation](ag-ui-adoption.md): the underlying input/event/capability matrix.
167
+ - [Obscura browser engine](obscura.md): optional binary-backed generic tools behind the session prompt loop.
package/docs/ag-ui.md CHANGED
@@ -221,6 +221,7 @@ Defaults / hard caps: request 64 KiB / 1 MiB; input 128 / 1024 messages, 32 / 25
221
221
  - [A2A interoperability](a2a.md): remote agent-to-agent tasks, not frontend protocol mapping.
222
222
  - [AG-UI adoption evaluation](ag-ui-adoption.md): official 0.0.57 event/input matrix and shipped explicit MCP/MCP Apps/A2A handshakes.
223
223
  - [ACP coding-host interop](acp.md): the full ACP reference — seam-based capability advertisement, session modes/config, MCP select, fs/terminal adapters, lifecycle mapping, elicitation, and caps.
224
+ - [Obscura browser engine](obscura.md): optional binary-backed generic tools selectable through the MCP adapter.
224
225
  - [MCP bridge/server](mcp-tools.md): `mcpApps` negotiation, bounded resources, and remote tool trust.
225
226
  - [A2A interoperability](a2a.md): verified rich task client and remote task lifecycle.
226
227
  - [Host security guide](host-security.md): authorization, ownership, redaction, and credential boundaries.
@@ -27,7 +27,7 @@ Do not use the bundle loader to discover providers — provider/model packages s
27
27
  | --- | --- |
28
28
  | `name` | Required agent name. |
29
29
  | `description?` | Optional description. |
30
- | `model?` | `ModelConfig` object, or a `"<provider>/<model>"` string resolved through `registries.models`. |
30
+ | `model?` | `ModelConfig` object, or a `"<provider>/<model>"` string resolved through `registries.models`. Optional at authoring time: when omitted, resolution falls back to `context.overrides.model` (host-injected selection); an explicit definition `model` drives registry resolution, and neither present fails closed with `Agent "<name>" has no model`. |
31
31
  | `tools?` | Tool names to activate from the active tool registry / `registries.tools`. Omitted means no active tools unless `activateAllCapabilities: true` is passed for migration. |
32
32
  | `skills?` | Skill names resolved via `resolveActiveSkills()`; omitted means no active skills unless `activateAllCapabilities: true` is passed for migration. `toolNames` enforcement applies at activation. |
33
33
  | `context?` | Context provider names from `registries.contextProviders`. |
@@ -128,6 +128,8 @@ Optional steer hooks on `LoopContext` (0.0.11): `hasPendingSteers?()` / `applyPe
128
128
 
129
129
  The snapshot is stored as `loopState: { name, revision, snapshot }` on the durable run state and cleared when the run reaches a terminal status. On resume, a name/revision mismatch between the stored `loopState` and the resolved strategy fails closed (`ERR_PRISM_LOOP_REVISION`), and the fingerprint check independently rejects any loop drift. Suspension occurs only before an input provider call or immediately before a tool side effect; completed provider turns remain in `SessionStore` history and are not repeated after `resumeAgentRun()`.
130
130
 
131
+ A strategy returned by `generateValidateReviseLoop()` is safe to reuse across sequential runs. Its built-in state is scoped to `(sessionId, runId)`; a new non-restored run resets attempts, artifact phase, saved schema, and pending repair messages, while a restored run keeps the checkpointed state. Arbitrary custom strategies are not cloned or reset automatically.
132
+
131
133
  ## Outputs / response / events
132
134
 
133
135
  `AgentLoopStrategy.run(ctx)` returns `Promise<Usage | undefined>` as a fallback for custom loops. Core runtime independently accumulates every usage-bearing provider turn in O(turns), persists scoped turn/run rows, and emits `agent_finished` with the aggregate.
@@ -231,6 +233,7 @@ await session.run(input, { loop: twoShotLoop });
231
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)).
232
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.
233
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.
234
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.
235
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/`.
236
239
 
@@ -221,6 +221,7 @@ Per-run options may narrow `limits` and append `guardrails`; they cannot replace
221
221
  - [Session stores and branching](session-stores-and-branching.md): `SessionStore`, memory store, branch helpers, and context rebuild.
222
222
  - [Compaction and retry policies](compaction-and-retry.md): compaction strategy/config APIs used by `session.compact()` and auto-compaction, plus retry policy/config APIs.
223
223
  - [Tools](tools.md): host-owned tool harness used by the bounded runtime tool loop.
224
+ - [Obscura browser engine](obscura.md): optional binary-backed tool array that composes into `createAgent({ tools })` with no host branch.
224
225
  - [Middleware hooks](middleware-hooks.md): hooks that configured assembly/runtime can run.
225
226
  - [CLI/RPC](cli-rpc.md): terminal and JSONL adapters over this runtime.
226
227
  - [Workflows](workflows.md): optional DAG orchestration that calls `AgentSession.run()` for agent nodes.
@@ -122,6 +122,7 @@ Default tests use fake Playwright APIs only. Protected live gate: `PRISM_LIVE_PL
122
122
 
123
123
  ## Related APIs
124
124
 
125
+ - [Obscura browser engine](obscura.md): optional host-installed Obscura headless browser connected with `chromium.connectOverCDP` through `connectObscuraCdp` — its returned browser plugs directly into `createBrowserTools`/`createBrowserManager` as the host-supplied Playwright browser; pages on one Obscura worker share one V8 isolate, and screenshots/PDF need a render-enabled build.
125
126
  - [Tools](tools.md): registry, exclusive dispatch, validation, and ledger.
126
127
  - [Web search, fetch, and extraction](web-tools.md): preferred non-interactive retrieval path.
127
128
  - [Guardrails](guardrails.md): untrusted external content handling.
@@ -417,6 +417,12 @@ Opt-in `ask_user_decision` for ambiguous, high-impact direction choices. Model m
417
417
  | Agent durable adapter | `validateAskUserDecisionAgentResume({ request, answer })` — same validation; **no** new `AgentRunInterruption` kinds in 0.0.11 |
418
418
 
419
419
  Custom-text caps match question defaults (2 KiB / hard 8 KiB). Options default max 6 (hard 16).
420
+ `allowCustom` defaults to `false` on **both** paths when omitted — the tool
421
+ path (`parseAllowCustom`) and the workflow suspend path
422
+ (`toAskUserDecisionSuspendData`) normalize at accept time, so the persisted
423
+ suspension always carries a boolean and survives JSON checkpoint round-trips;
424
+ a non-boolean value throws `allowCustom must be a boolean` at accept time,
425
+ never at resume time.
420
426
 
421
427
  ```ts
422
428
  import { createToolRegistry } from "@arnilo/prism";
@@ -440,7 +446,7 @@ return suspendAskUserDecision({
440
446
  question: "Ship sqlite or postgres?",
441
447
  options: [/* ≥2 with 3 pros + 3 cons each */],
442
448
  selectionMode: "single",
443
- allowCustom: false,
449
+ // allowCustom optional — defaults to false (tool-path parity)
444
450
  });
445
451
  // resumeWorkflow(..., { validateResume: createAskUserDecisionResumeValidator() })
446
452
  ```
@@ -93,6 +93,8 @@ createDefaultRetryPolicy(options?: DefaultRetryPolicyOptions): RetryPolicy
93
93
 
94
94
  `session.compact(options?)` emits `compaction_started`, runs the strategy on the current branch, runs `middleware.run("compaction", { context, result })` when middleware is configured, appends one standard `kind: "compaction"` entry under the current leaf, emits `compaction_finished`, and returns the appended result. Manual compaction rejects while a run is active.
95
95
 
96
+ > **Contract — compact at the task boundary.** `session.compact()` throws `Error("Agent session already has an active run")` while `run()`/`stream()` is in flight. Intended model: one `run()` per task, then compact. Do not design mid-run compaction. Auto-compaction (when `thresholdEntries` is set) already runs **before** provider input, not during the turn. Live demo: [`examples/autonomous-coding-loop.ts`](../examples/autonomous-coding-loop.ts) (`compact` node after execute/validate/gate).
97
+
96
98
  Auto-compaction checks at most once per `run()`, after input/model-change entries are appended and before provider input assembly. It runs only when `AgentConfig.compaction` or `RunOptions.compaction` supplies `thresholdEntries`, and it is skipped by `RunOptions.compaction: false`.
97
99
 
98
100
  `rebuildSessionContext()` detects the latest compaction entry on a branch. Its returned `entries` still contains the raw full branch, while `messages` contains only messages after the compaction boundary plus `keepEntryIds`, and `summaries` contains the compaction summary plus later summary entries.
@@ -170,6 +172,7 @@ The default strategy does not call a provider. Hosts that need model-generated s
170
172
  - [Session stores and branching](session-stores-and-branching.md): branch entries, compaction entries, and `rebuildSessionContext()` behavior.
171
173
  - [Input and prompt assembly](input-and-prompt-assembly.md): compacted summaries become default summary messages for provider input.
172
174
  - [Agent/session runtime](agent-session-runtime.md): `session.compact()`, opt-in auto-compaction, `RunOptions.retry`, and `retry_scheduled` runtime behavior.
175
+ - Example: [`examples/autonomous-coding-loop.ts`](../examples/autonomous-coding-loop.ts) — task-boundary compact after each iteration.
173
176
  - [Middleware hooks](middleware-hooks.md): `compaction` and `retry` middleware payload timing.
174
177
  - [Contribution registries](contribution-registries.md): compaction strategy and retry policy contributions.
175
178
  - [Configuration and manifests](configuration-and-manifests.md): `compactionStrategy` and `retryPolicy` manifest contribution kinds.
@@ -166,6 +166,52 @@ The runtime requires host-supplied `session`, an `appendEntry` callback bound to
166
166
 
167
167
  `createObservationalMemoryExtension()` registers only inert contributions. It does not start workers, compact sessions, read settings, resolve credentials, call providers, or execute tools/commands during setup.
168
168
 
169
+ ## Cross-session / delegation-tree recall (opt-in pattern)
170
+
171
+ Default is per-session: `attach()` + `appendEntry` bind one store/branch, and `recallObservationalMemory(entries, id)` / `createRecallMemoryTool({ getEntries })` see only the entries the host passes for that session. Supervisor children therefore produce observations the parent cannot recall. That is acceptable for v1 — the parent transcript already contains `delegate()` results, so parent OM covers milestones. There is no package primitive for a shared workspace scope (a namespaced multi-tenant store key is out of scope).
172
+
173
+ Hosts that need parent recall of child *source* work compose it themselves: wrap the shared `SessionStore.append` so eligible child messages (`isEligibleObservationSourceEntry`) are copied onto a workspace (or parent) session with a **new entry id** and that session's `sessionId`/`parentId`. Parent OM then observes those copies and mints **new** observation ids. Child OM, if attached, stays on the child session with its own ids.
174
+
175
+ ```ts
176
+ import { createId, type SessionStore } from "@arnilo/prism";
177
+ import { isEligibleObservationSourceEntry } from "@arnilo/prism-compaction-observational-memory";
178
+
179
+ function funnelChildMessagesToWorkspace(store: SessionStore, workspaceSessionId: string): SessionStore {
180
+ return {
181
+ async append(entry, options) {
182
+ await store.append(entry, options);
183
+ if (entry.sessionId === workspaceSessionId) return;
184
+ if (!isEligibleObservationSourceEntry(entry)) return;
185
+ const leaf = (await store.list(workspaceSessionId)).at(-1);
186
+ await store.append({
187
+ ...entry,
188
+ id: createId("entry"),
189
+ sessionId: workspaceSessionId,
190
+ parentId: leaf?.id,
191
+ });
192
+ },
193
+ list: (sessionId) => store.list(sessionId),
194
+ get: (id) => store.get?.(id) ?? Promise.resolve(undefined),
195
+ searchSessions: (query) => store.searchSessions?.(query) ?? Promise.reject(new Error("searchSessions unsupported")),
196
+ readBranchPath: store.readBranchPath?.bind(store),
197
+ };
198
+ }
199
+ ```
200
+
201
+ Wire the wrapped store into both the parent session and each supervisor child factory (`createAgent({ store })`). Parent `attach({ appendEntry: (entry, options) => store.append(entry, options) })` and `createRecallMemoryTool({ getEntries: () => parentSession.entries() })` then see funneled child messages plus parent-minted observations. Recreate the parent session with the store `leafId` after a restart so the workspace branch is the one that received the copies. [`examples/autonomous-coding-loop.ts`](../examples/autonomous-coding-loop.ts) shows parent OM attach/compact/recall in the supervisor loop; it records child outcomes on the parent session (same recall, no extra store wrap).
202
+
203
+ Rules that keep exact-id recall unambiguous:
204
+
205
+ - Recall always takes **one** branch (`session.entries()` / `getEntries(sessionId)`). Never concatenate parent + child lists into one `recallObservationalMemory()` call.
206
+ - Copies mint a new `entry.id`. `createMemorySessionStore` rejects duplicate ids globally; JSONL/DB adapters do too.
207
+ - Do **not** rewrite the child's OM `appendEntry` onto the workspace session. After each memory append the runtime checks the entry is visible at the **child** leaf and fails closed on a session/store mismatch. Funnel messages; let parent OM observe them.
208
+ - Do **not** copy `om.*` custom entries across. Their `sourceEntryIds` point at the origin session and would dangle on the workspace branch.
209
+ - Serialize funnel copies if concurrent children share the workspace tip (the sketch's `list().at(-1)` is not a lock).
210
+
211
+ Cost: the workspace branch grows with every funneled child message; parent `compactAfterTokens` / observation-pool caps still apply but fire sooner. Keep the per-session default unless parent recall of child sources is required.
212
+
213
+ Ownership: funnel only within the `OwnershipScope` already on the parent agent/store. Child factories receive that ownership from the supervisor; do not share a store across tenants or identities. Observations never leave the store the host scoped.
214
+
169
215
  ## Security and performance notes
170
216
 
171
217
  - Recall is exact-id only; there is no semantic search, vector store, or transcript browser.
@@ -187,6 +233,7 @@ The runtime requires host-supplied `session`, an `appendEntry` callback bound to
187
233
  - [Compaction and retry policies](compaction-and-retry.md): replaceable compaction strategy boundary.
188
234
  - [LLM compaction package](compaction-llm.md): existing optional compaction-package pattern.
189
235
  - [Session stores and branching](session-stores-and-branching.md): branch entries that observational memory reads and appends to.
236
+ - [Supervisor delegation](supervisors.md): child sessions whose messages this page's opt-in funnel can copy onto a workspace branch.
190
237
  - [Extensions](extensions.md): inert registration pattern for optional package contributions.
191
238
  - [Tools](tools.md): host activation and dispatch for optional recall tool contributions.
192
239
  - [CLI/RPC](cli-rpc.md): command contributions through explicitly wired RPC hosts.
@@ -389,7 +389,7 @@ For document or wide-column stores, map the relational tables above to the store
389
389
  - **Branches:** In document stores, a branch can be a lightweight document keyed by `leaf_entry_id` that points to the session and root. Rebuild still walks `parent_id` links in entries.
390
390
  - **Retention:** Use TTL columns or scheduled map-reduce/streaming jobs. TTL on `expires_at` or entry timestamps is the simplest NoSQL implementation.
391
391
 
392
- The Node JSONL session store is a single-process development adapter. It has no cross-process locking, no migrations, no retention enforcement, and no tenant isolation. Do not use it as a production multi-writer store.
392
+ The Node JSONL session store is a single-process development adapter. It has no cross-process locking, no migrations, no retention enforcement, and no tenant isolation. Do not use it as a production multi-writer store. Appends serialize per instance; a rejected append (conflict, duplicate, corrupt file) does not poison later appends on that instance. The rejected write is not committed.
393
393
 
394
394
  ## Request/response example
395
395
 
@@ -168,6 +168,48 @@ await agent.createSession().run("Use the Acme extension.", { activeSkills: ["acm
168
168
  - Middleware from `api.use()` runs only when the host passes `kernel.middleware` into runtime configuration.
169
169
  - Provider packages, provider request policies, system prompt contributions, instruction injectors, builders, strategies, commands, store factories, resource loaders, settings providers, and credential resolvers are all inert until host code selects or invokes them.
170
170
 
171
+ ### Host driver hooks (opt-in)
172
+
173
+ A contributed command can act — start a session run, start a workflow, steer
174
+ an active run — only when the **host** injects driver capabilities into the
175
+ execution context. Drivers are never package-supplied: a command that wants
176
+ them guards on `context.drivers` and degrades gracefully when the host
177
+ supplies none. Commands stay inert data in hosts without drivers, and the
178
+ context shape is unchanged (no `drivers` key at all).
179
+
180
+ ```ts
181
+ // Host opt-in (e.g. RPC session factory):
182
+ await runRpcServer({
183
+ stdin,
184
+ stdout,
185
+ createSession,
186
+ commands,
187
+ drivers: {
188
+ startRun: (input, options) => session.run(input, options),
189
+ startWorkflow: (workflow, input, options) => runWorkflow(workflow, input, options),
190
+ steer: (runId, input) => session.steer(runId, input),
191
+ },
192
+ });
193
+
194
+ // Contributed command (host-opt-in capability use):
195
+ registerCommand({
196
+ name: "acme.start",
197
+ async execute(args, context) {
198
+ if (!context.drivers?.startWorkflow) {
199
+ return { name: "acme.start", error: { message: "host did not supply workflow drivers" } };
200
+ }
201
+ const run = await context.drivers.startWorkflow(workflowFor(args), args.input);
202
+ return { name: "acme.start", value: { runId: run.runId, status: run.status } };
203
+ },
204
+ });
205
+ ```
206
+
207
+ `CommandDrivers` is typed (`startRun` / `startWorkflow` / `steer`) and exported
208
+ from the core contracts surface. Driver errors surface through the command's
209
+ normal error path — commands map failures to `CommandResult.error`
210
+ (`ErrorInfo`) or let the host error envelope carry them. Driver presence does
211
+ not affect command `metadata.trust` labeling.
212
+
171
213
  ## Security and performance notes
172
214
 
173
215
  - Prism does not sandbox extension code. Hosts should load only trusted packages or run untrusted packages in their own sandbox/process before calling Prism APIs.
package/docs/graft.md ADDED
@@ -0,0 +1,125 @@
1
+ # Graft context-graph integration
2
+
3
+ ## What it does
4
+
5
+ `@arnilo/prism-graft` is an optional package 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.
6
+
7
+ It registers six pull tools backed by the graft CLI (`--json`, argv-safe), a push-mode retrieval-pack context provider plus first-turn orientation injector carried on the `graft` skill, commands (`graft`, `graft-build`, `graft-check`, `graft-viz`), and an edit-watch middleware that computes blast radius after mutating tool calls. Import is inert; a missing graft CLI fails closed at `setup` with a bounded redacted error.
8
+
9
+ ## When to use it
10
+
11
+ Use it when a host wants agents to locate code by architecture, callers, and coupling before grep-spelunking. Three modes:
12
+
13
+ - `"pull"` (default) — register the tools; the agent decides when to query.
14
+ - `"push"` — per-turn retrieval pack (pointers only) + first-turn orientation, injected automatically.
15
+ - `"both"` — everything.
16
+
17
+ Install optional peer `@nanonets/graft@^0.13.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.
18
+
19
+ 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
+
21
+ ## Inputs / request
22
+
23
+ `createGraftExtension(options)`:
24
+
25
+ | Field | Type | Required | Purpose |
26
+ | --- | --- | --- | --- |
27
+ | `cliPath` / `packageRoot` | `string` | no | Explicit stub/binary or checkout root with a manifest-declared bin; default resolves optional peer `@nanonets/graft`. Relative paths rejected; explicit paths existence-checked at resolve time. |
28
+ | `mode` | `"pull" \| "push" \| "both"` | no | Surface selection. Default `pull`. |
29
+ | `projectDir` | `string` | no | Directory graft operates on. Default `process.cwd()` at setup. |
30
+ | `retrievalBudgetMs` | `number` | no | Wall-clock budget per CLI child call (default 8000). |
31
+ | `maxResultBytes` | `number` | no | Stdout cap before parsing (default 512 KiB). |
32
+ | `maxPromptChars` | `number` | no | Prompts longer than this never become ask argv (default 4096). |
33
+ | `allowUpstreamTelemetry` | `boolean` | no | Default false → children run with `DO_NOT_TRACK=1`. |
34
+ | `providerEnv` | `Record<string, string>` | no | Explicit graft provider settings (`GRAFT_API_KEY`, …). Never inherited from host env; only `GRAFT_*` keys reach the child. |
35
+ | `editToolNames` | `readonly string[]` | no | Tools triggering blast-radius lookup. Default `write`, `edit`, `move`. |
36
+ | `quietStartup`, `hideStatus` | `boolean` | no | Suppress startup status events / status reporting. |
37
+ | `appendEntry` | `(entry, opts?) => Promise<void>` | yes | Host session append (OM attach pattern). |
38
+ | `getEntries` | `() => readonly SessionEntry[] \| Promise<...>` | yes | Current branch entries for state restore. |
39
+
40
+ Pull tools (mode includes `pull`): `graft_ask`, `graft_grep`, `graft_callers`, `graft_skeleton`, `graft_map`, `graft_blast`.
41
+
42
+ Push surfaces (mode includes `push`): skill `graft` carrying context provider `graft-context` (per-turn pointers-only pack, gated: ≥12-char prompt, dedup by seen node ids, 32 KiB block ceiling) and instruction injector `graft-orient` (`first_turn`, byte-capped INDEX.md cut + staleness banner).
43
+
44
+ Registered commands: `graft` (`status` \| `build` \| `check` \| `viz` dispatch), plus `graft-build`, `graft-check`, `graft-viz` aliases.
45
+
46
+ ## Outputs / response / events
47
+
48
+ | Export | Purpose |
49
+ | --- | --- |
50
+ | `createGraftExtension(options)` | Returns an inert `Extension` until `kernel.load([...])`; emits `graft:loaded` on setup. |
51
+ | `resolveGraftCli(options)` | Fail-closed CLI resolution (`explicit` → command+argv, `peer-bin` → node + manifest bin). |
52
+ | `runGraftJson(cli, argv, options)` / `childEnv(options)` / `childTimeoutMs` / `DEFAULT_MAX_RESULT_BYTES` | Shared budgeted JSON runner for hosts building custom surfaces. |
53
+ | `readBoundedFile` / `redactPaths` / `GraftResolveError` | Bounded-read and redaction helpers. |
54
+
55
+ Events: `graft:status` (check/build outcomes), `graft:dirty` (post-edit, repo-relative path + optional `staleCountEstimate`), `graft:loaded` (mode + cliKind metadata).
56
+
57
+ Session custom entry shape (`data.type === "graft-state"`, CAS via `expectedParentId`):
58
+
59
+ ```json
60
+ { "kind": "custom", "data": { "type": "graft-state", "freshness": { "checkedAt": "...", "fresh": true }, "seen": ["node-a"], "savedTokensApprox": 120 } }
61
+ ```
62
+
63
+ The graph never rebuilds itself mid-session (no auto-rebuild): after edits, ask/grep results may lag one turn; graft self-refreshes on the next indexed query, or run `/graft build` for an immediate refresh. The skill text states this contract to the agent.
64
+
65
+ ## Request/response example
66
+
67
+ Tool call (pull):
68
+
69
+ ```json
70
+ { "name": "graft_ask", "arguments": { "query": "where is auth handled?", "count": 3 } }
71
+ → { "nodes": [{ "id": "auth-guard", "title": "requireAuth", "path": "src/auth.ts", "line": 41 }] }
72
+ ```
73
+
74
+ Status event:
75
+
76
+ ```json
77
+ { "type": "graft:status", "extension": "@arnilo/prism-graft", "metadata": { "fresh": true, "missing": 0, "stale": 2 } }
78
+ ```
79
+
80
+ ## Implementation example
81
+
82
+ See [`examples/graft-extension.ts`](../examples/graft-extension.ts) — network-free demo against the package fixture stub: one pull-tool call, one push turn with pack injection + dedup, one simulated edit producing blast radius, and the `DO_NOT_TRACK` child-env guard.
83
+
84
+ ```ts
85
+ import { createExtensionKernel, createMemorySessionStore } from "@arnilo/prism";
86
+ import { createGraftExtension } from "@arnilo/prism-graft";
87
+
88
+ const store = createMemorySessionStore();
89
+ const kernel = createExtensionKernel({ errorPolicy: "throw" });
90
+ await kernel.load([
91
+ createGraftExtension({
92
+ packageRoot: "./vendor/graft-checkout",
93
+ mode: "both",
94
+ quietStartup: true,
95
+ appendEntry: async (entry, options) => store.append(entry, options),
96
+ getEntries: async () => store.list("s1"),
97
+ }),
98
+ ]);
99
+ // Pull: dispatch graft_ask/… tools. Push: runs assemble the skill-carried
100
+ // provider + graft-orient injector. Edits: middleware emits graft:dirty.
101
+ ```
102
+
103
+ ## Extension and configuration notes
104
+
105
+ - Import alone registers nothing (`sideEffects: false`); no timers, watchers, or network. The only child processes are budgeted graft CLI calls.
106
+ - Retrieval happens in-process via Prism primitives (context provider, injector, tool_result middleware) — no external hook shims.
107
+ - Ask result shape is parsed tolerantly (`nodes|results|matches|hits`) because graft is pre-1.0; formatters emit pointers (`title` + `file:line` + `[[wikilink]]`), never source bodies.
108
+ - Not included in `@arnilo/prism-code`, `@arnilo/prism-sdk`, or the `prism-all` umbrella (deliberate opt-out, like Caveman/Ponytail) — opt-in install only.
109
+ - Multi-repo layouts work as upstream graft defines them (workspaces, submodules with `--follow-submodules`, sibling repos); point `projectDir` at the graft root that owns the target repo.
110
+
111
+ ## Security and performance notes
112
+
113
+ - Telemetry default-off: children always get `DO_NOT_TRACK=1` unless `allowUpstreamTelemetry` is true; child env is fixed-base — host env vars are never inherited, and only explicit `GRAFT_*` keys from `providerEnv` pass through. Route secrets like `GRAFT_API_KEY` through the host's credential resolution when populating `providerEnv`.
114
+ - Upstream output is untrusted: stdout capped (`maxResultBytes`), prompts capped (`maxPromptChars`), injected packs bounded (32 KiB), orientation cut byte-capped (8 KiB); error paths are logged redacted (absolute paths/home dirs).
115
+ - Every CLI call is wall-clock-budgeted (`retrievalBudgetMs`, minus fixed overhead for the timeout math) and every failure degrades silently: pull tools return structured errors, the push pack contributes nothing, edit-watch passes the tool result through untouched.
116
+ - No background workers; state persists through two CAS appends per turn at most (freshness patch, seen-set/saved-tokens update).
117
+
118
+ ## Related APIs
119
+
120
+ - [Ponytail behavior integration](ponytail.md): same adapter pattern (optional peer/upstream path, fail-closed setup, session custom entries).
121
+ - [Caveman behavior integration](caveman.md): complementary terse-communication mode package.
122
+ - [Indexed code search](indexed-code-search.md): literal `repository_search` seam — complement, not overlap.
123
+ - [Context and skills](context-and-skills.md): progressive catalog + `load_skill`; skill-carried context providers.
124
+ - [Instruction injection](instruction-injection.md): injector seams (`graft-orient` rides `first_turn`).
125
+ - [Extension kernel and event bus](extensions.md): explicit `kernel.load`, extension events.
@@ -155,6 +155,7 @@ Wire those values where they matter: provider adapters receive the resolved cred
155
155
  - Optional `@arnilo/prism-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()`.
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
+ - Optional `@arnilo/prism-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).
158
159
  - `@arnilo/prism-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.
159
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.
160
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.
@@ -172,7 +173,7 @@ Wire those values where they matter: provider adapters receive the resolved cred
172
173
  - License inventory: 160 locked third-party packages; all declare permissive MIT, ISC, BSD, Apache-2.0, or compatible dual licenses. No GPL, AGPL, SSPL, or missing lockfile license metadata.
173
174
  - Install scripts: only `better-sqlite3@12.11.1` runs an install script (`prebuild-install || node-gyp rebuild --release`), required by the explicitly installed SQLite adapter. Core and other optional packages add no install hook.
174
175
  - Secret scan: source, tests, docs, workflow files, package metadata, built tests, packed-install canary, and tarball deny-list checks found no private-key block or common live-token prefix. Runtime redaction fixtures cover requests, events, ledgers, stores, checkpoints, provider/OAuth errors, and credential ciphertext.
175
- - Threat suites pass for parameterized SQL/tenant isolation, HTTP URL/SSRF rejection, realpath/symlink containment, shell-metacharacter approval, schema prototype-pollution/remote-reference bounds, OAuth polling/abort/redaction, credential tamper/wrong-key/KDF floors, MCP result bounds/timeouts, and coding approval/path policy.
176
+ - Threat suites pass for parameterized SQL/tenant isolation, HTTP URL/SSRF rejection, realpath/symlink containment, shell-metacharacter approval, schema prototype-pollution/remote-reference bounds, OAuth polling/abort/redaction, credential tamper/wrong-key/KDF floors, MCP result bounds/timeouts, and coding approval/path policy. `security:threat-suites` also gates CodeQL-remediation regressions (plan 038): linear `trimTrailingSlashes`/parsers with no environment regex evaluation, single-pass HTML sanitization, crypto (not `Math.random`) fixture identifiers, and no clear-text error logging on password-handling paths.
176
177
 
177
178
  PostgreSQL TLS/network policy, MCP endpoint trust/credentials and egress policy beyond package origin/DNS pinning, provider base URLs, OS keychain availability, process sandboxing, workflow tenant identity, and ANSI/control-sequence sanitization in any host terminal renderer remain host boundaries. Prism 0.0.4 ships JSON-line RPC, not an interactive TUI; hosts must render untrusted model/tool text safely. Credential-gated PostgreSQL/provider/keychain tests are separate operator/CI gates, not silently replaced by mocks.
178
179
 
@@ -213,6 +214,7 @@ PostgreSQL TLS/network policy, MCP endpoint trust/credentials and egress policy
213
214
  - **Named threat-suites leg.** `npm run security:threat-suites` aggregates the Phase 8–11 conformance suites (durable-loop/HITL approval, coding sandbox/egress/forge, ACP protocol, OIDC/OPA/MCP-OAuth/OpenAPI/artifact) into one named 0.1.0 security evidence leg — same scripts as `npm test`, no rewrite; the Phase 7 tenant-isolation suite is its protected counterpart under `npm run test:postgres` (missing `PRISM_TEST_POSTGRES_URL` is a named blocked gate).
214
215
  - **Supply-chain negative fixtures.** `scripts/release-gate.test.mjs` verifies the tarball deny list rejects tampered content (plans/reviews/maps/tests), unexpected file types and credential material (native binaries, `.pem`/`.key`/`.p12`), and that a provenance flag suppressed in CI is detectable in the `release.mjs` publish dry-run arguments (`--provenance` mandatory under `GITHUB_ACTIONS`, never claimed on local OIDC-less publishes).
215
216
  - **Mandatory gate stack.** CodeQL/SAST, PR dependency review (fail on high), secret scan (source + unpacked tarballs), SPDX SBOM + license policy, tarball allow/deny content checks, and provenance (npm OIDC + GitHub build attestations on tarballs and SBOM) all run in `security.yml`/`release.yml`; evidence for the 0.1.0 tree is recorded in [0.1.0 readiness](0.1.0-readiness.md).
217
+ - **CodeQL query suite.** `.github/codeql/codeql-config.yml` selects the `security-extended` suite for `javascript-typescript` (with the default suite) on push/PR/schedule in `security.yml` (10-minute job bound; measured runtime ~3m22s on the audited SHA, last successful main run `33059128198`). The ignore list covers only generated `dist`, `node_modules`, and release/security artifact directories — first-party packages, threat suites, and fixtures that ship or execute are always scanned, so new alerts enter the same plan-038 ledger/remediation loop (config + guardrails asserted in `scripts/phase38-codeql-regression.test.mjs`). Local Task 6 gates (typecheck/lint/format/threat suites/audit/secret scan/SBOM) pass on the remediations; GitHub `state=open` stays non-zero until those remediations are the analyzed head. Groups G (`js/insufficient-password-hash` on RFC 7636 S256) and H (`js/incomplete-url-substring-sanitization` on a negative docs assertion) are maintainer-reviewed false positives queued for narrow dismissal after that analyze, not code changes.
216
218
  - **Live canaries are blocked gates, not skips.** The `live-canaries` and `sandbox-browser` workflows always set their gate env (`PRISM_LIVE_CANARIES=1`), so absent credentials fail the job loudly with a named owner (the workflow + dispatching operator) and retained `canary-report.json` evidence; the local silent-skip path exists only when the gate env is not set.
217
219
 
218
220
  ## Distributed events and tool effects