@sema-agent/core 7.35.2 → 7.37.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 (85) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/dist/brain/anthropic.js +6 -6
  3. package/dist/brain/errors.d.ts +1 -14
  4. package/dist/brain/errors.js +2 -6
  5. package/dist/brain/open-responses.js +7 -7
  6. package/dist/brain/openai.js +7 -7
  7. package/dist/brain/provider-echo-shape.d.ts +26 -0
  8. package/dist/brain/provider-echo-shape.js +12 -0
  9. package/dist/brain/tool-call-id.d.ts +27 -15
  10. package/dist/brain/tool-call-id.js +43 -0
  11. package/dist/core/ask-origin.d.ts +35 -5
  12. package/dist/core/ask-origin.js +23 -4
  13. package/dist/core/ask-question.d.ts +0 -30
  14. package/dist/core/ask-question.js +3 -2
  15. package/dist/core/checkpoint-store.d.ts +18 -6
  16. package/dist/core/checkpoint-store.js +2 -1
  17. package/dist/core/context-edit.d.ts +4 -0
  18. package/dist/core/context-edit.js +5 -2
  19. package/dist/core/decision-receipt.d.ts +21 -0
  20. package/dist/core/decision-receipt.js +63 -0
  21. package/dist/core/engine-notice.d.ts +15 -5
  22. package/dist/core/gate-fold.js +2 -1
  23. package/dist/core/gate-lanes.js +20 -23
  24. package/dist/core/gate-outcome.d.ts +7 -5
  25. package/dist/core/gate-outcome.js +5 -1
  26. package/dist/core/governance-codes.d.ts +1 -1
  27. package/dist/core/governance-codes.js +2 -0
  28. package/dist/core/hooks.d.ts +14 -2
  29. package/dist/core/hooks.js +7 -3
  30. package/dist/core/mode-policy.js +2 -1
  31. package/dist/core/park-selfcheck.js +2 -0
  32. package/dist/core/permission-rule-model.d.ts +2 -2
  33. package/dist/core/permission-rule-model.js +2 -2
  34. package/dist/core/permission-rules.js +2 -5
  35. package/dist/core/runner/active-skill-scope.js +5 -5
  36. package/dist/core/runner/decide-continuation.js +2 -1
  37. package/dist/core/runner/denial-limit-arms.d.ts +28 -18
  38. package/dist/core/runner/denial-limit-arms.js +11 -7
  39. package/dist/core/runner/inherited-ask-grants.d.ts +32 -31
  40. package/dist/core/runner/inherited-ask-grants.js +5 -0
  41. package/dist/core/runner/permission-rule-lanes.d.ts +3 -0
  42. package/dist/core/runner/permission-rule-lanes.js +7 -6
  43. package/dist/core/runner/prepare-context-lane.d.ts +4 -0
  44. package/dist/core/runner/prepare-context-lane.js +7 -5
  45. package/dist/core/runner/prepare-gate-stations.js +17 -12
  46. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  47. package/dist/core/runner/prepare-park-ask.d.ts +4 -2
  48. package/dist/core/runner/prepare-park-ask.js +8 -11
  49. package/dist/core/runner/prepare-policy-chain.d.ts +5 -8
  50. package/dist/core/runner/prepare-policy-chain.js +50 -31
  51. package/dist/core/runner/prepare-safety-scan.js +9 -2
  52. package/dist/core/runner/prepare-task.js +3 -3
  53. package/dist/core/runner/resume-apply.js +2 -1
  54. package/dist/core/runner/run-attachment-seats.js +3 -1
  55. package/dist/core/runner/run-harness-handlers.js +6 -1
  56. package/dist/core/runner/run-leg.js +11 -9
  57. package/dist/core/runner/run-recovery-lanes.js +2 -0
  58. package/dist/core/runner/session-rule-policy.js +5 -4
  59. package/dist/core/runner/tool-call-id-fold-seat.d.ts +13 -0
  60. package/dist/core/runner/tool-call-id-fold-seat.js +31 -0
  61. package/dist/core/sensitive-path-policy.js +2 -2
  62. package/dist/core/side-query.d.ts +5 -1
  63. package/dist/core/side-query.js +5 -1
  64. package/dist/core/store-contracts/checkpoint-store-contract.js +18 -0
  65. package/dist/core/tool-name-readings.d.ts +370 -0
  66. package/dist/core/tool-name-readings.js +137 -0
  67. package/dist/core/tool-policy.d.ts +79 -89
  68. package/dist/core/tool-policy.js +86 -49
  69. package/dist/core/tool-registry.d.ts +1 -31
  70. package/dist/core/tool-registry.js +2 -22
  71. package/dist/core/tool-result-budget.d.ts +3 -0
  72. package/dist/core/tool-result-budget.js +5 -3
  73. package/dist/core/tool-roster.d.ts +30 -9
  74. package/dist/core/tool-roster.js +45 -5
  75. package/dist/engine/llm/types.d.ts +4 -0
  76. package/dist/index.d.ts +1 -1
  77. package/dist/index.js +1 -1
  78. package/dist/tools/fs/file-read-check.d.ts +1 -1
  79. package/dist/tools/fs/file-read-check.js +4 -3
  80. package/dist/tools/fs/shell-read-check.d.ts +15 -1
  81. package/dist/tools/fs/shell-read-check.js +5 -2
  82. package/package.json +2 -1
  83. package/test/export-surface.snapshot.json +7 -1
  84. package/wire-changes/core@7.36.0.json +288 -0
  85. package/wire-changes/core@7.37.0.json +405 -0
@@ -6,12 +6,12 @@ import { cloneObserverInput, resolveHookTimeoutMs } from "../hooks.js";
6
6
  import { assertHookSeatsCallable } from "../hook-seats.js";
7
7
  import { isNamespacedCoveringRuleName, namespacedRuleNameCovers } from "../permission-rules.js";
8
8
  import { isIsolated } from "../remote-env.js";
9
- import { askApproverIdentity, carryEngineAttestations, carryPrecedingSettlement, checkToolPolicyProjection, combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, durableParkOptInDeclared, isLiveApproverSeat, resolveAsk, toolPolicyNameSets, wrapToolPolicy, attestAskUnresolvableCause } from "../tool-policy.js";
9
+ import { askApproverIdentity, carryEngineAttestations, carryPrecedingSettlement, checkToolPolicyProjection, combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, durableParkOptInDeclared, isLiveApproverSeat, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, wrapToolPolicy, attestAskUnresolvableCause } from "../tool-policy.js";
10
10
  import { RETIRED_TOOL_NAMES } from "../tool-name-aliases.js";
11
11
  import { NAMESPACED_NAME_SHAPES, protocolOf } from "../protocol-table.js";
12
12
  import { emitTrace } from "../trace.js";
13
13
  import { createActiveSkillScopePolicy } from "./active-skill-scope.js";
14
- import { arbitrateByMode, askMandateMember, classifyAsk } from "../ask-origin.js";
14
+ import { arbitrateByMode, askClassMembersOf, askMandateMember } from "../ask-origin.js";
15
15
  import { createModeGatePolicy, mountedToolDeclaresReadOnly } from "../mode-policy.js";
16
16
  const ancestorFacts = (ask) => ({ ...(ask ?? {}), ancestorResolved: true });
17
17
  import { headlessDenyAtFold, headlessDenyAtRecheck, INHERITED_STATION_FACTS, inheritedAskCarry, judgeInheritedClassifier, lateAskSettlementObserver, settleDenialLimitFallback } from "./denial-limit-arms.js";
@@ -20,12 +20,12 @@ import { askGrantShapeOf } from "./inherited-ask-grants.js";
20
20
  import { createPermissionRuleLanes, inheritedAskRuleEvidence, createRuleOffersOf } from "./permission-rule-lanes.js";
21
21
  import { createLiveSessionRules, createSessionRulePolicy } from "./session-rule-policy.js";
22
22
  import { thrownText } from "../thrown-value.js";
23
- import { anyNamed, firstNamed, ruleNamesOf } from "../tool-roster.js";
23
+ import { heldBy, heldValues, seatOf, wireOf } from "../tool-name-readings.js";
24
24
  export async function preparePolicyChain(input) {
25
- const { spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, hasSkillManifest, skillScope, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, permissionMode, scratchpadDir, toolRosterDeltas, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef } = input;
25
+ const { spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, toolMarkSources, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, hasSkillManifest, skillScope, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, permissionMode, scratchpadDir, toolRosterDeltas, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef } = input;
26
26
  const ancestorArbitrations = new Map();
27
- const arbitrateInherited = (ask, toolCallId) => {
28
- const verdict = arbitrateByMode(permissionMode.current, classifyAsk(ask, INHERITED_STATION_FACTS), { ...(ask.origin !== undefined ? { origin: ask.origin } : {}), ...(ask.mandate !== undefined ? { mandate: ask.mandate } : {}) });
27
+ const arbitrateInherited = (ask, toolCallId, classification) => {
28
+ const verdict = arbitrateByMode(permissionMode.current, classification, { ...(ask.origin !== undefined ? { origin: ask.origin } : {}), ...(ask.mandate !== undefined ? { mandate: ask.mandate } : {}) });
29
29
  if (verdict.kind === "ask" || verdict.kind === "accepted")
30
30
  return undefined;
31
31
  ancestorArbitrations.set(toolCallId, verdict);
@@ -188,7 +188,7 @@ export async function preparePolicyChain(input) {
188
188
  }, "mandate_unreconstructible", creq);
189
189
  const markerChannelFullDeny = (creq) => attestAskUnresolvableCause({ action: "deny", message: `no approver is reachable for the inherited approval of "${creq.toolName}" and the marker channel is at capacity — denied fail-closed` }, "approver_unavailable", creq);
190
190
  const inheritedAskGrants = new Map();
191
- const recordInheritedAskGrant = (toolCallId, approver, presented, waitMs, settled) => {
191
+ const recordInheritedAskGrant = (toolCallId, approver, presented, waitMs, settled, classes) => {
192
192
  if (typeof approver !== "function")
193
193
  return;
194
194
  if (presented === undefined)
@@ -198,7 +198,7 @@ export async function preparePolicyChain(input) {
198
198
  const argsJson = askGrantShapeOf(presented);
199
199
  if (argsJson === undefined)
200
200
  return;
201
- inheritedAskGrants.set(toolCallId, { approver, argsJson, presented, waitMs, settled });
201
+ inheritedAskGrants.set(toolCallId, { approver, argsJson, presented, waitMs, settled, classes });
202
202
  };
203
203
  const sandboxDeclared = isIsolated(executionEnv);
204
204
  const sandboxAdmissionArmed = (() => {
@@ -233,7 +233,8 @@ export async function preparePolicyChain(input) {
233
233
  list.push({ cls, layer });
234
234
  foldAskClasses.set(toolCallId, list);
235
235
  };
236
- const sandboxBoundaryCapable = (call) => anyNamed(egressTools, ruleNamesOf(call)) || !envHandToolNames.has(call.toolName);
236
+ const isDelegatedQuestion = (creq) => wireOf("inherited.questionFloat", creq) === ASK_USER_QUESTION_TOOL_NAME;
237
+ const sandboxBoundaryCapable = (call) => heldBy("gate.sandboxBoundary.egress", egressTools, call) || !envHandToolNames.has(wireOf("gate.sandboxBoundary.hand", call));
237
238
  const emitSandboxAdmitted = (info) => {
238
239
  emitTrace(deps.tracer, () => ({
239
240
  kind: "permission.sandbox_admitted",
@@ -258,7 +259,7 @@ export async function preparePolicyChain(input) {
258
259
  };
259
260
  const callerPolicyLayer = policy !== undefined && sameInstanceAncestorCount > 0
260
261
  ? wrapToolPolicy(policy, async (creq, csignal) => {
261
- const decision = await policy.check(creq, csignal ?? abortController.signal);
262
+ const decision = refuseOutOfContractDecision(await policy.check(creq, csignal ?? abortController.signal));
262
263
  sharedFirstDecision.set(creq.toolCallId, decision);
263
264
  if (sharedFirstDecision.size > SHARED_FIRST_DECISION_CAP) {
264
265
  const oldest = sharedFirstDecision.keys().next().value;
@@ -277,16 +278,33 @@ export async function preparePolicyChain(input) {
277
278
  : {}),
278
279
  ...(delegation.isDelegatedChild ? { isDelegatedChild: true } : {}),
279
280
  });
281
+ const seatMarksOf = (call) => {
282
+ const tierSeat = seatOf("report.riskAxes", irreversibilityTier, call);
283
+ return {
284
+ tier: tierSeat?.value,
285
+ egress: heldBy("report.riskAxes.egress", egressTools, call),
286
+ shellGated: tierSeat?.shellGated(shellGatedTools, toolMarkSources) === true,
287
+ sources: new Set(heldValues("report.markSources", toolMarkSources, call).flatMap((s) => [...s])),
288
+ };
289
+ };
280
290
  const riskAxesOf = (call) => {
281
- const names = ruleNamesOf(call);
282
- const tier = firstNamed(irreversibilityTier, names)?.value;
283
- const negatives = firstNamed(axisExplicitNegatives, names)?.value;
284
- const irreversible = tier === "always" || tier === "maybe" ? true : tier === "never" ? false : negatives?.irreversible;
285
- const egress = anyNamed(egressTools, names) ? true : negatives?.egress;
291
+ const seat = seatMarksOf(call);
292
+ const negatives = seatOf("report.riskAxes.negatives", axisExplicitNegatives, call)?.value;
293
+ const irreversible = seat.tier === "always" || seat.tier === "maybe" ? true : seat.tier === "never" ? false : negatives?.irreversible;
294
+ const egress = seat.egress ? true : negatives?.egress;
286
295
  if (irreversible === undefined && egress === undefined)
287
296
  return {};
288
297
  return { riskAxes: { ...(irreversible !== undefined ? { irreversible } : {}), ...(egress !== undefined ? { egress } : {}) } };
289
298
  };
299
+ const inheritedStationFacts = (call) => {
300
+ const seat = seatMarksOf(call);
301
+ return {
302
+ ...INHERITED_STATION_FACTS,
303
+ mandateMarks: { egress: seat.egress, shellGated: seat.shellGated, ...(seat.tier !== undefined ? { irreversibility: seat.tier } : {}) },
304
+ ...(seat.sources.size > 0 ? { toolMarkSources: seat.sources } : {}),
305
+ ...(deps.a2a?.isolatePeers === true ? { a2aIsolatePeers: true } : {}),
306
+ };
307
+ };
290
308
  const inheritedAskEvidence = inheritedAskRuleEvidence(deps.permissionRuleStore);
291
309
  const { personal: permissionRuleLane, org: permissionRuleOrgLane } = createPermissionRuleLanes({
292
310
  provider: deps.permissionRuleStore,
@@ -310,6 +328,7 @@ export async function preparePolicyChain(input) {
310
328
  egressTools,
311
329
  irreversibilityTier,
312
330
  shellGatedTools,
331
+ toolMarkSources,
313
332
  taskRoot: taskRootFinal,
314
333
  cwdRef: handsCwdRef,
315
334
  deniesDirectoryRead: readDenyMatcher !== undefined ? (d) => readDenyMatcher.matchPath(d) !== null : undefined,
@@ -334,7 +353,7 @@ export async function preparePolicyChain(input) {
334
353
  }
335
354
  let re;
336
355
  try {
337
- re = await pol.check({ ...creq, args: editArgs }, csignal ?? abortController.signal);
356
+ re = refuseOutOfContractDecision(await pol.check({ ...creq, args: editArgs }, csignal ?? abortController.signal));
338
357
  }
339
358
  catch (err) {
340
359
  return vetoed({
@@ -352,7 +371,7 @@ export async function preparePolicyChain(input) {
352
371
  }
353
372
  if (re.updatedInput !== undefined)
354
373
  editArgs = re.updatedInput;
355
- const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: ancestorAutoMode, ask: re, req: { ...creq, args: editArgs }, signal: csignal ?? abortController.signal, subject: "the approved edit", allowRewrite: editArgs, unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(ancestorDecider?.timeoutMs !== undefined ? { budgetMs: ancestorDecider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(onAskOf), onNotice: deps.onNotice, sessionId, runId }) });
374
+ const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: ancestorAutoMode, ask: re, req: { ...creq, args: editArgs }, signal: csignal ?? abortController.signal, subject: "the approved edit", allowRewrite: editArgs, facts: inheritedStationFacts(creq), unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(ancestorDecider?.timeoutMs !== undefined ? { budgetMs: ancestorDecider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(onAskOf), onNotice: deps.onNotice, sessionId, runId }) });
356
375
  if (judged.kind === "allow") {
357
376
  const cleared = { ...judged.result };
358
377
  carryEngineAttestations(judged.result, cleared);
@@ -363,7 +382,7 @@ export async function preparePolicyChain(input) {
363
382
  return vetoed(judged.result);
364
383
  const { ask: editAsk, fallback: editFallback, mintedHere: editMintedHere } = judged;
365
384
  re = editAsk;
366
- const editArbitrated = arbitrateInherited(re, creq.toolCallId);
385
+ const editArbitrated = arbitrateInherited(re, creq.toolCallId, judged.classification);
367
386
  if (editArbitrated !== undefined) {
368
387
  const out = editArbitrated.action === "allow" ? { action: "allow", updatedInput: editArgs } : editArbitrated;
369
388
  carryPrecedingSettlement(settledBy, out);
@@ -420,7 +439,7 @@ export async function preparePolicyChain(input) {
420
439
  return first;
421
440
  if (first.action === "allow")
422
441
  return { action: "allow" };
423
- if (creq.toolName === ASK_USER_QUESTION_TOOL_NAME) {
442
+ if (isDelegatedQuestion(creq)) {
424
443
  if (pc.contentMandate !== true || markInheritedUnavailable(creq.toolCallId))
425
444
  return first;
426
445
  return {
@@ -434,17 +453,17 @@ export async function preparePolicyChain(input) {
434
453
  return first;
435
454
  return mandateUnreconstructibleDeny(creq);
436
455
  }
437
- const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: pc.autoMode, ask: first, req: creq, signal: csignal ?? abortController.signal, subject: "this call", unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(pc.autoMode?.decider.timeoutMs !== undefined ? { budgetMs: pc.autoMode.decider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(pc.onAsk), onNotice: deps.onNotice, sessionId, runId }) });
456
+ const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: pc.autoMode, ask: first, req: creq, signal: csignal ?? abortController.signal, subject: "this call", facts: inheritedStationFacts(creq), unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(pc.autoMode?.decider.timeoutMs !== undefined ? { budgetMs: pc.autoMode.decider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(pc.onAsk), onNotice: deps.onNotice, sessionId, runId }) });
438
457
  if (judged.kind === "allow")
439
458
  return judged.result;
440
459
  if (judged.kind === "deny")
441
460
  return judged.result;
442
461
  const { ask: inheritedAsk, fallback, mintedHere } = judged;
443
- if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq) && classifyAsk(inheritedAsk, INHERITED_STATION_FACTS).row.sandboxAdmission) {
462
+ if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq) && judged.classification.row.sandboxAdmission) {
444
463
  recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
445
464
  return { action: "allow" };
446
465
  }
447
- const arbitrated = arbitrateInherited(inheritedAsk, creq.toolCallId);
466
+ const arbitrated = arbitrateInherited(inheritedAsk, creq.toolCallId, judged.classification);
448
467
  if (arbitrated !== undefined)
449
468
  return arbitrated;
450
469
  const presentedArgs = creq.args;
@@ -476,14 +495,14 @@ export async function preparePolicyChain(input) {
476
495
  if (resolved.updatedInput !== undefined) {
477
496
  return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, resolved, csignal, pc.autoMode);
478
497
  }
479
- recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved);
498
+ recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved, askClassMembersOf(judged.classification).classes);
480
499
  return { action: "allow" };
481
500
  },
482
501
  }
483
502
  : wrapToolPolicy(pc.policy, async (creq, csignal) => {
484
503
  let decision;
485
504
  try {
486
- decision = await pc.policy.check(creq, csignal ?? abortController.signal);
505
+ decision = refuseOutOfContractDecision(await pc.policy.check(creq, csignal ?? abortController.signal));
487
506
  }
488
507
  catch (err) {
489
508
  return {
@@ -493,7 +512,7 @@ export async function preparePolicyChain(input) {
493
512
  }
494
513
  if (decision.action !== "ask")
495
514
  return decision;
496
- if (creq.toolName === ASK_USER_QUESTION_TOOL_NAME) {
515
+ if (isDelegatedQuestion(creq)) {
497
516
  if (pc.contentMandate !== true || markInheritedUnavailable(creq.toolCallId))
498
517
  return decision;
499
518
  return {
@@ -508,17 +527,17 @@ export async function preparePolicyChain(input) {
508
527
  return mandateUnreconstructibleDeny(creq);
509
528
  }
510
529
  const presentedArgs = decision.updatedInput !== undefined ? decision.updatedInput : creq.args;
511
- const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: pc.autoMode, ask: decision, req: { ...creq, args: presentedArgs }, signal: csignal ?? abortController.signal, subject: "this call", allowRewrite: decision.updatedInput, unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(pc.autoMode?.decider.timeoutMs !== undefined ? { budgetMs: pc.autoMode.decider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(pc.onAsk), onNotice: deps.onNotice, sessionId, runId }) });
530
+ const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: pc.autoMode, ask: decision, req: { ...creq, args: presentedArgs }, signal: csignal ?? abortController.signal, subject: "this call", allowRewrite: decision.updatedInput, facts: inheritedStationFacts(creq), unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(pc.autoMode?.decider.timeoutMs !== undefined ? { budgetMs: pc.autoMode.decider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(pc.onAsk), onNotice: deps.onNotice, sessionId, runId }) });
512
531
  if (judged.kind === "allow")
513
532
  return judged.result;
514
533
  if (judged.kind === "deny")
515
534
  return judged.result;
516
535
  const { ask: inheritedAsk, fallback, mintedHere } = judged;
517
- if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq) && classifyAsk(inheritedAsk, INHERITED_STATION_FACTS).row.sandboxAdmission) {
536
+ if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq) && judged.classification.row.sandboxAdmission) {
518
537
  recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
519
538
  return decision.updatedInput !== undefined ? { action: "allow", updatedInput: decision.updatedInput } : { action: "allow" };
520
539
  }
521
- const arbitrated = arbitrateInherited(inheritedAsk.action === "ask" ? { ...inheritedAsk, ...(decision.updatedInput !== undefined ? { updatedInput: decision.updatedInput } : {}) } : inheritedAsk, creq.toolCallId);
540
+ const arbitrated = arbitrateInherited(inheritedAsk.action === "ask" ? { ...inheritedAsk, ...(decision.updatedInput !== undefined ? { updatedInput: decision.updatedInput } : {}) } : inheritedAsk, creq.toolCallId, judged.classification);
522
541
  if (arbitrated !== undefined)
523
542
  return arbitrated;
524
543
  const askT0 = now();
@@ -550,7 +569,7 @@ export async function preparePolicyChain(input) {
550
569
  return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, resolved, csignal, pc.autoMode);
551
570
  }
552
571
  if (pc.preToolUse === undefined) {
553
- recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved);
572
+ recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved, askClassMembersOf(judged.classification).classes);
554
573
  }
555
574
  if (decision.updatedInput === undefined)
556
575
  return resolved;
@@ -571,7 +590,7 @@ export async function preparePolicyChain(input) {
571
590
  sameInstanceAdapterIndices.add(j + callerSlotOffset);
572
591
  });
573
592
  const instrumentedChain = rewriteCapableChain.map((layer, idx) => wrapToolPolicy(layer, async (creq, csignal) => {
574
- const d = await layer.check(creq, csignal);
593
+ const d = refuseOutOfContractDecision(await layer.check(creq, csignal));
575
594
  if (d.updatedInput !== undefined)
576
595
  rewriteEmitterIndex.set(creq.toolCallId, idx);
577
596
  if (d.action === "ask") {
@@ -618,7 +637,7 @@ export async function preparePolicyChain(input) {
618
637
  const revalidated = {};
619
638
  try {
620
639
  for (const layer of recheckLayers) {
621
- const d = await layer.check(finalReq, signal);
640
+ const d = refuseOutOfContractDecision(await layer.check(finalReq, signal));
622
641
  if (d.action === "deny") {
623
642
  const carried = { ...d, updatedInput: decision.updatedInput };
624
643
  carryEngineAttestations(d, carried);
@@ -4,6 +4,8 @@ import { callerCardIdOf, offendingResultCard, rosterMemberBoundProblem, schemaKe
4
4
  import { toolFaceProblem } from "./tool-face-overlay.js";
5
5
  import { OFFLOAD_TOOL_NAME } from "../tool-result-store.js";
6
6
  import { PRESENT_PLAN_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME } from "../present-plan-tool.js";
7
+ const REPEAT_RISK = { read: 0, idempotent: 1, write: 2 };
8
+ const leastRepeatSafe = (a, b) => (REPEAT_RISK[a] >= REPEAT_RISK[b] ? a : b);
7
9
  export function prepareSafetyScan(input) {
8
10
  const { spec, deps } = input;
9
11
  const toolEffects = new Map();
@@ -120,11 +122,16 @@ export function prepareSafetyScan(input) {
120
122
  }
121
123
  }
122
124
  toolEffects.set(OFFLOAD_TOOL_NAME, "read");
125
+ const claimerEffects = new Map([...claimedAliases].map((a) => [a, toolEffects.get(a) ?? "write"]));
126
+ const recordHandEffect = (name, effect) => {
127
+ const claimed = claimerEffects.get(name);
128
+ toolEffects.set(name, claimed === undefined ? effect : leastRepeatSafe(claimed, effect));
129
+ };
123
130
  if (deps.executionEnvFactory || deps.executionEnv) {
124
131
  for (const [name, effect] of handBandEffects())
125
- toolEffects.set(name, effect);
132
+ recordHandEffect(name, effect);
126
133
  if (spec.handsReadOnly === true)
127
- toolEffects.set("Bash", "read");
134
+ recordHandEffect("Bash", "read");
128
135
  }
129
136
  if (spec.enablePlanMode === true) {
130
137
  toolEffects.set(PRESENT_PLAN_TOOL_NAME, "read");
@@ -740,7 +740,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
740
740
  releaseSignal = () => sig.removeEventListener("abort", onExternalAbort);
741
741
  }
742
742
  }
743
- const { hooks, hookTimeoutMs, preToolUseObservational, ownGatePreToolUse, basePolicyForResumeEdit, denyNarrowingPolicy, policyLayers, effectivePolicy, inheritedUnavailableAsks, inheritedAskGrants, askSourceIdentity, riskAxesOf, ruleOffersOf, permissionRuleLane, permissionRuleOrgLane, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, foldAskClasses, ancestorSandboxAdmissions, ancestorArbitrations } = await preparePolicyChain({ spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, hasSkillManifest, skillScope, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider: () => autoModeDeciderRef.current, permissionMode, scratchpadDir: spec.envFacts?.scratchpadDir, toolRosterDeltas: rosterSeat, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef });
743
+ const { hooks, hookTimeoutMs, preToolUseObservational, ownGatePreToolUse, basePolicyForResumeEdit, denyNarrowingPolicy, policyLayers, effectivePolicy, inheritedUnavailableAsks, inheritedAskGrants, askSourceIdentity, riskAxesOf, ruleOffersOf, permissionRuleLane, permissionRuleOrgLane, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, foldAskClasses, ancestorSandboxAdmissions, ancestorArbitrations } = await preparePolicyChain({ spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, toolMarkSources, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, hasSkillManifest, skillScope, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider: () => autoModeDeciderRef.current, permissionMode, scratchpadDir: spec.envFacts?.scratchpadDir, toolRosterDeltas: rosterSeat, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef });
744
744
  const { onAsk, preToolContexts, blockedToolCalls, gateOutcomes, batchHaltRef, blockedTracked, incompleteSuspendAdapter, durableSuspendInfraReady, resourceSuspendEligible, usageGovernance, durableApproval, wiringManifest, writeProtection, parkLaneArmed, gateMachineryActive, hookIdentity, hookEnvFace, sessionBoxBound } = prepareWiringManifest({ spec, deps, internals, resume, sessionId, runId, hostTaskId, taskScope, hooks, preToolUseObservational, policyLayers, effectivePolicy, handsEnabled, taskRootFinal, mcp, irreversibleTools, egressTools, shellReadStationArmed: handsReadFace.shellReadStationArmed, toolEffects, ownedEnv, executionEnv, offloadStore, onceLedger, usageWindows, envLifetimeSuspendAt, runtimeCaps, resolvedInteractionPosture, durableQuestionFace, liveQuestionFace, checkpointStore, sessions, modelGateManifest, autoModeArmReason, permissionMode, lockedPreflight, peerLaneRefs, frameSettlement, harness, pausedRef, frozenOnAsk, liveInheritedGate, delegation, toolRoster, readDenyBuiltinTiersActive: handsReadFace.readDenyBuiltinTiersActive, lspManagerWired, lspDiagnosticsLive: lspDiagnostics !== undefined, fileHistoryFact, sessionBox });
745
745
  const writeProtectionCheck = writeProtection?.check;
746
746
  writeProtectionJudgeRef.current = writeProtection?.judgeTarget;
@@ -753,10 +753,10 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
753
753
  const { askLane } = prepareAskLane({ gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, advertisedWritableDirs, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking: autoModeDenialTrackingRef, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash, grantReadRoot: handsReadFace.grantReadRoot });
754
754
  const { saga } = prepareSuspendSaga({ gateMachineryActive, abortController, harness, checkpointStore, deps, sessionId, hostTaskId, taskScope, liveSpendRef, resume, nestedStats, internals, activeTools, outputRef, readFileStateForCheckpoint, faceCheckpointSection, writeFaceCheckpointSection, reminderMark, handsCwdRef, worktreeSessionRef, undeliveredContinuationRef, inheritedParentConstraints, seedInheritedGate, ancestorRulesNow, inheritedShellGate, permissionMode, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter });
755
755
  const { suspendForResource, suspendForPlatformLimit, suspendForReview } = prepareBoundaryParks({ saga, spec, checkpointStore, abortController, harness, session, sessions, sessionId, deps, priorLedger, maxSlices, maxSuspends, resourceTotal, priorSuspendCount, suspendChainBase, humanReviewRef, liveSpendRef, now, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, pausedRef, remoteEnvFailures, resourceSuspendEligible, durableSuspendInfraReady, incompleteSuspendAdapter });
756
- const { parkAsk } = prepareParkAsk({ askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas: rosterSeat, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, irreversibilityTier, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef });
756
+ const { parkAsk } = prepareParkAsk({ askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas: rosterSeat, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, irreversibilityTier, toolMarkSources, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef });
757
757
  prepareGateStations({ askLane, parkAsk, tools, toolRosterDeltas: rosterSeat, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, readDenyInForce: () => ({ tiers: handsReadFace.readDenyBuiltinTiersActive, rows: [...readDenyAdditionsNormalized.map((e) => e.pattern), ...handsReadFaceInput.pathDenyRules.filter((r) => r.faces.includes("read")).map((r) => r.rule)] }), hookEnvFace, writeProtectionCheck, shellReadCheck, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider: autoModeDeciderRef, autoModeDenialTracking: autoModeDenialTrackingRef, askRoute: wiringManifest.ask, permissionMode, toolMarkSources, ...(deps.a2a?.isolatePeers === true ? { a2aIsolatePeers: true } : {}), ancestorArbitrations, runId, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash, frameSettlement });
758
758
  const postureFrameRef = {};
759
- const { microCompact, charsPerToken } = prepareContextLane({ model, deps, sessionId, runId, hostTaskId, offloadStore, microCompactKnob, harness, batchHaltRef, gitStatusRef, postureFrameRef, requestLossyRef, trimPressureRef, contextOverlay: legContextOverlay, ...(readFileStateForCheckpoint !== undefined ? { readFileState: readFileStateForCheckpoint } : {}) });
759
+ const { microCompact, charsPerToken } = prepareContextLane({ model, deps, sessionId, runId, hostTaskId, offloadStore, toolRosterDeltas: rosterSeat, microCompactKnob, harness, batchHaltRef, gitStatusRef, postureFrameRef, requestLossyRef, trimPressureRef, contextOverlay: legContextOverlay, ...(readFileStateForCheckpoint !== undefined ? { readFileState: readFileStateForCheckpoint } : {}) });
760
760
  const preparedHolder = {};
761
761
  const { cacheBreakDetector, cacheFingerprint, promptOverheadTokens, readTaskFile, normalizeAttachmentPath, isDedupStubResult, recentlyReadFiles, onCompactionApplied, detectExternalChanges, listBackgroundTasks, centerCompactionCandidate, effectiveReadFaceObserved, effectiveWriteFaceObserved, effectiveReadDenyObserved } = prepareTurnWiring({ deps, systemPromptSeat, harnessTools, fpRef, model, epochArtifactDigestForSnapshot, promptProfile, fableMitigations, systemBlocks, thinking, spec, sessionId, turnSnapshotRef, promptManifest, charsPerToken, handsEnabled, executionEnv, attachmentRootCanonical, additionalRootsCanonical, additionalReadRootsCanonical, readDenyMatcher, resolvedReadFace, handsCwdRef, readFileStateForCheckpoint, denyNarrowingPolicy, abortController, hostTaskId, taskScope, buildAssembleInputs, centerAdoptionRef, harnessRef, epochDeclaredSections, carrierReadFace, carrierWriteFace, readDenyAdditionsNormalized, preparedHolder });
762
762
  await onceLedger.settle(session, announcedListingsRef);
@@ -10,6 +10,7 @@ import { inlineUntrusted } from "../untrusted-text.js";
10
10
  import { deepJsonEqual, describeSuppliedValue } from "./decide-continuation.js";
11
11
  import { mintGateOutcome } from "./gate-exit.js";
12
12
  import { asThrownError, thrownText } from "../thrown-value.js";
13
+ import { wireOf } from "../tool-name-readings.js";
13
14
  export async function resumeApply(input) {
14
15
  const { cp, config, internals, outcomeGate, wakeMessage, resumeSignal, recheckGovernanceWindow, plainReviewOutcome, plainParkOutcome, runner, effectiveDelegationFacts } = input;
15
16
  let plainPolicyOutcome = input.plainPolicyOutcome;
@@ -78,7 +79,7 @@ export async function resumeApply(input) {
78
79
  plainPolicyOutcome.decision === "allow" &&
79
80
  plainPolicyOutcome.updatedInput !== undefined &&
80
81
  cp.pendingAction.kind === "tool_approval" &&
81
- cp.pendingAction.toolName !== ASK_USER_QUESTION_TOOL_NAME) {
82
+ wireOf("resume.editRecheckQuestionSkip", cp.pendingAction) !== ASK_USER_QUESTION_TOOL_NAME) {
82
83
  const editedArgs = plainPolicyOutcome.updatedInput;
83
84
  const editBudget = {
84
85
  resourceRemainingMicroUsd: remainingBudgetMicroUsd(cp.resourceLedger),
@@ -2,6 +2,8 @@ import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
2
2
  import { stripGitStatusUnits } from "./git-status-frame.js";
3
3
  import { SKILLS_LISTING_PROBE_HEADER } from "./synthetic-tools.js";
4
4
  import { AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, agentListingDeltaHeader, agentListingInitialHeader, createAttachmentState, replayAnnouncedListing, replayAnnouncedModels } from "./turn-attachments.js";
5
+ import { wireOf } from "../tool-name-readings.js";
6
+ import { rosterToolName } from "../tool-roster.js";
5
7
  export async function runAttachmentSeats(input) {
6
8
  const { spec, prepared, resume, rs, runner, next } = input;
7
9
  rs.counters.wroteThisRun = false;
@@ -174,7 +176,7 @@ export async function runAttachmentSeats(input) {
174
176
  if ((fvMarked || text.includes("<system-reminder>[final verification]")) && rs.counters.finalVerifyInjections < 2)
175
177
  rs.counters.finalVerifyInjections += 1;
176
178
  }
177
- else if (m.role === "toolResult" && !rs.counters.wroteThisRun && (prepared.toolEffects.get(m.toolName) ?? "write") !== "read") {
179
+ else if (m.role === "toolResult" && !rs.counters.wroteThisRun && (prepared.toolEffects.get(wireOf("verify.wroteThisRunSeed", { toolName: rosterToolName(prepared.toolRosterDeltas.current, m.toolName) })) ?? "write") !== "read") {
178
180
  rs.counters.wroteThisRun = true;
179
181
  }
180
182
  }
@@ -11,9 +11,13 @@ import { gatedCallIdOf } from "./park-commit.js";
11
11
  import { hasVerifiableStructureSignal } from "./grounding-signal.js";
12
12
  import { batchResponseDigest, toolEndBodyFrom } from "./tool-end-body.js";
13
13
  import { createResponseModelSeat } from "./response-model-seat.js";
14
+ import { createToolCallIdFoldSeat } from "./tool-call-id-fold-seat.js";
15
+ import { wireOf } from "../tool-name-readings.js";
16
+ import { rosterToolName } from "../tool-roster.js";
14
17
  export function createHarnessHandlers(input) {
15
18
  const { prepared, stats, rs, deps } = input;
16
19
  const responseModelSeat = createResponseModelSeat(deps.runnerHooks.onNotice, { sessionId: prepared.sessionId, runId: prepared.runId });
20
+ const toolCallIdFoldSeat = createToolCallIdFoldSeat(deps.runnerHooks.onNotice, { sessionId: prepared.sessionId, runId: prepared.runId });
17
21
  const { spec, queue, internals, ident, parentToolCallId, subagentName, pushContent, emitCommitted, startedToolCallIds, toolStartAt, toolProgress, writeFamilyOf, toolLabels, postToolBatchHook, batchArgs, internalsNotifier } = deps;
18
22
  let lastWorkspaceCwd = prepared.cwdRef?.current;
19
23
  const announceWorkspaceMove = () => {
@@ -147,6 +151,7 @@ export function createHarnessHandlers(input) {
147
151
  }
148
152
  if (m.role === "assistant") {
149
153
  rs.turn.turnResponseModel = responseModelSeat.observe(m);
154
+ toolCallIdFoldSeat.observe(m);
150
155
  }
151
156
  if (m.role === "assistant") {
152
157
  const rep = m.repetition;
@@ -219,7 +224,7 @@ export function createHarnessHandlers(input) {
219
224
  toolStartAt.delete(event.toolCallId);
220
225
  toolProgress.noteEnd(event.toolCallId);
221
226
  const toolNow = Date.now();
222
- if (!rs.counters.wroteThisRun && (prepared.toolEffects.get(event.toolName) ?? "write") !== "read") {
227
+ if (!rs.counters.wroteThisRun && (prepared.toolEffects.get(wireOf("verify.wroteThisRun", { toolName: rosterToolName(prepared.toolRosterDeltas.current, event.toolName) })) ?? "write") !== "read") {
223
228
  rs.counters.wroteThisRun = true;
224
229
  }
225
230
  if (rs.attach.attachState !== undefined && !event.isError) {
@@ -13,7 +13,7 @@ import { composeApprovalPostureDirective } from "../auto-mode-directive.js";
13
13
  import { mintSystemReminder, statedReminderBodiesOf } from "../reminder-mint.js";
14
14
  import { hasDestroy } from "../remote-env.js";
15
15
  import { advertisedWritableRootsSeat, policyRefusalMessage, refuseOutOfContractDecision } from "../tool-policy.js";
16
- import { callFaceSeat, judgeParkedToolIdentity, rosterEntryNamed, toolCallFaceOf } from "../tool-roster.js";
16
+ import { callFaceSeat, judgeParkedToolIdentity, parkedRowLiveFace, rosterEntryNamed } from "../tool-roster.js";
17
17
  import { deliverEngineNotice } from "../types.js";
18
18
  import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText } from "../untrusted-text.js";
19
19
  import { errorCodeOf } from "./assemble-result.js";
@@ -34,6 +34,7 @@ import { attachmentEnvelopeTags, collectDateChange, commitAgentListing, commitSk
34
34
  import { flushOwedAnnouncedSnapshot } from "./announce-once-ledger.js";
35
35
  import { asThrownError, thrownMarksOf, thrownText } from "../thrown-value.js";
36
36
  import { ruleSourcePhrase } from "../rule-denial-sentence.js";
37
+ import { wireOf } from "../tool-name-readings.js";
37
38
  export async function runLeg(input) {
38
39
  const { spec, queue, prepared, resume, rs, isDelegatedChild, entryActor, effectiveTimeoutMs, timeout, pushContent, ident, emitCommitted, writeFamilyOf, startedToolCallIds, announceWorkspaceMove, withBrainSinks, flushGitMirror, loopLatch, notificationLane, unsubscribeTaskNotifications, unsubGitRetry, unsubBoundary, unsub, runner, next } = input;
39
40
  let final;
@@ -563,12 +564,13 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
563
564
  if (decidedGate === undefined) {
564
565
  throw new CheckpointError("checkpoint.invalid_outcome", "pending action reached the resolver with no settlement record on its decision (gateOutcome) — refusing to execute or settle a decision that carries no record");
565
566
  }
566
- const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput !== undefined ? outcome.updatedInput : pendingAction.args);
567
+ const resolvedArgs = wireOf("resume.questionArgsShown", pendingAction) === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput !== undefined ? outcome.updatedInput : pendingAction.args);
567
568
  const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
568
- emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
569
+ const spelledName = pendingAction.names?.[0] ?? pendingAction.toolName;
570
+ emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: spelledName, ...pendingLabel, args: resolvedArgs });
569
571
  const settleEnd = async (isError, result, gate = decidedGate) => {
570
572
  prepared.toolProgressRef.current?.noteEnd(pendingAction.toolCallId);
571
- emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, prepared.structuredProjector, gate) });
573
+ emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: spelledName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, prepared.structuredProjector, gate) });
572
574
  const word = await resume.recordExecutionOutcome(gate);
573
575
  settleExecutionRecord(runner.deps.onNotice, word, { toolName: pendingAction.toolName, gate, sessionId: prepared.sessionId, runId: prepared.runId, scope: resume.cp.scope, ...(resume.cp.checkpointId !== undefined ? { checkpointId: resume.cp.checkpointId } : {}) });
574
576
  };
@@ -611,7 +613,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
611
613
  budget: beltBudget,
612
614
  ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
613
615
  ...advertisedWritableRootsSeat(prepared.advertisedWritableDirs),
614
- ...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)),
616
+ ...callFaceSeat(parkedRowLiveFace(prepared.toolRosterDeltas.current, pendingAction)),
615
617
  }, prepared.abortController.signal));
616
618
  const beltUnsafe = rechecked.action === "deny" ||
617
619
  (rechecked.updatedInput !== undefined && !deepJsonEqual(rechecked.updatedInput, resolvedArgs));
@@ -634,7 +636,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
634
636
  }
635
637
  if (prepared.permissionRuleOrg !== undefined) {
636
638
  const orgVerdict = prepared.permissionRuleOrg
637
- .adjudicate({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId, ...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)) })
639
+ .adjudicate({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId, ...callFaceSeat(parkedRowLiveFace(prepared.toolRosterDeltas.current, pendingAction)) })
638
640
  .catch(() => ({ status: "unavailable", disclosures: ["the org adjudication face threw on resume"] }));
639
641
  let orgWaitCancelled = false;
640
642
  const org = await settleOrgVerdictWithin(orgVerdict, { status: "unavailable", disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`] }, {
@@ -669,7 +671,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
669
671
  toolName: pendingAction.toolName,
670
672
  args: resolvedArgs,
671
673
  toolCallId: pendingAction.toolCallId,
672
- ...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)),
674
+ ...callFaceSeat(parkedRowLiveFace(prepared.toolRosterDeltas.current, pendingAction)),
673
675
  budget: beltBudget,
674
676
  ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
675
677
  ...advertisedWritableRootsSeat(prepared.advertisedWritableDirs),
@@ -691,7 +693,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
691
693
  toolName: pendingAction.toolName,
692
694
  args: resolvedArgs,
693
695
  toolCallId: pendingAction.toolCallId,
694
- ...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)),
696
+ ...callFaceSeat(parkedRowLiveFace(prepared.toolRosterDeltas.current, pendingAction)),
695
697
  budget: beltBudget,
696
698
  ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
697
699
  ...advertisedWritableRootsSeat(prepared.advertisedWritableDirs),
@@ -717,7 +719,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
717
719
  }
718
720
  if (prepared.shellReadCheck !== undefined) {
719
721
  const edited = outcome.updatedInput !== undefined && !deepJsonEqual(outcome.updatedInput, pendingAction.args);
720
- const readCall = { toolName: pendingAction.toolName, ...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)) };
722
+ const readCall = { toolName: pendingAction.toolName, ...callFaceSeat(parkedRowLiveFace(prepared.toolRosterDeltas.current, pendingAction)) };
721
723
  const judged = prepared.shellReadCheck.judge(readCall, resolvedArgs, prepared.cwdRef?.current, prepared.abortController.signal, {
722
724
  toolCallId: pendingAction.toolCallId,
723
725
  ...(edited ? { seat: "rejudge" } : {}),
@@ -1,5 +1,6 @@
1
1
  import { compactionKindOf } from "../task-notification.js";
2
2
  import { planRejectionClears } from "../context-edit.js";
3
+ import { rosterToolName } from "../tool-roster.js";
3
4
  import { landStaleResultClears } from "./prepare-context-lane.js";
4
5
  import { DEFAULT_COMPACTION_INSTRUCTIONS, maybeCompact, mintCompactedEvent } from "../auto-compaction.js";
5
6
  import { emitTrace } from "../trace.js";
@@ -122,6 +123,7 @@ export function runRecoveryLanes(input) {
122
123
  ? { declined: "no_candidates" }
123
124
  : planRejectionClears(proj.messages, {
124
125
  keyOf: proj.keyOf,
126
+ toolNameOf: (recorded) => rosterToolName(prepared.toolRosterDeltas.current, recorded),
125
127
  ...(prepared.microCompact.offloadPersist ? { offload: { persist: prepared.microCompact.offloadPersist } } : {}),
126
128
  });
127
129
  if ("declined" in plan) {
@@ -2,7 +2,7 @@ import { canonicalizeTarget, isWinFormPath, pathFamilyOf, writeTargetPath } from
2
2
  import { createCoarseCommandNamePolicy, namespacedCoveringEntries, namespacedCoveringHit } from "../tool-policy.js";
3
3
  import { continuesLineage } from "../session-policy-store.js";
4
4
  import { thrownText } from "../thrown-value.js";
5
- import { toolNameSetHas } from "../tool-roster.js";
5
+ import { heldBy, wireOf } from "../tool-name-readings.js";
6
6
  import { ruleDenialSentence } from "../rule-denial-sentence.js";
7
7
  import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, gateWaitFailureOf, withGateTimeout } from "../gate-bounded-await.js";
8
8
  import { declaredPathTargetOf, isProtectedWrite, pathConfinableWriteToolNames, skillScopeWriteToolNames } from "../tool-registry.js";
@@ -137,9 +137,10 @@ export function createSessionRulePolicy(rules, opts) {
137
137
  async check(req, signal) {
138
138
  const { toolDeny, toolAllow, toolDenyCovering, toolAllowCovering, cmdPolicy, allowDirs } = compile(await readRules(req.toolCallId, signal));
139
139
  const toolName = req.toolName;
140
- if (toolNameSetHas(toolDeny, req) || namespacedCoveringHit(toolDenyCovering, toolName))
140
+ const listed = (set) => heldBy("policy.sessionRules", set, req);
141
+ if (listed(toolDeny) || namespacedCoveringHit(toolDenyCovering, toolName))
141
142
  return deny(req.toolName);
142
- if (toolAllow && !toolNameSetHas(toolAllow, req) && !namespacedCoveringHit(toolAllowCovering, toolName)) {
143
+ if (toolAllow && !listed(toolAllow) && !namespacedCoveringHit(toolAllowCovering, toolName)) {
143
144
  return deny(req.toolName, "it is not in the session-rule allowlist");
144
145
  }
145
146
  if (cmdPolicy) {
@@ -151,7 +152,7 @@ export function createSessionRulePolicy(rules, opts) {
151
152
  }
152
153
  if (allowDirs) {
153
154
  if (!isProtectedWrite(req)) {
154
- const eff = toolEffects?.get(toolName) ?? "write";
155
+ const eff = toolEffects?.get(wireOf("policy.sessionRules.effect", req)) ?? "write";
155
156
  if (eff !== "read") {
156
157
  return deny(req.toolName, "it is write-capable and cannot be path-confined, while the rule confines writes to allowDirs");
157
158
  }
@@ -0,0 +1,13 @@
1
+ import type { EngineNotice } from "../engine-notice.js";
2
+ export interface ToolCallIdFoldSeat {
3
+ /** Observe one assistant message; announces the run's first folded call. */
4
+ observe(message: {
5
+ readonly content?: unknown;
6
+ }): void;
7
+ }
8
+ /** The notice's attribution ids — the same pair, for the same reason, as {@link ResponseModelSeatIds}. */
9
+ export interface ToolCallIdFoldSeatIds {
10
+ sessionId: string;
11
+ runId: string;
12
+ }
13
+ export declare function createToolCallIdFoldSeat(onNotice: ((notice: EngineNotice) => void) | undefined, ids: ToolCallIdFoldSeatIds): ToolCallIdFoldSeat;
@@ -0,0 +1,31 @@
1
+ import { deliverEngineNotice } from "../engine-notice.js";
2
+ import { providerToolCallId, toolCallIdFoldReason } from "../../brain/tool-call-id.js";
3
+ function isCall(block) {
4
+ return typeof block === "object" && block !== null && block.type === "toolCall" && typeof block.id === "string";
5
+ }
6
+ export function createToolCallIdFoldSeat(onNotice, ids) {
7
+ let announced = false;
8
+ return {
9
+ observe(message) {
10
+ if (announced || !Array.isArray(message.content))
11
+ return;
12
+ for (const call of message.content.filter(isCall)) {
13
+ const wireId = providerToolCallId(call);
14
+ const reason = wireId === call.id ? undefined : toolCallIdFoldReason(wireId);
15
+ if (reason === undefined)
16
+ continue;
17
+ announced = true;
18
+ deliverEngineNotice(onNotice, {
19
+ code: "route.tool_call_id_folded",
20
+ message: `The provider named a tool call with an id outside the shape the engine keys calls under ` +
21
+ `(${reason === "length" ? `${wireId.length} characters, over 128` : "a control byte, a space, a non-ASCII character or a `~`"}). ` +
22
+ `The engine keys it as "${call.id}" on every face — frames, cards, transcript, park rows — and hands the ` +
23
+ `provider its own id back for the call and its result, so the run is unchanged. Once per run; later calls ` +
24
+ `with such ids are folded the same way without another notice.`,
25
+ detail: { toolCallId: call.id, rawLength: wireId.length, reason, sessionId: ids.sessionId, runId: ids.runId },
26
+ });
27
+ return;
28
+ }
29
+ },
30
+ };
31
+ }
@@ -5,7 +5,7 @@ import { compileSegmentPattern, matchSegmentPatterns, READ_FACE_BUILTIN_DENY_TAB
5
5
  import { isProtectedWrite, protectivePathTargetOf } from "./tool-registry.js";
6
6
  import { pathTargetBaseOf } from "./effective-path-target.js";
7
7
  import { symlinkChainHops } from "./path-identity.js";
8
- import { toolNameSetHas } from "./tool-roster.js";
8
+ import { heldBy } from "./tool-name-readings.js";
9
9
  export function agentConfigDenyPatterns() {
10
10
  return READ_FACE_BUILTIN_DENY_TABLE.filter((row) => row.tier === "agent-config").map((row) => readDenyPatternRun(row.pattern).run);
11
11
  }
@@ -147,7 +147,7 @@ export function createSensitivePathPolicy(opts) {
147
147
  async check(req, signal) {
148
148
  if (compiled.length === 0)
149
149
  return { action: "allow" };
150
- if (guardedByName !== undefined ? !toolNameSetHas(guardedByName, req) : !isProtectedWrite(req))
150
+ if (guardedByName !== undefined ? !heldBy("policy.sensitivePathGuardedByName", guardedByName, req) : !isProtectedWrite(req))
151
151
  return { action: "allow" };
152
152
  const protective = protectivePathTargetOf(req);
153
153
  const path = writeTargetPath(req, protective);
@@ -76,9 +76,13 @@ export interface SideQuerySpec {
76
76
  export interface SideQueryResult {
77
77
  /** Concatenated assistant text (may be empty when the reply is tool-calls-only). */
78
78
  text: string;
79
- /** Tool calls the model made against the DEFINITIONS (never executed by the engine). */
79
+ /** Tool calls the model made against the DEFINITIONS (never executed by the engine). `id` is the engine id;
80
+ * `wireId` is the provider's own spelling, present only when the two differ (`ToolCall.wireId`, the same
81
+ * source). A caller replaying a call carries both on its `toolCall` block and keys the result by `id`; one that
82
+ * talks to the provider itself hands it `wireId ?? id`. */
80
83
  toolCalls: Array<{
81
84
  id: string;
85
+ wireId?: string;
82
86
  name: string;
83
87
  args: unknown;
84
88
  }>;
@@ -2,6 +2,7 @@ import { resolveTaskModel } from "./roles.js";
2
2
  import { readDegradation } from "../brain/degrading.js";
3
3
  import { cacheFamilyOf, uncachedInputTokensOf } from "./runner/usage-accounting.js";
4
4
  import { providerReportedModel } from "./message-utils.js";
5
+ import { providerToolCallId } from "../brain/tool-call-id.js";
5
6
  function normalizeSideQueryUsage(model, u) {
6
7
  const input = uncachedInputTokensOf(cacheFamilyOf(model), u);
7
8
  return input === u.input ? u : { ...u, input };
@@ -61,7 +62,10 @@ export async function runSideQuery(spec, deps) {
61
62
  .join("");
62
63
  const toolCalls = msg.content
63
64
  .filter((c) => c.type === "toolCall")
64
- .map((c) => ({ id: c.id, name: c.name, args: c.arguments }));
65
+ .map((c) => {
66
+ const wireId = providerToolCallId(c);
67
+ return { id: c.id, ...(wireId !== c.id ? { wireId } : {}), name: c.name, args: c.arguments };
68
+ });
65
69
  const degraded = readDegradation(msg);
66
70
  const servedModel = typeof msg.model === "string" && msg.model.length > 0 ? msg.model : resolved.model.id;
67
71
  const reportedModel = providerReportedModel(msg);