@sema-agent/core 5.18.0 → 5.19.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.
@@ -20,7 +20,7 @@ import { createSubagentWorktreeHelper, forkGovernanceDenial } from "../../agents
20
20
  import { createAgentTranscriptTool, AGENT_TRANSCRIPT_TOOL_NAME } from "../../agents/agent-transcript-tool.js";
21
21
  import { createSendMessageTool, SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
22
22
  import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
23
- import { askApproverIdentity, combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
23
+ import { askApproverIdentity, combinePolicies, isApprovalSettledBy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
24
24
  const PERSISTED_RULE_TOOL = "Bash";
25
25
  import { findAdmittingRule, suggestRulesForCommand } from "../permission-rule-model.js";
26
26
  import { ActiveSkillScope, createActiveSkillScopePolicy } from "./active-skill-scope.js";
@@ -28,7 +28,7 @@ import { CHANGED_FILES_MTIME_EPS_MS, fenceMcpServerInstructions, renderAgentList
28
28
  import { inlineUntrusted } from "../untrusted-text.js";
29
29
  import { emitTrace } from "../trace.js";
30
30
  import { createSessionRulePolicy } from "./session-rule-policy.js";
31
- import { cloneObserverInput, createHookEnvCapabilities, formatHookFeedback, runToolGate } from "../hooks.js";
31
+ import { cloneObserverInput, createHookEnvCapabilities, createPreToolUseConstraintPolicy, formatHookFeedback, runToolGate } from "../hooks.js";
32
32
  import { reconcileInterruptedSession } from "../session-reconcile.js";
33
33
  import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-detector.js";
34
34
  import { reservedCollisions, reservedFor } from "../../brain/request-params.js";
@@ -1057,6 +1057,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1057
1057
  const ownOrgVerdictRef = { current: undefined };
1058
1058
  const orgAdmissionCheckpointState = () => inheritedAdmittedOrgScopes !== undefined || ownOrgVerdictRef.current !== undefined || orgGovernedProvenance;
1059
1059
  const frozenOnAsk = spec.onAsk ?? deps.onAsk;
1060
+ const hookEnvSource = (ownedEnv ?? deps.executionEnv) != null ? executionEnv : undefined;
1061
+ const notifyOwnHookCrash = (err) => {
1062
+ try {
1063
+ deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "hook", sessionId });
1064
+ }
1065
+ catch {
1066
+ }
1067
+ };
1060
1068
  const frozenOnQuestion = spec.onQuestion ?? deps.onQuestion;
1061
1069
  const inheritedGateForChildren = () => {
1062
1070
  const ancestorRules = [
@@ -1066,8 +1074,25 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1066
1074
  const ownCallerPolicy = lockedPreflight.toolPolicy;
1067
1075
  const durableMandate = runtimeCaps?.forceDurableGate === true ||
1068
1076
  (spec.durableApproval !== undefined && !isLiveApproverSeat(frozenOnAsk));
1077
+ const ownPreToolUse = hooks?.preToolUse;
1078
+ const hookConstraint = ownPreToolUse !== undefined &&
1079
+ !(inheritedParentConstraints ?? []).some((pc) => pc.preToolUse === ownPreToolUse &&
1080
+ askApproverIdentity(pc.onAsk) === askApproverIdentity(frozenOnAsk) &&
1081
+ (pc.durableMandate === true) === durableMandate &&
1082
+ pc.hookEnv === hookEnvSource)
1083
+ ? [
1084
+ {
1085
+ policy: createPreToolUseConstraintPolicy(ownPreToolUse, hookEnvFace, notifyOwnHookCrash),
1086
+ preToolUse: ownPreToolUse,
1087
+ ...(hookEnvSource !== undefined ? { hookEnv: hookEnvSource } : {}),
1088
+ ...(frozenOnAsk !== undefined ? { onAsk: frozenOnAsk } : {}),
1089
+ ...(durableMandate ? { durableMandate: true } : {}),
1090
+ },
1091
+ ]
1092
+ : [];
1069
1093
  const parentConstraints = [
1070
1094
  ...(inheritedParentConstraints ?? []),
1095
+ ...hookConstraint,
1071
1096
  ...(ownCallerPolicy !== undefined
1072
1097
  ? [
1073
1098
  {
@@ -2920,6 +2945,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2920
2945
  auditPolicyNames(layer);
2921
2946
  const denyNarrowingPolicy = narrowingLayers.length === 0 ? undefined : narrowingLayers.length === 1 ? narrowingLayers[0] : combinePolicies(...narrowingLayers);
2922
2947
  const basePolicyForResumeEdit = lockedPreflight.toolPolicy;
2948
+ const hooks = spec.hooks ?? deps.hooks;
2923
2949
  const sameInstanceAncestorCount = policy === undefined ? 0 : (inheritedParentConstraints ?? []).reduce((n, pc) => (pc.policy === policy ? n + 1 : n), 0);
2924
2950
  const sharedFirstDecision = new Map();
2925
2951
  const SHARED_FIRST_DECISION_CAP = 256;
@@ -3067,125 +3093,133 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3067
3093
  editArgs = rr.updatedInput;
3068
3094
  }
3069
3095
  };
3070
- const parentConstraintWrappers = (inheritedParentConstraints ?? []).map((pc) => policy !== undefined && pc.policy === policy
3071
- ? {
3072
- check: async (creq, csignal) => {
3073
- const first = sharedFirstDecision.get(creq.toolCallId);
3074
- if (first === undefined) {
3075
- return {
3076
- action: "deny",
3077
- message: `inherited parent policy could not be arbitrated for "${creq.toolName}" ` +
3078
- `(shared-instance first decision unavailable); denied fail-closed`,
3079
- };
3080
- }
3081
- if (first.action === "deny")
3082
- return first;
3083
- if (first.action === "allow")
3084
- return { action: "allow" };
3085
- if (creq.toolName === ASK_USER_QUESTION_TOOL_NAME && (pc.durableMandate !== true || markInheritedUnavailable(creq.toolCallId))) {
3086
- return first;
3087
- }
3088
- if (pc.durableMandate === true) {
3089
- if (resolveCheckpointStore(spec, deps) !== undefined &&
3090
- (spec.durableApproval !== undefined || runtimeCaps?.forceDurableGate === true) &&
3091
- markInheritedUnavailable(creq.toolCallId)) {
3092
- return first;
3096
+ const parentConstraintWrappers = (inheritedParentConstraints ?? []).map((pc) => pc.preToolUse !== undefined &&
3097
+ pc.preToolUse === hooks?.preToolUse &&
3098
+ pc.durableMandate !== true &&
3099
+ askApproverIdentity(pc.onAsk) === askApproverIdentity(frozenOnAsk) &&
3100
+ pc.hookEnv === hookEnvSource
3101
+ ? { check: () => ({ action: "allow" }) }
3102
+ : policy !== undefined && pc.policy === policy
3103
+ ? {
3104
+ check: async (creq, csignal) => {
3105
+ const first = sharedFirstDecision.get(creq.toolCallId);
3106
+ if (first === undefined) {
3107
+ return {
3108
+ action: "deny",
3109
+ message: `inherited parent policy could not be arbitrated for "${creq.toolName}" ` +
3110
+ `(shared-instance first decision unavailable); denied fail-closed`,
3111
+ };
3093
3112
  }
3094
- return {
3095
- action: "deny",
3096
- message: `inherited parent policy requires durable approval for "${creq.toolName}" — the parent's durable ` +
3097
- `ask cannot be reconstructed in a delegated child; denied fail-closed (tighten-only)`,
3098
- };
3099
- }
3100
- const presentedArgs = creq.args;
3101
- const askT0 = now();
3102
- const resolved = await resolveAsk({
3103
- toolName: creq.toolName,
3104
- toolCallId: creq.toolCallId,
3105
- args: presentedArgs,
3106
- ...ruleSuggestionsOf(creq.toolName, presentedArgs),
3107
- message: first.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
3108
- ...askSourceIdentity(),
3109
- ...riskAxesOf(creq.toolName),
3110
- ...(first.action === "ask" && first.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3111
- }, pc.onAsk, csignal ?? abortController.signal);
3112
- const askWaitMs = Math.max(0, now() - askT0);
3113
- if (resolved.action === "deny" && resolved.approverUnavailable === true) {
3114
- if (markInheritedUnavailable(creq.toolCallId))
3113
+ if (first.action === "deny")
3114
+ return first;
3115
+ if (first.action === "allow")
3116
+ return { action: "allow" };
3117
+ if (creq.toolName === ASK_USER_QUESTION_TOOL_NAME && (pc.durableMandate !== true || markInheritedUnavailable(creq.toolCallId))) {
3115
3118
  return first;
3116
- return {
3117
- action: "deny",
3118
- message: `no approver is reachable for the inherited approval of "${creq.toolName}" and the marker channel is at capacity — denied fail-closed`,
3119
- };
3120
- }
3121
- if (resolved.action !== "allow")
3122
- return resolved;
3123
- if (resolved.updatedInput !== undefined) {
3124
- return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, csignal);
3125
- }
3126
- recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs);
3127
- return { action: "allow" };
3128
- },
3129
- }
3130
- : {
3131
- check: async (creq, csignal) => {
3132
- let decision;
3133
- try {
3134
- decision = await pc.policy.check(creq, csignal ?? abortController.signal);
3135
- }
3136
- catch (err) {
3137
- return {
3138
- action: "deny",
3139
- message: `inherited parent policy errored for "${creq.toolName}": ${err instanceof Error ? err.message : String(err)}`,
3140
- };
3141
- }
3142
- if (decision.action !== "ask")
3143
- return decision;
3144
- if (creq.toolName === ASK_USER_QUESTION_TOOL_NAME && (pc.durableMandate !== true || markInheritedUnavailable(creq.toolCallId))) {
3145
- return decision;
3146
- }
3147
- if (pc.durableMandate === true) {
3148
- if (resolveCheckpointStore(spec, deps) !== undefined &&
3149
- (spec.durableApproval !== undefined || runtimeCaps?.forceDurableGate === true) &&
3150
- markInheritedUnavailable(creq.toolCallId)) {
3151
- return decision;
3152
3119
  }
3153
- return {
3154
- action: "deny",
3155
- message: `inherited parent policy requires durable approval for "${creq.toolName}" — the parent's durable ` +
3156
- `ask cannot be reconstructed in a delegated child; denied fail-closed (tighten-only)`,
3157
- };
3158
- }
3159
- const presentedArgs = decision.updatedInput !== undefined ? decision.updatedInput : creq.args;
3160
- const askT0 = now();
3161
- const resolved = await resolveAsk({
3162
- toolName: creq.toolName,
3163
- toolCallId: creq.toolCallId,
3164
- args: presentedArgs,
3165
- ...ruleSuggestionsOf(creq.toolName, presentedArgs),
3166
- message: decision.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
3167
- ...askSourceIdentity(),
3168
- ...riskAxesOf(creq.toolName),
3169
- ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3170
- }, pc.onAsk, csignal ?? abortController.signal);
3171
- const askWaitMs = Math.max(0, now() - askT0);
3172
- if (resolved.action === "deny" && resolved.approverUnavailable === true) {
3173
- if (markInheritedUnavailable(creq.toolCallId))
3120
+ if (pc.durableMandate === true) {
3121
+ if (resolveCheckpointStore(spec, deps) !== undefined &&
3122
+ (spec.durableApproval !== undefined || runtimeCaps?.forceDurableGate === true) &&
3123
+ markInheritedUnavailable(creq.toolCallId)) {
3124
+ return first;
3125
+ }
3126
+ return {
3127
+ action: "deny",
3128
+ message: `inherited parent policy requires durable approval for "${creq.toolName}" — the parent's durable ` +
3129
+ `ask cannot be reconstructed in a delegated child; denied fail-closed (tighten-only)`,
3130
+ };
3131
+ }
3132
+ const presentedArgs = creq.args;
3133
+ const askT0 = now();
3134
+ const resolved = await resolveAsk({
3135
+ toolName: creq.toolName,
3136
+ toolCallId: creq.toolCallId,
3137
+ args: presentedArgs,
3138
+ ...ruleSuggestionsOf(creq.toolName, presentedArgs),
3139
+ message: first.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
3140
+ ...askSourceIdentity(),
3141
+ ...riskAxesOf(creq.toolName),
3142
+ ...(first.action === "ask" && first.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3143
+ }, pc.onAsk, csignal ?? abortController.signal);
3144
+ const askWaitMs = Math.max(0, now() - askT0);
3145
+ if (resolved.action === "deny" && resolved.approverUnavailable === true) {
3146
+ if (markInheritedUnavailable(creq.toolCallId))
3147
+ return first;
3148
+ return {
3149
+ action: "deny",
3150
+ message: `no approver is reachable for the inherited approval of "${creq.toolName}" and the marker channel is at capacity — denied fail-closed`,
3151
+ };
3152
+ }
3153
+ if (resolved.action !== "allow")
3154
+ return resolved;
3155
+ if (resolved.updatedInput !== undefined) {
3156
+ return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, csignal);
3157
+ }
3158
+ recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs);
3159
+ return { action: "allow" };
3160
+ },
3161
+ }
3162
+ : {
3163
+ check: async (creq, csignal) => {
3164
+ let decision;
3165
+ try {
3166
+ decision = await pc.policy.check(creq, csignal ?? abortController.signal);
3167
+ }
3168
+ catch (err) {
3169
+ return {
3170
+ action: "deny",
3171
+ message: `inherited parent policy errored for "${creq.toolName}": ${err instanceof Error ? err.message : String(err)}`,
3172
+ };
3173
+ }
3174
+ if (decision.action !== "ask")
3174
3175
  return decision;
3175
- return {
3176
- action: "deny",
3177
- message: `no approver is reachable for the inherited approval of "${creq.toolName}" and the marker channel is at capacity — denied fail-closed`,
3178
- };
3179
- }
3180
- if (resolved.action !== "allow")
3181
- return resolved;
3182
- if (resolved.updatedInput !== undefined) {
3183
- return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, csignal);
3184
- }
3185
- recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs);
3186
- return decision.updatedInput !== undefined ? { ...resolved, updatedInput: decision.updatedInput } : resolved;
3187
- },
3188
- });
3176
+ if (creq.toolName === ASK_USER_QUESTION_TOOL_NAME && (pc.durableMandate !== true || markInheritedUnavailable(creq.toolCallId))) {
3177
+ return decision;
3178
+ }
3179
+ if (pc.durableMandate === true) {
3180
+ if (resolveCheckpointStore(spec, deps) !== undefined &&
3181
+ (spec.durableApproval !== undefined || runtimeCaps?.forceDurableGate === true) &&
3182
+ markInheritedUnavailable(creq.toolCallId)) {
3183
+ return decision;
3184
+ }
3185
+ return {
3186
+ action: "deny",
3187
+ message: `inherited parent policy requires durable approval for "${creq.toolName}" the parent's durable ` +
3188
+ `ask cannot be reconstructed in a delegated child; denied fail-closed (tighten-only)`,
3189
+ };
3190
+ }
3191
+ const presentedArgs = decision.updatedInput !== undefined ? decision.updatedInput : creq.args;
3192
+ const askT0 = now();
3193
+ const resolved = await resolveAsk({
3194
+ toolName: creq.toolName,
3195
+ toolCallId: creq.toolCallId,
3196
+ args: presentedArgs,
3197
+ ...ruleSuggestionsOf(creq.toolName, presentedArgs),
3198
+ message: decision.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
3199
+ ...askSourceIdentity(),
3200
+ ...riskAxesOf(creq.toolName),
3201
+ ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3202
+ }, pc.onAsk, csignal ?? abortController.signal);
3203
+ const askWaitMs = Math.max(0, now() - askT0);
3204
+ if (resolved.action === "deny" && resolved.approverUnavailable === true) {
3205
+ if (markInheritedUnavailable(creq.toolCallId))
3206
+ return decision;
3207
+ return {
3208
+ action: "deny",
3209
+ message: `no approver is reachable for the inherited approval of "${creq.toolName}" and the marker channel is at capacity — denied fail-closed`,
3210
+ };
3211
+ }
3212
+ if (resolved.action !== "allow")
3213
+ return resolved;
3214
+ if (resolved.updatedInput !== undefined) {
3215
+ return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, csignal);
3216
+ }
3217
+ if (pc.preToolUse === undefined) {
3218
+ recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs);
3219
+ }
3220
+ return decision.updatedInput !== undefined ? { ...resolved, updatedInput: decision.updatedInput } : resolved;
3221
+ },
3222
+ });
3189
3223
  const rewriteCapableLayers = policy !== undefined || parentConstraintWrappers.length > 0;
3190
3224
  const rewriteEmitterIndex = new Map();
3191
3225
  const rewriteCapableChain = [
@@ -3269,7 +3303,6 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3269
3303
  },
3270
3304
  }
3271
3305
  : foldedPolicy;
3272
- const hooks = spec.hooks ?? deps.hooks;
3273
3306
  const onAsk = spec.onAsk ?? deps.onAsk;
3274
3307
  const handWriteTools = handsEnabled && spec.handsReadOnly !== true
3275
3308
  ? Object.keys(HAND_TOOL_EFFECTS).filter((name) => {
@@ -3304,6 +3337,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3304
3337
  }
3305
3338
  const preToolContexts = new Map();
3306
3339
  const blockedToolCalls = new Set();
3340
+ const approvalSettledBy = new Map();
3307
3341
  const blockedTracked = Boolean(hooks?.postToolUse || hooks?.preToolUse || hooks?.postToolUseFailure || hooks?.postToolBatch);
3308
3342
  const restoreSurfaceGap = ownedEnv !== undefined && isRemoteExecutionEnv(ownedEnv) && ownedEnv.capabilities.suspendable ? missingRestoreSurface(ownedEnv) : [];
3309
3343
  const incompleteSuspendAdapter = restoreSurfaceGap.length > 0 ? restoreSurfaceGap : undefined;
@@ -3390,7 +3424,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3390
3424
  const hookEnvFace = hookContextConsumerWired && (ownedEnv ?? deps.executionEnv) != null ? createHookEnvCapabilities(executionEnv) : undefined;
3391
3425
  if (effectivePolicy || hooks?.preToolUse || egressTools.size > 0 || irreversibleTools.size > 0 || resourceSuspendEligible || platformSuspendArmed || spec.enablePlanMode === true) {
3392
3426
  const adjudicate = effectivePolicy
3393
- ? (req) => raceAbort(Promise.resolve(effectivePolicy.check({ ...req, budget: budgetSnapshot }, abortController.signal)), abortController.signal, () => ({
3427
+ ? (req) => raceAbort(Promise.resolve(effectivePolicy.check({ ...req, budget: budgetSnapshot, ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}) }, abortController.signal)), abortController.signal, () => ({
3394
3428
  action: "deny",
3395
3429
  message: "policy check aborted (task timed out or cancelled)",
3396
3430
  }))
@@ -3917,7 +3951,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3917
3951
  if (!presented.ok) {
3918
3952
  throw new ParkRefusal(`the stored form of "${req.toolName}"'s arguments could not be presented for re-adjudication (${describeThrown(presented.cause)})`, { cause: presented.cause });
3919
3953
  }
3920
- const reprojected = refuseOutOfContractDecision(await basePolicyForResumeEdit.check({ toolName: req.toolName, args: presented.value, toolCallId: req.toolCallId }, abortController.signal));
3954
+ const reprojected = refuseOutOfContractDecision(await basePolicyForResumeEdit.check({
3955
+ toolName: req.toolName,
3956
+ args: presented.value,
3957
+ toolCallId: req.toolCallId,
3958
+ ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}),
3959
+ }, abortController.signal));
3921
3960
  const demanded = reprojected.updatedInput !== undefined ? tryCloneArgs(reprojected.updatedInput) : undefined;
3922
3961
  const rewroteTheFiledValue = reprojected.updatedInput !== undefined &&
3923
3962
  !(demanded?.ok === true &&
@@ -4156,6 +4195,17 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4156
4195
  if (blockedTracked && (result.block || result.suspend)) {
4157
4196
  blockedToolCalls.add(e.toolCallId);
4158
4197
  }
4198
+ const reported = result.settledBy;
4199
+ if (reported !== undefined) {
4200
+ const settling = result.block === true;
4201
+ if (!isApprovalSettledBy(reported) || (!settling && reported !== "human")) {
4202
+ deps.onError?.(new Error(`a tool-gate settlement reported settledBy "${String(reported)}" on ${settling ? "a blocked" : "an executing"} call — ` +
4203
+ `it is one of "human" / "timeout" / "aborted", and only "human" can be the source of a call that runs; the frame carries no source`), { phase: "config", sessionId });
4204
+ }
4205
+ else {
4206
+ approvalSettledBy.set(e.toolCallId, reported);
4207
+ }
4208
+ }
4159
4209
  return result.block
4160
4210
  ? { block: true, reason: result.reason }
4161
4211
  : result.updatedInput !== undefined
@@ -4444,7 +4494,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4444
4494
  : undefined;
4445
4495
  overheadState.promptChars = systemPrompt.length;
4446
4496
  const preparedHolder = {};
4447
- const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4497
+ const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettledBy, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4448
4498
  const prepared = buildPrepared();
4449
4499
  preparedHolder.current = prepared;
4450
4500
  return prepared;
@@ -5,6 +5,7 @@ import { type TaskOutcome } from "../task-outcome.js";
5
5
  import { type SideQuerySpec, type SideQueryResult } from "../side-query.js";
6
6
  import type { SessionStore } from "../session.js";
7
7
  import { type RecoveredOrphan } from "../session-reconcile.js";
8
+ import { type ApprovalSettledBy } from "../tool-policy.js";
8
9
  import type { AgentDefinition, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream } from "../types.js";
9
10
  export type ResumeTaskConfig = Omit<TaskSpec, "objective" | "sessionId">;
10
11
  interface ResumeRun {
@@ -17,12 +18,13 @@ interface ResumeRun {
17
18
  onEnvRestoreFailed?: (reason: ReopenReason) => Promise<void>;
18
19
  decisionDelivered?: boolean;
19
20
  }
20
- declare function toolEndBodyFrom(result: unknown, isError: boolean): {
21
+ declare function toolEndBodyFrom(result: unknown, isError: boolean, settledBy?: ApprovalSettledBy): {
21
22
  output?: unknown;
22
23
  truncated?: boolean;
23
24
  totalChars?: number;
24
25
  structured?: unknown;
25
26
  errorCode?: string;
27
+ settledBy?: ApprovalSettledBy;
26
28
  };
27
29
  export declare function reconciledToolEndBody(orphan: Pick<RecoveredOrphan, "text" | "errorKind">): ReturnType<typeof toolEndBodyFrom>;
28
30
  export declare const GOVERNANCE_READ_STALLED: unique symbol;
@@ -38,7 +38,7 @@ import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "../unt
38
38
  import { reconcileInterruptedSession } from "../session-reconcile.js";
39
39
  import { RunnerSharedToolResultStore } from "../tool-result-store.js";
40
40
  import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
41
- import { refuseOutOfContractDecision, toolPolicyNameSets } from "../tool-policy.js";
41
+ import { isApprovalSettledBy, refuseOutOfContractDecision, toolPolicyNameSets } from "../tool-policy.js";
42
42
  import { defaultTaskRegistry } from "../task-registry.js";
43
43
  import { discloseDroppedPending, isDelegatedAgentTerminal, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
44
44
  import { ToolDetachHub } from "../tool-detach.js";
@@ -126,7 +126,7 @@ function resumeDecisionWasNegative(resume) {
126
126
  }
127
127
  const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call that suspended for durable approval, so it was " +
128
128
  "NOT executed on resume. If you still need it, issue it again now.";
129
- function toolEndBodyFrom(result, isError) {
129
+ function toolEndBodyFrom(result, isError, settledBy) {
130
130
  const o = toolOutputFrom(result);
131
131
  const st = structuredFrom(result);
132
132
  const code = isError ? result?.details?.code : undefined;
@@ -134,6 +134,7 @@ function toolEndBodyFrom(result, isError) {
134
134
  ...(o !== undefined ? { output: o.output, ...(o.truncated ? { truncated: true } : {}), ...(o.totalChars !== undefined ? { totalChars: o.totalChars } : {}) } : {}),
135
135
  ...(st !== undefined ? { structured: st } : {}),
136
136
  ...(typeof code === "string" ? { errorCode: code } : {}),
137
+ ...(settledBy !== undefined ? { settledBy } : {}),
137
138
  };
138
139
  }
139
140
  export function reconciledToolEndBody(orphan) {
@@ -1202,13 +1203,16 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1202
1203
  prepared.lspDiagnostics.nudge(p);
1203
1204
  }
1204
1205
  }
1206
+ const settledBy = prepared.approvalSettledBy.get(event.toolCallId);
1207
+ if (settledBy !== undefined)
1208
+ prepared.approvalSettledBy.delete(event.toolCallId);
1205
1209
  pushContent({
1206
1210
  type: "tool_end",
1207
1211
  toolCallId: event.toolCallId,
1208
1212
  toolName: event.toolName,
1209
1213
  ...(toolLabels.get(event.toolName) !== undefined ? { label: toolLabels.get(event.toolName) } : {}),
1210
1214
  isError: event.isError,
1211
- ...toolEndBodyFrom(event.result, event.isError),
1215
+ ...toolEndBodyFrom(event.result, event.isError, settledBy),
1212
1216
  ...ident(),
1213
1217
  });
1214
1218
  announceWorkspaceMove();
@@ -3617,6 +3621,14 @@ export class Runner {
3617
3621
  if (decision !== "allow" && decision !== "deny") {
3618
3622
  throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the policy_ask domain — a decide is exactly "allow" or "deny"; refusing pre-CAS, the checkpoint stays pending`);
3619
3623
  }
3624
+ const settledBy = decide.settledBy;
3625
+ if (settledBy !== undefined && !isApprovalSettledBy(settledBy)) {
3626
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume settledBy "${describeSuppliedValue(settledBy)}" is outside the settlement vocabulary — it is exactly "human", "timeout" or "aborted" (or omitted); refusing pre-CAS, the checkpoint stays pending`, { field: "settledBy" });
3627
+ }
3628
+ if (settledBy !== undefined && settledBy !== "human" && decision === "allow") {
3629
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume carries decision "allow" with settledBy "${settledBy}" — a non-human settlement is a fail-closed end (nobody answered), so it cannot be the source of an approval that EXECUTES; ` +
3630
+ "supply \"human\", or omit the field if the allow came from configuration rather than a person; refusing pre-CAS, the checkpoint stays pending", { field: "settledBy" });
3631
+ }
3620
3632
  const reason = decide.reason;
3621
3633
  assertOutcomeText(reason, "reason");
3622
3634
  plainPolicyOutcome = {
@@ -3627,6 +3639,7 @@ export class Runner {
3627
3639
  ...(updatedInput !== undefined ? { updatedInput } : {}),
3628
3640
  ...(reason !== undefined ? { reason } : {}),
3629
3641
  ...(redeemedAnswer !== undefined ? { answer: redeemedAnswer } : {}),
3642
+ ...(settledBy !== undefined ? { settledBy } : {}),
3630
3643
  };
3631
3644
  if (plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason && sanitizeUntrustedText(plainPolicyOutcome.reason) !== plainPolicyOutcome.reason) {
3632
3645
  throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
@@ -3823,9 +3836,14 @@ export class Runner {
3823
3836
  const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput !== undefined ? outcome.updatedInput : pendingAction.args);
3824
3837
  const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
3825
3838
  emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
3826
- const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError) });
3839
+ const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, outcome.settledBy) });
3827
3840
  if (outcome.decision === "deny") {
3828
- const reason = outcome.reason ? delimitUntrusted("reviewer note", outcome.reason) : `The pending tool call "${pendingAction.toolName}" was denied by an approver.`;
3841
+ const defaultDenial = outcome.settledBy === "timeout"
3842
+ ? `No one answered the approval request for the pending tool call "${pendingAction.toolName}" — the approval window elapsed with no answer, so it was not executed.`
3843
+ : outcome.settledBy === "aborted"
3844
+ ? `The approval for the pending tool call "${pendingAction.toolName}" ended without anyone deciding it (it was cancelled or could not be delivered), so it was not executed.`
3845
+ : `The pending tool call "${pendingAction.toolName}" was denied by an approver.`;
3846
+ const reason = outcome.reason ? delimitUntrusted("reviewer note", outcome.reason) : defaultDenial;
3829
3847
  emitEnd(true, { content: formatHookFeedback(reason) });
3830
3848
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(reason), true));
3831
3849
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
@@ -3835,7 +3853,12 @@ export class Runner {
3835
3853
  throw new CheckpointError("checkpoint.invalid_outcome", `pending action reached the resolver with decision "${String(outcome.decision)}" — only "allow" executes and only "deny" injects a denial; refusing to execute`);
3836
3854
  }
3837
3855
  if (outcome.decision === "allow" && outcome.updatedInput !== undefined && prepared.basePolicyForResumeEdit) {
3838
- const rechecked = refuseOutOfContractDecision(await prepared.basePolicyForResumeEdit.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
3856
+ const rechecked = refuseOutOfContractDecision(await prepared.basePolicyForResumeEdit.check({
3857
+ toolName: pendingAction.toolName,
3858
+ args: resolvedArgs,
3859
+ toolCallId: pendingAction.toolCallId,
3860
+ ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
3861
+ }, prepared.abortController.signal));
3839
3862
  if (rechecked.action === "deny") {
3840
3863
  const editedDenial = formatHookFeedback(`The approver EDITED this call's input; the edited call is denied by the deployment's tool policy and was not executed${rechecked.message ? `: ${rechecked.message}` : ""}.`);
3841
3864
  emitEnd(true, { content: editedDenial });
@@ -3845,7 +3868,12 @@ export class Runner {
3845
3868
  }
3846
3869
  }
3847
3870
  if (prepared.denyNarrowingPolicy) {
3848
- const narrowed = refuseOutOfContractDecision(await prepared.denyNarrowingPolicy.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
3871
+ const narrowed = refuseOutOfContractDecision(await prepared.denyNarrowingPolicy.check({
3872
+ toolName: pendingAction.toolName,
3873
+ args: resolvedArgs,
3874
+ toolCallId: pendingAction.toolCallId,
3875
+ ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
3876
+ }, prepared.abortController.signal));
3849
3877
  if (narrowed.action === "deny") {
3850
3878
  const narrowedDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" is now denied by a session rule and was not executed${narrowed.message ? `: ${narrowed.message}` : ""}.`);
3851
3879
  emitEnd(true, { content: narrowedDenial });
@@ -58,7 +58,7 @@ export function createSessionRulePolicy(rules, opts) {
58
58
  if (typeof path !== "string" || path.length === 0) {
59
59
  return deny(`write tool "${req.toolName}" denied: session rule confines writes to allowDirs but the call has no resolvable path`);
60
60
  }
61
- const canon = await canonicalizeTarget(env, path, signal, rootPath);
61
+ const canon = await canonicalizeTarget(env, path, signal, req.cwd ?? rootPath);
62
62
  if (!canon.ok) {
63
63
  return deny(`write to "${path}" denied: its real target could not be resolved against the session-rule allowDirs`);
64
64
  }
@@ -64,7 +64,7 @@ export function createSensitivePathPolicy(opts) {
64
64
  const path = writeTargetPath(canonical, req.args);
65
65
  if (typeof path !== "string" || path.length === 0)
66
66
  return { action: "allow" };
67
- const canon = await canonicalizeTarget(opts.env, path, signal, opts.rootPath);
67
+ const canon = await canonicalizeTarget(opts.env, path, signal, req.cwd ?? opts.rootPath);
68
68
  if (!canon.ok) {
69
69
  if (canon.unresolvedSymlink) {
70
70
  return {
@@ -2,6 +2,7 @@ export interface ToolCallRequest {
2
2
  toolName: string;
3
3
  args: unknown;
4
4
  toolCallId: string;
5
+ cwd?: string;
5
6
  budget?: {
6
7
  resourceRemainingMicroUsd?: number;
7
8
  resourceSpentMicroUsd: number;
@@ -9,11 +10,15 @@ export interface ToolCallRequest {
9
10
  };
10
11
  }
11
12
  export type DecisionReason = "rule" | "mode" | "hook" | "safety" | "classifier" | "persisted_rule";
13
+ export type ApprovalSettledBy = "human" | "timeout" | "aborted";
14
+ export declare const APPROVAL_SETTLED_BY_VALUES: readonly ApprovalSettledBy[];
15
+ export declare function isApprovalSettledBy(v: unknown): v is ApprovalSettledBy;
12
16
  export type PermissionResult = {
13
17
  action: "allow";
14
18
  updatedInput?: unknown;
15
19
  message?: string;
16
20
  decisionReason?: DecisionReason;
21
+ settledBy?: Extract<ApprovalSettledBy, "human">;
17
22
  } | {
18
23
  action: "ask";
19
24
  updatedInput?: unknown;
@@ -25,6 +30,7 @@ export type PermissionResult = {
25
30
  updatedInput?: unknown;
26
31
  message?: string;
27
32
  decisionReason?: DecisionReason;
33
+ settledBy?: ApprovalSettledBy;
28
34
  };
29
35
  export declare function decisionText(d: PermissionResult): string | undefined;
30
36
  export interface ToolPolicy {