@zhivex-ai/core 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- import { randomBytes } from "node:crypto";
1
+ import { randomBytes, timingSafeEqual } from "node:crypto";
2
2
  import { cancelAgentRun, cancelAgentRunTree, resumeAgent, runAgent, streamAgent } from "./agent.js";
3
- import { createAgentHarnessBinding } from "./agent-harness.js";
3
+ import { createAgentExecutionEnvironmentBinding, createAgentHarnessBinding, fingerprintAgentHarness } from "./agent-harness.js";
4
4
  import { createAgentRunSnapshot, replayAgentRun } from "./agent-evaluation.js";
5
5
  import { createAgentTraceArtifact, createProductionTraceOptions, createHierarchicalAgentTrace, estimateAgentRunCost, summarizeAgentTrace } from "./agent-trace.js";
6
6
  import { createRedactionPolicy } from "./safety-policy.js";
@@ -68,6 +68,127 @@ const supervisedPermissions = new Set([
68
68
  ...writePermissions,
69
69
  "network"
70
70
  ]);
71
+ const allToolPermissions = new Set([
72
+ "read",
73
+ "write",
74
+ "network",
75
+ "filesystem",
76
+ "code-execution",
77
+ "shell",
78
+ "external-side-effect"
79
+ ]);
80
+ const allToolRiskLevels = new Set(["low", "medium", "high", "critical"]);
81
+ const allToolPolicyModes = new Set(["allow-all", "read-only", "deny-write", "supervised"]);
82
+ const allAgentTiers = new Set(["tier-a", "tier-b", "tier-c"]);
83
+ const allAgentStatuses = new Set([
84
+ "queued",
85
+ "running",
86
+ "completed",
87
+ "suspended",
88
+ "waiting_approval",
89
+ "cancel_requested",
90
+ "failed",
91
+ "cancelled",
92
+ "timed_out"
93
+ ]);
94
+ const allApprovalKinds = new Set([
95
+ "provider",
96
+ "local-tool",
97
+ "subagent"
98
+ ]);
99
+ const controlPlaneRecord = (value, name) => {
100
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
101
+ throw new ValidationError(`${name} must be an object.`);
102
+ }
103
+ return value;
104
+ };
105
+ const controlPlaneString = (value, name, allowEmpty = false) => {
106
+ if (typeof value !== "string" || (!allowEmpty && !value.trim())) {
107
+ throw new ValidationError(`${name} must be a${allowEmpty ? "" : " non-empty"} string.`);
108
+ }
109
+ return value;
110
+ };
111
+ const controlPlaneOptionalString = (value, name, allowEmpty = false) => value === undefined ? undefined : controlPlaneString(value, name, allowEmpty);
112
+ const controlPlaneFiniteNumber = (value, name, minimum = 0) => {
113
+ if (typeof value !== "number" || !Number.isFinite(value) || value < minimum) {
114
+ throw new ValidationError(`${name} must be a finite number greater than or equal to ${minimum}.`);
115
+ }
116
+ return value;
117
+ };
118
+ const cloneControlPlaneJson = (value, name) => {
119
+ try {
120
+ const serialized = JSON.stringify(value);
121
+ if (serialized === undefined) {
122
+ throw new Error("undefined is not JSON");
123
+ }
124
+ return JSON.parse(serialized);
125
+ }
126
+ catch (error) {
127
+ throw new ValidationError(`${name} must be finite JSON.`, { cause: error });
128
+ }
129
+ };
130
+ const cloneControlPlaneMetadata = (value, name) => {
131
+ if (value === undefined) {
132
+ return undefined;
133
+ }
134
+ controlPlaneRecord(value, name);
135
+ return cloneControlPlaneJson(value, name);
136
+ };
137
+ const normalizeControlPlaneSchemaVersion = (value, name) => {
138
+ if (value === undefined) {
139
+ return;
140
+ }
141
+ if (value !== AGENT_CONTROL_PLANE_SCHEMA_VERSION) {
142
+ if (typeof value === "number" && value > AGENT_CONTROL_PLANE_SCHEMA_VERSION) {
143
+ throw new ValidationError(`Unsupported ${name} schemaVersion ${value}.`);
144
+ }
145
+ throw new ValidationError(`${name} schemaVersion must be ${AGENT_CONTROL_PLANE_SCHEMA_VERSION}; only records without schemaVersion are treated as legacy.`);
146
+ }
147
+ };
148
+ const normalizeStringList = (value, name) => {
149
+ if (!Array.isArray(value)) {
150
+ throw new ValidationError(`${name} must be an array.`);
151
+ }
152
+ return value.map((entry, index) => controlPlaneString(entry, `${name}[${index}]`));
153
+ };
154
+ const normalizePermissionList = (value, name) => {
155
+ const permissions = normalizeStringList(value, name);
156
+ for (const permission of permissions) {
157
+ if (!allToolPermissions.has(permission)) {
158
+ throw new ValidationError(`${name} contains unsupported permission "${permission}".`);
159
+ }
160
+ }
161
+ return [...new Set(permissions)].sort();
162
+ };
163
+ const approvalRequestFingerprint = (approval) => fingerprintAgentHarness({
164
+ kind: approval.kind ?? "provider",
165
+ provider: approval.provider,
166
+ id: approval.id,
167
+ name: approval.name,
168
+ arguments: approval.arguments,
169
+ serverLabel: approval.serverLabel,
170
+ toolCallId: approval.toolCallId,
171
+ step: approval.step,
172
+ inputDigest: approval.inputDigest,
173
+ toolVersion: approval.toolVersion,
174
+ signature: approval.signature,
175
+ childRunId: approval.childRunId,
176
+ childAgentId: approval.childAgentId,
177
+ childApprovalRequestId: approval.childApprovalRequestId,
178
+ rawData: approval.rawData
179
+ });
180
+ const legacyApprovalRequestFingerprint = (approval) => fingerprintAgentHarness({
181
+ provider: approval.provider,
182
+ id: approval.id,
183
+ name: approval.name,
184
+ arguments: approval.arguments,
185
+ rawData: approval.rawData
186
+ });
187
+ const assertControlPlaneMigrationTarget = (targetVersion, name) => {
188
+ if (targetVersion !== AGENT_CONTROL_PLANE_SCHEMA_VERSION) {
189
+ throw new ValidationError(`Unsupported ${name} migration target ${targetVersion}.`);
190
+ }
191
+ };
71
192
  const normalizeNames = (names) => new Set((names ?? []).map((name) => name.toLowerCase()));
72
193
  const normalizePermissions = (permissions) => new Set(permissions ?? []);
73
194
  const normalizeRiskLevels = (levels) => new Set(levels ?? []);
@@ -79,9 +200,9 @@ const readAdvancedMetadata = (tool) => {
79
200
  };
80
201
  const toolPermissions = (tool) => {
81
202
  const permissions = readAdvancedMetadata(tool).permissions;
82
- return Array.isArray(permissions)
83
- ? permissions.filter((permission) => typeof permission === "string")
84
- : [];
203
+ return permissions === undefined
204
+ ? []
205
+ : normalizePermissionList(permissions, `Tool "${tool.name}" permissions`);
85
206
  };
86
207
  const toolAudit = (tool) => {
87
208
  const audit = readAdvancedMetadata(tool).audit;
@@ -110,6 +231,159 @@ const validateCapsuleId = (id) => {
110
231
  throw new ValidationError('Agent capsule "id" may only contain letters, numbers, dots, colons, underscores, and hyphens.');
111
232
  }
112
233
  };
234
+ const normalizeCapsuleSkill = (value, index) => {
235
+ const name = `AgentCapsuleManifest.skills[${index}]`;
236
+ const skill = controlPlaneRecord(value, name);
237
+ return {
238
+ id: controlPlaneString(skill.id, `${name}.id`),
239
+ name: controlPlaneOptionalString(skill.name, `${name}.name`),
240
+ version: controlPlaneOptionalString(skill.version, `${name}.version`),
241
+ description: controlPlaneOptionalString(skill.description, `${name}.description`, true),
242
+ path: controlPlaneOptionalString(skill.path, `${name}.path`),
243
+ metadata: cloneControlPlaneMetadata(skill.metadata, `${name}.metadata`)
244
+ };
245
+ };
246
+ const normalizeCapsuleMcpServer = (value, index) => {
247
+ const name = `AgentCapsuleManifest.mcpServers[${index}]`;
248
+ const server = controlPlaneRecord(value, name);
249
+ if (server.transport !== "stdio" && server.transport !== "http" && server.transport !== "sse" && server.transport !== "custom") {
250
+ throw new ValidationError(`${name}.transport is not supported.`);
251
+ }
252
+ if (server.riskLevel !== undefined && !allToolRiskLevels.has(server.riskLevel)) {
253
+ throw new ValidationError(`${name}.riskLevel is not supported.`);
254
+ }
255
+ return {
256
+ name: controlPlaneString(server.name, `${name}.name`),
257
+ transport: server.transport,
258
+ command: controlPlaneOptionalString(server.command, `${name}.command`),
259
+ url: controlPlaneOptionalString(server.url, `${name}.url`),
260
+ permissions: server.permissions === undefined
261
+ ? undefined
262
+ : normalizePermissionList(server.permissions, `${name}.permissions`),
263
+ riskLevel: server.riskLevel,
264
+ metadata: cloneControlPlaneMetadata(server.metadata, `${name}.metadata`)
265
+ };
266
+ };
267
+ const normalizeCapsuleEvaluation = (value, index) => {
268
+ const name = `AgentCapsuleManifest.evaluations[${index}]`;
269
+ const evaluation = controlPlaneRecord(value, name);
270
+ const datasetSize = evaluation.datasetSize === undefined
271
+ ? undefined
272
+ : controlPlaneFiniteNumber(evaluation.datasetSize, `${name}.datasetSize`);
273
+ if (datasetSize !== undefined && !Number.isSafeInteger(datasetSize)) {
274
+ throw new ValidationError(`${name}.datasetSize must be a safe integer.`);
275
+ }
276
+ return {
277
+ name: controlPlaneString(evaluation.name, `${name}.name`),
278
+ path: controlPlaneOptionalString(evaluation.path, `${name}.path`),
279
+ datasetSize,
280
+ metadata: cloneControlPlaneMetadata(evaluation.metadata, `${name}.metadata`)
281
+ };
282
+ };
283
+ const normalizeCapsulePolicy = (value) => {
284
+ if (value === undefined) {
285
+ return undefined;
286
+ }
287
+ const policy = controlPlaneRecord(value, "AgentCapsuleManifest.policy");
288
+ if (policy.toolPolicyMode !== undefined && !allToolPolicyModes.has(policy.toolPolicyMode)) {
289
+ throw new ValidationError("AgentCapsuleManifest.policy.toolPolicyMode is not supported.");
290
+ }
291
+ if (policy.defaultRequiresApproval !== undefined && typeof policy.defaultRequiresApproval !== "boolean") {
292
+ throw new ValidationError("AgentCapsuleManifest.policy.defaultRequiresApproval must be a boolean.");
293
+ }
294
+ if (policy.redaction !== undefined && typeof policy.redaction !== "boolean") {
295
+ throw new ValidationError("AgentCapsuleManifest.policy.redaction must be a boolean.");
296
+ }
297
+ return {
298
+ toolPolicyMode: policy.toolPolicyMode,
299
+ defaultRequiresApproval: policy.defaultRequiresApproval,
300
+ redaction: policy.redaction,
301
+ metadata: cloneControlPlaneMetadata(policy.metadata, "AgentCapsuleManifest.policy.metadata")
302
+ };
303
+ };
304
+ const normalizeCapsuleTool = (value, index) => {
305
+ const name = `AgentCapsuleManifest.tools[${index}]`;
306
+ const toolManifest = controlPlaneRecord(value, name);
307
+ if (toolManifest.kind !== "callable" && toolManifest.kind !== "hosted") {
308
+ throw new ValidationError(`${name}.kind is not supported.`);
309
+ }
310
+ if (toolManifest.riskLevel !== undefined && !allToolRiskLevels.has(toolManifest.riskLevel)) {
311
+ throw new ValidationError(`${name}.riskLevel is not supported.`);
312
+ }
313
+ if (typeof toolManifest.requiresApproval !== "boolean") {
314
+ throw new ValidationError(`${name}.requiresApproval must be a boolean.`);
315
+ }
316
+ return {
317
+ name: controlPlaneString(toolManifest.name, `${name}.name`),
318
+ kind: toolManifest.kind,
319
+ provider: controlPlaneOptionalString(toolManifest.provider, `${name}.provider`),
320
+ hostedType: controlPlaneOptionalString(toolManifest.hostedType, `${name}.hostedType`),
321
+ source: controlPlaneString(toolManifest.source, `${name}.source`),
322
+ permissions: normalizePermissionList(toolManifest.permissions, `${name}.permissions`),
323
+ riskLevel: toolManifest.riskLevel,
324
+ owner: controlPlaneOptionalString(toolManifest.owner, `${name}.owner`),
325
+ labels: [...new Set(normalizeStringList(toolManifest.labels, `${name}.labels`))].sort(),
326
+ requiresApproval: toolManifest.requiresApproval,
327
+ approvalVersion: controlPlaneOptionalString(toolManifest.approvalVersion, `${name}.approvalVersion`),
328
+ description: controlPlaneOptionalString(toolManifest.description, `${name}.description`, true)
329
+ };
330
+ };
331
+ const normalizeAgentCapsuleManifestFields = (value) => {
332
+ const input = controlPlaneRecord(value, "AgentCapsuleManifest");
333
+ normalizeControlPlaneSchemaVersion(input.schemaVersion, "AgentCapsuleManifest");
334
+ const id = controlPlaneString(input.id, "AgentCapsuleManifest.id");
335
+ validateCapsuleId(id);
336
+ if (!allAgentTiers.has(input.agentTier)) {
337
+ throw new ValidationError("AgentCapsuleManifest.agentTier is not supported.");
338
+ }
339
+ if (!Array.isArray(input.tools) || !Array.isArray(input.skills) || !Array.isArray(input.mcpServers) || !Array.isArray(input.evaluations)) {
340
+ throw new ValidationError("AgentCapsuleManifest tool, skill, MCP server, and evaluation collections must be arrays.");
341
+ }
342
+ const executionEnvironment = input.executionEnvironment === undefined
343
+ ? undefined
344
+ : cloneControlPlaneJson(controlPlaneRecord(input.executionEnvironment, "AgentCapsuleManifest.executionEnvironment"), "AgentCapsuleManifest.executionEnvironment");
345
+ if (executionEnvironment) {
346
+ createAgentExecutionEnvironmentBinding(executionEnvironment);
347
+ }
348
+ return {
349
+ schemaVersion: AGENT_CONTROL_PLANE_SCHEMA_VERSION,
350
+ id,
351
+ name: controlPlaneString(input.name, "AgentCapsuleManifest.name"),
352
+ version: controlPlaneString(input.version, "AgentCapsuleManifest.version"),
353
+ description: controlPlaneOptionalString(input.description, "AgentCapsuleManifest.description", true),
354
+ provider: controlPlaneString(input.provider, "AgentCapsuleManifest.provider"),
355
+ modelId: controlPlaneString(input.modelId, "AgentCapsuleManifest.modelId"),
356
+ agentTier: input.agentTier,
357
+ tools: input.tools.map(normalizeCapsuleTool).sort((left, right) => left.name.localeCompare(right.name)),
358
+ skills: input.skills.map(normalizeCapsuleSkill).sort((left, right) => left.id.localeCompare(right.id)),
359
+ mcpServers: input.mcpServers.map(normalizeCapsuleMcpServer).sort((left, right) => left.name.localeCompare(right.name)),
360
+ evaluations: input.evaluations.map(normalizeCapsuleEvaluation).sort((left, right) => left.name.localeCompare(right.name)),
361
+ policy: normalizeCapsulePolicy(input.policy),
362
+ executionEnvironment,
363
+ metadata: cloneControlPlaneMetadata(input.metadata, "AgentCapsuleManifest.metadata")
364
+ };
365
+ };
366
+ export const normalizeAgentCapsuleManifest = (value) => {
367
+ const input = controlPlaneRecord(value, "AgentCapsuleManifest");
368
+ const manifestWithoutFingerprint = normalizeAgentCapsuleManifestFields(input);
369
+ const fingerprint = controlPlaneString(input.fingerprint, "AgentCapsuleManifest.fingerprint");
370
+ const expectedFingerprint = createAgentHarnessBinding({
371
+ id: manifestWithoutFingerprint.id,
372
+ version: manifestWithoutFingerprint.version,
373
+ manifest: manifestWithoutFingerprint
374
+ }).fingerprint;
375
+ if (fingerprint !== expectedFingerprint) {
376
+ throw new ValidationError("AgentCapsuleManifest fingerprint does not match its normalized contract.");
377
+ }
378
+ return {
379
+ ...manifestWithoutFingerprint,
380
+ fingerprint
381
+ };
382
+ };
383
+ export const migrateAgentCapsuleManifest = (value, targetVersion = AGENT_CONTROL_PLANE_SCHEMA_VERSION) => {
384
+ assertControlPlaneMigrationTarget(targetVersion, "AgentCapsuleManifest");
385
+ return normalizeAgentCapsuleManifest(value);
386
+ };
113
387
  const inspectTools = (tools) => {
114
388
  const toolSet = toToolSet(tools);
115
389
  if (!toolSet) {
@@ -141,7 +415,7 @@ export const createAgentCapsule = (options) => {
141
415
  validateCapsuleId(id);
142
416
  const providerSupport = inspectProviderAgentSupport(options.agent.model);
143
417
  const tools = inspectTools(options.tools ?? options.agent.tools);
144
- const manifestWithoutFingerprint = {
418
+ const manifestWithoutFingerprint = normalizeAgentCapsuleManifestFields({
145
419
  schemaVersion: AGENT_CONTROL_PLANE_SCHEMA_VERSION,
146
420
  id,
147
421
  name: options.name ?? id,
@@ -162,16 +436,16 @@ export const createAgentCapsule = (options) => {
162
436
  policy: options.policy,
163
437
  executionEnvironment: options.agent.executionEnvironment?.manifest,
164
438
  metadata: options.metadata
165
- };
439
+ });
166
440
  const harness = createAgentHarnessBinding({
167
441
  id,
168
442
  version: manifestWithoutFingerprint.version,
169
443
  manifest: manifestWithoutFingerprint
170
444
  });
171
- const manifest = {
445
+ const manifest = normalizeAgentCapsuleManifest({
172
446
  ...manifestWithoutFingerprint,
173
447
  fingerprint: harness.fingerprint
174
- };
448
+ });
175
449
  return {
176
450
  manifest,
177
451
  agent: {
@@ -190,6 +464,9 @@ export const inspectAgentCapsule = (capsule) => {
190
464
  if ((hasWritePermission(toolManifest.permissions) || toolManifest.riskLevel === "critical") && !toolManifest.requiresApproval) {
191
465
  warnings.push(`Tool "${toolManifest.name}" has write or critical risk without approval.`);
192
466
  }
467
+ if (!toolManifest.permissions.length && !toolManifest.requiresApproval) {
468
+ warnings.push(`Tool "${toolManifest.name}" does not declare permissions and has no approval requirement.`);
469
+ }
193
470
  }
194
471
  if (capsule.providerSupport.agentTier === "tier-c") {
195
472
  warnings.push(`Model "${capsule.manifest.modelId}" is Tier C for agent workloads.`);
@@ -218,9 +495,9 @@ const requestPermissions = (request) => {
218
495
  const value = permissions && typeof permissions === "object" && !Array.isArray(permissions)
219
496
  ? permissions.permissions
220
497
  : undefined;
221
- return Array.isArray(value)
222
- ? value.filter((permission) => typeof permission === "string")
223
- : [];
498
+ return value === undefined
499
+ ? []
500
+ : normalizePermissionList(value, `Tool "${request.tool.name}" permissions`);
224
501
  };
225
502
  const requestRiskLevel = (request) => {
226
503
  const advanced = request.tool.metadata?.advancedRegistry;
@@ -293,25 +570,197 @@ export const createAgentToolPolicy = (options = {}) => {
293
570
  if (permissions.some((permission) => supervisedPermissions.has(permission) && !allowPermissions.has(permission))) {
294
571
  return approvalRequiredDecision(`Tool "${request.tool.name}" requests sensitive permissions.`, mode);
295
572
  }
573
+ if (permissions.length === 0) {
574
+ return approvalRequiredDecision(`Tool "${request.tool.name}" does not declare permissions and requires approval under supervised policy.`, mode);
575
+ }
296
576
  }
297
577
  return { approved: true, metadata: { policy: "agent-control-plane", mode } };
298
578
  };
299
579
  };
300
- export const createAgentApprovalQueue = (state, options = {}) => state.pendingApprovals.map((approval) => ({
301
- schemaVersion: AGENT_CONTROL_PLANE_SCHEMA_VERSION,
302
- type: "agent_approval_queue_item",
303
- runId: state.runId,
304
- agentId: state.agentId,
305
- provider: approval.provider,
306
- approvalRequestId: approval.id,
307
- name: approval.name,
308
- arguments: approval.arguments,
309
- approvalToken: `${options.tokenPrefix ?? "appr"}_${randomBytes(32).toString("base64url")}`,
310
- resumeUrl: typeof options.resumeUrl === "function" ? options.resumeUrl(approval, state) : options.resumeUrl,
311
- reason: typeof options.reason === "function" ? options.reason(approval, state) : options.reason,
312
- expiresAt: typeof options.expiresAt === "function" ? options.expiresAt(approval, state) : options.expiresAt,
313
- rawData: approval.rawData
314
- }));
580
+ export const normalizeAgentApprovalQueueItem = (value) => {
581
+ const input = controlPlaneRecord(value, "AgentApprovalQueueItem");
582
+ const legacy = input.schemaVersion === undefined;
583
+ const legacyShape = input.kind === undefined && input.requestFingerprint === undefined;
584
+ normalizeControlPlaneSchemaVersion(input.schemaVersion, "AgentApprovalQueueItem");
585
+ if (input.type !== "agent_approval_queue_item" && !(legacy && input.type === undefined)) {
586
+ throw new ValidationError('AgentApprovalQueueItem.type must be "agent_approval_queue_item".');
587
+ }
588
+ const kind = input.kind === undefined && legacyShape ? "provider" : input.kind;
589
+ if (!allApprovalKinds.has(kind)) {
590
+ throw new ValidationError("AgentApprovalQueueItem.kind is not supported.");
591
+ }
592
+ const provider = controlPlaneString(input.provider, "AgentApprovalQueueItem.provider");
593
+ const approvalRequestId = controlPlaneString(input.approvalRequestId, "AgentApprovalQueueItem.approvalRequestId");
594
+ const name = controlPlaneString(input.name, "AgentApprovalQueueItem.name");
595
+ const approvalToken = controlPlaneString(input.approvalToken, "AgentApprovalQueueItem.approvalToken");
596
+ if (approvalToken.length > 8_192) {
597
+ throw new ValidationError("AgentApprovalQueueItem.approvalToken exceeds the 8192-character limit.");
598
+ }
599
+ const argumentsValue = controlPlaneOptionalString(input.arguments, "AgentApprovalQueueItem.arguments", true);
600
+ const rawData = input.rawData === undefined
601
+ ? undefined
602
+ : cloneControlPlaneJson(input.rawData, "AgentApprovalQueueItem.rawData");
603
+ const requestFingerprint = input.requestFingerprint === undefined && legacyShape
604
+ ? legacyApprovalRequestFingerprint({
605
+ kind: "provider",
606
+ provider,
607
+ id: approvalRequestId,
608
+ name,
609
+ arguments: argumentsValue ?? "",
610
+ rawData: rawData ?? null
611
+ })
612
+ : controlPlaneString(input.requestFingerprint, "AgentApprovalQueueItem.requestFingerprint");
613
+ if (!/^sha256:[a-f0-9]{64}$/.test(requestFingerprint)) {
614
+ throw new ValidationError("AgentApprovalQueueItem.requestFingerprint must be a sha256 fingerprint.");
615
+ }
616
+ return {
617
+ schemaVersion: AGENT_CONTROL_PLANE_SCHEMA_VERSION,
618
+ type: "agent_approval_queue_item",
619
+ runId: controlPlaneString(input.runId, "AgentApprovalQueueItem.runId"),
620
+ agentId: controlPlaneOptionalString(input.agentId, "AgentApprovalQueueItem.agentId"),
621
+ kind: kind,
622
+ provider,
623
+ approvalRequestId,
624
+ name,
625
+ requestFingerprint,
626
+ arguments: argumentsValue,
627
+ approvalToken,
628
+ resumeUrl: controlPlaneOptionalString(input.resumeUrl, "AgentApprovalQueueItem.resumeUrl"),
629
+ reason: controlPlaneOptionalString(input.reason, "AgentApprovalQueueItem.reason", true),
630
+ expiresAt: input.expiresAt === undefined
631
+ ? undefined
632
+ : controlPlaneFiniteNumber(input.expiresAt, "AgentApprovalQueueItem.expiresAt"),
633
+ rawData
634
+ };
635
+ };
636
+ export const migrateAgentApprovalQueueItem = (value, targetVersion = AGENT_CONTROL_PLANE_SCHEMA_VERSION) => {
637
+ assertControlPlaneMigrationTarget(targetVersion, "AgentApprovalQueueItem");
638
+ return normalizeAgentApprovalQueueItem(value);
639
+ };
640
+ export const createAgentApprovalQueue = (state, options = {}) => {
641
+ const redaction = resolveLedgerRedaction(options.redaction);
642
+ return state.pendingApprovals.map((approval) => {
643
+ const reason = typeof options.reason === "function" ? options.reason(approval, state) : options.reason;
644
+ return normalizeAgentApprovalQueueItem({
645
+ schemaVersion: AGENT_CONTROL_PLANE_SCHEMA_VERSION,
646
+ type: "agent_approval_queue_item",
647
+ runId: state.runId,
648
+ agentId: state.agentId,
649
+ kind: approval.kind ?? "provider",
650
+ provider: approval.provider,
651
+ approvalRequestId: approval.id,
652
+ name: approval.name,
653
+ requestFingerprint: approvalRequestFingerprint(approval),
654
+ arguments: options.includeArguments
655
+ ? (redaction ? redaction.redactText(approval.arguments) : approval.arguments)
656
+ : undefined,
657
+ approvalToken: `${options.tokenPrefix ?? "appr"}_${randomBytes(32).toString("base64url")}`,
658
+ resumeUrl: typeof options.resumeUrl === "function" ? options.resumeUrl(approval, state) : options.resumeUrl,
659
+ reason: reason === undefined ? undefined : (redaction ? redaction.redactText(reason) : reason),
660
+ expiresAt: typeof options.expiresAt === "function" ? options.expiresAt(approval, state) : options.expiresAt,
661
+ rawData: options.includeRawData
662
+ ? (redaction ? redaction.redactJson(approval.rawData) : approval.rawData)
663
+ : undefined
664
+ });
665
+ });
666
+ };
667
+ const assertLedgerIdentity = (value, name, identity) => {
668
+ if (value.runId !== identity.runId ||
669
+ value.provider !== identity.provider ||
670
+ value.modelId !== identity.modelId ||
671
+ value.status !== identity.status) {
672
+ throw new ValidationError(`${name} identity does not match its AgentRunLedger.`);
673
+ }
674
+ };
675
+ export const normalizeAgentRunLedger = (value) => {
676
+ const input = controlPlaneRecord(value, "AgentRunLedger");
677
+ const legacy = input.schemaVersion === undefined;
678
+ normalizeControlPlaneSchemaVersion(input.schemaVersion, "AgentRunLedger");
679
+ if (input.type !== "agent_run_ledger" && !(legacy && input.type === undefined)) {
680
+ throw new ValidationError('AgentRunLedger.type must be "agent_run_ledger".');
681
+ }
682
+ const runId = controlPlaneString(input.runId, "AgentRunLedger.runId");
683
+ const provider = controlPlaneString(input.provider, "AgentRunLedger.provider");
684
+ const modelId = controlPlaneString(input.modelId, "AgentRunLedger.modelId");
685
+ if (!allAgentStatuses.has(input.status)) {
686
+ throw new ValidationError("AgentRunLedger.status is not supported.");
687
+ }
688
+ const status = input.status;
689
+ const identity = { runId, provider, modelId, status };
690
+ const snapshotRecord = controlPlaneRecord(input.snapshot, "AgentRunLedger.snapshot");
691
+ assertLedgerIdentity(snapshotRecord, "AgentRunLedger.snapshot", identity);
692
+ for (const field of ["toolCalls", "childRuns", "compactions", "pendingApprovals"]) {
693
+ if (!Array.isArray(snapshotRecord[field])) {
694
+ throw new ValidationError(`AgentRunLedger.snapshot.${field} must be an array.`);
695
+ }
696
+ }
697
+ controlPlaneFiniteNumber(snapshotRecord.steps, "AgentRunLedger.snapshot.steps");
698
+ controlPlaneString(snapshotRecord.outputText, "AgentRunLedger.snapshot.outputText", true);
699
+ const auditRecord = controlPlaneRecord(input.audit, "AgentRunLedger.audit");
700
+ assertLedgerIdentity(auditRecord, "AgentRunLedger.audit", identity);
701
+ if (auditRecord.type !== "agent_run_audit") {
702
+ throw new ValidationError('AgentRunLedger.audit.type must be "agent_run_audit".');
703
+ }
704
+ for (const field of ["steps", "toolCalls", "toolErrors", "approvals", "childRuns"]) {
705
+ controlPlaneFiniteNumber(auditRecord[field], `AgentRunLedger.audit.${field}`);
706
+ }
707
+ if (!Array.isArray(input.toolAudit)) {
708
+ throw new ValidationError("AgentRunLedger.toolAudit must be an array.");
709
+ }
710
+ for (const [index, entry] of input.toolAudit.entries()) {
711
+ const toolAuditRecord = controlPlaneRecord(entry, `AgentRunLedger.toolAudit[${index}]`);
712
+ if (toolAuditRecord.type !== "agent_tool_audit" ||
713
+ toolAuditRecord.runId !== runId ||
714
+ toolAuditRecord.provider !== provider ||
715
+ toolAuditRecord.modelId !== modelId) {
716
+ throw new ValidationError(`AgentRunLedger.toolAudit[${index}] identity does not match its ledger.`);
717
+ }
718
+ }
719
+ const traceRecord = controlPlaneRecord(input.trace, "AgentRunLedger.trace");
720
+ assertLedgerIdentity(traceRecord, "AgentRunLedger.trace", identity);
721
+ for (const field of ["steps", "events", "approvals"]) {
722
+ if (!Array.isArray(traceRecord[field])) {
723
+ throw new ValidationError(`AgentRunLedger.trace.${field} must be an array.`);
724
+ }
725
+ }
726
+ const summaryRecord = controlPlaneRecord(input.summary, "AgentRunLedger.summary");
727
+ assertLedgerIdentity(summaryRecord, "AgentRunLedger.summary", identity);
728
+ controlPlaneRecord(summaryRecord.latency, "AgentRunLedger.summary.latency");
729
+ for (const field of ["steps", "childRuns", "toolCalls", "toolErrors", "approvals"]) {
730
+ controlPlaneFiniteNumber(summaryRecord[field], `AgentRunLedger.summary.${field}`);
731
+ }
732
+ if (input.timeline !== undefined && !Array.isArray(input.timeline)) {
733
+ throw new ValidationError("AgentRunLedger.timeline must be an array.");
734
+ }
735
+ if (input.cost !== undefined) {
736
+ controlPlaneRecord(input.cost, "AgentRunLedger.cost");
737
+ }
738
+ return {
739
+ schemaVersion: AGENT_CONTROL_PLANE_SCHEMA_VERSION,
740
+ type: "agent_run_ledger",
741
+ runId,
742
+ agentId: controlPlaneOptionalString(input.agentId, "AgentRunLedger.agentId"),
743
+ provider,
744
+ modelId,
745
+ status,
746
+ snapshot: cloneControlPlaneJson(input.snapshot, "AgentRunLedger.snapshot"),
747
+ audit: cloneControlPlaneJson(input.audit, "AgentRunLedger.audit"),
748
+ toolAudit: cloneControlPlaneJson(input.toolAudit, "AgentRunLedger.toolAudit"),
749
+ timeline: input.timeline === undefined
750
+ ? undefined
751
+ : cloneControlPlaneJson(input.timeline, "AgentRunLedger.timeline"),
752
+ trace: cloneControlPlaneJson(input.trace, "AgentRunLedger.trace"),
753
+ summary: cloneControlPlaneJson(input.summary, "AgentRunLedger.summary"),
754
+ cost: input.cost === undefined
755
+ ? undefined
756
+ : cloneControlPlaneJson(input.cost, "AgentRunLedger.cost"),
757
+ metadata: cloneControlPlaneMetadata(input.metadata, "AgentRunLedger.metadata")
758
+ };
759
+ };
760
+ export const migrateAgentRunLedger = (value, targetVersion = AGENT_CONTROL_PLANE_SCHEMA_VERSION) => {
761
+ assertControlPlaneMigrationTarget(targetVersion, "AgentRunLedger");
762
+ return normalizeAgentRunLedger(value);
763
+ };
315
764
  export const createAgentRunLedger = (state, options = {}) => {
316
765
  const traceOptions = createProductionTraceOptions({
317
766
  ...options.trace,
@@ -327,13 +776,20 @@ export const createAgentRunLedger = (state, options = {}) => {
327
776
  includeOutputText: traceOptions.includeOutputText ?? false
328
777
  };
329
778
  const redaction = resolveLedgerRedaction(traceOptions.redaction);
330
- const snapshot = sanitizeLedgerValue(createAgentRunSnapshot(state), sanitizationOptions, redaction);
779
+ const snapshot = {
780
+ ...sanitizeLedgerValue(createAgentRunSnapshot(state), sanitizationOptions, redaction),
781
+ runId: state.runId,
782
+ agentId: state.agentId,
783
+ provider: state.provider,
784
+ modelId: state.modelId,
785
+ status: state.status
786
+ };
331
787
  const replay = replayAgentRun(state);
332
788
  const trace = createAgentTraceArtifact(state, traceOptions);
333
789
  const summary = summarizeAgentTrace(trace, { pricing: options.pricing });
334
790
  const auditOptions = { ...options, redaction: traceOptions.redaction };
335
791
  const audit = createAgentAuditRecord(state, auditOptions);
336
- return {
792
+ return normalizeAgentRunLedger({
337
793
  schemaVersion: AGENT_CONTROL_PLANE_SCHEMA_VERSION,
338
794
  type: "agent_run_ledger",
339
795
  runId: state.runId,
@@ -353,7 +809,7 @@ export const createAgentRunLedger = (state, options = {}) => {
353
809
  metadata: options.includeMetadata && audit.metadata
354
810
  ? audit.metadata
355
811
  : undefined
356
- };
812
+ });
357
813
  };
358
814
  const addDiff = (changes, field, left, right) => {
359
815
  if (JSON.stringify(left) !== JSON.stringify(right)) {
@@ -492,6 +948,11 @@ const toRunnerInput = (input) => {
492
948
  const { state: _state, handoff: _handoff, parentRunId: _parentRunId, ...runnerInput } = input;
493
949
  return runnerInput;
494
950
  };
951
+ const approvalTokensMatch = (expected, actual) => {
952
+ const expectedBytes = Buffer.from(expected);
953
+ const actualBytes = Buffer.from(actual);
954
+ return expectedBytes.length === actualBytes.length && timingSafeEqual(expectedBytes, actualBytes);
955
+ };
495
956
  export const createAgentControlPlane = (options) => {
496
957
  const runtimeAgent = options.agent.harness
497
958
  ? options.agent
@@ -523,6 +984,60 @@ export const createAgentControlPlane = (options) => {
523
984
  const output = await resumeAgent(runtimeAgent, input);
524
985
  return record(output.state);
525
986
  },
987
+ async resumeApproval(input) {
988
+ const { queueItem: rawQueueItem, approvalToken, approve, reason, now = Date.now(), ...resumeInput } = input;
989
+ const queueItem = normalizeAgentApprovalQueueItem(rawQueueItem);
990
+ const presentedToken = controlPlaneString(approvalToken, "Agent approval token");
991
+ if (!approvalTokensMatch(queueItem.approvalToken, presentedToken)) {
992
+ throw new ValidationError("Agent approval token is invalid.");
993
+ }
994
+ if (typeof approve !== "boolean") {
995
+ throw new ValidationError("Agent approval decision must be a boolean.");
996
+ }
997
+ const decisionReason = controlPlaneOptionalString(reason, "Agent approval reason", true);
998
+ controlPlaneFiniteNumber(now, "Agent approval clock");
999
+ if (queueItem.expiresAt !== undefined && now >= queueItem.expiresAt) {
1000
+ throw new ValidationError("Agent approval has expired.");
1001
+ }
1002
+ if (!runtimeAgent.store) {
1003
+ throw new ValidationError("Durable approval resume requires an AgentRunStore.");
1004
+ }
1005
+ const state = await runtimeAgent.store.load(queueItem.runId, resumeInput.scope);
1006
+ if (!state) {
1007
+ throw new ValidationError(`Agent run "${queueItem.runId}" was not found.`);
1008
+ }
1009
+ if (queueItem.agentId !== undefined && state.agentId !== queueItem.agentId) {
1010
+ throw new ValidationError("Agent approval queue item does not match the persisted agent.");
1011
+ }
1012
+ const pending = state.pendingApprovals.find((approval) => approval.provider === queueItem.provider &&
1013
+ approval.id === queueItem.approvalRequestId);
1014
+ if (!pending) {
1015
+ throw new ValidationError("Agent approval is no longer pending or was already consumed.");
1016
+ }
1017
+ if (pending.name !== queueItem.name) {
1018
+ throw new ValidationError("Agent approval queue item does not match the pending request.");
1019
+ }
1020
+ const currentFingerprint = approvalRequestFingerprint(pending);
1021
+ const legacyFingerprint = legacyApprovalRequestFingerprint(pending);
1022
+ const isLegacyProjection = queueItem.requestFingerprint === legacyFingerprint;
1023
+ if (queueItem.requestFingerprint !== currentFingerprint && !isLegacyProjection) {
1024
+ throw new ValidationError("Agent approval request fingerprint does not match the persisted request.");
1025
+ }
1026
+ if (!isLegacyProjection && queueItem.kind !== (pending.kind ?? "provider")) {
1027
+ throw new ValidationError("Agent approval queue item kind does not match the persisted request.");
1028
+ }
1029
+ const output = await resumeAgent(runtimeAgent, {
1030
+ ...resumeInput,
1031
+ state,
1032
+ approvals: [{
1033
+ provider: pending.provider,
1034
+ approvalRequestId: pending.id,
1035
+ approve,
1036
+ reason: decisionReason
1037
+ }]
1038
+ });
1039
+ return record(output.state);
1040
+ },
526
1041
  stream(input = {}) {
527
1042
  if (runner && hasSessionInput(options, input)) {
528
1043
  return runner.stream(toRunnerInput(input));