@arnilo/prism 0.2.6 → 0.2.8
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 +10 -2
- package/README.md +3 -2
- package/dist/agent-loops.js +4 -0
- package/dist/agent-run-lifecycle.js +2 -2
- package/dist/agent-session/helpers.d.ts +1 -1
- package/dist/agent-session/helpers.js +2 -1
- package/dist/agent-session/session.d.ts +1 -1
- package/dist/agent-session/session.js +28 -20
- package/dist/agent-session.d.ts +1 -1
- package/dist/agent-session.js +1 -1
- package/dist/agents.d.ts +1 -1
- package/dist/agents.js +1 -1
- package/dist/contracts-core/agent.d.ts +5 -5
- package/dist/contracts-core/agent.js +2 -0
- package/dist/contracts-core/extensions.d.ts +3 -3
- package/dist/contracts-core/extensions.js +2 -0
- package/dist/contracts-core/loop.d.ts +6 -1
- package/dist/contracts-core/session.d.ts +1 -1
- package/dist/contracts-core.d.ts +6 -6
- package/dist/contracts-core.js +6 -6
- package/dist/contracts-protocol.d.ts +8 -2
- package/dist/contracts.d.ts +1 -1
- package/dist/contracts.js +1 -1
- package/dist/field-policy.d.ts +119 -0
- package/dist/field-policy.js +418 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +5 -4
- package/dist/input.js +1 -1
- package/dist/redaction.d.ts +6 -5
- package/dist/redaction.js +18 -10
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +1 -1
- package/docs/0.1.0-readiness.md +7 -7
- package/docs/acp-agent.md +78 -0
- package/docs/acp.md +21 -10
- package/docs/ag-ui.md +1 -1
- package/docs/agent-definitions.md +1 -1
- package/docs/agent-events.md +2 -2
- package/docs/agent-loops.md +2 -2
- package/docs/audit-export.md +151 -0
- package/docs/coding-agent-tools.md +3 -1
- package/docs/coding-security.md +6 -0
- package/docs/data-classification.md +82 -0
- package/docs/disaster-recovery.md +71 -0
- package/docs/enterprise-postgres-state.md +60 -7
- package/docs/evaluations.md +45 -0
- package/docs/host-security.md +4 -0
- package/docs/index.md +11 -5
- package/docs/migration.md +27 -0
- package/docs/operations.md +104 -0
- package/docs/policy-and-audit.md +34 -0
- package/docs/release-0.2.7-evidence.md +514 -0
- package/docs/release-and-install.md +55 -8
- package/docs/structured-output.md +10 -10
- package/docs/workflows.md +48 -3
- package/package.json +4 -3
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## What it does
|
|
4
4
|
|
|
5
|
-
`@arnilo/prism-enterprise-postgres` is one optional PostgreSQL composition for
|
|
5
|
+
`@arnilo/prism-enterprise-postgres` is one optional PostgreSQL composition for existing enterprise state seams:
|
|
6
6
|
|
|
7
7
|
| State | Composition property | Durable behavior |
|
|
8
8
|
| --- | --- | --- |
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
| Work mutations | `workIdempotency` | Atomic claim/CAS lifecycle for connector effects. |
|
|
12
12
|
| Model routing | `modelRouter` | Shared rate, budget, and circuit state for router replicas. |
|
|
13
13
|
| Tool effects | `toolEffects` | Durable `ToolEffectStore` claim/CAS for recoverable tool side effects (migration 002). |
|
|
14
|
-
|
|
|
14
|
+
| ERP messaging | `erpMessaging` | Transactional outbox/inbox markers plus bounded, tenant-scoped at-least-once dispatch (migration 004). |
|
|
15
|
+
| Multi-party approvals | `createPostgresApprovalStore({ pool, schema, authority })` | Immutable approval requests, role/quorum decisions, revocation, bounded delegation, and atomic grant consumption (migration 005). |
|
|
15
16
|
|
|
16
|
-
`createPostgresEnterpriseState()` opens a host-supplied or adapter-owned `pg` pool, verifies/applies checksum-protected enterprise migrations (`001_enterprise_state`, `002_tool_effects`, `003_router_reservations`), and returns those stores plus explicit cleanup and close operations. Importing it performs no I/O. It is separate from session/run persistence in [`@arnilo/prism-session-store-postgres`](postgres-persistence.md).
|
|
17
|
+
`createPostgresEnterpriseState()` opens a host-supplied or adapter-owned `pg` pool, verifies/applies checksum-protected enterprise migrations (`001_enterprise_state`, `002_tool_effects`, `003_router_reservations`, `004_erp_messaging`, `005_erp_approvals`), and returns those stores plus explicit cleanup and close operations. Importing it performs no I/O. It is separate from session/run persistence in [`@arnilo/prism-session-store-postgres`](postgres-persistence.md).
|
|
17
18
|
|
|
18
19
|
## When to use it
|
|
19
20
|
|
|
@@ -58,7 +59,7 @@ interface PostgresEnterpriseState {
|
|
|
58
59
|
readonly workIdempotency: IdempotencyStore;
|
|
59
60
|
readonly modelRouter: ModelRouterStateStore;
|
|
60
61
|
readonly toolEffects: ToolEffectStore;
|
|
61
|
-
readonly
|
|
62
|
+
readonly erpMessaging: PostgresErpMessaging;
|
|
62
63
|
cleanup(input: EnterpriseStateCleanupInput): Promise<EnterpriseStateCleanupResult>;
|
|
63
64
|
close(): Promise<void>;
|
|
64
65
|
}
|
|
@@ -68,6 +69,10 @@ interface PostgresEnterpriseState {
|
|
|
68
69
|
|
|
69
70
|
Work mutations expose six observable states: **absent**, `in_progress`, `completed`, `failed_retryable`, `failed_terminal`, and `unknown`. `begin()` atomically returns `acquired` or the existing record; `complete`/`fail`/`markUnknown` use claim-token plus version compare-and-swap. `unknown` requires an operator/connector-specific `resolveUnknown` decision. It is not automatically replayed and it does **not** claim exactly-once external effects.
|
|
70
71
|
|
|
72
|
+
ERP messaging exposes outbox states `pending`, `dispatched`, `retryable`, `completed`, `unknown`, and `dead_letter`. The caller owns the `pg.PoolClient` transaction: business mutation plus `erpMessaging.outbox.append(client, input)` commit or roll back together. Consumers call `erpMessaging.inbox.record(client, input)` before their local mutation in the same transaction; duplicate delivery returns `false`. `dispatcher.claim()` uses bounded `FOR UPDATE SKIP LOCKED` pages and leases. Acknowledgement, retry, and unknown transitions use claim-token plus version CAS. Expired leases become `unknown`; replay/dead-letter requires a host-verified actor and non-empty audit reference. Delivery remains at-least-once, never exactly-once.
|
|
73
|
+
|
|
74
|
+
Approvals expose request states `pending`, `approved`, `rejected`, `revoked`, and `consumed`. Immutable request data (action digest, requester, role/quorum requirements, separation flag, expiry, delegation depth) plus a monotonic `revision` and the accepted decision array live in one row. `decide`/`revoke` lock the row `FOR UPDATE` and revision-check the terminal transition in one transaction; a rejection is a terminal veto. `consume` verifies tenant, action digest, expiry, policy revision, and revision before flipping `approved` → `consumed` inside the caller-owned transaction, so grant consumption and the protected action commit (or roll back) together. Roles come from the host `ApprovalAuthority`; Prism never treats model/tool/subagent claims as principals.
|
|
75
|
+
|
|
71
76
|
Model-router state is asynchronous and owner/principal/provider/model scoped. Supplying it to `createModelRouter({ stateStore })` requires awaited `resolve`, `recordUsage`, and `recordOutcome` calls with verified identity. The legacy synchronous `providerSource` facade throws `ERR_PRISM_MODEL_ROUTER_ASYNC_STATE` when durable state is configured.
|
|
72
77
|
|
|
73
78
|
## Request/response example
|
|
@@ -85,10 +90,55 @@ Model-router state is asynchronous and owner/principal/provider/model scoped. Su
|
|
|
85
90
|
}
|
|
86
91
|
```
|
|
87
92
|
|
|
88
|
-
A migration creates `prism_policy_decisions`, `prism_evaluations`, `prism_work_idempotency`, three `prism_model_router_*` tables, and its separate `prism_enterprise_migrations` history. Migration `003_router_reservations` adds the nullable-by-default `reservations` JSONB column to `prism_model_router_budgets` (atomic reservation slots for router admission; 0.2.1 readers ignore it). Startup serializes per-schema setup with an advisory transaction lock and rejects checksum or catalog drift rather than silently repairing it.
|
|
93
|
+
A migration creates `prism_policy_decisions`, `prism_evaluations`, `prism_work_idempotency`, three `prism_model_router_*` tables, `prism_erp_outbox`, `prism_erp_inbox`, `prism_erp_approvals`, and its separate `prism_enterprise_migrations` history. Migration `003_router_reservations` adds the nullable-by-default `reservations` JSONB column to `prism_model_router_budgets` (atomic reservation slots for router admission; 0.2.1 readers ignore it). Migration `004_erp_messaging` adds tenant/message and tenant/consumer/message primary keys plus claim, lease, and inbox indexes. Migration `005_erp_approvals` adds the one-row-per-request approval table (PK `tenant_id + id`, status check, decisions JSONB, status/created indexes). Startup serializes per-schema setup with an advisory transaction lock and rejects checksum or catalog drift rather than silently repairing it.
|
|
89
94
|
|
|
90
95
|
## Implementation example
|
|
91
96
|
|
|
97
|
+
```ts
|
|
98
|
+
import { createPostgresErpMessaging } from "@arnilo/prism-enterprise-postgres";
|
|
99
|
+
|
|
100
|
+
const messaging = createPostgresErpMessaging({ pool, schema: "prism" });
|
|
101
|
+
const client = await pool.connect();
|
|
102
|
+
try {
|
|
103
|
+
await client.query("BEGIN");
|
|
104
|
+
await client.query("UPDATE invoices SET status = $1 WHERE tenant_id = $2 AND id = $3", ["posted", tenantId, invoiceId]);
|
|
105
|
+
await messaging.outbox.append(client, {
|
|
106
|
+
tenantId,
|
|
107
|
+
messageId: `invoice:${invoiceId}:posted`,
|
|
108
|
+
topic: "invoice.posted",
|
|
109
|
+
payload: { invoiceId },
|
|
110
|
+
});
|
|
111
|
+
await client.query("COMMIT");
|
|
112
|
+
} catch (error) {
|
|
113
|
+
await client.query("ROLLBACK");
|
|
114
|
+
throw error;
|
|
115
|
+
} finally {
|
|
116
|
+
client.release();
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Consumer transaction uses same client:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
await client.query("BEGIN");
|
|
124
|
+
if (await messaging.inbox.record(client, { tenantId, consumer: "ledger", messageId })) {
|
|
125
|
+
await client.query("UPDATE ledger SET posted = TRUE WHERE tenant_id = $1 AND message_id = $2", [tenantId, messageId]);
|
|
126
|
+
}
|
|
127
|
+
await client.query("COMMIT");
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Dead-letter/replay is host-authorized and auditable:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
await messaging.dispatcher.replay({
|
|
134
|
+
tenantId,
|
|
135
|
+
messageId,
|
|
136
|
+
expectedVersion,
|
|
137
|
+
auditRef: "audit:erp-replay:2026-08-17T00:00:00Z",
|
|
138
|
+
authorizedBy: verifiedOperatorIdentity,
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
92
142
|
```ts
|
|
93
143
|
import type { AgentIdentity } from "@arnilo/prism";
|
|
94
144
|
import { createPostgresEnterpriseState, type PostgresEnterpriseState } from "@arnilo/prism-enterprise-postgres";
|
|
@@ -153,11 +203,13 @@ export async function recordEnterpriseState(state: PostgresEnterpriseState) {
|
|
|
153
203
|
## Extension and configuration notes
|
|
154
204
|
|
|
155
205
|
- `createModelRouter({ resolver, stateStore: state.modelRouter })` keeps allow-list, residency, fallback, and diagnostics behavior in `@arnilo/prism-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).
|
|
206
|
+
- `createPostgresErpMessaging({ pool, schema? })` is the direct messaging composition. `outbox.append` and `inbox.record` accept a caller-owned `PoolClient`; the host must put them in the same transaction as its local mutation. The dispatcher owns only short claim/transition transactions and never invokes business callbacks or stores executable handlers.
|
|
207
|
+
- `createPostgresApprovalStore({ pool, schema?, authority })` is the direct approval composition (migration 005). `authority.resolveRoles(actor, request)` and `policyRevision` are host-owned; Prism persists only accepted role grants and delegation chains. `decide`/`revoke` lock the request row and revision-check the terminal transition in one transaction. `consume` accepts an optional caller-owned `client`; grant consumption and the protected action commit (or roll back) together.
|
|
156
208
|
- Rate/budget/circuit tables are capped like the memory store: `consumeRate`/`readBudget`/`addUsage`/`reserveBudget` accept `maxRateKeys`/`maxBudgetKeys` (the router passes its resolved limits) and evict the least-recently-used row on new-key insert — never the row just inserted, never a budget row holding an active reservation — else fail closed with `ERR_PRISM_MODEL_ROUTER_STATE`. Cleanup prunes expired reservations within its bounded batch.
|
|
157
|
-
- Policy/evaluation/query public contracts stay in their owning packages. This package exports
|
|
209
|
+
- Policy/evaluation/query public contracts stay in their owning packages. This package exports `createPostgresEnterpriseState`, `createPostgresApprovalStore`, `createPostgresErpMessaging`, their options/result/types, and `EnterprisePostgresError`; it has no SQL, DDL, codec, queryable, or migration subpath.
|
|
158
210
|
- The fixed schema has no generic key/value table and no background cleanup scheduler. Schedule `state.cleanup()` from an authorized host job, size its bounded batch for the deployment, and monitor unknown work rows for reconciliation. Run protected integration checks with `PRISM_TEST_POSTGRES_URL="$DATABASE_URL" npm run test:postgres`; the command rejects an absent URL instead of silently skipping database coverage.
|
|
159
211
|
- The OPA adapter (`@arnilo/prism-policy/opa`, 0.0.28) records decisions into the same `state.policy` store unchanged via `evaluateAndAppend` — see [Policy and audit](policy-and-audit.md#opa-external-policy-adapter-arniloprism-policyopa-008).
|
|
160
|
-
- Request-path state SQL uses `SELECT`, `INSERT`, `UPDATE`, and `DELETE` on the
|
|
212
|
+
- Request-path state SQL uses `SELECT`, `INSERT`, `UPDATE`, and `DELETE` on the eight state tables. The open/migration lifecycle additionally needs schema/catalog/advisory-lock and DDL permissions. Use a deployment migration principal for that lifecycle and a least-privilege request role for request traffic; this release intentionally does not ship a migration CLI or worker.
|
|
161
213
|
|
|
162
214
|
## Security and performance notes
|
|
163
215
|
|
|
@@ -165,6 +217,7 @@ export async function recordEnterpriseState(state: PostgresEnterpriseState) {
|
|
|
165
217
|
- Every value is a bound parameter. Schema identifiers are validated; table/index names are fixed. Cursors embed and recheck ownership, so a foreign tenant cannot reuse a page cursor.
|
|
166
218
|
- Policy records cap at 64 KiB; evaluations at 64 KiB; work rows at 8 KiB; router material at 512 bytes. JSON rejects prototype-pollution keys, non-finite values, excess depth/properties, and over-size material.
|
|
167
219
|
- PostgreSQL transaction SQLSTATE `40001`/`40P01` retries whole safe transactions up to three times. Connector effects remain outside those transactions and ambiguous errors become `unknown` rather than being retried.
|
|
220
|
+
- ERP claim pages are tenant-scoped and bounded at 1,000 rows; default batch is 100, lease TTL is 30 seconds with a 5-minute hard cap, and retry attempts are capped at 10. Protected PostgreSQL evidence measured 1,000 queued rows per tenant across 10 tenants at p50 5.999 ms / p95 7.827 ms / p99 8.066 ms for 100-row claims; the representative plan used `prism_erp_outbox_claim_idx` with no sequential scan. This is comparison evidence, not a hardware-independent guarantee.
|
|
168
221
|
- Recorded `postgres:16-alpine` evidence (Node 24.18.0/Linux x64, 10 tenants × 10 principals × 1,000 policy/evaluation rows, 10,000 router keys, 16 clients) stayed below 50 ms p95 for point operations and 100 ms for cursor/cleanup pages. The highest recorded p95 was router-circuit contention at 28.410 ms. Fourteen representative `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` shapes used named indexes with no sequential scans. These are recorded comparison evidence, not hardware-independent guarantees.
|
|
169
222
|
|
|
170
223
|
## Related APIs
|
package/docs/evaluations.md
CHANGED
|
@@ -158,6 +158,51 @@ const page = await state.evaluations.query({ tenantId: "t1", userId: "u1", statu
|
|
|
158
158
|
|
|
159
159
|
Memory evaluation storage remains suitable for development and deterministic tests; it is not cross-replica production storage. PostgreSQL bounds each evaluation row to 64 KiB (reason/error 8 KiB each and metadata 32 KiB).
|
|
160
160
|
|
|
161
|
+
## ERP invariant evals (0.2.7)
|
|
162
|
+
|
|
163
|
+
Plan 027 adds two frozen exports to this package for the deterministic ERP release journey: `erpInvariantDataset` and `createErpInvariantScorers`. Scorers consume **structured journey facts only** — never model prose, credentials, or classified payloads. Each of the eight invariants is a hard 0/1 gate; no weighted average can hide an atomicity or security failure.
|
|
164
|
+
|
|
165
|
+
### Fact schema
|
|
166
|
+
|
|
167
|
+
The protected runner (`scripts/phase27-erp-journey.test.mjs`) carries the journey facts as JSON in `result.text`. Every scorer isolates its facts block and returns 1 only when every required fact is present and truthy:
|
|
168
|
+
|
|
169
|
+
| Invariant (scorer id) | Facts block | Required facts |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `atomic-intent` | `atomic` | `committedAtomically` |
|
|
172
|
+
| `single-local-effect` | `delivery` | `singleLocalEffect`, `duplicateDelivered`, `businessMutationCount` |
|
|
173
|
+
| `compensation-terminal` | `compensation` | `compensated`, `reconciled`, `terminalStatus` |
|
|
174
|
+
| `quorum-provenance` | `quorum` | `distinctApprovers`, `requesterDenied`, `subagentDenied`, `revokedDenied`, `provenance` |
|
|
175
|
+
| `chain-verification` | `chain` | `verified`, `tamperedDetected`, `nextDigest` |
|
|
176
|
+
| `no-leak` | `noLeak` | `classifiedDenied`, `crossTenantDenied`, `secretRedacted` |
|
|
177
|
+
| `fenced-failover` | `fencedFailover` | `resumedByPeer`, `staleWriteRejected`, `cursorPreserved`, `failoverMs` |
|
|
178
|
+
| `restore-equality` | `restore` | `factsMatch`, `digestsMatch`, `drEvidenceFresh`, `restoreMs` |
|
|
179
|
+
|
|
180
|
+
### Hard-gate usage
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import { createErpInvariantScorers, erpInvariantDataset, scoreRun } from "@arnilo/prism-evals";
|
|
184
|
+
|
|
185
|
+
const scorers = createErpInvariantScorers();
|
|
186
|
+
const records = await scoreRun({
|
|
187
|
+
result, // AgentRunResult whose .text is the JSON journey facts
|
|
188
|
+
scorers,
|
|
189
|
+
datasetId: erpInvariantDataset.id,
|
|
190
|
+
});
|
|
191
|
+
if (records.some((record) => record.status !== "scored" || record.score !== 1)) {
|
|
192
|
+
process.exitCode = 1; // a single failing invariant fails the whole gate
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Execution command and substitutes
|
|
197
|
+
|
|
198
|
+
```sh
|
|
199
|
+
# Protected run (requires a disposable PostgreSQL instance):
|
|
200
|
+
PRISM_TEST_POSTGRES_URL=postgresql://... node --test scripts/phase27-erp-journey.test.mjs
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The journey reuses the two-replica failover worker (`scripts/phase27-ha-worker.mjs`) and asserts the comprehensive DR drill evidence (`docs/_evidence/phase27-dr-evidence.json`) is present and not stale. Local substitutes are labelled in the journey evidence and never converted into production claims: an in-memory WORM/SIEM sink (host owns the immutable store in production), in-memory saga checkpoint/lease stores (saga durability is proven in its own suite), and a logical pg-client backup/restore of the ERP tables (comprehensive PITR is in the DR drill evidence). Passing this protected journey **does not** satisfy the 0.3.0 live-service matrix.
|
|
204
|
+
|
|
205
|
+
|
|
161
206
|
## Related APIs
|
|
162
207
|
|
|
163
208
|
- [Agent/session runtime](agent-session-runtime.md): `AgentRunResult` and `session.run()`
|
package/docs/host-security.md
CHANGED
|
@@ -131,6 +131,9 @@ Wire those values where they matter: provider adapters receive the resolved cred
|
|
|
131
131
|
|
|
132
132
|
## Security and performance notes
|
|
133
133
|
|
|
134
|
+
- 0.2.7 (plan 027 Task 8) adds field-level classification and fail-closed redaction at data boundaries: `applyFieldPolicy` walks JSON-like values with explicit `allow`/`redact`/`tokenize`/`deny` decisions, the protected default denies unknown fields on outbound/persisted boundaries, and labels come from per-boundary `labelFor` hints — never auto-discovered. Policy errors, cycles, unsupported types, and budget breaches fail closed without echoing values; the audit seam transforms before canonical hashing and retains only `{path, reason}` provenance; the telemetry seam drops attributes on policy error. See [Data classification and field-level redaction](data-classification.md) for the full contract, boundary matrix, limits, and the recorded overhead vs the pre-existing redaction walk.
|
|
135
|
+
|
|
136
|
+
|
|
134
137
|
- Fail closed: unknown providers, unknown tools, denied tools, invalid tool arguments, missing skill tool dependencies, trust failures, permission failures, append conflicts, and validator failures should stop the unsafe action.
|
|
135
138
|
- Prism does not sandbox host tools, extensions, provider adapters, credential resolvers, or custom middleware. Use OS/container/process isolation when code is untrusted.
|
|
136
139
|
- Redaction is exact known-secret replacement only. It is not arbitrary secret detection, entropy scanning, or DLP.
|
|
@@ -234,3 +237,4 @@ Every durable `AgentEventSource` page/subscribe and tool-effect claim rechecks e
|
|
|
234
237
|
- [Database persistence](database-persistence.md): production schema, ownership, indexes, retention, and adapter readiness checklist.
|
|
235
238
|
- [Enterprise PostgreSQL state](enterprise-postgres-state.md): durable policy/evaluation/work/router state and database-role boundary.
|
|
236
239
|
- [Provider caching](provider-caching.md): cache keys and provider-owned header safety rules.
|
|
240
|
+
- [Data classification and field-level redaction](data-classification.md): the field policy contract, protected default, and fail-closed boundary matrix (plan 027 Task 8).
|
package/docs/index.md
CHANGED
|
@@ -7,7 +7,8 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
|
|
|
7
7
|
|
|
8
8
|
## Identity and governance
|
|
9
9
|
- [Agent identity](agent-identity.md): host-verified `Principal` / `AgentIdentity`, delegation narrowing, ownership projection, and redacted telemetry refs for enterprise runs/tools/MCP/A2A/workflows; optional OIDC/JWKS verifier adapter (`@arnilo/prism-credentials-node/oidc` — pinned issuer/audience/JWKS, bounded claims, fail closed).
|
|
10
|
-
- [Policy and audit](policy-and-audit.md): optional `@arnilo/prism-policy` decision ledger (allow/deny/modify/approval), evidence refs only, cursor-paginated WORM export,
|
|
10
|
+
- [Policy and audit](policy-and-audit.md): optional `@arnilo/prism-policy` decision ledger (allow/deny/modify/approval), evidence refs only, cursor-paginated WORM export, durable PostgreSQL composition, and multi-party approvals — immutable requests with role/quorum requirements, requester/approver separation, expiry, revocation, bounded delegation, rejection, policy-revision pins, and atomic grant consumption via a host `ApprovalAuthority` (NIST AC-5 applied as control guidance, not certification); 0.0.28 adds the OPA REST evaluator (`@arnilo/prism-policy/opa` — pinned SSRF-checked endpoint, redacted input, fail-closed deny, optional bundle-revision pin); 0.2.1 makes the OPA decision fetch DNS-pinned (core `pinnedFetch`).
|
|
11
|
+
- [Signed, hash-chained audit export](audit-export.md): `@arnilo/prism-policy` exports tenant-scoped audit records as signed, hash-chained batches — canonical RFC 8785 envelopes, SHA-256 record chain, host-signed manifests, WORM acknowledgement gating cursor advance, SIEM mirroring with replayable pending status, redaction/legal-hold provenance, and independent `verifyAuditBatch`/CLI verification with no key storage or vendor SDKs in Prism.
|
|
11
12
|
- [Model routing](model-routing.md): optional `@arnilo/prism-model-router` allow-list/residency/budget/rate/circuit/fallback governance with redacted diagnostics; budget admission reserves per-request caps atomically (commit/release/TTL reconciliation); durable state requires awaited identity-scoped calls; host-configurable selection policies (reference cost/latency policy ranks by `ModelCost` then in-memory latency EMA fed from `recordOutcome`).
|
|
12
13
|
|
|
13
14
|
## Agent/session runtime
|
|
@@ -17,6 +18,9 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
|
|
|
17
18
|
- [Guardrails](guardrails.md): typed fail-closed input/output/tool checks with buffered provider output and redacted decision records.
|
|
18
19
|
- [Agent events](agent-events.md): live `session.subscribe` plus durable `AgentEventSource` page/subscribe/resume for cross-replica reconnect; message/progress deltas never create spans. Durable sources: PostgreSQL `LISTEN`/`NOTIFY` (reference, `@arnilo/prism-session-store-postgres` root export) and NATS JetStream (`@arnilo/prism-session-store-nats`, FR-5) with restart-stable durable consumer identity (`prism_<hmac16>`) for cursor resume across crash/restart.
|
|
19
20
|
- [Observability](observability.md): OTel GenAI agent/provider/tool hierarchy, host context parenting, bounded trace linkage, safe evaluation events, controlled metrics, and exporter isolation.
|
|
21
|
+
- [Operations runbook](operations.md): the high-availability/failover runbook for plan 027 Task 6 — LeaseStore/CheckpointStore fencing model, local-registry limitations, uncertain-commit replay rules, the recorded two-replica drill (`scripts/phase27-ha.test.mjs`, evidence in `docs/_evidence/phase27-ha-evidence.json`), failover ceiling, and the prohibition on manual lease unlocks.
|
|
22
|
+
- [Disaster recovery and backup operations](disaster-recovery.md): the plan 027 Task 7 runbook — standard-tool backup/restore/migration-rollback/PITR/DR drill (`scripts/phase27-dr.test.mjs`), guarded commands, app-level verification, the rollback decision tree, and measured RPO/RTO in `docs/_evidence/phase27-dr-evidence.json`.
|
|
23
|
+
- [Data classification and field-level redaction](data-classification.md): the plan 027 Task 8 contract — `applyFieldPolicy` walking JSON-like values with allow/redact/tokenize/deny decisions, the fail-closed protected default, per-boundary `labelFor` hints (no auto-discovery), sparse-copy overhead, and the ERP-T9 leak matrix incl. egress/audit/telemetry seams.
|
|
20
24
|
- [Evaluations](evaluations.md): deterministic and bounded trace/model-judge/pairwise scoring, CI thresholds, OTel trace-reference linkage, coding/browser adversarial fixtures, ID-only linkage to immutable owned run feedback, and optional durable PostgreSQL records.
|
|
21
25
|
- [Runs and usage ledger](runs-and-usage.md): durable run/event/tool/usage persistence, optional bounded FIFO durability policies, session snapshot caching, and immutable run/trace feedback.
|
|
22
26
|
- [Performance limits](performance.md): **0.1.0 capacity envelopes** (frozen performance contract, 24 network-free + 16 protected p95 rows, startup/pack rows), 0.0.26 coding-intelligence/process/forge/egress network-free evidence, 0.0.25 durable-loop/HITL/A2UI network-free evidence, 0.0.24 distributed event/effect PostgreSQL evidence, 0.0.23 enterprise state evidence, 0.0.15 network-free provider/RAG/memory benchmark evidence and frozen caps, bounded evaluation traces/judges/reports, and production sizing assumptions.
|
|
@@ -34,7 +38,7 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
|
|
|
34
38
|
- [Database persistence](database-persistence.md): production persistence contracts, shared checksummed migration/full-shape catalog primitives (`@arnilo/prism/testing/persistence-schema`), conditional append, indexes, `readBranchPath`, reference relational schema, retention/legal-hold/quota lifecycle (`lifecycle`), and NoSQL mapping. `appendSession` gains version/CAS (migration `008_session_version`); durable adapters must pass `assertStateConcurrencyConforms` (`@arnilo/prism/testing/state-concurrency-conformance`: approval/checkpoint-CAS/cursor/idempotency/reservation/conversation-metadata/unknown-outcome probes; memory leg in `npm test`, durable legs in `test:postgres`/`test:nats`, `scripts/phase22-conformance.test.mjs` gate accounting).
|
|
35
39
|
- [SQLite persistence](sqlite-persistence.md): optional `better-sqlite3` adapter with session/run storage, checkpoints/leases, feedback, FTS `searchSessions` (migration-v4), and transactionally verified/backfilled migration metadata.
|
|
36
40
|
- [PostgreSQL persistence](postgres-persistence.md): optional pooled `pg` adapter with session/run/checkpoint/lease/feedback storage, FTS `searchSessions` (migration-v4), advisory-locked checksummed/full-shape migrations, and opt-in live conformance.
|
|
37
|
-
- [Enterprise PostgreSQL state](enterprise-postgres-state.md): optional `@arnilo/prism-enterprise-postgres` composition for durable policy/evaluation/work-idempotency/model-router/`toolEffects` state, exact ownership, checksummed migrations (001-
|
|
41
|
+
- [Enterprise PostgreSQL state](enterprise-postgres-state.md): optional `@arnilo/prism-enterprise-postgres` composition for durable policy/evaluation/work-idempotency/model-router/`toolEffects` state, transactional ERP outbox/inbox messaging, and multi-party approval records (`createPostgresApprovalStore`, migration 005 — one locked row per request, revision-checked terminal transitions, caller-transaction grant consumption), exact tenant ownership, checksummed migrations (001-005; 003 adds router budget reservation slots; 004 adds bounded at-least-once dispatch), and explicit cleanup.
|
|
38
42
|
- [Migration guide](migration.md): **0.1.4 → 0.1.5** documented breaking cut — deprecated-option removal (the inert provider request knobs, `maxToolRounds` alias, observational-memory flat keys/worker aliases, `autoResizeImages`, `INIT_PROVIDERS`) with exact replacement table, before/after examples, and fail-closed refusal behavior; **0.0.28 → 0.1.0** release-candidate hardening (no migration); **0.0.17 → 0.1.0 upgrade matrix** (store compatibility per release line: compatible / tested migration / tested refusal, plus breaking-default callouts); **0.0.27** ACP coding-host interop (capability advertise-when, session modes/config, MCP select, lifecycle events, elicitation); **0.0.26** coding intelligence, managed processes, forge, and safe egress; **0.0.25** durable custom loops and shared human-in-the-loop decisions; **0.0.24** distributed events and recoverable tool effects; **0.0.23** enterprise PostgreSQL state adapters (async router and work reconciliation); **0.0.22** third-party behavior integrations (Caveman, Ponytail); **0.0.21** coding-tool capability gaps (`outputMode`, glob, read-before-write, delete/move, aggregator 9/4); **0.0.20** progressive skill disclosure, empty registry default, `load_skill`, priority budget demotion, optional tool-result fold; **0.0.19** observational memory lifecycle; **0.0.15** OpenAI hosted tools/continuation/Realtime, exact AI SDK v4 matrix, RAG lifecycle/reranking/trust/status, and memory export/rebuild; **0.0.14** conversations, memory consent/lifecycle, artifact co-work review, AG-UI co-work events, scoped M365/GWS OAuth connectors, browser checkpoints, device contracts, and Alibaba/Ollama providers; plus prior release migrations.
|
|
39
43
|
- [Node JSONL session store](node-jsonl-session-store.md): development-only JSONL file adapter for single-process Node hosts; no cross-process safety; `searchSessions` throws `SessionSearchUnsupportedError`.
|
|
40
44
|
- [Persistence, credentials, and multimodality primitives](persistence-credentials-multimodality-primitives.md): Plan 056 inventory — session/run-ledger/persistence contracts, credential/OAuth seams, content/resource/model capabilities, package dependency matrix, conformance matrix, and threat model for production adapters.
|
|
@@ -99,12 +103,13 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
|
|
|
99
103
|
- [Supervisor delegation](supervisors.md): optional explicit child allow-list, derived memory scopes, narrowing-only permissions, lifecycle hooks, nested delegation, cancellation, finite budgets, host-projected delegation telemetry, and separate A2A durable adapter boundary.
|
|
100
104
|
- [A2A interoperability](a2a.md): A2A 1.0 JSON-RPC/HTTPS cards plus host-owned durable task get/list/cancel/subscribe, shared `AgentEventSource` task adapter, bounded rich parts/replay, principal-scoped push configs, exact-origin verified client, rich stream seam for explicit AG-UI fronting, and server-side `createAgUiA2AServer` exposure of a local AG-UI agent (0.0.26).
|
|
101
105
|
- [Frontend interoperability (AG-UI and ACP)](ag-ui.md): optional `@arnilo/prism-ag-ui` full AG-UI 0.0.57 input/event/capability mapper, authorized Web handler/distributed source follow, opt-in A2UI painting middleware, explicit hardened MCP/MCP Apps/remote A2A adapters, a framework-free reference renderer subpath (`@arnilo/prism-ag-ui/renderer`, 0.0.26), and stable ACP sibling over shared redacted event and durable-approval seams; 0.0.14 adds reconnectable co-work events.
|
|
102
|
-
- [ACP coding-host interop](acp.md): stable ACP v1 `createPrismAcpAgent()`/`createAcpEventMapper()` over `@agentclientprotocol/sdk@1.3.0` — capability advertisement is a pure function of host seams (sessions load/list/delete/resume/dirs, close always, prompt media/embedded, MCP http/sse), client fs/terminal adapters, modes and config options as host overlays, `CodingLifecycleEvent` mapping, four-outcome approvals with elicitation, and frozen caps (0.0.27); 0.1.1 adds ownership-scoped persistence guidance for host-persisted modes/config (plan 013 Task 5 — the agent never persists them); 0.1.6 adds the optional host-owned `AcpSessionStore` durability seam — live registry (modes/config/cwd/ownership) survives agent restart, restore is ownership-scoped and fail-closed (plan 018 Task 2). 0.2.6 adds durable run recovery (plan 026 Task 5): bounded `activeRun` refs on persisted sessions, restart re-resolution against `AgentRunLifecycle` (suspended → pending approval ids, terminal → terminal, unprovable in-flight → unknown, never a restarted prompt), and durable ownership/version/fence-checked cancellation that never replays tools; docs/migration.md records the additive-field decision and the 0.2.5 → 0.2.6 downgrade rules.
|
|
106
|
+
- [ACP coding-host interop](acp.md): stable ACP v1 `createPrismAcpAgent()`/`createAcpEventMapper()` over `@agentclientprotocol/sdk@1.3.0` — capability advertisement is a pure function of host seams (sessions load/list/delete/resume/dirs, close always, prompt media/embedded, MCP http/sse), client fs/terminal adapters, modes and config options as host overlays, `CodingLifecycleEvent` mapping, four-outcome approvals with elicitation, and frozen caps (0.0.27); 0.1.1 adds ownership-scoped persistence guidance for host-persisted modes/config (plan 013 Task 5 — the agent never persists them); 0.1.6 adds the optional host-owned `AcpSessionStore` durability seam — live registry (modes/config/cwd/ownership) survives agent restart, restore is ownership-scoped and fail-closed (plan 018 Task 2). 0.2.6 adds durable run recovery (plan 026 Task 5): bounded `activeRun` refs on persisted sessions, restart re-resolution against `AgentRunLifecycle` (suspended → pending approval ids, terminal → terminal, unprovable in-flight → unknown, never a restarted prompt), and durable ownership/version/fence-checked cancellation that never replays tools; docs/migration.md records the additive-field decision and the 0.2.5 → 0.2.6 downgrade rules. 0.2.8 (plan 028) adds `session/load`/`session/resume` transcript replay (bounded, redacted chunks from the `sessions.transcript` seam), truthful `usage_update`, per-type `set_config_option` gates, explicit tool `kind` metadata, run-level error mapping, permission wire alignment, `agent_thought_chunk`, and the spawnable entrypoint; UNSTABLE-gated `plan_update`/`plan_removed` from coding plan lifecycle events (F5, client must advertise `ClientCapabilities.plan`); host-owned `session_info_update` titles and title pass-through in `session/list` (F6, `sessions.title` seam); opt-in `createCodingToolProjection()` for first-party edit/write diffs+locations (F7, deny-by-default unchanged); projected `toolResult` images as ACP content/image blocks (F8, `acpImageBytes` cap); host-owned slash commands as `available_commands_update` (F9, `acpCommandsPerUpdate` cap).
|
|
107
|
+
- [Spawnable ACP agent](acp-agent.md): `@arnilo/prism-acp-agent` — a ~200-line bin serving `createPrismAcpAgent` over stdio from a validated config file (single local user, coding tools bound to one workspace, sqlite/memory session store, MCP allow-list, modes/config options, mock provider by default; 0.2.8 plan 028 Task 10).
|
|
103
108
|
- [AG-UI adoption evaluation](ag-ui-adoption.md): official 0.0.57 input/event/capability matrix and shipped hardened MCP/MCP Apps/A2A handshake boundaries.
|
|
104
109
|
|
|
105
110
|
## CLI/RPC
|
|
106
111
|
- [CLI/RPC](cli-rpc.md): Run print/json modes and LF-delimited RPC over the public AgentSession runtime, including mid-run `steer`, branch-handle results, fixed `forkSession`, and `checkout`. `prism init` scaffolds a tiny TypeScript project with one selected provider and an offline mock test; `prism providers add <name>` scaffolds an OpenAI-compatible provider package (manifest, provider, models, cache helpers, conformance test, docs stub).
|
|
107
|
-
- [Workflows](workflows.md): optional `@arnilo/prism-workflows` typed bounded DAG orchestration — explicit recursive definition revisions, exact-owner cancellation/active identity, finite hard limits, durable human suspend/resume, schedules/background execution, revocable proactive schedule capability tokens, nested workflows, replay, coordination, events, and optional RPC/Web bindings. Compose coding plans/checkpoints via workspace Markdown + `state.coding` without a second runtime. Active-run registry is non-durable, in-process only, with bounded sweep/cap cleanup. Interactive TUI (C-012) deferred.
|
|
112
|
+
- [Workflows](workflows.md): optional `@arnilo/prism-workflows` typed bounded DAG orchestration plus linear durable sagas — explicit recursive definition revisions, exact-owner cancellation/active identity, finite hard limits, durable human suspend/resume, schedules/background execution, revocable proactive schedule capability tokens, nested workflows, replay, coordination, events, saga compensation/reconciliation, and optional RPC/Web bindings. Compose coding plans/checkpoints via workspace Markdown + `state.coding` without a second runtime. Active-run registry is non-durable, in-process only, with bounded sweep/cap cleanup. Interactive TUI (C-012) deferred.
|
|
108
113
|
- [Workflow orchestration primitives](workflow-orchestration-primitives.md): architecture inventory — workflow adapters consume core `CheckpointStore`, `LeaseStore`, and bounded `EventMultiplexer`; run control and optional RPC commands stay package-local.
|
|
109
114
|
- [Workflow/TUI scope](workflow-tui-primitives.md): records why 0.0.5 ships workflow APIs/RPC control but no interactive terminal UI.
|
|
110
115
|
|
|
@@ -129,7 +134,8 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
|
|
|
129
134
|
- [Ponytail behavior integration](ponytail.md): optional `@arnilo/prism-ponytail` — upstream Ponytail skills/commands, `ponytail-mode` injector, session `ponytail-mode` persistence; resolves peer `@dietrichgebert/ponytail` or `upstreamPath`; opt-in (not in code/sdk profiles).
|
|
130
135
|
|
|
131
136
|
## Release and install
|
|
132
|
-
- [Release and install](release-and-install.md): current **0.2.6** 50-package graph (root + 49 workspace packages) — plan 026 the fully-featured coding-agent-readiness cut: **host-selected PTY** (`pty: true` delegates only to the host `ptyBackend`, fails closed as unsupported when absent, bounded resize/TERM/attach caps), **indexed code search** (host-owned incremental index seam with explicit `indexed_literal`/`semantic` modes, literal remains the default, stale/failed/untrusted indexes fail closed `ERR_PRISM_INDEX_*`, results labeled `untrusted_index`), **coding workspaces** (`createCodingWorkspaceLifecycle`: durable CheckpointStore CAS records + LeaseStore fencing, locked worktrees, credential-free fingerprints, cleanup refusal matrix), **durable recovery** (process intent/ACP `activeRun` refs over Postgres/SQLite stores with attach-if-attested `recover()` and durable fence-checked cancellation, never fabricated exits), **patch review and diagnostics** (`createCodingPatchReviewManifest` + `assertCodingPatchAccepted` with pending/accepted/rejected/superseded bound to digest + revision + identity, opt-in LSP `syncDocument`/`diagnosticDelta`), and the **protected real coding journey** (packed consumer through real provider/Docker/Postgres/GitHub/Playwright/PTY services with retained evidence report; forge breadth GitLab/Bitbucket stays demand-gated); then plan 025 the maintainability-and-bounded-performance cut: **god-module splits** (the six remaining implementation monoliths — `src/contracts-core.ts` 1,719 L, `src/agent-session.ts` 2,049 L, `workflows/src/run.ts` 1,227 L, `server/src/handler.ts` 1,005 L, `coding-agent/src/repository.ts` 974 L, `ag-ui/src/acp/agent.ts` 836 L — split into cohesive family files behind preserved barrels, compat-preserving with zero breaking deltas, no `exports`-map subpath, `RuntimeAgentSession` kept as one class with a recorded reason), **persistence-mechanics dedup** (21 pure ownership/cursor/checkpoint/lifecycle/search helpers moved into the dependency-free `session-store-codecs`; postgres/sqlite adapters shrank 273 lines; SQL dialect stays per-adapter; no schema/shape change; cross-store conformance green), **bounded accumulation removed** (per-push `Buffer.concat` in language framing + tar parsing → chunk-array readers; framing ~100–200× faster at 4,000 chunks, tar linear at 8 MiB, caps fail-closed byte-identical; CLI `collectOutput` audited already linear), **dead-code cleanup internal-only** (62 candidates triaged: 2 internal removals + 60 allow-listed in `docs/_evidence/phase25-dead-exports-triage.md`), and **coverage close** (76 behavior-backed regressions; core 90.53/84.20/90.54 → 91.43/84.80/91.60); additive-only compat (105 helper exports), no migration; then plan 024 the package-documentation-and-compatibility-truth cut: **umbrella wording matches manifests** (`@arnilo/prism-providers` installs 11 of 14 provider adapters — Azure/Bedrock/Vertex are added separately by `prism-all`; `prism-all` installs 20 direct / 43 transitive packages and omits document-reader, OpenAPI tools, NATS, Caveman, Ponytail; membership unchanged in 0.2.x), **manifest-derived package truth** (`scripts/package-truth.mjs` → `scripts/package-truth.json` is the single source for counts, provider membership, and closures; docs literals regenerate from it and drift fails the gates), **peer-version policy Decision A** (exact `@arnilo/prism: 0.2.4` pins, atomic-upgrade rule, ERESOLVE refusal for partial upgrades, `^1.0.0` widening at 1.x), and **current-line truth** (`docs/0.1.0-readiness.md` at the 0.2.x line with 0.1.7 as the terminal 0.1.x baseline); no runtime contract delta (compat gate at 0.2.4: version literal only), no migration; then plan 023 the build-coverage-and-release-evidence-integrity cut: **build serialization** (dependency-free `scripts/with-build-lock.mjs` — one O_EXCL lockfile at `node_modules/.prism-build.lock` serializing every emit/test leaf so concurrent compilers can never expose a partial live `dist/`, stale-PID reclaim, env-overridable `PRISM_BUILD_LOCK_TIMEOUT_MS`, fail-closed; documented direct-`tsc` caveat), **corrected workspace coverage denominators** (package-local `--test-coverage-include=dist/**` so imported core `dist` no longer pollutes workspace rows — `mcp` 45.47→90.25, `rag` 19.70→94.82; evidence-based per-package thresholds in `scripts/coverage-thresholds.json` with `protectedException` for durable-leg packages shown separately, machine-readable `scripts/coverage-summary.json`), **machine-auditable release skip manifest** (`scripts/release-skip-manifest.mjs` → `scripts/release-evidence.json`: every surface recorded `pass`/`skip`/`blocked`/`protected` with reason and required env; the 33 protected/live skips named; a required surface without evidence records `blocked` and fails the release gate fail-closed — missing credentials/services can never convert into a green release), and **stabilized quality gates** (Biome 2.x `preset` config migration with zero lint diagnostics, the racy 150ms MCP bridge timing assert replaced by a deterministic barrier, load-sensitive guards carry documented `ponytail:` ceilings, machine-readable `lint-report.sarif` + `unused-report.json` retained by CI); no runtime contract delta (compat gate at 0.2.3: version literal only), no migration; then plan 022 the concurrent-state-and-durability-integrity cut: atomic model-budget reservation (`ModelRouterStateStore.reserveBudget`/`commitBudget`/`releaseBudget` with fencing tokens, `reservationTtlMs` expiry and unknown-usage reconciliation, rate/budget key-map caps with LRU eviction that never drops a held reservation), atomic conversation metadata (`SessionRecord.version` + `appendSession` `expectedVersion` CAS across Postgres/SQLite — create-only `0`, exact-version `N>0`, legacy last-write-wins when omitted; `SessionMetadataConflictError` `metadata_conflict` with versions only, HTTP 409; concurrent create/branch/archive single-statement with branch caps inside the CAS, archive wins, deleted rows never resurrect), single-consumer `EventMultiplexer` (`EventMultiplexerError` `ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER` instead of silent queue sharing), restart-stable NATS durable consumer identity (`prism_<hmac16>` with no random suffix — crash-resumed subscribe continues from the last ack, orphaned 0.2.1 consumers reclaimed on clean stop), and bounded non-durable active-run registries (sweep + fail-closed 512 cap `ERR_PRISM_WORKFLOW_RUN_REGISTRY_OVERFLOW`); new regression surface `scripts/phase22-security.test.mjs` (4 blockers + gate accounting over built public entrypoints) + packed plain-JS `security22.mjs` consumer + the `@arnilo/prism/testing/state-concurrency-conformance` harness (7 probes across memory/Postgres/SQLite/NATS legs, no timing-only sleeps) + the `scripts/phase22-conformance.test.mjs` gate; additive-only compat (new exports only, no removals); forward-only migrations 008 (`prism_sessions.version`) and 003 (`prism_model_router_budgets.reservations`); migration `0.2.1 → 0.2.2`; then plan 021 the provider-completion-and-outbound-trust-boundaries cut: strict stream completion is the shared OpenAI-compatible default (truncated streams fail `incomplete_delta`, explicit `strictCompletion: false` opt-out), bounded success bodies via `readBoundedResponseJson` on all discovery/quota/embeddings/upload/OAuth JSON endpoints (65,536-byte ceiling, depth/property/shape caps), DNS-pinned OIDC JWKS/OPA/content fetches through the core `pinnedFetch` primitive with 3xx redirects rejected outright (private/metadata answers fail closed `ssrf_denied`), shared bounded OAuth device/token polling (`pollDeviceCodeToken`) across provider-openai and credentials-node, and the four edge fixes (Azure/Vertex credential-once, Bedrock duplicate-case/repeated-query SigV4 canonicalization, OpenAI upload failed-DELETE retention, cache `__overflow__` tokens-only); public-entrypoint threat-suite `scripts/phase21-security.test.mjs` + packed plain-JS consumer; additive-only compat (MCP transport helpers re-exported from core, no removals); migration `0.2.0 → 0.2.1`; then plan 020 the fail-closed runtime-and-sandbox-security cut on the 0.2.x review-remediation line: durable-resume decision validation in core (`assertValidAgentRunResume` — unknown decisions/malformed batches fail closed with `ERR_PRISM_DECISION_*` before any state claim, checkpoint write, or tool execution; server parser remains defense in depth), isolated work-tool subprocess environments (`@arnilo/prism-work-tools` — fixed base allow-list + explicit env + forced HOME/telemetry + late-bound per-identity tokens, 64-name/64-KiB caps, absolute binary/configDir, linear output capture), and explicit sandbox capabilities (`@arnilo/prism-coding-security` — `SandboxAdapter.capabilities` with omission-is-false fail-closed resolution, `SandboxCodingComposition.capabilities` from verified wiring, `containmentClaim` deprecated as the conservative projection; Docker reports only verified controls, native reports filesystem/process/privilege `false`); public-entrypoint security conformance (`scripts/phase20-security.test.mjs`, wired into `security:threat-suites`), packed plain-JS consumer regressions, and the sandbox-browser workflow's fail-loud Docker/native capability evidence gate — 0.2.0 never ships while a blocker is skipped; migration and rollback notes in `docs/migration.md` `0.1.7 → 0.2.0`, store-compatible with 0.1.7 in both directions; 0.1.7 was the performance-and-DX patch — dependency-free `createCacheTelemetry()` per-provider/model cache hit/miss aggregator (bounded cardinality with `__overflow__`, token counters/rates only, host-activated), host-configurable `ModelRouterSelectionPolicy` on `createModelRouter` with the reference `createCostLatencySelection` (ModelCost rank then in-memory latency EMA, default ordered behavior byte-identical), `prism providers add <name>` OpenAI-compatible provider scaffold (manifest/provider/models/cache/conformance test/docs stub, npm-name + traversal + symlink-escape validation, placeholders only), and the async `AgUiProjection` verification closeout (plan 009 Task 15 evidence recorded, no new code); plan 017 the documented breaking cut — deprecated-option removal with `docs/migration.md` `0.1.4 → 0.1.5` section and reviewed compat-baseline regeneration via `--allow-break` then `--update-baseline`: the inert provider request knobs, `RunOptions.maxToolRounds`, observational-memory flat settings keys + top-level worker aliases, `ReadToolOptions.autoResizeImages`, `INIT_PROVIDERS`; all removals fail closed naming their replacement; plan 016 internal god-module split — `agents.ts`/`contracts.ts` reorganized behind barrel re-exports with a byte-identical public entry surface, measured tree-shaking improvement in `scripts/phase16-baseline.json`, and additive `@arnilo/prism-browser` Chrome DevTools Protocol capabilities — `browser_evaluate`/`browser_observe` and `block_urls`/`unblock_urls`/`throttle`/`emulate` act actions; plan 015 dead-code and deprecation hygiene on the frozen 0.1.x line — parameterized benchmark runner `scripts/benchmark.mjs` absorbing the per-version runners, archived review-coverage evidence in `docs/_evidence/`, non-blocking unused-code sweep `npm run sweep:unused`, opt-in checkpoint persistence for loaded-skill names and read-path sets; plan 014 Alibaba provider enrichment — embeddings, video input, verified compatible-mode surface decision table; plan 013 post-release hardening — build single-flight, MCP SSE relay test, combined coverage summary, canonical manifest-count narrative, ACP modes/config persistence guidance; Phase 12 release-candidate hardening; plan 012 — freeze manifest, compatibility matrix, upgrade matrix, packed-install e2e journeys, restart-recovery evidence, capacity envelopes, security policy), exact-peer/install/tarball rules, deterministic resumable publication and publish dry-run, frozen 0.1.x compatibility and support matrix (Node/PostgreSQL/platform/provider/protocol pins and unsupported combinations, machine-checked against `scripts/phase12-freeze-manifest.json`), protected PostgreSQL gate, pinned supply-chain gates, offline tests, the 0.0.15 provider/AI-SDK/RAG/memory protected live-canary matrix, and sandbox-browser Docker/Playwright gates. 0.2.6 (plan 026 Task 7) adds the protected coding journey: `scripts/phase26-coding-journey.test.mjs` runs a packed consumer through real provider calls, a digest-pinned Docker sandbox, the durable Postgres worktree lifecycle, provider-driven ACP edits with policy approval, named checks with `diagnosticDelta`, patch review over the server ArtifactService, cross-replica process recovery, durable cancellation, real GitHub PR push/reconcile/cleanup, host Playwright inspection, and the host PTY adapter (frozen profile) — the retained `scripts/phase26-coding-journey-report.json` gates release evidence (pass/blocked/protected, never a passing skip).
|
|
137
|
+
- [0.2.7 Task 0 scope evidence](release-0.2.7-evidence.md): frozen ERP primitives, demand decisions, threat mappings, budgets, protected-gate policy, and API ownership; not a production-readiness claim.
|
|
138
|
+
- [Release and install](release-and-install.md): current **0.2.8** 51-package graph (root + 50 workspace packages) — plan 026 the fully-featured coding-agent-readiness cut: **host-selected PTY** (`pty: true` delegates only to the host `ptyBackend`, fails closed as unsupported when absent, bounded resize/TERM/attach caps), **indexed code search** (host-owned incremental index seam with explicit `indexed_literal`/`semantic` modes, literal remains the default, stale/failed/untrusted indexes fail closed `ERR_PRISM_INDEX_*`, results labeled `untrusted_index`), **coding workspaces** (`createCodingWorkspaceLifecycle`: durable CheckpointStore CAS records + LeaseStore fencing, locked worktrees, credential-free fingerprints, cleanup refusal matrix), **durable recovery** (process intent/ACP `activeRun` refs over Postgres/SQLite stores with attach-if-attested `recover()` and durable fence-checked cancellation, never fabricated exits), **patch review and diagnostics** (`createCodingPatchReviewManifest` + `assertCodingPatchAccepted` with pending/accepted/rejected/superseded bound to digest + revision + identity, opt-in LSP `syncDocument`/`diagnosticDelta`), and the **protected real coding journey** (packed consumer through real provider/Docker/Postgres/GitHub/Playwright/PTY services with retained evidence report; forge breadth GitLab/Bitbucket stays demand-gated); then plan 025 the maintainability-and-bounded-performance cut: **god-module splits** (the six remaining implementation monoliths — `src/contracts-core.ts` 1,719 L, `src/agent-session.ts` 2,049 L, `workflows/src/run.ts` 1,227 L, `server/src/handler.ts` 1,005 L, `coding-agent/src/repository.ts` 974 L, `ag-ui/src/acp/agent.ts` 836 L — split into cohesive family files behind preserved barrels, compat-preserving with zero breaking deltas, no `exports`-map subpath, `RuntimeAgentSession` kept as one class with a recorded reason), **persistence-mechanics dedup** (21 pure ownership/cursor/checkpoint/lifecycle/search helpers moved into the dependency-free `session-store-codecs`; postgres/sqlite adapters shrank 273 lines; SQL dialect stays per-adapter; no schema/shape change; cross-store conformance green), **bounded accumulation removed** (per-push `Buffer.concat` in language framing + tar parsing → chunk-array readers; framing ~100–200× faster at 4,000 chunks, tar linear at 8 MiB, caps fail-closed byte-identical; CLI `collectOutput` audited already linear), **dead-code cleanup internal-only** (62 candidates triaged: 2 internal removals + 60 allow-listed in `docs/_evidence/phase25-dead-exports-triage.md`), and **coverage close** (76 behavior-backed regressions; core 90.53/84.20/90.54 → 91.43/84.80/91.60); additive-only compat (105 helper exports), no migration; then plan 024 the package-documentation-and-compatibility-truth cut: **umbrella wording matches manifests** (`@arnilo/prism-providers` installs 11 of 14 provider adapters — Azure/Bedrock/Vertex are added separately by `prism-all`; `prism-all` installs 20 direct / 43 transitive packages and omits document-reader, OpenAPI tools, NATS, Caveman, Ponytail; membership unchanged in 0.2.x), **manifest-derived package truth** (`scripts/package-truth.mjs` → `scripts/package-truth.json` is the single source for counts, provider membership, and closures; docs literals regenerate from it and drift fails the gates), **peer-version policy Decision A** (exact `@arnilo/prism: 0.2.4` pins, atomic-upgrade rule, ERESOLVE refusal for partial upgrades, `^1.0.0` widening at 1.x), and **current-line truth** (`docs/0.1.0-readiness.md` at the 0.2.x line with 0.1.7 as the terminal 0.1.x baseline); no runtime contract delta (compat gate at 0.2.4: version literal only), no migration; then plan 023 the build-coverage-and-release-evidence-integrity cut: **build serialization** (dependency-free `scripts/with-build-lock.mjs` — one O_EXCL lockfile at `node_modules/.prism-build.lock` serializing every emit/test leaf so concurrent compilers can never expose a partial live `dist/`, stale-PID reclaim, env-overridable `PRISM_BUILD_LOCK_TIMEOUT_MS`, fail-closed; documented direct-`tsc` caveat), **corrected workspace coverage denominators** (package-local `--test-coverage-include=dist/**` so imported core `dist` no longer pollutes workspace rows — `mcp` 45.47→90.25, `rag` 19.70→94.82; evidence-based per-package thresholds in `scripts/coverage-thresholds.json` with `protectedException` for durable-leg packages shown separately, machine-readable `scripts/coverage-summary.json`), **machine-auditable release skip manifest** (`scripts/release-skip-manifest.mjs` → `scripts/release-evidence.json`: every surface recorded `pass`/`skip`/`blocked`/`protected` with reason and required env; the 33 protected/live skips named; a required surface without evidence records `blocked` and fails the release gate fail-closed — missing credentials/services can never convert into a green release), and **stabilized quality gates** (Biome 2.x `preset` config migration with zero lint diagnostics, the racy 150ms MCP bridge timing assert replaced by a deterministic barrier, load-sensitive guards carry documented `ponytail:` ceilings, machine-readable `lint-report.sarif` + `unused-report.json` retained by CI); no runtime contract delta (compat gate at 0.2.3: version literal only), no migration; then plan 022 the concurrent-state-and-durability-integrity cut: atomic model-budget reservation (`ModelRouterStateStore.reserveBudget`/`commitBudget`/`releaseBudget` with fencing tokens, `reservationTtlMs` expiry and unknown-usage reconciliation, rate/budget key-map caps with LRU eviction that never drops a held reservation), atomic conversation metadata (`SessionRecord.version` + `appendSession` `expectedVersion` CAS across Postgres/SQLite — create-only `0`, exact-version `N>0`, legacy last-write-wins when omitted; `SessionMetadataConflictError` `metadata_conflict` with versions only, HTTP 409; concurrent create/branch/archive single-statement with branch caps inside the CAS, archive wins, deleted rows never resurrect), single-consumer `EventMultiplexer` (`EventMultiplexerError` `ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER` instead of silent queue sharing), restart-stable NATS durable consumer identity (`prism_<hmac16>` with no random suffix — crash-resumed subscribe continues from the last ack, orphaned 0.2.1 consumers reclaimed on clean stop), and bounded non-durable active-run registries (sweep + fail-closed 512 cap `ERR_PRISM_WORKFLOW_RUN_REGISTRY_OVERFLOW`); new regression surface `scripts/phase22-security.test.mjs` (4 blockers + gate accounting over built public entrypoints) + packed plain-JS `security22.mjs` consumer + the `@arnilo/prism/testing/state-concurrency-conformance` harness (7 probes across memory/Postgres/SQLite/NATS legs, no timing-only sleeps) + the `scripts/phase22-conformance.test.mjs` gate; additive-only compat (new exports only, no removals); forward-only migrations 008 (`prism_sessions.version`) and 003 (`prism_model_router_budgets.reservations`); migration `0.2.1 → 0.2.2`; then plan 021 the provider-completion-and-outbound-trust-boundaries cut: strict stream completion is the shared OpenAI-compatible default (truncated streams fail `incomplete_delta`, explicit `strictCompletion: false` opt-out), bounded success bodies via `readBoundedResponseJson` on all discovery/quota/embeddings/upload/OAuth JSON endpoints (65,536-byte ceiling, depth/property/shape caps), DNS-pinned OIDC JWKS/OPA/content fetches through the core `pinnedFetch` primitive with 3xx redirects rejected outright (private/metadata answers fail closed `ssrf_denied`), shared bounded OAuth device/token polling (`pollDeviceCodeToken`) across provider-openai and credentials-node, and the four edge fixes (Azure/Vertex credential-once, Bedrock duplicate-case/repeated-query SigV4 canonicalization, OpenAI upload failed-DELETE retention, cache `__overflow__` tokens-only); public-entrypoint threat-suite `scripts/phase21-security.test.mjs` + packed plain-JS consumer; additive-only compat (MCP transport helpers re-exported from core, no removals); migration `0.2.0 → 0.2.1`; then plan 020 the fail-closed runtime-and-sandbox-security cut on the 0.2.x review-remediation line: durable-resume decision validation in core (`assertValidAgentRunResume` — unknown decisions/malformed batches fail closed with `ERR_PRISM_DECISION_*` before any state claim, checkpoint write, or tool execution; server parser remains defense in depth), isolated work-tool subprocess environments (`@arnilo/prism-work-tools` — fixed base allow-list + explicit env + forced HOME/telemetry + late-bound per-identity tokens, 64-name/64-KiB caps, absolute binary/configDir, linear output capture), and explicit sandbox capabilities (`@arnilo/prism-coding-security` — `SandboxAdapter.capabilities` with omission-is-false fail-closed resolution, `SandboxCodingComposition.capabilities` from verified wiring, `containmentClaim` deprecated as the conservative projection; Docker reports only verified controls, native reports filesystem/process/privilege `false`); public-entrypoint security conformance (`scripts/phase20-security.test.mjs`, wired into `security:threat-suites`), packed plain-JS consumer regressions, and the sandbox-browser workflow's fail-loud Docker/native capability evidence gate — 0.2.0 never ships while a blocker is skipped; migration and rollback notes in `docs/migration.md` `0.1.7 → 0.2.0`, store-compatible with 0.1.7 in both directions; 0.1.7 was the performance-and-DX patch — dependency-free `createCacheTelemetry()` per-provider/model cache hit/miss aggregator (bounded cardinality with `__overflow__`, token counters/rates only, host-activated), host-configurable `ModelRouterSelectionPolicy` on `createModelRouter` with the reference `createCostLatencySelection` (ModelCost rank then in-memory latency EMA, default ordered behavior byte-identical), `prism providers add <name>` OpenAI-compatible provider scaffold (manifest/provider/models/cache/conformance test/docs stub, npm-name + traversal + symlink-escape validation, placeholders only), and the async `AgUiProjection` verification closeout (plan 009 Task 15 evidence recorded, no new code); plan 017 the documented breaking cut — deprecated-option removal with `docs/migration.md` `0.1.4 → 0.1.5` section and reviewed compat-baseline regeneration via `--allow-break` then `--update-baseline`: the inert provider request knobs, `RunOptions.maxToolRounds`, observational-memory flat settings keys + top-level worker aliases, `ReadToolOptions.autoResizeImages`, `INIT_PROVIDERS`; all removals fail closed naming their replacement; plan 016 internal god-module split — `agents.ts`/`contracts.ts` reorganized behind barrel re-exports with a byte-identical public entry surface, measured tree-shaking improvement in `scripts/phase16-baseline.json`, and additive `@arnilo/prism-browser` Chrome DevTools Protocol capabilities — `browser_evaluate`/`browser_observe` and `block_urls`/`unblock_urls`/`throttle`/`emulate` act actions; plan 015 dead-code and deprecation hygiene on the frozen 0.1.x line — parameterized benchmark runner `scripts/benchmark.mjs` absorbing the per-version runners, archived review-coverage evidence in `docs/_evidence/`, non-blocking unused-code sweep `npm run sweep:unused`, opt-in checkpoint persistence for loaded-skill names and read-path sets; plan 014 Alibaba provider enrichment — embeddings, video input, verified compatible-mode surface decision table; plan 013 post-release hardening — build single-flight, MCP SSE relay test, combined coverage summary, canonical manifest-count narrative, ACP modes/config persistence guidance; Phase 12 release-candidate hardening; plan 012 — freeze manifest, compatibility matrix, upgrade matrix, packed-install e2e journeys, restart-recovery evidence, capacity envelopes, security policy), exact-peer/install/tarball rules, deterministic resumable publication and publish dry-run, frozen 0.1.x compatibility and support matrix (Node/PostgreSQL/platform/provider/protocol pins and unsupported combinations, machine-checked against `scripts/phase12-freeze-manifest.json`), protected PostgreSQL gate, pinned supply-chain gates, offline tests, the 0.0.15 provider/AI-SDK/RAG/memory protected live-canary matrix, and sandbox-browser Docker/Playwright gates. 0.2.6 (plan 026 Task 7) adds the protected coding journey: `scripts/phase26-coding-journey.test.mjs` runs a packed consumer through real provider calls, a digest-pinned Docker sandbox, the durable Postgres worktree lifecycle, provider-driven ACP edits with policy approval, named checks with `diagnosticDelta`, patch review over the server ArtifactService, cross-replica process recovery, durable cancellation, real GitHub PR push/reconcile/cleanup, host Playwright inspection, and the host PTY adapter (frozen profile) — the retained `scripts/phase26-coding-journey-report.json` gates release evidence (pass/blocked/protected, never a passing skip).
|
|
133
139
|
- [0.1.0 / 1.0 readiness gates](0.1.0-readiness.md): command-per-gate 1.0 readiness table — frozen API surface + compat gate, migration/docs tripwires, budget table, live-suite matrix, security matrix, current-line status (**0.2.5** current line; 0.1.7 terminal 0.1.x baseline), signed-publication/live-canary prerequisites for 1.0, and Phase 12 demand-evidence entry criteria.
|
|
134
140
|
- [Review coverage archive](_evidence/): per-phase evidence freezes (plans 067–079, releases 0.0.4–0.0.16) — traceability matrices, provider validation, capability/primitive/limit matrices, benchmark budgets, and artifact-diet findings; tarball-excluded, kept in-repo for audit.
|
|
135
141
|
|
package/docs/migration.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Migration guide
|
|
2
2
|
|
|
3
|
+
## 0.2.7 → 0.2.8 ACP adoption fixes (additive)
|
|
4
|
+
|
|
5
|
+
Release **0.2.8** (plan 028) tightens ACP coding-host interop and adds the spawnable `@arnilo/prism-acp-agent` entrypoint. **Additive-only: no exported declaration removed or changed, no persisted 0.2.7 shape repurposed.**
|
|
6
|
+
|
|
7
|
+
Hosts that already speak ACP should re-check these wire behaviors (deny-by-default unchanged unless a new seam is wired):
|
|
8
|
+
|
|
9
|
+
- `usage_update` is omitted when the host cannot report a context window (never `size = used`).
|
|
10
|
+
- A terminal run `error` rejects `session/prompt` with `ERR_PRISM_ACP_RUN` instead of an `Agent error:` transcript chunk.
|
|
11
|
+
- Only boolean config options are advertised; `set_config_option` on a select option fails `ERR_PRISM_ACP_CAPABILITY`.
|
|
12
|
+
- Permission option kinds on the wire are `allow_once` / `allow_always` / `reject_once` / `reject_always`.
|
|
13
|
+
- New optional seams (`sessions.transcript`, `sessions.title`, `commands.list`, `capabilities.usage.contextWindow`, `createCodingToolProjection`, image `toolResult`) emit nothing when unwired.
|
|
14
|
+
|
|
15
|
+
No store migration. Rollback = restore the 0.2.7 manifests/tag. The added exports and `@arnilo/prism-acp-agent` simply disappear.
|
|
16
|
+
|
|
17
|
+
## 0.2.6 → 0.2.7 enterprise ERP production readiness (additive)
|
|
18
|
+
|
|
19
|
+
Release **0.2.7** (plan 027) adds the enterprise ERP production-readiness primitives behind optional host-activated seams: the transactional outbox/inbox + bounded dispatcher, the durable saga compensation/reconciliation engine, multi-party separation-of-duties approvals, signed hash-chained audit export with WORM/SIEM sinks, field-level classification + fail-closed redaction, and the deterministic ERP invariant evals. **Additive-only: no exported declaration removed or changed, no persisted 0.2.6 shape repurposed.**
|
|
20
|
+
|
|
21
|
+
New ERP tables use **separate forward-only migrations** (no down migrations exist; production rollback is roll-forward repair only):
|
|
22
|
+
|
|
23
|
+
- `prism_erp_outbox` / `prism_erp_inbox` (migration `004_erp_messaging`, version 4) — transactional outbox/inbox with `FOR UPDATE SKIP LOCKED` claim, `ON CONFLICT DO NOTHING` idempotent append, claim-token CAS, and three partial indexes. Outbox append must run in the caller-owned `PoolClient` transaction with the business mutation (atomicity is the host's responsibility).
|
|
24
|
+
- `prism_erp_approvals` (migration `005_erp_approvals`, version 5) — multi-party approval requests with decisions stored as JSONB, `FOR UPDATE` row locking for atomic quorum recomputation, rejection as any-party veto, expiry checked at every protected transition, and atomic grant consumption in the host transaction.
|
|
25
|
+
|
|
26
|
+
Saga state persists as a surrogate `WorkflowCheckpointRecord` through the existing `WorkflowCheckpointAdapter` (private workflow id `__prism_saga__/<key>`) — no saga-specific SQL or 0.2.6 shape is repurposed. Audit export, field policy, and ERP invariant evals are stateless or in-memory and add no persisted shape. Secret-manager adapters (Vault/AWS/Azure/GCP) stay **deferred** behind the demand gate; no adapter ships and no ambient credential discovery is added.
|
|
27
|
+
|
|
28
|
+
**Rollback notes.** Rollback = restore the 0.2.6 manifests/tag. The two new ERP migrations are forward-only; before downgrading, stop all 0.2.7 workers (outbox dispatcher, saga engine, audit exporter) and drop or ignore the `prism_erp_outbox`/`prism_erp_inbox`/`prism_erp_approvals` tables (they hold no 0.2.6 data). No 0.2.6 persisted shape changed, so an ordinary downgrade is store-safe; the added exports and ERP tables simply disappear. **"ERP production ready" remains blocked until the 0.3.0 live-service matrix is recorded** — this release adds the primitives and the protected journey evidence, not the live-service matrix.
|
|
29
|
+
|
|
3
30
|
## 0.2.5 → 0.2.6 durable recovery, workspaces, and coding-agent readiness (additive)
|
|
4
31
|
|
|
5
32
|
Release **0.2.6** (plan 026) adds the coding-agent readiness capabilities behind optional host-activated seams: host-selected PTY backends, the indexed/semantic repository-search seam, the ownership-scoped multi-repository/worktree lifecycle, durable process/ACP recovery, and the patch-review/diagnostics workflow. **Additive-only: no exported declaration removed or changed, no persisted 0.2.5 shape repurposed.**
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Operations runbook: high availability, failover, and fencing
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
This page is the operator runbook for the high-availability story proven by plan 027 Task 6: two replicas can serve, inspect, cancel, resume, and reconcile durable ACP/workflow/saga/outbox/export operations after either replica dies. Correctness never depends on a dead process's in-memory registry — it comes from the durable `LeaseStore` (owner, token, fencing counter, expiry, renewal), the `CheckpointStore` (version CAS plus monotonic fencing token), and idempotent side-effect sinks such as the ERP outbox (`ON CONFLICT DO NOTHING` on a stable message id). The two-process proof is `scripts/phase27-ha-worker.mjs` orchestrated by `scripts/phase27-ha.test.mjs`, which records exact commands, process IDs, injected failures, timings, and durable final states in `docs/_evidence/phase27-ha-evidence.json`.
|
|
6
|
+
|
|
7
|
+
## When to use it
|
|
8
|
+
|
|
9
|
+
- Before running any multi-replica deployment of the server, workflow coordinator, saga runner, ACP host, or enterprise dispatcher — read the local-registry limitations and the lease/fence model.
|
|
10
|
+
- When an operator or on-call engineer sees a hung lease, an uncertain commit, or a split-brain suspicion: follow "Failover procedure" and "Uncertain commits" below before touching anything.
|
|
11
|
+
- When sizing leases: the failover ceiling is lease TTL plus the peer's acquisition poll interval; the drill asserts `failoverMs <= ttlMs + 5000`.
|
|
12
|
+
|
|
13
|
+
## Inputs / request
|
|
14
|
+
|
|
15
|
+
- A `LeaseStore` and `CheckpointStore` backed by the same durable store (PostgreSQL through `createPostgresPersistence`, or the corresponding production adapter). Lease keys carry an ownership scope (tenant/account/user) that is part of the trust boundary.
|
|
16
|
+
- Operations that need a leader: `acquireLease({ namespace, key, ownerId, ttlMs })` returns a lease with `token` and monotonic `fencingToken`, or `null` while another owner holds it.
|
|
17
|
+
- Durable progress: `saveCheckpoint({ namespace, key, version, expectedVersion, fencingToken, value })` — versions strictly increase, `expectedVersion` must match the current version, and a lower or absent fencing token can never replace a fenced record.
|
|
18
|
+
- Idempotent side effects: give every external effect a stable id (ERP outbox `messageId` is the designed carrier) so replay is safe.
|
|
19
|
+
|
|
20
|
+
## Outputs / response / events
|
|
21
|
+
|
|
22
|
+
- A lease record: `{ namespace, key, ownerId, token, fencingToken, acquiredAt, expiresAt, updatedAt }`. Expired rows retain their fencing counter; the next owner inherits `fencingToken + 1`.
|
|
23
|
+
- A checkpoint record: `{ namespace, key, version, fencingToken?, value, createdAt, updatedAt }`. Cursor/value changes are CAS-committed; a peer can replay an unfinished step but can never skip ahead or move the cursor backward.
|
|
24
|
+
- Failover timing: the drill reports `failoverMs` (wall time between the owner's death and the peer's acquisition) and asserts it against the frozen ceiling.
|
|
25
|
+
|
|
26
|
+
## Request/response example
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
const lease = await stores.leases.tryAcquireLease({
|
|
30
|
+
namespace: "erp.ops", key: "invoice-42", ownerId: "worker-b", ttlMs: 30_000,
|
|
31
|
+
});
|
|
32
|
+
if (!lease) return "another replica owns invoice-42";
|
|
33
|
+
await stores.checkpoints.saveCheckpoint({
|
|
34
|
+
namespace: "erp.ops", key: "invoice-42",
|
|
35
|
+
version: 3, expectedVersion: 2, fencingToken: lease.fencingToken,
|
|
36
|
+
value: { cursor: 2, steps: ["reserve", "charge"] },
|
|
37
|
+
});
|
|
38
|
+
// Side effect with a stable id (idempotent replay):
|
|
39
|
+
await outbox.append(client, { tenantId, messageId: "pay-t/invoice-42/charge", topic: "erp.payment.requested", payload });
|
|
40
|
+
await stores.leases.releaseLease({ namespace: "erp.ops", key: "invoice-42", ownerId: "worker-b", token: lease.token });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Implementation example
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
# Protected two-replica drill (requires PRISM_TEST_POSTGRES_URL; Docker image
|
|
47
|
+
# postgres:16-alpine is the local stand-in). Recorded evidence lands in
|
|
48
|
+
# docs/_evidence/phase27-ha-evidence.json.
|
|
49
|
+
`PRISM_TEST_POSTGRES_URL` set to the protected connection string (locally a disposable `postgres:16-alpine` container): `node --test scripts/phase27-ha.test.mjs`
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The drill: worker A acquires, heartbeats, commits the charge effect into the
|
|
53
|
+
outbox, and is SIGKILLed inside the window between the effect commit and its
|
|
54
|
+
final cursor save. Worker B (separate process, own pool, no access to A's
|
|
55
|
+
registry) reads the durable state, waits out the lease expiry, acquires,
|
|
56
|
+
replays the uncertain commit idempotently (outbox count stays 1), finishes,
|
|
57
|
+
and releases. A stale-fence/stale-revision write and an old-token renewal are
|
|
58
|
+
rejected; two simultaneous acquisitions yield exactly one owner; a foreign
|
|
59
|
+
tenant's reads, writes, and lease takeover all fail closed.
|
|
60
|
+
|
|
61
|
+
## Extension and configuration notes
|
|
62
|
+
|
|
63
|
+
- Lease TTL is the availability knob: too short spams contention, too long
|
|
64
|
+
delays failover. The frozen drill ceiling is `ttlMs + 5000` ms; renew at
|
|
65
|
+
`ttlMs / 3` (the workflow coordinator, saga engine, and this drill all
|
|
66
|
+
follow this pattern).
|
|
67
|
+
- Local in-memory registries (workflow active-run map, coordinator active
|
|
68
|
+
map, A2A live-task registry, ACP session registry, coding-agent sessions,
|
|
69
|
+
RPC active runs) are optional fast paths only. On restart, every component
|
|
70
|
+
reloads authority, status, and cursors from durable stores; a killed
|
|
71
|
+
replica's registry is never required to make progress.
|
|
72
|
+
- SIEM/alerting: emit owner/fence/lease-age/state metadata only — never tenant
|
|
73
|
+
payloads, tokens, or credentials. Metrics to watch: lease-acquire latency,
|
|
74
|
+
fence counter jumps (a jump means a takeover happened), and outbox pending
|
|
75
|
+
depth.
|
|
76
|
+
|
|
77
|
+
## Security and performance notes
|
|
78
|
+
|
|
79
|
+
- Fencing is enforced at the store: a stale owner's write fails with
|
|
80
|
+
`ERR_PRISM_CHECKPOINT_CONFLICT` (stale version, failed CAS, or stale
|
|
81
|
+
fencing token), and a stale token's renewal returns `null`. There is
|
|
82
|
+
intentionally no "force unlock" operation — deleting a lease row manually
|
|
83
|
+
bypasses fencing and can cause split-brain writes; never do it.
|
|
84
|
+
- Tenant ownership is checked on every durable read/write; cross-tenant
|
|
85
|
+
reads, saves, and lease acquisitions fail closed with ownership-mismatch
|
|
86
|
+
errors.
|
|
87
|
+
- An uncertain commit (side effect landed, cursor not advanced) is resolved
|
|
88
|
+
by replaying the effect with its stable id — never by guessing. Reload
|
|
89
|
+
durable state before any retry; retries that skip the reload risk
|
|
90
|
+
overwriting a peer's fenced progress.
|
|
91
|
+
- Failover is bounded but not instantaneous: the peer cannot acquire until
|
|
92
|
+
the lease expires, so recovery time is at least the remaining TTL.
|
|
93
|
+
Performance is linear in record size; the drill records contention/latency
|
|
94
|
+
with bounded, jittered acquisition polls (no hot loops) and reports the
|
|
95
|
+
measured numbers in the evidence JSON.
|
|
96
|
+
|
|
97
|
+
## Related APIs
|
|
98
|
+
|
|
99
|
+
- `LeaseStore` / `CheckpointStore` — the durable contracts this runbook relies on.
|
|
100
|
+
- `createPostgresPersistence` — the PostgreSQL adapter used by the drill.
|
|
101
|
+
- `ErpOutboxStore` — the idempotent side-effect carrier used to make replay safe.
|
|
102
|
+
- `createWorkflowCoordinator` and `defineSaga`/`runSaga`/`resumeSaga` — higher-level consumers with the same fencing/cursor semantics.
|
|
103
|
+
- `scripts/phase27-ha-worker.mjs` / `scripts/phase27-ha.test.mjs` — the reproducible drill; `docs/_evidence/phase27-ha-evidence.json` — the recorded run.
|
|
104
|
+
- [Signed, hash-chained audit export](audit-export.md) — the cursor/CAS pattern applied to audit exports.
|
package/docs/policy-and-audit.md
CHANGED
|
@@ -118,6 +118,40 @@ Policy is optional. Hosts wire `record*` helpers or `evaluateAndAppend` at permi
|
|
|
118
118
|
- The OPA decision fetch (0.2.1) is DNS-pinned through the core `pinnedFetch` primitive: one resolve per request, every resolved address SSRF-checked before the connect (rebinding defense), redirects rejected outright, timeouts/retries unchanged, and private-answer denials surface `MediaContentError` (`ssrf_denied`) rather than a transport error.
|
|
119
119
|
- Export never full-scans: page size is capped; raise hard caps only with Phase 8 freeze + tests + docs updates.
|
|
120
120
|
|
|
121
|
+
## Multi-party approvals
|
|
122
|
+
|
|
123
|
+
Immutable approval requests carry an action digest, requester, and required roles/quorum; verified host identities vote through an explicit `ApprovalAuthority`. Prism records decisions and enforces separation-of-duties, expiry, revocation, bounded delegation, rejection, and policy-revision pins — it never resolves identities or roles itself.
|
|
124
|
+
|
|
125
|
+
| API / field | Meaning |
|
|
126
|
+
| --- | --- |
|
|
127
|
+
| `ApprovalAuthority` | Host-owned `policyRevision` + `resolveRoles(identity, request)` returning role grants (and delegation chains) |
|
|
128
|
+
| `ApprovalRequest` | Immutable request: `action {kind, digest}`, `requirements [{role, quorum}]`, `separateFromRequester`, `expiresAt`, `delegationMaxDepth` |
|
|
129
|
+
| `ApprovalRecord` | Request + `status` (`pending/approved/rejected/revoked/consumed`) + `revision` + immutable `decisions` + `policyRevision` |
|
|
130
|
+
| `ApprovalStore.create/decide/revoke/consume/get/query` | Durable transitions; every transition records an `auditRef` |
|
|
131
|
+
| `createMemoryApprovalStore({ authority })` | Single-process reference adapter sharing the pure transition logic |
|
|
132
|
+
| `createPostgresApprovalStore({ pool, schema, authority })` | Cross-replica storage (migration `005_erp_approvals`, `@arnilo/prism-enterprise-postgres`) |
|
|
133
|
+
|
|
134
|
+
Quorum rules:
|
|
135
|
+
|
|
136
|
+
- Each requirement needs `quorum` **distinct approved principals** through that role. Same principal voting the same role twice is idempotent; changing a vote is a conflict.
|
|
137
|
+
- A rejection is terminal and vetoes the request (`rejected`), so `consume` always denies.
|
|
138
|
+
- `separateFromRequester: true` denies the requester (and any authority chain deriving from the requester) from deciding.
|
|
139
|
+
- `delegationMaxDepth` bounds the persisted delegation chain (delegator first); grants cannot outlive the request, leave the tenant, or widen the action.
|
|
140
|
+
- Pin: `ApprovalAuthority.policyRevision` must equal the record's revision; a policy bump invalidates outstanding approvals (release denied).
|
|
141
|
+
|
|
142
|
+
Revocation semantics: `revoke` is only valid for `pending`/`approved` grants, is taken by a verified tenant actor with an `auditRef`, and is terminal. After consumption, revocation cannot undo the effect — the record stays `consumed` as provenance, and reconciliation is host work.
|
|
143
|
+
|
|
144
|
+
Operator audit queries: `query({ tenantId, status })` pages by `(created_at, id)`; `get({ tenantId, requestId })` returns full immutable decisions with grants and delegation chains. Every decision and manual transition carries the host `auditRef`.
|
|
145
|
+
|
|
146
|
+
```sql
|
|
147
|
+
-- pending approvals that never released (example host view)
|
|
148
|
+
SELECT id, policy_revision, expires_at, decisions
|
|
149
|
+
FROM prism.prism_erp_approvals
|
|
150
|
+
WHERE status = 'pending' AND expires_at < now();
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Hosts own identity verification and the role source; Prism does not certify NIST compliance. NIST SP 800-53 AC-5 (separation of duties) and AC-6 (least privilege) are control guidance only, not certification claims.
|
|
154
|
+
|
|
121
155
|
## OPA external policy adapter (`@arnilo/prism-policy/opa`, 0.0.28)
|
|
122
156
|
|
|
123
157
|
Optional `createOpaPolicyEvaluator` evaluates `PolicyEvaluateRequest`s against a host-pinned OPA REST endpoint (`POST /v1/data/<path>` with `{"input": <document>}`) and returns a core `PolicyEvaluator` for `evaluateAndAppend`. Native `fetch` only; no OPA SDK dependency.
|