@arnilo/prism 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.6] - 2026-08-11
4
+
5
+ ### Changed
6
+ - **Release 0.1.6 (plan 018)** is the coding-agent capability-closeouts patch on the frozen 0.1.x line — five demand-gated closeouts, all shipped, additive-only vs 0.1.5 (freeze manifest `scripts/phase18-freeze-manifest.json`; every closeout flipped to `demanded` by named demand evidence before its task landed, then the demand-gate registry validated demanded ⇒ implemented, deferred ⇒ untouched). (1) **Durable ACP session store** (`acp-session-store`): `@arnilo/prism-ag-ui` gains the host-owned `AcpSessionStore` seam on `CreatePrismAcpAgentOptions` — `save` (upsert on session/new, set_mode, set_config_option, never on cancel/prompt-end), `loadAll` (lazy, once per agent instance, after authorization, cross-tenant entries refused `ERR_PRISM_ACP_INPUT`), `evict` (on close/delete); the persisted entry shape `{sessionId, ownership, modeId, configValues, cwd, additionalDirectories, updatedAt}` deliberately excludes client/controller/budget/pending state; fail-closed restore drops corrupt/oversized entries, re-validates modes/config options, keeps the in-memory registry caps (32 default / 128 hard), and re-resolves the live session binding; absent seam = byte-identical 0.1.5 in-memory behavior; the whole persisted entry rides the optional `SecretRedactor` at the save boundary. (2) **Network-free native sandbox backend** (`native-sandbox`): `@arnilo/prism-coding-security` gains `createNativeSandbox` — spawn + POSIX rlimits + existing path containment, zero new dependencies; every command runs in a fresh network namespace via the OS `unshare` binary (plain or `--map-root-user` preflighted once at creation, fail-closed on macOS/Windows and where netns cannot be created), ulimit chains (`-v`/`-t`/`-n`) with `|| exit 126`, argv-only `exec` (never shell-interpolated), cwd containment via `assertPathInsideRoots`, process-group kill on timeout/abort, env allow-list (host env never inherited), output cap, `close({export})` tar parity, and a documented honest boundary (runs as the invoking OS user; egress denial + rlimits + cwd containment only). (3) **Bounded PDF/Office document reader** (`doc-reader`): new optional package `@arnilo/prism-document-reader` (the 50th publishable manifest) — `createDocumentReader({ maxBytes, maxPages, maxTextBytes, parsers })` behind optional peer parsers `pdf-parse`/`mammoth` (dynamic-import, fail-closed at creation with an install hint when absent), magic-byte format gating (never extension sniffing), null fall-through to the 0.1.5 text path, refuse-over-truncate for over-page PDFs, byte-safe text truncation, optional `SecretRedactor` at the adapter boundary, no embedded-content execution, no external resource fetch (egress tripwire test), extraction envelope recorded in `scripts/budgets.json`; `createReadTool` gains the additive `documentReader` slot with input/page/text caps re-checked in the read flow. (4) **Recursive delete + brace-expanding glob** (`delete-glob`): `delete` gains the per-call opt-in `recursive: true` (symlink children unlinked never followed, iterative post-order walk, per-call fan-out cap 10,000 default / 100,000 hard, partial deletion reported never silent, `maxEntries` bound); `glob` gains host-selected + per-call `braceExpansion` (`{a,b}` textual expansion, max 128 alternatives / 4096 expanded bytes, unbalanced/nested/empty braces and overflow fail closed, default matcher semantics unchanged). (5) **Checkpoint persistence for loaded-skill bodies** (`checkpoint-bodies`): durable runs may set `includeSkillBodies: true` on BOTH run and resume options (alongside `persistSessionState`) — the exact loaded-skill instructions ride the checkpoint (`{name, instructions}` pairs, ≤64 bodies / ≤256-char names / ≤262144-byte bodies / ≤1 MiB total, validated fail-closed on save and load, redacted at rest) so resume re-renders them registry-independently with no `load_skill` round-trip; names-only stays the default and 0.1.3/0.1.2 checkpoint shapes are byte-identical; `maxStateBytes` refuses oversize bodies with a recorded error, never truncates. Release graph **50** publishable manifests (root + 49 workspace packages — 14 provider adapters, 9 `prism-*` family/profile, 26 capability incl. `@arnilo/prism-document-reader`) at exact **0.1.6**. Exit gate green (core 1,433/1,433 + 190 script gates incl. phase18-freeze done-phase, `sdk:ready`, audit 0 moderate, pack dry-run 50/50 twice byte-identical, plain compat gate at 0.1.6 with 0 breaking deltas then version-literal baseline refresh, evidence in `scripts/phase18-baseline.json`). Store compatibility with 0.1.5: **compatible, no migration** (additive-only; no persisted-shape change). **Publication remains the operator handoff** (`docs/release-and-install.md` `0.1.6 publish handoff` — signed `v0.1.6` tag + npm OIDC).
7
+
3
8
  ## [0.1.5] - 2026-08-11
4
9
 
5
10
  ### Changed
@@ -20,6 +20,8 @@ export interface AgentRunLifecycleRequest {
20
20
  readonly agentId?: string;
21
21
  /** Opt-in (plan 015 Task 4): restore persisted loaded-skill names on resume. */
22
22
  readonly persistSessionState?: boolean;
23
+ /** Opt-in (plan 018 Task 6): restore persisted loaded-skill bodies on resume (requires `persistSessionState` too). */
24
+ readonly includeSkillBodies?: boolean;
23
25
  }
24
26
  /** Bounded live-event options for a durable lifecycle resume. */
25
27
  export interface AgentRunLifecycleStreamRequest extends AgentRunLifecycleRequest, SubscribeOptions {
@@ -28,6 +28,7 @@ export function createAgentRunLifecycle(options) {
28
28
  fencingToken: options.fencingToken,
29
29
  definitionRevision: resolved.definitionRevision,
30
30
  persistSessionState: request.persistSessionState,
31
+ includeSkillBodies: request.includeSkillBodies,
31
32
  });
32
33
  },
33
34
  async *resumeStream(ref, resume, request = {}) {
@@ -45,6 +46,7 @@ export function createAgentRunLifecycle(options) {
45
46
  maxQueuedEvents: request.maxQueuedEvents,
46
47
  overflow: request.overflow,
47
48
  persistSessionState: request.persistSessionState,
49
+ includeSkillBodies: request.includeSkillBodies,
48
50
  });
49
51
  },
50
52
  };
@@ -95,6 +97,11 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
95
97
  if (options.persistSessionState && state.sessionState?.loadedSkillNames) {
96
98
  session.restoreLoadedSkills(state.sessionState.loadedSkillNames);
97
99
  }
100
+ // Plan 018 Task 6 (closeout `checkpoint-bodies`): restore exact instructions so the
101
+ // resumed session renders them registry-independently (no load_skill round-trip).
102
+ if (options.persistSessionState && options.includeSkillBodies && state.sessionState?.loadedSkillBodies) {
103
+ session.restoreLoadedSkillBodies(state.sessionState.loadedSkillBodies);
104
+ }
98
105
  if (resume.decision !== undefined && resume.decisions !== undefined) {
99
106
  throw new AgentDecisionError("ERR_PRISM_DECISION_INVALID", "Resume accepts exactly one of decision or decisions");
100
107
  }
@@ -1,5 +1,6 @@
1
1
  import type { Agent, AgentRunInterruption, AgentRunRef, AgentRunState, AgentRunStateOptions, AgentRunStatusResult, CheckpointRecord, CheckpointStore, JsonValue, Message, ModelConfig, NestedRunRef, OwnershipScope, RunDecision, RunLimitCounters, StickyDecision, ToolCallContent } from "./contracts.js";
2
2
  import type { SecretRedactor } from "./redaction.js";
3
+ import { type LoadedSkillBodiesEntry } from "./skill-load.js";
3
4
  export declare const AGENT_RUN_STATE_NAMESPACE = "prism.agent-run";
4
5
  export declare const AGENT_RUN_STATE_SCHEMA_VERSION: 1;
5
6
  export declare const DEFAULT_MAX_AGENT_RUN_STATE_BYTES: number;
@@ -41,6 +42,7 @@ export interface StoredAgentRunState extends AgentRunState {
41
42
  */
42
43
  readonly sessionState?: {
43
44
  readonly loadedSkillNames?: readonly string[];
45
+ readonly loadedSkillBodies?: readonly LoadedSkillBodiesEntry[];
44
46
  };
45
47
  }
46
48
  /** Session-state caps (plan 015 Task 4): bounded names charged against the run-state byte budget. */
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { AgentLoopStateError, AgentRunStateError } from "./contracts.js";
3
+ import { validateLoadedSkillBodies } from "./skill-load.js";
3
4
  export const AGENT_RUN_STATE_NAMESPACE = "prism.agent-run";
4
5
  export const AGENT_RUN_STATE_SCHEMA_VERSION = 1;
5
6
  export const DEFAULT_MAX_AGENT_RUN_STATE_BYTES = 256 * 1024;
@@ -241,6 +242,15 @@ function validateSessionState(sessionState) {
241
242
  if (!sessionState || typeof sessionState !== "object") {
242
243
  throw new AgentRunStateError("Malformed agent run session state");
243
244
  }
245
+ const bodies = sessionState.loadedSkillBodies;
246
+ if (bodies !== undefined) {
247
+ try {
248
+ validateLoadedSkillBodies(bodies);
249
+ }
250
+ catch (error) {
251
+ throw new AgentRunStateError(error instanceof Error ? error.message : String(error));
252
+ }
253
+ }
244
254
  const names = sessionState.loadedSkillNames;
245
255
  if (names === undefined)
246
256
  return;
@@ -1,6 +1,7 @@
1
1
  import { type StoredAgentRunState } from "./agent-run-state.js";
2
2
  import type { Agent, AgentConfig, AgentEvent, AgentRunResult, AgentRunStateOptions, AgentSession, AgentSessionConfig, CompactionOptions, CompactionResult, OwnershipScope, RunDecision, RunOptions, SessionEntry, SteerOptions, SubscribeOptions } from "./contracts.js";
3
3
  import { type AgentInput } from "./input.js";
4
+ import { type LoadedSkillBodiesEntry } from "./skill-load.js";
4
5
  export declare function createAgent(config: AgentConfig): Agent;
5
6
  export declare function createAgentSession(config: AgentSessionConfig & {
6
7
  readonly agent: Agent;
@@ -36,8 +37,14 @@ export declare class RuntimeAgentSession implements AgentSession {
36
37
  private activeGatedRound?;
37
38
  private activeLoopTurn;
38
39
  private readonly loadedSkills;
40
+ /** Plan 018 Task 6 (closeout `checkpoint-bodies`): persisted exact instructions, registry-independent. */
41
+ private restoredSkillBodies;
42
+ /** Skills of the current run (for the bodies snapshot); replaced at each run start. */
43
+ private activeRunSkills;
39
44
  /** Plan 015 Task 4: re-add persisted loaded-skill names (names only; bodies re-resolve on demand). */
40
45
  restoreLoadedSkills(names: readonly string[]): void;
46
+ /** Plan 018 Task 6: restore persisted loaded-skill bodies (already validated fail-closed at load). */
47
+ restoreLoadedSkillBodies(bodies: readonly LoadedSkillBodiesEntry[]): void;
41
48
  private ledgerChain;
42
49
  private ledgerFailure;
43
50
  private snapshotGeneration;
@@ -16,6 +16,7 @@ import { RunLimitError, RunLimitTracker, resolveRunLimits } from "./run-limits.j
16
16
  import { createMemorySessionStore, createSessionEntry, getSessionBranchEntries, rebuildSessionContext, } from "./session-stores.js";
17
17
  import { resolveActiveSkills } from "./skills.js";
18
18
  import { createLoadedSkillSet, resolveSkillsDisclosure } from "./skill-disclosure.js";
19
+ import { applyRestoredSkillBodies, snapshotLoadedSkillBodies, validateLoadedSkillBodies, } from "./skill-load.js";
19
20
  import { resolveToolResultFold } from "./tool-result-fold.js";
20
21
  import { assertStructuredOutputRequestSupported, resolveRunProviderOptions } from "./structured-output.js";
21
22
  import { composeSystemPrompt, mergeSystemPromptConfig } from "./system-prompts.js";
@@ -65,11 +66,22 @@ export class RuntimeAgentSession {
65
66
  activeGatedRound;
66
67
  activeLoopTurn = 1;
67
68
  loadedSkills = createLoadedSkillSet();
69
+ /** Plan 018 Task 6 (closeout `checkpoint-bodies`): persisted exact instructions, registry-independent. */
70
+ restoredSkillBodies = [];
71
+ /** Skills of the current run (for the bodies snapshot); replaced at each run start. */
72
+ activeRunSkills = [];
68
73
  /** Plan 015 Task 4: re-add persisted loaded-skill names (names only; bodies re-resolve on demand). */
69
74
  restoreLoadedSkills(names) {
70
75
  for (const name of names)
71
76
  this.loadedSkills.add(name);
72
77
  }
78
+ /** Plan 018 Task 6: restore persisted loaded-skill bodies (already validated fail-closed at load). */
79
+ restoreLoadedSkillBodies(bodies) {
80
+ validateLoadedSkillBodies(bodies);
81
+ this.restoredSkillBodies = bodies;
82
+ for (const entry of bodies)
83
+ this.loadedSkills.add(entry.name);
84
+ }
73
85
  ledgerChain = Promise.resolve();
74
86
  ledgerFailure;
75
87
  snapshotGeneration = 0;
@@ -255,6 +267,7 @@ export class RuntimeAgentSession {
255
267
  await this.rebuildHistory();
256
268
  const { registry, tools } = activeTools(this.agent.config.tools);
257
269
  const activeSkills = this.resolveRunSkills(options, tools);
270
+ this.activeRunSkills = activeSkills; // for the durable bodies snapshot (plan 018 Task 6)
258
271
  if (options.model && JSON.stringify(options.model) !== JSON.stringify(this.agent.config.model)) {
259
272
  await this.appendEntry(createSessionEntry({
260
273
  sessionId: this.id,
@@ -464,7 +477,7 @@ export class RuntimeAgentSession {
464
477
  inputBuilder: this.agent.config.inputBuilder,
465
478
  promptBuilder: this.agent.config.promptBuilder,
466
479
  contextProviders,
467
- skills: activeSkills,
480
+ skills: this.restoredSkillBodies.length ? applyRestoredSkillBodies(activeSkills, this.restoredSkillBodies) : activeSkills,
468
481
  skillsDisclosure: resolveSkillsDisclosure(options.skillsDisclosure, this.agent.config.skillsDisclosure),
469
482
  toolResultFold: resolveToolResultFold(options.toolResultFold, this.agent.config.toolResultFold),
470
483
  loadedSkills: this.loadedSkills,
@@ -1108,7 +1121,17 @@ export class RuntimeAgentSession {
1108
1121
  if (!durable)
1109
1122
  throw new AgentRunStateError("Durable run state is not configured");
1110
1123
  const persisted = durable.options.persistSessionState
1111
- ? { ...state, sessionState: { loadedSkillNames: this.loadedSkills.list() } }
1124
+ ? {
1125
+ ...state,
1126
+ sessionState: {
1127
+ loadedSkillNames: this.loadedSkills.list(),
1128
+ ...(durable.options.includeSkillBodies
1129
+ ? {
1130
+ loadedSkillBodies: snapshotLoadedSkillBodies(this.activeRunSkills, this.loadedSkills, this.restoredSkillBodies.length ? new Map(this.restoredSkillBodies.map((e) => [e.name, e.instructions])) : undefined),
1131
+ }
1132
+ : {}),
1133
+ },
1134
+ }
1112
1135
  : state;
1113
1136
  const saved = await saveAgentRunState({
1114
1137
  checkpoints: durable.options.checkpoints,
@@ -156,6 +156,17 @@ export interface AgentRunStateOptions {
156
156
  * Default off: checkpoint shape is identical to 0.1.2.
157
157
  */
158
158
  readonly persistSessionState?: boolean;
159
+ /**
160
+ * Opt-in (plan 018 Task 6 closeout `checkpoint-bodies`): alongside
161
+ * `persistSessionState`, persist the exact loaded-skill instructions
162
+ * (`{name, instructions}` pairs, redacted at the checkpoint boundary like all state)
163
+ * so resume re-renders them registry-independently — no `load_skill` round-trip, no
164
+ * drift when the live registry changed or lost the skill. Both the run and the resume
165
+ * options must set it. Bounds: ≤64 bodies, ≤256-char names, ≤262144-byte bodies,
166
+ * ≤1 MiB total; the `maxStateBytes` ceiling refuses oversize with a recorded error
167
+ * (never silently truncates). Default off: checkpoint shape is identical to 0.1.3.
168
+ */
169
+ readonly includeSkillBodies?: boolean;
159
170
  }
160
171
  /** Versioned, redacted checkpoint payload. Treat as opaque except status/version/interruption. */
161
172
  export interface AgentRunState {
@@ -188,6 +199,8 @@ export interface AgentRunResumeOptions {
188
199
  readonly resumeNestedRun?: ResumeNestedRun;
189
200
  /** Opt-in (plan 015 Task 4): restore persisted loaded-skill names into the resumed session catalog. */
190
201
  readonly persistSessionState?: boolean;
202
+ /** Opt-in (plan 018 Task 6): restore persisted loaded-skill bodies (requires `persistSessionState` too). */
203
+ readonly includeSkillBodies?: boolean;
191
204
  }
192
205
  /** Bounded, abortable options for `resumeAgentRunStream()`. */
193
206
  export interface AgentRunResumeStreamOptions extends AgentRunResumeOptions, SubscribeOptions {
package/dist/index.d.ts CHANGED
@@ -86,8 +86,8 @@ export { createMemorySessionStore, createSessionEntry, getSessionBranchEntries,
86
86
  export { createChainedSettingsProvider, createStaticSettingsProvider } from "./settings.js";
87
87
  export type { LoadedSkillSet, SkillRenderContext, SkillsDisclosure } from "./skill-disclosure.js";
88
88
  export { createLoadedSkillSet, DEFAULT_MAX_SKILL_CATALOG_ENTRIES, DEFAULT_MAX_SKILL_DESCRIPTION_BYTES, DEFAULT_MAX_SKILL_INSTRUCTION_BYTES, EMPTY_SKILL_DESCRIPTION, HARD_MAX_SKILL_CATALOG_ENTRIES, HARD_MAX_SKILL_DESCRIPTION_BYTES, HARD_MAX_SKILL_INSTRUCTION_BYTES, isSkillDisclosureError, resolveSkillsDisclosure, SkillDisclosureError, } from "./skill-disclosure.js";
89
- export type { CreateLoadSkillToolOptions, ResolveSkillLoadOptions } from "./skill-load.js";
90
- export { createLoadSkillTool, DEFAULT_LOAD_SKILL_TOOL_NAME, isSkillLoadError, MAX_LOAD_SKILL_RESULT_BYTES, resolveSkillLoad, SKILL_LOAD_ERROR_CODE, SkillLoadError, } from "./skill-load.js";
89
+ export type { CreateLoadSkillToolOptions, LoadedSkillBodiesEntry, ResolveSkillLoadOptions, } from "./skill-load.js";
90
+ export { applyRestoredSkillBodies, createLoadSkillTool, DEFAULT_LOAD_SKILL_TOOL_NAME, HARD_MAX_PERSISTED_SKILL_BODY_TOTAL_BYTES, isSkillLoadError, MAX_LOAD_SKILL_RESULT_BYTES, MAX_PERSISTED_SKILL_BODIES, MAX_PERSISTED_SKILL_BODY_BYTES, MAX_PERSISTED_SKILL_BODY_NAME_CHARS, resolveSkillLoad, SKILL_LOAD_ERROR_CODE, SkillLoadError, snapshotLoadedSkillBodies, validateLoadedSkillBodies, } from "./skill-load.js";
91
91
  export type { ResolveActiveSkillsOptions, SkillRegistryOptions } from "./skills.js";
92
92
  export { createSkillRegistry, resolveActiveSkills } from "./skills.js";
93
93
  export { artifactStructuredOutputRequest, assertStructuredOutputRequestSupported, DEFAULT_MAX_STRUCTURED_OUTPUT_NAME_LENGTH, DEFAULT_MAX_STRUCTURED_OUTPUT_SCHEMA_BYTES, modelSupportsStructuredOutput, resolveRunProviderOptions, StructuredOutputError, validateStructuredOutputOptions, withoutStructuredOutput, } from "./structured-output.js";
@@ -105,5 +105,5 @@ export { createToolParameterValidator, createToolRegistry, dispatchToolCall, fil
105
105
  export type { ResolvedUseCaseModel, ResolveUseCaseModelInput, UseCaseModelBinding, } from "./use-case-model.js";
106
106
  export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
107
107
  export declare const name = "prism";
108
- export declare const version = "0.1.5";
108
+ export declare const version = "0.1.6";
109
109
  export declare const description = "Agent harness for AI providers, agents, sessions, and tools.";
package/dist/index.js CHANGED
@@ -46,7 +46,7 @@ export { assertPermission, assertTrusted, checkPermission, createStaticPermissio
46
46
  export { createMemorySessionStore, createSessionEntry, getSessionBranchEntries, listSessionBranches, rebuildSessionContext, } from "./session-stores.js";
47
47
  export { createChainedSettingsProvider, createStaticSettingsProvider } from "./settings.js";
48
48
  export { createLoadedSkillSet, DEFAULT_MAX_SKILL_CATALOG_ENTRIES, DEFAULT_MAX_SKILL_DESCRIPTION_BYTES, DEFAULT_MAX_SKILL_INSTRUCTION_BYTES, EMPTY_SKILL_DESCRIPTION, HARD_MAX_SKILL_CATALOG_ENTRIES, HARD_MAX_SKILL_DESCRIPTION_BYTES, HARD_MAX_SKILL_INSTRUCTION_BYTES, isSkillDisclosureError, resolveSkillsDisclosure, SkillDisclosureError, } from "./skill-disclosure.js";
49
- export { createLoadSkillTool, DEFAULT_LOAD_SKILL_TOOL_NAME, isSkillLoadError, MAX_LOAD_SKILL_RESULT_BYTES, resolveSkillLoad, SKILL_LOAD_ERROR_CODE, SkillLoadError, } from "./skill-load.js";
49
+ export { applyRestoredSkillBodies, createLoadSkillTool, DEFAULT_LOAD_SKILL_TOOL_NAME, HARD_MAX_PERSISTED_SKILL_BODY_TOTAL_BYTES, isSkillLoadError, MAX_LOAD_SKILL_RESULT_BYTES, MAX_PERSISTED_SKILL_BODIES, MAX_PERSISTED_SKILL_BODY_BYTES, MAX_PERSISTED_SKILL_BODY_NAME_CHARS, resolveSkillLoad, SKILL_LOAD_ERROR_CODE, SkillLoadError, snapshotLoadedSkillBodies, validateLoadedSkillBodies, } from "./skill-load.js";
50
50
  export { createSkillRegistry, resolveActiveSkills } from "./skills.js";
51
51
  export { artifactStructuredOutputRequest, assertStructuredOutputRequestSupported, DEFAULT_MAX_STRUCTURED_OUTPUT_NAME_LENGTH, DEFAULT_MAX_STRUCTURED_OUTPUT_SCHEMA_BYTES, modelSupportsStructuredOutput, resolveRunProviderOptions, StructuredOutputError, validateStructuredOutputOptions, withoutStructuredOutput, } from "./structured-output.js";
52
52
  export { composeSystemPrompt, mergeSystemPromptConfig } from "./system-prompts.js";
@@ -57,6 +57,6 @@ export { DEFAULT_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES, DEFAULT_TOOL_RESULT_FOLD_MI
57
57
  export { createToolParameterValidator, createToolRegistry, dispatchToolCall, filterTools } from "./tools.js";
58
58
  export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
59
59
  export const name = "prism";
60
- export const version = "0.1.5";
60
+ export const version = "0.1.6";
61
61
  export const description = "Agent harness for AI providers, agents, sessions, and tools.";
62
62
  //# sourceMappingURL=index.js.map
@@ -3,6 +3,29 @@ import { type LoadedSkillSet } from "./skill-disclosure.js";
3
3
  export declare const DEFAULT_LOAD_SKILL_TOOL_NAME: "load_skill";
4
4
  export declare const SKILL_LOAD_ERROR_CODE: "skill_load_failed";
5
5
  export declare const MAX_LOAD_SKILL_RESULT_BYTES = 512;
6
+ export declare const MAX_PERSISTED_SKILL_BODIES = 64;
7
+ export declare const MAX_PERSISTED_SKILL_BODY_NAME_CHARS = 256;
8
+ export declare const MAX_PERSISTED_SKILL_BODY_BYTES = 262144;
9
+ export declare const HARD_MAX_PERSISTED_SKILL_BODY_TOTAL_BYTES: number;
10
+ /** One persisted loaded-skill body (plan 018 closeout `checkpoint-bodies`). */
11
+ export interface LoadedSkillBodiesEntry {
12
+ readonly name: string;
13
+ readonly instructions: string;
14
+ }
15
+ /** Fail-closed shape/cap validation for a persisted bodies payload (load and save sides). */
16
+ export declare function validateLoadedSkillBodies(value: unknown): asserts value is readonly LoadedSkillBodiesEntry[];
17
+ /**
18
+ * Snapshot the exact instructions of every loaded skill (registry-independent).
19
+ * Loaded names without instructions are skipped (they render no body anyway);
20
+ * a restored body for a loaded name wins over the registry body.
21
+ */
22
+ export declare function snapshotLoadedSkillBodies(skills: readonly Skill[], loaded: LoadedSkillSet, restored?: ReadonlyMap<string, string>): LoadedSkillBodiesEntry[];
23
+ /**
24
+ * Apply persisted bodies to the skills a resumed session will render: replace the
25
+ * instructions of known skills and append synthesized skills for names the live
26
+ * registry no longer serves, so the exact loaded text renders registry-independently.
27
+ */
28
+ export declare function applyRestoredSkillBodies(skills: readonly Skill[], bodies: readonly LoadedSkillBodiesEntry[]): readonly Skill[];
6
29
  export declare class SkillLoadError extends Error {
7
30
  readonly code: "skill_load_failed";
8
31
  constructor(message: string);
@@ -3,6 +3,80 @@ import { HARD_MAX_SKILL_INSTRUCTION_BYTES } from "./skill-disclosure.js";
3
3
  export const DEFAULT_LOAD_SKILL_TOOL_NAME = "load_skill";
4
4
  export const SKILL_LOAD_ERROR_CODE = "skill_load_failed";
5
5
  export const MAX_LOAD_SKILL_RESULT_BYTES = 512;
6
+ // Bodies-mode persistence bounds (plan 018 Task 6 closeout `checkpoint-bodies`): the
7
+ // checkpoint `maxStateBytes` ceiling is the documented outer bound (oversize refuses at
8
+ // save with a recorded error); these caps bound the bodies payload itself on load.
9
+ // ponytail: module constants, not tunable options — hosts tune maxStateBytes for the ceiling.
10
+ export const MAX_PERSISTED_SKILL_BODIES = 64; // names-mode parity (MAX_PERSISTED_SKILL_NAMES)
11
+ export const MAX_PERSISTED_SKILL_BODY_NAME_CHARS = 256; // names-mode parity
12
+ export const MAX_PERSISTED_SKILL_BODY_BYTES = 262_144; // = HARD_MAX_SKILL_INSTRUCTION_BYTES (loader parity)
13
+ export const HARD_MAX_PERSISTED_SKILL_BODY_TOTAL_BYTES = 1024 * 1024; // = HARD_MAX_AGENT_RUN_STATE_BYTES
14
+ /** Fail-closed shape/cap validation for a persisted bodies payload (load and save sides). */
15
+ export function validateLoadedSkillBodies(value) {
16
+ if (!Array.isArray(value) || value.length > MAX_PERSISTED_SKILL_BODIES) {
17
+ throw new SkillLoadError(`Loaded-skill bodies exceed ${MAX_PERSISTED_SKILL_BODIES} entries`);
18
+ }
19
+ let totalBytes = 0;
20
+ for (const entry of value) {
21
+ if (!entry || typeof entry !== "object" || typeof entry.name !== "string") {
22
+ throw new SkillLoadError("Malformed loaded-skill body entry: name must be a string");
23
+ }
24
+ const { name, instructions } = entry;
25
+ if (name.length > MAX_PERSISTED_SKILL_BODY_NAME_CHARS) {
26
+ throw new SkillLoadError(`Loaded-skill body name exceeds ${MAX_PERSISTED_SKILL_BODY_NAME_CHARS} chars`);
27
+ }
28
+ if (typeof instructions !== "string") {
29
+ throw new SkillLoadError("Malformed loaded-skill body entry: instructions must be a string");
30
+ }
31
+ const bodyBytes = Buffer.byteLength(instructions, "utf8");
32
+ if (bodyBytes > MAX_PERSISTED_SKILL_BODY_BYTES) {
33
+ throw new SkillLoadError(`Loaded-skill body exceeds ${MAX_PERSISTED_SKILL_BODY_BYTES} bytes`);
34
+ }
35
+ totalBytes += Buffer.byteLength(name, "utf8") + bodyBytes;
36
+ if (totalBytes > HARD_MAX_PERSISTED_SKILL_BODY_TOTAL_BYTES) {
37
+ throw new SkillLoadError(`Loaded-skill bodies exceed ${HARD_MAX_PERSISTED_SKILL_BODY_TOTAL_BYTES} total bytes`);
38
+ }
39
+ }
40
+ }
41
+ /**
42
+ * Snapshot the exact instructions of every loaded skill (registry-independent).
43
+ * Loaded names without instructions are skipped (they render no body anyway);
44
+ * a restored body for a loaded name wins over the registry body.
45
+ */
46
+ export function snapshotLoadedSkillBodies(skills, loaded, restored) {
47
+ const byName = new Map(skills.map((skill) => [skill.name, skill]));
48
+ const entries = [];
49
+ for (const name of loaded.list()) {
50
+ const instructions = restored?.get(name) ?? byName.get(name)?.instructions;
51
+ if (!instructions)
52
+ continue;
53
+ entries.push({ name, instructions });
54
+ }
55
+ validateLoadedSkillBodies(entries); // refuses oversize with a recorded error (never truncates)
56
+ return entries;
57
+ }
58
+ /**
59
+ * Apply persisted bodies to the skills a resumed session will render: replace the
60
+ * instructions of known skills and append synthesized skills for names the live
61
+ * registry no longer serves, so the exact loaded text renders registry-independently.
62
+ */
63
+ export function applyRestoredSkillBodies(skills, bodies) {
64
+ validateLoadedSkillBodies(bodies);
65
+ if (bodies.length === 0)
66
+ return skills;
67
+ const byName = new Map(skills.map((skill) => [skill.name, skill]));
68
+ const out = skills.map((skill) => {
69
+ const body = bodies.find((entry) => entry.name === skill.name);
70
+ return body ? { ...skill, instructions: body.instructions } : skill;
71
+ });
72
+ const known = new Set(skills.map((skill) => skill.name));
73
+ for (const entry of bodies) {
74
+ if (!known.has(entry.name)) {
75
+ out.push({ name: entry.name, instructions: entry.instructions });
76
+ }
77
+ }
78
+ return out;
79
+ }
6
80
  export class SkillLoadError extends Error {
7
81
  code = SKILL_LOAD_ERROR_CODE;
8
82
  constructor(message) {
package/docs/acp.md CHANGED
@@ -111,7 +111,7 @@ const agent = createPrismAcpAgent({
111
111
 
112
112
  ### Persistence and ownership
113
113
 
114
- - **The agent never persists `modeId`/`configValues`.** Defaults are recomputed per session from the `modes`/`configOptions` seams — a fresh `session/new`, `load`, or `resume` always starts from `defaultModeId` / option `defaultValue`, and the agent's per-session registry is in-memory only. Persisting mode/config across sessions is a **host** decision, and host-side persistence MUST be ownership-scoped.
114
+ - **Without the durability seam the agent never persists `modeId`/`configValues`.** Defaults are recomputed per session from the `modes`/`configOptions` seams — a fresh `session/new`, `load`, or `resume` always starts from `defaultModeId` / option `defaultValue`, and the agent's per-session registry is in-memory only. Persisting mode/config across sessions is a **host** decision, and host-side persistence MUST be ownership-scoped.
115
115
  - **Host persistence MUST key by `sessions.ownership`.** `authorize` binds transport identity to ownership; a host store that persists `modeId`/`configValues` must refuse any restore whose stored ownership differs from the current session's ownership — a `sessionId` alone is never a sufficient key (session ids may collide across tenants). A cross-tenant restore rejects with `ERR_PRISM_ACP_INPUT` and never returns the other tenant's mode/config.
116
116
  - **Ownership-scoped restore (host-owned store).** The store is keyed by `sessionId` and records the owning `userId`; restore refuses on mismatch (this exact pattern is asserted in `packages/ag-ui/src/__tests__/acp-modes-config.test.ts`):
117
117
 
@@ -133,7 +133,7 @@ const agent = createPrismAcpAgent({
133
133
  ```
134
134
 
135
135
  Because the agent recomputes defaults on every `load`/`resume`, a host that restores state re-applies it after load through the same gated seams (`session/set_mode`, `session/set_config_option` — both run the `apply`/`onChange` hooks) and must refuse cross-tenant loads at the `authorize` seam first (falsy `authorize` = `Unauthorized ACP session`, before any mode/config state is reachable).
136
- - **Agent-owned persistence is 0.2.0.** A durable, ownership-scoped ACP session store (agent-side persistence of mode/config and session state) is roadmap 0.2.0 Module E, demand-gated; on the 0.1.x line the agent stays a thin per-session registry. See the [Host security guide](host-security.md) fail-closed checklist for the ACP boundary rows.
136
+ - **Durable registry (0.1.6, plan 018 closeout `acp-session-store`).** Pass `sessionStore` (`AcpSessionStore` from `@arnilo/prism-ag-ui/acp`) to let a restarted agent restore its live-session registry: `save` (on `session/new`, `set_mode`, `set_config_option`), `loadAll` (once per agent instance, lazily on first authorized touch), `evict` (on `close`/`delete`). The stored entry carries `sessionId`, `ownership`, `modeId`, `configValues`, `cwd`, `additionalDirectories`, `updatedAt` — never ephemeral stream state (client/controller/budget) or pending decisions. Restore re-resolves the live `AgentSession` through your `sessionFactory`, re-validates cwd/directories and mode/config values against the seams, enforces the registry cap, and drops corrupt or seam-mismatched entries fail-closed. The seam is additive-only: absent `sessionStore` ⇒ the agent behaves exactly as 0.1.5. Storage topology stays host-owned; the store is the trust boundary for tampering/replay. The full threat model and test mapping live in `docs/_evidence/phase18-primitive-review.md`; enforcement tests in `packages/ag-ui/src/__tests__/acp-session-store.test.ts`. The host-owned mode/config store pattern above stays valid for hosts that persist without the agent seam.
137
137
 
138
138
  ## Security and performance notes
139
139
 
@@ -198,7 +198,7 @@ if (result.status === "suspended") {
198
198
  }
199
199
  ```
200
200
 
201
- Resume requires exact checkpoint ownership, version, agent fingerprint, and revision. The fingerprint hashes the agent id/name, `definitionRevision`, model, instructions, system-prompt contributions, skills (name/instructions/tool names), tool definitions (name/parameters/exclusive), guardrail definitions (name/stage/revision), and loop strategy — changing any of them without bumping `definitionRevision` fails resume closed instead of silently continuing with different agent semantics. Prism CAS-claims approval before work, rechecks normal guardrail/permission/validation/limit paths, and marks a pending tool dispatched before its side effect. `createAgentRunLifecycle()` wraps the same core path for server/MCP hosts: adapters pass only authorized ownership, status returns only `{ state, version }`, and `resolveAgent()` supplies current agent/revision. `resumeStream()` uses that same claim path and bounded subscriber, so adapters do not poll or duplicate resume logic. Remote restart requires both checkpoint and session stores to be durable. A crash after that mark is ambiguous and is never replayed automatically; use host tool idempotency keyed by `runId`/`toolCallId` or resolve it manually. Checkpoints contain bounded redacted state plus session/leaf references, never provider objects, callbacks, signals, credentials, or raw secrets. State is bounded at save by `runState.maxStateBytes` (default 256 KB, at most the 1 MB hard cap); load bounds against the 1 MB hard cap only, so state saved with a raised limit stays resumable while oversized records are still rejected. Since 0.1.3 (plan 015 Task 4), durable runs may opt in to session-state persistence with `persistSessionState: true` on both the run and resume options: the loaded-skill **name catalog** (≤64 names, ≤256 chars each) rides the checkpoint and is restored into the resumed session's `LoadedSkillSet`; skill **bodies are never persisted** and re-resolve from the live registry via `load_skill`. Default off keeps the checkpoint shape byte-identical to 0.1.2. Built-in loop options are durable; custom `AgentLoopStrategy` instances are durable when they declare `snapshot`/`restore` hooks (see [Agent loops § Durable runs](agent-loops.md#durable-runs)) and reject before provider work otherwise.
201
+ Resume requires exact checkpoint ownership, version, agent fingerprint, and revision. The fingerprint hashes the agent id/name, `definitionRevision`, model, instructions, system-prompt contributions, skills (name/instructions/tool names), tool definitions (name/parameters/exclusive), guardrail definitions (name/stage/revision), and loop strategy — changing any of them without bumping `definitionRevision` fails resume closed instead of silently continuing with different agent semantics. Prism CAS-claims approval before work, rechecks normal guardrail/permission/validation/limit paths, and marks a pending tool dispatched before its side effect. `createAgentRunLifecycle()` wraps the same core path for server/MCP hosts: adapters pass only authorized ownership, status returns only `{ state, version }`, and `resolveAgent()` supplies current agent/revision. `resumeStream()` uses that same claim path and bounded subscriber, so adapters do not poll or duplicate resume logic. Remote restart requires both checkpoint and session stores to be durable. A crash after that mark is ambiguous and is never replayed automatically; use host tool idempotency keyed by `runId`/`toolCallId` or resolve it manually. Checkpoints contain bounded redacted state plus session/leaf references, never provider objects, callbacks, signals, credentials, or raw secrets. State is bounded at save by `runState.maxStateBytes` (default 256 KB, at most the 1 MB hard cap); load bounds against the 1 MB hard cap only, so state saved with a raised limit stays resumable while oversized records are still rejected. Since 0.1.3 (plan 015 Task 4), durable runs may opt in to session-state persistence with `persistSessionState: true` on both the run and resume options: the loaded-skill **name catalog** (≤64 names, ≤256 chars each) rides the checkpoint and is restored into the resumed session's `LoadedSkillSet`; skill **bodies are never persisted** and re-resolve from the live registry via `load_skill`. Since 0.1.6 (plan 018 closeout `checkpoint-bodies`), `includeSkillBodies: true` on BOTH the run and resume options additionally persists the exact loaded-skill **instructions** (`{name, instructions}` pairs, redacted at the checkpoint boundary like all state, ≤64 bodies / ≤256-char names / ≤262144-byte bodies / ≤1 MiB total) so resume re-renders them registry-independently — no `load_skill` round-trip and no dependence on the registry still serving the same text; `maxStateBytes` (default 256 KB) refuses oversize bodies with a recorded error, never silently truncates. Default off keeps the checkpoint shape byte-identical to 0.1.3. Built-in loop options are durable; custom `AgentLoopStrategy` instances are durable when they declare `snapshot`/`restore` hooks (see [Agent loops § Durable runs](agent-loops.md#durable-runs)) and reject before provider work otherwise.
202
202
 
203
203
  ## Secure composition
204
204
 
@@ -12,8 +12,8 @@
12
12
  | `createEditTool(cwd, options?)` | `edit` tool: precise exact-then-fuzzy text replacement in an existing file. |
13
13
  | `createRepoListTool(cwd, options?)` | `repo_list` tool: bounded deterministic repository listing. |
14
14
  | `createRepoSearchTool(cwd, options?)` | `repo_search` tool: bounded literal text search (`outputMode`: content / files_with_matches / count). |
15
- | `createGlobTool(cwd, options?)` | `glob` tool: bounded filename-pattern match (`*` / `?` / `**`; no brace expansion). |
16
- | `createDeleteTool(cwd, options?)` | `delete` tool: high-risk delete of a file or empty directory (no recursive delete, no trash). |
15
+ | `createGlobTool(cwd, options?)` | `glob` tool: bounded filename-pattern match (`*` / `?` / `**`; opt-in bounded `{a,b}` brace expansion via `braceExpansion`). |
16
+ | `createDeleteTool(cwd, options?)` | `delete` tool: high-risk delete of a file, empty directory, or (opt-in `recursive: true`) a directory tree (no trash). |
17
17
  | `createMoveTool(cwd, options?)` | `move` tool: high-risk rename/move within the workspace (`overwrite` default false). |
18
18
  | `createReadPathSet()` | Session-scoped path set for optional `requireReadBeforeWrite` soft guard. |
19
19
  | `createCodingTools(cwd, options?)` | Default nine tools (`shell`, `read`, `write`, `edit`, `repo_list`, `repo_search`, `glob`, `delete`, `move`). |
@@ -101,8 +101,8 @@ These are **out of scope** for the 0.0.21 package baseline (see roadmap Phase 9
101
101
  - **LSP language-server tools** — not in default aggregators; optional `createLanguageIntelligence` is Phase 9 (see [Language intelligence](language-intelligence.md)).
102
102
  - **Managed process sessions** — not in default aggregators; optional `createProcessSessions` is Phase 9 (see [Process sessions](process-sessions.md)).
103
103
  - **GitHub forge adapter** — not in default aggregators; optional `createGitHubForge` is Phase 9 (see [Forge integration](forge-integration.md)); no octokit dependency, no multi-forge abstraction.
104
- - **No recursive directory delete** — `delete` refuses non-empty directories.
105
- - **No brace-expansion globs** — `glob` supports only `*`, `?`, and `**`.
104
+ - **No recursive directory delete by default** — `delete` refuses non-empty directories unless the per-call `recursive: true` flag is set (plan 018 closeout `delete-glob`; bounded fan-out, symlink children unlinked but never followed).
105
+ - **No brace-expansion globs by default** — `glob` supports only `*`, `?`, and `**`; `{a,b}` expansion is opt-in (`braceExpansion`) and bounded (max 128 alternatives / 4096 expanded bytes, fail-closed).
106
106
 
107
107
  ## Inputs / request
108
108
 
@@ -153,6 +153,7 @@ Read a text or image file.
153
153
  | `transformImage` | — | Host callback `( { buffer, mimeType } ) => Promise<Buffer>` run after read, before base64. |
154
154
  | `maxLines` / `maxBytes` | 2000 / 50 KiB | Text page display limits (hard: 100,000 / 1 MiB). |
155
155
  | `maxScanBytes` | 64 MiB | Raw bytes scanned to reach one page (hard: 1 GiB). |
156
+ | `documentReader` | — | Optional host-selected `DocumentReader` (see [Document reader](document-reader.md)): after the image sniff and before the text page, supported PDF/DOCX files are extracted as literal text with `metadata.document = { format, pages, truncatedBy }`. Additive; absent reader = unchanged 0.1.5 behavior. |
156
157
  | `operations` | local fs | Pluggable bounded `ReadOperations` backend. |
157
158
  | `executionPolicy` | — | Structured pre-execution policy (see [Coding security](coding-security.md)). |
158
159
 
@@ -171,6 +172,7 @@ const read = createReadTool(cwd, {
171
172
  | --- | --- | --- |
172
173
  | `truncation` | text reads | `TruncationResult`. |
173
174
  | `image` | image reads | `{ mimeType, resized, bytes }`. `resized` is `true` when `transformImage` ran. |
175
+ | `document` | document reads | `{ format, pages, truncatedBy }` when a `documentReader` extracted the file. |
174
176
 
175
177
  > `autoResizeImages` was removed in 0.1.5; untyped callers now fail closed with a `TypeError` naming `transformImage` before any filesystem access.
176
178
 
@@ -299,7 +301,7 @@ Metadata includes `matches`, `truncated`, scan/skip counts; non-content modes al
299
301
 
300
302
  ### `glob`
301
303
 
302
- Find workspace files by filename pattern without shell `find`. Hand-rolled matcher: `*` (one path segment), `?` (one char), `**` (directories). Brace expansion (`{a,b}`) is **rejected**. Patterns match workspace-relative full paths (e.g. `src/util/a.ts`). Returns **files only** (directories traversed but not listed). Same exclude/hidden/depth/page/time caps as `repo_list`.
304
+ Find workspace files by filename pattern without shell `find`. Hand-rolled matcher: `*` (one path segment), `?` (one char), `**` (directories). Brace expansion (`{a,b}`) is **rejected by default**; set `braceExpansion: true` (per call or as the host option) for bounded expansion — max 128 alternatives and 4096 total expanded bytes, unbalanced/nested/empty braces and overflow fail closed. Expansion is textual only (never touches the filesystem) and result patterns still match workspace-relative full paths under the same exclude/hidden/depth/page/time caps. Patterns match workspace-relative full paths (e.g. `src/util/a.ts`). Returns **files only** (directories traversed but not listed). Same exclude/hidden/depth/page/time caps as `repo_list`.
303
305
 
304
306
  **Inputs:**
305
307
 
@@ -308,6 +310,7 @@ Find workspace files by filename pattern without shell `find`. Hand-rolled match
308
310
  | `pattern` | `string` | Glob pattern (required). |
309
311
  | `path` | `string` | Workspace-relative start directory (default root). |
310
312
  | `includeHidden` | `boolean` | Default false. |
313
+ | `braceExpansion` | `boolean` | Opt-in bounded `{a,b}` expansion (default: host option `createGlobTool(cwd, { braceExpansion })`, else false). |
311
314
  | `maxDepth` | `number` | Depth cap (default 32, hard 128). |
312
315
  | `maxResults` | `number` | Page size (default 1,000, hard 10,000). |
313
316
  | `offset` | `number` | Matches to skip (default 0). |
@@ -316,13 +319,15 @@ Find workspace files by filename pattern without shell `find`. Hand-rolled match
316
319
 
317
320
  ### `delete`
318
321
 
319
- High-risk: permanently delete a **single file or empty directory**. Non-empty directories fail closed (no recursive delete). Symlinks are unlinked as links (targets not followed for containment). **No trash daemon** — host undo is not automatic; gate with approval policy.
322
+ High-risk: permanently delete a **single file or empty directory**, or — with the per-call opt-in `recursive: true` — a whole directory tree. Non-empty directories fail closed without the flag. The recursive walk never follows symlinks: symlink children are unlinked as links, so a link pointing outside the workspace root can never drag the deletion out (the outside target is untouched). Every entry counts against a per-call fan-out cap (`maxEntries`, default 10,000, hard 100,000); exceeding it stops with an error naming the cap (partial deletion is reported, never silent). **No trash daemon** — host undo is not automatic; gate with approval policy.
320
323
 
321
324
  **Inputs:**
322
325
 
323
326
  | Field | Type | Purpose |
324
327
  | --- | --- | --- |
325
- | `path` | `string` | File or empty directory to delete. Required. |
328
+ | `path` | `string` | File, empty directory, or (with `recursive: true`) directory tree to delete. Required. |
329
+ | `recursive` | `boolean` | Per-call opt-in recursive directory delete (default false). |
330
+ | `maxEntries` | `number` | Per-call fan-out cap for recursive deletes (default 10,000, hard 100,000). |
326
331
 
327
332
  **Outputs:** confirmation with absolute path, or error (missing, non-empty dir, escape, abort).
328
333
 
@@ -13,6 +13,7 @@
13
13
  | `createSandboxCodingTools` / `createSandboxReadOnlyTools` | Thin wrappers that return `tools` only (compat); still require `workspaceMode`. |
14
14
  | `createSandboxFilesystemOperations` / `createSandboxRepositoryOperations` | Optional execFile-backed FS/list/search backends for a disposable sandbox tree. |
15
15
  | `createDockerSandbox(options)` | Creates one disposable non-root Docker container with read-only root/source, bounded tmpfs workspace, typed `execFile`, import/export, and stop/kill/cleanup. |
16
+ | `createNativeSandbox(options)` | Linux-only network-free backend: every command runs in a fresh network namespace (`unshare`), POSIX `ulimit` hard caps, cwd-in-root containment; fails closed at creation on platforms/privileges that cannot deny egress. Docker remains the stronger, documented reference backend. |
16
17
  | `SandboxProcessHandle` | Optional long-running process handle (`write`/`signal`/`kill`/`release`/`wait`) returned by `DisposableSandbox.startProcess?`. |
17
18
  | `createEgressPolicy(options)` | Deny-all allow-list policy: exact host/port/protocol rules plus frozen `npm-registry` / `github` presets; SHA-256 fingerprint. |
18
19
  | `createAllowListEgressProxy(options)` | HTTP forward proxy + CONNECT tunnel enforcing the policy: pinned DNS (rebinding defense), private/metadata IP denial, redirect re-validation + hop cap, byte/time caps, per-decision audit, attestation for sandbox composition. |
@@ -33,6 +34,8 @@ Use this package when coding tools need path scoping, human approval, command ru
33
34
 
34
35
  Use `createDockerSandbox()` when the host wants a production-reference containment boundary. Prism does **not** claim OS-level isolation unless the host constructs this adapter (or supplies an equivalent custom `DisposableSandbox`). Default policy denies shell/write/edit/delete/move without an `approve` callback and rejects paths outside configured roots. Coding shell definitions are marked `exclusive: true`, matching the approval policy's shell decision, so a single-shot turn containing shell work runs sequentially even when `toolConcurrency > 1`. Non-shell turns retain configured parallelism.
35
36
 
37
+ Use `createNativeSandbox()` when the host has no container runtime and needs network-free containment (0.1.6, plan 018 closeout `native-sandbox`). Linux only; creation fails closed with a documented error on other platforms or when the OS cannot create a network namespace (no root/CAP_SYS_ADMIN and no unprivileged user namespaces). Every command runs in a fresh netns — **loopback is down**, so even localhost connections fail; hosts that need loopback keep the Docker backend. Containment is egress denial + `ulimit` hard caps (address space from `memoryBytes`, CPU-time wall backstop, fd count from `maxFds`) + cwd-inside-root (`assertPathInsideRoots`, symlink-aware). The native backend does **not** isolate the filesystem: commands run as the invoking OS user with full host-tree access, so pair it with `createSandboxCodingComposition`/`createSandboxFilesystemOperations` (per-op `assertSandboxPath`) and the approval policy, exactly as with any custom `DisposableSandbox`. Host env is never inherited; `env` is an exact allow-list (PATH only by default). No `startProcess` (ProcessSessions fails closed with `ERR_PRISM_PROCESS_UNSUPPORTED`), no CPU-rate/pids/fs-size caps (cgroup-only). Secrets passed as `secrets` are redacted from surfaced errors. See `docs/_evidence/phase18-primitive-review.md` for the full threat model.
38
+
36
39
  Use `createEgressPolicy()` + `createAllowListEgressProxy()` when a coding agent needs outbound network access under an explicit allow list: package installs, forge API calls, or source fetches — never unrestricted egress. The proxy is inert until `start()`; nothing binds or resolves on import or construction.
37
40
 
38
41
  ## Allow-list egress composition
@@ -170,7 +170,7 @@ Catalog caps: **64** entries default / **256** hard; descriptions **512 B** defa
170
170
  ```ts
171
171
  import { assembleProviderInput, createLoadedSkillSet } from "@arnilo/prism";
172
172
 
173
- const loaded = createLoadedSkillSet(); // session-owned; opt-in checkpoint-persisted via runState.persistSessionState (names only) since 0.1.3
173
+ const loaded = createLoadedSkillSet(); // session-owned; opt-in checkpoint-persisted via runState.persistSessionState (names since 0.1.3, + includeSkillBodies exact instructions since 0.1.6)
174
174
  const request = await assembleProviderInput({
175
175
  model,
176
176
  input: "Hi",
@@ -256,7 +256,7 @@ Use `activateAllCapabilities: true` only as a temporary all-skills/all-tools com
256
256
  - Skill registry lookup is `Map`-backed, and selection is linear in requested skills plus active tools. Strict duplicate mode adds one O(1) `Map.has()` check during registration only.
257
257
  - Progressive catalog render is O(active skills) with byte/count caps; `load_skill` lookup is O(1). Budget eviction over context/skills is O(n log n) worst case.
258
258
  - `load_skill` cannot grant tools; loaded instructions are untrusted text bounded by hard caps. `toolResultFold` summarizer output is untrusted and capped; failures keep raw tool results.
259
- - Loaded-skill names are session-scoped. Since 0.1.3 (plan 015 Task 4) a durable run may opt in to persistence with `runState.persistSessionState: true` (and the same flag on resume options): the name catalog rides the run-state checkpoint (≤64 names, ≤256 chars each, charged against `maxStateBytes`) and is restored into the session `LoadedSkillSet` on resume, so progressive disclosure survives restart. **Bodies are never persisted** — they re-resolve from the live skill registry the next time the model loads the skill. Default off: checkpoint shape is identical to 0.1.2.
259
+ - Loaded-skill names are session-scoped. Since 0.1.3 (plan 015 Task 4) a durable run may opt in to persistence with `runState.persistSessionState: true` (and the same flag on resume options): the name catalog rides the run-state checkpoint (≤64 names, ≤256 chars each, charged against `maxStateBytes`) and is restored into the session `LoadedSkillSet` on resume, so progressive disclosure survives restart. **Bodies are never persisted by default** — they re-resolve from the live skill registry the next time the model loads the skill. Since 0.1.6 (plan 018 closeout `checkpoint-bodies`), `runState.includeSkillBodies: true` on both run and resume options persists the exact loaded-skill instructions (`{name, instructions}` pairs, redacted at rest like all checkpoint state, ≤64 bodies / ≤256-char names / ≤262144-byte bodies / ≤1 MiB total) and resume re-renders them registry-independently with no `load_skill` round-trip; the `maxStateBytes` ceiling refuses oversize bodies with a recorded error (never truncates). Default off: checkpoint shape is identical to 0.1.3.
260
260
  - These helpers perform no provider calls, tool execution, resource loading, package discovery, filesystem/network access, retries, timers, or watchers by themselves.
261
261
  - Context and skill output is host/extension data. Do not include secrets unless the host explicitly accepts that prompt exposure.
262
262
  - Active tools remain host-supplied; skills and middleware do not activate tools or grant permissions. Use `duplicate: "error"` when loading third-party skills to prevent silent name shadowing.
@@ -0,0 +1,85 @@
1
+ # Document reader (`@arnilo/prism-document-reader`)
2
+
3
+ ## What it does
4
+
5
+ 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.
6
+
7
+ ## When to use it
8
+
9
+ Use when coding agents must read PDF/Office files (specs, requirements docs, reports) as literal text. Do **not** use it when embedded content execution, macro evaluation, or external resource fetching is required — this adapter never does any of those by construction, and the optional peer parsers (`pdf-parse`, `mammoth`) are the only parsing code involved. Docker-less hosts that need document reads pair this with the network-free native sandbox backend (`@arnilo/prism-coding-security` `createNativeSandbox`) for the surrounding tool execution.
10
+
11
+ Activation is explicit: no file-extension sniffing anywhere enables parsing. Absent `documentReader` option = exactly the 0.1.5 read behavior.
12
+
13
+ ## Inputs / request
14
+
15
+ `createDocumentReader(options?)`:
16
+
17
+ | Option | Meaning | Default | Ceiling |
18
+ | --- | --- | --- | --- |
19
+ | `maxBytes` | Hard input size cap; oversize files refuse before loading | 32 MiB | 512 MiB |
20
+ | `maxPages` | Page cap for formats that report pages; over-page documents refuse | 1000 | 10 000 |
21
+ | `maxTextBytes` | Extracted-literal-text cap; over-cap results truncate (`truncatedBy: "bytes"`) | 2 MiB | 64 MiB |
22
+ | `parsers` | Host-selected `DocumentParser[]`; default wiring loads the optional peers | `[pdf, docx]` | — |
23
+ | `redactor` | Optional `SecretRedactor` applied to extracted text at the adapter boundary | none | — |
24
+
25
+ Format gating is magic-byte based: PDF header (`%PDF-`); DOCX zip container + `word/document.xml` part marker. Unsupported buffers return `null` and the read falls through to its text path.
26
+
27
+ ## Outputs / response / events
28
+
29
+ `DocumentReader.extract({ buffer, path, signal })` resolves to `{ text, format, pages, truncatedBy }` or `null`. The read tool returns the text as a normal text content block with `metadata.document = { format, pages, truncatedBy }`.
30
+
31
+ Errors: `DocumentReaderError` with code `ERR_PRISM_DOCUMENT_READER` for missing peers (at creation), over-page/oversize refusal, and parser output beyond `maxTextBytes`. Invalid caps throw `RangeError` at creation.
32
+
33
+ ## Request/response example
34
+
35
+ ```ts
36
+ import { createReadTool } from "@arnilo/prism-coding-agent";
37
+ import { createDocumentReader } from "@arnilo/prism-document-reader";
38
+
39
+ const documentReader = await createDocumentReader({
40
+ maxBytes: 32 * 1024 * 1024,
41
+ maxPages: 1000,
42
+ redactor: hostRedactor, // optional
43
+ });
44
+ const read = createReadTool(cwd, { documentReader });
45
+ ```
46
+
47
+ A `read` of `spec.pdf` yields text content extracted from the PDF (up to 2 MiB of literal text) with `metadata.document = { format: "pdf", pages, truncatedBy }`.
48
+
49
+ ## Implementation example
50
+
51
+ ```ts
52
+ import { createDocumentReader, createPdfParser, type DocumentParser } from "@arnilo/prism-document-reader";
53
+
54
+ // Host-selected parser wiring: swap in a different PDF backend without touching bounds.
55
+ const myPdfParser: DocumentParser = {
56
+ format: "pdf",
57
+ detect: (buffer) => buffer.toString("latin1", 0, 5) === "%PDF-",
58
+ extract: async (buffer, { maxPages, maxTextBytes }) => {
59
+ // ... host parser (must honor caps, never fetch, never execute)
60
+ return { text, pages, truncatedBy: null };
61
+ },
62
+ };
63
+ const reader = await createDocumentReader({ parsers: [myPdfParser, await createPdfParser()] });
64
+ ```
65
+
66
+ ## Extension and configuration notes
67
+
68
+ - Default parser wiring uses the optional peer dependencies `pdf-parse` (PDF) and `mammoth` (DOCX raw text). Both are declared optional (`peerDependenciesMeta`); `createDocumentReader` fails closed with a documented error at creation when a selected format's peer is absent — never at read time. Hosts pin parser versions (their CVE surface is the host's responsibility; parser advisory is reviewed at ship time).
69
+ - DOCX has no page concept in raw text: `pages` is always `1` and the page cap applies to PDF only; the text cap governs DOCX output.
70
+ - The read tool re-checks `maxTextBytes` on results (parity with its text-page bounds check) and refuses reader output beyond it.
71
+ - The adapter truncates over-cap text at a UTF-8 byte boundary (never splits a code point).
72
+
73
+ ## Security and performance notes
74
+
75
+ - No embedded-script execution, no macro evaluation, no external resource fetching — the peer raw-text surfaces are pure extractors, and the no-fetch property is enforced by an egress tripwire test.
76
+ - Decompression/size-bomb protection: the read tool stats and refuses files above `maxBytes` before loading; output is capped at `maxTextBytes`.
77
+ - Extraction envelope (recorded in `scripts/budgets.json` `docReader`, measured 2026-08-11): a max-cap 1000-page PDF (288 KB) extracts in ~162 ms with ~17 MB heap delta; the gate asserts completion within the ceiling or documented refusal.
78
+ - Parser code never receives a buffer whose format gate failed; random binaries never reach a parser.
79
+
80
+ ## Related APIs
81
+
82
+ - `createReadTool` / `DocumentReader` / `DocumentReaderResult` (`@arnilo/prism-coding-agent`)
83
+ - `SecretRedactor` (`@arnilo/prism` redaction)
84
+ - `docs/_evidence/phase18-primitive-review.md` (doc-reader threat model D1–D8)
85
+ - `docs/coding-security.md` (native sandbox backend for surrounding execution containment)
package/docs/index.md CHANGED
@@ -11,7 +11,7 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
11
11
  - [Model routing](model-routing.md): optional `@arnilo/prism-model-router` allow-list/residency/budget/rate/circuit/fallback governance with redacted diagnostics; durable state requires awaited identity-scoped calls.
12
12
 
13
13
  ## Agent/session runtime
14
- - [Agent/session runtime](agent-session-runtime.md): create explicit or opt-in secure agents/sessions, get direct `AgentRunResult` values from `run`/`prompt`, mid-run `steer` (turn-boundary or softInterrupt), use integrated `stream()`/`resumeAgentRunStream()`, shared batch pending-decisions / sticky run-scope approvals, subscribe to normalized events, and expose opted-in durable lifecycle capabilities.
14
+ - [Agent/session runtime](agent-session-runtime.md): create explicit or opt-in secure agents/sessions, get direct `AgentRunResult` values from `run`/`prompt`, mid-run `steer` (turn-boundary or softInterrupt), use integrated `stream()`/`resumeAgentRunStream()`, shared batch pending-decisions / sticky run-scope approvals, subscribe to normalized events, and expose opted-in durable lifecycle capabilities (0.1.6 plan 018 closeout `checkpoint-bodies`: optional `includeSkillBodies` persists the exact loaded-skill instructions with the names-only `persistSessionState`, so resume re-renders bodies registry-independently; ≤64 bodies, `maxStateBytes` refuses oversize).
15
15
  - [Agent definitions](agent-definitions.md): resolve declarative `AgentDefinition` values via `resolveAgentDefinition`, and turn app-config `<configRoot>/agents/<name>/AGENT.md` bundles into runnable agents via `discoverAgentBundles` / `resolveAgentBundle` (explicit tool/skill activation by name, fail-closed omitted capabilities, migration-only `activateAllCapabilities`, strict duplicate scope checks, configurable prompt layers, no auto-discovery).
16
16
  - [Agent loops](agent-loops.md): replaceable per-run control loops — `singleShotLoop` default, opt-in bounded artifact-loop tool rounds, and durable custom-loop `revision`/`snapshot`/`restore` hooks with fail-closed resume.
17
17
  - [Guardrails](guardrails.md): typed fail-closed input/output/tool checks with buffered provider output and redacted decision records.
@@ -74,11 +74,11 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
74
74
  - [Work connectors](work-connectors.md): connector principles, capability gates, scoped OAuth establishment (0.0.14), and out-of-scope boundaries (Slack/Teams channels not shipped) for Microsoft 365 / Google Workspace.
75
75
  - [Browser automation](browser-automation.md): optional `@arnilo/prism-browser` with host-supplied Playwright contexts, AI-mode snapshots/refs, ordered `browser_open`/`browser_snapshot`/`browser_act`/`browser_close` plus (0.1.4) `browser_evaluate`/`browser_observe` and CDP `block_urls`/`unblock_urls`/`throttle`/`emulate` act actions on Chromium hosts, egress/side-effect/upload/download/screenshot policy, finite page/action/snapshot/network/artifact caps, and 0.0.14 verified-state checkpoints with reload/verify-before-side-effect.
76
76
  - [Device adapters](device-adapters.md): deny-by-default realtime voice / desktop-control contract + conformance (0.0.14); no vendor package — admission fails closed without explicit consent+sandbox+approval, stream bounds, shared `RunLimits`, redacted telemetry.
77
- - [Coding agent tools](coding-agent-tools.md): optional `shell`, `read`, `write`, `edit`, `repo_list`, `repo_search`, `glob`, `delete`, and `move` definitions plus opt-in `createGitTools()` / `coding_check`, opt-in `createAskUserDecisionTool` (single/multi/free-text + durable suspend glue), and `runCodingGoalVerify`; durable plan/todo Markdown helpers with workflow `state.coding` checkpoint metadata; streamed text pages, `repo_search` `outputMode`, bounded glob, optional read-before-write, optional Git-aware (`createGitAwareRepositoryOperations`) ignore-aware enumeration with native fallback, finite Git/check/plan/ask caps, bounded image/edit reads and write/edit payloads, finite shell wall/total-output limits, secure host-owned spill cleanup, pluggable bounded operation contracts, per-path mutation serialization, and optional `ExecutionPolicy`. No PDF/trash/PTY in the 0.0.21 baseline; Phase 9 adds optional language intelligence (separate page). Limits do not sandbox host access—gate with permission/trust policy and `@arnilo/prism-coding-security`.
77
+ - [Coding agent tools](coding-agent-tools.md): optional `shell`, `read`, `write`, `edit`, `repo_list`, `repo_search`, `glob`, `delete`, and `move` definitions plus opt-in `createGitTools()` / `coding_check`, opt-in `createAskUserDecisionTool` (single/multi/free-text + durable suspend glue), and `runCodingGoalVerify`; durable plan/todo Markdown helpers with workflow `state.coding` checkpoint metadata; streamed text pages, `repo_search` `outputMode`, bounded glob, optional read-before-write, optional Git-aware (`createGitAwareRepositoryOperations`) ignore-aware enumeration with native fallback, finite Git/check/plan/ask caps, bounded image/edit reads and write/edit payloads, finite shell wall/total-output limits, secure host-owned spill cleanup, pluggable bounded operation contracts, per-path mutation serialization, and optional `ExecutionPolicy`. 0.1.6 adds the optional [document reader](document-reader.md) slot (`@arnilo/prism-document-reader`, plan 018 closeout `doc-reader`): bounded PDF/DOCX literal-text extraction behind `createReadTool({ documentReader })` with magic-byte format gating, input/page/text caps, fail-closed optional peer parsers, and no embedded-content execution or external fetching. 0.1.6 also adds opt-in recursive `delete` (`recursive: true`, bounded fan-out, symlink children never followed) and bounded `{a,b}` glob expansion (`braceExpansion`, max 128 alternatives / 4096 bytes, fail-closed) behind plan 018 closeout `delete-glob`. No PDF/trash/PTY in the 0.0.21 baseline (0.1.6's document reader is the demand-gated optional exception); Phase 9 adds optional language intelligence (separate page). Limits do not sandbox host access—gate with permission/trust policy and `@arnilo/prism-coding-security`.
78
78
  - [Language intelligence](language-intelligence.md): optional host-activated `createLanguageIntelligence` — bounded in-package LSP 3.17 JSON-RPC client (Content-Length framing), host-selected server command/args per language, workspace symbols/definitions/references/diagnostics/hover/rename; lazy spawn; URI root confinement; rename gated by `ExecutionPolicy` + atomic write/mutation queue; frozen message/diagnostic/pending/result/timeout/server caps. No `vscode-languageserver-protocol` dependency.
79
79
  - [Process sessions](process-sessions.md): optional host-activated `createProcessSessions` — long-running process registry (start/cursor-paged output/input/wait/signal/kill/release), native or sandbox `startProcess` backend (fail closed when absent), ownership/identity + expiry sweep on access, `reconcile` / sandbox-loss → `unknown` (never fabricates exitCode), durable command fingerprint metadata, `CodingProcessEvent` host sink, `ExecutionPolicy` before spawn and on mutate, frozen session/input/lifetime/output caps; PTY fails closed as unsupported.
80
80
  - [Forge integration](forge-integration.md): optional host-activated `createGitHubForge` — reference GitHub adapter (issue context, authenticated push via `BoundGitRunner` + `GIT_CONFIG_*` credential injection, PR create/update, review comments, check/status retrieval, bounded `reconcileHandoff`), every mutation gated by `ExecutionPolicy` and recorded in `ToolEffectStore` (retry never duplicates PRs/comments), typed `ForgeError` codes (auth/API/stale/rate-limit/limit/ownership), frozen page/payload/comment/concurrency/timeout caps, no octokit dependency, tokens never in argv/logs/events.
81
- - [Coding execution approval and sandboxing](coding-security.md): path/command approval, identity-scoped caching, shell-turn exclusivity, required `workspaceMode` (`host`/`sandbox`) with fail-closed mixed wiring, `createSandboxCodingComposition()` containment metadata, disposable Docker/OCI sandbox reference with bounded workspace import/export, optional `DisposableSandbox.startProcess` / `SandboxProcessHandle` for process-session backends, and allow-list egress (`createEgressPolicy` deny-all exact rules + frozen presets, `createAllowListEgressProxy` HTTP/CONNECT proxy with pinned-DNS rebinding defense, private/metadata IP denial, redirect re-validation + hop cap, byte/time caps, per-decision audit, `composeEgressSandboxNetwork` attestation recorded as `prism.egress.*` labels; TLS pass-through, no interception).
81
+ - [Coding execution approval and sandboxing](coding-security.md): path/command approval, identity-scoped caching, shell-turn exclusivity, required `workspaceMode` (`host`/`sandbox`) with fail-closed mixed wiring, `createSandboxCodingComposition()` containment metadata, disposable Docker/OCI sandbox reference with bounded workspace import/export (0.1.6 adds the Linux-only network-free `createNativeSandbox` backend — fresh netns per command via `unshare`, `ulimit` hard caps, cwd containment, fails closed where egress denial is impossible), optional `DisposableSandbox.startProcess` / `SandboxProcessHandle` for process-session backends, and allow-list egress (`createEgressPolicy` deny-all exact rules + frozen presets, `createAllowListEgressProxy` HTTP/CONNECT proxy with pinned-DNS rebinding defense, private/metadata IP denial, redirect re-validation + hop cap, byte/time caps, per-decision audit, `composeEgressSandboxNetwork` attestation recorded as `prism.egress.*` labels; TLS pass-through, no interception).
82
82
 
83
83
  ## Extensions/plugins
84
84
  - [Contribution discovery (workspace)](contribution-discovery.md): opt-in, realpath-contained directory scanner turning `SKILL.md`/`manifest.json` into inert `DiscoveredContribution` envelopes the host registers — no `import()`, no auto-activate, no provider scanning. Per-agent bundles remain app-controlled and are documented under Agent/session runtime.
@@ -99,7 +99,7 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
99
99
  - [Supervisor delegation](supervisors.md): optional explicit child allow-list, derived memory scopes, narrowing-only permissions, lifecycle hooks, nested delegation, cancellation, finite budgets, host-projected delegation telemetry, and separate A2A durable adapter boundary.
100
100
  - [A2A interoperability](a2a.md): A2A 1.0 JSON-RPC/HTTPS cards plus host-owned durable task get/list/cancel/subscribe, shared `AgentEventSource` task adapter, bounded rich parts/replay, principal-scoped push configs, exact-origin verified client, rich stream seam for explicit AG-UI fronting, and server-side `createAgUiA2AServer` exposure of a local AG-UI agent (0.0.26).
101
101
  - [Frontend interoperability (AG-UI and ACP)](ag-ui.md): optional `@arnilo/prism-ag-ui` full AG-UI 0.0.57 input/event/capability mapper, authorized Web handler/distributed source follow, opt-in A2UI painting middleware, explicit hardened MCP/MCP Apps/remote A2A adapters, a framework-free reference renderer subpath (`@arnilo/prism-ag-ui/renderer`, 0.0.26), and stable ACP sibling over shared redacted event and durable-approval seams; 0.0.14 adds reconnectable co-work events.
102
- - [ACP coding-host interop](acp.md): stable ACP v1 `createPrismAcpAgent()`/`createAcpEventMapper()` over `@agentclientprotocol/sdk@1.3.0` — capability advertisement is a pure function of host seams (sessions load/list/delete/resume/dirs, close always, prompt media/embedded, MCP http/sse), client fs/terminal adapters, modes and config options as host overlays, `CodingLifecycleEvent` mapping, four-outcome approvals with elicitation, and frozen caps (0.0.27); 0.1.1 adds ownership-scoped persistence guidance for host-persisted modes/config (plan 013 Task 5 — the agent never persists them).
102
+ - [ACP coding-host interop](acp.md): stable ACP v1 `createPrismAcpAgent()`/`createAcpEventMapper()` over `@agentclientprotocol/sdk@1.3.0` — capability advertisement is a pure function of host seams (sessions load/list/delete/resume/dirs, close always, prompt media/embedded, MCP http/sse), client fs/terminal adapters, modes and config options as host overlays, `CodingLifecycleEvent` mapping, four-outcome approvals with elicitation, and frozen caps (0.0.27); 0.1.1 adds ownership-scoped persistence guidance for host-persisted modes/config (plan 013 Task 5 — the agent never persists them); 0.1.6 adds the optional host-owned `AcpSessionStore` durability seam — live registry (modes/config/cwd/ownership) survives agent restart, restore is ownership-scoped and fail-closed (plan 018 Task 2).
103
103
  - [AG-UI adoption evaluation](ag-ui-adoption.md): official 0.0.57 input/event/capability matrix and shipped hardened MCP/MCP Apps/A2A handshake boundaries.
104
104
 
105
105
  ## CLI/RPC
@@ -129,7 +129,7 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
129
129
  - [Ponytail behavior integration](ponytail.md): optional `@arnilo/prism-ponytail` — upstream Ponytail skills/commands, `ponytail-mode` injector, session `ponytail-mode` persistence; resolves peer `@dietrichgebert/ponytail` or `upstreamPath`; opt-in (not in code/sdk profiles).
130
130
 
131
131
  ## Release and install
132
- - [Release and install](release-and-install.md): current **0.1.5** 49-package graph (root + 48 workspace packages; plan 017 the documented breaking cut — deprecated-option removal with `docs/migration.md` `0.1.4 → 0.1.5` section and reviewed compat-baseline regeneration via `--allow-break` then `--update-baseline`: the inert provider request knobs, `RunOptions.maxToolRounds`, observational-memory flat settings keys + top-level worker aliases, `ReadToolOptions.autoResizeImages`, `INIT_PROVIDERS`; all removals fail closed naming their replacement; plan 016 internal god-module split — `agents.ts`/`contracts.ts` reorganized behind barrel re-exports with a byte-identical public entry surface, measured tree-shaking improvement in `scripts/phase16-baseline.json`, and additive `@arnilo/prism-browser` Chrome DevTools Protocol capabilities — `browser_evaluate`/`browser_observe` and `block_urls`/`unblock_urls`/`throttle`/`emulate` act actions; plan 015 dead-code and deprecation hygiene on the frozen 0.1.x line — parameterized benchmark runner `scripts/benchmark.mjs` absorbing the per-version runners, archived review-coverage evidence in `docs/_evidence/`, non-blocking unused-code sweep `npm run sweep:unused`, opt-in checkpoint persistence for loaded-skill names and read-path sets; plan 014 Alibaba provider enrichment — embeddings, video input, verified compatible-mode surface decision table; plan 013 post-release hardening — build single-flight, MCP SSE relay test, combined coverage summary, canonical manifest-count narrative, ACP modes/config persistence guidance; Phase 12 release-candidate hardening; plan 012 — freeze manifest, compatibility matrix, upgrade matrix, packed-install e2e journeys, restart-recovery evidence, capacity envelopes, security policy), exact-peer/install/tarball rules, deterministic resumable publication and publish dry-run, frozen 0.1.x compatibility and support matrix (Node/PostgreSQL/platform/provider/protocol pins and unsupported combinations, machine-checked against `scripts/phase12-freeze-manifest.json`), protected PostgreSQL gate, pinned supply-chain gates, offline tests, the 0.0.15 provider/AI-SDK/RAG/memory protected live-canary matrix, and sandbox-browser Docker/Playwright gates.
132
+ - [Release and install](release-and-install.md): current **0.1.6** 50-package graph (root + 49 workspace packages, including the plan 018 optional `@arnilo/prism-document-reader` — the graph grew 49 → 50 because its doc-reader closeout was demanded; plan 017 the documented breaking cut — deprecated-option removal with `docs/migration.md` `0.1.4 → 0.1.5` section and reviewed compat-baseline regeneration via `--allow-break` then `--update-baseline`: the inert provider request knobs, `RunOptions.maxToolRounds`, observational-memory flat settings keys + top-level worker aliases, `ReadToolOptions.autoResizeImages`, `INIT_PROVIDERS`; all removals fail closed naming their replacement; plan 016 internal god-module split — `agents.ts`/`contracts.ts` reorganized behind barrel re-exports with a byte-identical public entry surface, measured tree-shaking improvement in `scripts/phase16-baseline.json`, and additive `@arnilo/prism-browser` Chrome DevTools Protocol capabilities — `browser_evaluate`/`browser_observe` and `block_urls`/`unblock_urls`/`throttle`/`emulate` act actions; plan 015 dead-code and deprecation hygiene on the frozen 0.1.x line — parameterized benchmark runner `scripts/benchmark.mjs` absorbing the per-version runners, archived review-coverage evidence in `docs/_evidence/`, non-blocking unused-code sweep `npm run sweep:unused`, opt-in checkpoint persistence for loaded-skill names and read-path sets; plan 014 Alibaba provider enrichment — embeddings, video input, verified compatible-mode surface decision table; plan 013 post-release hardening — build single-flight, MCP SSE relay test, combined coverage summary, canonical manifest-count narrative, ACP modes/config persistence guidance; Phase 12 release-candidate hardening; plan 012 — freeze manifest, compatibility matrix, upgrade matrix, packed-install e2e journeys, restart-recovery evidence, capacity envelopes, security policy), exact-peer/install/tarball rules, deterministic resumable publication and publish dry-run, frozen 0.1.x compatibility and support matrix (Node/PostgreSQL/platform/provider/protocol pins and unsupported combinations, machine-checked against `scripts/phase12-freeze-manifest.json`), protected PostgreSQL gate, pinned supply-chain gates, offline tests, the 0.0.15 provider/AI-SDK/RAG/memory protected live-canary matrix, and sandbox-browser Docker/Playwright gates.
133
133
  - [0.1.0 / 1.0 readiness gates](0.1.0-readiness.md): command-per-gate 1.0 readiness table — frozen API surface + compat gate, migration/docs tripwires, budget table, live-suite matrix, security matrix, current-line status (**0.0.23** published target), signed-publication/live-canary prerequisites for 1.0, and Phase 12 demand-evidence entry criteria.
134
134
  - [Review coverage archive](_evidence/): per-phase evidence freezes (plans 067–079, releases 0.0.4–0.0.16) — traceability matrices, provider validation, capability/primitive/limit matrices, benchmark budgets, and artifact-diet findings; tarball-excluded, kept in-repo for audit.
135
135
 
@@ -2,11 +2,11 @@
2
2
 
3
3
  ## What it does
4
4
 
5
- Prism is published as **49 publishable manifests**: the root `@arnilo/prism` core package plus **48 workspace packages** — 14 provider adapters, 9 `prism-*` family/profile packages, and 25 capability packages. (Regenerate the counts: `ls packages/*/package.json | wc -l` = 48 workspace; `ls -d packages/provider-*/ | wc -l` = 14; `ls -d packages/prism-*/ | wc -l` = 9; capability = 48 − 14 − 9 = 25; publishable = root + 48 = 49.) This page describes how they are packed, what each tarball contains, how to install them, the required `@arnilo/prism` peer dependency, the release workflow, and the offline test budget. The measurable 1.0 readiness gates (command-per-gate) live in [`0.1.0-readiness.md`](./0.1.0-readiness.md).
5
+ Prism is published as **50 publishable manifests**: the root `@arnilo/prism` core package plus **49 workspace packages** — 14 provider adapters, 9 `prism-*` family/profile packages, and 26 capability packages. (Regenerate the counts: `ls packages/*/package.json | wc -l` = 49 workspace; `ls -d packages/provider-*/ | wc -l` = 14; `ls -d packages/prism-*/ | wc -l` = 9; capability = 49 − 14 − 9 = 26; publishable = root + 49 = 50.) The 50th manifest is the 0.1.6 plan 018 optional `@arnilo/prism-document-reader` package (bounded PDF/Office literal-text extraction for the coding read tool; ships only because its `doc-reader` closeout is demanded — a deferred closeout keeps the graph at 49). This page describes how they are packed, what each tarball contains, how to install them, the required `@arnilo/prism` peer dependency, the release workflow, and the offline test budget. The measurable 1.0 readiness gates (command-per-gate) live in [`0.1.0-readiness.md`](./0.1.0-readiness.md).
6
6
 
7
7
  Core `@arnilo/prism` ships runtime, CLI, templates, and docs. Every code package has a required `@arnilo/prism@0.1.0` peer; profiles are pure manifests. Installation activates no provider, listener, database, browser, credential, or tool capability.
8
8
 
9
- Current **49** publishable manifests (root + 48 workspace packages):
9
+ Current **50** publishable manifests (root + 49 workspace packages):
10
10
 
11
11
  `@arnilo/prism`, `@arnilo/prism-ag-ui`, `@arnilo/prism-browser`, `@arnilo/prism-coding-agent`, `@arnilo/prism-coding-security`, `@arnilo/prism-compaction-llm`
12
12
  `@arnilo/prism-compaction-observational-memory`, `@arnilo/prism-credentials-node`, `@arnilo/prism-enterprise-postgres`, `@arnilo/prism-evals`, `@arnilo/prism-mcp`, `@arnilo/prism-memory`
@@ -15,7 +15,7 @@ Current **49** publishable manifests (root + 48 workspace packages):
15
15
  `@arnilo/prism-provider-alibaba`, `@arnilo/prism-provider-anthropic`, `@arnilo/prism-provider-azure`, `@arnilo/prism-provider-bedrock`, `@arnilo/prism-provider-google`, `@arnilo/prism-provider-kimi`
16
16
  `@arnilo/prism-provider-neuralwatt`, `@arnilo/prism-provider-ollama`, `@arnilo/prism-provider-openai`, `@arnilo/prism-provider-opencode-go`, `@arnilo/prism-provider-openrouter`, `@arnilo/prism-provider-vertex`
17
17
  `@arnilo/prism-provider-zai`, `@arnilo/prism-rag`, `@arnilo/prism-server`, `@arnilo/prism-session-store-codecs`, `@arnilo/prism-session-store-nats`, `@arnilo/prism-session-store-postgres`, `@arnilo/prism-session-store-sqlite`
18
- `@arnilo/prism-openapi-tools`, `@arnilo/prism-supervisor`, `@arnilo/prism-tool-validator-json-schema`, `@arnilo/prism-web-tools`, `@arnilo/prism-work-tools`, `@arnilo/prism-workflows`
18
+ `@arnilo/prism-openapi-tools`, `@arnilo/prism-supervisor`, `@arnilo/prism-tool-validator-json-schema`, `@arnilo/prism-web-tools`, `@arnilo/prism-work-tools`, `@arnilo/prism-workflows`, `@arnilo/prism-document-reader`
19
19
 
20
20
  Core ships `dist`, docs, templates, and `CHANGELOG.md`; code packages ship compiled output, README, license, and changelog. Family/profile packages ship manifest, README, and changelog. `@arnilo/prism-providers` includes all eleven `@arnilo/prism-provider-*` packages.
21
21
 
@@ -178,7 +178,7 @@ PRISM_LIVE_PROVIDER_TESTS=1 npm run test --workspaces --if-present
178
178
 
179
179
  ### 0.1.0 publish handoff (plan 012 Task 7)
180
180
 
181
- **Decision: GO when the operator prerequisites below are recorded.** Release **0.1.0** (Phase 12, plan 012) is the release-candidate hardening cut of the **0.0.28** graph: no new packages, public exports, schema migrations, or runtime dependencies (freeze manifest `scripts/phase12-freeze-manifest.json`). Publishable graph stays **49** publishable manifests (root + 48 workspace packages) at exact **0.1.0**. Store compatibility with 0.0.28: **compatible, no migration** ([migration](migration.md) `0.0.28 → 0.1.0`); the full `0.0.17 → 0.1.0` upgrade matrix is in the same page. All evidence for the tree under publication is recorded in [0.1.0 readiness](0.1.0-readiness.md) (capacity envelopes, restart-recovery, e2e journeys, threat-suites leg, audit at moderate).
181
+ **Decision: GO when the operator prerequisites below are recorded.** Release **0.1.0** (Phase 12, plan 012) is the release-candidate hardening cut of the **0.0.28** graph: no new packages, public exports, schema migrations, or runtime dependencies (freeze manifest `scripts/phase12-freeze-manifest.json`). At this line the canonical statement read **49 publishable manifests**: the root `@arnilo/prism` core package plus **48 workspace packages** — 14 provider adapters, 9 `prism-*` family/profile packages, and 25 capability packages. Publishable graph stays **49** publishable manifests (root + 48 workspace packages) at exact **0.1.0**. Store compatibility with 0.0.28: **compatible, no migration** ([migration](migration.md) `0.0.28 → 0.1.0`); the full `0.0.17 → 0.1.0` upgrade matrix is in the same page. All evidence for the tree under publication is recorded in [0.1.0 readiness](0.1.0-readiness.md) (capacity envelopes, restart-recovery, e2e journeys, threat-suites leg, audit at moderate).
182
182
 
183
183
  ```bash
184
184
  # Operator prerequisites (each a named blocked gate — none may be skipped):
@@ -274,6 +274,32 @@ git push origin v0.1.2 # tag push triggers release.yml publish job (prove
274
274
 
275
275
  **Rollback notes.** `release:publish --version 0.1.2 --resume --report release-artifacts/publish-report.json` resumes an interrupted publication and skips only registry versions whose internal dependency fingerprint matches the local manifest. A failed package aborts the run with its status written to the report; re-run after fixing the cause. npm cannot unpublish the `0.1.2` line after 72 hours — a post-publication defect ships as a `0.1.x` patch (additive-only compat promise, `release:gate` enforced), or as a documented break in the next line with a `docs/migration.md` entry. `0.1.2` is store-compatible with `0.1.1` in **both directions** (no migration ran — same checksum-protected contract), so an operator may defer or roll back the patch without a database rollback.
276
276
 
277
+ ### 0.1.6 publish handoff (plan 018 Task 7)
278
+
279
+ **Decision: GO when the operator prerequisites below are recorded.** Release **0.1.6** (plan 018) is the coding-agent capability-closeouts patch on the frozen 0.1.x line — **additive-only** vs 0.1.5 (plain compat gate at 0.1.6 passed with 0 breaking declaration deltas; the baseline text was regenerated with `--update-baseline` for the version literal only, no `--allow-break` anywhere). Five demand-gated closeouts shipped, each flipped to `demanded` by named demand evidence (operator `arn` for native-sandbox/doc-reader/delete-glob/checkpoint-bodies, user `Clay` for acp-session-store) before its task landed; the demand-gate registry (`scripts/phase18-freeze-manifest.json`) machine-checks demanded ⇒ implemented, deferred ⇒ untouched. Shipped: (1) **durable ACP session store** — `@arnilo/prism-ag-ui` `AcpSessionStore` host seam (`save`/`loadAll`/`evict`), persisted `{sessionId, ownership, modeId, configValues, cwd, additionalDirectories, updatedAt}`, lazy ownership-scoped restore, fail-closed drops, absent seam = 0.1.5 behavior; (2) **network-free native sandbox** — `createNativeSandbox` in `@arnilo/prism-coding-security` (fresh netns per command via the OS `unshare` binary, chained ulimits with `|| exit 126`, argv-only exec, cwd containment, process-group kill, env allow-list, Linux-only fail-closed); (3) **bounded PDF/Office document reader** — new optional package `@arnilo/prism-document-reader` (the 50th manifest, graph 49 → 50) with optional `pdf-parse`/`mammoth` peers fail-closed at creation, magic-byte gating, null fall-through, caps + redaction at the adapter boundary; (4) **recursive delete + brace-expanding glob** — per-call `recursive: true` with fan-out cap and symlink-unlink-not-follow, host-selected/per-call `braceExpansion` bounded to 128 alternatives / 4096 expanded bytes, fail-closed on overflow/malformed braces; (5) **checkpoint persistence for loaded-skill bodies** — opt-in `includeSkillBodies` on run + resume options (names-only stays default, 0.1.3 shapes byte-identical), ≤64 bodies / ≤256-char names / ≤262144-byte bodies / ≤1 MiB total, `maxStateBytes` refusal, redacted at rest, registry-independent resume render. Store compatibility with 0.1.5: **compatible, no migration** (additive-only; no persisted-shape change; `docs/migration.md` gains no entries). Exit gate green: npm test core 1,433/1,433 + 190 script gates (incl. phase18-freeze done-phase), `sdk:ready` exit 0, audit 0 moderate, pack dry-run 50/50 twice byte-identical, budget/benchmark gates green; evidence in `scripts/phase18-baseline.json` `exitGate`. Rollback = restore the 0.1.5 manifests/tag.
280
+
281
+ ```bash
282
+ # Operator prerequisites recorded: clean tree at the v0.1.6 tag candidate, GPG key, npm OIDC publisher.
283
+ npm test # core + workspace suites + all script gates
284
+ npm run sdk:ready # typecheck, lint, format, test, pack, release:gate
285
+ node scripts/release.mjs gate --version 0.1.6 # plain additive gate, 0 breaking deltas
286
+ npm run pack:dry-run # twice; diff reports — deterministic
287
+ npm audit --audit-level=moderate
288
+ npm run release:check -- --version 0.1.6 --report /tmp/prism-0.1.6-preflight.json
289
+ npm run release:publish -- --version 0.1.6 --dry-run --allow-dirty --allow-untagged --report /tmp/prism-0.1.6-dry-run.json
290
+ # run the dry-run twice and diff the reports: deterministic, byte-identical
291
+
292
+ # Sign the release on the clean tagged tree (operator GPG key):
293
+ git tag -s v0.1.6 -m "Prism 0.1.6 — coding-agent capability closeouts (additive)"
294
+ git verify-tag v0.1.6
295
+ git push origin v0.1.6 # tag push triggers release.yml publish job (provenance, attestations)
296
+
297
+ # Real publication never bypasses the gates: release.mjs refuses
298
+ # --allow-dirty/--allow-untagged without --dry-run.
299
+ ```
300
+
301
+ **Rollback notes.** `release:publish --version 0.1.6 --resume --report release-artifacts/publish-report.json` resumes an interrupted publication and skips only registry versions whose internal dependency fingerprint matches the local manifest. A failed package aborts the run with its status written to the report; re-run after fixing the cause. npm cannot unpublish the `0.1.6` line after 72 hours — a post-publication defect ships as a `0.1.x` patch (additive-only compat promise, `release:gate` enforced), or as a documented break in the next line with a `docs/migration.md` entry. `0.1.6` is store-compatible with `0.1.5` in **both directions** (no migration ran — same checksum-protected contract), so an operator may defer or roll back the patch without a database rollback. The next line **0.1.7** continues the frozen 0.1.x additive promise; the 0.2.0 module line (delegated agents, agent-owned persistence, host-owned seam expansions) is the next documented cut.
302
+
277
303
  ### 0.1.5 publish handoff (plan 017 Task 4)
278
304
 
279
305
  **Decision: GO when the operator prerequisites below are recorded.** Release **0.1.5** (plan 017) is the **documented breaking cut** on the frozen 0.1.x line — deprecated-option removal, with the removed-symbols list, replacements, before/after examples, dynamic-config refusal behavior, store compatibility, and rollback in the top `docs/migration.md` `0.1.4 → 0.1.5` section. Removed: `ProviderRequestOptions.timeoutMs`/`maxRetries`/`maxRetryDelayMs` (inert in first-party providers; abort/retry lives at the host layer — replacements `RunOptions.signal`/`AgentConfig.retry`/`RunOptions.retry`), `RunOptions.maxToolRounds` (→ `limits.maxToolRounds`; CLI `--max-tool-rounds` unchanged), `ObservationalMemorySettingsInput` pre-0.0.19 flat keys and top-level `workerProvider`/`workerModel` aliases (→ nested `observation`/`reflection`/`dropper` configs; `sessionModel` fallback unchanged), `ReadToolOptions.autoResizeImages` (→ `transformImage`), and `INIT_PROVIDERS` (→ `listInitProviders()`). Every removal **fails closed** for untyped callers with a `TypeError` naming the replacement before any provider call, tool call, filesystem access, compaction, or session append. Compat baselines were regenerated only after the reviewed `--allow-break` break report: `arnilo__prism.txt` (removed `INIT_PROVIDERS` const + `maxToolRounds`/provider-knob member lines — interface members are not baseline text, so the delta is the `INIT_PROVIDERS` line), `arnilo__prism-coding-agent.txt` (`autoResizeImages` is an interface member — baseline delta limited to statement/re-export text if any), `arnilo__prism-compaction-observational-memory.txt` (flat keys and worker aliases are interface members — no baseline line delta expected). Publishable graph stays **49** manifests (root + 48 workspace) at exact **0.1.5**. Store compatibility with 0.1.4: **compatible, no migration** (removed options were inert aliases; nested replacements resolve to the same active values; `DEFAULT_RUN_LIMITS.maxToolRounds` 8 / hard cap 64 unchanged); zero new dependencies (lockfile name-set unchanged).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnilo/prism",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Agent harness for AI providers, agents, sessions, and tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -134,6 +134,7 @@
134
134
  "packages/enterprise-postgres",
135
135
  "packages/browser",
136
136
  "packages/ag-ui",
137
+ "packages/document-reader",
137
138
  "packages/prism-*"
138
139
  ],
139
140
  "scripts": {
@@ -142,7 +143,7 @@
142
143
  "build": "npm run build:core && npm run build --workspaces --if-present",
143
144
  "typecheck": "npm run build && npm run typecheck --workspaces --if-present && tsc -p examples --noEmit",
144
145
  "sweep:unused": "node scripts/sweep-unused.mjs",
145
- "test": "npm run build && node --test dist/__tests__/*.test.js && node --test scripts/release-gate.test.mjs scripts/tooling-gate.test.mjs scripts/budget-gate.test.mjs scripts/phase8-conformance.test.mjs scripts/phase9-conformance.test.mjs scripts/phase10-conformance.test.mjs scripts/phase11-conformance.test.mjs scripts/phase11-freeze.test.mjs scripts/phase12-freeze.test.mjs scripts/phase13-freeze.test.mjs scripts/phase14-freeze.test.mjs scripts/phase15-freeze.test.mjs scripts/phase16-freeze.test.mjs scripts/phase17-freeze.test.mjs scripts/benchmark-0.1.0.test.mjs scripts/sweep-unused.test.mjs scripts/e2e-enterprise-journey.test.mjs scripts/e2e-coding-journey.test.mjs && npm run test --workspaces --if-present",
146
+ "test": "npm run build && node --test dist/__tests__/*.test.js && node --test scripts/release-gate.test.mjs scripts/tooling-gate.test.mjs scripts/budget-gate.test.mjs scripts/phase8-conformance.test.mjs scripts/phase9-conformance.test.mjs scripts/phase10-conformance.test.mjs scripts/phase11-conformance.test.mjs scripts/phase11-freeze.test.mjs scripts/phase12-freeze.test.mjs scripts/phase13-freeze.test.mjs scripts/phase14-freeze.test.mjs scripts/phase15-freeze.test.mjs scripts/phase16-freeze.test.mjs scripts/phase17-freeze.test.mjs scripts/phase18-freeze.test.mjs scripts/benchmark-0.1.0.test.mjs scripts/sweep-unused.test.mjs scripts/e2e-enterprise-journey.test.mjs scripts/e2e-coding-journey.test.mjs && npm run test --workspaces --if-present",
146
147
  "test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=60 --test-coverage-functions=70 --test-coverage-branches=75 --test-coverage-exclude='**/__tests__/**' --test-coverage-exclude='**/node_modules/**' --test-coverage-exclude='**/scripts/**' --test-coverage-exclude='**/packages/**' --test-coverage-exclude='**/examples/**' dist/__tests__/*.test.js && node scripts/coverage-summary.mjs",
147
148
  "coverage:summary": "node scripts/coverage-summary.mjs",
148
149
  "lint": "biome lint .",