@opengeni/runtime 2.4.3 → 2.5.1-canary.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.
Files changed (36) hide show
  1. package/dist/{agent-UA75SUZ7.js → agent-Q2VNH5FV.js} +2 -2
  2. package/dist/{chunk-ORKCCH5X.js → chunk-25B3EQRW.js} +3 -398
  3. package/dist/{chunk-ORKCCH5X.js.map → chunk-25B3EQRW.js.map} +1 -1
  4. package/dist/{chunk-BV2DSPZK.js → chunk-26GUTY32.js} +267 -181
  5. package/dist/chunk-26GUTY32.js.map +1 -0
  6. package/dist/{chunk-YRKIQK25.js → chunk-CD22I76N.js} +2 -2
  7. package/dist/{chunk-7K6WOXFS.js → chunk-GB763TSV.js} +17 -1
  8. package/dist/chunk-GB763TSV.js.map +1 -0
  9. package/dist/{chunk-WATBACLT.js → chunk-LGLJ5PVD.js} +5 -88
  10. package/dist/chunk-LGLJ5PVD.js.map +1 -0
  11. package/dist/index.d.ts +12 -5
  12. package/dist/index.js +11 -5
  13. package/dist/{localSources-GDQRRHT3.js → localSources-Z5IJIIRV.js} +3 -3
  14. package/dist/model-input.d.ts +2 -1
  15. package/dist/runtime-skills.d.ts +28 -5
  16. package/dist/sandbox/index.js +3 -3
  17. package/dist/skill-files.d.ts +4 -0
  18. package/dist/skill-library.d.ts +1 -1
  19. package/dist/skill-library.js +3 -1
  20. package/dist/workspace-tool-gateway.js +5 -5
  21. package/package.json +11 -11
  22. package/src/bundled_management_skills/opengeni-skills/SKILL.md +3 -1
  23. package/src/index.ts +184 -33
  24. package/src/lazy-tool-transport.ts +0 -3
  25. package/src/model-input.ts +1 -1
  26. package/src/run-events.ts +23 -0
  27. package/src/runtime-skills.ts +122 -161
  28. package/src/sandbox/routing/routing-session.ts +1 -1
  29. package/src/skill-files.ts +20 -0
  30. package/src/skill-library.ts +1 -0
  31. package/dist/chunk-7K6WOXFS.js.map +0 -1
  32. package/dist/chunk-BV2DSPZK.js.map +0 -1
  33. package/dist/chunk-WATBACLT.js.map +0 -1
  34. /package/dist/{agent-UA75SUZ7.js.map → agent-Q2VNH5FV.js.map} +0 -0
  35. /package/dist/{chunk-YRKIQK25.js.map → chunk-CD22I76N.js.map} +0 -0
  36. /package/dist/{localSources-GDQRRHT3.js.map → localSources-Z5IJIIRV.js.map} +0 -0
package/src/index.ts CHANGED
@@ -184,6 +184,7 @@ import {
184
184
  type SerializedTool,
185
185
  type Tool,
186
186
  } from "@openai/agents";
187
+ import { getToolSearchExecution, getToolSearchProviderCallId } from "@openai/agents-core/utils";
187
188
  import {
188
189
  Capabilities,
189
190
  Manifest,
@@ -196,7 +197,6 @@ import {
196
197
  inContainerMountStrategy,
197
198
  s3Mount,
198
199
  shell,
199
- skills,
200
200
  type SandboxClient,
201
201
  type SandboxSessionLike,
202
202
  type SandboxSessionState,
@@ -214,6 +214,8 @@ import { AsyncLocalStorage } from "node:async_hooks";
214
214
  import { createHash, randomUUID } from "node:crypto";
215
215
  import { dirname, isAbsolute, join, posix as posixPath } from "node:path";
216
216
 
217
+ import { z } from "zod";
218
+
217
219
  import { sanitizeHistoryItemsForModel } from "./history-sanitizer";
218
220
  import { OPENGENI_OPERATIONAL_INSTRUCTIONS } from "./operational-instructions";
219
221
  import {
@@ -277,13 +279,18 @@ import {
277
279
  import { workspaceSkills, type WorkspaceSkillSearchPath } from "./workspace-skills";
278
280
  import {
279
281
  composeRuntimeSkills,
282
+ readRuntimeSkill,
283
+ skillCatalogFromComposition,
280
284
  type EffectiveSkillSelection,
281
285
  type RuntimeSkillActivation,
282
286
  type RuntimeSkillComposition,
287
+ type RuntimeSkillIndexEntry,
283
288
  } from "./runtime-skills";
284
289
  export {
285
290
  composeRuntimeSkills,
286
291
  loadNativeToolSkillArtifacts,
292
+ readRuntimeSkill,
293
+ skillCatalogFromComposition,
287
294
  type EffectiveSkillSelection,
288
295
  type InstalledSkillActivation,
289
296
  type NativeToolSkillSet,
@@ -293,6 +300,7 @@ export {
293
300
  type RuntimeSkillArtifactFile,
294
301
  type RuntimeSkillComposition,
295
302
  type RuntimeSkillDescriptor,
303
+ type RuntimeSkillIndexEntry,
296
304
  type SessionSkillActivation,
297
305
  } from "./runtime-skills";
298
306
  import {
@@ -316,6 +324,7 @@ import {
316
324
  composeCallModelInputFilters,
317
325
  contextRobustnessFilterForSettings,
318
326
  incrementalModelInputProjectionFilter,
327
+ stripProviderItemId,
319
328
  } from "./model-input";
320
329
  import {
321
330
  recordModelPreparationManifestInventory,
@@ -1014,9 +1023,12 @@ export async function summarizeForCompaction(
1014
1023
  // items without flattening tool history into a fake user transcript.
1015
1024
  const request: ModelRequest = {
1016
1025
  systemInstructions: options.systemInstructions ?? "",
1017
- input: input as AgentInputItem[],
1026
+ input: input.map(detachCompactionResponseItemIdentity) as AgentInputItem[],
1018
1027
  modelSettings: {
1019
1028
  maxTokens,
1029
+ // Azure can select a historical tool despite empty schemas. Keep this
1030
+ // verified policy off subscription/gateway transports with other contracts.
1031
+ ...(provider.wireProfile === "azure-openai" ? { toolChoice: "none" as const } : {}),
1020
1032
  // Azure rejects store:false; the Codex subscription transport enforces
1021
1033
  // it independently. The OpenAI platform path remains explicitly storeless.
1022
1034
  ...(settings.openaiProvider === "azure" ? {} : { store: false }),
@@ -1050,6 +1062,47 @@ export async function summarizeForCompaction(
1050
1062
  return summary;
1051
1063
  }
1052
1064
 
1065
+ /**
1066
+ * Portable checkpoints carry inline history, not references to stored Responses
1067
+ * items. An assistant/tool item's provider id can require its original reasoning
1068
+ * item even when the full message is supplied. The portable preparation removes
1069
+ * opaque reasoning, so retaining those dependent ids makes Azure reject the
1070
+ * checkpoint. Remove only provider item identity from this request-local copy;
1071
+ * callId/call_id, tool payloads, order, and canonical history stay unchanged.
1072
+ */
1073
+ const DETACHABLE_COMPACTION_ITEM_TYPES = new Set([
1074
+ "message",
1075
+ "reasoning",
1076
+ "function_call",
1077
+ "function_call_result",
1078
+ "shell_call",
1079
+ "shell_call_output",
1080
+ "computer_call",
1081
+ "computer_call_result",
1082
+ "apply_patch_call",
1083
+ "apply_patch_call_output",
1084
+ ]);
1085
+
1086
+ function detachCompactionResponseItemIdentity(
1087
+ item: Record<string, unknown>,
1088
+ ): Record<string, unknown> {
1089
+ const clientToolSearch =
1090
+ (item.type === "tool_search_call" || item.type === "tool_search_output") &&
1091
+ getToolSearchExecution(item) !== "server" &&
1092
+ Boolean(getToolSearchProviderCallId(item));
1093
+ const providerData = item.providerData as Record<string, unknown> | undefined;
1094
+ const webSearch =
1095
+ item.type === "hosted_tool_call" &&
1096
+ (providerData?.type === "web_search_call" || providerData?.type === "web_search");
1097
+ // Unlike web search, hosted file search requires its id on Azure input.
1098
+ // Approval/program references also remain intact. A universal strip is unsafe.
1099
+ if (!DETACHABLE_COMPACTION_ITEM_TYPES.has(String(item.type)) && !clientToolSearch && !webSearch)
1100
+ return item;
1101
+ // The SDK reserves providerData.id for these types; only the top-level id is
1102
+ // emitted. Share the normal inference primitive without changing its policy.
1103
+ return stripProviderItemId(item as AgentInputItem) as Record<string, unknown>;
1104
+ }
1105
+
1053
1106
  /**
1054
1107
  * Serialize the agent's model-visible tools for a remote_v2 compact request.
1055
1108
  *
@@ -1287,11 +1340,13 @@ export function compactionProviderFailureDiagnostics(error: unknown): Record<str
1287
1340
  let type: string | null = null;
1288
1341
  let requestId: string | null = null;
1289
1342
  let eventType: string | null = null;
1343
+ let rejectionReason: "missing_required_reasoning_item" | null = null;
1290
1344
  const seen = new Set<object>();
1291
1345
  for (let depth = 0; depth < 6 && current && typeof current === "object"; depth += 1) {
1292
1346
  if (seen.has(current)) break;
1293
1347
  seen.add(current);
1294
1348
  const record = current as Record<string, unknown>;
1349
+ rejectionReason ??= compactionRejectionReason(record);
1295
1350
  if (!errorName && current instanceof Error) {
1296
1351
  errorName = boundCompactionDiagnosticField(current.name);
1297
1352
  }
@@ -1342,6 +1397,7 @@ export function compactionProviderFailureDiagnostics(error: unknown): Record<str
1342
1397
  const nestedError = record.error;
1343
1398
  if (nestedError && typeof nestedError === "object" && !seen.has(nestedError)) {
1344
1399
  const nested = nestedError as Record<string, unknown>;
1400
+ rejectionReason ??= compactionRejectionReason(nested);
1345
1401
  if (code === null && typeof nested.code === "string") {
1346
1402
  code = boundCompactionDiagnosticField(nested.code);
1347
1403
  }
@@ -1372,9 +1428,24 @@ export function compactionProviderFailureDiagnostics(error: unknown): Record<str
1372
1428
  type,
1373
1429
  requestId,
1374
1430
  ...(eventType ? { eventType } : {}),
1431
+ ...(rejectionReason ? { rejectionReason } : {}),
1375
1432
  };
1376
1433
  }
1377
1434
 
1435
+ function compactionRejectionReason(
1436
+ record: Record<string, unknown>,
1437
+ ): "missing_required_reasoning_item" | null {
1438
+ // Classify the known provider protocol rejection without persisting the
1439
+ // message, referenced item ids, or arbitrary provider-owned fields.
1440
+ return typeof record.message === "string" &&
1441
+ record.message.length <= 1024 &&
1442
+ /^Item '[A-Za-z0-9_-]+' of type '[a-z_]+' was provided without its required 'reasoning' item: '[A-Za-z0-9_-]+'\.$/.test(
1443
+ record.message,
1444
+ )
1445
+ ? "missing_required_reasoning_item"
1446
+ : null;
1447
+ }
1448
+
1378
1449
  const COMPACTION_DIAGNOSTIC_FIELD_MAX_BYTES = 256;
1379
1450
  const COMPACTION_DIAGNOSTIC_TRUNCATION_MARKER = "…[truncated]";
1380
1451
 
@@ -1548,6 +1619,8 @@ export type CodemodeTokenWriterSession = SandboxSessionLike;
1548
1619
 
1549
1620
  const agentSkillSelections = new WeakMap<object, readonly EffectiveSkillSelection[]>();
1550
1621
  const emptySkillSelections: readonly EffectiveSkillSelection[] = Object.freeze([]);
1622
+ const agentRuntimeSkillIndex = new WeakMap<object, readonly RuntimeSkillIndexEntry[]>();
1623
+ const emptyRuntimeSkillIndex: readonly RuntimeSkillIndexEntry[] = Object.freeze([]);
1551
1624
  const agentInstructionInspection = new WeakMap<object, PersistentAgentInstructionInspection>();
1552
1625
  const emptyInstructionInspection: PersistentAgentInstructionInspection = Object.freeze({
1553
1626
  layers: Object.freeze([]),
@@ -1566,6 +1639,11 @@ export function effectiveSkillSelectionsForAgent(
1566
1639
  return agentSkillSelections.get(agent) ?? emptySkillSelections;
1567
1640
  }
1568
1641
 
1642
+ /** Sandbox-independent Skill index admitted for this agent. */
1643
+ export function runtimeSkillIndexForAgent(agent: object): readonly RuntimeSkillIndexEntry[] {
1644
+ return agentRuntimeSkillIndex.get(agent) ?? emptyRuntimeSkillIndex;
1645
+ }
1646
+
1569
1647
  export function persistentAgentInstructionInspectionFor(
1570
1648
  agent: object,
1571
1649
  ): PersistentAgentInstructionInspection {
@@ -1648,6 +1726,8 @@ const modelToolInvocation = new AsyncLocalStorage<ModelToolInvocation>();
1648
1726
  const agentInputWaitYields = new WeakMap<object, InputWaitYield>();
1649
1727
 
1650
1728
  export type BuildAgentOptions = {
1729
+ /** Live authority fence for intrinsic sandbox tools outside the MCP gateway. */
1730
+ authorizeAttemptExecution?: () => Promise<void> | void;
1651
1731
  /** Trusted attempt-local wait receipt shared with prepared tools. */
1652
1732
  inputWaitYield?: InputWaitYield;
1653
1733
  model?: Model;
@@ -1873,10 +1953,8 @@ export type BuildAgentOptions = {
1873
1953
  * executable tool catalog.
1874
1954
  */
1875
1955
  skillActivations?: readonly RuntimeSkillActivation[];
1876
- /** Server-backed Skill descriptors, independent of sandbox capabilities. */
1956
+ /** Host-owned descriptors and reader; mutually exclusive with skillActivations. */
1877
1957
  skillCatalog?: readonly SkillCatalogDescriptor[];
1878
- /** Shared reader serves configured Skills; filesystem discovery remains for repo Skills only. */
1879
- serverSkillReading?: boolean;
1880
1958
  /**
1881
1959
  * Internal per-attempt cancellation boundary. The worker supplies Temporal's
1882
1960
  * signal so an in-flight shell process is interrupted immediately instead of
@@ -2294,6 +2372,12 @@ export function hasCanonicalEditableArtifactToolSurface(
2294
2372
  });
2295
2373
  }
2296
2374
 
2375
+ const SkillReadToolInput = z.object({
2376
+ skill: z.string().min(1).max(512),
2377
+ listFiles: z.boolean().optional(),
2378
+ paths: z.array(z.string().min(1).max(1024)).min(1).max(128).optional(),
2379
+ });
2380
+
2297
2381
  export function buildOpenGeniAgent(
2298
2382
  settings: Settings,
2299
2383
  resources: ResourceRef[],
@@ -2309,6 +2393,29 @@ export function buildOpenGeniAgent(
2309
2393
  const editableArtifactToolsAvailable = hasCanonicalEditableArtifactToolSurface(
2310
2394
  options.attemptToolCatalog,
2311
2395
  );
2396
+ const hostSuppliedSkillCatalog = options.skillCatalog !== undefined;
2397
+ if (hostSuppliedSkillCatalog && options.skillActivations?.length) {
2398
+ throw new Error(
2399
+ "Supply either host Skill catalog/reader or runtime Skill activations, not both.",
2400
+ );
2401
+ }
2402
+ // Site authoring needs filesystem execution; managed and connected compute
2403
+ // are equivalent. Reading supplied Skill files never needs either.
2404
+ const filesystemAvailable = (options.activeSandboxBackend ?? settings.sandboxBackend) !== "none";
2405
+ const skillComposition = composeRuntimeSkills(options.skillActivations ?? [], {
2406
+ editableArtifacts: !hostSuppliedSkillCatalog && editableArtifactToolsAvailable,
2407
+ sites: !hostSuppliedSkillCatalog && filesystemAvailable,
2408
+ videoGeneration: !hostSuppliedSkillCatalog && Boolean(options.videoGeneration),
2409
+ });
2410
+ const skillCatalog = hostSuppliedSkillCatalog
2411
+ ? options.skillCatalog
2412
+ : skillComposition.index.length > 0
2413
+ ? skillCatalogFromComposition(skillComposition)
2414
+ : undefined;
2415
+ const instructionOptions: BuildAgentOptions = {
2416
+ ...options,
2417
+ ...(skillCatalog !== undefined ? { skillCatalog } : {}),
2418
+ };
2312
2419
  // Resolved per-turn gating. Each override defaults to today's settings-derived
2313
2420
  // behaviour, so the legacy global-client callers (no resolved model) build the
2314
2421
  // exact same agent as before; the multi-provider worker path passes the
@@ -2441,7 +2548,26 @@ export function buildOpenGeniAgent(
2441
2548
  ...(videoGenerationTool ? [videoGenerationTool] : []),
2442
2549
  ...(humanInputTool ? [humanInputTool] : []),
2443
2550
  ];
2444
- const instructionInspection = inspectPersistentAgentInstructions(settings, options);
2551
+ const embeddedSkillReadTool =
2552
+ !hostSuppliedSkillCatalog && skillComposition.artifacts.length > 0
2553
+ ? agentTool({
2554
+ name: "skill_read",
2555
+ description:
2556
+ "Read Skill text without starting a sandbox. Omit paths to read SKILL.md; provide relative paths to read exactly those files, never implicitly adding SKILL.md. Set listFiles:true without paths to list relative paths only, with no file bodies. Use an id or name from the Skill index.",
2557
+ parameters: SkillReadToolInput,
2558
+ errorFunction: null,
2559
+ execute: (input) =>
2560
+ JSON.stringify(
2561
+ readRuntimeSkill(skillComposition, {
2562
+ skill: input.skill,
2563
+ ...(input.paths !== undefined ? { paths: input.paths } : {}),
2564
+ ...(input.listFiles !== undefined ? { listFiles: input.listFiles } : {}),
2565
+ }),
2566
+ ),
2567
+ })
2568
+ : null;
2569
+ if (embeddedSkillReadTool) agentTools.push(embeddedSkillReadTool);
2570
+ const instructionInspection = inspectPersistentAgentInstructions(settings, instructionOptions);
2445
2571
  const baseConfig = {
2446
2572
  name: "OpenGeni Agent",
2447
2573
  model: options.model ?? settings.openaiModel,
@@ -2504,6 +2630,8 @@ export function buildOpenGeniAgent(
2504
2630
  const agent = new Agent(baseConfig);
2505
2631
  if (options.inputWaitYield) agentInputWaitYields.set(agent, options.inputWaitYield);
2506
2632
  agentInstructionInspection.set(agent, instructionInspection);
2633
+ agentSkillSelections.set(agent, skillComposition.selections);
2634
+ agentRuntimeSkillIndex.set(agent, skillComposition.index);
2507
2635
  if (options.missingSessionTitleHint ?? options.genesisTitleHint) {
2508
2636
  agentsNeedingGenesisTitleDirective.add(agent);
2509
2637
  }
@@ -2532,26 +2660,6 @@ export function buildOpenGeniAgent(
2532
2660
  return agent;
2533
2661
  }
2534
2662
 
2535
- const skillComposition = composeRuntimeSkills(
2536
- options.serverSkillReading ? [] : (options.skillActivations ?? []),
2537
- {
2538
- editableArtifacts: !options.serverSkillReading && editableArtifactToolsAvailable,
2539
- // Sites guidance is bundled capability metadata, not eager tool authority.
2540
- // Tool discovery/execution remains governed by the lazy attempt gateway.
2541
- sites:
2542
- !options.serverSkillReading &&
2543
- (options.activeSandboxBackend ?? settings.sandboxBackend) !== "selfhosted",
2544
- // A connected machine owns its filesystem, and its session deliberately
2545
- // does not materialize host-local lazy entries. Advertising this bundled
2546
- // skill there makes load_skill report a path that does not exist. Keep the
2547
- // executable tools (whose descriptions contain the full short workflow),
2548
- // but expose the filesystem-backed helper only where it can be delivered.
2549
- videoGeneration:
2550
- !options.serverSkillReading &&
2551
- Boolean(options.videoGeneration) &&
2552
- options.activeSandboxBackend !== "selfhosted",
2553
- },
2554
- );
2555
2663
  if (options.activeSandboxBackend === "selfhosted" && !options.sandboxWorkspaceRoot) {
2556
2664
  throw new Error("A Connected Machine agent requires its reported workspace root");
2557
2665
  }
@@ -2572,6 +2680,9 @@ export function buildOpenGeniAgent(
2572
2680
  ),
2573
2681
  ...(runAs ? { runAs } : {}),
2574
2682
  capabilities: buildAgentCapabilitiesFromComposition(settings, skillComposition, {
2683
+ ...(options.authorizeAttemptExecution
2684
+ ? { authorizeAttemptExecution: options.authorizeAttemptExecution }
2685
+ : {}),
2575
2686
  ...(editableArtifactToolsAvailable ? { editableArtifactToolsAvailable: true } : {}),
2576
2687
  ...(options.videoGeneration ? { videoGenerationAvailable: true } : {}),
2577
2688
  ...repositoryWorkspaceSkillPathsOption(resources),
@@ -2595,6 +2706,7 @@ export function buildOpenGeniAgent(
2595
2706
  }),
2596
2707
  });
2597
2708
  agentSkillSelections.set(agent, skillComposition.selections);
2709
+ agentRuntimeSkillIndex.set(agent, skillComposition.index);
2598
2710
  if (options.inputWaitYield) agentInputWaitYields.set(agent, options.inputWaitYield);
2599
2711
  agentInstructionInspection.set(agent, instructionInspection);
2600
2712
  if (options.missingSessionTitleHint ?? options.genesisTitleHint) {
@@ -3306,6 +3418,7 @@ export function buildAgentCapabilities(
3306
3418
  settings: Settings,
3307
3419
  skillActivations: readonly RuntimeSkillActivation[] = [],
3308
3420
  options: {
3421
+ authorizeAttemptExecution?: () => Promise<void> | void;
3309
3422
  editableArtifactToolsAvailable?: boolean;
3310
3423
  videoGenerationAvailable?: boolean;
3311
3424
  workspaceSkillPaths?: readonly WorkspaceSkillSearchPath[];
@@ -3335,6 +3448,7 @@ function buildAgentCapabilitiesFromComposition(
3335
3448
  settings: Settings,
3336
3449
  skillComposition: RuntimeSkillComposition,
3337
3450
  options: {
3451
+ authorizeAttemptExecution?: () => Promise<void> | void;
3338
3452
  editableArtifactToolsAvailable?: boolean;
3339
3453
  videoGenerationAvailable?: boolean;
3340
3454
  workspaceSkillPaths?: readonly WorkspaceSkillSearchPath[];
@@ -3380,7 +3494,7 @@ function buildAgentCapabilitiesFromComposition(
3380
3494
  ? { configureTools: configureFilesystemTools }
3381
3495
  : {}),
3382
3496
  });
3383
- if (options.structuredToolTransport === false) {
3497
+ if (options.structuredToolTransport === false || options.authorizeAttemptExecution) {
3384
3498
  neutralizeStructuredToolTransport(filesystemCapability);
3385
3499
  }
3386
3500
  const caps: ReturnType<typeof Capabilities.default> = [
@@ -3389,11 +3503,6 @@ function buildAgentCapabilitiesFromComposition(
3389
3503
  ...(toolCancellation ? {} : { configureTools: withExecOpCorrelation }),
3390
3504
  }),
3391
3505
  ];
3392
- caps.push(
3393
- skills({
3394
- lazyFrom: skillComposition.lazySource,
3395
- }),
3396
- );
3397
3506
  if (options.workspaceSkillPaths?.length) {
3398
3507
  caps.push(
3399
3508
  workspaceSkills(
@@ -3411,6 +3520,25 @@ function buildAgentCapabilitiesFromComposition(
3411
3520
  );
3412
3521
  }
3413
3522
  }
3523
+ if (options.authorizeAttemptExecution) {
3524
+ for (const capability of caps) {
3525
+ const target = capability as unknown as { tools(): Tool<unknown>[] };
3526
+ const original = target.tools;
3527
+ target.tools = function () {
3528
+ return original.call(this).map((tool) => {
3529
+ if (tool.type !== "function") return tool;
3530
+ const invoke = tool.invoke;
3531
+ return {
3532
+ ...tool,
3533
+ invoke: async (context, input, details) => {
3534
+ await options.authorizeAttemptExecution!();
3535
+ return invoke(context, input, details);
3536
+ },
3537
+ };
3538
+ });
3539
+ };
3540
+ }
3541
+ }
3414
3542
  return caps;
3415
3543
  }
3416
3544
 
@@ -3542,6 +3670,10 @@ export type PrepareToolsOptions = {
3542
3670
  attemptToolDefinitions?: readonly AttemptToolDefinition[];
3543
3671
  /** Host authorization applied after catalog/input validation and before execution. */
3544
3672
  attemptToolAuthorize?: AttemptToolAuthorization;
3673
+ /** Live accepted-attempt fence, evaluated at execution rather than catalog
3674
+ * preparation. Includes model and Codemode calls and runs before consuming
3675
+ * connector approval authority. Does not alter catalog identity. */
3676
+ authorizeAttemptExecution?: () => Promise<void> | void;
3545
3677
  /** Attempt-bound connector policy installed into the canonical gateway lifecycle. */
3546
3678
  connectorActionPolicy?: ConnectorActionPolicyHooks;
3547
3679
  /** Private connector identities for exact-name attempt-local tools. */
@@ -4387,10 +4519,29 @@ async function prepareAttemptToolEnvironment(
4387
4519
  options.connectorActionPolicy,
4388
4520
  );
4389
4521
  const subjectId = options.subjectId ?? "worker:mcp-model";
4522
+ const guardedDefinitions = options.authorizeAttemptExecution
4523
+ ? definitions.map((definition) => ({
4524
+ ...definition,
4525
+ lifecycle: {
4526
+ prepare: async (
4527
+ input: Parameters<NonNullable<AttemptToolDefinition["lifecycle"]>["prepare"]>[0],
4528
+ ) => {
4529
+ const prior = await definition.lifecycle?.prepare(input);
4530
+ return {
4531
+ begin: async () => {
4532
+ await options.authorizeAttemptExecution!();
4533
+ await prior?.begin?.();
4534
+ },
4535
+ ...(prior?.complete ? { complete: prior.complete } : {}),
4536
+ };
4537
+ },
4538
+ },
4539
+ }))
4540
+ : definitions;
4390
4541
  const environment = createAttemptToolEnvironment({
4391
4542
  scope,
4392
4543
  generation: options.attemptToolCatalogGeneration ?? 1,
4393
- definitions,
4544
+ definitions: guardedDefinitions,
4394
4545
  confirmModelApproval: ({ modelName, subjectId: callerSubjectId }) =>
4395
4546
  callerSubjectId === subjectId &&
4396
4547
  activeModelToolInvocation(modelName)?.approvalConfirmed === true,
@@ -31,9 +31,6 @@ const ALWAYS_VISIBLE_BASE_TOOL_NAMES: ReadonlySet<string> = new Set([
31
31
  "view_image",
32
32
  // A plain function tool on Chat Completions; Responses uses type apply_patch.
33
33
  "apply_patch",
34
- // The SDK skills capability prints the skill index in the instructions and
35
- // mandates this call before any SKILL.md read.
36
- "load_skill",
37
34
  // The server-backed reader needs neither sandbox setup nor tool search.
38
35
  "skill_read",
39
36
  "request_human_input",
@@ -49,7 +49,7 @@ export const stripProviderItemIdsFilter: CallModelInputFilter = ({ modelData })
49
49
  return projected ? { ...modelData, input: projected } : modelData;
50
50
  };
51
51
 
52
- function stripProviderItemId(item: AgentInputItem): AgentInputItem {
52
+ export function stripProviderItemId(item: AgentInputItem): AgentInputItem {
53
53
  if (!item || typeof item !== "object" || !("id" in item)) return item;
54
54
  const { id: _id, ...rest } = item as Record<string, unknown>;
55
55
  return rest as AgentInputItem;
package/src/run-events.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  approvalIdentifier,
5
5
  RequestHumanInteractionToolInput,
6
6
  RequestHumanInputToolInput,
7
+ canonicalSkillReviewQuestion,
7
8
  sessionEventMediaPreview,
8
9
  sessionEventMediaPreviewFromDataUrl,
9
10
  type SessionEventMediaPreview,
@@ -690,6 +691,28 @@ export function serializeHumanInputRequests(
690
691
  throw new Error("Human-input interruption is missing a stable tool-call identity");
691
692
  }
692
693
  const input = RequestHumanInputToolInput.parse(interruptionArguments(item));
694
+ const reviews = input.questions.filter((question) => question.skillReview);
695
+ if (reviews.length > 0) {
696
+ if (input.questions.length !== 1) {
697
+ throw new Error("Skill review requires one dedicated human-input question");
698
+ }
699
+ const question = reviews[0]!;
700
+ const canonical = canonicalSkillReviewQuestion(question);
701
+ // Normalize only known wire differences, never turn misleading text
702
+ // into a trusted review based solely on a model-supplied reference.
703
+ // The DB still binds that reference to the receipt and live human.
704
+ if (!canonical) {
705
+ throw new Error("Skill review must use the exact host-owned confirmation presentation");
706
+ }
707
+ return {
708
+ toolCallId,
709
+ input: {
710
+ ...input,
711
+ questions: [canonical],
712
+ allowSkip: false,
713
+ },
714
+ };
715
+ }
693
716
  return {
694
717
  toolCallId,
695
718
  input: {