@arnilo/prism 0.0.24 → 0.0.26
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 +41 -0
- package/dist/agent-loops.js +37 -5
- package/dist/agent-run-state.d.ts +27 -1
- package/dist/agent-run-state.js +80 -4
- package/dist/agents.js +884 -77
- package/dist/contracts.d.ts +181 -4
- package/dist/contracts.js +53 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/tools.d.ts +2 -1
- package/dist/tools.js +17 -2
- package/docs/0.1.0-readiness.md +9 -8
- package/docs/a2a.md +24 -0
- package/docs/ag-ui-adoption.md +1 -1
- package/docs/ag-ui.md +102 -3
- package/docs/agent-events.md +25 -0
- package/docs/agent-loops.md +9 -1
- package/docs/agent-session-runtime.md +9 -2
- package/docs/coding-agent-tools.md +29 -3
- package/docs/coding-security.md +36 -2
- package/docs/forge-integration.md +113 -0
- package/docs/host-security.md +1 -0
- package/docs/index.md +13 -10
- package/docs/language-intelligence.md +162 -0
- package/docs/mcp-tools.md +2 -0
- package/docs/migration.md +48 -0
- package/docs/performance.md +23 -6
- package/docs/process-sessions.md +147 -0
- package/docs/release-and-install.md +41 -16
- package/docs/server.md +1 -0
- package/docs/supervisors.md +4 -0
- package/docs/workflows.md +1 -1
- package/package.json +2 -2
package/docs/ag-ui.md
CHANGED
|
@@ -35,7 +35,8 @@ npm install @arnilo/prism @arnilo/prism-ag-ui
|
|
|
35
35
|
| `lifecycle` + `resolveRun` | Optional durable status/resume path. Required only for a resumed interruption. |
|
|
36
36
|
| `interrupts.resume` | Optional host aggregate-policy callback for multiple AG-UI interrupts; it returns one current-version core approve/deny decision. |
|
|
37
37
|
| `replay` | Optional page adapter or `createAgentEventSourceAgUiReplay(source, options)` for gap-free distributed replay/live follow. |
|
|
38
|
-
| `projection` | Explicit safe tool/state/messages/activity/reasoning/raw/custom/interrupt projection. Omit each callback for default deny. |
|
|
38
|
+
| `projection` | Explicit safe tool/state/messages/activity/reasoning/raw/custom/interrupt projection. Omit each callback for default deny. Prefer `composeAgUiProjections(createMessagesFromSessionProjection(...), createStateFromStoreProjection(...), createActivityFromToolProgressProjection(), host)` for standard families. |
|
|
39
|
+
| `a2ui` | Opt-in A2UI painting middleware (`{ catalogId, mode, renderToolName?, allowedCatalogIds?, limits? }`). Detects `a2ui_operations` tool results and/or streams from `render_a2ui` args; paints `a2ui-surface` activity events. Absent = inert. |
|
|
39
40
|
| `capabilities` | Optional host declaration narrowed to implemented SSE/projector/lifecycle features; read `handler.capabilities`. |
|
|
40
41
|
| `redactor`, `limits` | Host redaction and narrowing-only finite caps. |
|
|
41
42
|
|
|
@@ -53,7 +54,27 @@ ACP maps assistant text to `agent_message_chunk`, safe tool lifecycle to `tool_c
|
|
|
53
54
|
|
|
54
55
|
Selected MCP tools use normal `TOOL_CALL_*` core dispatch. Linked Apps add safe `mcp-apps` activity; app-only tools stay model-hidden. The separate reauthorizing Apps proxy allow-lists initialize/ping/logging/tool/resource calls for one bridge; its sandbox helper returns CSP/iframe config and never executes HTML.
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
### UI-initiated mutation retry through `ToolEffectStore` (FR-4)
|
|
58
|
+
|
|
59
|
+
`createAgUiMcpAppHandler` accepts an optional `effectStore` (Phase 7 `ToolEffectStore`) plus `effectContext` (identity/ownership; falls back to `authorization.ownership` + `context.identity`). Every approved `tools/call` then records `begin` → `markDispatched` → `complete`/`fail`/`markUnknown` in the store; effect keys derive from identity + ownership + tool name + arguments hash (`deriveAppEffectKey`). The proxy **never auto-retries** — the host decides:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { createAgUiMcpAppHandler, reconcileAppEffect } from "@arnilo/prism-ag-ui";
|
|
63
|
+
|
|
64
|
+
const handler = createAgUiMcpAppHandler({
|
|
65
|
+
apps, authorize, context, approveToolCall, allowedOrigins,
|
|
66
|
+
effectStore, // optional: records UI mutations for idempotent retry
|
|
67
|
+
effectContext: ({ authorization, context }) => ({ identity: context.identity, ownership: authorization.ownership }),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// After transport/abort loss the record is `unknown`; the host verifies the
|
|
71
|
+
// actual outcome and resolves it (claim/CAS), then the UI can retry idempotently:
|
|
72
|
+
await reconcileAppEffect({ effectStore, identity, ownership, sessionId, runId, toolName, arguments: args, outcome: "completed", result });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
A retried call whose record is `completed` replays the recorded result without re-dispatching; `failed_retryable`/`failed_terminal`/`dispatched`/`unknown` records fail closed with a `409` until the host reconciles. Wrong-owner or unresolvable identity/ownership fails closed; absent `effectStore` keeps 0.0.25 behavior exactly.
|
|
76
|
+
|
|
77
|
+
`createAgUiA2AAdapter()` maps verified task text/activity. Non-text/tool/A2UI parts need host `projectPart`; non-streaming fallback accepts only a terminal task, otherwise host follows saved correlation. `createAgUiA2AServer()` fronts a local AG-UI agent as an A2A 1.0 server for remote A2A clients (reverse direction; see [A2A interoperability](a2a.md)).
|
|
57
78
|
|
|
58
79
|
Co-work uses bounded, redacted `CUSTOM prism.cowork.*` events through `mapCoWork()` / `createCoWorkReplay()`; see [Work artifacts and review](work-artifacts-and-review.md).
|
|
59
80
|
|
|
@@ -103,11 +124,89 @@ All identity, authorization, session/thread mapping, durable checkpoint lookup,
|
|
|
103
124
|
|
|
104
125
|
`AgUiProjection` is an allow-list. Without a callback, raw tool arguments/results/progress, arbitrary state/patches/transcripts/activity/reasoning/raw events, paths, ACP locations/diffs/terminals/raw I/O, and frontend-supplied tools remain absent. Reasoning signatures do not become AG-UI encrypted values automatically: a host must explicitly provide an already client-encrypted opaque value. `input.project` is also an allow-list: do not merge client state/forwarded props into ownership, identity, tools, permissions, provider options, or media fetch policy.
|
|
105
126
|
|
|
127
|
+
### Reasoning encrypted-value helper (FR-3)
|
|
128
|
+
|
|
129
|
+
`createReasoningEncryptedValue({ encrypt, content, event, maxBytes? })` produces the `encryptedValue` fragment for the `reasoning` projection callback (AG-UI `REASONING_ENCRYPTED_VALUE`):
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { createReasoningEncryptedValue } from "@arnilo/prism-ag-ui";
|
|
133
|
+
|
|
134
|
+
const mapper = createAgUiEventMapper({
|
|
135
|
+
projection: {
|
|
136
|
+
reasoning: (content, event) =>
|
|
137
|
+
createReasoningEncryptedValue({ encrypt: hostEncryptForClient, content, event }),
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`encrypt` is host-owned (client key) and receives the redacted `ThinkingContent` and the Prism event; return `undefined` to decline. The helper is synchronous and pure like the other projection callbacks: it never infers an encrypted value from a Prism reasoning signature, fails closed (returns `undefined`) when `encrypt` is missing, throws, or returns a non-string, and truncates output to `maxBytes` (default `DEFAULT_MAX_REASONING_BYTES`, clamped to `HARD_MAX_REASONING_BYTES`). The mapper additionally caps the emitted value at the resolved `maxReasoningBytes` limit.
|
|
143
|
+
|
|
106
144
|
Co-work projection reuses the same allow-list: `AgUiProjection.coWork(event)` may return a curated, JSON-serializable payload for a co-work event; absent it, the redacted event fields are exposed. Wire `coWorkContext` to derive thread/artifact/identity from the authorized request (never client JSON) and `coWork` to a `createCoWorkReplay()` over your durable artifact/draft/snapshot stores. The handler projects one bounded page after the run; mount a dedicated cursor-paged co-work endpoint when full pagination is needed.
|
|
107
145
|
|
|
146
|
+
Durable interrupts carry the shared decision batch: the fallback interrupt includes the redacted `pendingDecisions` under `metadata` and its `responseSchema` accepts either the legacy `{ decision: "approve" | "deny" }` or a `{ decisions: [{ approvalId, outcome, reason?, modifiedArguments?, elicitation? }] }` batch. All batch entries are shape- and cap-validated at the boundary (count ≤ 128, ids ≤ 128 chars, four outcomes, reason ≤ 8 KiB, payloads ≤ 64 KiB) and core re-validates each against the recorded pending set under the single CAS. `interrupts.resume` may return the batch form (`{ decisions, expectedVersion? }`); legacy `editedArgs` resume payloads still deny. ACP permission prompts offer the four outcomes (`allow_once` / `allow_always` / `reject_once` / `reject_always`) and map them onto the batch; a cancelled prompt stays deny-closed.
|
|
147
|
+
|
|
148
|
+
### Standard projectors (opt-in)
|
|
149
|
+
|
|
150
|
+
Three batteries-included factories return `AgUiProjection` fragments. Compose with host projectors via `composeAgUiProjections(...fragments)` — **first defined callback wins** (left to right); `undefined` fragments are skipped. Absent factories keep 0.0.24 default-deny.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
createAgUiHandler({
|
|
154
|
+
projection: composeAgUiProjections(
|
|
155
|
+
// async transcript source: AgentSession.entries() is async
|
|
156
|
+
createMessagesFromSessionProjection({
|
|
157
|
+
getMessages: async () => (await session.entries()).map(entryToAgUiMessage),
|
|
158
|
+
redact,
|
|
159
|
+
}),
|
|
160
|
+
createStateFromStoreProjection(runStateStore),
|
|
161
|
+
createActivityFromToolProgressProjection(),
|
|
162
|
+
hostCustom,
|
|
163
|
+
),
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Every `AgUiProjection` callback may return a promise (types are `Awaitable<T>` — Task 15, 0.0.26), so projectors can call async host APIs like `session.entries()` directly. Sync-only hosts keep exact prior behavior: sync return values short-circuit, hooks are awaited strictly in event order (never `Promise.all`), and a rejected hook fails closed per event (omitted value, stream continues) exactly like today's sync throw handling. `createMessagesFromSessionProjection({ getMessages })` accepts an async transcript source and emits `MESSAGES_SNAPSHOT` from it at `agent_started` and `message_finished` (no sync `getMessages` needed for full session history); `agent_finished` is terminal and the mapper projects nothing after it, so the final snapshot arrives at the last `message_finished`.
|
|
168
|
+
|
|
169
|
+
| Factory | Emits | Notes |
|
|
170
|
+
| --- | --- | --- |
|
|
171
|
+
| `createMessagesFromSessionProjection` | `MESSAGES_SNAPSHOT` | Host `getMessages()` for authorized history (sync or async), or live `message_finished` accumulation. Caps 128/1024. Redact drops closed. |
|
|
172
|
+
| `createStateFromStoreProjection(store)` | `STATE_SNAPSHOT` on `agent_started`; RFC 6902 `STATE_DELTA` (add/replace/remove) when `store.get()` changes | Host store; optional `subscribe` only marks dirty — no Prism watcher. Oversized/throw → drop closed. |
|
|
173
|
+
| `createActivityFromToolProgressProjection` | `ACTIVITY_SNAPSHOT` / `ACTIVITY_DELTA` from `tool_execution_progress` | Default `activityType: "tool-progress"`. Missing progress+metadata → drop closed. |
|
|
174
|
+
|
|
175
|
+
### A2UI painting middleware (opt-in)
|
|
176
|
+
|
|
177
|
+
`createAgUiHandler({ a2ui: { catalogId, mode } })` paints A2UI v0.9 surfaces without a host `projection.activity` callback:
|
|
178
|
+
|
|
179
|
+
| Mode | Source | Paint |
|
|
180
|
+
| --- | --- | --- |
|
|
181
|
+
| `fixed-schema` | Tool result `{ a2ui_operations: [...] }` | First batch with `createSurface` → `ACTIVITY_SNAPSHOT` (`activityType: "a2ui-surface"`); later batches → `ACTIVITY_DELTA` |
|
|
182
|
+
| `streaming` | `tool_call_delta` args of `renderToolName` (default `render_a2ui`) | Progressive `ACTIVITY_SNAPSHOT` with `replace: true` only when complete ops extractable — never partial JSON |
|
|
183
|
+
| `both` | Both paths; streamed surfaces are not re-painted from the final envelope |
|
|
184
|
+
|
|
185
|
+
Host `catalogId` is stamped when absent; model-supplied ids outside `allowedCatalogIds` (default `[catalogId]`) are overwritten. Invalid ops emit one bounded `CUSTOM` event `prism.a2ui.error` and paint nothing. Caps: 64/512 ops per message, 64 KiB/1 MiB per op, 16/64 surfaces per run, depth 32/64.
|
|
186
|
+
|
|
187
|
+
User actions arrive as untrusted `AgUiA2UiAction` values on `input.project({ a2uiActions })` (from `forwardedProps.a2uiAction` or activity/tool-result shapes). Without `input.project` they stay default-deny — Prism never synthesizes a `log_a2ui_event` tool call (documented divergence from official `@ag-ui/a2ui-middleware`). Example: `examples/ag-ui-a2ui.ts`.
|
|
188
|
+
|
|
189
|
+
### Reference frontend renderer (Task 14, 0.0.26)
|
|
190
|
+
|
|
191
|
+
`@arnilo/prism-ag-ui/renderer` ships a framework-free client renderer for AG-UI/A2UI surfaces: it consumes an AG-UI event stream (SSE via `@ag-ui/client`, or any `AsyncIterable`) and renders `a2ui-surface` activity snapshots/deltas into DOM surfaces from a host component catalog. No framework dependency, no host build step, no jsdom (tests use an in-memory DOM stub).
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import { createA2UiRenderer } from "@arnilo/prism-ag-ui/renderer";
|
|
195
|
+
|
|
196
|
+
const renderer = createA2UiRenderer({
|
|
197
|
+
stream: agUiEventStream, // SSE or AsyncIterable of AGUIEvent
|
|
198
|
+
catalog: myComponents, // optional; defaults to Text/Container/Column/Row/Button
|
|
199
|
+
onAction: (action) => sendA2UiAction(action), // optional: Button clicks etc.
|
|
200
|
+
onError: (error) => console.warn(error.code, error.message),
|
|
201
|
+
});
|
|
202
|
+
const surface = await renderer.surface("chat"); // detached DOM node, kept in sync
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The core is a DOM-free state machine (`reduceA2UiOps`): operations become a surface/component model (adjacency list with `id`/`component`/flat props, A2UI v0.9 JSON-Pointer data model, `deleteSurface`); a thin binding layer renders the model through catalog component renderers (framework-free `(props, ctx, dom) => node` functions). Snapshots replace a surface's model (streaming mode sends cumulative ops); RFC 6902 deltas append. The same frozen caps as the server painter are enforced client-side: 64/512 ops per message, 64 KiB/1 MiB per op, 16/64 surfaces per run, depth 32/64. Invalid or oversized ops drop closed with one bounded `prism.a2ui.error` event (host logging via `onError`); unknown catalog components render an explicit placeholder. The renderer never executes remote HTML: only `createElement`/`createTextNode`/`appendChild`, no HTML-string assignment, no dynamic code evaluation. Data bindings `{"path": "/pointer"}` resolve against the per-surface data model; `deleteSurface` detaches content. The main `@arnilo/prism-ag-ui` entry stays runtime-agnostic — DOM code lives only behind the `renderer` subpath (type-only re-exports). Hosts embedding it should follow the MCP Apps CSP/sandbox guidance (`docs/ag-ui-adoption.md`) for iframe/worker placement.
|
|
206
|
+
|
|
108
207
|
## Security and performance notes
|
|
109
208
|
|
|
110
|
-
Authorize every start/replay/resume/proxy/follow. Treat protocol fields, MCP metadata/HTML, and A2A cards/parts as untrusted; persist run/task correlation before output and redact streams. MCP Apps requires extension acknowledgement, exact proxy origin, same-bridge visibility, approval, `ui://` HTML/MIME bounds, and sandbox CSP. It never retries UI mutations;
|
|
209
|
+
Authorize every start/replay/resume/proxy/follow. Treat protocol fields, MCP metadata/HTML, and A2A cards/parts as untrusted; persist run/task correlation before output and redact streams. MCP Apps requires extension acknowledgement, exact proxy origin, same-bridge visibility, approval, `ui://` HTML/MIME bounds, and sandbox CSP. It never retries UI mutations; with an `effectStore` it records them for host-driven idempotent retry and unknown-outcome reconciliation (FR-4).
|
|
111
210
|
|
|
112
211
|
Defaults / hard caps: request 64 KiB / 1 MiB; input 128 / 1024 messages, 32 / 256 tools/contexts, 8 / 64 interrupts, 16 / 64 media parts, and 64 KiB / 1 MiB text/state/media; frontend tool/context payloads 16 KiB / 256 KiB; projected event/state/activity/reasoning/raw values 64 KiB / 1 MiB; patches 128 / 4096 operations; JSON depth 16 / 64, properties 128 / 4096, arrays 512 / 8192; cursor 4 / 16 KiB; replay page 100 / 500 records; queue 128 / 4096 events; stream 10,000 / 100,000 events and 10 / 64 MiB; wall time 120 seconds / 30 minutes. Overflow yields a bounded error/closed stream, not an unbounded queue. SSE is declared; WebSocket/protobuf/push are not. Reconnect is at-least-once, so clients de-duplicate stable event/message/tool IDs.
|
|
113
212
|
|
package/docs/agent-events.md
CHANGED
|
@@ -22,6 +22,31 @@ Event records preserve emission order within a run because the runtime drains pe
|
|
|
22
22
|
|
|
23
23
|
`AgentEventSource` (`createMemoryAgentEventSource` / `persistence.events` on PostgreSQL) appends, pages, and subscribes with opaque ownership-bound cursors. `subscribe` registers wake interest before replaying history so replay-to-live handoff has no gap. Delivery is at-least-once; consumers dedupe `record.id`. PostgreSQL uses transactional sequence allocation plus `LISTEN`/`NOTIFY` wakeups with polling fallback. Transport adapters (server SSE `Last-Event-ID`, AG-UI, A2A `afterEventId`) map source envelopes only — they do not invent private replay loops. This is not exactly-once.
|
|
24
24
|
|
|
25
|
+
### Placement (FR-7 answer, 0.0.26)
|
|
26
|
+
|
|
27
|
+
The durable `AgentEventSource` **stays in `@arnilo/prism-session-store-postgres`** for the 0.0.26 line and is importable from the package root (FR-6):
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createPostgresAgentEventSource } from "@arnilo/prism-session-store-postgres";
|
|
31
|
+
const source = createPostgresAgentEventSource({ pool, schema: "prism", cursorSecret });
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
PostgreSQL `LISTEN`/`NOTIFY` remains the **reference durable implementation**; `createPostgresPersistence` still bundles the same source as `persistence.events` (the canonical path — no behavior change). The standalone root export exists for consumers that want a durable source without full persistence. The migration path from the 0.0.24/0.0.25 API is: `persistence.events` and `createPostgresAgentEventSource` both keep working unchanged; a future relocation (if any) ships a replacement export with a deprecation note before removing the old one. See [migration](migration.md) `0.0.25 → 0.0.26` and the FR-6/FR-7 record `prism-agent-event-source-export-and-location.md`.
|
|
35
|
+
|
|
36
|
+
### NATS JetStream adapter (FR-5)
|
|
37
|
+
|
|
38
|
+
`@arnilo/prism-session-store-nats` ships a sibling durable `AgentEventSource` over NATS JetStream for JetStream backbones (Postgres remains the reference implementation):
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { connect } from "@nats-io/transport-node";
|
|
42
|
+
import { createNatsAgentEventSource, createNatsJetStream } from "@arnilo/prism-session-store-nats";
|
|
43
|
+
|
|
44
|
+
const nc = await connect({ servers: process.env.NATS_URL });
|
|
45
|
+
const source = createNatsAgentEventSource({ connection: await createNatsJetStream(nc), stream: "prism_agent_events" });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
One subject per run (`prism.agent-events.<tenant>.<session>.<run>`); the JetStream per-subject sequence is the per-run event sequence. `append` is idempotent by `record.id` within the stream's dedupe window; `page`/`subscribe` replay per subject from HMAC-signed cursors; `subscribe` uses a durable pull consumer with explicit acks (at-least-once, 30s redelivery, dedupe by `record.id`); `cleanup` deletes ownership-scoped messages older than `before`. The host provisions the stream (subjects `prism.agent-events.>`, retention limits, dedupe window). Inert on import; network-free tests use an in-memory fake of the narrow `NatsJetStream` seam.
|
|
49
|
+
|
|
25
50
|
## Inputs / request
|
|
26
51
|
|
|
27
52
|
```ts
|
package/docs/agent-loops.md
CHANGED
|
@@ -118,7 +118,15 @@ Optional steer hooks on `LoopContext` (0.0.11): `hasPendingSteers?()` / `applyPe
|
|
|
118
118
|
|
|
119
119
|
## Durable runs
|
|
120
120
|
|
|
121
|
-
`RunOptions.runState` supports
|
|
121
|
+
`RunOptions.runState` supports the built-in loop options (`single-shot` and `generate-validate-revise`) and custom strategies that opt into durable state. `single-shot` is durable via the runtime's pending-call mechanism and carries no loop-local state. `generate-validate-revise` snapshots `{ attempts, artifactPhase, savedSchema, pendingHistory }` at `revision: "1"`. A custom `AgentLoopStrategy` must declare both snapshot hooks or durable configuration rejects it with `AgentLoopStateError` (`ERR_PRISM_LOOP_NOT_DURABLE`) before any provider call:
|
|
122
|
+
|
|
123
|
+
| Member | Purpose |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `revision?: string` | Host-authored loop revision. Joins the durable-run fingerprint, so a loop change without a `definitionRevision` bump fails closed on resume. |
|
|
126
|
+
| `snapshot?(): JsonValue` | Capture loop-local resumable state at suspension. Must be JSON-compatible; core redacts it and bounds it inside the durable run-state envelope (`maxStateBytes`, depth 32). A non-JSON value fails the run with `ERR_PRISM_LOOP_SNAPSHOT`. |
|
|
127
|
+
| `restore?(snapshot): void` | Rehydrate from the captured snapshot; must throw on drift. Called once before `run(ctx)` on resume. Also available as `ctx.restoredLoopState`. |
|
|
128
|
+
|
|
129
|
+
The snapshot is stored as `loopState: { name, revision, snapshot }` on the durable run state and cleared when the run reaches a terminal status. On resume, a name/revision mismatch between the stored `loopState` and the resolved strategy fails closed (`ERR_PRISM_LOOP_REVISION`), and the fingerprint check independently rejects any loop drift. Suspension occurs only before an input provider call or immediately before a tool side effect; completed provider turns remain in `SessionStore` history and are not repeated after `resumeAgentRun()`.
|
|
122
130
|
|
|
123
131
|
## Outputs / response / events
|
|
124
132
|
|
|
@@ -176,7 +176,14 @@ await agent.createSession().run("Hi", { model: overrideModel });
|
|
|
176
176
|
|
|
177
177
|
## Durable interruption
|
|
178
178
|
|
|
179
|
-
Set `runState` with a host-owned `CheckpointStore`, stable `definitionRevision`, and `interruptBeforeTool: true` to suspend at a persisted pre-side-effect boundary. A suspended result has `status: "suspended"`, a redacted `interruption`, and `runState.version`; it releases session resources before returning.
|
|
179
|
+
Set `runState` with a host-owned `CheckpointStore`, stable `definitionRevision`, and `interruptBeforeTool: true` to suspend at a persisted pre-side-effect boundary. A suspended result has `status: "suspended"`, a redacted `interruption`, and `runState.version`; it releases session resources before returning. When a provider turn requests several tools, the round is collected into **one** suspension whose `interruption.pendingDecisions` holds one redacted `PendingDecision` per gated call (`approvalId`, kind, scope with tool name/effect kind/identity/arguments hash — never raw arguments); ungated calls still dispatch.
|
|
180
|
+
|
|
181
|
+
`resumeAgentRun` accepts exactly one of:
|
|
182
|
+
|
|
183
|
+
- `decision: "approve" | "deny"` — legacy single-approval path. `approve` allows every pending decision once; `deny` terminates the run as `denied`.
|
|
184
|
+
- `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.
|
|
185
|
+
|
|
186
|
+
`*_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.
|
|
180
187
|
|
|
181
188
|
```ts
|
|
182
189
|
const result = await session.run("Publish draft", {
|
|
@@ -189,7 +196,7 @@ if (result.status === "suspended") {
|
|
|
189
196
|
}
|
|
190
197
|
```
|
|
191
198
|
|
|
192
|
-
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.
|
|
199
|
+
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. 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.
|
|
193
200
|
|
|
194
201
|
## Secure composition
|
|
195
202
|
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
| `createCodingCheckTool(cwd, options)` | Named host-declared checks; model selects only a name. |
|
|
24
24
|
| `createAskUserDecisionTool(options)` | Opt-in user decision tool (`ask_user_decision`); host supplies `ask` callback. Not in default aggregators. |
|
|
25
25
|
| `createLocalRepositoryOperations(limits?)` | Default streaming Node filesystem backend for list/search/glob. |
|
|
26
|
+
| `createGitAwareRepositoryOperations(cwd, options?)` | Optional Git `ls-files` ignore-aware enumeration with native fallback; host-only `includeIgnored`. |
|
|
27
|
+
| `createLanguageIntelligence(options)` | Optional host-activated LSP language intelligence (symbols/definitions/references/diagnostics/hover/rename); see [Language intelligence](language-intelligence.md). |
|
|
28
|
+
| `createProcessSessions(options)` | Optional managed long-running process sessions (start/output/input/wait/signal/kill/release); see [Process sessions](process-sessions.md). |
|
|
29
|
+
| `createGitHubForge(options)` | Optional reference GitHub forge adapter (issue context, push, PR create/update, review comments, checks, handoff reconcile) with `ToolEffectStore` idempotency; see [Forge integration](forge-integration.md). |
|
|
26
30
|
| `createGitOperations(options)` | Typed Git operations backend (argument arrays, safe config, finite output). |
|
|
27
31
|
| `buildCodingCheckpointMetadata` / `validateCodingCheckpointMetadata` / `assertCodingResumeAllowed` | Bounded durable coding-task metadata for workflow `state.coding` (no second runtime). |
|
|
28
32
|
| `writeCodingPlanFile` / `readCodingPlanFile` / `createCodingPlanMarkdown` / `parseCodingPlanTodos` | Workspace plan/todo Markdown helpers with finite byte/todo caps and hash verification. |
|
|
@@ -89,12 +93,14 @@ const tools = createCodingTools(workspaceRoot, {
|
|
|
89
93
|
|
|
90
94
|
### Phase 4 non-goals (0.0.21)
|
|
91
95
|
|
|
92
|
-
These are **out of scope** for
|
|
96
|
+
These are **out of scope** for the 0.0.21 package baseline (see roadmap Phase 9 / later for LSP and process work):
|
|
93
97
|
|
|
94
98
|
- **No PDF / document reader** — text and supported images only via `read`.
|
|
95
99
|
- **No trash / recycle daemon** — `delete` / `move` are permanent; host undo is not automatic.
|
|
96
|
-
- **No PTY / interactive process control
|
|
97
|
-
- **
|
|
100
|
+
- **No PTY / interactive process control in `shell`** — `shell` stays one-shot; optional `createProcessSessions` covers long-running attach/input (PTY still unsupported — see [Process sessions](process-sessions.md)).
|
|
101
|
+
- **LSP language-server tools** — not in default aggregators; optional `createLanguageIntelligence` is Phase 9 (see [Language intelligence](language-intelligence.md)).
|
|
102
|
+
- **Managed process sessions** — not in default aggregators; optional `createProcessSessions` is Phase 9 (see [Process sessions](process-sessions.md)).
|
|
103
|
+
- **GitHub forge adapter** — not in default aggregators; optional `createGitHubForge` is Phase 9 (see [Forge integration](forge-integration.md)); no octokit dependency, no multi-forge abstraction.
|
|
98
104
|
- **No recursive directory delete** — `delete` refuses non-empty directories.
|
|
99
105
|
- **No brace-expansion globs** — `glob` supports only `*`, `?`, and `**`.
|
|
100
106
|
|
|
@@ -226,6 +232,23 @@ A BOM is stripped before matching and re-prepended on write; original line endin
|
|
|
226
232
|
|
|
227
233
|
List repository entries with deterministic relative paths. Uses Node `opendir`/`lstat` only — no glob dependency. Prefer `glob` when you already know a filename pattern. Prefer `repo_search` to find text inside files. Does not follow symlinks; rejects path escapes outside the workspace root. Hidden names and excluded basenames (default `.git`, `node_modules`, `dist`) are skipped unless `includeHidden` is set / host `exclude` is overridden.
|
|
228
234
|
|
|
235
|
+
#### Git-aware enumeration
|
|
236
|
+
|
|
237
|
+
`createGitAwareRepositoryOperations(cwd, options?)` is an optional `RepositoryOperations` backend that enumerates via fixed `git ls-files --cached --others --exclude-standard -z` (honors nested `.gitignore`, `$GIT_DIR/info/exclude`, and exclude-standard rules). Inject it through `ToolsOptions.repository.operations` (or per-tool `repository.operations`).
|
|
238
|
+
|
|
239
|
+
- **Detection:** cached `git rev-parse --is-inside-work-tree`. Outside a Git work tree, or when detection fails, delegates to `options.fallback` (default: `createLocalRepositoryOperations`).
|
|
240
|
+
- **Fail closed:** after successful detection, `ls-files` errors throw `RepositoryError` — no silent mid-session fallback.
|
|
241
|
+
- **Ignored paths:** stay excluded unless the host sets `includeIgnored: true` (factory option only; never a model-facing tool argument). Tracked-but-ignored files remain visible via `--cached` (Git semantics).
|
|
242
|
+
- **Bounds:** at most two Git invocations per operation; stdout capped by `DEFAULT_MAX_LS_FILES_OUTPUT_BYTES` (8 MiB, hard 64 MiB). Existing repo depth/entry/file/result/time caps still apply. No per-file Git spawn; no hand-rolled ignore parser; argv is never model-supplied.
|
|
243
|
+
- **Security:** `.git` internals never listed; paths re-checked against the workspace root; symlink escapes match native fail-closed behavior.
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { createCodingTools, createGitAwareRepositoryOperations } from "@arnilo/prism-coding-agent";
|
|
247
|
+
|
|
248
|
+
const operations = createGitAwareRepositoryOperations(cwd); // native fallback outside Git
|
|
249
|
+
const tools = createCodingTools(cwd, { repository: { operations } });
|
|
250
|
+
```
|
|
251
|
+
|
|
229
252
|
**Inputs:**
|
|
230
253
|
|
|
231
254
|
| Field | Type | Purpose |
|
|
@@ -530,6 +553,9 @@ Every configurable value is a positive safe integer (context may be zero); Prism
|
|
|
530
553
|
|
|
531
554
|
## Related APIs
|
|
532
555
|
|
|
556
|
+
- [Language intelligence](language-intelligence.md): optional host-activated LSP contract (`createLanguageIntelligence`) — symbols/definitions/references/diagnostics/hover/rename.
|
|
557
|
+
- [Process sessions](process-sessions.md): optional managed long-running processes (`createProcessSessions`) — start/output/input/wait/signal/kill/release.
|
|
558
|
+
- [Forge integration](forge-integration.md): optional GitHub adapter (`createGitHubForge`) — issue context, authenticated push, PR create/update, review comments, checks, bounded handoff reconcile; effect-store idempotency, no duplicate PRs/comments on retry, tokens never in argv/logs/events.
|
|
533
559
|
- [Tools](tools.md): the host-owned tool harness — `createToolRegistry`, `dispatchToolCall`, filtering, and the `ToolDefinition` contract these factories satisfy.
|
|
534
560
|
- [Public contracts](public-contracts.md): `ToolDefinition`, `ToolResult`, `ToolExecutionContext`, `ContentBlock`, and `JsonObject` shapes.
|
|
535
561
|
- [Host security guide](host-security.md): fail-closed checklist for permission policies, tool validation, and trust boundaries that must gate these tools.
|
package/docs/coding-security.md
CHANGED
|
@@ -13,6 +13,11 @@
|
|
|
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
|
+
| `SandboxProcessHandle` | Optional long-running process handle (`write`/`signal`/`kill`/`release`/`wait`) returned by `DisposableSandbox.startProcess?`. |
|
|
17
|
+
| `createEgressPolicy(options)` | Deny-all allow-list policy: exact host/port/protocol rules plus frozen `npm-registry` / `github` presets; SHA-256 fingerprint. |
|
|
18
|
+
| `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. |
|
|
19
|
+
| `composeEgressSandboxNetwork(attestation, name)` | Validated custom Docker network carrying proxy attestation; recorded as `prism.egress.*` container labels. |
|
|
20
|
+
| `assertEgressAttestation(attestation)` | Fail-closed validation of proxy attestation evidence. |
|
|
16
21
|
| `assertPathInsideRoots`, `isPathInsideReal` | Symlink-aware path containment helpers. |
|
|
17
22
|
| `evaluateCommandRules`, `hasShellMetacharacters` | Command classification helpers. |
|
|
18
23
|
|
|
@@ -28,6 +33,32 @@ Use this package when coding tools need path scoping, human approval, command ru
|
|
|
28
33
|
|
|
29
34
|
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.
|
|
30
35
|
|
|
36
|
+
Use `createEgressPolicy()` + `createAllowListEgressProxy()` when a coding agent needs outbound network access under an explicit allow list: package installs, forge API calls, or source fetches — never unrestricted egress. The proxy is inert until `start()`; nothing binds or resolves on import or construction.
|
|
37
|
+
|
|
38
|
+
## Allow-list egress composition
|
|
39
|
+
|
|
40
|
+
`createEgressPolicy({ allow, presets })` builds a deny-all policy. Rules are exact `{ host, port, protocol }` triples — no wildcards, no CIDR, no regex. Presets (`npm-registry`, `github`) expand to explicit rule lists at construction. The policy exposes a stable SHA-256 `fingerprint` over the canonical rule set.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { createEgressPolicy, createAllowListEgressProxy } from "@arnilo/prism-coding-security";
|
|
44
|
+
|
|
45
|
+
const policy = createEgressPolicy({
|
|
46
|
+
allow: [{ host: "api.github.com", port: 443, protocol: "https" }],
|
|
47
|
+
presets: ["npm-registry"],
|
|
48
|
+
});
|
|
49
|
+
const proxy = createAllowListEgressProxy({ policy, audit: (record) => host.recordEgress(record) });
|
|
50
|
+
const endpoint = await proxy.start(); // 127.0.0.1:0 by default; bind a reachable interface for containers
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Every request is checked against the policy before any DNS or connect. HTTPS goes through CONNECT tunnels with TLS passed through untouched — no interception, no MITM. DNS answers are resolved once, pinned, and the connected socket's remote address is verified against the pinned set (rebinding defense); private/link-local/metadata ranges (`10/8`, `172.16/12`, `192.168/16`, `127/8`, `169.254/16` incl. `169.254.169.254`, CGNAT, ULA, `::1`, `fe80::/10`) are denied unless the matching rule sets `allowPrivate: true`. Plain-HTTP redirects are followed up to `redirectHops` with every hop re-validated against policy; redirects to unlisted hosts or non-http targets fail closed. Request/response bytes and total transfer time are capped; oversized or slow-loris transfers are cut with `ERR_PRISM_EGRESS_LIMIT`. Every allow/deny writes an `EgressAuditRecord` (id, ts, decision, host, port, protocol, reason, bytes, duration, client address) — never headers, bodies, or tokens. `reloadPolicy()` is the only way to change rules and bumps `policyVersion`; `attestation()` returns `{ proxyEndpoint, denyDirectEgress: true, policyFingerprint, policyVersion, startedAt }` for sandbox composition.
|
|
54
|
+
|
|
55
|
+
Sandbox composition: `composeEgressSandboxNetwork(proxy.attestation(), networkName)` returns a custom `DockerNetworkConfig` whose attestation is validated and recorded as `prism.egress.endpoint` / `prism.egress.fingerprint` / `prism.egress.policyVersion` / `prism.egress.denyDirect=1` container labels. The adapter records evidence; the host must actually restrict the Docker network so the proxy is the only reachable path (e.g., a dedicated network with only the proxy container attached). A custom network without valid attestation fails closed for egress claims, mirroring `assertBrowserSandboxNetwork`.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const network = composeEgressSandboxNetwork(proxy.attestation(), "egress-net");
|
|
59
|
+
const sandbox = await createDockerSandbox({ docker, image, sourceRoot, user, network, limits });
|
|
60
|
+
```
|
|
61
|
+
|
|
31
62
|
## Inputs / request
|
|
32
63
|
|
|
33
64
|
| Option | Default | Purpose |
|
|
@@ -72,7 +103,7 @@ Use `createDockerSandbox()` when the host wants a production-reference containme
|
|
|
72
103
|
|
|
73
104
|
`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.
|
|
74
105
|
|
|
75
|
-
`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.
|
|
106
|
+
`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).
|
|
76
107
|
|
|
77
108
|
## Request/response example
|
|
78
109
|
|
|
@@ -143,7 +174,7 @@ Policies are ordinary host values: attach one globally through `createCodingTool
|
|
|
143
174
|
|
|
144
175
|
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`.
|
|
145
176
|
|
|
146
|
-
Callback approval remains process-local. For approval that must survive restart, wrap the action in an opted-in workflow `toolNode({ approval: { reason, data?, resumeSchema? } })`. The workflow persists `suspended` state before any tool side effect. After explicit approve, it recomputes the action and invokes this package's current `ExecutionPolicy`; durable approval never populates or bypasses the process-local approval cache. Adapters should emit chunks through `request.onData` as they arrive and honor `request.signal`/`request.timeout`; buffering is unnecessary. Coding-agent composes caller abort with its total-output controller, so ignoring the supplied signal defeats process termination even though Prism stops retaining output at the cap. Default caching is `none`; use run-scoped caching only when repeated approval within one run is desired, and session scope only when that wider lifecycle is intentional.
|
|
177
|
+
Callback approval remains process-local. For approval that must survive restart, wrap the action in an opted-in workflow `toolNode({ approval: { reason, data?, resumeSchema? } })`. `ask_user_decision` also maps onto the shared decision model: inside a durable gated agent run its call suspends as a kind-`elicitation` pending decision whose schema carries the choice contract (option-id enums) plus the full question/options UX payload on a Prism-owned schema extension property; the resume decision's `elicitation` payload (a `selectedId`/`selectedIds`/`customText` answer) is validated against the schema and the tool-level answer-shape rules, then resolves the call without invoking the blocking `ask()` callback. The process-local `ask()` path and the workflow suspend/resume path are unchanged. The workflow persists `suspended` state before any tool side effect. After explicit approve, it recomputes the action and invokes this package's current `ExecutionPolicy`; durable approval never populates or bypasses the process-local approval cache. Adapters should emit chunks through `request.onData` as they arrive and honor `request.signal`/`request.timeout`; buffering is unnecessary. Coding-agent composes caller abort with its total-output controller, so ignoring the supplied signal defeats process termination even though Prism stops retaining output at the cap. Default caching is `none`; use run-scoped caching only when repeated approval within one run is desired, and session scope only when that wider lifecycle is intentional.
|
|
147
178
|
|
|
148
179
|
## Security and performance notes
|
|
149
180
|
|
|
@@ -151,11 +182,14 @@ Containment resolves symlinks and rejects paths outside roots. Command rules are
|
|
|
151
182
|
|
|
152
183
|
Docker sandbox containment—not command regexes—enforces filesystem/network/process boundaries for the reference adapter. Network defaults to none; a custom Docker network still requires a host firewall/proxy for DNS/egress claims. Import rejects symlink escapes, devices, FIFOs, and sockets; export counts entries/bytes and hashes before host retention. Secrets in `secrets` are redacted from adapter errors and never exported as environment metadata. Unified workspace mode reuses existing sandbox/repo/coding hard caps and does not introduce unbounded host↔container sync loops. Host mode and `allowMixedWorkspaceWiring` never claim disposable containment. Durable workflow denial/cancellation is terminal and attributable; approved resume still fails if roots, command rules, read-only mode, or other policy changed while suspended. Cache keys are fixed-size SHA-256 digests of selected identity plus action shape; caches remain process-local, retain at most 1,000 decisions with oldest-entry eviction, and have no default/global mode. Path checks and cache lookup are local; sandbox latency belongs to the supplied adapter and Docker daemon.
|
|
153
184
|
|
|
185
|
+
The egress proxy is a policy enforcer, not a firewall: it cannot stop a container whose Docker network reaches the internet directly. Egress attestation (`denyDirectEgress: true`) is a claim the host must make true by network topology; the adapter records it as evidence and fails closed when it is absent or malformed. The proxy performs no TLS interception, no DNS rebinding of its own beyond pinning, and no content filtering; audit records contain no secrets. Frozen caps: 32 concurrent connections (hard 256), 64 MiB request/response bytes (hard 1 GiB), 600 s transfer time (hard 1 h), 128 rules (hard 1,024), 5 redirect hops (hard 10).
|
|
186
|
+
|
|
154
187
|
## Related APIs
|
|
155
188
|
|
|
156
189
|
- [Coding agent tools](coding-agent-tools.md): durable plan/todo Markdown helpers and `state.coding` checkpoint metadata for restart/resume without a second runtime
|
|
157
190
|
- [Workflows](workflows.md): `runWorkflow` / `resumeWorkflow` / `startWorkflowBackground` composition for coding tasks
|
|
158
191
|
- [Host security guide](host-security.md)
|
|
159
192
|
- [Performance limits](performance.md)
|
|
193
|
+
- [Forge integration](forge-integration.md): GitHub adapter whose mutations can be routed through the egress proxy
|
|
160
194
|
- [Tool execution primitives](tool-execution-primitives.md)
|
|
161
195
|
- [Security/auth/trust](settings-auth-trust-security.md)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# GitHub forge integration
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
`createGitHubForge` is an optional host-activated adapter in `@arnilo/prism-coding-agent` for the six **proven** GitHub operations: issue context read, authenticated push, pull-request create/update, review comments, and check/status retrieval, plus bounded handoff reconciliation. It is GitHub-first by freeze decision — there is no multi-forge generic abstraction, and no octokit dependency: HTTP uses Node's global `fetch` with a bounded streaming reader, timeout, and rate-limit backoff; push reuses the existing `BoundGitRunner` with the token injected via `GIT_CONFIG_*` environment variables (`http.extraHeader`) — never argv, never persisted, never in logs/events. Every mutation flows through Phase 8 approval (`ExecutionPolicy`) and Phase 7 `ToolEffectStore` idempotency keys; a retry after a completed call returns the existing record instead of duplicating the PR or comment.
|
|
6
|
+
|
|
7
|
+
| Export | Purpose |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `createGitHubForge(options)` | Build a `ForgeOperations` adapter bound to one `"owner/repo"`. |
|
|
10
|
+
| `ForgeOperations` | `issueContext` / `push` / `createPullRequest` / `updatePullRequest` / `createReviewComment` / `checks` / `reconcileHandoff`. |
|
|
11
|
+
| `ForgeIssueContext` / `ForgePullRequest` / `ForgeCheck` | Bounded response shapes (state, head/base, url, check status/conclusion). |
|
|
12
|
+
| `ForgeHandoffReport` | Push/PR/check state, commits, changed paths, diffstat, warnings; never auto-merges. |
|
|
13
|
+
| `ForgeError` | Typed failures: `ERR_PRISM_FORGE_AUTH` / `_API` / `_STALE` / `_RATE_LIMIT` / `_LIMIT` / `_OWNERSHIP`. |
|
|
14
|
+
| `resolveForgeLimits` / `DEFAULT_MAX_FORGE_*` / `HARD_MAX_FORGE_*` | Pages per operation, payload bytes, comments, concurrency ceiling, request timeout. |
|
|
15
|
+
|
|
16
|
+
## When to use it
|
|
17
|
+
|
|
18
|
+
Use when a coding agent needs to open/update PRs, comment on reviews, push a branch with scoped credentials, or verify handoff state against GitHub before deciding the next step. Do not use as a general GitHub SDK, an auto-merge engine (`reconcileHandoff` never merges), or a replacement for host-owned App installation flows — the adapter resolves credentials through the host's `CredentialResolverSource`-compatible resolver and never stores them.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createGitHubForge } from "@arnilo/prism-coding-agent";
|
|
22
|
+
|
|
23
|
+
const forge = createGitHubForge({
|
|
24
|
+
credentials: { name: "github", resolver: myCredentialResolver }, // App installation token preferred; PAT allowed
|
|
25
|
+
repository: "acme/repo",
|
|
26
|
+
cwd: workspaceRoot, // local checkout for push
|
|
27
|
+
git: { gitPath: "/usr/bin/git" },
|
|
28
|
+
policy, // mutations gated here; denials propagate as ERR_PRISM_EXECUTION_DENIED, no request attempted
|
|
29
|
+
effectStore, // REQUIRED: idempotency + unknown-outcome recovery
|
|
30
|
+
identity, ownership, sessionId, runId, // durable context for mutation effect keys
|
|
31
|
+
});
|
|
32
|
+
const pr = await forge.createPullRequest({ head: "feature/x", base: "main", title: "Add x", body: "Closes #1" });
|
|
33
|
+
const report = await forge.reconcileHandoff({ base: "main", head: "feature/x" });
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Inputs / request
|
|
37
|
+
|
|
38
|
+
`createGitHubForge` options:
|
|
39
|
+
|
|
40
|
+
| Field | Type | Purpose |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| `credentials` | `ForgeCredentialResolverSource` | `{ name, resolver }`; resolver is called per request with `provider: "github"` and `metadata.repository`. |
|
|
43
|
+
| `repository` | `string` | `"owner/repo"`, validated at construction and immutable per instance. |
|
|
44
|
+
| `cwd` | `string` | Local checkout the adapter pushes from. |
|
|
45
|
+
| `git` | `CreateGitRunnerOptions \| BoundGitRunner` | Reused for authenticated push (`git push origin <ref>`). |
|
|
46
|
+
| `policy?` | `ExecutionPolicy` | Every mutation is gated (`kind: "forge"`, risk `high`) before any network or git call. |
|
|
47
|
+
| `effectStore` | `ToolEffectStore` | **Required.** `begin → markDispatched → execute → complete/fail` per mutation. |
|
|
48
|
+
| `identity?` / `ownership?` / `sessionId?` / `runId?` | durable context | Required for mutations; without them mutations fail closed with `ERR_PRISM_FORGE_LIMIT`. Tenant mismatch fails at construction with `ERR_PRISM_FORGE_OWNERSHIP`. |
|
|
49
|
+
| `limits?` | `ForgeLimits` | Pages per operation (default 10, hard 100), payload bytes (1 MiB / 8 MiB), comments per review (100 / 1000), request concurrency ceiling (4 / 8), timeout (30 s / 120 s). |
|
|
50
|
+
| `fetch?` | `typeof fetch` | Host-injectable fetch (defaults to `globalThis.fetch`); route through an egress proxy or inject a mock in tests. |
|
|
51
|
+
|
|
52
|
+
Mutation inputs are validated before any request: refs must not start with `-` or contain NUL/newlines and fit the git ref cap; numbers must be positive integers; PR title/body and comment body are required and bounded by `payloadBytes`. `updatePullRequest` with no fields to change fails closed.
|
|
53
|
+
|
|
54
|
+
## Outputs / response / events
|
|
55
|
+
|
|
56
|
+
- `issueContext({ number })` → `ForgeIssueContext` (title, state, body, labels, author, updatedAt, url). Read-only; no policy gate, no effect record.
|
|
57
|
+
- `push({ refspec? })` → `{ remoteRef }` (`refs/heads/<branch>`). Resolves the current branch with `git rev-parse` when no refspec is given. Token reaches git as `GIT_CONFIG_VALUE_0 = "AUTHORIZATION: basic <base64(x-access-token:<token>)>"`; argv carries only `git push origin <ref>`.
|
|
58
|
+
- `createPullRequest({ head, base, title, body })` → `ForgePullRequest`. Idempotent twice over: the effect key replays completed results, and a 422 `"already exists"` response fetches and returns the open PR instead of failing.
|
|
59
|
+
- `updatePullRequest({ number, title?, body?, state? })` → `ForgePullRequest`. 422 (stale head/base) maps to `ERR_PRISM_FORGE_STALE`.
|
|
60
|
+
- `createReviewComment({ number, path, line, body })` → `{ id }`. Retry with identical args replays the completed effect — no duplicate comment.
|
|
61
|
+
- `checks({ ref })` → `ForgeCheck[]`: check-runs plus commit statuses, deduped by name, paginated up to `pagesPerOperation`.
|
|
62
|
+
- `reconcileHandoff({ base, head })` → `ForgeHandoffReport`: `pushed`, `aheadBy`/`behindBy`, `alreadyUpToDate`, `alreadyMerged`, `pullRequest?`, `checks`, bounded `commits`/`changedPaths`/`diffstat`, `warnings`. A missing head ref reports `pushed: false` with a warning. Never pushes, opens, or merges — the host decides next steps from the report.
|
|
63
|
+
|
|
64
|
+
Every mutation result is recorded in the `effectStore` with a stable key derived from tenant + session + run + operation + canonical arguments. Replay of a completed record returns the stored result; replay of a `dispatched`/`unknown` record fails closed (`ERR_PRISM_FORGE_API`, "requires reconciliation") so a crash mid-mutation never duplicates work — verify actual state via `reconcileHandoff`, then resolve through the store.
|
|
65
|
+
|
|
66
|
+
## Request/response example
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"action": { "kind": "forge", "operation": "create_pull_request", "risk": "high", "metadata": { "repository": "acme/repo", "head": "feature/x", "base": "main" } },
|
|
71
|
+
"decision": { "allowed": true }
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Implementation example
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { createGitHubForge } from "@arnilo/prism-coding-agent";
|
|
79
|
+
|
|
80
|
+
const forge = createGitHubForge({
|
|
81
|
+
credentials: { name: "github-app", resolver },
|
|
82
|
+
repository: "acme/repo",
|
|
83
|
+
cwd: "/srv/jobs/task-1/checkout",
|
|
84
|
+
git: { gitPath: "/usr/bin/git" },
|
|
85
|
+
policy,
|
|
86
|
+
effectStore,
|
|
87
|
+
identity, ownership, sessionId, runId,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await forge.push({ refspec: "feature/x" });
|
|
91
|
+
await forge.createPullRequest({ head: "feature/x", base: "main", title: "Add x", body: "Closes #1" });
|
|
92
|
+
const checks = await forge.checks({ ref: "feature/x" });
|
|
93
|
+
const report = await forge.reconcileHandoff({ base: "main", head: "feature/x" });
|
|
94
|
+
if (!report.alreadyMerged && report.pushed) {
|
|
95
|
+
// host decides: update PR, comment, or stop — the adapter never auto-merges
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Extension and configuration notes
|
|
100
|
+
|
|
101
|
+
Credentials resolve per call through the host resolver; GitHub App installation tokens and PATs are both supported (same `Bearer` REST header and `x-access-token` git header). Least-privilege guidance: App installation tokens with `contents: write` + `pull_requests: write` + `issues: read` cover the six operations; PATs should be fine-grained to the single repository and read/write scope needed. Policy denials propagate as the core `ExecutionDeniedError` (`ERR_PRISM_EXECUTION_DENIED`) — no forge request is attempted — so hosts can distinguish refusal from forge failure. Pagination is sequential (per-request `pagesPerOperation` cap); `requestConcurrency` is a validated ceiling, not a target. The adapter performs no DNS/egress control itself — sandboxed hosts route forge traffic through the Phase 9 egress policy (Task 6).
|
|
102
|
+
|
|
103
|
+
## Security and performance notes
|
|
104
|
+
|
|
105
|
+
Tokens never appear in argv, git config files, logs, model context, or stored events: REST uses the `Authorization` header on a bounded `fetch`, and git uses `GIT_CONFIG_*` environment variables scoped to the single push process. Request bodies and responses are bounded by `payloadBytes` (streamed, content-length pre-checked); timeouts and rate-limit backoff respect `requestTimeoutMs` and `Retry-After`; page fetches stop at `pagesPerOperation`. Repository binding is fixed at construction; tenant binding is checked per mutation; ownership mismatch fails closed. Rate-limit responses map to `ERR_PRISM_FORGE_RATE_LIMIT`, 404 to `ERR_PRISM_FORGE_API`, 422 to `ERR_PRISM_FORGE_STALE`, 401/403 to `ERR_PRISM_FORGE_AUTH`, and cap violations to `ERR_PRISM_FORGE_LIMIT`.
|
|
106
|
+
|
|
107
|
+
## Related APIs
|
|
108
|
+
|
|
109
|
+
- [Tool effects](tool-effects.md): `ToolEffectStore` idempotency and unknown-outcome recovery used by every forge mutation
|
|
110
|
+
- [Coding agent tools](coding-agent-tools.md): `createGitTools`, `createBoundGitRunner`, `createGitOperations` (push rides the same runner)
|
|
111
|
+
- [Coding execution approval and sandboxing](coding-security.md): `ExecutionPolicy` gates, egress policy (Phase 9)
|
|
112
|
+
- [Host security guide](host-security.md)
|
|
113
|
+
- [Performance limits](performance.md)
|
package/docs/host-security.md
CHANGED
|
@@ -148,6 +148,7 @@ Wire those values where they matter: provider adapters receive the resolved cred
|
|
|
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. 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
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.
|
|
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.
|
|
151
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()`.
|
|
152
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.
|
|
153
154
|
- Device adapters (0.0.14, `resolveDevicePolicy`/`assertDeviceAdmit`) are deny-by-default: admission fails closed without explicit `enabled`, an explicit sandbox, approval (when required), an under-budget session count, and shared `RunLimits`. Stream chunks over the frozen cap are dropped with a marker; telemetry is redacted before emit/persist. No vendor voice/desktop package ships in 0.0.14 (demand-gated 0.1.x); device adapters cannot broaden consent/memory/network/file/browser/connector/tool permissions (gate 8).
|