@arnilo/prism 0.1.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.0] - 2026-08-13
4
+
5
+ ### Changed
6
+ - **Release 0.2.0 (plan 020)** is the first cut of the 0.2.x review-remediation line — fail-closed runtime and sandbox security, closing the three blockers from the 2026-08-12 comprehensive review. API surface **additive-only** vs 0.1.7 (plain compat gate at 0.2.0: 0 breaking deltas; no `--allow-break`), three documented security-motivated behavior tightenings in `docs/migration.md` `0.1.7 → 0.2.0`: (1) **durable-resume input validation** (`resume-validation`, core) — `assertValidAgentRunResume` runs once at the top of `prepareAgentRunResume` before any state claim/checkpoint write/tool execution, covering all four public resume entrypoints; unknown legacy decisions (`"sideways"`), malformed batches, oversized reasons/elicitation, and duplicate approval ids fail closed with stable `ERR_PRISM_DECISION_*` codes, version untouched, zero tool calls; the server parser stays defense in depth. (2) **work-tool environment isolation** (`work-tools-env`, `@arnilo/prism-work-tools`) — `createCliRunner` children no longer inherit ambient host env: fixed base allow-list (PATH/LANG/LC_ALL/TZ + Windows system keys), explicit validated `env`, forced HOME/telemetry controls, late-bound per-identity tokens, 64-name/64-KiB caps (`ERR_PRISM_WORK_ENV`), absolute `binary`/`configDir` required, linear output capture (single final `Buffer.concat`). (3) **explicit sandbox capabilities** (`sandbox-capabilities`, `@arnilo/prism-coding-security`) — `SandboxAdapter.capabilities` (`workspaceCoherent`/`filesystemIsolated`/`networkIsolated`/`processIsolated`/`privilegeIsolated`/`egressRestricted`); omission/malformed metadata resolves all isolation `false`; `SandboxCodingComposition.capabilities` resolved from real wiring + validated adapter metadata; `containmentClaim` retained as `@deprecated` conservative projection (`workspaceCoherent && filesystemIsolated && networkIsolated && processIsolated`); Docker reports only verified controls, native reports filesystem/process/privilege `false`; authorization reads individual capabilities (docs/coding-security.md capability table + docs/host-security.md). New regression surface: `scripts/phase20-security.test.mjs` (public built entrypoints, all three blockers + gate accounting, wired into `security:threat-suites`), packed plain-JS consumer regressions in install-smoke, and the sandbox-browser workflow now records Docker/native capability evidence with a fail-loud 0.2.0 blocker gate (never a passing skip). Release graph stays **50** publishable manifests (root + 49 workspace packages) at exact **0.2.0**; zero new runtime dependency names (dependency fingerprint unchanged). Store compatibility with 0.1.7: **compatible, no migration** (no persisted-shape change). Exit gate green (core + script gates incl. phase20-freeze done-phase, `sdk:ready`, audit 0 moderate, pack dry-run 50/50 twice byte-identical, plain reviewed compat gate at 0.2.0, Docker daemon + native netns protected evidence, evidence in `scripts/phase20-baseline.json`). **Publication remains the operator handoff** (`docs/release-and-install.md` `0.2.0 publish handoff` — signed `v0.2.0` tag + npm OIDC).
7
+
3
8
  ## [0.1.7] - 2026-08-12
4
9
 
5
10
  ### Changed
@@ -1,8 +1,17 @@
1
1
  import type { StoredAgentRunState } from "./agent-run-state.js";
2
- import { Agent, AgentRunState, AgentRunStateOptions, DecisionScope, NestedRunOutcome, PendingDecision, RunDecision, StickyDecision, ToolResult } from "./contracts.js";
2
+ import { Agent, AgentRunResume, AgentRunState, AgentRunStateOptions, DecisionScope, NestedRunOutcome, PendingDecision, RunDecision, StickyDecision, ToolResult } from "./contracts.js";
3
3
  import type { AgentIdentity } from "./identity.js";
4
4
  /** Pending decisions of a suspended state, synthesizing the legacy single-approval shape. */
5
5
  export declare function pendingDecisionsOf(state: StoredAgentRunState): readonly PendingDecision[] | undefined;
6
+ /**
7
+ * Transport-neutral shape validation for the complete `AgentRunResume` input (plan 020 Task 2).
8
+ * Runs before any checkpoint read/write, agent resolution, subscription, or tool execution so
9
+ * untyped callers (plain JavaScript, `as any`) cannot make resume fall through to approval or
10
+ * crash with raw TypeErrors. State-dependent checks (foreign/stale/duplicate ids, scope,
11
+ * schema, policy) stay in {@link resolveRunDecisions}. Errors never include tool arguments,
12
+ * elicitation payloads, credentials, or foreign approval details.
13
+ */
14
+ export declare function assertValidAgentRunResume(resume: AgentRunResume): void;
6
15
  interface ResolvedRunDecisions {
7
16
  readonly decisionsById: ReadonlyMap<string, RunDecision>;
8
17
  readonly stickyDecisions: readonly StickyDecision[];
@@ -21,6 +21,87 @@ export function pendingDecisionsOf(state) {
21
21
  }
22
22
  return undefined;
23
23
  }
24
+ /**
25
+ * Transport-neutral shape validation for the complete `AgentRunResume` input (plan 020 Task 2).
26
+ * Runs before any checkpoint read/write, agent resolution, subscription, or tool execution so
27
+ * untyped callers (plain JavaScript, `as any`) cannot make resume fall through to approval or
28
+ * crash with raw TypeErrors. State-dependent checks (foreign/stale/duplicate ids, scope,
29
+ * schema, policy) stay in {@link resolveRunDecisions}. Errors never include tool arguments,
30
+ * elicitation payloads, credentials, or foreign approval details.
31
+ */
32
+ export function assertValidAgentRunResume(resume) {
33
+ const invalid = (message) => new AgentDecisionError("ERR_PRISM_DECISION_INVALID", message);
34
+ if (resume === null || typeof resume !== "object" || Array.isArray(resume)) {
35
+ throw invalid("Resume must be a non-null object");
36
+ }
37
+ if (!Number.isSafeInteger(resume.expectedVersion) || resume.expectedVersion <= 0) {
38
+ throw invalid("Resume expectedVersion must be a positive safe integer");
39
+ }
40
+ const hasDecision = resume.decision !== undefined;
41
+ const hasDecisions = resume.decisions !== undefined;
42
+ if (hasDecision && hasDecisions)
43
+ throw invalid("Resume accepts exactly one of decision or decisions");
44
+ if (!hasDecision && !hasDecisions)
45
+ throw invalid("Resume requires a decision or decisions");
46
+ if (hasDecision) {
47
+ if (resume.decision !== "approve" && resume.decision !== "deny") {
48
+ throw invalid("Unknown legacy decision");
49
+ }
50
+ return;
51
+ }
52
+ const decisions = resume.decisions;
53
+ if (!Array.isArray(decisions))
54
+ throw invalid("Decision batch must be an array");
55
+ if (decisions.length === 0)
56
+ throw invalid("Decision batch must not be empty");
57
+ if (decisions.length > HARD_MAX_PENDING_DECISIONS) {
58
+ throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Decision batch exceeds ${HARD_MAX_PENDING_DECISIONS} entries`);
59
+ }
60
+ const seen = new Set();
61
+ for (const decision of decisions) {
62
+ if (decision === null || typeof decision !== "object" || Array.isArray(decision)) {
63
+ throw invalid("Decision batch entries must be objects");
64
+ }
65
+ if (typeof decision.approvalId !== "string" || decision.approvalId.length === 0 || decision.approvalId.length > 128) {
66
+ throw invalid("Decision approvalId must be a bounded non-empty string");
67
+ }
68
+ if (seen.has(decision.approvalId)) {
69
+ throw new AgentDecisionError("ERR_PRISM_DECISION_DUPLICATE", "Duplicate approval decision in batch");
70
+ }
71
+ seen.add(decision.approvalId);
72
+ if (decision.outcome !== "allow_once" &&
73
+ decision.outcome !== "allow_for_run" &&
74
+ decision.outcome !== "reject_once" &&
75
+ decision.outcome !== "reject_for_run") {
76
+ throw invalid("Unknown approval outcome");
77
+ }
78
+ if (decision.reason !== undefined) {
79
+ if (typeof decision.reason !== "string")
80
+ throw invalid("Decision reason must be a string");
81
+ if (Buffer.byteLength(decision.reason, "utf8") > MAX_DECISION_REASON_BYTES) {
82
+ throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Decision reason exceeds ${MAX_DECISION_REASON_BYTES} bytes`);
83
+ }
84
+ }
85
+ for (const key of ["modifiedArguments", "elicitation"]) {
86
+ const payload = decision[key];
87
+ if (payload === undefined)
88
+ continue;
89
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
90
+ throw invalid(`Decision ${key} must be a JSON object`);
91
+ }
92
+ let text;
93
+ try {
94
+ text = JSON.stringify(payload);
95
+ }
96
+ catch {
97
+ throw invalid(`Decision ${key} must be a JSON object`);
98
+ }
99
+ if (text === undefined || Buffer.byteLength(text, "utf8") > MAX_ELICITATION_BYTES) {
100
+ throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Decision ${key} exceeds ${MAX_ELICITATION_BYTES} bytes`);
101
+ }
102
+ }
103
+ }
104
+ }
24
105
  /**
25
106
  * Validate one decision batch against the suspended state. Fail-closed and atomic: any
26
107
  * invalid entry rejects the whole batch before any CAS, leaving state and version untouched.
@@ -1,6 +1,6 @@
1
1
  import { agentFingerprint, loadAgentRunState, publicState, saveAgentRunState } from "./agent-run-state.js";
2
- import { AgentDecisionError, AgentRunStateError } from "./contracts.js";
3
- import { pendingDecisionsOf, resolveRunDecisions } from "./agent-approval.js";
2
+ import { AgentRunStateError } from "./contracts.js";
3
+ import { assertValidAgentRunResume, pendingDecisionsOf, resolveRunDecisions } from "./agent-approval.js";
4
4
  import { RuntimeAgentSession, throwIfAbortedSignal } from "./agent-session.js";
5
5
  function assertAgentId(actual, expected) {
6
6
  if (expected !== undefined && actual !== expected)
@@ -82,6 +82,10 @@ export async function* resumeAgentRunStream(agent, ref, resume, options) {
82
82
  }
83
83
  async function prepareAgentRunResume(agent, ref, resume, options, signal) {
84
84
  throwIfAbortedSignal(signal);
85
+ // Plan 020 Task 2: one shared shape assertion before any checkpoint read/write, agent
86
+ // resolution, subscription, or tool execution. Unknown legacy decisions (e.g. "sideways")
87
+ // and malformed untyped batches fail closed here instead of falling through to approval.
88
+ assertValidAgentRunResume(resume);
85
89
  const { record, state } = await loadAgentRunState(options.checkpoints, ref, options.ownership);
86
90
  if (state.definitionRevision !== options.definitionRevision ||
87
91
  state.agentId !== (agent.config.id ?? agent.config.name) ||
@@ -102,9 +106,6 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
102
106
  if (options.persistSessionState && options.includeSkillBodies && state.sessionState?.loadedSkillBodies) {
103
107
  session.restoreLoadedSkillBodies(state.sessionState.loadedSkillBodies);
104
108
  }
105
- if (resume.decision !== undefined && resume.decisions !== undefined) {
106
- throw new AgentDecisionError("ERR_PRISM_DECISION_INVALID", "Resume accepts exactly one of decision or decisions");
107
- }
108
109
  const pendingDecisions = pendingDecisionsOf(state);
109
110
  // Legacy approve maps to allow-once on every pending decision; legacy deny keeps its
110
111
  // terminal-denied behavior. Batch decisions are validated and applied atomically below.
@@ -118,9 +119,6 @@ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
118
119
  signal,
119
120
  })
120
121
  : undefined;
121
- if (resume.decision === undefined && resume.decisions === undefined) {
122
- throw new AgentDecisionError("ERR_PRISM_DECISION_INVALID", "Resume requires a decision or decisions");
123
- }
124
122
  if (resolved && resolved.remaining.length > 0) {
125
123
  throwIfAbortedSignal(signal);
126
124
  const single = resolved.remaining.length === 1 ? resolved.remaining[0] : undefined;
package/dist/index.d.ts CHANGED
@@ -107,5 +107,5 @@ export { createToolParameterValidator, createToolRegistry, dispatchToolCall, fil
107
107
  export type { ResolvedUseCaseModel, ResolveUseCaseModelInput, UseCaseModelBinding, } from "./use-case-model.js";
108
108
  export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
109
109
  export declare const name = "prism";
110
- export declare const version = "0.1.7";
110
+ export declare const version = "0.2.0";
111
111
  export declare const description = "Agent harness for AI providers, agents, sessions, and tools.";
package/dist/index.js CHANGED
@@ -58,6 +58,6 @@ export { DEFAULT_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES, DEFAULT_TOOL_RESULT_FOLD_MI
58
58
  export { createToolParameterValidator, createToolRegistry, dispatchToolCall, filterTools } from "./tools.js";
59
59
  export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
60
60
  export const name = "prism";
61
- export const version = "0.1.7";
61
+ export const version = "0.2.0";
62
62
  export const description = "Agent harness for AI providers, agents, sessions, and tools.";
63
63
  //# sourceMappingURL=index.js.map
@@ -187,6 +187,8 @@ Set `runState` with a host-owned `CheckpointStore`, stable `definitionRevision`,
187
187
 
188
188
  `*_for_run` outcomes append a `StickyDecision` to the durable run state: later calls in the same run matching the scope exactly (all recorded fields) proceed or are blocked without a new suspension, policy still enforced at dispatch. Sticky decisions expire when the run reaches any terminal status. Caps: 32 pending decisions per run (hard 128), 64 sticky decisions (hard 256), 2 KB decision reasons, 16 KB elicitation payloads.
189
189
 
190
+ **Runtime input validation (0.2.0, plan 020 Task 2).** Every public resume entrypoint (`resumeAgentRun`, `resumeAgentRunStream`, `AgentRunLifecycle.resume()`/`resumeStream()`) validates the complete resume input in core before any checkpoint read/write, agent resolution, subscription, or tool execution: a non-null object, positive safe-integer `expectedVersion`, exactly one of `decision`/`decisions`, legacy `decision` exactly `approve`/`deny`, and a non-empty batch ≤ 128 entries whose entries are objects with a bounded non-empty `approvalId`, a whitelisted outcome, an optional string `reason` within the 2 KB limit, and JSON-object `modifiedArguments`/`elicitation` within the 16 KB limit. Unknown legacy decisions (e.g. `"sideways"`) and malformed untyped batches fail closed with `AgentDecisionError` (`ERR_PRISM_DECISION_INVALID`/`..._LIMIT`/`..._DUPLICATE`) under a **no-side-effect guarantee**: zero checkpoint writes/CAS changes, zero tool calls, zero resumed events. This holds for plain-JavaScript and `as any` callers; the server's transport parser is defense in depth, not the security boundary. State-dependent checks (foreign/stale approval ids, scope, schema, policy) still run in the atomic batch resolver.
191
+
190
192
  ```ts
191
193
  const result = await session.run("Publish draft", {
192
194
  runState: { checkpoints, definitionRevision: "2026-07-20.1", interruptBeforeTool: true },
@@ -13,7 +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
+ | `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. Reports truthful capability metadata (`networkIsolated`/`egressRestricted` true, `filesystemIsolated`/`processIsolated`/`privilegeIsolated` false). Docker remains the stronger, documented reference backend. |
17
17
  | `SandboxProcessHandle` | Optional long-running process handle (`write`/`signal`/`kill`/`release`/`wait`) returned by `DisposableSandbox.startProcess?`. |
18
18
  | `createEgressPolicy(options)` | Deny-all allow-list policy: exact host/port/protocol rules plus frozen `npm-registry` / `github` presets; SHA-256 fingerprint. |
19
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. |
@@ -34,7 +34,7 @@ Use this package when coding tools need path scoping, human approval, command ru
34
34
 
35
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.
36
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.
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`. Its `capabilities` report `networkIsolated: true` and `egressRestricted: true` but `filesystemIsolated`/`processIsolated`/`privilegeIsolated: false` — the native backend is never a containment boundary for untrusted code (see [Sandbox capabilities](#sandbox-capabilities-020-plan-020-task-4)). 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
38
 
39
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.
40
40
 
@@ -104,7 +104,27 @@ const sandbox = await createDockerSandbox({ docker, image, sourceRoot, user, net
104
104
 
105
105
  `createCodingApprovalPolicy()` returns an `ExecutionPolicy`. Allowed checks return `ExecutionDecision { allowed: true }`; denied checks include a stable reason; shell decisions set `exclusive: true`. Sandbox adapters return coding-agent-compatible `BashOperations`, receive `onData(Buffer)` for ordered stdout/stderr forwarding through the shell tool's existing bounded accumulator, and never grant policy approval themselves.
106
106
 
107
- `createSandboxCodingComposition()` returns `{ tools, composition }` where `SandboxCodingComposition` carries `workspaceMode`, `containmentClaim`, `mixedWiringAllowed`, `warnings`, `workspaceRoot`, and optional `treeIdentity` (from `importIdentity` / `lastExportIdentity`). `containmentClaim` is `true` only for sandbox mode with tree backends bound and mixed wiring denied. Host mode and escape-hatch mixed wiring always set `containmentClaim: false` — never treat host mode as contained execution.
107
+ `createSandboxCodingComposition()` returns `{ tools, composition }` where `SandboxCodingComposition` carries `workspaceMode`, `capabilities`, `containmentClaim` (deprecated), `mixedWiringAllowed`, `warnings`, `workspaceRoot`, and optional `treeIdentity` (from `importIdentity` / `lastExportIdentity`). `capabilities` is a complete, frozen `SandboxCapabilities` object: `workspaceCoherent` derives from actual shell/filesystem/repository wiring; the isolation fields derive only from validated adapter capability metadata, never from `execFile`/`close` duck typing or custom operations being present. Host mode and escape-hatch mixed wiring always report no isolation capability — never treat host mode as contained execution.
108
+
109
+ ### Sandbox capabilities (0.2.0, plan 020 Task 4)
110
+
111
+ Every sandbox backend and every composition reports a frozen, complete capability object:
112
+
113
+ | Capability | Meaning | Docker | Native | Custom / omitted |
114
+ | --- | --- | --- | --- | --- |
115
+ | `workspaceCoherent` | Shell, filesystem, and repository tools observe one workspace tree. | true | true | true only when tree backends are bound and mixed wiring denied |
116
+ | `filesystemIsolated` | Sandbox processes cannot touch the host filesystem. | true | **false** (full host-tree access by design) | false unless host attests |
117
+ | `networkIsolated` | No reachable network. | true only for `network: { mode: "none" }` | true (fresh netns per command, egress denied, loopback down) | false unless host attests |
118
+ | `processIsolated` | Sandbox processes run in a separate process namespace. | true | **false** | false unless host attests |
119
+ | `privilegeIsolated` | Sandbox processes cannot obtain host privileges. | **false** (root-in-container without user namespaces is not reliable) | **false** | false unless host attests |
120
+ | `egressRestricted` | Any egress is forced through a controlled proxy/firewall. | true for mode `none`, or a custom network carrying a validated `EgressAttestation` | true (no egress at all) | false unless host attests |
121
+
122
+ Rules:
123
+
124
+ - **Omission is false.** A `SandboxAdapter` without `capabilities` metadata — or with malformed metadata (non-object, missing fields, non-boolean values, unknown keys) — resolves every isolation field false. `resolveSandboxCapabilities()` validates, copies, and freezes explicit metadata; a backend can never gain a capability by omission, interface shape, or mixed wiring.
125
+ - **Explicit metadata is host attestation.** Prism validates shape and freezes the object; the host is responsible for the underlying controls being real.
126
+ - **`containmentClaim` is deprecated (0.2.0).** Retained for 0.1.7 compatibility as the conservative projection `workspaceCoherent && filesystemIsolated && networkIsolated && processIsolated` (privilege isolation excluded). It can only be `true` when every required capability is true — never authorize a security-sensitive action from this boolean alone; use `composition.capabilities`.
127
+ - **Capability construction is O(1)** — one small frozen object per sandbox/composition; no command, filesystem, Docker, DNS, or network operation.
108
128
 
109
129
  `createDockerSandbox()` returns a `DisposableSandbox`: typed `execFile(file, args)`, shell-compatible `exec`, `status`, cooperative `stop`, forced `kill`, and idempotent `close`. Import may surface `importIdentity`; successful export updates `lastExportIdentity`. `close({ export })` can stream a bounded workspace tar plus SHA-256/entry/byte metadata through a host callback; checkpoints should retain only host artifact references/hashes, never whole workspaces. Optional `startProcess?(SandboxExecFileRequest)` returns a `SandboxProcessHandle` for long-running work consumed by coding-agent `createProcessSessions({ sandbox })`; absence means one-shot-only — ProcessSessions fails closed with `ERR_PRISM_PROCESS_UNSUPPORTED` (no native fallback). The Docker reference adapter does not implement `startProcess` yet; capability is detected, never assumed. See [Process sessions](process-sessions.md).
110
130
 
@@ -151,7 +171,8 @@ const { tools, composition } = createSandboxCodingComposition("/srv/jobs/task-1/
151
171
  executionPolicy: policy,
152
172
  repository: { exclude: [".git", "node_modules", "dist"] },
153
173
  });
154
- // composition.containmentClaim === true when backends are bound
174
+ // composition.capabilities.filesystemIsolated === true for the Docker adapter with network: none
175
+ // composition.containmentClaim is deprecated compatibility metadata only
155
176
 
156
177
  // Same-tree Git/check (opt-in; not folded into coding tools):
157
178
  const gitTools = createGitTools(composition.workspaceRoot, {
@@ -161,7 +182,7 @@ const gitTools = createGitTools(composition.workspaceRoot, {
161
182
 
162
183
  // Host mode (explicit non-contained): omit sandbox; never claim containment.
163
184
  const host = createSandboxCodingComposition(hostCwd, { workspaceMode: "host", executionPolicy: policy });
164
- // host.composition.containmentClaim === false
185
+ // host.composition.capabilities: workspaceCoherent only; every isolation field false; containmentClaim false
165
186
 
166
187
  await sandbox.execFile({ file: "npm", args: ["test"], cwd: "/workspace" });
167
188
  await sandbox.close({
@@ -173,7 +194,7 @@ await sandbox.close({
173
194
 
174
195
  Policies are ordinary host values: attach one globally through `createCodingTools()`/`createReadOnlyTools()`/`createSandboxCodingComposition()` or per tool. A per-tool policy overrides the shared policy. `SandboxAdapter` / `DisposableSandbox` are replaceable and host-owned; approval policy and sandboxing are separate layers. Custom remote sandboxes can implement `DisposableSandbox` without using Docker.
175
196
 
176
- `createSandboxCodingComposition()` requires `workspaceMode`. Sandbox mode auto-wires FS/list/search through `DisposableSandbox.execFile` (or host-supplied custom operations) so mutations stay on the disposable tree until export. Host mode runs every coding tool against the host cwd and never sets `containmentClaim`. Sandbox shell + host FS throws unless `allowMixedWorkspaceWiring: true` (warnings + `containmentClaim: false`). Opt-in structured Git tools (`createGitTools(composition.workspaceRoot, { execFile: sandbox.execFile, commitIdentity })`) share the same tree/cwd; Prism still never pushes or opens PRs. Optional `@arnilo/prism-browser` can share the same disposable boundary: use `assertBrowserSandboxNetwork()` before browse-ready custom networks, and `createSharedSandboxBrowserOptions({ workspaceRoot, downloadsRoot, containedProxyAttestation })` so uploads/downloads align with `/workspace` and `/downloads`. Close the browser context before disposing the sandbox.
197
+ `createSandboxCodingComposition()` requires `workspaceMode`. Sandbox mode auto-wires FS/list/search through `DisposableSandbox.execFile` (or host-supplied custom operations) so mutations stay on the disposable tree until export. Host mode runs every coding tool against the host cwd and reports no isolation capability (`containmentClaim` deprecated false). Sandbox shell + host FS throws unless `allowMixedWorkspaceWiring: true` (warnings + all capabilities false). Opt-in structured Git tools (`createGitTools(composition.workspaceRoot, { execFile: sandbox.execFile, commitIdentity })`) share the same tree/cwd; Prism still never pushes or opens PRs. Optional `@arnilo/prism-browser` can share the same disposable boundary: use `assertBrowserSandboxNetwork()` before browse-ready custom networks, and `createSharedSandboxBrowserOptions({ workspaceRoot, downloadsRoot, containedProxyAttestation })` so uploads/downloads align with `/workspace` and `/downloads`. Close the browser context before disposing the sandbox.
177
198
 
178
199
  The Docker reference adapter starts by recorded container ID/label, uses argument arrays only, mounts source read-only, populates a size-bounded tmpfs `/workspace`, drops all capabilities, enables `no-new-privileges`, runs with `--init`, and never exposes the Docker socket, privileged mode, or host PID/IPC namespaces. Image pull/build/update stays outside Prism. Protected real-Docker checks are opt-in via `PRISM_TEST_DOCKER_SANDBOX=1` with host-supplied `PRISM_TEST_DOCKER_BIN` and digest-pinned `PRISM_TEST_DOCKER_IMAGE`.
179
200
 
@@ -147,7 +147,7 @@ Wire those values where they matter: provider adapters receive the resolved cred
147
147
  - AG-UI MCP Apps requires negotiated `mcpApps`, exact proxy origin/auth, owned-run context, approval, one bridge, separate-origin sandbox (`allow-scripts allow-same-origin`), and no-wider CSP. Never execute HTML in host origin or retry a UI mutation; Task 4 adds recovery.
148
148
  - AG-UI A2A requires exact-origin verified client, host-owned task selection/correlation, explicit data/tool/A2UI projection, and reauthorized follow/cancel.
149
149
  - `@arnilo/prism-server` exposes no agent/workflow by default and requires `authorize()` for every matched operation. Derive complete tenant/account/user ownership from validated host identity, never request JSON. Workflow active identity and cancellation compare exact ownership; a tenant-only scope intentionally cannot cancel a checkpoint/run carrying account or user identity. The artifact review service (`createArtifactService`) requires authenticated identity + thread ownership on every attach/revise/compare/approve/reject/download, resolves concurrent reviewers via checkpoint CAS (no lost approvals), rejects local filesystem paths in `uri`/citations, redacts records before persist and on response, and serves downloads only through signed expiring links that are reauthorized against the token's ownership per request. When a blob store is wired (`bodies: ArtifactBodyStore`, 0.0.28), delivery links additionally resolve through `bodies.presign`; the reference `createS3ArtifactBodyStore` verifies ownership on every operation, verifies size/SHA-256/MIME on put and get (fail closed), refuses delete under legal hold (host `isHeld` callback), keeps credentials host-resolved, and never discloses bucket/path/key in errors, telemetry, or artifact records. Pass the current explicitly revised workflow definition so recursive hash mismatch fails before abort or durable mutation. Configure exact host/origin allow-lists where needed, wire redaction before execution, retain tool/workflow policy checks, and adapt the Web handler behind host TLS/rate limits. Disconnect abort is default; persistent reconnect/status belongs to durable workflow checkpoints, not an invented in-memory agent result cache.
150
- - Coding tools from `@arnilo/prism-coding-agent` accept an optional `ExecutionPolicy` checked inside each tool before side effects; shared policy propagation includes `createReadOnlyTools()`. They enforce finite text-scan/image/edit/write/shell limits, repository list/search depth/entry/match/scan/time caps, structured Git path/ref/message/output/patch/worktree caps, named-check concurrency/output caps, a 600-second default shell wall time, and a 64 MiB default total-output ceiling. Opt-in `createGitTools()` uses argument arrays with hooks/credential prompts/external diff disabled, requires host `commitIdentity` for commits, and never pushes or opens PRs. Successful truncated shell output leaves a host-owned exclusive `0600` temp file; delete `metadata.fullOutputPath` after use. Error/abort/timeout/overflow removes unpublished spills. Custom read/edit/shell/repository backends must honor supplied caps/signals. Use `@arnilo/prism-coding-security` for path roots, command rules, identity-scoped approval caching, required `workspaceMode` on `createSandboxCodingComposition()` / `createSandboxCodingTools()`, and the optional `createDockerSandbox()` reference adapter. **Host mode is never contained execution** (`containmentClaim: false`). Sandbox mode claims containment only when FS backends target the disposable tree; mixed wiring requires `allowMixedWorkspaceWiring` and still does not claim containment. Limits alone are not containment: construct the Docker adapter (absolute CLI, digest-pinned image, network none by default) or an equivalent host sandbox before treating coding execution as production-safe. Docker daemon/image trust, egress firewall/proxy, and artifact retention remain host-owned.
150
+ - Coding tools from `@arnilo/prism-coding-agent` accept an optional `ExecutionPolicy` checked inside each tool before side effects; shared policy propagation includes `createReadOnlyTools()`. They enforce finite text-scan/image/edit/write/shell limits, repository list/search depth/entry/match/scan/time caps, structured Git path/ref/message/output/patch/worktree caps, named-check concurrency/output caps, a 600-second default shell wall time, and a 64 MiB default total-output ceiling. Opt-in `createGitTools()` uses argument arrays with hooks/credential prompts/external diff disabled, requires host `commitIdentity` for commits, and never pushes or opens PRs. Successful truncated shell output leaves a host-owned exclusive `0600` temp file; delete `metadata.fullOutputPath` after use. Error/abort/timeout/overflow removes unpublished spills. Custom read/edit/shell/repository backends must honor supplied caps/signals. Use `@arnilo/prism-coding-security` for path roots, command rules, identity-scoped approval caching, required `workspaceMode` on `createSandboxCodingComposition()` / `createSandboxCodingTools()`, and the optional `createDockerSandbox()` reference adapter. **Host mode is never contained execution** (every isolation capability false). Sandbox mode reports isolation only from validated adapter capability metadata: `composition.capabilities` carries the frozen `SandboxCapabilities` object (`workspaceCoherent`, `filesystemIsolated`, `networkIsolated`, `processIsolated`, `privilegeIsolated`, `egressRestricted`); the deprecated `containmentClaim` is a conservative projection and must never be used alone. Authorize security-sensitive actions from the individual capabilities the policy actually needs — e.g. require `filesystemIsolated` before hosting untrusted coding tasks, and `egressRestricted` before any network-capable run. Mixed wiring requires `allowMixedWorkspaceWiring` and still reports no isolation. Limits alone are not containment: construct the Docker adapter (absolute CLI, digest-pinned image, network none by default) or an equivalent host sandbox before treating coding execution as production-safe. Docker daemon/image trust, egress firewall/proxy, and artifact retention remain host-owned.
151
151
  - Allow-list egress (0.0.26, `@arnilo/prism-coding-security`): `createEgressPolicy()` is deny-all with exact host/port/protocol rules and frozen `npm-registry`/`github` presets; `createAllowListEgressProxy()` is an HTTP forward proxy + CONNECT tunnel that pins DNS answers and verifies the connected address (rebinding defense), denies private/link-local/metadata ranges unless a rule opts in, re-validates every redirect hop against policy, and cuts oversized/slow transfers at frozen byte/time caps. TLS passes through without interception. Every allow/deny writes an audit record with no secrets. The proxy is inert until `start()`; `reloadPolicy()` is the only rule change path. `composeEgressSandboxNetwork(proxy.attestation(), name)` records validated attestation as `prism.egress.*` container labels — evidence, not enforcement: the host must restrict the Docker network so the proxy is the only reachable path, and `denyDirectEgress: true` is a claim the host makes true by topology. The proxy is not a firewall and cannot stop a container whose network reaches the internet directly.
152
152
  - Optional `@arnilo/prism-browser` requires a host-supplied Playwright Browser (`playwright-core@1.61.0` peer). Import is inert. One non-persistent context belongs to one run; actions serialize; refs are snapshot-scoped; CSS/evaluate/CDP/persistent profiles are denied. Context routing + `serviceWorkers: "block"` deny file/data/blob/devtools/private/loopback by default and require contained-proxy attestation for external egress (Playwright routing is defense in depth, not DNS containment). Uploads are realpath-rooted; downloads quarantine with hash/MIME until host `approveRelease`; screenshots return bounded `ImageContent`. Observation vs mutation/high-impact actions map to `ExecutionPolicy`. Treat snapshot/page text as untrusted external content. Close contexts with `browser_close` or `manager.closeRun(runId)` on abort/terminal. Browser control endpoint, binary/image pin, and real egress firewall/proxy remain host-owned. Shared sandbox: `createSharedSandboxBrowserOptions()` + `assertBrowserSandboxNetwork()`.
153
153
  - Browser verified-state checkpoints (0.0.14, `createBrowserCheckpointLedger()`) store URL + domain-state hash + host data refs only — never serialized browser internals (cookies/storage/contexts). After any resume/interruption the ledger fails closed (`assertVerifiedBeforeSideEffect`) until the host reloads + verifies, so side effects never replay on stale state.
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; host-configurable selection policies (reference cost/latency policy ranks by `ModelCost` then in-memory latency EMA fed from `recordOutcome`).
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 (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).
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; 0.2.0 plan 020 Task 2: durable resume input is validated in core before any side effect — fail-closed durable resume rejects unknown legacy decisions and malformed batches with zero checkpoint writes, tool calls, or resumed events).
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.
@@ -70,7 +70,7 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
70
70
  - [Tool validator JSON Schema package](../packages/tool-validator-json-schema/README.md): optional `@arnilo/prism-tool-validator-json-schema` adapter for `tool.parameters`.
71
71
  - [MCP client bridge and server exposure](mcp-tools.md): SDK-1.30.0 bounded tools/resources/prompts, host-owned roots/sampling/elicitation, exact-origin DNS-pinned client transport, and principal-bound opt-in Streamable HTTP sessions. 0.0.28 adds MCP OAuth: `createMcpOAuthTransport`/`createMcpOAuthFetch`/`createMcpClientAuth` (RFC 9728/8414 discovery, PKCE, RFC 8707 audience binding, RFC 7009 revocation, host-owned bounded state) and server `protectedResource` metadata + `WWW-Authenticate` challenges.
72
72
  - [Web search, fetch, and extraction](web-tools.md): optional host-selected Brave/Exa discovery and Firecrawl Markdown/schema tools with native fetch, stable citations, late credentials, finite limits, and explicit untrusted-content boundaries.
73
- - [Work tools](work-tools.md): optional `@arnilo/prism-work-tools` identity-scoped M365 + GWS connectors (hard-coded CLI argv, draft-then-approve, state-machine idempotency, shared result shapes); 0.0.14 adds a late-bound per-identity `tokenProvider` (env-only, fail-closed).
73
+ - [Work tools](work-tools.md): optional `@arnilo/prism-work-tools` identity-scoped M365 + GWS connectors (hard-coded CLI argv, draft-then-approve, state-machine idempotency, shared result shapes); 0.0.14 adds a late-bound per-identity `tokenProvider` (env-only, fail-closed); 0.2.0 plan 020 Task 3 provides an isolated subprocess environment (fixed allow-listed base + explicit env + late-bound token env, forced `HOME`/telemetry controls, 64-name/64-KiB caps) and requires host-pinned **absolute** binary/configDir paths.
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.
@@ -78,7 +78,7 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
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 (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).
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()` sandbox capability metadata — 0.2.0 plan 020 Task 4 ships explicit `SandboxCapabilities` (`workspaceCoherent`/`filesystemIsolated`/`networkIsolated`/`processIsolated`/`privilegeIsolated`/`egressRestricted`) with omission resolving false, truthful Docker/native metadata, and `containmentClaim` retained only as a deprecated conservative projection — 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.
@@ -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.7** 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 019 the performance-and-DX patch — dependency-free `createCacheTelemetry()` per-provider/model cache hit/miss aggregator (bounded cardinality with `__overflow__`, token counters/rates only, host-activated), host-configurable `ModelRouterSelectionPolicy` on `createModelRouter` with the reference `createCostLatencySelection` (ModelCost rank then in-memory latency EMA, default ordered behavior byte-identical), `prism providers add <name>` OpenAI-compatible provider scaffold (manifest/provider/models/cache/conformance test/docs stub, npm-name + traversal + symlink-escape validation, placeholders only), and the async `AgUiProjection` verification closeout (plan 009 Task 15 evidence recorded, no new code); 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.2.0** 50-package graph (root + 49 workspace packages) — plan 020 the fail-closed runtime-and-sandbox-security cut on the 0.2.x review-remediation line: durable-resume decision validation in core (`assertValidAgentRunResume` — unknown decisions/malformed batches fail closed with `ERR_PRISM_DECISION_*` before any state claim, checkpoint write, or tool execution; server parser remains defense in depth), isolated work-tool subprocess environments (`@arnilo/prism-work-tools` — fixed base allow-list + explicit env + forced HOME/telemetry + late-bound per-identity tokens, 64-name/64-KiB caps, absolute binary/configDir, linear output capture), and explicit sandbox capabilities (`@arnilo/prism-coding-security` — `SandboxAdapter.capabilities` with omission-is-false fail-closed resolution, `SandboxCodingComposition.capabilities` from verified wiring, `containmentClaim` deprecated as the conservative projection; Docker reports only verified controls, native reports filesystem/process/privilege `false`); public-entrypoint security conformance (`scripts/phase20-security.test.mjs`, wired into `security:threat-suites`), packed plain-JS consumer regressions, and the sandbox-browser workflow's fail-loud Docker/native capability evidence gate — 0.2.0 never ships while a blocker is skipped; migration and rollback notes in `docs/migration.md` `0.1.7 → 0.2.0`, store-compatible with 0.1.7 in both directions; 0.1.7 was the performance-and-DX patch — dependency-free `createCacheTelemetry()` per-provider/model cache hit/miss aggregator (bounded cardinality with `__overflow__`, token counters/rates only, host-activated), host-configurable `ModelRouterSelectionPolicy` on `createModelRouter` with the reference `createCostLatencySelection` (ModelCost rank then in-memory latency EMA, default ordered behavior byte-identical), `prism providers add <name>` OpenAI-compatible provider scaffold (manifest/provider/models/cache/conformance test/docs stub, npm-name + traversal + symlink-escape validation, placeholders only), and the async `AgUiProjection` verification closeout (plan 009 Task 15 evidence recorded, no new code); 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
 
package/docs/migration.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Migration guide
2
2
 
3
+ ## 0.1.7 → 0.2.0 fail-closed runtime and sandbox security (plan 020)
4
+
5
+ Release **0.2.0** (plan 020) is the first cut of the 0.2.x review-remediation line: it closes the three security blockers found in the 2026-08-12 comprehensive review. The API surface is **additive-only** (plain compat gate at 0.2.0 shows zero removed/changed declarations; no `--allow-break`), but three behaviors are deliberately tightened for security, so untyped/legacy callers may now fail where 0.1.7 silently proceeded:
6
+
7
+ 1. **Durable-resume decision validation (core).** `resumeAgentRun`/`resumeAgentRunStream` (and the lifecycle/resume-stream entrypoints behind them) now validate the resume payload **before any state claim, checkpoint write, or tool execution**. Unknown legacy decisions (anything other than `approve`/`deny`), malformed decision batches, oversized reasons/elicitation, and duplicate approval ids fail closed with a stable `AgentDecisionError` (`ERR_PRISM_DECISION_INVALID`/`ERR_PRISM_DECISION_LIMIT`/`ERR_PRISM_DECISION_DUPLICATE`), leave the checkpoint version untouched, and execute no tool. In 0.1.7 an unknown decision string (e.g. `"sideways"`) was accepted, the checkpoint was CAS-claimed to `running`, and the suspended tool executed. The HTTP server parser (`readAgentDecisions`) is unchanged — it remains defense in depth, not the security boundary.
8
+
9
+ ```js
10
+ // 0.2.0: fails closed, no side effect, version untouched
11
+ try {
12
+ await resumeAgentRun(agent, ref, { expectedVersion: v, decision: "sideways" }, opts);
13
+ } catch (error) {
14
+ error.code; // "ERR_PRISM_DECISION_INVALID"
15
+ }
16
+ ```
17
+
18
+ 2. **Work-tool subprocess environments (`@arnilo/prism-work-tools`).** `createCliRunner` no longer inherits the full host `process.env`. The child environment is now: fixed base allow-list (`PATH`, `LANG`, `LC_ALL`, `TZ`; Windows adds `SYSTEMROOT`/`SystemRoot`/`TEMP`/`TMP`/`PATHEXT`/`COMSPEC`), then explicit validated `options.env`, then forced controls (`HOME` = `configDir`, `CLIMICROSOFT365_DISABLETELEMETRY=1`), then the late-bound per-identity token layer (`M365_ACCESSTOKEN`/`GOOGLE_ACCESS_TOKEN` style). Caps: 64 names / 64 KiB total (`ERR_PRISM_WORK_ENV`). `binary` and `configDir` must now be **absolute paths** (`path.isAbsolute`), and output capture is linear (single final `Buffer.concat`, capped at `maxStdoutBytes`/`maxStderrBytes`). In 0.1.7 the child inherited every ambient host variable.
19
+
20
+ 3. **Explicit sandbox capabilities (`@arnilo/prism-coding-security`).** `SandboxAdapter` gains the optional `capabilities` field — `workspaceCoherent`/`filesystemIsolated`/`networkIsolated`/`processIsolated`/`privilegeIsolated`/`egressRestricted` (immutable booleans). Omission or malformed metadata resolves every isolation field `false` (fail-closed). `SandboxCodingComposition` now carries a resolved `capabilities` object; the old boolean `containmentClaim` is **deprecated** and is the conservative projection `workspaceCoherent && filesystemIsolated && networkIsolated && processIsolated`. Built-ins: Docker reports `filesystemIsolated: true`/`processIsolated: true`/`networkIsolated: true` only for `--network=none`/attested networks, `privilegeIsolated: false` by default; native sandbox reports `networkIsolated: true`/`egressRestricted: true` but **never** filesystem/process/privilege isolation. In 0.1.7 any `DisposableSandbox`-shaped adapter could make `containmentClaim` report `true` with no isolation-capability inspection; in 0.2.0 an un-attested adapter claims `workspaceCoherent` at most. Authorization should read the individual capabilities, never the deprecated boolean.
21
+
22
+ **Store compatibility:** 0.2.0 is store-compatible with 0.1.7 in both directions — no persisted-shape change, no migration step. Checkpoint, session-store, approval, and registry payloads are byte-identical; only the resume *input* validation is new.
23
+
24
+ **Rollout:** upgrade core first (resume validation applies immediately to all hosts), then `@arnilo/prism-work-tools` (pass absolute `binary`/`configDir` and any ambient keys your connector needs via `options.env` — the allow-list is deny-by-default by design), then `@arnilo/prism-coding-security` (capability-aware policy code; the deprecated `containmentClaim` keeps working with the stricter semantics).
25
+
26
+ **Rollback risk:** restoring 0.1.7 restores all three defects — rollback is **not** a mitigation. Hosts that must roll back should disable resume side effects and work-tool execution at their own boundary until they can return to 0.2.0.
27
+
3
28
  ## 0.1.4 → 0.1.5 deprecated-option removal (documented breaking cut)
4
29
 
5
30
  Release **0.1.5** (plan 017) removes the deprecated compatibility surface that 0.1.x kept after 0.0.19: the inert provider timeout/retry knobs, the `maxToolRounds` run-option alias, the pre-0.0.19 observational-memory flat keys and worker aliases, the read-tool `autoResizeImages` flag, and the `INIT_PROVIDERS` constant. This is the **documented breaking cut** announced in the 0.1.4 migration section; every other 0.1.x release keeps the compat baseline green. Three roadmap labels from the original 0.1.5 task were corrected during planning and are honored here:
@@ -133,7 +133,7 @@ Important request shapes:
133
133
  | `AgentSessionConfig` | Session creation input: optional id, agent, store, leaf id, and metadata. |
134
134
  | `RunOptions` | Per-run overrides: optional abort signal, model, input layout, run limits (incl. `limits.maxToolRounds`), provider options/request policies, system prompt layers, compaction, retry, metadata, skill selection, validate, redactor, and loop. |
135
135
  | `SubscribeOptions` / `SubscriberOverflowPolicy` | Live `AgentEvent` subscriber queue limit and overflow policy: `maxQueuedEvents`, `overflow: "close" \| "drop_oldest" \| "drop_newest"`. |
136
- | `resumeAgentRunStream` / `AgentRunResumeStreamOptions` | One durable-run event stream: existing checkpoint/resume options plus `signal` and bounded subscriber options. `AgentRunLifecycle.resumeStream()` adds host capability resolution; no protocol types enter core. |
136
+ | `resumeAgentRunStream` / `AgentRunResumeStreamOptions` | One durable-run event stream: existing checkpoint/resume options plus `signal` and bounded subscriber options. `AgentRunLifecycle.resumeStream()` adds host capability resolution; no protocol types enter core. Runtime resume validation (0.2.0): every resume entrypoint validates the full input in core before any checkpoint write, tool call, or event — unknown legacy decisions and malformed batches fail closed with `AgentDecisionError` and no side effect; see [Agent/session runtime § Durable interruption](agent-session-runtime.md#durable-interruption). |
137
137
  | `AgentConfig.loop` / `RunOptions.loop` | Replaceable per-run control loop: `singleShotLoop` default, `generate-validate-revise` options, or a custom `AgentLoopStrategy`. `RunOptions.loop` wins. See [Agent loops](agent-loops.md). |
138
138
  | `AgentLoopStrategy` | `{ name; run(ctx: LoopContext): Promise<Usage \| undefined> }` — orchestrates shared runtime primitives via `LoopContext`. |
139
139
  | `LoopContext` | Loop-facing surface: run ids, signal, live `history`, `input`/`inputMessages`/`maxToolRounds`, and bound `assemble`/`generate`/`dispatchToolCall`/`appendMessage`/`emit` primitives. |
@@ -274,6 +274,25 @@ 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.2.0 publish handoff (plan 020 Task 6)
278
+
279
+ **Decision: GO when the operator prerequisites below are recorded.** Release **0.2.0** (plan 020) is the first cut of the 0.2.x review-remediation line — fail-closed runtime and sandbox security. API surface **additive-only** vs 0.1.7 (plain compat gate at 0.2.0: 0 breaking declaration deltas — the three blockers are behavior tightenings, not removals; `containmentClaim` retained deprecated; baseline text regenerated with `--update-baseline`, no `--allow-break` anywhere; freeze manifest `scripts/phase20-freeze-manifest.json` machine-checks each task's diff stayed inside its allowed files). Shipped: (1) **durable-resume input validation** — `assertValidAgentRunResume` at the top of `prepareAgentRunResume` covers all four public resume entrypoints; unknown legacy decisions (`"sideways"`), malformed batches, oversized reasons/elicitation, duplicate approval ids fail closed `ERR_PRISM_DECISION_*` with zero checkpoint writes/tool calls (server parser stays defense in depth); (2) **work-tool environment isolation** — `createCliRunner` children get a fixed base allow-list + explicit env + forced HOME/telemetry controls + late-bound per-identity tokens, 64-name/64-KiB caps `ERR_PRISM_WORK_ENV`, absolute binary/configDir, linear output capture; (3) **explicit sandbox capabilities** — `SandboxAdapter.capabilities` (six immutable booleans, omission/malformed ⇒ all false), composition capabilities from verified wiring, `containmentClaim` deprecated as the conservative projection; Docker reports only verified controls, native reports filesystem/process/privilege false; docs/coding-security.md capability table, docs/host-security.md authorization guidance. New regression surface: `scripts/phase20-security.test.mjs` (public built entrypoints, wired into `security:threat-suites`), packed plain-JS consumer regressions in install-smoke, and the sandbox-browser workflow's fail-loud 0.2.0 blocker gate recording Docker/native capability evidence — **0.2.0 does not ship while any blocker is skipped**. Store compatibility with 0.1.7: **compatible, no migration** (no persisted-shape change; `docs/migration.md` `0.1.7 → 0.2.0` section). Exit gate green: npm test core + script gates (incl. phase20-freeze done-phase), `sdk:ready` exit 0, audit 0 moderate, pack dry-run 50/50 twice byte-identical, plain reviewed compat gate at 0.2.0, Docker daemon + native netns protected evidence; evidence in `scripts/phase20-baseline.json` `exitGate`. Rollback = restore the 0.1.7 manifests/tag — but rollback restores the three defects, so hosts should disable resume side effects and work-tool execution at their own boundary if rollback is unavoidable.
280
+
281
+ ```bash
282
+ # Operator prerequisites recorded: clean tree at the v0.2.0 tag candidate, GPG key, npm OIDC publisher.
283
+ npm test # core + workspace suites + all script gates
284
+ npm run security:threat-suites # phase8-11 + phase20 public-entry conformance
285
+ npm run sdk:ready # typecheck, lint, format, test, coverage, pack, release:gate
286
+ node scripts/release.mjs gate --version 0.2.0 # plain reviewed additive gate, 0 breaking deltas
287
+ npm run pack:dry-run # twice; diff reports — deterministic
288
+ npm audit --audit-level=moderate
289
+ npm run release:check -- --version 0.2.0 --report /tmp/prism-0.2.0-preflight.json
290
+ npm run release:publish -- --version 0.2.0 --dry-run --allow-dirty --allow-untagged --report /tmp/prism-0.2.0-dry-run.json
291
+ # run the dry-run twice and diff the reports: deterministic, byte-identical
292
+ ```
293
+
294
+ Protected evidence (never a passing skip): `docker info` + digest-pinned image (e.g. `PRISM_TEST_DOCKER_SANDBOX=1 PRISM_TEST_DOCKER_BIN=/usr/bin/docker PRISM_TEST_DOCKER_IMAGE=ubuntu@sha256:... npm test -w @arnilo/prism-coding-security -- --test-name-pattern "protected Docker"`) and native netns capability (`unshare --net` / `--net --map-root-user` must succeed; T9 native capability test runs, not skips). The sandbox-browser workflow fails loudly when this evidence is missing.
295
+
277
296
  ### 0.1.7 publish handoff (plan 019 Task 6)
278
297
 
279
298
  **Decision: GO when the operator prerequisites below are recorded.** Release **0.1.7** (plan 019) is the performance-and-DX patch on the frozen 0.1.x line — **additive-only** vs 0.1.6 (plain compat gate at 0.1.7 passed with 0 breaking declaration deltas; the baseline text was regenerated with `--update-baseline` for the version literal only, no `--allow-break` anywhere; freeze manifest `scripts/phase19-freeze-manifest.json` machine-checks each task's diff stayed inside its allowed files). Shipped: (1) **prompt-cache telemetry surface** — dependency-free `createCacheTelemetry()` aggregator in core, host-activated, per-provider/model request counts + aggregate hit rate + cache-read/write token totals + estimated savings, bounded cardinality (cap 256 distinct keys, `__overflow__` bucket), token counters/rates only (never prompt content, cache keys, or identity), O(1) `record()`; (2) **model-router selection policies** — additive `ModelRouterSelectionPolicy` on `createModelRouter` (default ordered behavior byte-identical) with the reference `createCostLatencySelection` ranking by `ModelCost` then in-memory latency EMA fed from `recordOutcome({ latencyMs })`, permutation-only reorder of already-allowed candidates, misbehavior fails closed `ERR_PRISM_MODEL_ROUTER_POLICY`; (3) **async AgUiProjection closeout** — plan 009 Task 15 surface verified with evidence (`asyncHooks: {verified: true, gapFound: false}` in `scripts/phase19-baseline.json`), no new code; (4) **`prism providers add <name>` scaffold** — new CLI subcommand generating an OpenAI-compatible provider package (manifest, provider via `createOpenAICompatibleProvider`, starter models, cache helpers, offline conformance test, docs stub) with npm-name/traversal/symlink-escape validation and placeholders only — never secrets; scaffold output is host-chosen and never auto-registered. Store compatibility with 0.1.6: **compatible, no migration** (additive-only; no persisted-shape change; `docs/migration.md` gains no entries). Exit gate green: npm test core + script gates (incl. phase19-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/phase19-baseline.json` `exitGate`. Rollback = restore the 0.1.6 manifests/tag.
@@ -106,6 +106,28 @@ Mutation tools (`*_mail_draft_send`, `*_draft_*`) create an in-adapter draft and
106
106
 
107
107
  Call `begin({ identity, key, op })` **before** the external effect. After it succeeds, call `complete`, `fail`, or `markUnknown` with the returned claim token and version. The connector effect stays outside the database transaction, so this is claim-before-effect/deduplication—not exactly-once delivery. Claims default to 15 minutes (hard 60 minutes); expired claims transition to `unknown`; attempts default to 3 (hard 5). Stored rows contain no request body, token, raw provider response, or unrestricted payload.
108
108
 
109
+ ## Subprocess environment isolation (0.2.0, plan 020 Task 3)
110
+
111
+ `createCliRunner` never inherits the host environment. The child process receives only:
112
+
113
+ 1. **Fixed platform base** — allow-listed locale/system keys copied from the host: `PATH`, `LANG`, `LC_ALL`, `TZ`, `SYSTEMROOT`/`SystemRoot`, `TEMP`, `TMP`, `PATHEXT`, `COMSPEC`. Nothing else from `process.env` crosses the boundary, so unrelated ambient variables (e.g. `PRISM_PROOF_SECRET`) cannot reach the CLI. Additions to the list are deliberate one-line allow-list changes.
114
+ 2. **Explicit host env** — non-secret values passed via the `env` option (e.g. `{ LANG: "C.UTF-8" }`).
115
+ 3. **Late-bound per-identity token env** — the `tokenProvider` result, merged per call; never argv, never model context.
116
+ 4. **Forced reserved controls** — `HOME` is always the isolated `configDir` and `CLIMICROSOFT365_DISABLETELEMETRY` is always `"1"`; neither the explicit map nor the token layer can override them (any attempt fails closed with `ERR_PRISM_WORK_ENV` before spawn).
117
+
118
+ Environment maps are validated before spawn: NUL-free, `[A-Za-z_][A-Za-z0-9_]*` names, string values, no case-insensitive duplicate or reserved keys (Windows canonicalizes PATH/system key casing so Node's first-lexicographic-key behavior cannot select an attacker-controlled duplicate), and fixed caps of 64 names / 64 KiB total (`ERR_PRISM_WORK_LIMIT`).
119
+
120
+ ### Host-pinned absolute paths
121
+
122
+ `binary` and `configDir` must be **absolute** paths (`path.isAbsolute`); empty, relative, or NUL-containing values are rejected at construction with `ERR_PRISM_WORK_BINARY` / `ERR_PRISM_WORK_CONFIG` before any spawn.
123
+
124
+ ### Migration (0.1.7 → 0.2.0)
125
+
126
+ - Any host env your CLI needed beyond the fixed base must move into the explicit `env` map (non-secret) or the per-identity token layer (secrets).
127
+ - Relative `binary`/`configDir` values now fail at construction; resolve them to absolute paths.
128
+ - Per-call `runOpts.env` keys colliding case-insensitively with `HOME` or the telemetry-disable control now fail closed instead of being silently overridden.
129
+ - Output capture is linear: chunks are accumulated in an array with one final `Buffer.concat`, and the process is killed/rejected before bytes beyond the stdout/stderr caps are retained.
130
+
109
131
  ## Limits
110
132
 
111
133
  | Resource | Default / hard |
@@ -126,7 +148,8 @@ Approved mutations require core-derived `context.idempotencyKey` and a configure
126
148
  - Connector tokens (0.0.14): an optional `tokenProvider` resolves a per-identity access token into an env var per call — never argv, never model context. A missing/expired/revoked/cross-identity/wrong-tenant token fails the call closed before any exec. Refresh is late-bound and single-flighted per account (no refresh storm under reconnect). Build one with `createOAuthWorkTokenProvider()` from `@arnilo/prism-credentials-node`.
127
149
  - External mail recipients fail closed unless `externalRecipients.allow` returns true.
128
150
  - Anonymous / `anyone` sharing denied.
129
- - CLI stdout/stderr capped; NDJSON page streams strictly parsed and page-capped; process killed on timeout/abort/overflow.
151
+ - CLI stdout/stderr capped (linear chunk capture, killed/rejected before bytes beyond the cap are retained); NDJSON page streams strictly parsed and page-capped; process killed on timeout/abort/overflow.
152
+ - Subprocess environment isolated (0.2.0): fixed allow-listed base + explicit `env` + late-bound token env; `HOME`/telemetry controls forced; reserved/duplicate/NUL/over-cap env and non-absolute binary/configDir fail before spawn. See [Subprocess environment isolation](#subprocess-environment-isolation-020-plan-020-task-3).
130
153
 
131
154
  ## Related
132
155
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnilo/prism",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "Agent harness for AI providers, agents, sessions, and tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -143,7 +143,7 @@
143
143
  "build": "npm run build:core && npm run build --workspaces --if-present",
144
144
  "typecheck": "npm run build && npm run typecheck --workspaces --if-present && tsc -p examples --noEmit",
145
145
  "sweep:unused": "node scripts/sweep-unused.mjs",
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/phase19-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/phase19-freeze.test.mjs scripts/phase20-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",
147
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",
148
148
  "coverage:summary": "node scripts/coverage-summary.mjs",
149
149
  "lint": "biome lint .",
@@ -156,7 +156,7 @@
156
156
  "release:publish": "node scripts/release.mjs publish",
157
157
  "sdk:ready": "npm run typecheck && npm run lint && npm run format:check && npm test && npm run test:coverage && npm run pack:dry-run && npm run release:gate",
158
158
  "release:gate": "node scripts/release.mjs gate",
159
- "security:threat-suites": "node --test scripts/phase8-conformance.test.mjs scripts/phase9-conformance.test.mjs scripts/phase10-conformance.test.mjs scripts/phase11-conformance.test.mjs"
159
+ "security:threat-suites": "node --test scripts/phase8-conformance.test.mjs scripts/phase9-conformance.test.mjs scripts/phase10-conformance.test.mjs scripts/phase11-conformance.test.mjs scripts/phase20-security.test.mjs"
160
160
  },
161
161
  "devDependencies": {
162
162
  "@biomejs/biome": "^2.5.5",