@arnilo/prism 0.0.96 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +285 -2
- package/README.md +17 -3
- package/dist/agent-definitions.js +2 -3
- package/dist/agent-event-source.d.ts +11 -0
- package/dist/agent-event-source.js +512 -0
- package/dist/agent-loops.d.ts +5 -0
- package/dist/agent-loops.js +99 -14
- package/dist/agent-run-lifecycle.d.ts +5 -2
- package/dist/agent-run-lifecycle.js +18 -2
- package/dist/agent-run-state.d.ts +27 -1
- package/dist/agent-run-state.js +113 -7
- package/dist/agents.d.ts +3 -1
- package/dist/agents.js +1255 -129
- package/dist/artifacts.d.ts +132 -0
- package/dist/artifacts.js +44 -0
- package/dist/cache-helpers.js +18 -9
- package/dist/checkpoints.d.ts +4 -0
- package/dist/checkpoints.js +17 -9
- package/dist/cli-init.js +3 -7
- package/dist/cli-runner.d.ts +2 -6
- package/dist/cli-runner.js +71 -33
- package/dist/compaction.js +5 -4
- package/dist/config.js +7 -4
- package/dist/content.js +26 -24
- package/dist/context-budget.d.ts +67 -0
- package/dist/context-budget.js +288 -0
- package/dist/contracts.d.ts +590 -8
- package/dist/contracts.js +142 -1
- package/dist/contribution-parsing.js +6 -2
- package/dist/contributions.d.ts +2 -0
- package/dist/contributions.js +3 -0
- package/dist/conversations.d.ts +50 -0
- package/dist/conversations.js +98 -0
- package/dist/credentials.d.ts +22 -2
- package/dist/credentials.js +18 -3
- package/dist/devices.d.ts +94 -0
- package/dist/devices.js +138 -0
- package/dist/event-multiplexer.js +18 -4
- package/dist/extensions.d.ts +18 -1
- package/dist/extensions.js +79 -6
- package/dist/feedback.js +12 -10
- package/dist/guardrails.d.ts +1 -1
- package/dist/guardrails.js +26 -17
- package/dist/identity.d.ts +92 -0
- package/dist/identity.js +265 -0
- package/dist/index.d.ts +94 -72
- package/dist/index.js +48 -36
- package/dist/input.d.ts +10 -1
- package/dist/input.js +152 -52
- package/dist/instruction-injection.d.ts +1 -1
- package/dist/middleware.js +9 -1
- package/dist/models.d.ts +2 -0
- package/dist/models.js +3 -0
- package/dist/node/agent-definitions.js +16 -8
- package/dist/node/contribution-discovery.d.ts +1 -2
- package/dist/node/contribution-discovery.js +3 -3
- package/dist/node/session-store-jsonl.js +13 -7
- package/dist/node/settings.d.ts +1 -1
- package/dist/node/settings.js +1 -1
- package/dist/node/system-project-prompts.js +2 -4
- package/dist/node/trust.js +1 -1
- package/dist/persistence-lifecycle.d.ts +103 -0
- package/dist/persistence-lifecycle.js +202 -0
- package/dist/provider-events.d.ts +1 -0
- package/dist/provider-events.js +6 -1
- package/dist/provider-request-policy.js +3 -4
- package/dist/providers/media.d.ts +1 -1
- package/dist/providers/openai-compatible.d.ts +46 -1
- package/dist/providers/openai-compatible.js +123 -53
- package/dist/providers/openai-primitives.js +10 -7
- package/dist/providers/transport.d.ts +6 -0
- package/dist/providers/transport.js +21 -0
- package/dist/providers.d.ts +2 -0
- package/dist/providers.js +3 -0
- package/dist/redaction.d.ts +1 -0
- package/dist/redaction.js +26 -9
- package/dist/resources.d.ts +2 -2
- package/dist/resources.js +2 -2
- package/dist/retry.d.ts +5 -0
- package/dist/retry.js +8 -1
- package/dist/rpc.js +55 -11
- package/dist/run-ledger.d.ts +6 -0
- package/dist/run-ledger.js +16 -13
- package/dist/run-limits.js +49 -10
- package/dist/secure-agent.js +8 -2
- package/dist/security.js +7 -2
- package/dist/session-stores.d.ts +7 -2
- package/dist/session-stores.js +195 -21
- package/dist/skill-disclosure.d.ts +35 -0
- package/dist/skill-disclosure.js +101 -0
- package/dist/skill-load.d.ts +25 -0
- package/dist/skill-load.js +112 -0
- package/dist/structured-output.d.ts +5 -1
- package/dist/structured-output.js +20 -2
- package/dist/system-prompts.js +7 -2
- package/dist/testing/agent-event-source-conformance.d.ts +4 -0
- package/dist/testing/agent-event-source-conformance.js +54 -0
- package/dist/testing/compaction-conformance.js +5 -1
- package/dist/testing/extension-conformance.js +15 -3
- package/dist/testing/feedback.d.ts +1 -3
- package/dist/testing/feedback.js +1 -1
- package/dist/testing/persistence-schema.d.ts +2 -2
- package/dist/testing/persistence-schema.js +280 -35
- package/dist/testing/provider-conformance.js +3 -3
- package/dist/testing/run-ledger-conformance.js +1 -1
- package/dist/testing/session-store-conformance.d.ts +6 -0
- package/dist/testing/session-store-conformance.js +37 -2
- package/dist/testing/tool-conformance.js +30 -5
- package/dist/testing/tool-effect-store-conformance.d.ts +9 -0
- package/dist/testing/tool-effect-store-conformance.js +85 -0
- package/dist/thinking.js +4 -1
- package/dist/tool-effects.d.ts +15 -0
- package/dist/tool-effects.js +352 -0
- package/dist/tool-result-fold.d.ts +40 -0
- package/dist/tool-result-fold.js +176 -0
- package/dist/tools.d.ts +8 -3
- package/dist/tools.js +248 -13
- package/docs/0.1.0-readiness.md +202 -0
- package/docs/a2a.md +33 -2
- package/docs/acp.md +126 -0
- package/docs/ag-ui-adoption.md +77 -0
- package/docs/ag-ui.md +225 -0
- package/docs/agent-events.md +34 -3
- package/docs/agent-identity.md +144 -0
- package/docs/agent-loops.md +17 -2
- package/docs/agent-session-runtime.md +21 -4
- package/docs/browser-automation.md +5 -0
- package/docs/caveman.md +129 -0
- package/docs/cli-rpc.md +3 -6
- package/docs/coding-agent-tools.md +229 -25
- package/docs/coding-security.md +77 -11
- package/docs/compaction-and-retry.md +5 -2
- package/docs/compaction-llm.md +20 -1
- package/docs/compaction-observational-memory.md +52 -8
- package/docs/context-and-skills.md +94 -7
- package/docs/contribution-registries.md +1 -0
- package/docs/conversations.md +135 -0
- package/docs/credential-storage.md +34 -1
- package/docs/credentials-and-redaction.md +11 -1
- package/docs/database-persistence.md +27 -7
- package/docs/device-adapters.md +97 -0
- package/docs/enterprise-postgres-state.md +178 -0
- package/docs/evaluations.md +14 -1
- package/docs/extensions.md +4 -1
- package/docs/forge-integration.md +113 -0
- package/docs/guardrails.md +16 -2
- package/docs/host-security.md +35 -4
- package/docs/index.md +69 -37
- package/docs/input-and-prompt-assembly.md +8 -7
- package/docs/language-intelligence.md +162 -0
- package/docs/mcp-tools.md +62 -5
- package/docs/middleware-hooks.md +2 -2
- package/docs/migration.md +423 -2
- package/docs/model-routing.md +111 -0
- package/docs/multimodal-content.md +8 -5
- package/docs/node-jsonl-session-store.md +1 -1
- package/docs/observability.md +2 -0
- package/docs/openapi-tools.md +56 -0
- package/docs/performance.md +282 -0
- package/docs/policy-and-audit.md +171 -0
- package/docs/ponytail.md +127 -0
- package/docs/postgres-persistence.md +8 -4
- package/docs/process-sessions.md +147 -0
- package/docs/provider-caching.md +13 -1
- package/docs/provider-conformance.md +29 -5
- package/docs/provider-packages.md +43 -2
- package/docs/provider-request-policies.md +2 -0
- package/docs/providers/ai-sdk.md +24 -7
- package/docs/providers/alibaba.md +179 -0
- package/docs/providers/anthropic.md +93 -0
- package/docs/providers/azure.md +74 -0
- package/docs/providers/bedrock.md +72 -0
- package/docs/providers/google.md +89 -0
- package/docs/providers/ollama.md +166 -0
- package/docs/providers/openai-compatible.md +31 -2
- package/docs/providers/openai.md +24 -5
- package/docs/providers/openrouter.md +2 -0
- package/docs/providers/vertex.md +71 -0
- package/docs/public-contracts.md +61 -4
- package/docs/rag.md +41 -12
- package/docs/release-and-install.md +323 -206
- package/docs/resource-loading.md +3 -0
- package/docs/runs-and-usage.md +3 -0
- package/docs/server.md +44 -6
- package/docs/session-store-conformance.md +2 -0
- package/docs/session-stores.md +41 -2
- package/docs/sqlite-persistence.md +11 -3
- package/docs/structured-output.md +7 -1
- package/docs/supervisors.md +8 -0
- package/docs/tool-effects.md +95 -0
- package/docs/tools.md +5 -0
- package/docs/work-artifacts-and-review.md +102 -0
- package/docs/work-connectors.md +32 -0
- package/docs/work-tools.md +137 -0
- package/docs/workflows.md +6 -0
- package/docs/working-and-semantic-memory.md +40 -7
- package/package.json +30 -8
- package/templates/init/providers.json +22 -0
- package/docs/review-coverage-2026-07-14.md +0 -260
- package/docs/review-coverage-2026-07-15.md +0 -193
- package/docs/review-coverage-2026-07-17-provider-validation.md +0 -192
- package/docs/review-coverage-2026-07-19-phase-3.md +0 -174
- package/docs/review-coverage-2026-07-20-phase-4.md +0 -175
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Conversations
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
`@arnilo/prism-server` ships a durable, user-scoped conversation service: create/list/get/continue/branch/archive/export/delete conversation threads on top of the existing session and event-ledger seams. A thread **is** an ownership-scoped session branch plus a `prismConversation` marker in `SessionRecord.metadata`; content stays in session entries and the redacted event ledger. Reconnectable replay pages durable redacted events without ever rerunning a provider or tool.
|
|
6
|
+
|
|
7
|
+
Core (`@arnilo/prism`) exports only conversation **types and pure helpers** (`ConversationThread`, `ConversationError`, `CONVERSATION_METADATA_KEY`, thread-bound replay cursor codec, `conversationThreadFromRecord`, `conversationMarkerMetadata`). The service and optional HTTP handler live in `@arnilo/prism-server`.
|
|
8
|
+
|
|
9
|
+
## When to use it
|
|
10
|
+
|
|
11
|
+
Use it when a host needs persistent personal/work-agent conversations with reconnect, branch, archive, export, and deletion semantics without building a second session/event system. Hosts own authentication, agent selection, UI, transport chrome, and blob storage.
|
|
12
|
+
|
|
13
|
+
Do not use it as a chat UI, a push/always-on daemon, or a file store. Slack/Teams channels, realtime voice, and desktop-control vendors are deferred (0.1.x); device adapters are contract + deny-by-default conformance only in 0.0.14.
|
|
14
|
+
|
|
15
|
+
## Inputs / request
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createConversationService, createConversationHandler } from "@arnilo/prism-server";
|
|
19
|
+
import { createSqlitePersistence } from "@arnilo/prism-session-store-sqlite";
|
|
20
|
+
|
|
21
|
+
const persistence = createSqlitePersistence({ filename }); // implements ConversationServiceStore
|
|
22
|
+
const service = createConversationService(persistence, {
|
|
23
|
+
redactor, // required: replay/export serve redacted rows only; continue runs with it
|
|
24
|
+
sessionFactory: ({ thread, leafId, ownership, signal }) =>
|
|
25
|
+
agent.createSession({ id: thread.id, ...(leafId ? { leafId } : {}) }), // host binds agent/store/leaf
|
|
26
|
+
runOptions?, // narrowable RunOptions minus ownership/identity/signal/redactor/idempotencyKey
|
|
27
|
+
limits?: ConversationLimits, // frozen defaults/caps below
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await service.create({ ownership, title?, id?, requestId?, metadata? });
|
|
31
|
+
await service.list({ ownership, cursor?, limit? });
|
|
32
|
+
await service.get({ ownership, threadId });
|
|
33
|
+
await service.continue({ ownership, threadId, message, requestId?, leafId? });
|
|
34
|
+
await service.branch({ ownership, threadId, leafId });
|
|
35
|
+
await service.archive({ ownership, threadId });
|
|
36
|
+
await service.export({ ownership, threadId, cursor? });
|
|
37
|
+
await service.delete({ ownership, threadId });
|
|
38
|
+
await service.replay({ ownership, threadId, cursor?, limit? });
|
|
39
|
+
|
|
40
|
+
const handler = createConversationHandler({ service, authorize, basePath?: "/prism/conversations", redactor?, limits? });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`ConversationServiceStore` is a narrow `Pick` of `ProductionPersistenceStore`: `querySessions`, `queryEvents`, optional `appendSession` (required at factory time; sqlite/postgres implement it), and optional `lifecycle.applyRetention` (required for delete). Stores without `appendSession` fail closed at construction.
|
|
44
|
+
|
|
45
|
+
## Outputs / response / events
|
|
46
|
+
|
|
47
|
+
- `create`/`get`/`branch`/`archive` → `ConversationThread` (`id`, `title?`, `state: "active" | "archived"`, `branches`, timestamps, ownership projection, host metadata).
|
|
48
|
+
- `list` → ownership-scoped `PersistencePage<ConversationThread>` (newest first), marker-filtered; non-conversation sessions never appear.
|
|
49
|
+
- `continue` → `AgentRunResult` from one agent turn on the thread session; history rebuilds from durable entries, so the agent sees prior turns.
|
|
50
|
+
- `replay` → `{ records: AgentEventRecord[], nextCursor?, terminal }`; records are durable redacted ledger rows ordered by `(timestamp, id)`; `terminal` marks `agent_finished`/`agent_denied`/`error`.
|
|
51
|
+
- `export` → `{ thread, events, nextCursor?, truncated }`; redacted, byte/page-capped, cursor-resumable.
|
|
52
|
+
- `delete` → `{ deleted, held }` via persistence lifecycle; legal holds always win.
|
|
53
|
+
- HTTP handler routes: `POST {base}` create · `GET {base}` list · `GET {base}/{id}` · `DELETE {base}/{id}` · `POST {base}/{id}/continue|branch|archive|export` · `GET {base}/{id}/events?cursor=&limit=` replay.
|
|
54
|
+
|
|
55
|
+
## Request/response example
|
|
56
|
+
|
|
57
|
+
```http
|
|
58
|
+
POST /prism/conversations HTTP/1.1
|
|
59
|
+
content-type: application/json
|
|
60
|
+
|
|
61
|
+
{ "title": "Q3 planning" }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{ "id": "conv_9b0f…", "title": "Q3 planning", "state": "active", "branches": [], "createdAt": "…", "updatedAt": "…", "tenantId": "t1", "userId": "u1" }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Reconnect after a dropped connection: `GET {base}/{id}/events` (optionally with the last `nextCursor`) pages the same durable events; clients dedupe by stable record `id` (at-least-once across the page boundary). No provider or tool call is re-executed by replay/export.
|
|
69
|
+
|
|
70
|
+
## Implementation example
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const thread = await service.create({ ownership, title: "draft review" });
|
|
74
|
+
await service.continue({ ownership, threadId: thread.id, message: "summarize the attached plan", requestId: "ui-req-1" });
|
|
75
|
+
|
|
76
|
+
// Branch from a known leaf (e.g. last entry id), then fork a continue from it.
|
|
77
|
+
const branched = await service.branch({ ownership, threadId: thread.id, leafId });
|
|
78
|
+
await service.continue({ ownership, threadId: thread.id, message: "try a shorter version", leafId });
|
|
79
|
+
|
|
80
|
+
// Reconnectable replay.
|
|
81
|
+
let cursor: string | undefined;
|
|
82
|
+
do {
|
|
83
|
+
const page = await service.replay({ ownership, threadId: thread.id, ...(cursor ? { cursor } : {}) });
|
|
84
|
+
render(page.records);
|
|
85
|
+
cursor = page.nextCursor;
|
|
86
|
+
} while (cursor);
|
|
87
|
+
|
|
88
|
+
await service.archive({ ownership, threadId: thread.id });
|
|
89
|
+
const result = await service.delete({ ownership, threadId: thread.id }); // { deleted: true, held: false }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Extension and configuration notes
|
|
93
|
+
|
|
94
|
+
Frozen limits (default / hard cap; hosts may tighten, never raise past hard caps):
|
|
95
|
+
|
|
96
|
+
| Resource | Default / hard cap |
|
|
97
|
+
| --- | ---: |
|
|
98
|
+
| Thread list page | 50 / 200 |
|
|
99
|
+
| Replay/export page rows | 100 / 500 |
|
|
100
|
+
| Replay/export cursor | 4 KiB / 16 KiB |
|
|
101
|
+
| Thread title | 256 B / 2 KiB |
|
|
102
|
+
| Client request id | 256 B / 2 KiB |
|
|
103
|
+
| Active branches per thread | 16 / 64 |
|
|
104
|
+
| Export payload per request | 8 MiB / 32 MiB |
|
|
105
|
+
| Export pages per request | 100 / 500 |
|
|
106
|
+
| Handler request body | 64 KiB / 1 MiB |
|
|
107
|
+
|
|
108
|
+
Behavior notes:
|
|
109
|
+
|
|
110
|
+
- `create` with an explicit `id` is idempotent get-or-create; generated ids are `conv_<uuid>`.
|
|
111
|
+
- `continue` `requestId` flows into session-append idempotency (`RunRecord.idempotencyKey` + append dedup), so exact retries deduplicate.
|
|
112
|
+
- `continue` on an archived thread fails closed (`thread_archived`); `leafId` must be a branch ref recorded by `branch()`.
|
|
113
|
+
- Replay cursors are thread-bound: a cursor minted for one thread is rejected on another (`cursor_thread_mismatch`).
|
|
114
|
+
- Ledger rows from runs that had no redactor are never served by replay/export (fail-closed skip).
|
|
115
|
+
- Export truncates at page granularity when the next page would exceed `exportBytes`; a single page larger than `exportBytes` cannot be exported (raise the cap or page via `replay`).
|
|
116
|
+
- Branch refs live in thread metadata (read-modify-write); concurrent `branch()` calls can lose a ref, so the cap is approximate and the entry tree remains the content source of truth.
|
|
117
|
+
- Deletion purges the whole session ledger (entries, runs, events, tool calls, usage, branches, search rows) through `lifecycle.applyRetention`; legal holds block deletion and report `held: true`.
|
|
118
|
+
|
|
119
|
+
## Security and performance notes
|
|
120
|
+
|
|
121
|
+
- Every operation starts from host-verified ownership (and optional `AgentIdentity`, which must project onto ownership without widening); wrong-user access returns not-found, never leaked existence.
|
|
122
|
+
- `appendSession` upserts set ownership columns only on create; metadata/`updatedAt` on update — ownership is immutable after create.
|
|
123
|
+
- Replay/export serve `redacted: true` ledger rows only and pass through the service redactor; no local paths, raw tool payloads, or secrets are emitted.
|
|
124
|
+
- All loops are bounded by the frozen caps above; review/agent turns consume shared `RunLimits` via the host's `runOptions`.
|
|
125
|
+
- No new permission surface: conversations reuse session/event/identity/redaction/lifecycle seams (roadmap gate 8).
|
|
126
|
+
|
|
127
|
+
## Related APIs
|
|
128
|
+
|
|
129
|
+
- [Web-standard server handler](server.md): authorized agent/workflow routes; the conversation handler mounts beside it.
|
|
130
|
+
- [Session stores](session-stores.md): branch/append/checkout semantics a thread builds on.
|
|
131
|
+
- [Database persistence](database-persistence.md): `ProductionPersistenceStore`, `appendSession`, `SessionQuery` id/metadataKey filters, retention/legal-hold lifecycle.
|
|
132
|
+
- [Agents and sessions](agent-identity.md): verified identity and ownership projection.
|
|
133
|
+
- [Credentials and redaction](credentials-and-redaction.md): `SecretRedactor` used by replay/export/continue.
|
|
134
|
+
- [Browser automation](browser-automation.md): verified-state checkpoints scope to the runs a thread owns; reload/verify before side effect.
|
|
135
|
+
- [Device adapters](device-adapters.md): deny-by-default voice/desktop sessions bind to a thread's run and consume shared `RunLimits`.
|
|
@@ -17,6 +17,8 @@ Factories:
|
|
|
17
17
|
- `createOAuthCredentialStoreAdapter(store)`
|
|
18
18
|
- `rotateEncryptedCredentialStorePassphrase(options)`
|
|
19
19
|
|
|
20
|
+
The `@arnilo/prism-credentials-node/oidc` subpath adds the optional OIDC/JWKS identity verifier (`createOidcIdentityVerifier`) — pinned issuer/audience/JWKS verification over native `fetch` + WebCrypto (see [Agent identity](agent-identity.md)).
|
|
21
|
+
|
|
20
22
|
Core `@arnilo/prism` remains storage-free. Hosts choose a backend explicitly at startup; there is no global credential singleton and no silent fallback from keychain to plaintext file storage.
|
|
21
23
|
|
|
22
24
|
## When to use it
|
|
@@ -146,6 +148,33 @@ await refreshOAuthCredential({
|
|
|
146
148
|
});
|
|
147
149
|
```
|
|
148
150
|
|
|
151
|
+
Workload OAuth providers (0.0.14) — Microsoft 365 / Google Workspace over the shared OAuth2 seam (PKCE + device code + refresh + revoke), least-privilege scopes per read/mutation bundle:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { revokeOAuthCredential } from "@arnilo/prism";
|
|
155
|
+
import {
|
|
156
|
+
createMicrosoft365OAuthProvider,
|
|
157
|
+
createGoogleWorkspaceOAuthProvider,
|
|
158
|
+
createOAuthWorkTokenProvider,
|
|
159
|
+
createOAuthCredentialStoreAdapter,
|
|
160
|
+
} from "@arnilo/prism-credentials-node";
|
|
161
|
+
|
|
162
|
+
// Read-only mail/calendar (no mutation scopes requested).
|
|
163
|
+
const m365 = createMicrosoft365OAuthProvider({ clientId: "<app-id>", capabilities: ["mail", "calendar"], access: "read" });
|
|
164
|
+
const creds = await m365.login({ onDeviceCode: ({ userCode, verificationUri }) => host.showCode(userCode, verificationUri) });
|
|
165
|
+
await createOAuthCredentialStoreAdapter(store).set("microsoft365", creds);
|
|
166
|
+
|
|
167
|
+
// Late-bound, per-identity token for a work-tools connector (env var, never argv/model context).
|
|
168
|
+
const tokenProvider = createOAuthWorkTokenProvider({
|
|
169
|
+
provider: m365,
|
|
170
|
+
store: createOAuthCredentialStoreAdapter(store),
|
|
171
|
+
envVar: "M365_ACCESSTOKEN",
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Revocation: best-effort upstream (GWS supports RFC 7009; M365 does not) + mandatory local delete.
|
|
175
|
+
await revokeOAuthCredential({ provider: m365, credentials: creds, store: createOAuthCredentialStoreAdapter(store) });
|
|
176
|
+
```
|
|
177
|
+
|
|
149
178
|
Passphrase rotation:
|
|
150
179
|
|
|
151
180
|
```ts
|
|
@@ -205,7 +234,8 @@ const providers = createOpenAIProviderPackage({ apiKey });
|
|
|
205
234
|
- Use distinct `namespace` or vault paths per tenant/environment.
|
|
206
235
|
- Keychain `list()` / `listOAuth()` are intentionally unsupported — enumerate credentials through host configuration instead of scanning the OS store.
|
|
207
236
|
- Combine with `createExplicitCredentialResolver()` so runtime overrides still win over stored values.
|
|
208
|
-
- Wire `createOAuthCredentialStoreAdapter(store)` into `refreshOAuthCredential()`
|
|
237
|
+
- Wire `createOAuthCredentialStoreAdapter(store)` into `refreshOAuthCredential()` only for an OAuth flow explicitly selected by the host and authorized by that provider. In 0.0.12 that means OpenAI Codex; in 0.0.14 the Microsoft 365 / Google Workspace workload providers (`createMicrosoft365OAuthProvider` / `createGoogleWorkspaceOAuthProvider`) are added over the same seam with least-privilege read/mutation scope bundles. Anthropic and Google *model* packages still accept API keys only. Never import or migrate Claude Code/Gemini CLI credential files, setup tokens, browser sessions, or CLI OAuth rows into this store.
|
|
238
|
+
- Enterprise cloud providers (`azure` / `bedrock` / `vertex`) expect host workload-identity callbacks (Entra / IAM / ADC), not this local encrypted/keychain store as a cloud token minting service. Store may hold opaque refresh material only when the host already owns the cloud auth flow.
|
|
209
239
|
|
|
210
240
|
## Security and performance notes
|
|
211
241
|
|
|
@@ -216,6 +246,8 @@ const providers = createOpenAIProviderPackage({ apiKey });
|
|
|
216
246
|
- Keychain operations use `@napi-rs/keyring`'s abort-aware `AsyncEntry`, so native work runs outside the JavaScript event loop. A main-loop timer aborts and rejects at `timeoutMs`; native cancellation remains OS/backend-dependent and may briefly retain one libuv worker after rejection.
|
|
217
247
|
- Keychain payloads are bytes rather than password strings and are zeroed after parse/write. Unknown native errors are mapped to sanitized typed errors; no native message or secret value is echoed.
|
|
218
248
|
- Never log passphrases, derived keys, or decrypted credential payloads.
|
|
249
|
+
- Optional host KMS: `encryptWithHostKms` / `decryptWithHostKms` wrap a random AES-256-GCM DEK via host `HostKms.wrapKey`/`unwrapKey` (timeout ≤ 60 s). Envelope sizes reuse vault/file caps. Keys are never logged. `createMemoryHostKms` is for tests only.
|
|
250
|
+
- Storage is not OAuth eligibility. A durable store may persist credentials for a provider only after the host selects a provider-authorized flow; it must not be used to piggyback on a vendor CLI or consumer subscription.
|
|
219
251
|
- Live keychain tests are opt-in (`PRISM_TEST_KEYCHAIN=1`); default `npm test` stays offline.
|
|
220
252
|
|
|
221
253
|
## MCP authentication boundary
|
|
@@ -229,6 +261,7 @@ MCP credentials remain host inputs: resolve them before constructing client `req
|
|
|
229
261
|
## Related APIs
|
|
230
262
|
|
|
231
263
|
- [Credentials and redaction](credentials-and-redaction.md): core resolver helpers and `refreshOAuthCredential()`
|
|
264
|
+
- [Azure OpenAI / Foundry](providers/azure.md) / [Amazon Bedrock](providers/bedrock.md) / [Google Vertex AI](providers/vertex.md): enterprise workload-identity credential callbacks
|
|
232
265
|
- [Web search, fetch, and extraction](web-tools.md): late-bound Brave/Exa/Firecrawl credentials
|
|
233
266
|
- [Security/auth/trust](settings-auth-trust-security.md): host-owned settings/credentials boundaries
|
|
234
267
|
- [Persistence, credentials, and multimodality primitives](persistence-credentials-multimodality-primitives.md): Plan 056 threat model and conformance matrix rows 7–10
|
|
@@ -8,6 +8,7 @@ Prism provides small helpers for host-owned credentials and known-secret redacti
|
|
|
8
8
|
- `createExplicitCredentialResolver()`: tries named resolver sources in caller-provided order, such as runtime override → stored → env object → fallback.
|
|
9
9
|
- `createEnvCredentialResolver()`: reads only a caller-supplied env-like object and map.
|
|
10
10
|
- `refreshOAuthCredential()`: calls a provider OAuth refresh function and writes the result to a caller-owned store when supplied.
|
|
11
|
+
- `revokeOAuthCredential()`: best-effort upstream revocation (`OAuthProvider.revoke?`) followed by a mandatory caller-owned store delete, so a revoked token fails closed locally even if the provider has no revocation endpoint.
|
|
11
12
|
- `CredentialValueSource`: the accepted source type for `resolveCredentialValue()`.
|
|
12
13
|
- `redactSecrets()`: replaces known secret string values inside strings, arrays, and plain objects.
|
|
13
14
|
- `errorToErrorInfo()`: converts unknown errors into `ErrorInfo` and redacts known secret values from error text.
|
|
@@ -41,6 +42,7 @@ resolveCredentialValue(
|
|
|
41
42
|
createExplicitCredentialResolver(sources: readonly CredentialResolverSource[]): CredentialResolver
|
|
42
43
|
createEnvCredentialResolver(env: Readonly<Record<string, string | undefined>>, map: Readonly<Record<string, string>>): CredentialResolver
|
|
43
44
|
refreshOAuthCredential(options: { provider: OAuthProvider; credentials: OAuthCredentials; store?: OAuthCredentialStore }): Promise<OAuthCredentials>
|
|
45
|
+
revokeOAuthCredential(options: { provider: OAuthProvider; credentials: OAuthCredentials; store?: RevocableOAuthCredentialStore }): Promise<void>
|
|
44
46
|
redactSecrets<T>(value: T, secrets: readonly (string | undefined)[]): T
|
|
45
47
|
errorToErrorInfo(error: unknown, secrets?: readonly (string | undefined)[]): ErrorInfo
|
|
46
48
|
```
|
|
@@ -102,6 +104,14 @@ console.log(error.message);
|
|
|
102
104
|
- Keep resolved credential values local to the request path. Do not put them in registries, model configs, messages, provider events, agent events, session entries, compaction summaries, or logs.
|
|
103
105
|
- Future settings/config loaders may provide credential resolver instances, but core helpers remain storage-free.
|
|
104
106
|
|
|
107
|
+
### Subscription OAuth eligibility
|
|
108
|
+
|
|
109
|
+
In 0.0.12, OpenAI Codex is Prism's only first-party subscription OAuth flow. It is explicit and host-invoked through `createOpenAICodexOAuthProvider()`; hosts own login UI and may use `createOAuthCredentialStoreAdapter()` for deliberately selected durable storage.
|
|
110
|
+
|
|
111
|
+
Anthropic and Google provider packages are API-key-only. Do not scrape or import Claude Code/Gemini CLI credential files, setup tokens, environment values, or browser sessions, and do not route a user's Claude.ai/Gemini subscription through Prism. Anthropic states that developers building products must use Claude Console API keys or a supported cloud provider and may not offer Claude.ai login or route Free/Pro/Max credentials ([legal and compliance](https://docs.anthropic.com/en/docs/claude-code/legal-and-compliance)). Gemini CLI states that third-party software using its OAuth to access backend services violates applicable terms; its FAQ names Vertex AI or Google AI Studio API keys as the supported third-party path ([terms](https://github.com/google-gemini/gemini-cli/blob/main/docs/resources/tos-privacy.md), [FAQ](https://github.com/google-gemini/gemini-cli/blob/main/docs/resources/faq.md)).
|
|
112
|
+
|
|
113
|
+
A future provider-local OAuth adapter needs published permission for third-party products, documented authorize/token/refresh endpoints and scopes, PKCE/state where required, abort/expiry/bounded-response/redaction/store-round-trip fixtures, and legal review before registration. Until then, absence is intentional.
|
|
114
|
+
|
|
105
115
|
## Security and performance notes
|
|
106
116
|
|
|
107
117
|
- Redaction only removes exact known secret values passed to the helper. It is not a general-purpose secret detector.
|
|
@@ -122,4 +132,4 @@ console.log(error.message);
|
|
|
122
132
|
- [LLM compaction package](compaction-llm.md): resolves optional summary-provider credentials per compaction call and redacts exact known values.
|
|
123
133
|
- [OpenAI-compatible provider](providers/openai-compatible.md): resolves API keys per request and redacts known values from adapter errors.
|
|
124
134
|
|
|
125
|
-
Phase 10 added `createMemoryCredentialStore()`, `createChainedCredentialResolver()`, and `createSecretRedactor()` for opt-in in-memory auth and runtime redaction. Phase 11 adds OAuth/API-key contracts plus explicit resolver order helpers. Core still has no persistent secret store and does not read environment variables or files for credentials. For durable storage, use [`@arnilo/prism-credentials-node`](credential-storage.md) encrypted-file or keychain backends. See [Security/auth/trust](settings-auth-trust-security.md).
|
|
135
|
+
Phase 10 added `createMemoryCredentialStore()`, `createChainedCredentialResolver()`, and `createSecretRedactor()` for opt-in in-memory auth and runtime redaction. By default the memory store serves a providerless record for a provider-scoped request of the same name — that record is then shared across every provider; pass `{ allowProviderFallback: false }` for exact-match-only resolution (strict provider scoping). Phase 11 adds OAuth/API-key contracts plus explicit resolver order helpers. Core still has no persistent secret store and does not read environment variables or files for credentials. For durable storage, use [`@arnilo/prism-credentials-node`](credential-storage.md) encrypted-file or keychain backends. See [Security/auth/trust](settings-auth-trust-security.md).
|
|
@@ -8,6 +8,8 @@ Prism itself does not ship a production database adapter. The built-in `SessionS
|
|
|
8
8
|
|
|
9
9
|
Plan 056 Task 1 adds dialect-neutral shared primitives under `@arnilo/prism/testing/persistence-schema`, `@arnilo/prism/testing/session-store-conformance`, and `@arnilo/prism/testing/run-ledger-conformance`. Task 2 ships `@arnilo/prism-session-store-sqlite` (see [SQLite persistence](sqlite-persistence.md)); Task 3 ships `@arnilo/prism-session-store-postgres` (see [PostgreSQL persistence](postgres-persistence.md)). Both implement dialect-local SQL against the shared model; Prism core still ships no ORM, driver, or migration runner.
|
|
10
10
|
|
|
11
|
+
Release 0.0.23 additionally ships [`@arnilo/prism-enterprise-postgres`](enterprise-postgres-state.md), a separate PostgreSQL composition for policy decisions, evaluations, work-mutation claims, and model-router state. It is not a `ProductionPersistenceStore` replacement and does not store sessions/runs. Its fixed `prism_enterprise_migrations` history is independent of `prism_migrations`; hosts may use both compositions against the same validated schema.
|
|
12
|
+
|
|
11
13
|
## When to use it
|
|
12
14
|
|
|
13
15
|
Use these contracts when you write a database-backed `SessionStore` or a separate persistence adapter that needs:
|
|
@@ -72,6 +74,10 @@ Important shapes:
|
|
|
72
74
|
| `RetentionPolicy` | Policy with `maxAgeDays`, `maxEntriesPerSession`, `maxTotalBytes`, `archiveStore`, and `appliedKinds`. |
|
|
73
75
|
| `MigrationRecord` | Applied migration with name, version, timestamp, checksum, and applied-by. |
|
|
74
76
|
|
|
77
|
+
Optional session-record write seam (0.0.14): `appendSession?(record: SessionRecord)` upserts a session row — ownership columns are set on create only, `metadata`/`updatedAt` on update — so hosts (e.g. the [conversation service](conversations.md)) can durably mark and title sessions without entry writes. `SessionQuery` gained two bounded filters for the same seam: `id` (exact session lookup) and `metadataKey` (sessions whose `metadata` object contains a top-level key, validated by `assertSessionMetadataKey`). SQLite implements it with `json_extract`, PostgreSQL with a `jsonb` existence check; both keep ownership filtering intact.
|
|
78
|
+
|
|
79
|
+
Artifact co-work review (0.0.14) reuses the generic `CheckpointStore` rather than adding a dedicated table: the [artifact service](work-artifacts-and-review.md) stores each artifact as a versioned checkpoint value (namespace `prism.artifact`, key `threadId:artifactId`, category `artifact`). The checkpoint `version` is the compare-and-swap counter that resolves concurrent reviewers; revision numbers, approvals, and `lastValidatedVersion` live inside the JSON value. SQLite/Postgres already persist checkpoints durably, so there is no separate artifact schema or migration, and records carry metadata/hashes/refs only — never file bodies.
|
|
80
|
+
|
|
75
81
|
## Outputs / response / events
|
|
76
82
|
|
|
77
83
|
Each `query*` method returns a `PersistencePage<T>`:
|
|
@@ -334,19 +340,30 @@ Use `cursor`/`limit` when an adapter pages very long branches. Do not implement
|
|
|
334
340
|
|
|
335
341
|
## Retention policies
|
|
336
342
|
|
|
337
|
-
A retention policy is a host-managed rule attached to sessions via `retention_policy_id`.
|
|
343
|
+
A retention policy is a host-managed rule attached to sessions via `retention_policy_id`. Phase 8 adds optional `ProductionPersistenceStore.lifecycle` (`createMemoryPersistenceLifecycle`, SQLite/Postgres `persistence.lifecycle`) for bounded apply/hold/export/quota:
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
await store.lifecycle.putLegalHold({ tenantId, userId, resourceKind: "session", resourceId, reason });
|
|
347
|
+
await store.lifecycle.applyRetention({ tenantId, userId, policy, candidates }); // hold wins over delete; SQL adapters purge the whole session ledger (entries, runs, events, tool calls, usage, branches, search rows) in FK order
|
|
348
|
+
await store.lifecycle.exportUnderHold({ tenantId, userId, cursor, limit }); // redacted
|
|
349
|
+
await store.lifecycle.setTenantQuota({ tenantId, userId, resourceKind: "run", limit: 100 });
|
|
350
|
+
await store.lifecycle.consumeTenantQuota({ tenantId, userId, resourceKind: "run" }); // fails closed when exhausted
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Schema **v5** migration `005_lifecycle_hold_quota` adds `prism_legal_holds` and `prism_tenant_quotas`. Legal hold always blocks delete for the held resource id. Export pages are redacted refs only (no prompts/bodies/secrets).
|
|
354
|
+
|
|
355
|
+
Host background jobs may still:
|
|
338
356
|
|
|
339
357
|
1. Select policies whose `max_age_days`, `max_entries_per_session`, or `max_total_bytes` thresholds are exceeded.
|
|
340
|
-
2.
|
|
341
|
-
3. Respect `applied_kinds`
|
|
342
|
-
4.
|
|
343
|
-
5. Write audit metadata to the migration or host audit log; do not delete the policy row unless explicitly requested.
|
|
358
|
+
2. Pass candidate session ids into `applyRetention` (or let SQL adapters discover expired sessions).
|
|
359
|
+
3. Respect `applied_kinds` when selecting candidates.
|
|
360
|
+
4. Never silently purge under hold.
|
|
344
361
|
|
|
345
|
-
Retention jobs should not run inside the agent/session runtime.
|
|
362
|
+
Retention jobs should not run inside the agent/session runtime.
|
|
346
363
|
|
|
347
364
|
## Migrations
|
|
348
365
|
|
|
349
|
-
Hosts own schema migrations. Prism publishes only the TypeScript contracts; no DDL is generated or executed by the core library. First-party SQLite/PostgreSQL adapters automatically verify their checked-in schema-
|
|
366
|
+
Hosts own schema migrations. Prism publishes only the TypeScript contracts; no DDL is generated or executed by the core library. First-party SQLite/PostgreSQL adapters automatically verify their checked-in schema-v5 history and catalog at open, before runtime writes. Their catalog reads are bounded metadata queries/PRAGMAs, not table-data scans.
|
|
350
367
|
|
|
351
368
|
Each new adapter-owned migration row records the contract SHA-256 checksum. A complete known v0.0.5 history whose checksum values are all `NULL` is a one-time compatibility case: under the SQLite transaction or PostgreSQL advisory transaction lock, the adapter verifies the full current shape, backfills all checksums, and continues. Unknown/duplicate/out-of-order/name-version/checksum mismatch, mixed/partial legacy values, or any missing/renamed/wrong-type/null/default/key/index artifact fails closed. Restore or apply a reviewed host migration; never edit checksums to silence drift.
|
|
352
369
|
|
|
@@ -434,6 +451,8 @@ const dbStore: ProductionPersistenceStore = {
|
|
|
434
451
|
- Cursor values and idempotency keys are host-defined and opaque to Prism.
|
|
435
452
|
- First-party SQLite/PostgreSQL adapters expose `persistence.checkpoints` and `persistence.leases`, backed by package-owned `prism_checkpoints` / `prism_leases` tables. `@arnilo/prism-workflows` consumes them for durable resume, human suspension, multi-process coordination, Phase 11 schedule records/fire leases, shared state, and replay lineage; workflow code owns no SQL table. `suspended`/`denied`, schedules, state history, and replay lineage remain namespaces/categories plus bounded checkpoint JSON values, so Phases 8 and 11 need no database migration.
|
|
436
453
|
|
|
454
|
+
Schema version **7** adds the exact-owner durable event retention index (`prism_agent_events_owner_timestamp_sequence_idx`). Distributed subscribe/LISTEN remains PostgreSQL-only via `persistence.events`.
|
|
455
|
+
|
|
437
456
|
## Security and performance notes
|
|
438
457
|
|
|
439
458
|
- **No credentials in storage.** The contracts never include `CredentialResolver`, `AIProvider`, `ProviderResolver`, provider API keys, or credential values.
|
|
@@ -446,6 +465,7 @@ const dbStore: ProductionPersistenceStore = {
|
|
|
446
465
|
## Related APIs
|
|
447
466
|
|
|
448
467
|
- [Session store conformance](session-store-conformance.md): executable adapter baseline for append/idempotency/conflict/branch invariants.
|
|
468
|
+
- [Enterprise PostgreSQL state](enterprise-postgres-state.md): durable governance and connector/router state outside the session/run contract.
|
|
449
469
|
- [Migration guide](migration.md): before/after shapes for moving from in-memory/JSONL to this contract.
|
|
450
470
|
- [Performance limits](performance.md): production sizing, subscriber queues, branch-read limits, and database adapter guidance.
|
|
451
471
|
- [Session stores and branching](session-stores-and-branching.md): `SessionStore`, `SessionEntry`, branch helpers, and runtime branch semantics.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Device adapters
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
Optional realtime voice and desktop OS / computer-control surface for Prism agents, shipped in 0.0.14 as a **contract + deny-by-default policy only** in `@arnilo/prism` (`src/devices.ts`). No vendor voice or desktop-control implementation ships in 0.0.14 — those are demand-gated to 0.1.x. The contract composes over the existing `PermissionPolicy`, `RunLimits`, approval (`tool_approval`), and redactor seams; it adds no second approval runtime and no device framework.
|
|
6
|
+
|
|
7
|
+
## When to use it
|
|
8
|
+
|
|
9
|
+
- A host wants to admit a realtime voice or desktop-control device for an agent run and needs a fail-closed policy boundary before writing the vendor adapter.
|
|
10
|
+
- You need conformance fixtures (denial, approval, stream bounds, session budget, run accounting, redaction) to validate a future vendor adapter against the deny-by-default contract.
|
|
11
|
+
- You must guarantee device side effects never run without explicit consent + sandbox + approval, and never replay after reconnect.
|
|
12
|
+
|
|
13
|
+
Do **not** use it to broaden consent, memory, network, file, browser, connector, or tool permissions (roadmap gate 8 forbids this).
|
|
14
|
+
|
|
15
|
+
## Inputs / request
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import type { DeviceAdapter, DevicePolicyOptions, DeviceAdmitRequest } from "@arnilo/prism";
|
|
19
|
+
|
|
20
|
+
const adapter: DeviceAdapter = {
|
|
21
|
+
kind: "voice", // "voice" | "desktop-control"
|
|
22
|
+
enabled: false, // deny-by-default: admit only on explicit true
|
|
23
|
+
requireApproval: true, // every side effect requires approval
|
|
24
|
+
sandbox: "sandbox-a", // host-owned sandbox id (required to admit)
|
|
25
|
+
network: "egress-strict", // host-owned network/egress policy id
|
|
26
|
+
limits: { maxChunkBytes: 1_048_576, maxConcurrentSessions: 1 },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const options: DevicePolicyOptions = { runLimits: { maxTurns: 8, maxToolCalls: 50 } };
|
|
30
|
+
const admit: DeviceAdmitRequest = { approved: true, activeSessions: 0 };
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Outputs / response / events
|
|
34
|
+
|
|
35
|
+
| Export | Purpose |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `resolveDevicePolicy(adapter, options?)` | Resolve caps; reject unknown kinds and caps above the hard ceiling. |
|
|
38
|
+
| `assertDeviceAdmit(policy, request)` | Fail-closed admission gate (disabled / unsandboxed / unapproved / over-budget / unaccounted all deny). |
|
|
39
|
+
| `acceptDeviceChunk(policy, bytes)` | Stream bound: oversize chunks dropped with `marker: "dropped_oversize"`, never forwarded. |
|
|
40
|
+
| `redactDeviceTelemetry(redactor, telemetry)` | Metadata-safe telemetry: apply the host redactor before any emit/persist. |
|
|
41
|
+
| `runDevicePolicyConformance(adapter, options?)` | Conformance pair for future vendor adapters; returns `{ passed }`. |
|
|
42
|
+
| `DevicePolicyError` | Stable error (`ERR_PRISM_DEVICE_DISABLED` / `_APPROVAL` / `_SESSIONS` / `_CHUNK` / `_RUN_LIMITS` / `_INPUT`). |
|
|
43
|
+
|
|
44
|
+
## Request/response example
|
|
45
|
+
|
|
46
|
+
```jsonc
|
|
47
|
+
// assertDeviceAdmit on a disabled device throws (fail closed):
|
|
48
|
+
// DevicePolicyError: voice device is disabled by default (ERR_PRISM_DEVICE_DISABLED)
|
|
49
|
+
|
|
50
|
+
// acceptDeviceChunk(policy, 9_000_000) with a 1 MiB cap:
|
|
51
|
+
{ "accepted": false, "bytes": 9000000, "marker": "dropped_oversize" }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Implementation example
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import {
|
|
58
|
+
assertDeviceAdmit,
|
|
59
|
+
acceptDeviceChunk,
|
|
60
|
+
redactDeviceTelemetry,
|
|
61
|
+
resolveDevicePolicy,
|
|
62
|
+
createSecretRedactor,
|
|
63
|
+
} from "@arnilo/prism";
|
|
64
|
+
|
|
65
|
+
const policy = resolveDevicePolicy(
|
|
66
|
+
{ kind: "desktop-control", enabled: true, requireApproval: true, sandbox: "sandbox-a" },
|
|
67
|
+
{ runLimits: { maxTurns: 8, maxToolCalls: 50 } },
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Re-admit on every resume (side effects never replay after reconnect).
|
|
71
|
+
assertDeviceAdmit(policy, { approved: hostApprovedSideEffect, activeSessions: currentSessions });
|
|
72
|
+
|
|
73
|
+
// Stream bound + redaction on each audio/screenshot chunk.
|
|
74
|
+
const chunk = acceptDeviceChunk(policy, frameBytes);
|
|
75
|
+
if (chunk.accepted) emit(redactDeviceTelemetry(createSecretRedactor([token]), frame));
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Extension and configuration notes
|
|
79
|
+
|
|
80
|
+
- Frozen caps: audio/screenshot/stream chunk **1 MiB / 8 MiB**; concurrent device sessions per identity **1 / 4**. Device wall time / turns / tool calls consume the shared `RunLimits` (admission fails closed without run accounting).
|
|
81
|
+
- `enabled` resolves to `true` only on an explicit `true`; any other value is disabled. `requireApproval` stays `true` unless the host explicitly sets `false` (it should not).
|
|
82
|
+
- Vendor voice / desktop-control packages are **deferred to 0.1.x** and ship only if Task 0 records measured demand. This page documents the contract they must satisfy via `runDevicePolicyConformance`.
|
|
83
|
+
|
|
84
|
+
## Security and performance notes
|
|
85
|
+
|
|
86
|
+
- Deny-by-default: admission requires explicit `enabled`, an explicit `sandbox`, approval (when required), an under-budget session count, and shared `RunLimits` — any missing condition fails closed.
|
|
87
|
+
- Side effects never replay after reconnect: hosts must re-admit on every resume/interruption.
|
|
88
|
+
- Secrets are isolated from audio/screenshot/stream paths: apply `redactDeviceTelemetry` before any emit/persist; telemetry must be metadata-safe.
|
|
89
|
+
- No permission broadening: device adapters cannot widen consent, memory, network, file, browser, connector, or tool permissions (gate 8).
|
|
90
|
+
|
|
91
|
+
## Related APIs
|
|
92
|
+
|
|
93
|
+
- [Browser automation](browser-automation.md): verified-state checkpoints + reload/verify-before-side-effect for browser composition.
|
|
94
|
+
- [Conversations](conversations.md): durable threads that own the runs device sessions bind to.
|
|
95
|
+
- [Host security](host-security.md): approval, sandbox, and egress trust boundaries device adapters compose over.
|
|
96
|
+
- [Performance and resource limits](performance.md): shared `RunLimits` accounting.
|
|
97
|
+
- [Migration](migration.md): 0.0.14 additive seams and 0.1.x device vendor deferral.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Enterprise PostgreSQL state
|
|
2
|
+
|
|
3
|
+
## What it does
|
|
4
|
+
|
|
5
|
+
`@arnilo/prism-enterprise-postgres` is one optional PostgreSQL composition for the four existing enterprise state seams:
|
|
6
|
+
|
|
7
|
+
| State | Composition property | Durable behavior |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| Policy decisions | `policy` | Append-only `PolicyDecisionStore` with owner-bound cursor pages. |
|
|
10
|
+
| Evaluations | `evaluations` | `EvaluationStore` append/query records with exact owner pages. |
|
|
11
|
+
| Work mutations | `workIdempotency` | Atomic claim/CAS lifecycle for connector effects. |
|
|
12
|
+
| Model routing | `modelRouter` | Shared rate, budget, and circuit state for router replicas. |
|
|
13
|
+
| Tool effects | `toolEffects` | Durable `ToolEffectStore` claim/CAS for recoverable tool side effects (migration 002). |
|
|
14
|
+
| Tool effects | `toolEffects` | Durable `ToolEffectStore` claim/CAS for recoverable tool side effects (migration 002). |
|
|
15
|
+
|
|
16
|
+
`createPostgresEnterpriseState()` opens a host-supplied or adapter-owned `pg` pool, verifies/applies checksum-protected enterprise migrations (`001_enterprise_state`, `002_tool_effects`), 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
|
+
## When to use it
|
|
19
|
+
|
|
20
|
+
Use it for a multi-process production host that needs policy/evaluation audit state, connector mutation reconciliation, or model-router limits to survive restart and coordinate across replicas.
|
|
21
|
+
|
|
22
|
+
Use memory/file stores only for tests, demos, or a deliberately single-process host. They do not provide PostgreSQL cross-replica coordination. This package is optional; it adds no database driver to `@arnilo/prism` core.
|
|
23
|
+
|
|
24
|
+
## Inputs / request
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { createPostgresEnterpriseState } from "@arnilo/prism-enterprise-postgres";
|
|
28
|
+
import { Pool } from "pg";
|
|
29
|
+
|
|
30
|
+
const pool = new Pool({
|
|
31
|
+
connectionString: process.env.DATABASE_URL,
|
|
32
|
+
max: 10,
|
|
33
|
+
ssl: { rejectUnauthorized: true },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const state = await createPostgresEnterpriseState({ pool, schema: "prism" });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Input | Meaning |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `pool` | Existing `pg` pool. Exactly one of `pool` and `connectionString` is required; caller retains pool lifecycle. |
|
|
42
|
+
| `connectionString` | Creates an adapter-owned pool when `pool` is omitted. |
|
|
43
|
+
| `schema` | Validated identifier; defaults to `"prism"`. It is never interpolated as an unchecked SQL identifier. |
|
|
44
|
+
| `poolMax` / `poolConfig` | Adapter-owned pool settings; maximum defaults to 10 and is capped at 100. Put TLS in `poolConfig.ssl`. |
|
|
45
|
+
| `skipMigrations` | Isolated-test escape hatch only. Production opens verify/apply the fixed migration before request traffic. |
|
|
46
|
+
| `cleanup({ tenantId, accountId?, userId?, principalId, limit?, signal? })` | Explicit exact-owner cleanup; default 100 and hard maximum 500 rows. No worker starts automatically. |
|
|
47
|
+
|
|
48
|
+
Every durable policy/work/router action starts from an active host-verified `AgentIdentity`. Evaluation records/queries must be projected by the host from that verified ownership. PostgreSQL rejects missing tenant scope; optional account/user values are normalized and matched exactly, including absence.
|
|
49
|
+
|
|
50
|
+
## Outputs / response / events
|
|
51
|
+
|
|
52
|
+
`PostgresEnterpriseState` has this public shape:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
interface PostgresEnterpriseState {
|
|
56
|
+
readonly policy: PolicyDecisionStore;
|
|
57
|
+
readonly evaluations: EvaluationStore;
|
|
58
|
+
readonly workIdempotency: IdempotencyStore;
|
|
59
|
+
readonly modelRouter: ModelRouterStateStore;
|
|
60
|
+
readonly toolEffects: ToolEffectStore;
|
|
61
|
+
readonly toolEffects: ToolEffectStore;
|
|
62
|
+
cleanup(input: EnterpriseStateCleanupInput): Promise<EnterpriseStateCleanupResult>;
|
|
63
|
+
close(): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`close()` ends only a pool created from `connectionString`; it leaves a caller-owned pool open. `cleanup()` returns `{ removed, transitioned }`. It transitions expired work claims to `unknown` and abandoned circuit probes back to cooldown before deleting expired/idle retained state.
|
|
68
|
+
|
|
69
|
+
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
|
+
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
|
+
|
|
73
|
+
## Request/response example
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"schema": "prism",
|
|
78
|
+
"cleanup": {
|
|
79
|
+
"tenantId": "tenant-1",
|
|
80
|
+
"userId": "user-7",
|
|
81
|
+
"principalId": "agent-9",
|
|
82
|
+
"limit": 100
|
|
83
|
+
},
|
|
84
|
+
"result": { "removed": 12, "transitioned": 1 }
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
A migration creates `prism_policy_decisions`, `prism_evaluations`, `prism_work_idempotency`, three `prism_model_router_*` tables, and its separate `prism_enterprise_migrations` history. Startup serializes per-schema setup with an advisory transaction lock and rejects checksum or catalog drift rather than silently repairing it.
|
|
89
|
+
|
|
90
|
+
## Implementation example
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import type { AgentIdentity } from "@arnilo/prism";
|
|
94
|
+
import { createPostgresEnterpriseState, type PostgresEnterpriseState } from "@arnilo/prism-enterprise-postgres";
|
|
95
|
+
|
|
96
|
+
const identity: AgentIdentity = {
|
|
97
|
+
tenantId: "tenant-1",
|
|
98
|
+
userId: "user-7",
|
|
99
|
+
principal: { kind: "agent", id: "agent-9" },
|
|
100
|
+
scopes: ["enterprise:write"],
|
|
101
|
+
verified: true,
|
|
102
|
+
issuedAt: "2026-08-03T00:00:00.000Z",
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export async function recordEnterpriseState(state: PostgresEnterpriseState) {
|
|
106
|
+
const now = new Date().toISOString();
|
|
107
|
+
await state.policy.append({
|
|
108
|
+
id: "policy-1",
|
|
109
|
+
policyId: "mail",
|
|
110
|
+
policyVersion: "2026-08-03",
|
|
111
|
+
outcome: "approval",
|
|
112
|
+
identity,
|
|
113
|
+
target: { kind: "draft", id: "draft-1" },
|
|
114
|
+
evidenceRefs: ["rule:external-recipient"],
|
|
115
|
+
createdAt: now,
|
|
116
|
+
});
|
|
117
|
+
await state.evaluations.append({
|
|
118
|
+
id: "eval-1",
|
|
119
|
+
scorerId: "quality",
|
|
120
|
+
status: "scored",
|
|
121
|
+
score: 1,
|
|
122
|
+
sampled: true,
|
|
123
|
+
tenantId: identity.tenantId,
|
|
124
|
+
userId: identity.userId,
|
|
125
|
+
createdAt: now,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const claim = await state.workIdempotency.begin({ identity, key: "mail-send-1", op: "mail.send" });
|
|
129
|
+
if (claim.outcome === "acquired") {
|
|
130
|
+
// Run approved connector effect outside PostgreSQL transaction.
|
|
131
|
+
await state.workIdempotency.complete({
|
|
132
|
+
identity,
|
|
133
|
+
key: "mail-send-1",
|
|
134
|
+
op: "mail.send",
|
|
135
|
+
claimToken: claim.record.claimToken!,
|
|
136
|
+
expectedVersion: claim.record.version,
|
|
137
|
+
result: { draftId: "draft-1", resourceId: "message-1" },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await state.modelRouter.addUsage({
|
|
142
|
+
key: { tenantId: "tenant-1", userId: "user-7", principalId: "agent-9", provider: "openai", model: "gpt-4.1-mini" },
|
|
143
|
+
tokens: 100,
|
|
144
|
+
windowMs: 86_400_000,
|
|
145
|
+
now: Date.now(),
|
|
146
|
+
});
|
|
147
|
+
return state.cleanup({ tenantId: "tenant-1", userId: "user-7", principalId: "agent-9" });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// `state` comes from `await createPostgresEnterpriseState({ pool, schema: "prism" })`.
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Extension and configuration notes
|
|
154
|
+
|
|
155
|
+
- `createModelRouter({ resolver, stateStore: state.modelRouter })` keeps allow-list, residency, fallback, and diagnostics behavior in `@arnilo/prism-model-router`; this package only supplies durable state.
|
|
156
|
+
- Policy/evaluation/query public contracts stay in their owning packages. This package exports only `createPostgresEnterpriseState`, its options/result types, and `EnterprisePostgresError`; it has no SQL, DDL, codec, queryable, or migration subpath.
|
|
157
|
+
- 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.
|
|
158
|
+
- 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).
|
|
159
|
+
- Request-path state SQL uses `SELECT`, `INSERT`, `UPDATE`, and `DELETE` on the six 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.
|
|
160
|
+
|
|
161
|
+
## Security and performance notes
|
|
162
|
+
|
|
163
|
+
- Configure TLS, database credentials, pool timeouts, backups, restore drills, retention schedule, and database role grants in the host. Never put connection strings, tokens, prompts, raw connector responses, unrestricted payloads, or provider credentials in records.
|
|
164
|
+
- 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.
|
|
165
|
+
- 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.
|
|
166
|
+
- 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.
|
|
167
|
+
- 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.
|
|
168
|
+
|
|
169
|
+
## Related APIs
|
|
170
|
+
|
|
171
|
+
- [Policy and audit](policy-and-audit.md): policy record and WORM export semantics.
|
|
172
|
+
- [Evaluations](evaluations.md): scorer/evaluation record lifecycle.
|
|
173
|
+
- [Work tools](work-tools.md): draft approval, unknown outcomes, and connector boundaries.
|
|
174
|
+
- [Model routing](model-routing.md): durable asynchronous router migration.
|
|
175
|
+
- [PostgreSQL persistence](postgres-persistence.md): sessions/runs/checkpoints/leases adapter.
|
|
176
|
+
- [Database persistence](database-persistence.md): host retention and persistence guidance.
|
|
177
|
+
- [Host security](host-security.md): database ownership, TLS, secrets, and role boundaries.
|
|
178
|
+
- [Migration guide](migration.md): 0.0.22 → 0.0.23 upgrade steps.
|
package/docs/evaluations.md
CHANGED
|
@@ -146,11 +146,24 @@ Release 0.0.9 ships curated network-free adversarial fixtures in package tests:
|
|
|
146
146
|
|
|
147
147
|
Fixtures reuse `@arnilo/prism-evals` (`defineDataset` / `defineScorer` / `scoreRun` / `assertEvaluationThreshold` / `serializeEvaluationReport`). Optional SWE-bench-compatible or live-browser harnesses remain host adapters — they are not default dependencies or quality claims. Protected real Docker/Playwright gates stay env-gated (`PRISM_TEST_DOCKER_SANDBOX`, `PRISM_LIVE_PLAYWRIGHT`) and never enter `sdk:ready`.
|
|
148
148
|
|
|
149
|
+
## PostgreSQL enterprise state (0.0.23)
|
|
150
|
+
|
|
151
|
+
`createPostgresEnterpriseState({ pool, schema }).evaluations` implements this package's existing `EvaluationStore`. The host creates an `EvaluationRecord` from verified ownership before append; every PostgreSQL query requires tenant scope, uses exact normalized account/user matching, and returns owner-bound opaque cursor pages. It is durable across reopen and supports the existing id/scorer/session/run/trace/dataset/item/experiment/status filters.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const state = await createPostgresEnterpriseState({ pool });
|
|
155
|
+
await state.evaluations.append(record); // record is already host-owned and redacted
|
|
156
|
+
const page = await state.evaluations.query({ tenantId: "t1", userId: "u1", status: "scored", limit: 100 });
|
|
157
|
+
```
|
|
158
|
+
|
|
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
|
+
|
|
149
161
|
## Related APIs
|
|
150
162
|
|
|
151
163
|
- [Agent/session runtime](agent-session-runtime.md): `AgentRunResult` and `session.run()`
|
|
152
164
|
- [Runs and usage ledger](runs-and-usage.md): run/session identity for score linkage
|
|
153
165
|
- [Observability](observability.md): use `onTraceReference` or bounded `traceId(runId)` to supply `ScoreRunOptions.traceId`; evaluation telemetry emits no reason/explanation content
|
|
154
166
|
- [Coding agent tools](coding-agent-tools.md) / [Browser automation](browser-automation.md) / [Workflows](workflows.md): network-free coding-task composition at `examples/durable-coding-workflow.ts`; adversarial coding/browser eval example at `examples/coding-browser-evaluation.ts`
|
|
155
|
-
- [Performance limits](performance.md): `scripts/benchmark-0.0.9.mjs` coding/browser evidence fields
|
|
167
|
+
- [Performance limits](performance.md): `scripts/benchmark-0.0.11.mjs` search/budget evidence, `scripts/benchmark-0.0.10.mjs` workspace-mode evidence, and `scripts/benchmark-0.0.9.mjs` coding/browser evidence fields
|
|
168
|
+
- [Enterprise PostgreSQL state](enterprise-postgres-state.md): durable owner-scoped evaluation storage.
|
|
156
169
|
- [Release and install](release-and-install.md): optional package install and protected sandbox-browser workflow
|