@arnilo/prism 0.0.12 → 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 (75) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +8 -2
  3. package/dist/agents.js +21 -2
  4. package/dist/artifacts.d.ts +78 -0
  5. package/dist/artifacts.js +24 -0
  6. package/dist/contracts.d.ts +35 -1
  7. package/dist/contracts.js +8 -0
  8. package/dist/conversations.d.ts +50 -0
  9. package/dist/conversations.js +97 -0
  10. package/dist/credentials.d.ts +14 -0
  11. package/dist/credentials.js +9 -0
  12. package/dist/devices.d.ts +94 -0
  13. package/dist/devices.js +138 -0
  14. package/dist/extensions.d.ts +11 -0
  15. package/dist/extensions.js +15 -0
  16. package/dist/identity.d.ts +92 -0
  17. package/dist/identity.js +257 -0
  18. package/dist/index.d.ts +15 -5
  19. package/dist/index.js +8 -3
  20. package/dist/persistence-lifecycle.d.ts +103 -0
  21. package/dist/persistence-lifecycle.js +204 -0
  22. package/dist/providers/openai-compatible.d.ts +5 -1
  23. package/dist/providers/openai-compatible.js +15 -6
  24. package/dist/providers/openai-primitives.js +5 -2
  25. package/dist/secure-agent.js +7 -1
  26. package/dist/testing/persistence-schema.d.ts +2 -2
  27. package/dist/testing/persistence-schema.js +35 -2
  28. package/dist/tools.d.ts +2 -0
  29. package/dist/tools.js +6 -0
  30. package/docs/a2a.md +2 -0
  31. package/docs/ag-ui.md +5 -0
  32. package/docs/agent-identity.md +111 -0
  33. package/docs/browser-automation.md +3 -0
  34. package/docs/conversations.md +135 -0
  35. package/docs/credential-storage.md +31 -1
  36. package/docs/credentials-and-redaction.md +2 -0
  37. package/docs/database-persistence.md +22 -7
  38. package/docs/device-adapters.md +97 -0
  39. package/docs/extensions.md +1 -0
  40. package/docs/guardrails.md +3 -0
  41. package/docs/host-security.md +9 -3
  42. package/docs/index.md +26 -13
  43. package/docs/mcp-tools.md +2 -0
  44. package/docs/migration.md +48 -0
  45. package/docs/model-routing.md +102 -0
  46. package/docs/observability.md +2 -0
  47. package/docs/performance.md +21 -0
  48. package/docs/policy-and-audit.md +128 -0
  49. package/docs/postgres-persistence.md +1 -1
  50. package/docs/provider-caching.md +4 -0
  51. package/docs/provider-packages.md +12 -2
  52. package/docs/provider-request-policies.md +2 -0
  53. package/docs/providers/alibaba.md +179 -0
  54. package/docs/providers/azure.md +74 -0
  55. package/docs/providers/bedrock.md +72 -0
  56. package/docs/providers/google.md +1 -0
  57. package/docs/providers/ollama.md +166 -0
  58. package/docs/providers/openai-compatible.md +3 -1
  59. package/docs/providers/openrouter.md +2 -0
  60. package/docs/providers/vertex.md +71 -0
  61. package/docs/public-contracts.md +3 -1
  62. package/docs/release-and-install.md +149 -7
  63. package/docs/review-coverage-2026-07-23-phase-8.md +245 -0
  64. package/docs/review-coverage-2026-07-25-phase-9.md +256 -0
  65. package/docs/runs-and-usage.md +2 -0
  66. package/docs/server.md +37 -4
  67. package/docs/sqlite-persistence.md +1 -1
  68. package/docs/supervisors.md +2 -0
  69. package/docs/work-artifacts-and-review.md +100 -0
  70. package/docs/work-connectors.md +32 -0
  71. package/docs/work-tools.md +117 -0
  72. package/docs/workflows.md +4 -0
  73. package/docs/working-and-semantic-memory.md +20 -5
  74. package/package.json +4 -1
  75. package/templates/init/providers.json +22 -0
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";
@@ -7,6 +7,7 @@ export { CHECKPOINT_CONFLICT_CODE, CheckpointConflictError, createMemoryCheckpoi
7
7
  export { AGENT_RUN_STATE_NAMESPACE, AGENT_RUN_STATE_SCHEMA_VERSION, DEFAULT_MAX_AGENT_RUN_STATE_BYTES, HARD_MAX_AGENT_RUN_STATE_BYTES, agentFingerprint, loadAgentRunState } from "./agent-run-state.js";
8
8
  export { createAgentRunLifecycle } from "./agent-run-lifecycle.js";
9
9
  export { LEASE_CONFLICT_CODE, LeaseConflictError, createMemoryLeaseStore } from "./leases.js";
10
+ export { createMemoryPersistenceLifecycle, isResourceHeld, PersistenceLifecycleError, DEFAULT_LIFECYCLE_PAGE_SIZE, HARD_LIFECYCLE_PAGE_SIZE, DEFAULT_MAX_HOLD_REASON_BYTES, HARD_MAX_HOLD_REASON_BYTES, } from "./persistence-lifecycle.js";
10
11
  export { createEventMultiplexer } from "./event-multiplexer.js";
11
12
  export { applyCacheControl, cacheHitRate, cacheSavings, cacheUsageReport, mapCacheRetention, sanitizeCacheKey } from "./cache-helpers.js";
12
13
  export { resolveAgentDefinition } from "./agent-definitions.js";
@@ -15,7 +16,7 @@ export { assertJsonObject, isJsonObject, loadConfigLayers, mergeConfigLayers } f
15
16
  export { createDefaultCompactionStrategy, isCompactionEntryData } from "./compaction.js";
16
17
  export { createDefaultRetryPolicy, isTransientErrorInfo, waitForRetry } from "./retry.js";
17
18
  export { createContributionRegistries, createContributionRegistry, registerDiscoveredContributions } from "./contributions.js";
18
- export { createChainedCredentialResolver, createEnvCredentialResolver, createExplicitCredentialResolver, createMemoryCredentialStore, refreshOAuthCredential, resolveCredentialValue } from "./credentials.js";
19
+ export { createChainedCredentialResolver, createEnvCredentialResolver, createExplicitCredentialResolver, createMemoryCredentialStore, refreshOAuthCredential, revokeOAuthCredential, resolveCredentialValue } from "./credentials.js";
19
20
  export { createExtensionEventBus, createExtensionKernel } from "./extensions.js";
20
21
  export { createModelRegistry } from "./models.js";
21
22
  export { authMethodKey, defineProviderPackage, systemPromptContributionKey } from "./provider-packages.js";
@@ -34,10 +35,14 @@ export { assembleProviderInput, createDefaultInputBuilder, createDefaultPromptBu
34
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";
35
36
  export { createMockProvider } from "./mock-provider.js";
36
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";
37
41
  export { providerContentDelta, providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, toolCallContent, toolCallFromArgumentsText, } from "./provider-events.js";
38
42
  export { createProviderRegistry, createProviderResolver } from "./providers.js";
39
43
  export { createSecretRedactor, errorToErrorInfo, redactAgentEvent, redactMessage, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry } from "./redaction.js";
40
44
  export { assertPermission, assertTrusted, checkPermission, createStaticPermissionPolicy, createStaticTrustPolicy, denialToErrorInfo, isTrusted, PermissionDeniedError, TrustDeniedError } from "./security.js";
45
+ export { assertIdentityActive, assertIdentityMatchesOwnership, assertIdentityPropagation, DEFAULT_IDENTITY_LIMITS, HARD_IDENTITY_LIMITS, IdentityError, identityTelemetryAttributes, narrowIdentity, ownershipFromIdentity, resolveIdentityLimits, resolveRunIdentity, } from "./identity.js";
41
46
  export { applyExecutionDecision, assertExecutionAllowed, checkExecution, ExecutionDeniedError } from "./execution-policy.js";
42
47
  export { createSkillRegistry, resolveActiveSkills } from "./skills.js";
43
48
  export { resolveInstructionInjectors, runInstructionInjectors } from "./instruction-injection.js";
@@ -46,6 +51,6 @@ export { assertGuardrailsAllowed, GuardrailError, MAX_GUARDRAIL_CONCURRENCY, run
46
51
  export { createRunLimitTracker, DEFAULT_RUN_LIMITS, HARD_MAX_RUN_COST, HARD_RUN_LIMITS, RunLimitError, RunLimitTracker, resolveRunLimits } from "./run-limits.js";
47
52
  export { dispatchToolCallsInOrder, generateValidateReviseLoop, isAgentLoopOptions, resolveLoop, resolveToolConcurrency, singleShotLoop } from "./agent-loops.js";
48
53
  export const name = "prism";
49
- export const version = "0.0.12";
54
+ export const version = "0.0.14";
50
55
  export const description = "Agent harness for AI providers, agents, sessions, and tools.";
51
56
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,103 @@
1
+ import type { OwnershipScope, PersistencePage, PersistenceQuery, RetentionPolicy } from "./contracts.js";
2
+ /** Resource classes covered by holds, quotas, and retention sweeps. */
3
+ export type PersistenceResourceKind = "session" | "entry" | "run" | "event" | "tool_call" | "usage" | "checkpoint" | "feedback" | "audit" | "work_artifact" | "connector_operation";
4
+ export declare const DEFAULT_LIFECYCLE_PAGE_SIZE = 100;
5
+ export declare const HARD_LIFECYCLE_PAGE_SIZE = 500;
6
+ export declare const DEFAULT_MAX_HOLD_REASON_BYTES = 1024;
7
+ export declare const HARD_MAX_HOLD_REASON_BYTES: number;
8
+ export declare class PersistenceLifecycleError extends Error {
9
+ readonly code: string;
10
+ constructor(message: string, code?: string);
11
+ }
12
+ export interface LegalHoldRecord extends OwnershipScope {
13
+ readonly id: string;
14
+ readonly resourceKind: PersistenceResourceKind;
15
+ readonly resourceId: string;
16
+ readonly reason: string;
17
+ readonly createdAt: string;
18
+ readonly createdBy?: string;
19
+ readonly metadata?: Readonly<Record<string, unknown>>;
20
+ }
21
+ export interface PutLegalHoldInput extends OwnershipScope {
22
+ readonly resourceKind: PersistenceResourceKind;
23
+ readonly resourceId: string;
24
+ readonly reason: string;
25
+ readonly id?: string;
26
+ readonly createdBy?: string;
27
+ readonly metadata?: Readonly<Record<string, unknown>>;
28
+ readonly signal?: AbortSignal;
29
+ }
30
+ export interface ReleaseLegalHoldInput extends OwnershipScope {
31
+ readonly id: string;
32
+ readonly signal?: AbortSignal;
33
+ }
34
+ export interface LegalHoldQuery extends PersistenceQuery, OwnershipScope {
35
+ readonly resourceKind?: PersistenceResourceKind;
36
+ readonly resourceId?: string;
37
+ readonly holdId?: string;
38
+ readonly signal?: AbortSignal;
39
+ }
40
+ export interface ApplyRetentionInput extends OwnershipScope {
41
+ readonly policy: RetentionPolicy;
42
+ /** When omitted, adapters discover expired/over-limit session ids for this ownership page. */
43
+ readonly candidates?: readonly string[];
44
+ readonly cursor?: string;
45
+ readonly limit?: number;
46
+ readonly signal?: AbortSignal;
47
+ }
48
+ export interface ApplyRetentionResult {
49
+ readonly deleted: readonly string[];
50
+ readonly skippedHeld: readonly string[];
51
+ readonly nextCursor?: string;
52
+ }
53
+ export interface LegalHoldExportItem {
54
+ readonly holdId: string;
55
+ readonly resourceKind: PersistenceResourceKind;
56
+ readonly resourceId: string;
57
+ readonly reason: string;
58
+ readonly createdAt: string;
59
+ readonly redacted: true;
60
+ }
61
+ export interface ExportUnderHoldInput extends OwnershipScope {
62
+ readonly holdId?: string;
63
+ readonly resourceKind?: PersistenceResourceKind;
64
+ readonly cursor?: string;
65
+ readonly limit?: number;
66
+ readonly signal?: AbortSignal;
67
+ }
68
+ export interface TenantQuota extends OwnershipScope {
69
+ readonly resourceKind: PersistenceResourceKind;
70
+ readonly limit: number;
71
+ readonly used: number;
72
+ readonly updatedAt: string;
73
+ }
74
+ export interface SetTenantQuotaInput extends OwnershipScope {
75
+ readonly resourceKind: PersistenceResourceKind;
76
+ readonly limit: number;
77
+ readonly signal?: AbortSignal;
78
+ }
79
+ export interface ConsumeTenantQuotaInput extends OwnershipScope {
80
+ readonly resourceKind: PersistenceResourceKind;
81
+ /** Units to consume. Default 1. */
82
+ readonly delta?: number;
83
+ readonly signal?: AbortSignal;
84
+ }
85
+ /** Optional persistence lifecycle capability (retention / hold / export / quota). */
86
+ export interface PersistenceLifecycleStore {
87
+ putLegalHold(input: PutLegalHoldInput): Promise<LegalHoldRecord>;
88
+ releaseLegalHold(input: ReleaseLegalHoldInput): Promise<boolean>;
89
+ listLegalHolds(query: LegalHoldQuery): Promise<PersistencePage<LegalHoldRecord>>;
90
+ /** Deletes candidates not under legal hold. Hold always wins over retention. */
91
+ applyRetention(input: ApplyRetentionInput): Promise<ApplyRetentionResult>;
92
+ exportUnderHold(input: ExportUnderHoldInput): Promise<PersistencePage<LegalHoldExportItem>>;
93
+ setTenantQuota(input: SetTenantQuotaInput): Promise<TenantQuota>;
94
+ getTenantQuota(input: OwnershipScope & {
95
+ readonly resourceKind: PersistenceResourceKind;
96
+ readonly signal?: AbortSignal;
97
+ }): Promise<TenantQuota | null>;
98
+ /** Fails closed when used + delta would exceed limit. */
99
+ consumeTenantQuota(input: ConsumeTenantQuotaInput): Promise<TenantQuota>;
100
+ }
101
+ export declare function createMemoryPersistenceLifecycle(): PersistenceLifecycleStore;
102
+ /** True when a session/resource id is under an active legal hold in the page of holds. */
103
+ export declare function isResourceHeld(holds: readonly Pick<LegalHoldRecord, "resourceKind" | "resourceId">[], resourceKind: PersistenceResourceKind, resourceId: string): boolean;
@@ -0,0 +1,204 @@
1
+ export const DEFAULT_LIFECYCLE_PAGE_SIZE = 100;
2
+ export const HARD_LIFECYCLE_PAGE_SIZE = 500;
3
+ export const DEFAULT_MAX_HOLD_REASON_BYTES = 1024;
4
+ export const HARD_MAX_HOLD_REASON_BYTES = 8 * 1024;
5
+ export class PersistenceLifecycleError extends Error {
6
+ code;
7
+ constructor(message, code = "ERR_PRISM_PERSISTENCE_LIFECYCLE") {
8
+ super(message);
9
+ this.code = code;
10
+ this.name = "PersistenceLifecycleError";
11
+ }
12
+ }
13
+ export function createMemoryPersistenceLifecycle() {
14
+ const holds = new Map();
15
+ const quotas = new Map();
16
+ const deleted = new Set();
17
+ return {
18
+ async putLegalHold(input) {
19
+ throwIfAborted(input.signal);
20
+ assertOwnership(input);
21
+ const reason = assertReason(input.reason);
22
+ if (!input.resourceId || !input.resourceKind) {
23
+ throw new PersistenceLifecycleError("resourceKind and resourceId are required", "ERR_PRISM_LIFECYCLE_HOLD");
24
+ }
25
+ const id = input.id ?? crypto.randomUUID();
26
+ const record = {
27
+ id,
28
+ resourceKind: input.resourceKind,
29
+ resourceId: input.resourceId,
30
+ reason,
31
+ createdAt: new Date().toISOString(),
32
+ ...(input.createdBy === undefined ? {} : { createdBy: input.createdBy }),
33
+ ...(input.metadata === undefined ? {} : { metadata: input.metadata }),
34
+ ...ownership(input),
35
+ };
36
+ holds.set(id, record);
37
+ return record;
38
+ },
39
+ async releaseLegalHold(input) {
40
+ throwIfAborted(input.signal);
41
+ assertOwnership(input);
42
+ const current = holds.get(input.id);
43
+ if (!current)
44
+ return false;
45
+ assertSameOwnership(input, current);
46
+ holds.delete(input.id);
47
+ return true;
48
+ },
49
+ async listLegalHolds(query) {
50
+ throwIfAborted(query.signal);
51
+ assertOwnership(query);
52
+ const limit = pageLimit(query.limit);
53
+ const items = [...holds.values()]
54
+ .filter((hold) => sameOwnership(query, hold))
55
+ .filter((hold) => query.holdId === undefined || hold.id === query.holdId)
56
+ .filter((hold) => query.resourceKind === undefined || hold.resourceKind === query.resourceKind)
57
+ .filter((hold) => query.resourceId === undefined || hold.resourceId === query.resourceId)
58
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
59
+ const start = query.cursor ? items.findIndex((item) => item.id === query.cursor) + 1 : 0;
60
+ const slice = items.slice(Math.max(0, start), Math.max(0, start) + limit);
61
+ const next = start + limit < items.length ? slice.at(-1)?.id : undefined;
62
+ return { items: slice, ...(next === undefined ? {} : { nextCursor: next }) };
63
+ },
64
+ async applyRetention(input) {
65
+ throwIfAborted(input.signal);
66
+ assertOwnership(input);
67
+ const held = new Set([...holds.values()]
68
+ .filter((hold) => sameOwnership(input, hold) && hold.resourceKind === "session")
69
+ .map((hold) => hold.resourceId));
70
+ const candidates = input.candidates ?? [];
71
+ const deletedIds = [];
72
+ const skippedHeld = [];
73
+ for (const id of candidates) {
74
+ if (held.has(id)) {
75
+ skippedHeld.push(id);
76
+ continue;
77
+ }
78
+ deleted.add(id);
79
+ deletedIds.push(id);
80
+ }
81
+ return { deleted: deletedIds, skippedHeld };
82
+ },
83
+ async exportUnderHold(input) {
84
+ throwIfAborted(input.signal);
85
+ assertOwnership(input);
86
+ const page = await this.listLegalHolds({
87
+ ...ownership(input),
88
+ holdId: input.holdId,
89
+ resourceKind: input.resourceKind,
90
+ cursor: input.cursor,
91
+ limit: input.limit,
92
+ signal: input.signal,
93
+ });
94
+ return {
95
+ items: page.items.map((hold) => ({
96
+ holdId: hold.id,
97
+ resourceKind: hold.resourceKind,
98
+ resourceId: hold.resourceId,
99
+ reason: hold.reason,
100
+ createdAt: hold.createdAt,
101
+ redacted: true,
102
+ })),
103
+ ...(page.nextCursor === undefined ? {} : { nextCursor: page.nextCursor }),
104
+ };
105
+ },
106
+ async setTenantQuota(input) {
107
+ throwIfAborted(input.signal);
108
+ assertOwnership(input);
109
+ if (!Number.isSafeInteger(input.limit) || input.limit < 0) {
110
+ throw new PersistenceLifecycleError("limit must be a non-negative safe integer", "ERR_PRISM_LIFECYCLE_QUOTA");
111
+ }
112
+ const key = quotaKey(input, input.resourceKind);
113
+ const previous = quotas.get(key);
114
+ const record = {
115
+ ...ownership(input),
116
+ resourceKind: input.resourceKind,
117
+ limit: input.limit,
118
+ used: previous?.used ?? 0,
119
+ updatedAt: new Date().toISOString(),
120
+ };
121
+ if (record.used > record.limit) {
122
+ throw new PersistenceLifecycleError("quota already exceeded", "ERR_PRISM_LIFECYCLE_QUOTA");
123
+ }
124
+ quotas.set(key, record);
125
+ return record;
126
+ },
127
+ async getTenantQuota(input) {
128
+ throwIfAborted(input.signal);
129
+ assertOwnership(input);
130
+ return quotas.get(quotaKey(input, input.resourceKind)) ?? null;
131
+ },
132
+ async consumeTenantQuota(input) {
133
+ throwIfAborted(input.signal);
134
+ assertOwnership(input);
135
+ const delta = input.delta ?? 1;
136
+ if (!Number.isSafeInteger(delta) || delta < 1) {
137
+ throw new PersistenceLifecycleError("delta must be a positive safe integer", "ERR_PRISM_LIFECYCLE_QUOTA");
138
+ }
139
+ const key = quotaKey(input, input.resourceKind);
140
+ const current = quotas.get(key);
141
+ if (!current) {
142
+ throw new PersistenceLifecycleError("tenant quota not configured", "ERR_PRISM_LIFECYCLE_QUOTA");
143
+ }
144
+ if (current.used + delta > current.limit) {
145
+ throw new PersistenceLifecycleError(`tenant quota exhausted for ${input.resourceKind}`, "ERR_PRISM_LIFECYCLE_QUOTA_EXHAUSTED");
146
+ }
147
+ const next = {
148
+ ...current,
149
+ used: current.used + delta,
150
+ updatedAt: new Date().toISOString(),
151
+ };
152
+ quotas.set(key, next);
153
+ return next;
154
+ },
155
+ };
156
+ }
157
+ /** True when a session/resource id is under an active legal hold in the page of holds. */
158
+ export function isResourceHeld(holds, resourceKind, resourceId) {
159
+ return holds.some((hold) => hold.resourceKind === resourceKind && hold.resourceId === resourceId);
160
+ }
161
+ function ownership(input) {
162
+ return {
163
+ ...(input.tenantId === undefined ? {} : { tenantId: input.tenantId }),
164
+ ...(input.accountId === undefined ? {} : { accountId: input.accountId }),
165
+ ...(input.userId === undefined ? {} : { userId: input.userId }),
166
+ };
167
+ }
168
+ function sameOwnership(a, b) {
169
+ return a.tenantId === b.tenantId && a.accountId === b.accountId && a.userId === b.userId;
170
+ }
171
+ function assertSameOwnership(expected, actual) {
172
+ if (!sameOwnership(expected, actual)) {
173
+ throw new PersistenceLifecycleError("ownership mismatch", "ERR_PRISM_LIFECYCLE_OWNERSHIP");
174
+ }
175
+ }
176
+ function assertOwnership(input) {
177
+ if (![input.tenantId, input.accountId, input.userId].some((value) => typeof value === "string" && value.length > 0)) {
178
+ throw new PersistenceLifecycleError("ownership required", "ERR_PRISM_LIFECYCLE_OWNERSHIP");
179
+ }
180
+ }
181
+ function assertReason(reason) {
182
+ if (typeof reason !== "string" || !reason.trim()) {
183
+ throw new PersistenceLifecycleError("reason is required", "ERR_PRISM_LIFECYCLE_HOLD");
184
+ }
185
+ if (Buffer.byteLength(reason, "utf8") > HARD_MAX_HOLD_REASON_BYTES) {
186
+ throw new PersistenceLifecycleError("reason exceeds limit", "ERR_PRISM_LIFECYCLE_HOLD");
187
+ }
188
+ return reason;
189
+ }
190
+ function pageLimit(limit) {
191
+ const resolved = limit ?? DEFAULT_LIFECYCLE_PAGE_SIZE;
192
+ if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > HARD_LIFECYCLE_PAGE_SIZE) {
193
+ throw new PersistenceLifecycleError(`limit must be 1..${HARD_LIFECYCLE_PAGE_SIZE}`, "ERR_PRISM_LIFECYCLE_LIMITS");
194
+ }
195
+ return resolved;
196
+ }
197
+ function quotaKey(ownership, kind) {
198
+ return `${ownership.tenantId ?? ""}\0${ownership.accountId ?? ""}\0${ownership.userId ?? ""}\0${kind}`;
199
+ }
200
+ function throwIfAborted(signal) {
201
+ if (signal?.aborted)
202
+ throw signal.reason ?? new DOMException("Aborted", "AbortError");
203
+ }
204
+ //# sourceMappingURL=persistence-lifecycle.js.map
@@ -1,9 +1,13 @@
1
- import type { AIProvider } from "../contracts.js";
1
+ import type { AIProvider, ProviderRequest } from "../contracts.js";
2
2
  import { type CredentialValueSource } from "../credentials.js";
3
3
  export interface OpenAICompatibleProviderOptions {
4
4
  readonly id?: string;
5
5
  readonly baseUrl: string;
6
6
  readonly apiKey?: CredentialValueSource;
7
7
  readonly fetch?: typeof fetch;
8
+ /** Override chat-completions URL (default `${baseUrl}/chat/completions`). */
9
+ readonly chatCompletionsUrl?: string | ((request: ProviderRequest) => string);
10
+ /** Default `bearer`. Azure resource keys use `api-key`; host-signed fetches may use `none`. */
11
+ readonly authStyle?: "bearer" | "api-key" | "none";
8
12
  }
9
13
  export declare function createOpenAICompatibleProvider(options: OpenAICompatibleProviderOptions): AIProvider;
@@ -16,13 +16,22 @@ export function createOpenAICompatibleProvider(options) {
16
16
  const secrets = [apiKey];
17
17
  const tools = new Map();
18
18
  try {
19
- const response = await fetchImpl(`${options.baseUrl.replace(/\/$/, "")}/chat/completions`, {
19
+ const url = typeof options.chatCompletionsUrl === "function"
20
+ ? options.chatCompletionsUrl(request)
21
+ : options.chatCompletionsUrl
22
+ ?? `${options.baseUrl.replace(/\/$/, "")}/chat/completions`;
23
+ const authStyle = options.authStyle ?? "bearer";
24
+ const headers = {
25
+ ...Object.fromEntries(Object.entries(request.options?.headers ?? {}).filter((entry) => typeof entry[1] === "string")),
26
+ "content-type": "application/json",
27
+ };
28
+ if (apiKey && authStyle === "api-key")
29
+ headers["api-key"] = apiKey;
30
+ if (apiKey && authStyle === "bearer")
31
+ headers.authorization = `Bearer ${apiKey}`;
32
+ const response = await fetchImpl(url, {
20
33
  method: "POST",
21
- headers: {
22
- ...request.options?.headers,
23
- "content-type": "application/json",
24
- ...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
25
- },
34
+ headers,
26
35
  body: JSON.stringify(toOpenAIRequest(request)),
27
36
  signal: request.signal,
28
37
  });
@@ -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
@@ -1,5 +1,6 @@
1
1
  import { createAgent } from "./agents.js";
2
2
  import { validateRunStateOptions } from "./agent-run-state.js";
3
+ import { assertIdentityActive, assertIdentityMatchesOwnership } from "./identity.js";
3
4
  import { resolveRunLimits } from "./run-limits.js";
4
5
  import { createToolParameterValidator, createToolRegistry } from "./tools.js";
5
6
  /** Build an opt-in agent whose security-critical defaults cannot be replaced per run. */
@@ -27,6 +28,10 @@ export function createSecureAgent(options) {
27
28
  throw new TypeError(`Secure agent tool ${tool.name} requires a non-empty parameters schema`);
28
29
  }
29
30
  resolveRunLimits(options.limits);
31
+ if (options.identity) {
32
+ assertIdentityActive(options.identity);
33
+ assertIdentityMatchesOwnership(options.identity, options.ownership);
34
+ }
30
35
  const runState = Object.freeze({ ...options.runState, definitionRevision: options.definitionRevision, interruptBeforeTool: true });
31
36
  validateRunStateOptions(runState);
32
37
  const config = Object.freeze({
@@ -38,6 +43,7 @@ export function createSecureAgent(options) {
38
43
  permission: options.permission,
39
44
  trust: options.trust,
40
45
  ownership: Object.freeze({ ...options.ownership }),
46
+ ...(options.identity ? { identity: Object.freeze({ ...options.identity, scopes: Object.freeze([...options.identity.scopes]) }) } : {}),
41
47
  limits: Object.freeze({ ...options.limits }),
42
48
  guardrails: freezeGuardrails(options.guardrails),
43
49
  runState,
@@ -46,7 +52,7 @@ export function createSecureAgent(options) {
46
52
  return createAgent(config);
47
53
  }
48
54
  function withoutSecureFields(options) {
49
- const { tools: _tools, toolArgumentValidator: _validator, redactor: _redactor, permission: _permission, trust: _trust, ownership: _ownership, limits: _limits, guardrails: _guardrails, definitionRevision: _revision, runState: _runState, ...config } = options;
55
+ const { tools: _tools, toolArgumentValidator: _validator, redactor: _redactor, permission: _permission, trust: _trust, ownership: _ownership, identity: _identity, limits: _limits, guardrails: _guardrails, definitionRevision: _revision, runState: _runState, ...config } = options;
50
56
  return config;
51
57
  }
52
58
  function freezeGuardrails(guardrails) {
@@ -1,7 +1,7 @@
1
1
  import type { PersistencePage, SessionEntry, SessionEntryQuery } from "../contracts.js";
2
2
  /** Current shared persistence schema version for production database adapters. */
3
- export declare const PERSISTENCE_SCHEMA_VERSION = 4;
4
- export type PersistenceTableName = "prism_tenants" | "prism_accounts" | "prism_users" | "prism_agent_definitions" | "prism_sessions" | "prism_branches" | "prism_session_entries" | "prism_session_append_idempotency" | "prism_runs" | "prism_agent_events" | "prism_tool_calls" | "prism_usage" | "prism_run_feedback" | "prism_retention_policies" | "prism_migrations";
3
+ export declare const PERSISTENCE_SCHEMA_VERSION = 5;
4
+ export type PersistenceTableName = "prism_tenants" | "prism_accounts" | "prism_users" | "prism_agent_definitions" | "prism_sessions" | "prism_branches" | "prism_session_entries" | "prism_session_append_idempotency" | "prism_runs" | "prism_agent_events" | "prism_tool_calls" | "prism_usage" | "prism_run_feedback" | "prism_retention_policies" | "prism_legal_holds" | "prism_tenant_quotas" | "prism_migrations";
5
5
  export type PersistenceColumnType = "text" | "integer" | "number" | "boolean" | "json" | "timestamp";
6
6
  export interface PersistenceColumnDefinition {
7
7
  readonly name: string;
@@ -4,7 +4,7 @@ import { createHash } from "node:crypto";
4
4
  // this module defines the shared table/index/pagination/migration expectations
5
5
  // adapter authors implement and test against before shipping dialect-specific DDL.
6
6
  /** Current shared persistence schema version for production database adapters. */
7
- export const PERSISTENCE_SCHEMA_VERSION = 4;
7
+ export const PERSISTENCE_SCHEMA_VERSION = 5;
8
8
  /** Guidance adapters must follow: values are bound parameters, never interpolated. */
9
9
  export const PARAMETERIZED_QUERY_GUIDANCE = "Bind every user-supplied value (session ids, idempotency keys, tenant ids, timestamps, JSON payloads) as a query parameter. Quote/validate schema and table identifiers only; never interpolate untrusted strings into SQL text.";
10
10
  const TENANT_COLUMNS = [
@@ -258,6 +258,33 @@ export function createPersistenceSchemaModel() {
258
258
  { name: "metadata", type: "json", nullable: true },
259
259
  ],
260
260
  },
261
+ {
262
+ name: "prism_legal_holds",
263
+ primaryKey: ["id"],
264
+ columns: [
265
+ { name: "id", type: "text" },
266
+ ...TENANT_COLUMNS,
267
+ { name: "resource_kind", type: "text" },
268
+ { name: "resource_id", type: "text" },
269
+ { name: "reason", type: "text" },
270
+ { name: "created_at", type: "timestamp" },
271
+ { name: "created_by", type: "text", nullable: true },
272
+ { name: "metadata", type: "json", nullable: true },
273
+ ],
274
+ },
275
+ {
276
+ name: "prism_tenant_quotas",
277
+ primaryKey: ["id"],
278
+ columns: [
279
+ { name: "id", type: "text" },
280
+ ...TENANT_COLUMNS,
281
+ { name: "resource_kind", type: "text" },
282
+ { name: "limit_count", type: "integer" },
283
+ { name: "used_count", type: "integer" },
284
+ { name: "updated_at", type: "timestamp" },
285
+ ],
286
+ uniqueKeys: [["tenant_id", "account_id", "user_id", "resource_kind"]],
287
+ },
261
288
  {
262
289
  name: "prism_migrations",
263
290
  primaryKey: ["id"],
@@ -298,6 +325,9 @@ export function createPersistenceSchemaModel() {
298
325
  { name: "prism_run_feedback_run_created_idx", table: "prism_run_feedback", columns: ["run_id", "created_at", "id"], purpose: "run feedback lookup" },
299
326
  { name: "prism_run_feedback_trace_created_idx", table: "prism_run_feedback", columns: ["trace_id", "created_at", "id"], purpose: "trace feedback lookup" },
300
327
  { name: "prism_agent_definitions_name_version_idx", table: "prism_agent_definitions", columns: ["name", "version"], purpose: "definition lookup" },
328
+ { name: "prism_legal_holds_owner_resource_idx", table: "prism_legal_holds", columns: ["tenant_id", "account_id", "user_id", "resource_kind", "resource_id"], purpose: "hold lookup by owned resource" },
329
+ { name: "prism_legal_holds_created_id_idx", table: "prism_legal_holds", columns: ["created_at", "id"], purpose: "hold export pagination" },
330
+ { name: "prism_tenant_quotas_owner_kind_idx", table: "prism_tenant_quotas", columns: ["tenant_id", "account_id", "user_id", "resource_kind"], purpose: "tenant quota lookup" },
301
331
  { name: "prism_migrations_name_version_idx", table: "prism_migrations", columns: ["name", "version"], purpose: "applied-migration lookup (table constraint enforces uniqueness)" },
302
332
  ],
303
333
  };
@@ -320,7 +350,9 @@ function migrationStep(version, name, description) {
320
350
  : version === 4
321
351
  // Adapter-local FTS objects (SQLite FTS5 / Postgres tsvector) map to this canonical name.
322
352
  ? { search: ["prism_session_search"], indexes: ["prism_sessions_updated_id_idx"] }
323
- : (() => { throw new Error(`Unknown migration version ${version}`); })();
353
+ : version === 5
354
+ ? { tables: ["prism_legal_holds", "prism_tenant_quotas"], indexes: ["prism_legal_holds_owner_resource_idx", "prism_legal_holds_created_id_idx", "prism_tenant_quotas_owner_kind_idx"] }
355
+ : (() => { throw new Error(`Unknown migration version ${version}`); })();
324
356
  return {
325
357
  version,
326
358
  name,
@@ -338,6 +370,7 @@ export function createPersistenceMigrationContract() {
338
370
  migrationStep(2, "002_usage_scope", "Distinguish provider-turn usage from aggregate run totals."),
339
371
  migrationStep(3, "003_run_feedback", "Add immutable ownership-scoped run/trace feedback and evaluation links."),
340
372
  migrationStep(4, "004_session_search", "Add bounded session search indexes and adapter-local FTS objects."),
373
+ migrationStep(5, "005_lifecycle_hold_quota", "Add legal-hold and tenant-quota tables for retention lifecycle."),
341
374
  ],
342
375
  lockGuidance: "Acquire a dialect-specific migration lock before applying steps (PostgreSQL advisory lock; SQLite exclusive transaction). Only one process should migrate at a time.",
343
376
  leastPrivilegeGuidance: "Run migrations with a DDL-capable role; use a separate least-privilege runtime role limited to INSERT/SELECT/UPDATE on adapter tables. Never grant migration credentials to the agent runtime.",
package/dist/tools.d.ts CHANGED
@@ -43,6 +43,8 @@ export interface DispatchToolCallOptions {
43
43
  readonly redactor?: SecretRedactor;
44
44
  readonly ledger?: RunLedger;
45
45
  readonly ownership?: OwnershipScope;
46
+ /** Host-verified identity; asserted active before tool side effects when present. */
47
+ readonly identity?: import("./identity.js").AgentIdentity;
46
48
  /** Tool stages run after middleware normalization and before side effects/exposure. */
47
49
  readonly guardrails?: Guardrails;
48
50
  /** Shared run tracker; direct hosts may supply one for their call scope. */
package/dist/tools.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { isJsonObject } from "./config.js";
2
2
  import { createId } from "./ids.js";
3
3
  import { GuardrailError, runGuardrails } from "./guardrails.js";
4
+ import { assertIdentityActive, assertIdentityMatchesOwnership } from "./identity.js";
4
5
  import { errorToErrorInfo, redactRunLedgerRecord, redactSecrets } from "./redaction.js";
5
6
  import { assertCanRegister } from "./registry-options.js";
6
7
  import { assertPermission, assertTrusted } from "./security.js";
@@ -89,6 +90,7 @@ export async function dispatchToolCall(options) {
89
90
  const context = {
90
91
  ...options.context,
91
92
  toolCallId: mediatedCall.id,
93
+ identity: options.identity ?? options.context.identity,
92
94
  progress: async (progress, metadata) => {
93
95
  await options.context.progress?.(progress, metadata);
94
96
  await options.emit?.({
@@ -108,6 +110,10 @@ export async function dispatchToolCall(options) {
108
110
  },
109
111
  };
110
112
  try {
113
+ if (context.identity) {
114
+ assertIdentityActive(context.identity);
115
+ assertIdentityMatchesOwnership(context.identity, options.ownership);
116
+ }
111
117
  await assertTrusted(options.trust, { kind: "tool", target: mediatedCall.name, capability: "execute", metadata: options.context.metadata });
112
118
  await assertPermission(options.permission, { kind: "tool", action: "execute", target: mediatedCall.name, metadata: options.context.metadata });
113
119
  }
package/docs/a2a.md CHANGED
@@ -80,6 +80,7 @@ Defaults/hard caps include: request 64 KiB/1 MiB; response 1/8 MiB; event 64 KiB
80
80
  ## Security and performance notes
81
81
 
82
82
  - Authorize every operation; lifecycle/push adapters enforce exact owner again at durable storage boundary. Missing and foreign tasks/configs share `-32001`.
83
+ - Optional `A2AAuthorization.identity` is host-verified; the handler asserts activity/ownership match and forwards identity into `session.run`. Cross-tenant or widened scopes fail closed.
83
84
  - URL policy must reject private, loopback, link-local, rebound, redirected, or otherwise disallowed destinations. Package never fetches file URLs. Host push delivery must repeat equivalent checks for every attempt/redirect and process event IDs idempotently.
84
85
  - Push token/auth credentials are accepted only into host adapter input and removed from protocol reads/responses. Keep them out of task parts, events, telemetry, ledgers, and errors.
85
86
  - Known-secret redaction applies before handler JSON/SSE output. Client redacts mapped text/errors. Raw/data/url content remains explicitly untrusted.
@@ -88,6 +89,7 @@ Defaults/hard caps include: request 64 KiB/1 MiB; response 1/8 MiB; event 64 KiB
88
89
 
89
90
  ## Related APIs
90
91
 
92
+ - [Agent identity](agent-identity.md)
91
93
  - [Supervisor delegation](supervisors.md)
92
94
  - [Agent/session runtime](agent-session-runtime.md)
93
95
  - [Workflows](workflows.md)
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.