@arnilo/prism 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +10 -2
  2. package/README.md +3 -2
  3. package/dist/agent-loops.js +4 -0
  4. package/dist/agent-run-lifecycle.js +2 -2
  5. package/dist/agent-session/helpers.d.ts +1 -1
  6. package/dist/agent-session/helpers.js +2 -1
  7. package/dist/agent-session/session.d.ts +1 -1
  8. package/dist/agent-session/session.js +28 -20
  9. package/dist/agent-session.d.ts +1 -1
  10. package/dist/agent-session.js +1 -1
  11. package/dist/agents.d.ts +1 -1
  12. package/dist/agents.js +1 -1
  13. package/dist/contracts-core/agent.d.ts +5 -5
  14. package/dist/contracts-core/agent.js +2 -0
  15. package/dist/contracts-core/extensions.d.ts +3 -3
  16. package/dist/contracts-core/extensions.js +2 -0
  17. package/dist/contracts-core/loop.d.ts +6 -1
  18. package/dist/contracts-core/session.d.ts +1 -1
  19. package/dist/contracts-core.d.ts +6 -6
  20. package/dist/contracts-core.js +6 -6
  21. package/dist/contracts-protocol.d.ts +8 -2
  22. package/dist/contracts.d.ts +1 -1
  23. package/dist/contracts.js +1 -1
  24. package/dist/field-policy.d.ts +119 -0
  25. package/dist/field-policy.js +418 -0
  26. package/dist/index.d.ts +6 -4
  27. package/dist/index.js +5 -4
  28. package/dist/input.js +1 -1
  29. package/dist/redaction.d.ts +6 -5
  30. package/dist/redaction.js +18 -10
  31. package/dist/tools.d.ts +1 -1
  32. package/dist/tools.js +1 -1
  33. package/docs/0.1.0-readiness.md +7 -7
  34. package/docs/acp-agent.md +78 -0
  35. package/docs/acp.md +21 -10
  36. package/docs/ag-ui.md +1 -1
  37. package/docs/agent-definitions.md +1 -1
  38. package/docs/agent-events.md +2 -2
  39. package/docs/agent-loops.md +2 -2
  40. package/docs/audit-export.md +151 -0
  41. package/docs/coding-agent-tools.md +3 -1
  42. package/docs/coding-security.md +6 -0
  43. package/docs/data-classification.md +82 -0
  44. package/docs/disaster-recovery.md +71 -0
  45. package/docs/enterprise-postgres-state.md +60 -7
  46. package/docs/evaluations.md +45 -0
  47. package/docs/host-security.md +4 -0
  48. package/docs/index.md +11 -5
  49. package/docs/migration.md +27 -0
  50. package/docs/operations.md +104 -0
  51. package/docs/policy-and-audit.md +34 -0
  52. package/docs/release-0.2.7-evidence.md +514 -0
  53. package/docs/release-and-install.md +55 -8
  54. package/docs/structured-output.md +10 -10
  55. package/docs/workflows.md +48 -3
  56. package/package.json +4 -3
@@ -0,0 +1,418 @@
1
+ /**
2
+ * Field-level classification and fail-closed redaction (plan 027 Task 8).
3
+ *
4
+ * A dependency-free, JSON-like structured-value walker that evaluates an
5
+ * explicit host policy against every field of a value destined to cross a
6
+ * data boundary (provider prompt egress, tool dispatch/result persistence,
7
+ * artifact write/read/export, audit hashing, telemetry attributes/events,
8
+ * lifecycle export). The policy classifies fields by label, path, destination,
9
+ * and tenant; unknown fields fail closed under the protected default.
10
+ *
11
+ * Non-goals (frozen): no automatic sensitive-data discovery (labels come from
12
+ * an explicit `labelFor` hint function supplied by the boundary owner), no
13
+ * global registry, no decorator framework, no second policy language. Existing
14
+ * hardcoded secret redaction (`createSecretRedactor`) stays in place as
15
+ * defense in depth and runs *after* policy transformation at egress seams.
16
+ */
17
+ export class FieldPolicyError extends Error {
18
+ code = "ERR_PRISM_FIELD_POLICY";
19
+ path;
20
+ constructor(path, message) {
21
+ super(`field policy rejected value at ${path}: ${message}`);
22
+ this.name = "FieldPolicyError";
23
+ this.path = path;
24
+ }
25
+ }
26
+ export const FIELD_POLICY_LIMITS = Object.freeze({
27
+ maxDepth: 32,
28
+ maxKeys: 10_000,
29
+ maxChars: 1_000_000,
30
+ maxPolicyMs: 5_000,
31
+ tokenPrefix: "tok_",
32
+ });
33
+ const REDACTED = "[REDACTED]";
34
+ const DENIED = "[DENIED]";
35
+ function tokenOf(prefix, path, original, sha) {
36
+ const hash = sha(`${path}\u0000${original}`);
37
+ return `${prefix}${hash.slice(0, 12)}`;
38
+ }
39
+ const ALLOW = { action: "allow" };
40
+ const DENY_UNKNOWN = Object.freeze({ action: "deny", reason: "unknown-field" });
41
+ const DENY_UNKNOWN_LABEL = Object.freeze({ action: "deny", reason: "unknown-label" });
42
+ const denied = (reason) => Object.freeze({ action: "deny", reason });
43
+ function normalizeDecision(value, path) {
44
+ if (value && typeof value === "object" && typeof value.action === "string") {
45
+ const candidate = value;
46
+ const action = candidate.action;
47
+ if (action === "allow" || action === "redact" || action === "tokenize" || action === "deny") {
48
+ // Returned decision objects are used as-is (host policies may reuse
49
+ // frozen constants; a hostile mutation is their own risk).
50
+ if (candidate.reason !== undefined && typeof candidate.reason !== "string") {
51
+ throw new FieldPolicyError(path, "policy returned an invalid reason");
52
+ }
53
+ return candidate;
54
+ }
55
+ }
56
+ throw new FieldPolicyError(path, "policy returned an invalid decision");
57
+ }
58
+ const JSON_PRIMITIVE_KINDS = new Set(["string", "number", "boolean"]);
59
+ const kindOf = (value) => {
60
+ if (value === null)
61
+ return "null";
62
+ if (Array.isArray(value))
63
+ return "array";
64
+ if (typeof value === "object")
65
+ return "object";
66
+ return (JSON_PRIMITIVE_KINDS.has(typeof value) ? typeof value : "object");
67
+ };
68
+ function checkLimits(ctx, path, depth) {
69
+ // Time budget only when explicitly configured; the default walk is pure
70
+ // structural (depth/keys/bytes below), so the hot path avoids Date.now().
71
+ // Time budget only when explicitly configured; the default walk is pure
72
+ // structural (depth/keys/bytes below), so the hot path avoids Date.now().
73
+ if (ctx.hasTimeBudget && Date.now() - ctx.startedAt > ctx.maxPolicyMs) {
74
+ throw new FieldPolicyError(path, `policy walk exceeded ${ctx.maxPolicyMs}ms`);
75
+ }
76
+ if (depth > ctx.maxDepth)
77
+ throw new FieldPolicyError(path, `depth exceeds ${ctx.maxDepth}`);
78
+ }
79
+ /**
80
+ * Applies `policy` to every field of a JSON-like value.
81
+ *
82
+ * Decisions: `allow` keeps the value; `redact` replaces string leaves with
83
+ * `[REDACTED]` while preserving container shape; `tokenize` replaces string
84
+ * leaves with a deterministic token; `deny` replaces the value with `[DENIED]`.
85
+ * Unknown labels fail closed under the protected default. Hostile values
86
+ * (cycles, unsupported types, over-budget walks) throw `FieldPolicyError`
87
+ * instead of stringifying guesses. The input is never mutated; only fields the
88
+ * policy changed are allocated.
89
+ */
90
+ export function applyFieldPolicy(value, policy, options) {
91
+ if (typeof policy !== "function")
92
+ throw new FieldPolicyError("", "policy must be a function");
93
+ const ctx = {
94
+ policy,
95
+ options,
96
+ tokenSha: (text) => sha256Hex(text),
97
+ maxDepth: options.maxDepth ?? FIELD_POLICY_LIMITS.maxDepth,
98
+ maxKeys: options.maxKeys ?? FIELD_POLICY_LIMITS.maxKeys,
99
+ maxChars: options.maxChars ?? FIELD_POLICY_LIMITS.maxChars,
100
+ hasTimeBudget: options.maxPolicyMs !== undefined,
101
+ maxPolicyMs: options.maxPolicyMs,
102
+ labelCache: new Map(),
103
+ input: {
104
+ path: "",
105
+ destination: options.destination,
106
+ label: undefined,
107
+ kind: "object",
108
+ direction: options.direction ?? "outbound",
109
+ ...(options.tenantId !== undefined ? { tenantId: options.tenantId } : {}),
110
+ ...(options.purpose !== undefined ? { purpose: options.purpose } : {}),
111
+ },
112
+ budgetRemaining: options.maxChars ?? FIELD_POLICY_LIMITS.maxChars,
113
+ nodesVisited: 0,
114
+ startedAt: Date.now(),
115
+ };
116
+ return walkValue(value, ctx, "", 0);
117
+ }
118
+ function walkValue(input, ctx, path, depth) {
119
+ const active = new Set();
120
+ const climb = (value, p, d, key) => {
121
+ if (value === null || typeof value !== "object") {
122
+ return leaf(value, ctx, p, d, key);
123
+ }
124
+ if (value instanceof Date || value instanceof RegExp || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
125
+ return leaf(value, ctx, p, d, key);
126
+ }
127
+ if (d > ctx.maxDepth)
128
+ throw new FieldPolicyError(p, `depth exceeds ${ctx.maxDepth}`);
129
+ if (active.has(value))
130
+ throw new FieldPolicyError(p, "cyclic reference is not supported (fail closed)");
131
+ active.add(value);
132
+ try {
133
+ // The root has no key and never substitutes the whole payload; its own
134
+ // decision governs the walk of children only.
135
+ const decision = p === "" ? ALLOW : decide(ctx, value, p, d, undefined, key);
136
+ if (decision.action === "deny")
137
+ return deny();
138
+ if (decision.action === "redact")
139
+ return shapePreserving(value, ctx, p, d, "redact", active);
140
+ if (decision.action === "tokenize")
141
+ return shapePreserving(value, ctx, p, d, "tokenize", active);
142
+ return walkChildren(value, ctx, p, d, climb);
143
+ }
144
+ finally {
145
+ active.delete(value);
146
+ }
147
+ };
148
+ return climb(input, path, depth, "");
149
+ }
150
+ /** labelFor results are key-scoped (paths only reach the first call); cached per walk. */
151
+ function labelForKey(ctx, key, path) {
152
+ const labelFor = ctx.options.labelFor;
153
+ if (!labelFor)
154
+ return undefined;
155
+ if (ctx.labelCache.has(key))
156
+ return ctx.labelCache.get(key);
157
+ const label = labelFor(key, path);
158
+ ctx.labelCache.set(key, label);
159
+ return label;
160
+ }
161
+ function decide(ctx, value, path, depth, override, key) {
162
+ if (override)
163
+ return override;
164
+ const kind = kindOf(value);
165
+ const label = labelForKey(ctx, key, path);
166
+ const input = ctx.input; // reused per node; see the reuse contract on WalkContext
167
+ input.path = path;
168
+ input.label = label;
169
+ input.kind = kind;
170
+ let decision;
171
+ try {
172
+ decision = ctx.policy(input);
173
+ }
174
+ catch (error) {
175
+ throw new FieldPolicyError(path, `policy threw ${error instanceof Error ? error.name : typeof error} (value never echoed)`);
176
+ }
177
+ const normalized = normalizeDecision(decision, path);
178
+ checkLimits(ctx, path, depth); // slow policy over budget trips here
179
+ if (normalized.action !== "allow") {
180
+ ctx.options.onRedact?.(path, normalized.reason ?? normalized.action);
181
+ }
182
+ return normalized;
183
+ }
184
+ function leaf(input, ctx, path, depth, key) {
185
+ if (input === undefined || typeof input === "bigint" || typeof input === "symbol" || typeof input === "function") {
186
+ throw new FieldPolicyError(path, `unsupported value type ${typeof input} (fail closed, no stringification)`);
187
+ }
188
+ if (typeof input === "string") {
189
+ ctx.budgetRemaining -= input.length;
190
+ if (ctx.budgetRemaining < 0)
191
+ throw new FieldPolicyError(path, `string budget exceeds ${ctx.maxChars}`);
192
+ }
193
+ const decision = decide(ctx, input, path, depth, undefined, key);
194
+ switch (decision.action) {
195
+ case "allow":
196
+ return input;
197
+ case "redact":
198
+ return typeof input === "string" ? REDACTED : input;
199
+ case "tokenize":
200
+ return typeof input === "string"
201
+ ? tokenOf(ctx.options.tokenPrefix ?? FIELD_POLICY_LIMITS.tokenPrefix, path, input, ctx.tokenSha)
202
+ : input;
203
+ case "deny":
204
+ return deny();
205
+ }
206
+ }
207
+ // decide() already recorded the redaction entry for this path
208
+ function deny() {
209
+ return DENIED;
210
+ }
211
+ function shapePreserving(input, ctx, path, depth, mode, active) {
212
+ if (active.has(input))
213
+ throw new FieldPolicyError(path, "cyclic reference is not supported (fail closed)");
214
+ const forced = { action: mode };
215
+ if (Array.isArray(input)) {
216
+ const out = new Array(input.length);
217
+ for (let index = 0; index < input.length; index += 1) {
218
+ out[index] = safeChild(input[index], ctx, `${path}[${index}]`, depth + 1, forced, active);
219
+ }
220
+ return out;
221
+ }
222
+ if (isPlainRecord(input)) {
223
+ const out = {};
224
+ let keysChecked = 0;
225
+ for (const [key, item] of Object.entries(input)) {
226
+ keysChecked += 1;
227
+ if (keysChecked > ctx.maxKeys)
228
+ throw new FieldPolicyError(`${path}.${key}`, `key count exceeds ${ctx.maxKeys}`);
229
+ out[key] = safeChild(item, ctx, joinPath(path, key), depth + 1, forced, active);
230
+ }
231
+ return out;
232
+ }
233
+ if (input instanceof Map) {
234
+ const out = {};
235
+ for (const [key, item] of input) {
236
+ out[String(key)] = safeChild(item, ctx, joinPath(path, String(key)), depth + 1, forced, active);
237
+ }
238
+ return out;
239
+ }
240
+ if (input instanceof Set) {
241
+ return [...input].map((item, index) => safeChild(item, ctx, `${path}[${index}]`, depth + 1, forced, active));
242
+ }
243
+ throw new FieldPolicyError(path, `unsupported value ${input?.constructor?.name ?? "object"} (fail closed, no stringification)`);
244
+ }
245
+ function walkChildren(input, ctx, path, depth, climb) {
246
+ if (Array.isArray(input)) {
247
+ let changed = false;
248
+ const out = new Array(input.length);
249
+ for (let index = 0; index < input.length; index += 1) {
250
+ const item = input[index];
251
+ const next = climb(item, `${path}[${index}]`, depth + 1, String(index));
252
+ if (next !== item)
253
+ changed = true;
254
+ out[index] = next;
255
+ }
256
+ return changed ? out : input;
257
+ }
258
+ if (isPlainRecord(input)) {
259
+ let changed = false;
260
+ let keysChecked = 0;
261
+ const out = {};
262
+ for (const key in input) {
263
+ if (!Object.hasOwn(input, key))
264
+ continue;
265
+ keysChecked += 1;
266
+ if (keysChecked > ctx.maxKeys)
267
+ throw new FieldPolicyError(joinPath(path, key), `key count exceeds ${ctx.maxKeys}`);
268
+ const item = input[key];
269
+ if (item === undefined)
270
+ continue;
271
+ const next = climb(item, joinPath(path, key), depth + 1, key);
272
+ out[key] = next;
273
+ if (next !== item)
274
+ changed = true;
275
+ }
276
+ return changed ? out : input; // sparse copy: untouched subtrees share the input
277
+ }
278
+ if (input instanceof Map) {
279
+ let changed = false;
280
+ const out = {};
281
+ for (const [entryKey, item] of input) {
282
+ out[String(entryKey)] = climb(item, joinPath(path, String(entryKey)), depth + 1, String(entryKey));
283
+ if (out[String(entryKey)] !== item)
284
+ changed = true;
285
+ }
286
+ return changed ? out : input;
287
+ }
288
+ if (input instanceof Set) {
289
+ let changed = false;
290
+ const items = [...input];
291
+ const out = new Array(items.length);
292
+ for (let index = 0; index < items.length; index += 1) {
293
+ out[index] = climb(items[index], `${path}[${index}]`, depth + 1, String(index));
294
+ if (out[index] !== items[index])
295
+ changed = true;
296
+ }
297
+ return changed ? out : input;
298
+ }
299
+ throw new FieldPolicyError(path, `unsupported value ${input?.constructor?.name ?? "object"} (fail closed, no stringification)`);
300
+ }
301
+ function safeChild(item, ctx, path, depth, forced, active) {
302
+ if (item === null || typeof item !== "object") {
303
+ return applyForcedLeaf(item, ctx, path, forced);
304
+ }
305
+ if (item instanceof Date || item instanceof RegExp || ArrayBuffer.isView(item) || item instanceof ArrayBuffer) {
306
+ return item;
307
+ }
308
+ checkLimits(ctx, path, depth);
309
+ return shapePreserving(item, ctx, path, depth, forced.action, active);
310
+ }
311
+ function applyForcedLeaf(item, ctx, path, forced) {
312
+ if (typeof item === "string") {
313
+ ctx.budgetRemaining -= item.length;
314
+ if (ctx.budgetRemaining < 0)
315
+ throw new FieldPolicyError(path, `string budget exceeds ${ctx.maxChars}`);
316
+ }
317
+ switch (forced.action) {
318
+ case "redact":
319
+ return typeof item === "string" ? REDACTED : item;
320
+ case "tokenize":
321
+ return typeof item === "string"
322
+ ? tokenOf(ctx.options.tokenPrefix ?? FIELD_POLICY_LIMITS.tokenPrefix, path, item, ctx.tokenSha)
323
+ : item;
324
+ case "deny":
325
+ return DENIED;
326
+ default:
327
+ return item;
328
+ }
329
+ }
330
+ function isPlainRecord(value) {
331
+ if (value === null || typeof value !== "object")
332
+ return false;
333
+ const proto = Object.getPrototypeOf(value);
334
+ return proto === Object.prototype || proto === null;
335
+ }
336
+ /** `a.b` for object keys; arrays use `[i]`; root fields use bare `key`. */
337
+ function joinPath(base, key) {
338
+ return base ? `${base}.${key}` : key;
339
+ }
340
+ function sha256Hex(text) {
341
+ // FNV-1a is enough for token differentiation and avoids a crypto import at
342
+ // every leaf; collisions change a token value, never cross boundaries.
343
+ let hash = 0x811c9dc5;
344
+ for (let i = 0; i < text.length; i += 1) {
345
+ hash ^= text.charCodeAt(i);
346
+ hash = Math.imul(hash, 0x01000193);
347
+ }
348
+ return (hash >>> 0).toString(16).padStart(8, "0") + (hash >>> 8).toString(16).padStart(8, "0").slice(0, 4);
349
+ }
350
+ /**
351
+ * Fail-closed default for the protected profile (frozen behavior):
352
+ * unknown labels are denied on outbound boundaries and allowed inbound;
353
+ * `secret`/`financial` denied, `personal` redacted, listed public labels
354
+ * allowed. Labels are never auto-discovered — `labelFor` decides them.
355
+ */
356
+ export function createProtectedFieldPolicy(options = {}) {
357
+ const publicLabels = new Set(options.publicLabels ?? ["public"]);
358
+ const deniedLabels = {
359
+ secret: "secret-egress",
360
+ financial: "financial-egress",
361
+ ...options.deniedLabels,
362
+ };
363
+ const LABEL_DENY_REASONS = deniedLabels;
364
+ const redactedLabels = {
365
+ personal: "personal-data",
366
+ ...options.redactedLabels,
367
+ };
368
+ const tokenizedLabels = { token: "tokenization", ...options.tokenizedLabels };
369
+ const outbound = new Set(["prompt", "tool", "artifact", "audit", "telemetry", "export", "persistence"]);
370
+ return (input) => {
371
+ const { label } = input;
372
+ if (label === undefined || label === "") {
373
+ // Fail closed: unknown fields do not cross outbound/persisted boundaries.
374
+ if (input.direction === "outbound" && outbound.has(input.destination))
375
+ return DENY_UNKNOWN;
376
+ return ALLOW;
377
+ }
378
+ if (publicLabels.has(label))
379
+ return ALLOW;
380
+ if (label in deniedLabels)
381
+ return denied(LABEL_DENY_REASONS[label] ?? "denied");
382
+ if (label in redactedLabels)
383
+ return { action: "redact", reason: redactedLabels[label] };
384
+ if (label in tokenizedLabels)
385
+ return { action: "tokenize", reason: tokenizedLabels[label] };
386
+ return DENY_UNKNOWN_LABEL;
387
+ };
388
+ }
389
+ /** Identity policy: every field allowed (explicit override for trusted marks). */
390
+ export const ALLOW_FIELD_POLICY = () => ALLOW;
391
+ /**
392
+ * Adapts a `FieldPolicy` to the audit-export redaction hook. Transformation
393
+ * runs BEFORE canonical hashing so the exported chain verifies exactly.
394
+ * Only `{path, reason}` provenance survives; values never echo into the list.
395
+ */
396
+ export function createAuditFieldRedactor(policy, options = {}) {
397
+ return {
398
+ apply(record) {
399
+ const redactions = [];
400
+ const transformed = applyFieldPolicy(record, policy, {
401
+ destination: "audit",
402
+ direction: "outbound",
403
+ tenantId: options.tenantId,
404
+ purpose: options.purpose ?? "audit-export",
405
+ labelFor: options.labelFor,
406
+ onRedact: (path, reason) => {
407
+ if (options.collectProvenance !== false)
408
+ redactions.push({ path, reason });
409
+ },
410
+ });
411
+ return {
412
+ record: transformed,
413
+ ...(redactions.length ? { redactions } : {}),
414
+ };
415
+ },
416
+ };
417
+ }
418
+ //# sourceMappingURL=field-policy.js.map
package/dist/index.d.ts CHANGED
@@ -1,6 +1,4 @@
1
1
  export { resolveAgentDefinition } from "./agent-definitions.js";
2
- export { abortableSleep, pollDeviceCodeToken, redactOAuthError, throwIfAborted, type OAuthTokenSuccessPayload, type PollDeviceCodeTokenOptions, } from "./oauth-device-code.js";
3
- export { boundResponse, defaultResolver, isLoopbackAddress, isLoopbackHostname, normalizeHostname, pinnedFetch, raceAbort, requestPinned, resolvePinnedAddress, type PinnedFetchOptions, } from "./pinned-fetch.js";
4
2
  export type { AgentEventSourceErrorCode } from "./agent-event-source.js";
5
3
  export { AgentEventSourceError, createMemoryAgentEventSource } from "./agent-event-source.js";
6
4
  export { dispatchToolCallsInOrder, generateValidateReviseLoop, isAgentLoopOptions, resolveLoop, resolveToolConcurrency, singleShotLoop, } from "./agent-loops.js";
@@ -14,7 +12,7 @@ export { ARTIFACT_BODY_ERROR_CODES, ARTIFACT_CHECKPOINT_NAMESPACE, ArtifactBodyS
14
12
  export type { ApplyCacheControlOptions, CacheControlledContentBlock, CacheControlledMessage, CacheControlValue, CacheUsageReport, } from "./cache-helpers.js";
15
13
  export { applyCacheControl, cacheHitRate, cacheSavings, cacheUsageReport, mapCacheRetention, sanitizeCacheKey } from "./cache-helpers.js";
16
14
  export type { CacheTelemetry, CacheTelemetryOptions, CacheTelemetryReport, CacheTelemetrySample, } from "./cache-telemetry.js";
17
- export { CACHE_TELEMETRY_OVERFLOW_KEY, CacheTelemetryError, DEFAULT_CACHE_TELEMETRY_CAP, createCacheTelemetry, } from "./cache-telemetry.js";
15
+ export { CACHE_TELEMETRY_OVERFLOW_KEY, CacheTelemetryError, createCacheTelemetry, DEFAULT_CACHE_TELEMETRY_CAP, } from "./cache-telemetry.js";
18
16
  export type { MemoryCheckpointStoreOptions } from "./checkpoints.js";
19
17
  export { CHECKPOINT_CONFLICT_CODE, CheckpointConflictError, createMemoryCheckpointStore } from "./checkpoints.js";
20
18
  export type { DefaultCompactionStrategyOptions } from "./compaction.js";
@@ -45,6 +43,8 @@ export type { ExtensionErrorPolicy, ExtensionEventBus, ExtensionEventHandler, Ex
45
43
  export { createExtensionEventBus, createExtensionKernel } from "./extensions.js";
46
44
  export type { MemoryRunFeedbackStoreOptions, PrepareRunFeedbackOptions, RunFeedbackLimits, RunFeedbackRun, RunFeedbackRunResolver, } from "./feedback.js";
47
45
  export { createMemoryRunFeedbackStore, prepareRunFeedback, RunFeedbackError, requireRunFeedbackOwnership, runFeedbackPageLimit, } from "./feedback.js";
46
+ export type { ApplyFieldPolicyOptions, AuditFieldRedaction, AuditFieldRedactorLike, AuditFieldRedactorOptions, FieldPolicy, FieldPolicyAction, FieldPolicyDecision, FieldPolicyInput, ProtectedFieldPolicyOptions, } from "./field-policy.js";
47
+ export { ALLOW_FIELD_POLICY, applyFieldPolicy, createAuditFieldRedactor, createProtectedFieldPolicy, FIELD_POLICY_LIMITS, FieldPolicyError, } from "./field-policy.js";
48
48
  export type { GuardrailRunResult, RunGuardrailsOptions, } from "./guardrails.js";
49
49
  export { assertGuardrailsAllowed, GuardrailError, MAX_GUARDRAIL_CONCURRENCY, runGuardrails } from "./guardrails.js";
50
50
  export type { AgentIdentity, AssertIdentityActiveOptions, IdentityLimits, IdentityVerifier, NarrowIdentityOptions, Principal, ResolvedIdentityLimits, } from "./identity.js";
@@ -62,9 +62,11 @@ export type { MockProviderOptions } from "./mock-provider.js";
62
62
  export { createMockProvider } from "./mock-provider.js";
63
63
  export type { ModelRegistry, ModelRegistryOptions } from "./models.js";
64
64
  export { createModelRegistry } from "./models.js";
65
+ export { abortableSleep, type OAuthTokenSuccessPayload, type PollDeviceCodeTokenOptions, pollDeviceCodeToken, redactOAuthError, throwIfAborted, } from "./oauth-device-code.js";
65
66
  export { createProviderTurnMetadata, readProviderHttpStatus } from "./observability.js";
66
67
  export type { ApplyRetentionInput, ApplyRetentionResult, ConsumeTenantQuotaInput, ExportUnderHoldInput, LegalHoldExportItem, LegalHoldQuery, LegalHoldRecord, PersistenceLifecycleStore, PersistenceResourceKind, PutLegalHoldInput, ReleaseLegalHoldInput, SetTenantQuotaInput, TenantQuota, } from "./persistence-lifecycle.js";
67
68
  export { createMemoryPersistenceLifecycle, DEFAULT_LIFECYCLE_PAGE_SIZE, DEFAULT_MAX_HOLD_REASON_BYTES, HARD_LIFECYCLE_PAGE_SIZE, HARD_MAX_HOLD_REASON_BYTES, isResourceHeld, PersistenceLifecycleError, } from "./persistence-lifecycle.js";
69
+ export { boundResponse, defaultResolver, isLoopbackAddress, isLoopbackHostname, normalizeHostname, type PinnedFetchOptions, pinnedFetch, raceAbort, requestPinned, resolvePinnedAddress, } from "./pinned-fetch.js";
68
70
  export { providerContentDelta, providerContinuationRequired, providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, toolCallContent, toolCallFromArgumentsText, } from "./provider-events.js";
69
71
  export { authMethodKey, defineProviderPackage, systemPromptContributionKey } from "./provider-packages.js";
70
72
  export type { SessionCachePolicyOptions } from "./provider-request-policy.js";
@@ -109,5 +111,5 @@ export { createToolParameterValidator, createToolRegistry, dispatchToolCall, fil
109
111
  export type { ResolvedUseCaseModel, ResolveUseCaseModelInput, UseCaseModelBinding, } from "./use-case-model.js";
110
112
  export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
111
113
  export declare const name = "prism";
112
- export declare const version = "0.2.6";
114
+ export declare const version = "0.2.8";
113
115
  export declare const description = "Agent harness for AI providers, agents, sessions, and tools.";
package/dist/index.js CHANGED
@@ -1,6 +1,4 @@
1
1
  export { resolveAgentDefinition } from "./agent-definitions.js";
2
- export { abortableSleep, pollDeviceCodeToken, redactOAuthError, throwIfAborted, } from "./oauth-device-code.js";
3
- export { boundResponse, defaultResolver, isLoopbackAddress, isLoopbackHostname, normalizeHostname, pinnedFetch, raceAbort, requestPinned, resolvePinnedAddress, } from "./pinned-fetch.js";
4
2
  export { AgentEventSourceError, createMemoryAgentEventSource } from "./agent-event-source.js";
5
3
  export { dispatchToolCallsInOrder, generateValidateReviseLoop, isAgentLoopOptions, resolveLoop, resolveToolConcurrency, singleShotLoop, } from "./agent-loops.js";
6
4
  export { createAgentRunLifecycle } from "./agent-run-lifecycle.js";
@@ -8,7 +6,7 @@ export { AGENT_RUN_STATE_NAMESPACE, AGENT_RUN_STATE_SCHEMA_VERSION, agentFingerp
8
6
  export { createAgent, createAgentSession, resumeAgentRun, resumeAgentRunStream } from "./agents.js";
9
7
  export { ARTIFACT_BODY_ERROR_CODES, ARTIFACT_CHECKPOINT_NAMESPACE, ArtifactBodyStoreError, ArtifactError, artifactApprovalState, artifactCheckpointKey, } from "./artifacts.js";
10
8
  export { applyCacheControl, cacheHitRate, cacheSavings, cacheUsageReport, mapCacheRetention, sanitizeCacheKey } from "./cache-helpers.js";
11
- export { CACHE_TELEMETRY_OVERFLOW_KEY, CacheTelemetryError, DEFAULT_CACHE_TELEMETRY_CAP, createCacheTelemetry, } from "./cache-telemetry.js";
9
+ export { CACHE_TELEMETRY_OVERFLOW_KEY, CacheTelemetryError, createCacheTelemetry, DEFAULT_CACHE_TELEMETRY_CAP, } from "./cache-telemetry.js";
12
10
  export { CHECKPOINT_CONFLICT_CODE, CheckpointConflictError, createMemoryCheckpointStore } from "./checkpoints.js";
13
11
  export { createDefaultCompactionStrategy, isCompactionEntryData } from "./compaction.js";
14
12
  export { assertJsonObject, isJsonObject, loadConfigLayers, mergeConfigLayers } from "./config.js";
@@ -24,6 +22,7 @@ export { createEventMultiplexer, EVENT_MULTIPLEXER_SINGLE_CONSUMER_CODE, EventMu
24
22
  export { applyExecutionDecision, assertExecutionAllowed, checkExecution, ExecutionDeniedError } from "./execution-policy.js";
25
23
  export { createExtensionEventBus, createExtensionKernel } from "./extensions.js";
26
24
  export { createMemoryRunFeedbackStore, prepareRunFeedback, RunFeedbackError, requireRunFeedbackOwnership, runFeedbackPageLimit, } from "./feedback.js";
25
+ export { ALLOW_FIELD_POLICY, applyFieldPolicy, createAuditFieldRedactor, createProtectedFieldPolicy, FIELD_POLICY_LIMITS, FieldPolicyError, } from "./field-policy.js";
27
26
  export { assertGuardrailsAllowed, GuardrailError, MAX_GUARDRAIL_CONCURRENCY, runGuardrails } from "./guardrails.js";
28
27
  export { assertIdentityActive, assertIdentityMatchesOwnership, assertIdentityPropagation, DEFAULT_IDENTITY_LIMITS, HARD_IDENTITY_LIMITS, IdentityError, identityTelemetryAttributes, narrowIdentity, ownershipFromIdentity, resolveIdentityLimits, resolveRunIdentity, } from "./identity.js";
29
28
  export { assembleProviderInput, createDefaultInputBuilder, createDefaultPromptBuilder, renderPromptTemplate, resolveContextProviders, } from "./input.js";
@@ -33,8 +32,10 @@ export { definePrismManifest, parsePrismManifest } from "./manifests.js";
33
32
  export { createMiddlewareRegistry } from "./middleware.js";
34
33
  export { createMockProvider } from "./mock-provider.js";
35
34
  export { createModelRegistry } from "./models.js";
35
+ export { abortableSleep, pollDeviceCodeToken, redactOAuthError, throwIfAborted, } from "./oauth-device-code.js";
36
36
  export { createProviderTurnMetadata, readProviderHttpStatus } from "./observability.js";
37
37
  export { createMemoryPersistenceLifecycle, DEFAULT_LIFECYCLE_PAGE_SIZE, DEFAULT_MAX_HOLD_REASON_BYTES, HARD_LIFECYCLE_PAGE_SIZE, HARD_MAX_HOLD_REASON_BYTES, isResourceHeld, PersistenceLifecycleError, } from "./persistence-lifecycle.js";
38
+ export { boundResponse, defaultResolver, isLoopbackAddress, isLoopbackHostname, normalizeHostname, pinnedFetch, raceAbort, requestPinned, resolvePinnedAddress, } from "./pinned-fetch.js";
38
39
  export { providerContentDelta, providerContinuationRequired, providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, toolCallContent, toolCallFromArgumentsText, } from "./provider-events.js";
39
40
  export { authMethodKey, defineProviderPackage, systemPromptContributionKey } from "./provider-packages.js";
40
41
  export { createProviderRequestPolicyChain, createSessionCachePolicy, mergeProviderRequestOptions } from "./provider-request-policy.js";
@@ -60,6 +61,6 @@ export { DEFAULT_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES, DEFAULT_TOOL_RESULT_FOLD_MI
60
61
  export { createToolParameterValidator, createToolRegistry, dispatchToolCall, filterTools } from "./tools.js";
61
62
  export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
62
63
  export const name = "prism";
63
- export const version = "0.2.6";
64
+ export const version = "0.2.8";
64
65
  export const description = "Agent harness for AI providers, agents, sessions, and tools.";
65
66
  //# sourceMappingURL=index.js.map
package/dist/input.js CHANGED
@@ -4,8 +4,8 @@ import { runInstructionInjectors } from "./instruction-injection.js";
4
4
  import { redactMessage } from "./redaction.js";
5
5
  import { loadTextResource } from "./resources.js";
6
6
  import { skillMessages as buildSkillMessages } from "./skill-disclosure.js";
7
- import { foldToolResultHistory, foldToolResults } from "./tool-result-fold.js";
8
7
  import { composeSystemPrompt } from "./system-prompts.js";
8
+ import { foldToolResultHistory, foldToolResults } from "./tool-result-fold.js";
9
9
  export function createDefaultInputBuilder() {
10
10
  return {
11
11
  name: "default-input",
@@ -1,13 +1,14 @@
1
1
  import type { AgentEvent, ErrorInfo, Message, ProviderRequest, RunLedgerRecord, SessionEntry } from "./contracts.js";
2
+ import type { FieldPolicy } from "./field-policy.js";
2
3
  export interface SecretRedactor {
3
4
  redact<T>(value: T): T;
4
5
  }
5
6
  export declare function createSecretRedactor(secrets: readonly (string | undefined)[]): SecretRedactor;
6
- export declare function redactMessage(message: Message, redactor?: SecretRedactor): Message;
7
- export declare function redactAgentEvent(event: AgentEvent, redactor?: SecretRedactor): AgentEvent;
8
- export declare function redactSessionEntry(entry: SessionEntry, redactor?: SecretRedactor): SessionEntry;
9
- export declare function redactProviderRequest(request: ProviderRequest, redactor?: SecretRedactor): ProviderRequest;
10
- export declare function redactRunLedgerRecord<T extends RunLedgerRecord>(record: T, redactor?: SecretRedactor): T;
7
+ export declare function redactMessage(message: Message, redactor?: SecretRedactor, fieldPolicy?: FieldPolicy, destination?: string, labelFor?: (key: string, path: string) => string | undefined): Message;
8
+ export declare function redactAgentEvent(event: AgentEvent, redactor?: SecretRedactor, fieldPolicy?: FieldPolicy, destination?: string, labelFor?: (key: string, path: string) => string | undefined): AgentEvent;
9
+ export declare function redactSessionEntry(entry: SessionEntry, redactor?: SecretRedactor, fieldPolicy?: FieldPolicy, destination?: string, labelFor?: (key: string, path: string) => string | undefined): SessionEntry;
10
+ export declare function redactProviderRequest(request: ProviderRequest, redactor?: SecretRedactor, fieldPolicy?: FieldPolicy, destination?: string, labelFor?: (key: string, path: string) => string | undefined): ProviderRequest;
11
+ export declare function redactRunLedgerRecord<T extends RunLedgerRecord>(record: T, redactor?: SecretRedactor, fieldPolicy?: FieldPolicy, destination?: string, labelFor?: (key: string, path: string) => string | undefined): T;
11
12
  export declare function resolveRedactor(redactor?: SecretRedactor, secrets?: readonly (string | undefined)[]): SecretRedactor | undefined;
12
13
  export declare function redactSecrets<T>(value: T, secrets: readonly (string | undefined)[]): T;
13
14
  export declare function errorToErrorInfo(error: unknown, secrets?: readonly (string | undefined)[]): ErrorInfo;
package/dist/redaction.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { applyFieldPolicy } from "./field-policy.js";
1
2
  const REDACTED = "[REDACTED]";
2
3
  // Depth bound matching agent-run-state.ts; hostile deep structures yield a placeholder
3
4
  // instead of a stack overflow.
@@ -5,20 +6,27 @@ const MAX_REDACT_DEPTH = 32;
5
6
  export function createSecretRedactor(secrets) {
6
7
  return { redact: (value) => redactSecrets(value, secrets) };
7
8
  }
8
- export function redactMessage(message, redactor) {
9
- return redactor?.redact(message) ?? message;
9
+ /** Applies a field policy after secret redaction; identity when absent (compat). */
10
+ function afterSecrets(value, redactor, policy, destination, labelFor) {
11
+ const secretRedacted = redactor?.redact(value) ?? value;
12
+ return policy
13
+ ? applyFieldPolicy(secretRedacted, policy, { destination, direction: "outbound", ...(labelFor ? { labelFor } : {}) })
14
+ : secretRedacted;
10
15
  }
11
- export function redactAgentEvent(event, redactor) {
12
- return redactor?.redact(event) ?? event;
16
+ export function redactMessage(message, redactor, fieldPolicy, destination = "prompt", labelFor) {
17
+ return afterSecrets(message, redactor, fieldPolicy, destination, labelFor);
13
18
  }
14
- export function redactSessionEntry(entry, redactor) {
15
- return redactor?.redact(entry) ?? entry;
19
+ export function redactAgentEvent(event, redactor, fieldPolicy, destination = "telemetry", labelFor) {
20
+ return afterSecrets(event, redactor, fieldPolicy, destination, labelFor);
16
21
  }
17
- export function redactProviderRequest(request, redactor) {
18
- return redactor?.redact(request) ?? request;
22
+ export function redactSessionEntry(entry, redactor, fieldPolicy, destination = "persistence", labelFor) {
23
+ return afterSecrets(entry, redactor, fieldPolicy, destination, labelFor);
19
24
  }
20
- export function redactRunLedgerRecord(record, redactor) {
21
- return redactor?.redact(record) ?? record;
25
+ export function redactProviderRequest(request, redactor, fieldPolicy, destination = "prompt", labelFor) {
26
+ return afterSecrets(request, redactor, fieldPolicy, destination, labelFor);
27
+ }
28
+ export function redactRunLedgerRecord(record, redactor, fieldPolicy, destination = "run-ledger", labelFor) {
29
+ return afterSecrets(record, redactor, fieldPolicy, destination, labelFor);
22
30
  }
23
31
  export function resolveRedactor(redactor, secrets) {
24
32
  return redactor ?? (secrets?.some((secret) => Boolean(secret)) ? createSecretRedactor(secrets) : undefined);
package/dist/tools.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AgentEvent, ErrorInfo, Guardrails, JsonObject, OwnershipScope, RunLedger, ToolCallContent, ToolDefinition, ToolExecutionContext, ToolEffectDeclaration, ToolEffectStore, ToolRegistry, ToolResult } from "./contracts.js";
1
+ import type { AgentEvent, ErrorInfo, Guardrails, JsonObject, OwnershipScope, RunLedger, ToolCallContent, ToolDefinition, ToolEffectDeclaration, ToolEffectStore, ToolExecutionContext, ToolRegistry, ToolResult } from "./contracts.js";
2
2
  import type { MiddlewareRegistry } from "./middleware.js";
3
3
  import { type SecretRedactor } from "./redaction.js";
4
4
  import { type DuplicateRegistrationOptions } from "./registry-options.js";
package/dist/tools.js CHANGED
@@ -5,7 +5,7 @@ import { createId } from "./ids.js";
5
5
  import { errorToErrorInfo, redactRunLedgerRecord, redactSecrets } from "./redaction.js";
6
6
  import { assertCanRegister } from "./registry-options.js";
7
7
  import { assertPermission, assertTrusted } from "./security.js";
8
- import { deriveToolEffectKey, toolEffectArgumentsHash, ToolEffectError } from "./tool-effects.js";
8
+ import { deriveToolEffectKey, ToolEffectError, toolEffectArgumentsHash } from "./tool-effects.js";
9
9
  /** Wrap a schema adapter as the existing `ToolValidator` seam used by dispatch and the agent runtime. */
10
10
  export function createToolParameterValidator(validator, options = {}) {
11
11
  const missingSchema = options.missingSchema ?? "allow";
@@ -1,6 +1,6 @@
1
1
  # 0.1.0 / 1.0 Readiness Gates
2
2
 
3
- Status: **0.2.6** is the current release line (the 0.2.x review-remediation line: fail-closed runtime/sandbox security, provider completion and outbound trust boundaries, concurrent-state/durability integrity, build/coverage/release-evidence integrity, package/documentation/compatibility truth, maintainability and bounded performance, fully featured coding-agent readiness); **0.1.7** was the terminal 0.1.x baseline; **1.0** readiness remains operator-gated, not automatic.
3
+ Status: **0.2.8** is the current release line (the 0.2.x review-remediation line: fail-closed runtime/sandbox security, provider completion and outbound trust boundaries, concurrent-state/durability integrity, build/coverage/release-evidence integrity, package/documentation/compatibility truth, maintainability and bounded performance, fully featured coding-agent readiness, enterprise ERP production readiness, ACP adoption fixes); **0.1.7** was the terminal 0.1.x baseline; **1.0** readiness remains operator-gated, not automatic.
4
4
 
5
5
  This page distills runnable readiness gates into one command-per-gate table.
6
6
  The **Last evidence** column records the 0.1.0-tree snapshot (plan 012 Tasks
@@ -20,15 +20,15 @@ Historical release lines (0.0.16 floor → 0.0.27 Phase 10 ACP interop → 0.1.0
20
20
  keep their per-phase evidence in the pages above; this page records the 0.2.6
21
21
  snapshot (plan 026) with the 0.1.x tables below as the historical record.
22
22
 
23
- ## Current line (0.2.6)
23
+ ## Current line (0.2.8)
24
24
 
25
25
  | Item | Status |
26
26
  |---|---|
27
- | Published graph | **50** publishable manifests at exact **0.2.6** (root + 49 workspace packages: 14 provider adapters + 9 `prism-*` family/profile + 26 capability; generated by `node scripts/package-truth.mjs` → `scripts/package-truth.json`) |
28
- | Current-line cut | The 0.2.x review-remediation line, additive-only vs the frozen 0.1.x contract: 0.2.0 fail-closed runtime/sandbox security (durable-resume decision validation, work-tool env isolation, explicit sandbox capabilities), 0.2.1 provider completion + outbound trust boundaries (strict stream completion, bounded success bodies, DNS-pinned OIDC/OPA fetches), 0.2.2 concurrent-state/durability integrity (model-budget reservation, conversation-metadata CAS, single-consumer EventMultiplexer, NATS durable identity), 0.2.3 build/coverage/release-evidence integrity (build single-flight, corrected coverage denominators, release skip manifest, stabilized quality gates), 0.2.4 package/documentation/compatibility truth (umbrella wording matches manifests, manifest-derived package truth, peer-version policy Decision A, current-line truth), 0.2.5 maintainability and bounded performance (god-module splits into cohesive family files behind preserved barrels, persistence-mechanics dedup into `session-store-codecs`, quadratic `Buffer.concat` removed from framing/tar, dead-code cleanup internal-only, 76 behavior-backed coverage regressions), 0.2.6 fully featured coding-agent readiness (host-selected PTY backend, scalable indexed code-search seam, multi-worktree/repository lifecycle, forge breadth demand-gated, durable ACP/process recovery, patch review + incremental diagnostics, protected real coding journey) |
29
- | Upgrade path | `docs/migration.md` `0.2.5 → 0.2.6` (additive; three new versioned record namespaces + optional ACP `activeRun` ref; rollback = stop 0.2.6 workers, mark active records unknown, restore 0.2.5 manifests/tag); store-compatible throughout 0.2.x |
30
- | Compat promise | Additive-only vs the frozen 0.1.x contract; `scripts/compat-baseline` regenerated at 0.2.6 (version literal + additive PTY/index/workspace/recovery/review exports), zero breaking deltas |
31
- | Security policy | `npm audit --audit-level=moderate` 0 at 0.2.6; threat-suites legs (phase8–11 + phase20–26) green; protected Postgres recovery/workspace conformance, PTY, real coding journey, NATS/live-canary legs operator-gated |
27
+ | Published graph | **51** publishable manifests at exact **0.2.8** (root + 50 workspace packages: 14 provider adapters + 9 `prism-*` family/profile + 27 capability; generated by `node scripts/package-truth.mjs` → `scripts/package-truth.json`) |
28
+ | Current-line cut | The 0.2.x review-remediation line, additive-only vs the frozen 0.1.x contract: 0.2.0 fail-closed runtime/sandbox security, 0.2.1 provider completion + outbound trust boundaries, 0.2.2 concurrent-state/durability integrity, 0.2.3 build/coverage/release-evidence integrity, 0.2.4 package/documentation/compatibility truth, 0.2.5 maintainability and bounded performance, 0.2.6 fully featured coding-agent readiness, 0.2.7 enterprise ERP production readiness, 0.2.8 ACP adoption fixes (client-neutrality guard, truthful ACP wire, spawnable `@arnilo/prism-acp-agent`, thought/plan/title/image/command updates) |
29
+ | Upgrade path | `docs/migration.md` `0.2.7 → 0.2.8` (additive; no store migration; rollback = restore 0.2.7 manifests/tag); store-compatible throughout 0.2.x |
30
+ | Compat promise | Additive-only vs the frozen 0.1.x contract; `scripts/compat-baseline` regenerated at 0.2.8 (version literal + additive ACP/agent exports), zero breaking deltas |
31
+ | Security policy | `npm audit --audit-level=moderate` 0 at 0.2.8; threat-suites legs (phase8–11 + phase20–26) green; protected Postgres recovery/workspace conformance, PTY, real coding journey, NATS/live-canary legs operator-gated |
32
32
  | Docs freeze | tripwires green including the canonical manifest-count tripwire (50/49/14/9/26), the plan 024 package-truth tests (generator reproducibility + artifact equality + closure asserts + derived docs truth), the plan 025 bounded-accumulation near-limit probe, and the plan 026 freeze tripwires (per-task markers, threat T1–T8 test mapping, exit gate green) |
33
33
  | 0.1.x line | **0.1.7** (plan 019) is the terminal 0.1.x baseline; the 0.1.1 table below keeps the plan 013 snapshot; the 0.1.0 table keeps the plan 012 snapshot; the **0.0.16** values remain the historical network-free floor |
34
34