@arnilo/prism 0.0.13 → 0.0.14

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +8 -2
  3. package/dist/artifacts.d.ts +78 -0
  4. package/dist/artifacts.js +24 -0
  5. package/dist/contracts.d.ts +10 -0
  6. package/dist/contracts.js +8 -0
  7. package/dist/conversations.d.ts +50 -0
  8. package/dist/conversations.js +97 -0
  9. package/dist/credentials.d.ts +14 -0
  10. package/dist/credentials.js +9 -0
  11. package/dist/devices.d.ts +94 -0
  12. package/dist/devices.js +138 -0
  13. package/dist/index.d.ts +10 -4
  14. package/dist/index.js +6 -3
  15. package/dist/providers/openai-primitives.js +5 -2
  16. package/docs/ag-ui.md +5 -0
  17. package/docs/browser-automation.md +3 -0
  18. package/docs/conversations.md +135 -0
  19. package/docs/credential-storage.md +28 -1
  20. package/docs/credentials-and-redaction.md +2 -0
  21. package/docs/database-persistence.md +5 -1
  22. package/docs/device-adapters.md +97 -0
  23. package/docs/host-security.md +4 -2
  24. package/docs/index.md +16 -12
  25. package/docs/migration.md +21 -0
  26. package/docs/performance.md +2 -0
  27. package/docs/policy-and-audit.md +1 -0
  28. package/docs/provider-caching.md +4 -0
  29. package/docs/provider-packages.md +4 -1
  30. package/docs/providers/alibaba.md +179 -0
  31. package/docs/providers/ollama.md +166 -0
  32. package/docs/release-and-install.md +75 -5
  33. package/docs/review-coverage-2026-07-25-phase-9.md +256 -0
  34. package/docs/server.md +4 -0
  35. package/docs/work-artifacts-and-review.md +100 -0
  36. package/docs/work-connectors.md +5 -1
  37. package/docs/work-tools.md +3 -0
  38. package/docs/workflows.md +4 -0
  39. package/docs/working-and-semantic-memory.md +20 -5
  40. package/package.json +1 -1
  41. package/templates/init/providers.json +22 -0
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type * from "./contracts.js";
2
2
  export type { RunLimitCounters, RunLimitName, SecureAgentOptions } from "./contracts.js";
3
- export { isSessionEntryKind, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SessionAppendConflictError, isSessionAppendConflict, AgentRunError, AgentRunStateError, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SESSION_SEARCH_UNSUPPORTED_CODE, SessionSearchUnsupportedError, isSessionSearchUnsupported, DEFAULT_SESSION_SEARCH_LIMIT, HARD_MAX_SESSION_SEARCH_LIMIT, DEFAULT_MAX_SESSION_SEARCH_QUERY_BYTES, HARD_MAX_SESSION_SEARCH_QUERY_BYTES, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, HARD_MAX_SESSION_SEARCH_SNIPPET_BYTES, DEFAULT_MAX_SESSION_SEARCH_CURSOR_BYTES, HARD_MAX_SESSION_SEARCH_CURSOR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_FTS_CANDIDATES, HARD_MAX_SESSION_SEARCH_FTS_CANDIDATES, resolveSessionSearchQuery, DEFAULT_MAX_PENDING_STEERS, HARD_MAX_PENDING_STEERS, DEFAULT_MAX_PENDING_STEER_BYTES, HARD_MAX_PENDING_STEER_BYTES } from "./contracts.js";
3
+ export { isSessionEntryKind, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SessionAppendConflictError, isSessionAppendConflict, AgentRunError, AgentRunStateError, assertSessionMetadataKey, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SESSION_SEARCH_UNSUPPORTED_CODE, SessionSearchUnsupportedError, isSessionSearchUnsupported, DEFAULT_SESSION_SEARCH_LIMIT, HARD_MAX_SESSION_SEARCH_LIMIT, DEFAULT_MAX_SESSION_SEARCH_QUERY_BYTES, HARD_MAX_SESSION_SEARCH_QUERY_BYTES, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, HARD_MAX_SESSION_SEARCH_SNIPPET_BYTES, DEFAULT_MAX_SESSION_SEARCH_CURSOR_BYTES, HARD_MAX_SESSION_SEARCH_CURSOR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_FTS_CANDIDATES, HARD_MAX_SESSION_SEARCH_FTS_CANDIDATES, resolveSessionSearchQuery, DEFAULT_MAX_PENDING_STEERS, HARD_MAX_PENDING_STEERS, DEFAULT_MAX_PENDING_STEER_BYTES, HARD_MAX_PENDING_STEER_BYTES } from "./contracts.js";
4
4
  export { createAgent, createAgentSession, resumeAgentRun, resumeAgentRunStream } from "./agents.js";
5
5
  export { createBatchedRunLedger, isFlushableRunLedger, DEFAULT_LEDGER_BATCH_ENTRIES, HARD_LEDGER_BATCH_ENTRIES, DEFAULT_LEDGER_BATCH_BYTES, HARD_LEDGER_BATCH_BYTES, DEFAULT_LEDGER_BATCH_DELAY_MS, HARD_LEDGER_BATCH_DELAY_MS, } from "./run-ledger.js";
6
6
  export type { BatchedRunLedgerOptions } from "./run-ledger.js";
@@ -30,10 +30,10 @@ export { createDefaultRetryPolicy, isTransientErrorInfo, waitForRetry } from "./
30
30
  export type { DefaultRetryPolicyOptions } from "./retry.js";
31
31
  export { createContributionRegistries, createContributionRegistry, registerDiscoveredContributions } from "./contributions.js";
32
32
  export type { ContributionRegistries, ContributionRegistriesOptions, ContributionRegistry, ContributionRegistryOptions } from "./contributions.js";
33
- export { createChainedCredentialResolver, createEnvCredentialResolver, createExplicitCredentialResolver, createMemoryCredentialStore, refreshOAuthCredential, resolveCredentialValue } from "./credentials.js";
33
+ export { createChainedCredentialResolver, createEnvCredentialResolver, createExplicitCredentialResolver, createMemoryCredentialStore, refreshOAuthCredential, revokeOAuthCredential, resolveCredentialValue } from "./credentials.js";
34
34
  export { createExtensionEventBus, createExtensionKernel } from "./extensions.js";
35
35
  export type { ExtensionErrorPolicy, ExtensionEventBus, ExtensionEventHandler, ExtensionKernel, ExtensionKernelOptions, ExtensionLoadPolicy } from "./extensions.js";
36
- export type { CredentialRecord, CredentialValueSource, MemoryCredentialStore } from "./credentials.js";
36
+ export type { CredentialRecord, CredentialValueSource, MemoryCredentialStore, RevocableOAuthCredentialStore } from "./credentials.js";
37
37
  export { createModelRegistry } from "./models.js";
38
38
  export { authMethodKey, defineProviderPackage, systemPromptContributionKey } from "./provider-packages.js";
39
39
  export { assertStructuredOutputRequestSupported, artifactStructuredOutputRequest, DEFAULT_MAX_STRUCTURED_OUTPUT_NAME_LENGTH, DEFAULT_MAX_STRUCTURED_OUTPUT_SCHEMA_BYTES, modelSupportsStructuredOutput, resolveRunProviderOptions, StructuredOutputError, validateStructuredOutputOptions, withoutStructuredOutput, } from "./structured-output.js";
@@ -62,6 +62,12 @@ export type { ContextBudget, ContextBudgetMessageGroups, ContextBudgetOmission,
62
62
  export type { AgentInput, AssembleProviderInputOptions, DefaultInputBuilder, DefaultInputBuildContext, DefaultPromptBuilder, InputAttachment, PromptInstruction, PromptTemplateOptions, ResolveContextOptions } from "./input.js";
63
63
  export { createMockProvider } from "./mock-provider.js";
64
64
  export { createMemorySessionStore, createSessionEntry, getSessionBranchEntries, listSessionBranches, rebuildSessionContext } from "./session-stores.js";
65
+ export { CONVERSATION_METADATA_KEY, DEFAULT_MAX_CONVERSATION_CURSOR_BYTES, HARD_MAX_CONVERSATION_CURSOR_BYTES, ConversationError, conversationMarkerMetadata, conversationThreadFromRecord, decodeConversationReplayCursor, encodeConversationReplayCursor, } from "./conversations.js";
66
+ export type { ConversationBranchRef, ConversationReplayCursor, ConversationThread, ConversationThreadState, } from "./conversations.js";
67
+ export { ARTIFACT_CHECKPOINT_NAMESPACE, ArtifactError, artifactApprovalState, artifactCheckpointKey, } from "./artifacts.js";
68
+ export type { ArtifactApproval, ArtifactApprovalState, ArtifactCitation, ArtifactDecisionState, ArtifactDeliveryToken, ArtifactRecord, ArtifactRevision, } from "./artifacts.js";
69
+ export { DEFAULT_DEVICE_MAX_CHUNK_BYTES, DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS, HARD_DEVICE_MAX_CHUNK_BYTES, HARD_DEVICE_MAX_CONCURRENT_SESSIONS, DevicePolicyError, acceptDeviceChunk, assertDeviceAdmit, redactDeviceTelemetry, resolveDevicePolicy, runDevicePolicyConformance, } from "./devices.js";
70
+ export type { DeviceAdapter, DeviceAdmitRequest, DeviceChunkResult, DeviceConformanceResult, DeviceKind, DevicePolicyErrorCode, DevicePolicyOptions, DeviceStreamLimits, ResolvedDevicePolicy, } from "./devices.js";
65
71
  export type { CreateMemorySessionStoreOptions, CreateSessionEntryOptions, MemorySessionSearchMode, SessionBranch, SessionBranchOptions, SessionContextSnapshot } from "./session-stores.js";
66
72
  export type { MockProviderOptions } from "./mock-provider.js";
67
73
  export { providerContentDelta, providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, toolCallContent, toolCallFromArgumentsText, } from "./provider-events.js";
@@ -89,5 +95,5 @@ export type { DispatchToolCallOptions, ToolArgumentValidationError, ToolArgument
89
95
  export type { DuplicateRegistrationOptions, DuplicateRegistrationPolicy } from "./registry-options.js";
90
96
  export { dispatchToolCallsInOrder, generateValidateReviseLoop, isAgentLoopOptions, resolveLoop, resolveToolConcurrency, singleShotLoop } from "./agent-loops.js";
91
97
  export declare const name = "prism";
92
- export declare const version = "0.0.13";
98
+ export declare const version = "0.0.14";
93
99
  export declare const description = "Agent harness for AI providers, agents, sessions, and tools.";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { isSessionEntryKind, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SessionAppendConflictError, isSessionAppendConflict, AgentRunError, AgentRunStateError, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SESSION_SEARCH_UNSUPPORTED_CODE, SessionSearchUnsupportedError, isSessionSearchUnsupported, DEFAULT_SESSION_SEARCH_LIMIT, HARD_MAX_SESSION_SEARCH_LIMIT, DEFAULT_MAX_SESSION_SEARCH_QUERY_BYTES, HARD_MAX_SESSION_SEARCH_QUERY_BYTES, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, HARD_MAX_SESSION_SEARCH_SNIPPET_BYTES, DEFAULT_MAX_SESSION_SEARCH_CURSOR_BYTES, HARD_MAX_SESSION_SEARCH_CURSOR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_FTS_CANDIDATES, HARD_MAX_SESSION_SEARCH_FTS_CANDIDATES, resolveSessionSearchQuery, DEFAULT_MAX_PENDING_STEERS, HARD_MAX_PENDING_STEERS, DEFAULT_MAX_PENDING_STEER_BYTES, HARD_MAX_PENDING_STEER_BYTES } from "./contracts.js";
1
+ export { isSessionEntryKind, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SessionAppendConflictError, isSessionAppendConflict, AgentRunError, AgentRunStateError, assertSessionMetadataKey, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SESSION_SEARCH_UNSUPPORTED_CODE, SessionSearchUnsupportedError, isSessionSearchUnsupported, DEFAULT_SESSION_SEARCH_LIMIT, HARD_MAX_SESSION_SEARCH_LIMIT, DEFAULT_MAX_SESSION_SEARCH_QUERY_BYTES, HARD_MAX_SESSION_SEARCH_QUERY_BYTES, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, HARD_MAX_SESSION_SEARCH_SNIPPET_BYTES, DEFAULT_MAX_SESSION_SEARCH_CURSOR_BYTES, HARD_MAX_SESSION_SEARCH_CURSOR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_FTS_CANDIDATES, HARD_MAX_SESSION_SEARCH_FTS_CANDIDATES, resolveSessionSearchQuery, DEFAULT_MAX_PENDING_STEERS, HARD_MAX_PENDING_STEERS, DEFAULT_MAX_PENDING_STEER_BYTES, HARD_MAX_PENDING_STEER_BYTES } from "./contracts.js";
2
2
  export { createAgent, createAgentSession, resumeAgentRun, resumeAgentRunStream } from "./agents.js";
3
3
  export { createBatchedRunLedger, isFlushableRunLedger, DEFAULT_LEDGER_BATCH_ENTRIES, HARD_LEDGER_BATCH_ENTRIES, DEFAULT_LEDGER_BATCH_BYTES, HARD_LEDGER_BATCH_BYTES, DEFAULT_LEDGER_BATCH_DELAY_MS, HARD_LEDGER_BATCH_DELAY_MS, } from "./run-ledger.js";
4
4
  export { createSecureAgent } from "./secure-agent.js";
@@ -16,7 +16,7 @@ export { assertJsonObject, isJsonObject, loadConfigLayers, mergeConfigLayers } f
16
16
  export { createDefaultCompactionStrategy, isCompactionEntryData } from "./compaction.js";
17
17
  export { createDefaultRetryPolicy, isTransientErrorInfo, waitForRetry } from "./retry.js";
18
18
  export { createContributionRegistries, createContributionRegistry, registerDiscoveredContributions } from "./contributions.js";
19
- export { createChainedCredentialResolver, createEnvCredentialResolver, createExplicitCredentialResolver, createMemoryCredentialStore, refreshOAuthCredential, resolveCredentialValue } from "./credentials.js";
19
+ export { createChainedCredentialResolver, createEnvCredentialResolver, createExplicitCredentialResolver, createMemoryCredentialStore, refreshOAuthCredential, revokeOAuthCredential, resolveCredentialValue } from "./credentials.js";
20
20
  export { createExtensionEventBus, createExtensionKernel } from "./extensions.js";
21
21
  export { createModelRegistry } from "./models.js";
22
22
  export { authMethodKey, defineProviderPackage, systemPromptContributionKey } from "./provider-packages.js";
@@ -35,6 +35,9 @@ export { assembleProviderInput, createDefaultInputBuilder, createDefaultPromptBu
35
35
  export { applyContextBudget, CONTEXT_BUDGET_ERROR_CODE, CONTEXT_BUDGET_REPORT_METADATA_KEY, ContextBudgetError, DEFAULT_MAX_CONTEXT_BUDGET_OMISSIONS, estimateAssemblyTokens, estimateMessageBytes, estimateMessageTokens, estimateTextBytes, estimateTextTokens, getContextBudgetReport, HARD_MAX_CONTEXT_BUDGET_BYTES, HARD_MAX_CONTEXT_BUDGET_OMISSIONS, HARD_MAX_CONTEXT_BUDGET_TOKENS, isContextBudgetError, resolveContextBudget, } from "./context-budget.js";
36
36
  export { createMockProvider } from "./mock-provider.js";
37
37
  export { createMemorySessionStore, createSessionEntry, getSessionBranchEntries, listSessionBranches, rebuildSessionContext } from "./session-stores.js";
38
+ export { CONVERSATION_METADATA_KEY, DEFAULT_MAX_CONVERSATION_CURSOR_BYTES, HARD_MAX_CONVERSATION_CURSOR_BYTES, ConversationError, conversationMarkerMetadata, conversationThreadFromRecord, decodeConversationReplayCursor, encodeConversationReplayCursor, } from "./conversations.js";
39
+ export { ARTIFACT_CHECKPOINT_NAMESPACE, ArtifactError, artifactApprovalState, artifactCheckpointKey, } from "./artifacts.js";
40
+ export { DEFAULT_DEVICE_MAX_CHUNK_BYTES, DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS, HARD_DEVICE_MAX_CHUNK_BYTES, HARD_DEVICE_MAX_CONCURRENT_SESSIONS, DevicePolicyError, acceptDeviceChunk, assertDeviceAdmit, redactDeviceTelemetry, resolveDevicePolicy, runDevicePolicyConformance, } from "./devices.js";
38
41
  export { providerContentDelta, providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, toolCallContent, toolCallFromArgumentsText, } from "./provider-events.js";
39
42
  export { createProviderRegistry, createProviderResolver } from "./providers.js";
40
43
  export { createSecretRedactor, errorToErrorInfo, redactAgentEvent, redactMessage, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry } from "./redaction.js";
@@ -48,6 +51,6 @@ export { assertGuardrailsAllowed, GuardrailError, MAX_GUARDRAIL_CONCURRENCY, run
48
51
  export { createRunLimitTracker, DEFAULT_RUN_LIMITS, HARD_MAX_RUN_COST, HARD_RUN_LIMITS, RunLimitError, RunLimitTracker, resolveRunLimits } from "./run-limits.js";
49
52
  export { dispatchToolCallsInOrder, generateValidateReviseLoop, isAgentLoopOptions, resolveLoop, resolveToolConcurrency, singleShotLoop } from "./agent-loops.js";
50
53
  export const name = "prism";
51
- export const version = "0.0.13";
54
+ export const version = "0.0.14";
52
55
  export const description = "Agent harness for AI providers, agents, sessions, and tools.";
53
56
  //# sourceMappingURL=index.js.map
@@ -115,7 +115,8 @@ export function mapOpenAIChatUsage(usage) {
115
115
  && wire.total_tokens === undefined
116
116
  && wire.prompt_cache_hit_tokens === undefined
117
117
  && wire.prompt_tokens_details?.cached_tokens === undefined
118
- && wire.prompt_tokens_details?.cache_write_tokens === undefined) {
118
+ && wire.prompt_tokens_details?.cache_write_tokens === undefined
119
+ && wire.prompt_tokens_details?.cache_creation_input_tokens === undefined) {
119
120
  return undefined;
120
121
  }
121
122
  return {
@@ -123,7 +124,9 @@ export function mapOpenAIChatUsage(usage) {
123
124
  outputTokens: wire.completion_tokens,
124
125
  totalTokens: wire.total_tokens,
125
126
  cacheReadTokens: wire.prompt_tokens_details?.cached_tokens ?? wire.prompt_cache_hit_tokens,
126
- cacheWriteTokens: wire.prompt_tokens_details?.cache_write_tokens,
127
+ // Some OpenAI-compatible vendors report cache writes as `cache_write_tokens`,
128
+ // others as the `cache_creation_input_tokens` variant; accept both.
129
+ cacheWriteTokens: wire.prompt_tokens_details?.cache_write_tokens ?? wire.prompt_tokens_details?.cache_creation_input_tokens,
127
130
  };
128
131
  }
129
132
  //# sourceMappingURL=openai-primitives.js.map
package/docs/ag-ui.md CHANGED
@@ -47,6 +47,8 @@ A Prism durable `agent_suspended` returns `RUN_FINISHED` with interrupt id `${ru
47
47
 
48
48
  ACP maps assistant text to `agent_message_chunk`, safe tool lifecycle to `tool_call`/`tool_call_update`, provider usage to `usage_update`, and durable suspension to `session/request_permission`. Only `allow_once` approves; reject, cancellation, unknown outcomes, and request failure deny. It advertises only close-session capability—no terminal, filesystem, MCP, editor state, location, diff, or raw input/output capability.
49
49
 
50
+ Co-work review (0.0.14) projects durable collaboration state over the same stream as named `CUSTOM` events, one per kind: `prism.cowork.artifact.progress`, `prism.cowork.artifact.approval.requested`, `prism.cowork.draft.connector.pending`, `prism.cowork.browser.snapshot`, and `prism.cowork.artifact.download.link`. `AgUiEventMapper.mapCoWork()` (and ACP `mapCoWork()` parity) validate, host-project (`AgUiProjection.coWork`), redact, and byte-cap each event; malformed or oversized events fail closed to nothing rather than leak. The handler accepts optional `coWorkContext` (thread/artifact/identity) and a durable `coWork` source (`createCoWorkReplay()`); one bounded, redacted page is appended after the run stream. Because projection is a pure read + map, disconnect/resume from a cursor replays co-work state without duplicate side effects. Download-link events carry an authorized, expiring token only — hosts fetch the body and never receive local paths, raw credentials, injected browser secrets, or tool-argument dumps.
51
+
50
52
  ## Request/response example
51
53
 
52
54
  ```json
@@ -105,6 +107,8 @@ All identity, authorization, session/thread mapping, durable checkpoint lookup,
105
107
 
106
108
  `AgUiProjection` is an allow-list. Without a callback, raw tool arguments/results/progress, paths, arbitrary state, raw Prism events, ACP locations/diffs/terminals/raw I/O, and frontend-supplied tools remain absent. Use a projector that returns a redacted display value, not a host filesystem path or tool payload.
107
109
 
110
+ Co-work projection reuses the same allow-list: `AgUiProjection.coWork(event)` may return a curated, JSON-serializable payload for a co-work event; absent it, the redacted event fields are exposed. Wire `coWorkContext` to derive thread/artifact/identity from the authorized request (never client JSON) and `coWork` to a `createCoWorkReplay()` over your durable artifact/draft/snapshot stores. The handler projects one bounded page after the run; mount a dedicated cursor-paged co-work endpoint when full pagination is needed.
111
+
108
112
  ## Security and performance notes
109
113
 
110
114
  Authorize every start, replay, resume, ACP new/prompt/cancel/close request. Treat thread IDs, run IDs, cursors, client messages, resume payloads, and protocol output as untrusted. Persist run ↔ protocol correlation before exposing an interrupt. Keep `SecretRedactor` active for streaming and ledger writes.
@@ -121,3 +125,4 @@ Benchmark command/result placeholder: Task 8 adds `node scripts/benchmark-0.0.12
121
125
  - [Web-standard server handler](server.md): generic Prism HTTP API, separate from AG-UI.
122
126
  - [A2A interoperability](a2a.md): remote agent-to-agent tasks, not frontend protocol mapping.
123
127
  - [Host security guide](host-security.md): authorization, ownership, redaction, and credential boundaries.
128
+ - [Work artifacts and review](work-artifacts-and-review.md): durable artifact service that produces the co-work approval/progress/download-link events projected here.
@@ -106,6 +106,7 @@ await browser.close();
106
106
  - Observation (`snapshot`, `wait`, open-without-url, `close`) vs mutation/high-impact (`navigate`, click/form, dialog accept, upload, download release, popup select) is classified for `ExecutionPolicy` / `beforeSideEffect`.
107
107
  - `createSharedSandboxBrowserOptions()` aligns browser uploads/downloads with Task 1 sandbox `/workspace` and `/downloads`. `assertBrowserSandboxNetwork()` in `@arnilo/prism-coding-security` fails closed for custom Docker networks without browser egress attestation.
108
108
  - Raw CSS is absent from production defaults. Ref resolution uses Playwright’s built-in `aria-ref=` selector with a package-owned snapshot ref table for staleness checks.
109
+ - Verified-state checkpoints (0.0.14): `createBrowserCheckpointLedger()` records navigation state — URL, a domain-state hash, and host-owned data refs — never serialized browser internals (cookies/storage/contexts), which are fragile and secret-bearing. Frozen caps: URL 8 KiB/16 KiB, domain-state hash 256 B/1 KiB, host-data ref 2 KiB/8 KiB (refs only, never bodies), 16/64 checkpoints per run (oldest evicted). After any resume/interruption `markResumed(runId)` marks state stale; `assertVerifiedBeforeSideEffect(runId)` fails closed until the host reloads + `verify()`s, so side effects never replay on stale state. Checkpoints are run-scoped: a conversation thread composes through the run it owns, reusing the manager's sandbox/egress/approval/limit policy above.
109
110
 
110
111
  ## Security and performance notes
111
112
 
@@ -121,4 +122,6 @@ Default tests use fake Playwright APIs only. Protected live gate: `PRISM_LIVE_PL
121
122
  - [Host security](host-security.md): browser endpoint, approval, egress proxy, and artifact trust boundaries.
122
123
  - [Performance and resource limits](performance.md): browser ceilings and charging points.
123
124
  - [Coding execution approval and sandboxing](coding-security.md): optional shared disposable sandbox for coding+browser.
125
+ - [Conversations](conversations.md): durable threads that own the runs browser checkpoints scope to.
126
+ - [Device adapters](device-adapters.md): deny-by-default voice/desktop-control contracts (no vendor package in 0.0.14).
124
127
  - [Migration](migration.md): additive optional package activation.
@@ -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`.
@@ -146,6 +146,33 @@ await refreshOAuthCredential({
146
146
  });
147
147
  ```
148
148
 
149
+ 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:
150
+
151
+ ```ts
152
+ import { revokeOAuthCredential } from "@arnilo/prism";
153
+ import {
154
+ createMicrosoft365OAuthProvider,
155
+ createGoogleWorkspaceOAuthProvider,
156
+ createOAuthWorkTokenProvider,
157
+ createOAuthCredentialStoreAdapter,
158
+ } from "@arnilo/prism-credentials-node";
159
+
160
+ // Read-only mail/calendar (no mutation scopes requested).
161
+ const m365 = createMicrosoft365OAuthProvider({ clientId: "<app-id>", capabilities: ["mail", "calendar"], access: "read" });
162
+ const creds = await m365.login({ onDeviceCode: ({ userCode, verificationUri }) => host.showCode(userCode, verificationUri) });
163
+ await createOAuthCredentialStoreAdapter(store).set("microsoft365", creds);
164
+
165
+ // Late-bound, per-identity token for a work-tools connector (env var, never argv/model context).
166
+ const tokenProvider = createOAuthWorkTokenProvider({
167
+ provider: m365,
168
+ store: createOAuthCredentialStoreAdapter(store),
169
+ envVar: "M365_ACCESSTOKEN",
170
+ });
171
+
172
+ // Revocation: best-effort upstream (GWS supports RFC 7009; M365 does not) + mandatory local delete.
173
+ await revokeOAuthCredential({ provider: m365, credentials: creds, store: createOAuthCredentialStoreAdapter(store) });
174
+ ```
175
+
149
176
  Passphrase rotation:
150
177
 
151
178
  ```ts
@@ -205,7 +232,7 @@ const providers = createOpenAIProviderPackage({ apiKey });
205
232
  - Use distinct `namespace` or vault paths per tenant/environment.
206
233
  - Keychain `list()` / `listOAuth()` are intentionally unsupported — enumerate credentials through host configuration instead of scanning the OS store.
207
234
  - Combine with `createExplicitCredentialResolver()` so runtime overrides still win over stored values.
208
- - 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; Anthropic and Google packages 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.
235
+ - 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.
209
236
  - 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.
210
237
 
211
238
  ## Security and performance notes
@@ -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
  ```
@@ -72,6 +72,10 @@ Important shapes:
72
72
  | `RetentionPolicy` | Policy with `maxAgeDays`, `maxEntriesPerSession`, `maxTotalBytes`, `archiveStore`, and `appliedKinds`. |
73
73
  | `MigrationRecord` | Applied migration with name, version, timestamp, checksum, and applied-by. |
74
74
 
75
+ 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.
76
+
77
+ 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.
78
+
75
79
  ## Outputs / response / events
76
80
 
77
81
  Each `query*` method returns a `PersistencePage<T>`:
@@ -338,7 +342,7 @@ A retention policy is a host-managed rule attached to sessions via `retention_po
338
342
 
339
343
  ```ts
340
344
  await store.lifecycle.putLegalHold({ tenantId, userId, resourceKind: "session", resourceId, reason });
341
- await store.lifecycle.applyRetention({ tenantId, userId, policy, candidates }); // hold wins over delete
345
+ 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
342
346
  await store.lifecycle.exportUnderHold({ tenantId, userId, cursor, limit }); // redacted
343
347
  await store.lifecycle.setTenantQuota({ tenantId, userId, resourceKind: "run", limit: 100 });
344
348
  await store.lifecycle.consumeTenantQuota({ tenantId, userId, resourceKind: "run" }); // fails closed when exhausted
@@ -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.
@@ -135,13 +135,15 @@ Wire those values where they matter: provider adapters receive the resolved cred
135
135
  - Redaction is exact known-secret replacement only. It is not arbitrary secret detection, entropy scanning, or DLP.
136
136
  - Known secrets must be passed into redactors before data is emitted or persisted. Redact again in host adapters if they transform records after Prism redaction.
137
137
  - Tool `parameters` metadata is not validated by default. Add a `ToolValidator`, use `createToolParameterValidator()` with a schema adapter, or install `@arnilo/prism-tool-validator-json-schema` before side effects. Its untrusted-schema adapter rejects non-local refs, forbidden keys/cycles/non-finite values and bounds bytes/depth/properties/keywords/refs plus its LRU cache before Ajv compilation; do not raise caps above documented hard limits.
138
- - Treat embeddings as untrusted numeric input. `@arnilo/prism-memory` rejects empty, non-number, NaN, and infinite vectors before in-memory similarity or pgvector parameters; custom `Embedder`/`VectorStore` implementations must retain the same boundary.
138
+ - Treat embeddings as untrusted numeric input. `@arnilo/prism-memory` rejects empty, non-number, NaN, and infinite vectors before in-memory similarity or pgvector parameters; custom `Embedder`/`VectorStore` implementations must retain the same boundary. Memory entries carry consent/source/visibility; revoked, invisible, or (in strict mode) consent-less entries never enter prompts, events, exports, or telemetry, and `forget`/`applyRetention` are real bounded deletes.
139
139
  - Evaluation trace readers require exact supplied ownership plus session/run identity, reject cursor/identity drift, and redact before bounded scorer/judge input. Model-judge callbacks receive no credential resolver, tools, or workspace; keep live judges outside default CI and redact report artifacts.
140
140
  - Prism-generated session/run/tool/workflow/evaluation IDs use Node cryptographic UUIDs. Keep host-provided IDs authorization-scoped and validate them as untrusted identifiers; do not substitute timestamps or `Math.random()` for durable/security-relevant IDs.
141
141
  - MCP client tools from `@arnilo/prism-mcp` are untrusted remote servers. Stdio remains an explicit host executable. Streamable HTTP requires exact HTTPS origins, rejects credentials/fragments/redirects/private or mixed DNS, pins a validated address on every SDK request/reconnect, and bounds each response; plaintext is explicit loopback-only development mode. Discovery has finite page/tool/cursor/metadata/schema totals and commits atomically. Every result branch shares byte/depth/property bounds before core dispatch; supply a known-secret `SecretRedactor`, `PermissionPolicy`, and `ToolValidator` there. MCP server direction exposes only passed tools/commands/resources/prompts, requires per-operation `authorize`, and retains core gates. Sampling, roots, model/credential selection, and elicitation consent stay host-owned; URL elicitation is never opened automatically. Stateful web mode requires host `resolveAuthInfo` plus `resolveIdentity`, exact origin policy, and binds every POST/GET/DELETE/SSE request to one non-secret principal; mismatches return 404. Handler still needs TLS and edge rate limiting. See [MCP client/server exposure](mcp-tools.md).
142
- - `@arnilo/prism-server` exposes no agent/workflow by default and requires `authorize()` for every matched operation. Derive complete tenant/account/user ownership from validated host identity, never request JSON. Workflow active identity and cancellation compare exact ownership; a tenant-only scope intentionally cannot cancel a checkpoint/run carrying account or user identity. Pass the current explicitly revised workflow definition so recursive hash mismatch fails before abort or durable mutation. Configure exact host/origin allow-lists where needed, wire redaction before execution, retain tool/workflow policy checks, and adapt the Web handler behind host TLS/rate limits. Disconnect abort is default; persistent reconnect/status belongs to durable workflow checkpoints, not an invented in-memory agent result cache.
142
+ - `@arnilo/prism-server` exposes no agent/workflow by default and requires `authorize()` for every matched operation. Derive complete tenant/account/user ownership from validated host identity, never request JSON. Workflow active identity and cancellation compare exact ownership; a tenant-only scope intentionally cannot cancel a checkpoint/run carrying account or user identity. The artifact review service (`createArtifactService`) requires authenticated identity + thread ownership on every attach/revise/compare/approve/reject/download, resolves concurrent reviewers via checkpoint CAS (no lost approvals), rejects local filesystem paths in `uri`/citations, redacts records before persist and on response, and serves downloads only through signed expiring links that are reauthorized against the token's ownership per request. Pass the current explicitly revised workflow definition so recursive hash mismatch fails before abort or durable mutation. Configure exact host/origin allow-lists where needed, wire redaction before execution, retain tool/workflow policy checks, and adapt the Web handler behind host TLS/rate limits. Disconnect abort is default; persistent reconnect/status belongs to durable workflow checkpoints, not an invented in-memory agent result cache.
143
143
  - Coding tools from `@arnilo/prism-coding-agent` accept an optional `ExecutionPolicy` checked inside each tool before side effects; shared policy propagation includes `createReadOnlyTools()`. They enforce finite text-scan/image/edit/write/shell limits, repository list/search depth/entry/match/scan/time caps, structured Git path/ref/message/output/patch/worktree caps, named-check concurrency/output caps, a 600-second default shell wall time, and a 64 MiB default total-output ceiling. Opt-in `createGitTools()` uses argument arrays with hooks/credential prompts/external diff disabled, requires host `commitIdentity` for commits, and never pushes or opens PRs. Successful truncated shell output leaves a host-owned exclusive `0600` temp file; delete `metadata.fullOutputPath` after use. Error/abort/timeout/overflow removes unpublished spills. Custom read/edit/shell/repository backends must honor supplied caps/signals. Use `@arnilo/prism-coding-security` for path roots, command rules, identity-scoped approval caching, required `workspaceMode` on `createSandboxCodingComposition()` / `createSandboxCodingTools()`, and the optional `createDockerSandbox()` reference adapter. **Host mode is never contained execution** (`containmentClaim: false`). Sandbox mode claims containment only when FS backends target the disposable tree; mixed wiring requires `allowMixedWorkspaceWiring` and still does not claim containment. Limits alone are not containment: construct the Docker adapter (absolute CLI, digest-pinned image, network none by default) or an equivalent host sandbox before treating coding execution as production-safe. Docker daemon/image trust, egress firewall/proxy, and artifact retention remain host-owned.
144
144
  - Optional `@arnilo/prism-browser` requires a host-supplied Playwright Browser (`playwright-core@1.61.0` peer). Import is inert. One non-persistent context belongs to one run; actions serialize; refs are snapshot-scoped; CSS/evaluate/CDP/persistent profiles are denied. Context routing + `serviceWorkers: "block"` deny file/data/blob/devtools/private/loopback by default and require contained-proxy attestation for external egress (Playwright routing is defense in depth, not DNS containment). Uploads are realpath-rooted; downloads quarantine with hash/MIME until host `approveRelease`; screenshots return bounded `ImageContent`. Observation vs mutation/high-impact actions map to `ExecutionPolicy`. Treat snapshot/page text as untrusted external content. Close contexts with `browser_close` or `manager.closeRun(runId)` on abort/terminal. Browser control endpoint, binary/image pin, and real egress firewall/proxy remain host-owned. Shared sandbox: `createSharedSandboxBrowserOptions()` + `assertBrowserSandboxNetwork()`.
145
+ - Browser verified-state checkpoints (0.0.14, `createBrowserCheckpointLedger()`) store URL + domain-state hash + host data refs only — never serialized browser internals (cookies/storage/contexts). After any resume/interruption the ledger fails closed (`assertVerifiedBeforeSideEffect`) until the host reloads + verifies, so side effects never replay on stale state.
146
+ - Device adapters (0.0.14, `resolveDevicePolicy`/`assertDeviceAdmit`) are deny-by-default: admission fails closed without explicit `enabled`, an explicit sandbox, approval (when required), an under-budget session count, and shared `RunLimits`. Stream chunks over the frozen cap are dropped with a marker; telemetry is redacted before emit/persist. No vendor voice/desktop package ships in 0.0.14 (demand-gated 0.1.x); device adapters cannot broaden consent/memory/network/file/browser/connector/tool permissions (gate 8).
145
147
  - `@arnilo/prism-credentials-node` rejects oversized/malformed envelopes and excessive scrypt work before KDF allocation, uses async scrypt, and requires restrictive existing/new Unix vault modes. Keep vault ownership and parent-directory access host-controlled; review before `chmod 600`, never auto-weaken a file policy. Keychain calls use abort-aware native async work with finite timeout/payload caps and sanitized errors. OS prompts, service availability, and whether a native backend promptly honors cancellation remain host/platform boundaries; no plaintext fallback is attempted.
146
148
  - LLM compaction always sends finite summary `maxTokens`, retains bounded deltas/events, and bounds/redacts provider/factory/policy error detail. Observational-memory workers cap turns, calls, arguments, results, transcript, and surfaced errors; unknown tools fail before execution, while invalid results can only be rejected after a host tool returns and may therefore follow side effects. Pass all known provider/credential/tool secrets into compaction/runtime options; exact replacement is not secret discovery.
147
149
  - Default remote-media loading resolves every DNS answer, rejects the hostname if any address is non-public, and pins one validated address through the request. Explicit `allowedHostnames` can trust private destinations. A host-supplied `fetch` owns DNS/rebinding/proxy/redirect safety; a custom `requestUrl` must connect to its supplied validated address.