@sema-agent/core 5.18.1 → 5.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/dist/agents/roster-store.js +3 -0
  3. package/dist/brain/circuit-breaker.js +14 -3
  4. package/dist/brain/timeout.d.ts +1 -0
  5. package/dist/brain/timeout.js +11 -0
  6. package/dist/core/background-agent-store.d.ts +1 -0
  7. package/dist/core/background-agent-store.js +5 -0
  8. package/dist/core/fs-write-gate-policy.js +1 -1
  9. package/dist/core/hooks.d.ts +3 -1
  10. package/dist/core/hooks.js +32 -0
  11. package/dist/core/mailbox-store.js +2 -0
  12. package/dist/core/mcp.d.ts +4 -0
  13. package/dist/core/mcp.js +58 -11
  14. package/dist/core/retention-policy.d.ts +7 -0
  15. package/dist/core/retention-policy.js +21 -0
  16. package/dist/core/runner/active-skill-scope.js +1 -1
  17. package/dist/core/runner/prepare-task.d.ts +3 -0
  18. package/dist/core/runner/prepare-task.js +195 -121
  19. package/dist/core/runner/runtask.js +29 -4
  20. package/dist/core/runner/session-rule-policy.js +1 -1
  21. package/dist/core/sensitive-path-policy.js +1 -1
  22. package/dist/core/task-registry-agent.js +2 -0
  23. package/dist/core/tool-policy.d.ts +1 -0
  24. package/dist/core/tool-policy.js +21 -12
  25. package/dist/core/workflow-run-store.js +2 -0
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/orchestration/run-spec.js +5 -1
  29. package/dist/orchestration/workflow.js +13 -2
  30. package/dist/stores/file/background-agent-store.js +2 -1
  31. package/dist/stores/file/mailbox-store.js +2 -0
  32. package/dist/stores/file/workflow-run-store.js +2 -0
  33. package/dist/tools/fs/bash-readonly-classifier.d.ts +5 -2
  34. package/dist/tools/fs/bash-readonly-classifier.js +129 -17
  35. package/dist/tools/web.js +32 -5
  36. package/package.json +1 -1
@@ -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 = preToolUseObservational ? undefined : 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,25 @@ 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;
2949
+ const preToolUseObservational = hooks?.preToolUse !== undefined && hooks.preToolUseObservational === true;
2950
+ const ownGatePreToolUse = !preToolUseObservational
2951
+ ? hooks?.preToolUse
2952
+ : async (toolName, input, ctx) => {
2953
+ const r = await hooks.preToolUse(toolName, cloneObserverInput(input), ctx);
2954
+ if (r === undefined)
2955
+ return undefined;
2956
+ try {
2957
+ const action = typeof r.action === "string" ? r.action : "(non-string action)";
2958
+ deps.onError?.(new Error(`a PreToolUse hook declared observational (Hooks.preToolUseObservational) returned a decision ("${action}") while screening ` +
2959
+ `"${toolName}" — the decision was NOT adopted and the call proceeded as if the hook had no opinion. ` +
2960
+ `A declared-observational face must return undefined; drop the declaration if its verdicts are meant to count ` +
2961
+ `(they then also travel to delegated children as an inherited screening constraint).`), { phase: "hook", sessionId, classification: "observational-hook-verdict-ignored" });
2962
+ }
2963
+ catch {
2964
+ }
2965
+ return undefined;
2966
+ };
2923
2967
  const sameInstanceAncestorCount = policy === undefined ? 0 : (inheritedParentConstraints ?? []).reduce((n, pc) => (pc.policy === policy ? n + 1 : n), 0);
2924
2968
  const sharedFirstDecision = new Map();
2925
2969
  const SHARED_FIRST_DECISION_CAP = 256;
@@ -3067,125 +3111,134 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3067
3111
  editArgs = rr.updatedInput;
3068
3112
  }
3069
3113
  };
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;
3114
+ const parentConstraintWrappers = (inheritedParentConstraints ?? []).map((pc) => pc.preToolUse !== undefined &&
3115
+ pc.preToolUse === hooks?.preToolUse &&
3116
+ !preToolUseObservational &&
3117
+ pc.durableMandate !== true &&
3118
+ askApproverIdentity(pc.onAsk) === askApproverIdentity(frozenOnAsk) &&
3119
+ pc.hookEnv === hookEnvSource
3120
+ ? { check: () => ({ action: "allow" }) }
3121
+ : policy !== undefined && pc.policy === policy
3122
+ ? {
3123
+ check: async (creq, csignal) => {
3124
+ const first = sharedFirstDecision.get(creq.toolCallId);
3125
+ if (first === undefined) {
3126
+ return {
3127
+ action: "deny",
3128
+ message: `inherited parent policy could not be arbitrated for "${creq.toolName}" ` +
3129
+ `(shared-instance first decision unavailable); denied fail-closed`,
3130
+ };
3093
3131
  }
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))
3132
+ if (first.action === "deny")
3133
+ return first;
3134
+ if (first.action === "allow")
3135
+ return { action: "allow" };
3136
+ if (creq.toolName === ASK_USER_QUESTION_TOOL_NAME && (pc.durableMandate !== true || markInheritedUnavailable(creq.toolCallId))) {
3115
3137
  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
3138
  }
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))
3139
+ if (pc.durableMandate === true) {
3140
+ if (resolveCheckpointStore(spec, deps) !== undefined &&
3141
+ (spec.durableApproval !== undefined || runtimeCaps?.forceDurableGate === true) &&
3142
+ markInheritedUnavailable(creq.toolCallId)) {
3143
+ return first;
3144
+ }
3145
+ return {
3146
+ action: "deny",
3147
+ message: `inherited parent policy requires durable approval for "${creq.toolName}" — the parent's durable ` +
3148
+ `ask cannot be reconstructed in a delegated child; denied fail-closed (tighten-only)`,
3149
+ };
3150
+ }
3151
+ const presentedArgs = creq.args;
3152
+ const askT0 = now();
3153
+ const resolved = await resolveAsk({
3154
+ toolName: creq.toolName,
3155
+ toolCallId: creq.toolCallId,
3156
+ args: presentedArgs,
3157
+ ...ruleSuggestionsOf(creq.toolName, presentedArgs),
3158
+ message: first.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
3159
+ ...askSourceIdentity(),
3160
+ ...riskAxesOf(creq.toolName),
3161
+ ...(first.action === "ask" && first.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3162
+ }, pc.onAsk, csignal ?? abortController.signal);
3163
+ const askWaitMs = Math.max(0, now() - askT0);
3164
+ if (resolved.action === "deny" && resolved.approverUnavailable === true) {
3165
+ if (markInheritedUnavailable(creq.toolCallId))
3166
+ return first;
3167
+ return {
3168
+ action: "deny",
3169
+ message: `no approver is reachable for the inherited approval of "${creq.toolName}" and the marker channel is at capacity — denied fail-closed`,
3170
+ };
3171
+ }
3172
+ if (resolved.action !== "allow")
3173
+ return resolved;
3174
+ if (resolved.updatedInput !== undefined) {
3175
+ return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, csignal);
3176
+ }
3177
+ recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs);
3178
+ return { action: "allow" };
3179
+ },
3180
+ }
3181
+ : {
3182
+ check: async (creq, csignal) => {
3183
+ let decision;
3184
+ try {
3185
+ decision = await pc.policy.check(creq, csignal ?? abortController.signal);
3186
+ }
3187
+ catch (err) {
3188
+ return {
3189
+ action: "deny",
3190
+ message: `inherited parent policy errored for "${creq.toolName}": ${err instanceof Error ? err.message : String(err)}`,
3191
+ };
3192
+ }
3193
+ if (decision.action !== "ask")
3174
3194
  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
- });
3195
+ if (creq.toolName === ASK_USER_QUESTION_TOOL_NAME && (pc.durableMandate !== true || markInheritedUnavailable(creq.toolCallId))) {
3196
+ return decision;
3197
+ }
3198
+ if (pc.durableMandate === true) {
3199
+ if (resolveCheckpointStore(spec, deps) !== undefined &&
3200
+ (spec.durableApproval !== undefined || runtimeCaps?.forceDurableGate === true) &&
3201
+ markInheritedUnavailable(creq.toolCallId)) {
3202
+ return decision;
3203
+ }
3204
+ return {
3205
+ action: "deny",
3206
+ message: `inherited parent policy requires durable approval for "${creq.toolName}" the parent's durable ` +
3207
+ `ask cannot be reconstructed in a delegated child; denied fail-closed (tighten-only)`,
3208
+ };
3209
+ }
3210
+ const presentedArgs = decision.updatedInput !== undefined ? decision.updatedInput : creq.args;
3211
+ const askT0 = now();
3212
+ const resolved = await resolveAsk({
3213
+ toolName: creq.toolName,
3214
+ toolCallId: creq.toolCallId,
3215
+ args: presentedArgs,
3216
+ ...ruleSuggestionsOf(creq.toolName, presentedArgs),
3217
+ message: decision.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
3218
+ ...askSourceIdentity(),
3219
+ ...riskAxesOf(creq.toolName),
3220
+ ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3221
+ }, pc.onAsk, csignal ?? abortController.signal);
3222
+ const askWaitMs = Math.max(0, now() - askT0);
3223
+ if (resolved.action === "deny" && resolved.approverUnavailable === true) {
3224
+ if (markInheritedUnavailable(creq.toolCallId))
3225
+ return decision;
3226
+ return {
3227
+ action: "deny",
3228
+ message: `no approver is reachable for the inherited approval of "${creq.toolName}" and the marker channel is at capacity — denied fail-closed`,
3229
+ };
3230
+ }
3231
+ if (resolved.action !== "allow")
3232
+ return resolved;
3233
+ if (resolved.updatedInput !== undefined) {
3234
+ return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, csignal);
3235
+ }
3236
+ if (pc.preToolUse === undefined) {
3237
+ recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs);
3238
+ }
3239
+ return decision.updatedInput !== undefined ? { ...resolved, updatedInput: decision.updatedInput } : resolved;
3240
+ },
3241
+ });
3189
3242
  const rewriteCapableLayers = policy !== undefined || parentConstraintWrappers.length > 0;
3190
3243
  const rewriteEmitterIndex = new Map();
3191
3244
  const rewriteCapableChain = [
@@ -3269,7 +3322,6 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3269
3322
  },
3270
3323
  }
3271
3324
  : foldedPolicy;
3272
- const hooks = spec.hooks ?? deps.hooks;
3273
3325
  const onAsk = spec.onAsk ?? deps.onAsk;
3274
3326
  const handWriteTools = handsEnabled && spec.handsReadOnly !== true
3275
3327
  ? Object.keys(HAND_TOOL_EFFECTS).filter((name) => {
@@ -3277,7 +3329,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3277
3329
  return eff === "write" || eff === "idempotent";
3278
3330
  })
3279
3331
  : [];
3280
- const hasEffectAwareGate = Boolean(policyLayers.length > 0 || hooks?.preToolUse);
3332
+ const hasEffectAwareGate = Boolean(policyLayers.length > 0 || (hooks?.preToolUse !== undefined && !preToolUseObservational));
3281
3333
  const destructiveMcpUngated = mcp.tools.some((t) => !irreversibleTools.has(t.name) && !egressTools.has(t.name) && (toolEffects.get(t.name) ?? "write") !== "read");
3282
3334
  const firstPartyWriteUngated = (spec.tools ?? [])
3283
3335
  .map((t) => t.name)
@@ -3391,7 +3443,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3391
3443
  const hookEnvFace = hookContextConsumerWired && (ownedEnv ?? deps.executionEnv) != null ? createHookEnvCapabilities(executionEnv) : undefined;
3392
3444
  if (effectivePolicy || hooks?.preToolUse || egressTools.size > 0 || irreversibleTools.size > 0 || resourceSuspendEligible || platformSuspendArmed || spec.enablePlanMode === true) {
3393
3445
  const adjudicate = effectivePolicy
3394
- ? (req) => raceAbort(Promise.resolve(effectivePolicy.check({ ...req, budget: budgetSnapshot }, abortController.signal)), abortController.signal, () => ({
3446
+ ? (req) => raceAbort(Promise.resolve(effectivePolicy.check({ ...req, budget: budgetSnapshot, ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}) }, abortController.signal)), abortController.signal, () => ({
3395
3447
  action: "deny",
3396
3448
  message: "policy check aborted (task timed out or cancelled)",
3397
3449
  }))
@@ -3531,6 +3583,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3531
3583
  carried.spentMicroUsd = prior + parkedSpendMicroUsd;
3532
3584
  return carried;
3533
3585
  };
3586
+ const screeningParkDisclosedRef = { done: false };
3534
3587
  const serializeCheckpointState = (workspaceHandle, parkedSpendMicroUsd) => ({
3535
3588
  activeTools: [...activeTools],
3536
3589
  outputRef: { value: outputRef.value, set: outputRef.set },
@@ -3587,6 +3640,22 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3587
3640
  await memoryEngineSession.harvest("checkpoint");
3588
3641
  try {
3589
3642
  await checkpointStore.put(token, cp);
3643
+ const committedCount = cp.state.inheritedGate?.parentConstraintCount;
3644
+ if (cp.state.inheritedGate?.requiresParentConstraint === true &&
3645
+ !screeningParkDisclosedRef.done &&
3646
+ (inheritedParentConstraints ?? []).some((pc) => pc.preToolUse !== undefined)) {
3647
+ screeningParkDisclosedRef.done = true;
3648
+ try {
3649
+ deps.onError?.(new Error(`durable park under an inherited PreToolUse SCREENING constraint: this checkpoint records ` +
3650
+ `requiresParentConstraint with parentConstraintCount=${committedCount ?? "(unrecorded)"}, and the live ` +
3651
+ `closures cannot be persisted. A resume on THIS Runner re-supplies them automatically; a resume on a fresh ` +
3652
+ `Runner (restart / another replica) must hand back the whole chain via resumeStream(..., internals), ` +
3653
+ `rebuilding the screening entry with createPreToolUseConstraintPolicy(hook, env) — otherwise the row stays ` +
3654
+ `pending. If the face only observes, declare Hooks.preToolUseObservational and it stops entering the chain.`), { phase: "degraded", sessionId, classification: "screening-constraint-in-durable-chain" });
3655
+ }
3656
+ catch {
3657
+ }
3658
+ }
3590
3659
  return { ok: true };
3591
3660
  }
3592
3661
  catch (putErr) {
@@ -3918,7 +3987,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3918
3987
  if (!presented.ok) {
3919
3988
  throw new ParkRefusal(`the stored form of "${req.toolName}"'s arguments could not be presented for re-adjudication (${describeThrown(presented.cause)})`, { cause: presented.cause });
3920
3989
  }
3921
- const reprojected = refuseOutOfContractDecision(await basePolicyForResumeEdit.check({ toolName: req.toolName, args: presented.value, toolCallId: req.toolCallId }, abortController.signal));
3990
+ const reprojected = refuseOutOfContractDecision(await basePolicyForResumeEdit.check({
3991
+ toolName: req.toolName,
3992
+ args: presented.value,
3993
+ toolCallId: req.toolCallId,
3994
+ ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}),
3995
+ }, abortController.signal));
3922
3996
  const demanded = reprojected.updatedInput !== undefined ? tryCloneArgs(reprojected.updatedInput) : undefined;
3923
3997
  const rewroteTheFiledValue = reprojected.updatedInput !== undefined &&
3924
3998
  !(demanded?.ok === true &&
@@ -4114,7 +4188,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4114
4188
  result = await runToolGate({
4115
4189
  onNotifyError: (f) => emitTrace(deps.tracer, () => ({ kind: "observer.notify_failed", version: 1, taskId: spec.taskId ?? sessionId, site: f.site, message: f.error.message, ts: Date.now() })),
4116
4190
  event: e,
4117
- preToolUse: hooks?.preToolUse,
4191
+ preToolUse: ownGatePreToolUse,
4118
4192
  ...(hookEnvFace !== undefined ? { hookEnv: hookEnvFace } : {}),
4119
4193
  adjudicate,
4120
4194
  resolveAsk: resolveAskBound,
@@ -1357,6 +1357,7 @@ export class Runner {
1357
1357
  const hooks = h
1358
1358
  ? {
1359
1359
  ...(typeof h.preToolUse === "function" ? { preToolUse: (t, i, c) => h.preToolUse(t, i, c) } : {}),
1360
+ ...(typeof h.preToolUse === "function" && h.preToolUseObservational === true ? { preToolUseObservational: true } : {}),
1360
1361
  ...(typeof h.postToolUse === "function" ? { postToolUse: (t, i, o, c) => h.postToolUse(t, i, o, c) } : {}),
1361
1362
  ...(typeof h.userPromptSubmit === "function" ? { userPromptSubmit: (p) => h.userPromptSubmit(p) } : {}),
1362
1363
  ...(typeof h.stop === "function" ? { stop: (c) => h.stop(c) } : {}),
@@ -3853,7 +3854,12 @@ export class Runner {
3853
3854
  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`);
3854
3855
  }
3855
3856
  if (outcome.decision === "allow" && outcome.updatedInput !== undefined && prepared.basePolicyForResumeEdit) {
3856
- const rechecked = refuseOutOfContractDecision(await prepared.basePolicyForResumeEdit.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
3857
+ const rechecked = refuseOutOfContractDecision(await prepared.basePolicyForResumeEdit.check({
3858
+ toolName: pendingAction.toolName,
3859
+ args: resolvedArgs,
3860
+ toolCallId: pendingAction.toolCallId,
3861
+ ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
3862
+ }, prepared.abortController.signal));
3857
3863
  if (rechecked.action === "deny") {
3858
3864
  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}` : ""}.`);
3859
3865
  emitEnd(true, { content: editedDenial });
@@ -3863,7 +3869,12 @@ export class Runner {
3863
3869
  }
3864
3870
  }
3865
3871
  if (prepared.denyNarrowingPolicy) {
3866
- const narrowed = refuseOutOfContractDecision(await prepared.denyNarrowingPolicy.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
3872
+ const narrowed = refuseOutOfContractDecision(await prepared.denyNarrowingPolicy.check({
3873
+ toolName: pendingAction.toolName,
3874
+ args: resolvedArgs,
3875
+ toolCallId: pendingAction.toolCallId,
3876
+ ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
3877
+ }, prepared.abortController.signal));
3867
3878
  if (narrowed.action === "deny") {
3868
3879
  const narrowedDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" is now denied by a session rule and was not executed${narrowed.message ? `: ${narrowed.message}` : ""}.`);
3869
3880
  emitEnd(true, { content: narrowedDenial });
@@ -4074,14 +4085,28 @@ function startTimeout(harness, abortController, remainingMs, softSuspendable = f
4074
4085
  if (remainingMs !== undefined) {
4075
4086
  const hardMs = Math.max(0, remainingMs + (softSuspendable ? WALLTIME_SUSPEND_GRACE_SEC * 1000 : 0));
4076
4087
  const scheduledAtMs = Date.now() + hardMs;
4077
- const timer = setTimeout(() => {
4088
+ let leftMs = hardMs;
4089
+ let timer;
4090
+ const armChunk = () => {
4091
+ const chunk = Math.max(0, Math.min(leftMs, MAX_TIMER_DELAY_MS));
4092
+ timer = setTimeout(() => {
4093
+ leftMs -= chunk;
4094
+ if (leftMs > 0) {
4095
+ armChunk();
4096
+ return;
4097
+ }
4098
+ fireHardAbort();
4099
+ }, chunk);
4100
+ };
4101
+ const fireHardAbort = () => {
4078
4102
  if (state.fired)
4079
4103
  return;
4080
4104
  state.fired = true;
4081
4105
  state.latenessMs = Math.max(0, Date.now() - scheduledAtMs);
4082
4106
  abortController.abort();
4083
4107
  void harness.abort();
4084
- }, hardMs);
4108
+ };
4109
+ armChunk();
4085
4110
  state.clear = () => clearTimeout(timer);
4086
4111
  }
4087
4112
  return state;
@@ -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 {
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
+ import { assertRetentionPolicy } from "./retention-policy.js";
2
3
  import { uuidv7 } from "../internal/harness.js";
3
4
  import { canAccessAgentRecord, BackgroundAgentStoreError, clearRevivedRowTerminalPayload, REVIVED_ROW_CLEARED_FIELDS, STALE_RUNNING_REAP_ATTRIBUTION, } from "./background-agent-store.js";
4
5
  import { shutdownDebug } from "./shutdown-debug.js";
@@ -163,6 +164,7 @@ export function endDurableClaimLane(core, id) {
163
164
  core.claimingHandles.delete(id);
164
165
  }
165
166
  export async function reapDurableAgentsLane(core, scope, deps, policy) {
167
+ assertRetentionPolicy("reapDurableAgents", policy);
166
168
  const now = policy.now ?? Date.now();
167
169
  if (policy.staleRunningMaxAgeMs !== undefined) {
168
170
  await deps.store.reap(scope, now, { staleRunningMaxAgeMs: policy.staleRunningMaxAgeMs });
@@ -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;
@@ -1,10 +1,10 @@
1
1
  import { homedir } from "node:os";
2
- import { isAbsolute, join, normalize as normalizePath, sep } from "node:path";
2
+ import { join, normalize as normalizePath, posix as posixPath, sep, win32 as winPath } from "node:path";
3
3
  import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
4
4
  import { boundInputHashOf } from "./canonical-json.js";
5
5
  import { inlineUntrusted } from "./untrusted-text.js";
6
6
  import { parsePermissionRule } from "./permission-rules.js";
7
- import { writeTargetPath } from "../tools/fs/safety.js";
7
+ import { isAbsolutePathForm, isWinFormPath, writeTargetPath } from "../tools/fs/safety.js";
8
8
  export const APPROVAL_SETTLED_BY_VALUES = ["human", "timeout", "aborted"];
9
9
  export function isApprovalSettledBy(v) {
10
10
  return typeof v === "string" && APPROVAL_SETTLED_BY_VALUES.includes(v);
@@ -117,6 +117,9 @@ export function createApprovalPolicy(opts) {
117
117
  if (opts.approvalTimeoutMs !== undefined && !Number.isFinite(opts.approvalTimeoutMs)) {
118
118
  throw new Error(`createApprovalPolicy: approvalTimeoutMs must be a finite number of ms (got ${opts.approvalTimeoutMs}) — omit it to wait indefinitely`);
119
119
  }
120
+ if (opts.approvalTimeoutMs !== undefined && opts.approvalTimeoutMs > 2_147_483_647) {
121
+ throw new Error(`createApprovalPolicy: approvalTimeoutMs must not exceed 2147483647ms (~24.8 days) (got ${opts.approvalTimeoutMs}) — a host timer truncates a larger delay and fires immediately, denying every request at once; omit it to wait indefinitely`);
122
+ }
120
123
  const need = new Set(opts.requireApproval);
121
124
  const deny = new Set(opts.deny ?? []);
122
125
  const auto = new Set(opts.autoAllow ?? []);
@@ -526,13 +529,16 @@ export function createTranscriptIntegrityPolicy(opts) {
526
529
  const readAllow = new Set(opts?.readAllow ?? BASH_READONLY_DEFAULT_ALLOW);
527
530
  const shellTools = canonicalToolNameSet(opts?.tools);
528
531
  const dirSegs = dirs.map((d) => pathSegments(d).map(foldPathCase));
529
- const inProtectedDir = (p) => {
530
- const abs = lexicalPath(p, home);
532
+ const inProtectedDir = (p, liveCwd) => {
533
+ const rawRelative = !isAbsolutePathForm(p);
534
+ const joinInFamily = (base, rel) => isWinFormPath(base) ? winPath.join(base, rel) : posixPath.join(base, rel);
535
+ const abs = liveCwd !== undefined && rawRelative ? lexicalPath(joinInFamily(liveCwd, p), home) : lexicalPath(p, home);
531
536
  const segs = pathSegments(abs).map(foldPathCase);
532
- if (isAbsolute(abs)) {
533
- return dirSegs.some((d) => d.length <= segs.length && d.every((s, i) => s === segs[i]));
534
- }
535
- return dirSegs.some((d) => relativeContinuesDirTail(segs, d));
537
+ if (isAbsolutePathForm(abs) && dirSegs.some((d) => d.length <= segs.length && d.every((s, i) => s === segs[i])))
538
+ return true;
539
+ if (!rawRelative)
540
+ return false;
541
+ return dirSegs.some((d) => relativeContinuesDirTail(pathSegments(lexicalPath(p, home)).map(foldPathCase), d));
536
542
  };
537
543
  const referencesProtectedDir = (cmd) => {
538
544
  const hay = foldPathCase(cmd);
@@ -571,18 +577,21 @@ export function createTranscriptIntegrityPolicy(opts) {
571
577
  if (typeof command !== "string")
572
578
  return ALLOW;
573
579
  const cmd = command.normalize("NFC");
574
- if (!referencesProtectedDir(cmd))
580
+ const cwdInsideProtected = req.cwd !== undefined && inProtectedDir(req.cwd);
581
+ if (!referencesProtectedDir(cmd) && !cwdInsideProtected)
575
582
  return ALLOW;
576
583
  const parsed = parseLeadingCommandName(command);
577
584
  if ("name" in parsed && readAllow.has(parsed.name))
578
585
  return ALLOW;
579
586
  return askReason("name" in parsed
580
- ? `\`${parsed.name}\` is not a read-only command`
581
- : "the command references the transcript directory and is not a single read-only command (fail-closed)");
587
+ ? `\`${parsed.name}\` is not a read-only command${cwdInsideProtected ? " and the shell's working directory is inside the transcript directory" : ""}`
588
+ : cwdInsideProtected
589
+ ? "the shell's working directory is inside the transcript directory and this is not a single read-only command (fail-closed)"
590
+ : "the command references the transcript directory and is not a single read-only command (fail-closed)");
582
591
  }
583
592
  if (toolName === "Write" || toolName === "Edit" || toolName === "NotebookEdit") {
584
593
  const p = writeTargetPath(toolName, req.args);
585
- if (typeof p === "string" && inProtectedDir(p)) {
594
+ if (typeof p === "string" && inProtectedDir(p, req.cwd)) {
586
595
  return askReason(`"${p}" is inside the session-transcript directory`);
587
596
  }
588
597
  }
@@ -1,3 +1,4 @@
1
+ import { assertRetentionPolicy } from "./retention-policy.js";
1
2
  export function summarizeWorkflowRun(run) {
2
3
  const latestPhase = run.phases.at(-1);
3
4
  return {
@@ -73,6 +74,7 @@ export class InMemoryWorkflowRunStore {
73
74
  return queryWorkflowRuns(this.runs.values(), scope, opts);
74
75
  }
75
76
  async reap(scope, now, opts) {
77
+ assertRetentionPolicy("WorkflowRunStore.reap", opts);
76
78
  if (opts?.maxAgeMs === undefined && opts?.keep === undefined)
77
79
  return 0;
78
80
  const terminal = [...this.runs.values()].filter((r) => r.scope === scope && isTerminalWorkflowStatus(r.status));
package/dist/index.d.ts CHANGED
@@ -131,7 +131,7 @@ export { parseAllowRuleText, formatAllowRuleText, ruleAdmitsCommand, findAdmitti
131
131
  export { removePersistedRule, applyTombstones, sameScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, type PermissionRuleStore, type PermissionRuleStoreProvider, type StoredAllowRules, type RemoveResult, type PutResult, } from "./core/permission-rule-store.js";
132
132
  export { prepareCardApproval, confirmRuleApproval, type ConfirmResult, type ConfirmRefusalReason, redeemRuleTicket, redeemRuleBatch, prepareCcImport, prepareStarterBatch, mintRuleTicket, STARTER_RULES, InMemoryRuleApprovalRecordStore, type RuleTicket, type RuleCandidate, type RuleApprovalKind, type RuleApprovalRecord, type RuleApprovalRecordStore, type RuleConsentDeps, type RedeemResult, type CcImportLayer, type ImportedSettingsLayer, type ImportPreview, type ImportResult, } from "./core/permission-rule-consent.js";
133
133
  export { FilePermissionRuleStoreProvider } from "./stores/file/permission-rule-store.js";
134
- export { formatHookFeedback, runToolGate, createHookEnvCapabilities, type Hooks, type HookToolContext, type HookEnvCapabilities, type HookToolOutput, type PreToolUseResult, type PostToolUseResult, type UserPromptSubmitResult, type HookToolFailure, type PostToolUseFailureResult, type PostToolBatchCall, type PostToolBatchResult, type PreCompactContext, type PreCompactResult, type PostCompactContext, type StopFailureContext, type PermissionDeniedPayload, type PermissionDeniedSource, } from "./core/hooks.js";
134
+ export { formatHookFeedback, runToolGate, createHookEnvCapabilities, createPreToolUseConstraintPolicy, type Hooks, type HookToolContext, type HookEnvCapabilities, type HookToolOutput, type PreToolUseResult, type PostToolUseResult, type UserPromptSubmitResult, type HookToolFailure, type PostToolUseFailureResult, type PostToolBatchCall, type PostToolBatchResult, type PreCompactContext, type PreCompactResult, type PostCompactContext, type StopFailureContext, type PermissionDeniedPayload, type PermissionDeniedSource, } from "./core/hooks.js";
135
135
  export { InMemoryMemoryStore, composeMemoryBlock, composeLayeredMemoryBlock, normalizeMemorySpec, type NormalizedMemorySpec, type MemorySpecInput, supportsConsolidation, supportsPeriodicConsolidation, firstSentence, expandLexicalTerms, lexicalSearchMatch, type Embedder, classifyPromotable, enforcePromotableWriteGate, guardedMemoryStore, MemoryGateError, detectSecret, enforceSecretWriteGate, enforceStructuredNoteSecretGate, type UtilityGate, type MemoryStore, type MemoryVectorMode, type ScoredMemory, type MemoryNoteHeader, type MemoryNoteRecord, type MemoryNoteType, type StructuredNoteInput, } from "./core/memory.js";
136
136
  export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type MemoryGetDetails, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, type MemoryAnnouncement, type ScanFinding, type MemoryEngineOptions, type MemoryInjection, type MemoryBackend, type MemoryEntry, type MemoryEntryFrontmatter, type MemoryEntryHeader, type ScoredMemoryEntry, type NotePatch, type PatchReport, type MaterializedFile, type MemorySessionHandle, type HarvestReport, type HarvestRejection, type HarvestRejectionCode, type ParsedEntryFile, type ScannedEntryFile, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, type ParsedScopeKey, migrateScope, type MigrateScopeReport, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, type RemoteMemoryFile, type RemoteMemoryBaseline, type RemoteMaterialization, type RemoteHarvestResult, type InboundEntryFinding, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, type MemorySyncCursor, type MemorySyncConflict, type MemorySyncPlan, type RecallSource, syncMemoryScope, type MemorySyncTransport, type MemorySyncRequestBody, type MemorySyncResponseBody, type MemorySyncClientConflict, type SyncMemoryScopeOptions, type MemorySyncClientResult, } from "./core/memory-engine/index.js";
137
137
  export { SHARED_MEMORY_READ_CAP_BYTES, SHARED_MEMORY_LIST_PAGE_SIZE, SharedMemoryStoreError, type SharedMemoryStoreProvider, type SharedMemoryStoreReader, type SharedMemoryStoreInfo, type SharedMemoryDocumentEntry, type SharedMemorySnapshot, type SharedMemoryRequestContext, type MemoryListDetails, type MemoryReadDetails, } from "./core/shared-memory/types.js";