@arnilo/prism 0.7.0 → 0.8.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 +35 -0
- package/README.md +12 -11
- package/dist/agent-approval.d.ts +4 -0
- package/dist/agent-approval.js +5 -1
- package/dist/agent-run-lifecycle.js +29 -5
- package/dist/agent-run-state.d.ts +13 -1
- package/dist/agent-run-state.js +12 -2
- package/dist/agent-session/helpers.js +6 -1
- package/dist/agent-session/session/assemble.js +126 -5
- package/dist/agent-session/session/persist.d.ts +16 -0
- package/dist/agent-session/session/persist.js +58 -2
- package/dist/agent-session/session/provider-round.d.ts +3 -3
- package/dist/agent-session/session/provider-round.js +12 -6
- package/dist/agent-session/session/tool-round.js +4 -1
- package/dist/agent-session/session/types.d.ts +12 -1
- package/dist/agent-session/session.d.ts +2 -0
- package/dist/agent-session/session.js +2 -0
- package/dist/checkpoints.js +7 -11
- package/dist/contracts-core/content.d.ts +5 -0
- package/dist/contracts-core/loop.d.ts +42 -0
- package/dist/contracts-core/run-limits.d.ts +2 -0
- package/dist/contracts-protocol.d.ts +15 -3
- package/dist/contracts-run-state.d.ts +26 -5
- package/dist/evidence-grounding.d.ts +29 -0
- package/dist/evidence-grounding.js +162 -0
- package/dist/host-composition.d.ts +13 -0
- package/dist/host-composition.js +33 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -1
- package/dist/provider-events.d.ts +3 -1
- package/dist/provider-events.js +2 -2
- package/dist/providers/transport.d.ts +3 -1
- package/dist/providers/transport.js +36 -0
- package/dist/redaction.js +18 -2
- package/dist/run-bundle.d.ts +89 -0
- package/dist/run-bundle.js +149 -0
- package/dist/testing/state-concurrency-conformance.js +5 -12
- package/docs/ag-ui.md +5 -0
- package/docs/agent-loops.md +33 -0
- package/docs/agent-session-runtime.md +4 -3
- package/docs/coding-security.md +1 -0
- package/docs/coding-tools.md +0 -1
- package/docs/compaction-observational-memory.md +1 -1
- package/docs/connected-apps.md +116 -0
- package/docs/context-and-skills.md +13 -0
- package/docs/core.md +1 -1
- package/docs/diagrams.md +6 -6
- package/docs/document-reader.md +9 -9
- package/docs/documents.md +32 -11
- package/docs/durable-runs.md +87 -0
- package/docs/enterprise-postgres-state.md +4 -0
- package/docs/execution-timeline.md +6 -0
- package/docs/guardrails.md +33 -0
- package/docs/history/079-messaging-primitive-review.md +391 -0
- package/docs/history/080-messaging-followon-primitive-review.md +234 -0
- package/docs/history/081-connected-apps-primitive-review.md +74 -0
- package/docs/history/083-prism-work-primitive-review.md +84 -0
- package/docs/history/084-primitive-review.md +96 -0
- package/docs/history/085-honesty-and-cut-primitive-review.md +91 -0
- package/docs/history/README.md +5 -0
- package/docs/history/release-handoffs.md +38 -0
- package/docs/host-compositions.md +8 -6
- package/docs/host-security.md +2 -2
- package/docs/index.md +47 -29
- package/docs/live-testing.md +5 -3
- package/docs/mcp-tools.md +1 -0
- package/docs/messaging-channel-operations.md +166 -0
- package/docs/messaging-channels.md +150 -0
- package/docs/migrate-to-0.8.md +124 -0
- package/docs/migration.md +30 -0
- package/docs/model-registry.md +12 -2
- package/docs/openapi-tools.md +1 -1
- package/docs/operations.md +1 -3
- package/docs/options-index.md +34 -2
- package/docs/peer-dependencies.md +6 -6
- package/docs/postgres-persistence.md +1 -1
- package/docs/provider-layer.md +2 -2
- package/docs/provider-packages.md +20 -20
- package/docs/providers/neuralwatt.md +5 -1
- package/docs/rag.md +1 -1
- package/docs/release-and-install.md +52 -46
- package/docs/run-bundle.md +92 -0
- package/docs/runs-and-usage.md +14 -0
- package/docs/server.md +2 -0
- package/docs/sheets.md +9 -9
- package/docs/signal-channel.md +112 -0
- package/docs/speech.md +5 -1
- package/docs/sqlite-persistence.md +1 -1
- package/docs/telegram-channel.md +157 -0
- package/docs/testing.md +2 -2
- package/docs/wiki.md +1 -1
- package/docs/work-artifacts-and-review.md +1 -1
- package/docs/work-connectors.md +9 -9
- package/docs/work-sandbox.md +115 -0
- package/docs/work-tools.md +38 -16
- package/package.json +5 -3
- package/templates/business-worker/manifest.json +2 -1
- package/templates/business-worker/src/agent.ts.tmpl +1 -1
- package/templates/business-worker/src/tests/agent.test.ts.tmpl +1 -1
|
@@ -61,7 +61,7 @@ string | Message | readonly Message[]
|
|
|
61
61
|
|
|
62
62
|
`session.stream(input, options?)` subscribes first, starts exactly one run, yields only that run's events, and terminates when the run succeeds, fails, or aborts. Early consumer return aborts the owned run and releases the session. `SubscribeOptions.maxQueuedEvents` / `overflow` may be passed alongside `RunOptions`.
|
|
63
63
|
|
|
64
|
-
`resumeAgentRunStream(agent, ref, resume, options?)` does the same for one existing suspended durable run. It validates checkpoint ownership, revision/fingerprint, and `expectedVersion`, then subscribes before emitting `agent_started` / `agent_resumed` and resumed message/tool/terminal events. `AgentRunResumeStreamOptions` combines `AgentRunResumeOptions` (including the optional `onSession` observer seam a supervisor uses to attach a child event pump to the rebuilt session) with `
|
|
64
|
+
`resumeAgentRunStream(agent, ref, resume, options?)` does the same for one existing suspended durable run. It validates checkpoint ownership, revision/fingerprint, and `expectedVersion`, then subscribes before emitting `agent_started` / `agent_resumed` and resumed message/tool/terminal events. `AgentRunResumeStreamOptions` combines `AgentRunResumeOptions` (including the optional `onSession` observer seam a supervisor uses to attach a child event pump to the rebuilt session) with `maxQueuedEvents` and `overflow`; early return aborts only resumed execution. Since 0.8.0 (plan 080 Task 3), `AgentRunResumeOptions.signal` is inherited by both entrypoints, so `resumeAgentRun()` aborts a live resumed provider/tool turn the same way `resumeAgentRunStream()` does — checked before each preparation step and threaded into the resumed execution. It does not replay a claimed/dispatched tool, poll a ledger, or retain a worker. `createAgentRunLifecycle().resumeStream(ref, resume, request?)` adds the same behavior after host agent-capability resolution.
|
|
65
65
|
|
|
66
66
|
`session.subscribe(options?)` remains available for hosts that want a long-lived subscriber across runs. Subscribe before `run()` to observe that run's events. The consumer loop and `session.run()` must run concurrently (e.g. start the `for await` consumer, then `await Promise.all([consumer, session.run("Hi")])`): events are only emitted during a live run, so awaiting the subscribe loop before calling `run()` deadlocks. Prefer `session.stream()` when you only need one run's events. `SubscribeOptions.maxQueuedEvents` defaults to `1024` (minimum `1`) and caps events queued while the consumer is not awaiting `next()`. `SubscribeOptions.overflow` defaults to `"close"`; it clears queued payload events, delivers one `event_subscriber_overflow` notice to that subscriber, then closes it. `"drop_oldest"` keeps newest events; `"drop_newest"` ignores new events while full.
|
|
67
67
|
|
|
@@ -186,11 +186,12 @@ Set `runState` with a host-owned `CheckpointStore`, stable `definitionRevision`,
|
|
|
186
186
|
`resumeAgentRun` accepts exactly one of:
|
|
187
187
|
|
|
188
188
|
- `decision: "approve" | "deny"` — legacy single-approval path. `approve` allows every pending decision once; `deny` terminates the run as `denied`.
|
|
189
|
+
- `decision: "continue"` — crash recovery for a running-state checkpoint written by [`checkpointPolicy: "every-turn"`](durable-runs.md): resumes from the last provider-turn boundary without re-dispatching tools. It requires a running state and never bypasses a gate — a suspended run still needs `approve`/`deny` or a decision batch.
|
|
189
190
|
- `decisions: readonly RunDecision[]` — one atomic batch. Every entry validates against the recorded pending set (unknown/foreign `approvalId`, duplicates, stale `expectedVersion`, invalid outcomes fail the whole batch closed with `AgentDecisionError` and leave state and version untouched). Outcomes: `allow_once`, `allow_for_run`, `reject_once`, `reject_for_run`. `reject_*` continues the run with a blocked tool result carrying the bounded (2 KB) `reason`. `modifiedArguments` are revalidated (schema, then input guardrails; permission/trust re-run at dispatch) and produce a new arguments hash. `elicitation` payloads are validated against the pending decision's `elicitationSchema` (required keys plus the configured host validator) and resolve the suspended call without executing it. A batch deciding a strict subset persists the decided entries and re-suspends with the remainder pending at the bumped version.
|
|
190
191
|
|
|
191
192
|
`*_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. Frontend adapters (such as AG-UI with `capabilities.humanInTheLoop.approveWithEdits`) and the server resume endpoint (`POST .../resume` with `modifiedArguments`) map human edits directly to `RunDecision` entries with `modifiedArguments` under single atomic CAS, revalidating tool parameter schemas and invalidating stale draft approvals.
|
|
192
193
|
|
|
193
|
-
**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.
|
|
194
|
+
**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`/`continue`, 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.
|
|
194
195
|
|
|
195
196
|
```ts
|
|
196
197
|
const result = await session.run("Publish draft", {
|
|
@@ -203,7 +204,7 @@ if (result.status === "suspended") {
|
|
|
203
204
|
}
|
|
204
205
|
```
|
|
205
206
|
|
|
206
|
-
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. Since 0.7.0 (plan 074 P3), `persistSessionState: true` also carries the opt-in [attention compiler](attention-compiler.md)'s sticky frontier (`sessionState.attentionSticky`: 32-hex thinking keys plus tool-call ids, newest 256 of each, redacted like all state) so a resumed run keeps its thinking strips and tool stubs instead of re-deciding its first turn from the ratio; a malformed frontier is dropped entry by entry and never blocks a resume. Since 0.7.0, `onSession` hands the reconstructed session to a caller-supplied observer before the resumed run starts, so an observer (the supervisor's child-event pump) can subscribe while the run is still live; it is called for every resume outcome, a throw fails closed before any event or tool work, and the session is valid only for the duration of that resume. 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.
|
|
207
|
+
Resume requires exact checkpoint ownership, version, agent fingerprint, and revision. A checkpoint load or delete under a non-matching ownership scope reads as absent (`null`), and a save against a foreign-owned record fails as a generic `ERR_PRISM_CHECKPOINT_CONFLICT` (plan 080 Task 3) — a tenant cannot distinguish “another tenant owns this key” from “missing”, and callers that relied on the old `Checkpoint ownership mismatch` throw now see the same miss they would for an unknown key. 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. Since 0.7.0 (plan 074 P3), `persistSessionState: true` also carries the opt-in [attention compiler](attention-compiler.md)'s sticky frontier (`sessionState.attentionSticky`: 32-hex thinking keys plus tool-call ids, newest 256 of each, redacted like all state) so a resumed run keeps its thinking strips and tool stubs instead of re-deciding its first turn from the ratio; a malformed frontier is dropped entry by entry and never blocks a resume. Since 0.7.0, `onSession` hands the reconstructed session to a caller-supplied observer before the resumed run starts, so an observer (the supervisor's child-event pump) can subscribe while the run is still live; it is called for every resume outcome, a throw fails closed before any event or tool work, and the session is valid only for the duration of that resume. 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. For mid-run crash recovery (`checkpointPolicy: "every-turn"` plus `decision: "continue"`), see [Durable runs](durable-runs.md).
|
|
207
208
|
|
|
208
209
|
## Secure composition
|
|
209
210
|
|
package/docs/coding-security.md
CHANGED
|
@@ -219,6 +219,7 @@ A native Windows backend (Job objects / AppContainer) is tracked, not scheduled.
|
|
|
219
219
|
|
|
220
220
|
## Related APIs
|
|
221
221
|
|
|
222
|
+
- [Work sandbox](work-sandbox.md): work image + `createWorkComposition`; host injects `createDockerSandbox` (prism-work does not fork it)
|
|
222
223
|
- [Coding agent tools](coding-agent-tools.md): durable plan/todo Markdown helpers and `state.coding` checkpoint metadata for restart/resume without a second runtime
|
|
223
224
|
- [Hosted sandboxes](hosted-sandboxes.md): E2B pause/resume adapter, filesystem-only snapshots, reconnect by sandbox id
|
|
224
225
|
- [Workflows](workflows.md): `runWorkflow` / `resumeWorkflow` / `startWorkflowBackground` composition for coding tasks
|
package/docs/coding-tools.md
CHANGED
|
@@ -24,7 +24,6 @@ npm install @dietrichgebert/ponytail
|
|
|
24
24
|
|---|---|---|
|
|
25
25
|
| `@arnilo/prism-coding-tools/agent` | Core coding tools (read, write, edit, search, bash, git, diagnostics, check, ast-grep, lsp) | — |
|
|
26
26
|
| `@arnilo/prism-coding-tools/security` | Sandbox execution adapters (Docker/OCI, native disposable sandbox, approval policies, egress proxy) | — |
|
|
27
|
-
| `@arnilo/prism-coding-tools/document-reader` | Bounded PDF/DOCX literal-text extraction; optional host-selected Mistral OCR (native fetch, no SDK peer) | `pdf-parse`, `mammoth` |
|
|
28
27
|
| `@arnilo/prism-coding-tools/openapi` | OpenAPI 3.x tool generator and executor with SSRF protection and parameter validation | — |
|
|
29
28
|
| `@arnilo/prism-coding-tools/computer-use-linux` | Linux desktop observation and targeting tool bridge | — |
|
|
30
29
|
| `@arnilo/prism-coding-tools/dev` | Loopback-only developer inspector, event timeline visualizer, and local replay server | — |
|
|
@@ -67,7 +67,7 @@ Worker limits are finite positive safe integers:
|
|
|
67
67
|
| Recent-message window | — | 512 KiB | `renderRecentMessageWindow()` hard cap |
|
|
68
68
|
| Recall page size | 20 | 100 | `retrieval.pageLimit` / recall tool `limit` |
|
|
69
69
|
|
|
70
|
-
Direct `runObserver()` / `runReflector()` / `runDropper()` calls retain required `maxTurns` and accept the corresponding shorter worker fields (`maxToolCalls`, `maxResultBytes`, etc.). Named default/hard constants and `resolveMemoryWorkerLimits()` are exported.
|
|
70
|
+
Direct `runObserver()` / `runReflector()` / `runDropper()` calls retain required `maxTurns` and accept the corresponding shorter worker fields (`maxToolCalls`, `maxResultBytes`, etc.). Workers are tool-only: text, thinking, and done events are ignored; a turn with no `tool_call` succeeds as a no-op and records nothing. Named default/hard constants and `resolveMemoryWorkerLimits()` are exported.
|
|
71
71
|
|
|
72
72
|
## Outputs / response / events
|
|
73
73
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Connected apps
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
`createConnectedAppSession()` groups host-selected MCP bridges under one verified identity. It exposes only prefixed `ToolDefinition`s selected by the host and leaves transport construction, OAuth, credentials, and remote effect classification with that host.
|
|
6
|
+
|
|
7
|
+
## When to use it
|
|
8
|
+
|
|
9
|
+
Use connected apps when one agent needs a small, identity-scoped set of SaaS or internal MCP servers. Use typed [work tools](work-tools.md) instead for high-trust Google Workspace or Microsoft 365 actions that require Prism's draft/approve lifecycle.
|
|
10
|
+
|
|
11
|
+
## Inputs / request
|
|
12
|
+
|
|
13
|
+
| Input | Required | Contract |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| `identity` | yes | Active, verified `AgentIdentity` bound to every connection. A `bind()` identity, if supplied, must match tenant, account, user, and principal. |
|
|
16
|
+
| `select` | yes | Host admission callback. `false` denies before any MCP connection. |
|
|
17
|
+
| `effect` | no | Shared `McpToolEffectPolicy`. Omit it to retain MCP's `external_mutation` / `unsupported` default. |
|
|
18
|
+
| `connect` | no | Test seam. Production uses `connectMcpTools`. |
|
|
19
|
+
| `maxApps` | no | Maximum bindings; defaults to 8 and has a hard cap of 32. |
|
|
20
|
+
| `bind({ appId, serverId, transport, allowTools, effect })` | yes | `appId` and `serverId` are unique session identifiers. `transport` is already host-built. `allowTools` is an exact remote-name allowlist; per-binding `effect` overrides the shared policy. |
|
|
21
|
+
|
|
22
|
+
## Outputs / response / events
|
|
23
|
+
|
|
24
|
+
`bind()` connects one admitted bridge. `tools()` returns its prefixed tools without re-listing. `refresh()` re-lists every bound bridge. `list()` returns only `appId`, `serverId`, and visible prefixed tool names. `unbind()` and `close()` close their bridges.
|
|
25
|
+
|
|
26
|
+
## Host composition inspection
|
|
27
|
+
|
|
28
|
+
Pass only `apps.list()` identifiers into `inspectHostComposition()`; inspection never connects, refreshes, or receives a transport. A business host with connected apps must provide a verified identity.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
const bindings = apps.list();
|
|
32
|
+
const report = inspectHostComposition({
|
|
33
|
+
profile: "business",
|
|
34
|
+
agent,
|
|
35
|
+
connectedApps: {
|
|
36
|
+
appIds: bindings.map(({ appId }) => appId),
|
|
37
|
+
serverIds: bindings.map(({ serverId }) => serverId),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Request/response example
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"binding": {
|
|
47
|
+
"appId": "slack",
|
|
48
|
+
"serverId": "slack",
|
|
49
|
+
"transport": { "type": "stdio", "command": "/usr/bin/slack-mcp", "args": ["mcp"] },
|
|
50
|
+
"allowTools": ["list_channels", "post_message"]
|
|
51
|
+
},
|
|
52
|
+
"list": [{ "appId": "slack", "serverId": "slack", "tools": ["mcp:slack:list_channels", "mcp:slack:post_message"] }]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Implementation example
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { createToolRegistry, type AgentIdentity } from "@arnilo/prism";
|
|
60
|
+
import { createConnectedAppSession } from "@arnilo/prism-mcp";
|
|
61
|
+
|
|
62
|
+
const identity: AgentIdentity = {
|
|
63
|
+
tenantId: "tenant-a",
|
|
64
|
+
userId: "user-a",
|
|
65
|
+
principal: { kind: "user", id: "user-a" },
|
|
66
|
+
scopes: ["tools:execute"],
|
|
67
|
+
issuedAt: new Date().toISOString(),
|
|
68
|
+
verified: true,
|
|
69
|
+
};
|
|
70
|
+
const apps = createConnectedAppSession({
|
|
71
|
+
identity,
|
|
72
|
+
select: ({ transport }) => transport.type === "stdio" && transport.command === "/usr/bin/slack-mcp",
|
|
73
|
+
effect: ({ remoteName }) =>
|
|
74
|
+
remoteName.startsWith("list_") ? { kind: "none", idempotency: "none" } : undefined,
|
|
75
|
+
});
|
|
76
|
+
await apps.bind({
|
|
77
|
+
appId: "slack",
|
|
78
|
+
serverId: "slack",
|
|
79
|
+
transport: { type: "stdio", command: "/usr/bin/slack-mcp", args: ["mcp"] },
|
|
80
|
+
allowTools: ["list_channels", "post_message"],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const registry = createToolRegistry({ duplicate: "error" });
|
|
84
|
+
for (const tool of apps.tools()) registry.register(tool);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Slack MCP wrap example
|
|
88
|
+
|
|
89
|
+
[`examples/connected-slack-mcp.ts`](../examples/connected-slack-mcp.ts) is a network-free template: a mock bridge exposes read and write Slack names, an exact `allowTools` list exposes only the intended tools, and the host policy marks `list_*`, `get_*`, and `search_*` as observations. `post_*`, `update_*`, and `delete_*` remain the MCP external-mutation default.
|
|
90
|
+
|
|
91
|
+
For a real Slack server, the host `select` callback must admit its exact stdio command or HTTP origin. Build credentials into stdio `env` or MCP OAuth before `bind()`; never place tokens in model context.
|
|
92
|
+
|
|
93
|
+
## Open Connector sidecar (example only)
|
|
94
|
+
|
|
95
|
+
[`examples/open-connector-sidecar/`](../examples/open-connector-sidecar/README.md) shows the opposite shape: Prism as the host for a sibling connector gateway over loopback MCP. The recipe pins an immutable `ghcr.io/oomol-lab/open-connector` release tag (never `main`/`tip`/`latest`), binds port `3000` on `127.0.0.1`, and keeps runtime tokens host-supplied.
|
|
96
|
+
|
|
97
|
+
Admission stays with the host: `select` must admit the exact loopback origin with `allowLoopbackHttp: true`, and an exact `allowTools` list (`search_actions`, `get_action_guide`, `execute_action`) keeps the provider catalog and connection listings off the model. Reads stay observations; `execute_action` is intentionally left unclassified and therefore remains `external_mutation` / `unsupported`. MCP `execute_action` accepts no `Idempotency-Key` — use HTTP `POST /v1/actions/:actionId` for retry-safe writes, or keep writes in [work tools](work-tools.md).
|
|
98
|
+
|
|
99
|
+
Identity mapping is host glue: Open Connector has no Prism identity, so the host resolves `connectionName`/`x-oo-connector-alias` from the verified `AgentIdentity` and issues one runtime token per identity. OC provider egress (including `skipDnsValidation` executors and `OOMOL_CONNECT_ALLOWED_PROXIES`) stays inside Open Connector; Prism's `pinnedFetch` policy cannot be layered over it.
|
|
100
|
+
|
|
101
|
+
No workspace package depends on Open Connector, no OC source is vendored, and `examples/open-connector-sidecar.ts` proves the admission and effect behavior network-free.
|
|
102
|
+
|
|
103
|
+
## Extension and configuration notes
|
|
104
|
+
|
|
105
|
+
Build stdio `env` and Streamable HTTP `requestInit.headers` in host code before `bind()`. Use `createMcpOAuthTransport()` when the host chooses MCP OAuth. The session does not discover catalogs, construct commands, resolve credentials, or add a second OAuth implementation.
|
|
106
|
+
|
|
107
|
+
## Security and performance notes
|
|
108
|
+
|
|
109
|
+
`select` is mandatory and deny-by-default. The session never infers an effect from remote descriptions or annotations; unclassified tools stay external mutations with unsupported idempotency. `allowTools` is an allowlist. `list()` excludes transports, headers, environment, and tokens. Binding performs one MCP connect; `tools()` uses cached bridge definitions until explicit `refresh()`.
|
|
110
|
+
|
|
111
|
+
## Related APIs
|
|
112
|
+
|
|
113
|
+
- [MCP client bridge and server exposure](mcp-tools.md): underlying MCP transports, bridge limits, OAuth, and tool mapping.
|
|
114
|
+
- [Agent identity](agent-identity.md): verified identity lifecycle and delegation boundaries.
|
|
115
|
+
- [Recoverable tool effects](tool-effects.md): effect declarations and mutation recovery semantics.
|
|
116
|
+
- [Work tools](work-tools.md): typed high-trust M365 and Google Workspace actions.
|
|
@@ -211,6 +211,19 @@ Mode slices and skill bodies are independent: the injector can add `PONYTAIL MOD
|
|
|
211
211
|
|
|
212
212
|
Pure validation without the tool: `resolveSkillLoad({ registry, name, tools, loaded, activeSkillNames })`.
|
|
213
213
|
|
|
214
|
+
### Bundled work skills (`docx`, `xlsx`, `powerpoint`, `pdf`)
|
|
215
|
+
|
|
216
|
+
`@arnilo/prism-work/skills` vendors four MIT Hermes productivity skills. `loadWorkSkills()` reads the committed `SKILL.md` files (64 KiB cap) and overlays Prism `toolNames` in TypeScript — vendored markdown is never edited.
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
import { createSkillRegistry } from "@arnilo/prism";
|
|
220
|
+
import { loadWorkSkills } from "@arnilo/prism-work/skills";
|
|
221
|
+
|
|
222
|
+
const registry = createSkillRegistry(loadWorkSkills(), { duplicate: "error" });
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Scripts run only via work-sandbox `execFile` (argv, no shell). See [Work sandbox](work-sandbox.md).
|
|
226
|
+
|
|
214
227
|
### Context budget priority and skill demotion
|
|
215
228
|
|
|
216
229
|
When `assembleProviderInput` runs with `contextBudget`, `applyContextBudget` evicts droppable sections in layout order. Within `context` blocks and skills, victims sort by ascending `ContextBlock.priority` (missing = **0**), then LIFO within the same priority.
|
package/docs/core.md
CHANGED
|
@@ -41,7 +41,6 @@ Every peer below is optional and fails closed at first use; the [optional peer d
|
|
|
41
41
|
| `@arnilo/prism-core/governance/observability` | OpenTelemetry instrumentation and event tracing | `@opentelemetry/api` |
|
|
42
42
|
| `@arnilo/prism-core/credentials/node` | Keyring-backed encrypted credential store, scrypt envelope encryption, OAuth2 PKCE providers, and OIDC identity verification | `@napi-rs/keyring` (bundled) |
|
|
43
43
|
| `@arnilo/prism-core/enterprise/postgres` | Unified multi-tenant enterprise PostgreSQL state (approvals, evaluations, model-router, policy, tool effects, work idempotency) | `pg` |
|
|
44
|
-
| `@arnilo/prism-core/integrations/work` | Microsoft 365 and Google Workspace CLI tool adapters with approval gates and idempotency | — |
|
|
45
44
|
| `@arnilo/prism-core/validation/json-schema` | Ajv-backed JSON Schema tool argument validation | `ajv` (bundled) |
|
|
46
45
|
|
|
47
46
|
## Usage Examples
|
|
@@ -85,3 +84,4 @@ const validator = createJsonSchemaToolArgumentValidator();
|
|
|
85
84
|
- Subpaths never load database drivers (`pg`, `better-sqlite3`) unless the specific database subpath is imported.
|
|
86
85
|
- All database and network drivers fail closed with clear actionable error messages when peers are omitted.
|
|
87
86
|
- Root `@arnilo/prism` remains dependency-free contracts and CLI runner.
|
|
87
|
+
- Messaging channels are `@arnilo/prism-channels` (`/telegram`, `/signal`), not a `@arnilo/prism-core` subpath.
|
package/docs/diagrams.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Diagramming, draw.io embed client, and mxGraph XML validation (`@arnilo/prism-
|
|
1
|
+
# Diagramming, draw.io embed client, and mxGraph XML validation (`@arnilo/prism-work/diagrams`)
|
|
2
2
|
|
|
3
3
|
## What it does
|
|
4
4
|
|
|
5
|
-
The `@arnilo/prism-
|
|
5
|
+
The `@arnilo/prism-work/diagrams` package provides an origin-enforced draw.io / diagrams.net iframe embed client, XXE-safe mxGraph XML validation, and byte-stable deterministic XML canonicalization for content hashing and visual artifact workflows in Prism applications and agent runtimes.
|
|
6
6
|
|
|
7
7
|
### Core Capabilities
|
|
8
8
|
|
|
@@ -17,7 +17,7 @@ The `@arnilo/prism-office/diagrams` package provides an origin-enforced draw.io
|
|
|
17
17
|
|
|
18
18
|
## When to use it
|
|
19
19
|
|
|
20
|
-
Use `@arnilo/prism-
|
|
20
|
+
Use `@arnilo/prism-work/diagrams` when applications, host workspaces, or autonomous agents need to:
|
|
21
21
|
1. Embed an interactive, self-hosted draw.io / diagrams.net editor inside a web or Electron iframe with strictly enforced cross-origin security.
|
|
22
22
|
2. Coordinate diagram editing lifecycles (`init` handshake, `load`, `save`, `autosave`, `merge`, and `export`).
|
|
23
23
|
3. Execute save-with-preview workflows generating SVG (`xmlsvg`) or PNG (`xmlpng`) visual snapshots from the active editor session.
|
|
@@ -148,7 +148,7 @@ Outbound host-to-editor action postMessage:
|
|
|
148
148
|
## Implementation example
|
|
149
149
|
|
|
150
150
|
```ts
|
|
151
|
-
import { createDrawioEmbed, validateDrawioXml, canonicalizeDrawioXml } from "@arnilo/prism-
|
|
151
|
+
import { createDrawioEmbed, validateDrawioXml, canonicalizeDrawioXml } from "@arnilo/prism-work/diagrams";
|
|
152
152
|
|
|
153
153
|
// 1. Initialize embed client with strict origin binding
|
|
154
154
|
const embed = createDrawioEmbed({
|
|
@@ -240,8 +240,8 @@ const summary = validateDrawioXml(xml, {
|
|
|
240
240
|
|
|
241
241
|
## Related APIs
|
|
242
242
|
|
|
243
|
-
- [`@arnilo/prism-
|
|
244
|
-
- [`@arnilo/prism-
|
|
243
|
+
- [`@arnilo/prism-work/documents`](./documents.md): Specification-compliant OpenXML document generation and preview rendering for DOCX, XLSX, and PPTX.
|
|
244
|
+
- [`@arnilo/prism-work/sheets`](./sheets.md): Spreadsheet and CSV parsing engine with strict financial decimal safety guarantees.
|
|
245
245
|
- [`@arnilo/prism-web-tools/browser`](./browser-automation.md): Browser automation tools and quarantine lifecycle.
|
|
246
246
|
- [`@arnilo/prism-ag-ui`](./ag-ui.md): Agent-User Interface projection and timeline components.
|
|
247
247
|
- [`@arnilo/prism-core/governance/observability`](./observability.md): OpenTelemetry instrumentation and trace adapters.
|
package/docs/document-reader.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
# Document reader (`@arnilo/prism-
|
|
1
|
+
# Document reader (`@arnilo/prism-work/document-reader`)
|
|
2
2
|
|
|
3
3
|
> **Optional peer install:** `pdf-parse` and/or `mammoth` — see [Optional peer dependencies](peer-dependencies.md). OCR uses **native fetch**, not an SDK peer.
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
|
-
Optional bounded literal-text extraction for PDF and
|
|
7
|
+
Optional bounded literal-text extraction for PDF, DOCX, XLSX, and PPTX files, consumed by the coding `read` tool. `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. XLSX renders as TSV and PPTX as a slide title/bullet outline through the in-package OOXML parser. Scanned PDFs/images need a **host-selected** `createMistralOcrParser({ apiKey })` passed in `parsers` — default wiring never calls an external OCR service.
|
|
8
8
|
|
|
9
9
|
## When to use it
|
|
10
10
|
|
|
11
|
-
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
|
|
11
|
+
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. PDF/DOCX use optional peer parsers (`pdf-parse`, `mammoth`); XLSX/PPTX use the bounded Prism OOXML parser. Docker-less hosts that need document reads pair this with the network-free native sandbox backend (`@arnilo/prism-coding-tools/security` `createNativeSandbox`) for the surrounding tool execution.
|
|
12
12
|
|
|
13
13
|
Activation is explicit: no file-extension sniffing anywhere enables parsing. Absent `documentReader` option = exactly the 0.1.5 read behavior.
|
|
14
14
|
|
|
@@ -21,10 +21,10 @@ Activation is explicit: no file-extension sniffing anywhere enables parsing. Abs
|
|
|
21
21
|
| `maxBytes` | Hard input size cap; oversize files refuse before loading | 32 MiB | 512 MiB |
|
|
22
22
|
| `maxPages` | Page cap for formats that report pages; over-page documents refuse | 1000 | 10 000 |
|
|
23
23
|
| `maxTextBytes` | Extracted-literal-text cap; over-cap results truncate (`truncatedBy: "bytes"`) | 2 MiB | 64 MiB |
|
|
24
|
-
| `parsers` | Host-selected `DocumentParser[]`; default wiring
|
|
24
|
+
| `parsers` | Host-selected `DocumentParser[]`; default wiring includes peer-backed PDF/DOCX plus OOXML XLSX/PPTX | `[pdf, docx, xlsx, pptx]` | — |
|
|
25
25
|
| `redactor` | Optional `SecretRedactor` applied to extracted text at the adapter boundary | none | — |
|
|
26
26
|
|
|
27
|
-
Format gating is magic-byte based: PDF header (`%PDF-`); DOCX zip container
|
|
27
|
+
Format gating is magic-byte based: PDF header (`%PDF-`); DOCX/XLSX/PPTX zip container plus `word/document.xml`, `xl/workbook.xml`, or `ppt/presentation.xml` part marker. Unsupported buffers return `null` and the read falls through to its text path.
|
|
28
28
|
|
|
29
29
|
## Outputs / response / events
|
|
30
30
|
|
|
@@ -36,7 +36,7 @@ Errors: `DocumentReaderError` with code `ERR_PRISM_DOCUMENT_READER` for missing
|
|
|
36
36
|
|
|
37
37
|
```ts
|
|
38
38
|
import { createReadTool } from "@arnilo/prism-coding-tools/agent";
|
|
39
|
-
import { createDocumentReader } from "@arnilo/prism-
|
|
39
|
+
import { createDocumentReader } from "@arnilo/prism-work/document-reader";
|
|
40
40
|
|
|
41
41
|
const documentReader = await createDocumentReader({
|
|
42
42
|
maxBytes: 32 * 1024 * 1024,
|
|
@@ -51,7 +51,7 @@ A `read` of `spec.pdf` yields text content extracted from the PDF (up to 2 MiB o
|
|
|
51
51
|
## Implementation example
|
|
52
52
|
|
|
53
53
|
```ts
|
|
54
|
-
import { createDocumentReader, createPdfParser, type DocumentParser } from "@arnilo/prism-
|
|
54
|
+
import { createDocumentReader, createPdfParser, type DocumentParser } from "@arnilo/prism-work/document-reader";
|
|
55
55
|
|
|
56
56
|
// Host-selected parser wiring: swap in a different PDF backend without touching bounds.
|
|
57
57
|
const myPdfParser: DocumentParser = {
|
|
@@ -64,7 +64,7 @@ const myPdfParser: DocumentParser = {
|
|
|
64
64
|
};
|
|
65
65
|
const reader = await createDocumentReader({ parsers: [myPdfParser, await createPdfParser()] });
|
|
66
66
|
|
|
67
|
-
import { createMistralOcrParser } from "@arnilo/prism-
|
|
67
|
+
import { createMistralOcrParser } from "@arnilo/prism-work/document-reader";
|
|
68
68
|
const ocr = createMistralOcrParser({
|
|
69
69
|
apiKey: hostKey, // never read from process.env
|
|
70
70
|
recordUsage: (u) => router.recordUsage({ /* Task 7 */ tokens: 0, costUsd: hostPrice(u) }),
|
|
@@ -74,7 +74,7 @@ const scanned = await createDocumentReader({ parsers: [ocr] }); // not in the de
|
|
|
74
74
|
|
|
75
75
|
## Extension and configuration notes
|
|
76
76
|
|
|
77
|
-
- Default parser wiring uses
|
|
77
|
+
- Default parser wiring uses optional peer dependencies `pdf-parse` (PDF) and `mammoth` (DOCX raw text), plus in-package XLSX/PPTX parsing. Missing selected peers fail closed at creation — never at read time. XLSX sheet count and PPTX slide count share `maxPages`; text remains capped at `maxTextBytes`.
|
|
78
78
|
- `createMistralOcrParser` is **not** a default parser. It POSTs `https://api.mistral.ai/v1/ocr` (`mistral-ocr-latest`) with inline `data:` URLs (`include_image_base64: false`). No Files API upload, so no remote cleanup. Host `documentUrl` values pass `assertSsrfAllowedUrl`. Extracted markdown is untrusted. Caps: 8 MiB / 32 pages / 60 s / 1 in-flight by default (hard 50 MiB / 10 000 pages / 180 s / 4). Pass `recordUsage` to admit cost through Task 7 accounting. `baseUrl` selects residency.
|
|
79
79
|
- 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.
|
|
80
80
|
- The read tool re-checks `maxTextBytes` on results (parity with its text-page bounds check) and refuses reader output beyond it.
|
package/docs/documents.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Documents, spreadsheets, and presentations (`@arnilo/prism-
|
|
1
|
+
# Documents, spreadsheets, and presentations (`@arnilo/prism-work/documents`)
|
|
2
2
|
|
|
3
3
|
## What it does
|
|
4
4
|
|
|
5
|
-
The `@arnilo/prism-
|
|
5
|
+
The `@arnilo/prism-work/documents` package provides specification-compliant, AI-native OpenXML document generation, parsing, patching, and bounded preview rendering for Microsoft Word (`.docx`), Microsoft Excel (`.xlsx`), and Microsoft PowerPoint (`.pptx`) artifacts.
|
|
6
6
|
|
|
7
7
|
It operates on a canonical, typed abstract syntax tree (AST) called the **Prism Document Model** (`DocModel`, `SheetModel`, `DeckModel`):
|
|
8
8
|
- **Pure in-memory doctrine**: Functions accept `Uint8Array` container buffers or typed model objects and emit `Uint8Array` buffers or JSON models. Zero filesystem reads, zero network I/O, zero `process.env` lookups, and zero child process spawns.
|
|
@@ -15,13 +15,13 @@ It operates on a canonical, typed abstract syntax tree (AST) called the **Prism
|
|
|
15
15
|
|
|
16
16
|
## When to use it
|
|
17
17
|
|
|
18
|
-
Use `@arnilo/prism-
|
|
18
|
+
Use `@arnilo/prism-work/documents` whenever autonomous agents, coding assistants, workflow orchestrators, or enterprise applications need to:
|
|
19
19
|
1. Synthesize professional DOCX reports, financial XLSX spreadsheets, or PPTX presentation decks from structured LLM outputs.
|
|
20
20
|
2. Ingest existing OOXML artifacts into a structured, validated document model for analysis or automated summarization.
|
|
21
21
|
3. Perform atomic, validated updates or localized edits to documents using typed patch operations.
|
|
22
22
|
4. Render safe, bounded HTML previews or framework-neutral outline and grid snapshots in web and desktop hosts.
|
|
23
23
|
|
|
24
|
-
Do **not** use this package for collaborative real-time editing (OT/CRDT), macro execution,
|
|
24
|
+
Do **not** use this package for collaborative real-time editing (OT/CRDT), macro execution, in-memory spreadsheet formula evaluation (formulas are preserved verbatim as `{ formula, cachedValue? }` read-only pairs), or OLE binaries (`.doc` / `.xls` / `.ppt`). Recalculation, slide PNG QA, and legacy convert run out of process in the [work sandbox](work-sandbox.md) (`xlsx_recalc.py`, `soffice --convert-to pdf|docx|xlsx|pptx`, `pdftoppm`).
|
|
25
25
|
|
|
26
26
|
## Inputs / request
|
|
27
27
|
|
|
@@ -40,6 +40,24 @@ Do **not** use this package for collaborative real-time editing (OT/CRDT), macro
|
|
|
40
40
|
| `documentModelSchema` | `(kind: DocumentKind, slice?: string \| readonly string[]) => JsonSchema` | Retrieves the Draft-07 JSON Schema for a document kind, or a self-contained sliced sub-schema with resolved `$defs` (`docModelSchema` / `sheetModelSchema` / `deckModelSchema` expose the unsliced schemas). |
|
|
41
41
|
| `validateDocumentModel`| `(model: unknown) => asserts model is DocumentModel` | Validates arbitrary JSON objects against Draft-07 document schemas and structural invariants. |
|
|
42
42
|
|
|
43
|
+
### Agent tool facade
|
|
44
|
+
|
|
45
|
+
`@arnilo/prism-work/tools` exports `createOfficeTools({ caps?, redactor?, filesystem?, artifacts? })`: six thin wrappers over this package — `office_parse`, `office_import`, `office_generate`, `office_patch`, `office_diff`, and `office_preview`.
|
|
46
|
+
|
|
47
|
+
- Parse/import accept a document `kind` plus exactly one bounded `bytesBase64` or sandbox-relative `path`; results are marked `untrusted_external`.
|
|
48
|
+
- Generate returns only `{ contentHash, byteLength, format }`; set `outputPath` for a contained sandbox write or `artifact: true` for the host-provided `ArtifactBodyStore`. Raw OOXML never enters tool content.
|
|
49
|
+
- Patch is pure unless it also writes generated bytes. Parse/import/diff/preview declare no effect; generate/patch declare an external mutation only when a sandbox/artifact target is requested.
|
|
50
|
+
- Filesystem paths must be relative to the configured root and pass the host/sandbox realpath containment hook when supplied. OOXML is still PK-gated, cap-limited, redacted at parse, and never executes macros.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { createOfficeTools } from "@arnilo/prism-work/tools";
|
|
54
|
+
|
|
55
|
+
const tools = createOfficeTools({
|
|
56
|
+
filesystem: { root: "/workspace", readFile, writeFile },
|
|
57
|
+
artifacts: { bodies, createRef }, // host-owned ArtifactBodyStore and ownership-aware ref factory
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
43
61
|
### Capacity Limits and Defaults
|
|
44
62
|
|
|
45
63
|
Caps are strictly enforced in memory before compute-intensive translation or parsing:
|
|
@@ -107,7 +125,7 @@ import {
|
|
|
107
125
|
renderPreviewBlocks,
|
|
108
126
|
renderPreviewHtml,
|
|
109
127
|
type DocModel,
|
|
110
|
-
} from "@arnilo/prism-
|
|
128
|
+
} from "@arnilo/prism-work/documents";
|
|
111
129
|
|
|
112
130
|
// 1. Define typed document model
|
|
113
131
|
const doc: DocModel = {
|
|
@@ -156,7 +174,7 @@ history.apply([{ op: "set", target: { title: true }, value: "Updated Review" }])
|
|
|
156
174
|
console.log(history.canUndo()); // true
|
|
157
175
|
const restored = history.undo(); // restored to "Executive Summary" state
|
|
158
176
|
|
|
159
|
-
import { diffDocument } from "@arnilo/prism-
|
|
177
|
+
import { diffDocument } from "@arnilo/prism-work/documents";
|
|
160
178
|
const diff = diffDocument(parsed, restored, { maxOps: 4096 });
|
|
161
179
|
|
|
162
180
|
// 6. Generate structured preview blocks & safe HTML
|
|
@@ -167,7 +185,7 @@ const htmlSnippet = renderPreviewHtml(restored, { maxHtmlBytes: 256 * 1024 });
|
|
|
167
185
|
## Extension and configuration notes
|
|
168
186
|
|
|
169
187
|
### Sub-package Pinning
|
|
170
|
-
To avoid pulling in CLI frameworks or extraneous dependencies, `@arnilo/prism-
|
|
188
|
+
To avoid pulling in CLI frameworks or extraneous dependencies, `@arnilo/prism-work/documents` directly pins the exact underlying modular packages:
|
|
171
189
|
- `@office-open/docx@0.12.3`
|
|
172
190
|
- `@office-open/xlsx@0.12.3`
|
|
173
191
|
- `@office-open/pptx@0.12.3`
|
|
@@ -203,20 +221,23 @@ const telemetry: DocumentsTelemetry = {
|
|
|
203
221
|
```
|
|
204
222
|
|
|
205
223
|
### Decimal Fidelity Ceiling
|
|
206
|
-
Financial worksheets often require exact decimal representations that JavaScript 64-bit binary floating-point numbers cannot represent without precision loss. `@arnilo/prism-
|
|
224
|
+
Financial worksheets often require exact decimal representations that JavaScript 64-bit binary floating-point numbers cannot represent without precision loss. `@arnilo/prism-work/documents` supports canonical string decimals (`{ type: "decimal", value: "1500000.00" }`). Generate emits that string; it does not coerce through `Number()`. Parse round-trips `{ type: "decimal", value: "1500000.00" }`.
|
|
225
|
+
|
|
226
|
+
`list` blocks generate as OOXML numbering (`w:numPr`), not a `• ` prefix in `w:t`. Charts, pivots, comments, and headers remain fidelity-reported drops.
|
|
207
227
|
|
|
208
228
|
## Security and performance notes
|
|
209
229
|
|
|
210
230
|
- **Pure In-Memory Operation**: No temporary files, no shell execution, no binary spawning, and zero network sockets.
|
|
211
|
-
- **ZIP Signature Gating**: Buffers must begin with PK zip container signatures (`0x50, 0x4B, 0x03, 0x04`). Extension-based type inference is strictly prohibited.
|
|
231
|
+
- **ZIP Signature Gating**: Buffers must begin with PK zip container signatures (`0x50, 0x4B, 0x03, 0x04`). Extension-based type inference is strictly prohibited. OLE compound files (`.doc` / `.xls` / `.ppt`, magic `D0 CF 11 E0`) fail this gate. Convert them in the [work sandbox](work-sandbox.md) then `office_parse`. Encrypted OOXML is still dropped on import.
|
|
212
232
|
- **Fail-Closed Caps**: Input size and element count caps are evaluated before entering XML translation passes, preventing zip-bomb and decompression amplification attacks.
|
|
213
233
|
- **Sanitize-by-Construction HTML**: `renderPreviewHtml` strictly entity-encodes all text fields, neutralizes dangerous protocols (`javascript:`, `http://`, `https://`), strips raw script/image tags, and caps output size to prevent DOM-based XSS and memory exhaustion.
|
|
214
234
|
- **Performance Budget**: Warm generation of 200-block documents completes in under 15 ms; parse and round-trip equality checks complete in under 100 ms.
|
|
215
235
|
|
|
216
236
|
## Related APIs
|
|
217
237
|
|
|
218
|
-
- [`@arnilo/prism-
|
|
219
|
-
- [`@arnilo/prism-
|
|
238
|
+
- [`@arnilo/prism-work/document-reader`](./document-reader.md): Bounded literal text extraction from PDF and DOCX documents for coding agent tools; optional host-selected Mistral OCR parser.
|
|
239
|
+
- [`@arnilo/prism-work/connectors`](./work-tools.md): Microsoft 365 and Google Workspace identity-scoped connectors.
|
|
220
240
|
- [`@arnilo/prism-coding-tools/agent`](./coding-agent-tools.md): Coding tools and file operations.
|
|
221
241
|
- [`@arnilo/prism-core/governance/observability`](./observability.md): OpenTelemetry instrumentation and trace adapters.
|
|
222
242
|
- [Work artifacts and review](work-artifacts-and-review.md): evidence-bound artifact citations and `evidenceDigest` approvals.
|
|
243
|
+
- [Work sandbox](work-sandbox.md): LibreOffice formula recalc, slide rasterization, and OLE convert; this package still does not spawn `soffice`.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Durable runs
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
Long investigations survive a host process that dies mid-run. With `checkpointPolicy: "every-turn"`, the durable store holds a running-state checkpoint at each provider-turn boundary — after the previous turn's tool results are in the session store, before the next provider request. A restarted worker resumes the *same* run with `decision: "continue"`: no tool is re-dispatched, and the session history is rebuilt from the checkpoint's session/leaf reference instead of being re-run from turn zero.
|
|
6
|
+
|
|
7
|
+
This is crash recovery for the in-run state, not an orchestrator. The host workflow engine (Temporal, a queue, a supervisor) still owns scheduling, retries, and completion; Prism owns only the run's turns, counters, loop-local state, and sticky attention frontier.
|
|
8
|
+
|
|
9
|
+
## When to use it
|
|
10
|
+
|
|
11
|
+
- A `Do`-style investigation can outlive its worker process (deploys, evictions, OOM kills, spot reclamation) and re-running the paid turns is unacceptable.
|
|
12
|
+
- The host wants a bounded, explicit recovery point rather than "restart the whole run".
|
|
13
|
+
- An external orchestrator needs to resume a single run without replaying its tools.
|
|
14
|
+
|
|
15
|
+
For approval suspension and batch decisions, see [Agent/session runtime § Durable interruption](agent-session-runtime.md#durable-interruption); `every-turn` is additive to that machinery and uses the same store, redaction, bounds, fingerprint, and CAS.
|
|
16
|
+
|
|
17
|
+
## Inputs / request
|
|
18
|
+
|
|
19
|
+
`AgentRunStateOptions` (per-run `RunOptions.runState` or `AgentConfig.runState`):
|
|
20
|
+
|
|
21
|
+
| Field | Meaning |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `checkpointPolicy` | `"decision"` (default) persists only on suspension/terminal status. `"every-turn"` adds one running-state checkpoint per provider turn. |
|
|
24
|
+
| `checkpoints` | The host's `CheckpointStore`; the same store serves suspension, crash recovery, and status. |
|
|
25
|
+
| `definitionRevision` | Host-authored revision participating in the fingerprint; a change without a revision bump refuses resume. |
|
|
26
|
+
| `persistSessionState` | Also carries loaded-skill names and the attention sticky frontier into each turn checkpoint. |
|
|
27
|
+
| `includeSkillBodies` | Alongside `persistSessionState`, carries exact skill instructions. |
|
|
28
|
+
| `maxStateBytes` | Save-side byte ceiling (default 256 KB, hard 1 MB). Applies to every turn checkpoint identically. |
|
|
29
|
+
|
|
30
|
+
Resume uses `resumeAgentRun` / `resumeAgentRunStream` with `{ expectedVersion, decision: "continue" }`. The checkpoint records its own cadence, so a continued run keeps writing turn checkpoints without the host repeating `checkpointPolicy`.
|
|
31
|
+
|
|
32
|
+
## Outputs / response / events
|
|
33
|
+
|
|
34
|
+
Each turn checkpoint is a normal durable state (schema v1) carrying status `running`, the current `leafId`, run counters and wall deadline, loop-local state when the loop declares `snapshot`/`restore`, the run's `toolNames` grant, and — with `persistSessionState` — the loaded-skill catalog plus sticky attention frontier. Hard gates are unchanged: CAS `expectedVersion`, ownership/fencing, redaction at the checkpoint boundary, `maxStateBytes`, and the agent fingerprint (`agentFingerprint`) over id, revision, model, instructions, system prompt, skills, tools, guardrails, and loop revision.
|
|
35
|
+
|
|
36
|
+
A crash leaves the last checkpoint at status `running`. `decision: "continue"` accepts exactly that: a running checkpoint with no interruption and no unresolved pending decisions. Everything else fails closed with `AgentRunStateError` and zero checkpoint writes:
|
|
37
|
+
|
|
38
|
+
- `expectedVersion` mismatch, ownership/fencing mismatch, revision or fingerprint mismatch (`Stale or non-running agent run resume`, `Agent revision or fingerprint mismatch on resume`).
|
|
39
|
+
- Status `suspended` — approvals, elicitations, and input guardrails still require `approve`/`deny` or a `RunDecision` batch; `continue` never bypasses a gate.
|
|
40
|
+
- Any interruption, pending decision, or ready-to-dispatch pending call recorded in the state.
|
|
41
|
+
|
|
42
|
+
The resumed run emits `agent_resumed` with the claimed version, reuses the recorded run counter snapshot, restores loop-local state, and dispatches nothing that was already persisted. Terminal saves then drop the pending markers as before.
|
|
43
|
+
|
|
44
|
+
**Ambiguity window.** A turn checkpoint is taken between turns, so a crash can lose at most the one provider turn that was in flight; that turn is re-requested on resume. A crash *inside* a tool's side effect is still the pre-existing ambiguous case — an already-marked `dispatched` call is never replayed automatically; resolve it manually or key host effects on `runId`/`toolCallId` idempotency. Counter caveat: the interrupted turn's `maxTurns` charge was already recorded at assembly, and the resumed turn charges once more, so a crash costs one extra turn against a finite `maxTurns` budget.
|
|
45
|
+
|
|
46
|
+
## Request/response example
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { createAgent, createMemoryCheckpointStore, providerDone, providerTextDelta, resumeAgentRun } from "@arnilo/prism";
|
|
50
|
+
|
|
51
|
+
const checkpoints = createMemoryCheckpointStore();
|
|
52
|
+
const agent = createAgent({
|
|
53
|
+
id: "investigation",
|
|
54
|
+
model: { provider: "mock", model: "demo" },
|
|
55
|
+
provider: { id: "mock", async *generate() { yield providerTextDelta("done"); yield providerDone(); } },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Worker 1: long run, crash-recoverable between turns.
|
|
59
|
+
const session = agent.createSession({ id: "investigation-session" });
|
|
60
|
+
const first = await session
|
|
61
|
+
.run("Investigate", { runState: { checkpoints, definitionRevision: "2026-09-20.1", checkpointPolicy: "every-turn" } })
|
|
62
|
+
.catch(() => undefined); // worker died
|
|
63
|
+
|
|
64
|
+
// Worker 2: same stores, same run, continue from the last turn boundary.
|
|
65
|
+
const resumed = await resumeAgentRun(
|
|
66
|
+
agent,
|
|
67
|
+
{ runId, sessionId: session.id },
|
|
68
|
+
{ decision: "continue", expectedVersion: checkpointVersion },
|
|
69
|
+
{ checkpoints, definitionRevision: "2026-09-20.1" },
|
|
70
|
+
);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The complete network-free demo — one tool execution across the crash, resumed from the turn checkpoint — is [`examples/durable-investigation.ts`](../examples/durable-investigation.ts).
|
|
74
|
+
|
|
75
|
+
## Extension and configuration notes
|
|
76
|
+
|
|
77
|
+
- `checkpointPolicy: "decision"` is byte-identical to the pre-0.8 behavior: no turn writes, no extra events, no state fields. Turn checkpoints appear only when the option is set.
|
|
78
|
+
- The policy is recorded on the state (when non-default) and restored on resume, so hosts do not thread the option through `AgentRunResumeOptions`.
|
|
79
|
+
- Custom loops declared durable via `snapshot`/`restore` hooks keep their loop-local state across a crash resume exactly as they do across a suspension.
|
|
80
|
+
- Per-run `toolNames` grants, run counters, and wall deadlines all ride the turn checkpoint; the resumed run cannot widen any of them.
|
|
81
|
+
|
|
82
|
+
## Security and performance notes
|
|
83
|
+
|
|
84
|
+
- `"continue"` is a host-API action only. Prism's AG-UI interrupt resolution accepts `approve`/`deny` only, channel adapters resume with `deny`, and there is no server route that forwards an untrusted `continue`; adding one would create an approval-bypass path.
|
|
85
|
+
- Every gate that protects a suspension protects a continue resume: exact ownership, fencing token, fingerprint, revision, CAS version, and the absence of unresolved work. A running checkpoint is a recovery point, never an authorization.
|
|
86
|
+
- Cost is one bounded checkpoint write per provider turn (same redaction and `maxStateBytes` ceiling as suspension writes). A 40-turn investigation under `"every-turn"` therefore writes 40 checkpoint rows plus the terminal save, while the default `"decision"` policy writes at most one row per approval or suspension. Each row carries the run frontier, counters, run limits, and loop snapshot — not the message history, which stays in the session store and is pointed at by `leafId` — so the store grows with turns, not with turns × transcript; a state that would exceed `maxStateBytes` (default 256 KiB, `DEFAULT_MAX_AGENT_RUN_STATE_BYTES`) fails closed rather than truncating. Pick `"every-turn"` when a worker restart must cost at most one turn of thinking, and leave the default for runs with many cheap turns.
|
|
87
|
+
- Checkpoints never contain provider objects, callbacks, signals, credentials, or raw secrets; the payload is bounded and redacted like any other durable state.
|
|
@@ -200,6 +200,10 @@ export async function recordEnterpriseState(state: PostgresEnterpriseState) {
|
|
|
200
200
|
// `state` comes from `await createPostgresEnterpriseState({ pool, schema: "prism" })`.
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
+
## Composing with messaging channels
|
|
204
|
+
|
|
205
|
+
An outbox row is a natural fit for the `deliver` seam of `createMessagingRuntime` when the host wants each reply recorded alongside its own business state: append in the host transaction, commit, then hand the reply to the transport adapter. The channel runtime never imports this package and `@arnilo/prism-channels` has no `pg` peer — the composition is host code, because only the host knows which local mutation the row belongs with and which transport it owns. The appended payload should carry correlation ids (`connectionId`, the answered event id, reply kind) rather than message text, and the tenant id should come from the resolved identity's ownership scope rather than from the inbound event. `examples/messaging-outbox.ts` runs this seam offline against a fake pool.
|
|
206
|
+
|
|
203
207
|
## Extension and configuration notes
|
|
204
208
|
|
|
205
209
|
- `createModelRouter({ resolver, stateStore: state.modelRouter })` keeps allow-list, residency, fallback, and diagnostics behavior in `@arnilo/prism-core/governance/model-router`; this package only supplies durable state. Router admission reservations (`reserveBudget`/`commitBudget`/`releaseBudget` on `state.modelRouter`) live in the `reservations` JSONB column of `prism_model_router_budgets`: one atomic UPSERT per admission, fencing-token-guarded commit/release in a SERIALIZABLE transaction, and TTL reconciliation as unknown usage; see [Model routing](model-routing.md).
|
|
@@ -70,6 +70,8 @@ interface ExecutionTimeline {
|
|
|
70
70
|
readonly workflowRevision?: string;
|
|
71
71
|
readonly traceId?: string;
|
|
72
72
|
readonly status: string;
|
|
73
|
+
readonly stopReason?: AgentFinishReason;
|
|
74
|
+
readonly stopDetail?: string;
|
|
73
75
|
readonly startedAt: string;
|
|
74
76
|
readonly finishedAt?: string;
|
|
75
77
|
readonly input?: unknown;
|
|
@@ -152,6 +154,10 @@ const timeline = projectWorkflowTimeline(workflowEvents, {
|
|
|
152
154
|
|
|
153
155
|
See runnable host demo in `examples/execution-timeline.ts` for offline workflow timeline projection, cockpit summary, and Mermaid diagram export.
|
|
154
156
|
|
|
157
|
+
### Stop reasons
|
|
158
|
+
|
|
159
|
+
Run-level `stopReason` mirrors `agent_finished.finishReason` when the loop stopped on a ceiling or a host turn policy (`"host_policy"`); `status` reads `finished:<stopReason>` for those runs and `succeeded` for a natural end. `stopDetail` carries the host's `turnPolicy.stop` reason, bounded to 256 bytes and redacted at the runtime boundary. See [Runs and usage ledger § Clean stops and stop reasons](runs-and-usage.md#clean-stops-and-stop-reasons).
|
|
160
|
+
|
|
155
161
|
## Bounds
|
|
156
162
|
|
|
157
163
|
| Dimension | Default | Hard cap |
|