@sema-agent/core 5.22.0 → 5.24.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 (59) hide show
  1. package/CHANGELOG.md +169 -1
  2. package/dist/agents/subagent.js +3 -2
  3. package/dist/core/checkpoint-store.d.ts +38 -3
  4. package/dist/core/checkpoint-store.js +2 -1
  5. package/dist/core/governance-codes.js +3 -0
  6. package/dist/core/hooks.d.ts +69 -2
  7. package/dist/core/hooks.js +100 -15
  8. package/dist/core/memory-engine/engine.d.ts +28 -1
  9. package/dist/core/memory-engine/engine.js +62 -3
  10. package/dist/core/memory-engine/index.d.ts +1 -1
  11. package/dist/core/memory-engine/index.js +1 -1
  12. package/dist/core/memory-engine/layout.d.ts +69 -3
  13. package/dist/core/memory-engine/layout.js +75 -6
  14. package/dist/core/permission-rule-consent.js +2 -1
  15. package/dist/core/permission-rule-org.d.ts +36 -2
  16. package/dist/core/permission-rule-org.js +23 -0
  17. package/dist/core/permission-rule-store.d.ts +25 -14
  18. package/dist/core/permission-rule-store.js +7 -2
  19. package/dist/core/permission-rule-sync.d.ts +8 -0
  20. package/dist/core/permission-rule-sync.js +35 -6
  21. package/dist/core/runner/prepare-task.d.ts +10 -2
  22. package/dist/core/runner/prepare-task.js +120 -11
  23. package/dist/core/runner/runtask.js +46 -1
  24. package/dist/core/runner/session-file-state-replay.js +3 -0
  25. package/dist/core/tool-policy.d.ts +37 -4
  26. package/dist/core/tool-policy.js +49 -19
  27. package/dist/core/tool-result-store.d.ts +17 -1
  28. package/dist/core/tool-result-store.js +79 -4
  29. package/dist/core/trace.d.ts +47 -0
  30. package/dist/core/types.d.ts +45 -5
  31. package/dist/core/wiring-manifest.d.ts +16 -1
  32. package/dist/core/wiring-manifest.js +7 -1
  33. package/dist/index.d.ts +18 -10
  34. package/dist/index.js +5 -3
  35. package/dist/orchestration/goal.d.ts +10 -0
  36. package/dist/orchestration/goal.js +6 -5
  37. package/dist/stores/file/adoption/adopt.d.ts +146 -0
  38. package/dist/stores/file/adoption/adopt.js +611 -0
  39. package/dist/stores/file/adoption/marker.d.ts +202 -0
  40. package/dist/stores/file/adoption/marker.js +205 -0
  41. package/dist/stores/file/background-agent-store.js +2 -0
  42. package/dist/stores/file/checkpoint-store.js +2 -0
  43. package/dist/stores/file/file-snapshot-store.js +2 -0
  44. package/dist/stores/file/index.d.ts +2 -0
  45. package/dist/stores/file/index.js +4 -0
  46. package/dist/stores/file/mailbox-store.js +2 -0
  47. package/dist/stores/file/memory-store.js +2 -0
  48. package/dist/stores/file/session-policy-store.d.ts +11 -1
  49. package/dist/stores/file/session-policy-store.js +9 -2
  50. package/dist/stores/file/session-store.js +2 -0
  51. package/dist/stores/file/task-list-store.js +2 -0
  52. package/dist/stores/file/tool-result-store.js +2 -0
  53. package/dist/stores/file/usage-window-store.js +2 -0
  54. package/dist/stores/file/workflow-journal-store.js +2 -0
  55. package/dist/stores/file/workflow-run-store.js +2 -0
  56. package/dist/tools/fs/bash-readonly-classifier.js +59 -10
  57. package/dist/tools/fs/fs-bash.js +7 -4
  58. package/dist/tools/monitor.js +3 -3
  59. package/package.json +3 -2
@@ -1,5 +1,6 @@
1
1
  import { parseAllowRuleText } from "./permission-rule-model.js";
2
2
  import { applyTombstones, errText, joinFrontiers, joinRuleStates, normalizePersistedRule, ruleSyncVector, sameScope, screenRuleSyncState, writerOf, } from "./permission-rule-store.js";
3
+ import { emitTrace } from "./trace.js";
3
4
  export const PERMISSION_RULE_SYNC_PATH = "/v1/permission-rules/sync";
4
5
  export const LOCAL_OWNER_UNSYNCABLE_CODE = "permission_rules.local_owner_unsyncable";
5
6
  function refuseLocalOwnerSync() {
@@ -19,6 +20,34 @@ export async function syncPermissionRules(opts) {
19
20
  if (typeof opts.principal !== "string" || opts.principal === "") {
20
21
  throw new Error("syncPermissionRules requires a verified principal — an unauthenticated deployment has no cloud bucket to sync");
21
22
  }
23
+ const traceClock = opts.now ?? Date.now;
24
+ const disclose = (result) => {
25
+ const tracer = opts.tracer;
26
+ if (tracer === undefined)
27
+ return result;
28
+ for (const r of result.resurrected) {
29
+ emitTrace(tracer, () => ({
30
+ kind: "permission.rule_sync_resurrected",
31
+ version: 1,
32
+ principal: opts.principal,
33
+ rule: r.rule,
34
+ scopeKind: r.scope.kind,
35
+ ts: traceClock(),
36
+ }));
37
+ }
38
+ for (const d of result.dropped) {
39
+ emitTrace(tracer, () => ({
40
+ kind: "permission.rule_sync_dropped",
41
+ version: 1,
42
+ principal: opts.principal,
43
+ rule: d.rule,
44
+ scopeKind: d.scope.kind,
45
+ reason: d.reason,
46
+ ts: traceClock(),
47
+ }));
48
+ }
49
+ return result;
50
+ };
22
51
  const store = opts.provider.forPrincipal(opts.principal);
23
52
  const writer = writerOf(store);
24
53
  if (writer === undefined) {
@@ -111,7 +140,7 @@ export async function syncPermissionRules(opts) {
111
140
  res = await writer.apply(delta, { expectedRev: current.rev });
112
141
  }
113
142
  catch (err) {
114
- return {
143
+ return disclose({
115
144
  ok: false,
116
145
  pushed: { addDots: raw.rules.reduce((n, r) => n + r.adds.length, 0), tombstones: raw.tombstones.length },
117
146
  landed: { newAdds: 0, newTombstones: 0 },
@@ -119,7 +148,7 @@ export async function syncPermissionRules(opts) {
119
148
  dropped,
120
149
  rev: current.rev,
121
150
  warnings: [...warnings, `the store refused the sync landing: ${errText(err)} — nothing landed, the local state is unchanged`],
122
- };
151
+ });
123
152
  }
124
153
  if ("conflict" in res) {
125
154
  current = await writer.readRaw();
@@ -156,7 +185,7 @@ export async function syncPermissionRules(opts) {
156
185
  if (!dropped.some((x) => x.dot.actor === q.dot.actor && x.dot.counter === q.dot.counter && x.reason === q.reason))
157
186
  dropped.push(q);
158
187
  }
159
- return {
188
+ return disclose({
160
189
  ok: dropped.length === 0 && warnings.length === 0,
161
190
  pushed: { addDots: raw.rules.reduce((n, r) => n + r.adds.length, 0), tombstones: raw.tombstones.length },
162
191
  landed: { newAdds, newTombstones },
@@ -164,9 +193,9 @@ export async function syncPermissionRules(opts) {
164
193
  dropped,
165
194
  rev: landedRaw.rev,
166
195
  ...(warnings.length > 0 ? { warnings } : {}),
167
- };
196
+ });
168
197
  }
169
- return {
198
+ return disclose({
170
199
  ok: false,
171
200
  pushed: { addDots: raw.rules.reduce((n, r) => n + r.adds.length, 0), tombstones: raw.tombstones.length },
172
201
  landed: { newAdds: 0, newTombstones: 0 },
@@ -174,7 +203,7 @@ export async function syncPermissionRules(opts) {
174
203
  dropped,
175
204
  rev: current.rev,
176
205
  warnings: [...warnings, `optimistic-concurrency retries exhausted after ${SYNC_MAX_ATTEMPTS} attempts — nothing landed, the local state is unchanged`],
177
- };
206
+ });
178
207
  }
179
208
  function isDot(v) {
180
209
  const d = v;
@@ -8,10 +8,10 @@ import type { HarvestReport, MemorySessionHandle } from "../memory-engine/types.
8
8
  import { StoredSession } from "../session.js";
9
9
  import type { SessionStore } from "../session.js";
10
10
  import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
11
- import type { OnAsk, ToolPolicy } from "../tool-policy.js";
11
+ import type { OnAsk, ToolCallRequest, ToolPolicy } from "../tool-policy.js";
12
12
  import { type ActiveSkillFrame } from "./active-skill-scope.js";
13
13
  import type { SessionPermissionRules } from "../session-policy-store.js";
14
- import { type Hooks } from "../hooks.js";
14
+ import { type Hooks, type OrgGateVerdict } from "../hooks.js";
15
15
  import { type RecoveredOrphan } from "../session-reconcile.js";
16
16
  import { CacheBreakDetector, type ToolFingerprintInput } from "../cache-break-detector.js";
17
17
  import { type BrainCallGuardrailRef } from "../../brain/timeout.js";
@@ -214,6 +214,14 @@ export interface Prepared {
214
214
  /** RB-63: the deployment's own caller policy, re-checked on a durable resume ONLY when the approver
215
215
  * rewrote the pending call's args (see the composition site for why the edit case is special). */
216
216
  basePolicyForResumeEdit?: ToolPolicy;
217
+ /** design/182 §7 — the ORG adjudication face, re-resolved on a durable RESUME before an approved
218
+ * pending call executes. The resume path bypasses the harness gate by design (a human already
219
+ * adjudicated the checkpointed call), which is exactly where org policy skew is most likely: the
220
+ * suspend may have outlived the snapshot revision that was current when it was minted. Present only
221
+ * on a governed deployment. */
222
+ permissionRuleOrg?: {
223
+ adjudicate: (req: ToolCallRequest) => Promise<OrgGateVerdict>;
224
+ };
217
225
  /** Removes the `spec.signal` abort listener on task end (else a long-lived signal leaks listeners). */
218
226
  releaseSignal: () => void;
219
227
  /**
@@ -30,6 +30,7 @@ import { policyAskClassOf } from "../ask-class.js";
30
30
  import { emitTrace } from "../trace.js";
31
31
  import { createSessionRulePolicy } from "./session-rule-policy.js";
32
32
  import { cloneObserverInput, createHookEnvCapabilities, createPreToolUseConstraintPolicy, formatHookFeedback, runToolGate } from "../hooks.js";
33
+ import { orgRuleVerdictFor } from "../permission-rule-org.js";
33
34
  import { reconcileInterruptedSession } from "../session-reconcile.js";
34
35
  import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-detector.js";
35
36
  import { reservedCollisions, reservedFor } from "../../brain/request-params.js";
@@ -84,7 +85,7 @@ import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-
84
85
  import { createLspTool, gitCheckIgnoreFilter, resolveLspPath } from "../lsp.js";
85
86
  import { resolveKey } from "../../tools/fs/safety.js";
86
87
  import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
87
- import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
88
+ import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
88
89
  import { boundInputHashOf } from "../canonical-json.js";
89
90
  import { countElicitOptIns, deriveAskEffective, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
90
91
  import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
@@ -3144,12 +3145,22 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3144
3145
  };
3145
3146
  const permissionRuleLane = (() => {
3146
3147
  const provider = deps.permissionRuleStore;
3148
+ const localOwnerDeclared = deps.localOwnerRules === true;
3149
+ if (localOwnerDeclared) {
3150
+ if (provider === undefined) {
3151
+ throw new Error("RunnerDeps.localOwnerRules is declared but no permissionRuleStore provider is wired — there is no bucket for the local owner to hold rules in; refusing rather than running as if the declaration were absent");
3152
+ }
3153
+ if (provider.forLocalOwner === undefined) {
3154
+ throw new Error("RunnerDeps.localOwnerRules is declared but the wired permissionRuleStore provider implements no forLocalOwner() face — a provider without a local-owner bucket cannot honor the declaration; refusing rather than silently resolving zero rules");
3155
+ }
3156
+ }
3147
3157
  if (provider === undefined)
3148
3158
  return undefined;
3149
3159
  const root = taskRootPath;
3150
3160
  return {
3151
3161
  admits: async (req) => {
3152
- if (spec.principal === undefined || spec.principal === "")
3162
+ const anonymous = spec.principal === undefined || spec.principal === "";
3163
+ if (anonymous && !localOwnerDeclared)
3153
3164
  return undefined;
3154
3165
  if (req.toolName !== PERSISTED_RULE_TOOL)
3155
3166
  return undefined;
@@ -3158,7 +3169,10 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3158
3169
  return undefined;
3159
3170
  let listed;
3160
3171
  try {
3161
- listed = await provider.forPrincipal(spec.principal).list();
3172
+ listed = anonymous
3173
+ ?
3174
+ await provider.forLocalOwner().list()
3175
+ : await provider.forPrincipal(spec.principal).list();
3162
3176
  }
3163
3177
  catch (err) {
3164
3178
  emitTrace(deps.tracer, () => ({
@@ -3174,6 +3188,31 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3174
3188
  },
3175
3189
  };
3176
3190
  })();
3191
+ const permissionRuleOrgLane = (() => {
3192
+ const overlay = deps.permissionRuleOrg;
3193
+ if (overlay === undefined)
3194
+ return undefined;
3195
+ return {
3196
+ adjudicate: async (req) => {
3197
+ if (req.toolName === ASK_USER_QUESTION_TOOL_NAME && questionToolMounted)
3198
+ return { status: "available" };
3199
+ let resolution;
3200
+ try {
3201
+ resolution = await overlay.resolve();
3202
+ }
3203
+ catch (err) {
3204
+ return { status: "unavailable", disclosures: [`the org rule overlay threw: ${err instanceof Error ? err.message : String(err)}`] };
3205
+ }
3206
+ if (resolution.status === "unavailable")
3207
+ return { status: "unavailable", disclosures: resolution.disclosures };
3208
+ const command = req.args?.command;
3209
+ if (req.toolName !== PERSISTED_RULE_TOOL || typeof command !== "string")
3210
+ return { status: "available" };
3211
+ const verdict = orgRuleVerdictFor(resolution.rules, { tool: req.toolName, command });
3212
+ return verdict === undefined ? { status: "available" } : { status: "available", verdict };
3213
+ },
3214
+ };
3215
+ })();
3177
3216
  const ruleSuggestionsOf = (toolName, args) => {
3178
3217
  if (permissionRuleLane === undefined || toolName !== PERSISTED_RULE_TOOL)
3179
3218
  return {};
@@ -3621,6 +3660,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3621
3660
  sessionDurability: resolveDeclaredDurability(sessions, "sessionStore"),
3622
3661
  backgroundAgentStoreWired: deps.backgroundAgentStore !== undefined,
3623
3662
  permissionRuleStoreWired: deps.permissionRuleStore !== undefined,
3663
+ permissionRuleSyncWired: deps.permissionRuleSyncWired === true,
3664
+ permissionRuleOrgGoverned: deps.permissionRuleOrg !== undefined,
3624
3665
  hostChildEventSinkWired: deps.onBackgroundChildEvent !== undefined,
3625
3666
  lockedConfigWired: deps.lockedConfig !== undefined,
3626
3667
  complianceWired: deps.compliancePostureResolver !== undefined,
@@ -3837,8 +3878,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3837
3878
  return { ok: false };
3838
3879
  if (memoryEngineSession)
3839
3880
  await memoryEngineSession.harvest("checkpoint");
3840
- try {
3841
- await checkpointStore.put(token, cp);
3881
+ const announceCommittedScreeningPark = () => {
3842
3882
  const committedCount = cp.state.inheritedGate?.parentConstraintCount;
3843
3883
  if (cp.state.inheritedGate?.requiresParentConstraint === true &&
3844
3884
  !screeningParkDisclosedRef.done &&
@@ -3855,10 +3895,59 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3855
3895
  catch {
3856
3896
  }
3857
3897
  }
3898
+ };
3899
+ const confirmPutOutcome = async (putErr) => {
3900
+ if (putErr?.code === "checkpoint.already_exists")
3901
+ return "absent";
3902
+ try {
3903
+ const row = await checkpointStore.get(token);
3904
+ if (row == null)
3905
+ return "absent";
3906
+ const sameCall = (a, b) => a.kind === b.kind && (a.kind !== "tool_approval" || b.kind !== "tool_approval" || a.toolCallId === b.toolCallId);
3907
+ const isOurs = row.scope === cp.scope && row.sessionId === cp.sessionId && row.leafId === cp.leafId && sameCall(row.pendingAction, cp.pendingAction);
3908
+ return isOurs ? "committed" : "absent";
3909
+ }
3910
+ catch (readErr) {
3911
+ deps.onError?.(readErr, { phase: "config", sessionId });
3912
+ return "unknown";
3913
+ }
3914
+ };
3915
+ try {
3916
+ await checkpointStore.put(token, cp);
3917
+ announceCommittedScreeningPark();
3858
3918
  return { ok: true };
3859
3919
  }
3860
3920
  catch (putErr) {
3861
3921
  deps.onError?.(putErr, { phase: "config", sessionId });
3922
+ const confirmed = await confirmPutOutcome(putErr);
3923
+ if (confirmed === "committed") {
3924
+ try {
3925
+ deps.onError?.(new Error(`durable approval checkpoint: the store's write reported a failure but the row EXISTS — the commit ` +
3926
+ `landed and its acknowledgement was lost (a network backend can complete the INSERT and drop the ` +
3927
+ `connection before replying). The run suspends on that row rather than continuing, which would leave ` +
3928
+ `a redeemable pending checkpoint behind. This backend's acknowledgement path is lossy — the operator ` +
3929
+ `face carries the store's own message.`), { phase: "degraded", sessionId, classification: "checkpoint-put-confirmation-lost" });
3930
+ }
3931
+ catch {
3932
+ }
3933
+ announceCommittedScreeningPark();
3934
+ return { ok: true };
3935
+ }
3936
+ if (confirmed === "unknown") {
3937
+ const unknownReason = "the approval checkpoint's state cannot be established (the store rejected the write and then could not be read back; a committed-but-unacknowledged row may exist) — the run is stopped rather than continued past an approval whose durable record is unknown";
3938
+ try {
3939
+ deps.onError?.(new Error(`durable approval checkpoint: ${unknownReason}`), {
3940
+ phase: "degraded",
3941
+ sessionId,
3942
+ classification: "checkpoint-put-outcome-unknown",
3943
+ });
3944
+ }
3945
+ catch {
3946
+ }
3947
+ abortController.abort();
3948
+ void harness.abort();
3949
+ return { ok: false, reason: unknownReason };
3950
+ }
3862
3951
  const reason = "the approval checkpoint could not be persisted (the store rejected the write; the deployment's error face carries the store's own message)";
3863
3952
  if (remoteEnv !== undefined && remoteHandle?.snapshotId !== undefined) {
3864
3953
  const { outcome: back, attempts: backAttempts } = await restoreWorkspaceWithRetry(remoteEnv, remoteHandle.snapshotId, {
@@ -4147,7 +4236,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4147
4236
  }
4148
4237
  };
4149
4238
  const suspendAsk = parkLaneArmed && checkpointStore !== undefined
4150
- ? async (req, postHookArgs, safety, liveFaceUnavailable) => {
4239
+ ? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval) => {
4151
4240
  const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
4152
4241
  if (syncFirstEligible &&
4153
4242
  runtimeCaps?.forceDurableGate !== true &&
@@ -4256,12 +4345,15 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4256
4345
  ...(effectiveShellGate !== "off" ? { shellGateDoctrine: effectiveShellGate } : {}),
4257
4346
  });
4258
4347
  gate =
4259
- safety !== undefined
4348
+ safety !== undefined || realApproval !== undefined
4260
4349
  ? {
4261
4350
  kind: "irreversible_ask",
4262
- reason: `human approval required before safety-tightened tool "${req.toolName}"`,
4351
+ reason: safety !== undefined
4352
+ ? `human approval required before safety-tightened tool "${req.toolName}"`
4353
+ : `real human approval required for tool "${req.toolName}" (non-budgetable: ${realApproval.origin})`,
4263
4354
  toolName: req.toolName,
4264
- safetyAxis: safety,
4355
+ ...(safety !== undefined ? { safetyAxis: safety } : {}),
4356
+ ...(realApproval !== undefined ? { realApproval } : {}),
4265
4357
  riskDescriptor,
4266
4358
  }
4267
4359
  : durableApproval
@@ -4284,7 +4376,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4284
4376
  const mintedAt = Date.now();
4285
4377
  cp = {
4286
4378
  token,
4287
- version: f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : approvalLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
4379
+ version: realApproval !== undefined ? REAL_APPROVAL_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : approvalLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
4288
4380
  scope,
4289
4381
  sessionId,
4290
4382
  leafId,
@@ -4422,6 +4514,23 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4422
4514
  },
4423
4515
  }
4424
4516
  : {}),
4517
+ ...(permissionRuleOrgLane
4518
+ ? {
4519
+ orgRules: {
4520
+ adjudicate: permissionRuleOrgLane.adjudicate,
4521
+ contentAskToolMounted: questionToolMounted,
4522
+ onUnavailable: (info) => emitTrace(deps.tracer, () => ({
4523
+ kind: "permission.org_snapshot_unavailable",
4524
+ version: 1,
4525
+ taskId: spec.taskId ?? sessionId,
4526
+ toolName: info.toolName,
4527
+ toolCallId: info.toolCallId,
4528
+ message: info.message,
4529
+ ts: Date.now(),
4530
+ })),
4531
+ },
4532
+ }
4533
+ : {}),
4425
4534
  isMarkedUnresolvable: (toolCallId) => inheritedUnavailableAsks.has(toolCallId),
4426
4535
  ...(sandboxAdmissionArmed
4427
4536
  ? {
@@ -4750,7 +4859,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4750
4859
  : undefined;
4751
4860
  overheadState.promptChars = systemPrompt.length;
4752
4861
  const preparedHolder = {};
4753
- 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 } : {}) });
4862
+ 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 } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), 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 } : {}) });
4754
4863
  const prepared = buildPrepared();
4755
4864
  preparedHolder.current = prepared;
4756
4865
  return prepared;
@@ -1,6 +1,6 @@
1
1
  import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
2
2
  import { snapshotActorAssertion } from "../../internal/llm.js";
3
- import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, MAX_SUPPORTED_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
3
+ import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
4
4
  import { engineVersion } from "../version.js";
5
5
  import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
6
6
  import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
@@ -9,6 +9,7 @@ import { ASK_USER_QUESTION_TOOL_NAME, canonicalizeCapturedPlainData, classifyQue
9
9
  import { boundInputHashOf } from "../canonical-json.js";
10
10
  import { computeCostMicroUsd, modelCostToPricing } from "../pricing.js";
11
11
  import { emitTrace } from "../trace.js";
12
+ import { ORG_ADJUDICATION_TIMEOUT_MS, settleOrgVerdictWithin } from "../permission-rule-org.js";
12
13
  import { emitTaskOutcome } from "../task-outcome.js";
13
14
  import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
14
15
  import { primaryActivityArg } from "../arg-summary.js";
@@ -3703,6 +3704,21 @@ export class Runner {
3703
3704
  if (checkpointVersionOf(cp) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
3704
3705
  throw new CheckpointError("checkpoint.unsupported_version", `checkpoint version ${checkpointVersionOf(cp)} is newer than this worker supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION})`);
3705
3706
  }
3707
+ const preCasGateBit = cp.gate.kind === "irreversible_ask" ? cp.gate.realApproval : undefined;
3708
+ const preCasBitWellFormed = preCasGateBit !== undefined &&
3709
+ typeof preCasGateBit === "object" &&
3710
+ (preCasGateBit.origin === "org_rule" ||
3711
+ preCasGateBit.origin === "org_unavailable" ||
3712
+ preCasGateBit.origin === "policy");
3713
+ if (checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION ? !preCasBitWellFormed : preCasGateBit !== undefined) {
3714
+ throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION
3715
+ ? `a v${checkpointVersionOf(cp)} checkpoint must carry a well-formed non-budgetable realApproval gate bit (origin org_rule/org_unavailable/policy) on an irreversible_ask gate — this row does not; refusing to resume a damaged real-approval row (corruption / downgrade guard), the checkpoint stays pending`
3716
+ : `a v${checkpointVersionOf(cp)} checkpoint carries a realApproval gate bit no release of that version ever minted — refusing to honor a fabricated origin (corruption / forgery guard), the checkpoint stays pending`);
3717
+ }
3718
+ if ((preCasGateBit?.origin === "org_rule" || preCasGateBit?.origin === "org_unavailable") &&
3719
+ this.deps.permissionRuleOrg === undefined) {
3720
+ throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's approval was minted under organization governance (${preCasGateBit.origin}) and this worker has no org adjudication wiring (permissionRuleOrg) — a governed approval may only be redeemed where governance can be enforced; the checkpoint stays pending, resume it on an org-wired worker`);
3721
+ }
3706
3722
  const retiredWalltimeTotal = cp.resourceLedger?.totalWalltimeSec;
3707
3723
  if (retiredWalltimeTotal !== undefined) {
3708
3724
  throw new CheckpointError("checkpoint.walltime_axis_retired", "this checkpoint's ledger carries a cross-slice WALL-CLOCK allocation (resourceLedger.totalWalltimeSec), an axis this engine version retired — " +
@@ -4008,6 +4024,35 @@ export class Runner {
4008
4024
  return;
4009
4025
  }
4010
4026
  }
4027
+ const gateRealApproval = resume.cp.gate.kind === "irreversible_ask" ? resume.cp.gate.realApproval : undefined;
4028
+ const gateOrgGoverned = gateRealApproval?.origin === "org_rule" || gateRealApproval?.origin === "org_unavailable";
4029
+ if (gateOrgGoverned && prepared.permissionRuleOrg === undefined) {
4030
+ const unwiredDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: its approval was minted under organization governance (${gateRealApproval.origin}), and this worker has no org adjudication wiring — a governed approval may only be redeemed where governance can be enforced. This approval is spent; re-issue the call on an org-wired worker.`);
4031
+ emitEnd(true, { content: unwiredDenial });
4032
+ const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, unwiredDenial, true));
4033
+ emitCommitted(eid, "toolResult", pendingAction.toolCallId);
4034
+ return;
4035
+ }
4036
+ if (prepared.permissionRuleOrg !== undefined) {
4037
+ const orgVerdict = prepared.permissionRuleOrg
4038
+ .adjudicate({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId })
4039
+ .catch(() => ({ status: "unavailable", disclosures: ["the org adjudication face threw on resume"] }));
4040
+ 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)`] }, { signal: prepared.abortController.signal, timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
4041
+ const blocked = org.status === "unavailable"
4042
+ ? gateRealApproval?.origin === "org_unavailable"
4043
+ ? undefined
4044
+ : "this deployment is org-governed and cannot currently adjudicate against an organization policy snapshot"
4045
+ : org.verdict?.behavior === "deny"
4046
+ ? `an organization policy rule (${org.verdict.rule}) denies it`
4047
+ : undefined;
4048
+ if (blocked !== undefined) {
4049
+ const orgDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${blocked}. This approval is spent — the call has to be re-issued and approved again once organization policy permits it.`);
4050
+ emitEnd(true, { content: orgDenial });
4051
+ const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, orgDenial, true));
4052
+ emitCommitted(eid, "toolResult", pendingAction.toolCallId);
4053
+ return;
4054
+ }
4055
+ }
4011
4056
  if (prepared.denyNarrowingPolicy) {
4012
4057
  const narrowed = refuseOutOfContractDecision(await prepared.denyNarrowingPolicy.check({
4013
4058
  toolName: pendingAction.toolName,
@@ -1,4 +1,5 @@
1
1
  import { isAbsolutePathForm } from "../../tools/fs/safety.js";
2
+ import { isOffloadedDetailReplacement } from "../tool-result-store.js";
2
3
  const READ_TOOL = "Read";
3
4
  const WRITE_TOOL = "Write";
4
5
  const RETRACTING_RESULTS = [
@@ -50,6 +51,8 @@ export function wholeFileRecordsFromTranscript(messages) {
50
51
  const content = isRead ? wholeFileFromReadCard(rest) : typeof rest.content === "string" ? rest.content : undefined;
51
52
  if (content === undefined)
52
53
  continue;
54
+ if (isOffloadedDetailReplacement(content))
55
+ continue;
53
56
  byPath.set(filePath, { path: filePath, content, at: m.timestamp });
54
57
  }
55
58
  return [...byPath.values()];
@@ -69,8 +69,16 @@ export interface ToolCallRequest {
69
69
  /** Where a decision came from, for audit (design/37). `"sandbox"` (F-012 L2) marks an allow the
70
70
  * sandbox-admission leg resolved: the deployment declared an isolated execution env, every surviving
71
71
  * ask on the call was engine-classified sandbox-local, and the call crosses no declared boundary —
72
- * disclosed durably as `permission.sandbox_admitted`. */
73
- export type DecisionReason = "rule" | "mode" | "hook" | "safety" | "classifier" | "persisted_rule" | "sandbox";
72
+ * disclosed durably as `permission.sandbox_admitted`.
73
+ *
74
+ * design/182 §7 adds the two ORG-layer words, both TIGHTENING-only:
75
+ * - `"org_rule"` — an organization policy rule spoke (a deny, or an ask that no configuration can
76
+ * dismiss). Distinct from `"rule"` (a deployment `ToolPolicy` rule) because the AUTHORITY differs:
77
+ * an org rule comes from the org's server-published snapshot, not from this deployment's policy.
78
+ * - `"org_unavailable"` — an org-governed deployment could not adjudicate against a snapshot, so the
79
+ * whole decision boundary failed closed (see `ORG_UNAVAILABLE_DECISION_REASON`, the single
80
+ * spelling this word is minted from). */
81
+ export type DecisionReason = "rule" | "mode" | "hook" | "safety" | "classifier" | "persisted_rule" | "sandbox" | "org_rule" | "org_unavailable";
74
82
  /**
75
83
  * WHO (or what) ENDED an approval — the machine-readable twin of a settlement's human-readable text,
76
84
  * so a consumer tells "a person decided this" from "nobody answered" without prose-matching a sentence.
@@ -78,10 +86,20 @@ export type DecisionReason = "rule" | "mode" | "hook" | "safety" | "classifier"
78
86
  * what ended the WAIT.
79
87
  *
80
88
  * - `"human"` — a person, or the approver acting for one, returned a final verdict (allow or deny).
81
- * - `"timeout"` — the configured approval window elapsed with no answer.
89
+ * - `"timeout"` — an approval window elapsed with no answer.
82
90
  * - `"aborted"` — every other NON-HUMAN end: the task aborted, the approver threw or reported nobody
83
91
  * reachable, the decision arrived out of contract, a store or transport gave way, retries ran out.
84
92
  *
93
+ * **Which windows `"timeout"` speaks for** (#114①, 2026-08-09 — the promise this note used to make was
94
+ * wider than the code): the engine stamps it at the waits IT owns — `createApprovalPolicy`'s
95
+ * `approvalTimeoutMs` window, and the durable park's TTL. The SYNCHRONOUS `onAsk` leg is not one of
96
+ * them: there the deployment owns the window (the engine starts no timer for a callback it does not
97
+ * schedule), so an unanswered card and a refused one arrive as the same `false` and the engine records
98
+ * `"human"` rather than inventing a cause it did not observe. A host that DOES time its own card out
99
+ * can say so — {@link AskOutcome}'s object arm carries an optional `settledBy` for exactly this — but a
100
+ * host that does not is indistinguishable, by construction. Read an absent `"timeout"` as "no window
101
+ * the engine owns elapsed", never as "nobody's window elapsed".
102
+ *
85
103
  * The three words are exhaustive and mutually exclusive over the ways an approval can end, and the
86
104
  * minimum discrimination a consumer needs — someone refused vs nobody answered — is `"human"` vs the
87
105
  * other two.
@@ -694,17 +712,32 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
694
712
  * untyped bridge is a fail-closed deny naming the defect (the historical truthy leniency was a
695
713
  * fail-open on the security face with no live producer).
696
714
  * - `"unavailable"` — the G1 per-ask routing verdict (see {@link OnAsk}).
697
- * - `{ allow, updatedInput? }` — verdict PLUS an operator EDIT of the presented args
715
+ * - `{ allow, updatedInput?, settledBy? }` — verdict PLUS an operator EDIT of the presented args
698
716
  * (whole-replacement form, e.g. ctrl+g "edit script in $EDITOR"): the human approved a MODIFIED
699
717
  * action, and executing the un-edited args would betray that consent. `allow` folds STRICTLY
700
718
  * (`allow === true`) — an object arm is a deliberate caller, so no truthy leniency — and
701
719
  * `updatedInput` is honored only on allow (a deny edit never executes anything). The edit is
702
720
  * applied as the resolved decision's own rewrite: the gate re-validates it against the tool
703
721
  * schema exactly like a hook/policy rewrite.
722
+ *
723
+ * #114② (2026-08-09) — `settledBy` on the object arm is the SYNCHRONOUS leg's only channel for saying
724
+ * what ended the wait. This leg's window belongs to the HOST (`onAsk` is a callback the deployment
725
+ * owns; the engine starts no timer for it), so a host that timed its own approval card out could
726
+ * previously only report a plain `false`, which the engine correctly recorded as `"human"` — a person
727
+ * refusing. The two words the host may self-report:
728
+ * - `"human"` — a person answered (identical to omitting the field);
729
+ * - `"timeout"` — the host's own approval window elapsed with no answer. Legal ONLY with
730
+ * `allow: false`: a window that elapsed cannot be the thing that approved an action, so
731
+ * `{allow: true, settledBy: "timeout"}` is a contradiction and is refused fail-closed (the same
732
+ * narrowing the `PermissionResult` allow arm already spells in its type).
733
+ * `"aborted"` is deliberately NOT accepted here — that word names the engine's OWN fail-closed ends
734
+ * (abort, throw, unavailable, out-of-contract value), each already stamped at its own arm, and a
735
+ * self-reported one would let a host relabel its refusal as an engine failure.
704
736
  */
705
737
  export type AskOutcome = boolean | "unavailable" | {
706
738
  allow: boolean;
707
739
  updatedInput?: unknown;
740
+ settledBy?: Extract<ApprovalSettledBy, "human" | "timeout">;
708
741
  };
709
742
  /**
710
743
  * ruled 2026-08-04 — forward an approver into a delegated child, stamping every ask it raises with the
@@ -122,12 +122,9 @@ export function createAllowDenyPolicy(opts) {
122
122
  if (entry.startsWith("mcp__")) {
123
123
  const segments = entry.slice("mcp__".length).split("__");
124
124
  if (segments.some((seg) => seg.length === 0)) {
125
- invalid.push({
126
- entry,
127
- list,
128
- message: `"${entry}" is a malformed MCP tool name (empty segment) — it can never match any mounted tool. ` +
129
- `Use \`mcp__<server>\` for every tool of a server, or \`mcp__<server>__<tool>\` for one tool.`,
130
- });
125
+ const lesson = `a malformed MCP tool name (empty segment) — it can never match any mounted tool. ` +
126
+ `Use \`mcp__<server>\` for every tool of a server, or \`mcp__<server>__<tool>\` for one tool.`;
127
+ invalid.push({ entry, list, message: `"${entry}" is ${lesson}`, lesson });
131
128
  continue;
132
129
  }
133
130
  }
@@ -135,14 +132,11 @@ export function createAllowDenyPolicy(opts) {
135
132
  kept.push(entry);
136
133
  continue;
137
134
  }
138
- invalid.push({
139
- entry,
140
- list,
141
- message: `"${entry}" is a rule CONTENT form, not a tool name a name set matches raw tool names, so this entry ` +
142
- `can never match any mounted tool (in an allow list it removes the tool entirely). Use the tool NAME here, ` +
143
- `and route the narrowing through the lane that speaks this grammar: parameter rules via ` +
144
- `createPermissionRulePolicy, Bash command prefixes via the persisted allow-rule lane.`,
145
- });
135
+ const lesson = `a rule CONTENT form, not a tool name — a name set matches raw tool names, so this entry ` +
136
+ `can never match any mounted tool (in an allow list it removes the tool entirely). Use the tool NAME here, ` +
137
+ `and route the narrowing through the lane that speaks this grammar: parameter rules via ` +
138
+ `createPermissionRulePolicy, Bash command prefixes via the persisted allow-rule lane.`;
139
+ invalid.push({ entry, list, message: `"${entry}" is ${lesson}`, lesson });
146
140
  }
147
141
  return kept;
148
142
  };
@@ -150,8 +144,16 @@ export function createAllowDenyPolicy(opts) {
150
144
  const screenedDeny = screen(opts.deny, "deny");
151
145
  if (invalid.length > 0) {
152
146
  if ((opts.onInvalidName ?? "throw") === "throw") {
147
+ const byLesson = new Map();
148
+ for (const i of invalid) {
149
+ const group = byLesson.get(i.lesson) ?? [];
150
+ group.push({ entry: i.entry, list: i.list });
151
+ byLesson.set(i.lesson, group);
152
+ }
153
153
  const e = new Error(`createAllowDenyPolicy: ${invalid.length} entr${invalid.length === 1 ? "y is" : "ies are"} not tool name(s):\n` +
154
- invalid.map((i) => ` [${i.list}] ${i.message}`).join("\n"));
154
+ [...byLesson.entries()]
155
+ .map(([lesson, group]) => ` Each of the following is ${lesson}\n` + group.map((g) => ` [${g.list}] "${g.entry}"`).join("\n"))
156
+ .join("\n"));
155
157
  e.code = "config.invalid_tool_name_set";
156
158
  e.issues = invalid;
157
159
  throw e;
@@ -844,12 +846,40 @@ export async function resolveAsk(req, onAsk, signal) {
844
846
  };
845
847
  }
846
848
  if (typeof ok === "object" && ok !== null) {
847
- if (ok.allow !== true) {
848
- return { action: "deny", message: `approval denied for "${req.toolName}": ${req.message}`, decisionReason: "mode", settledBy: "human" };
849
+ const supplied = ok.settledBy;
850
+ const allowed = ok.allow;
851
+ const suppliedEdit = ok.updatedInput;
852
+ if (supplied !== undefined && supplied !== "human" && supplied !== "timeout") {
853
+ return {
854
+ action: "deny",
855
+ message: `the approver for "${req.toolName}" reported settledBy "${typeof supplied === "string" ? containThrownText(supplied) : supplied === null ? "null" : typeof supplied}", which is outside what a synchronous ` +
856
+ `approver may self-report — it is exactly "human" or "timeout" (or omitted); denied fail-closed`,
857
+ decisionReason: "mode",
858
+ settledBy: "aborted",
859
+ };
860
+ }
861
+ if (supplied === "timeout" && allowed === true) {
862
+ return {
863
+ action: "deny",
864
+ message: `the approver for "${req.toolName}" returned an allow settled by "timeout" — an elapsed approval window cannot be ` +
865
+ `what approved a call; denied fail-closed (report timeout with allow:false, or allow with settledBy "human"/omitted)`,
866
+ decisionReason: "mode",
867
+ settledBy: "timeout",
868
+ };
869
+ }
870
+ if (allowed !== true) {
871
+ return {
872
+ action: "deny",
873
+ message: supplied === "timeout"
874
+ ? `approval for "${req.toolName}" was not answered before the approver's own window elapsed: ${req.message}`
875
+ : `approval denied for "${req.toolName}": ${req.message}`,
876
+ decisionReason: "mode",
877
+ settledBy: supplied === "timeout" ? "timeout" : "human",
878
+ };
849
879
  }
850
- if (ok.updatedInput === undefined)
880
+ if (suppliedEdit === undefined)
851
881
  return { action: "allow", decisionReason: "mode", presentedInput: presented.value, settledBy: "human" };
852
- const edit = tryCloneArgs(ok.updatedInput);
882
+ const edit = tryCloneArgs(suppliedEdit);
853
883
  if (!edit.ok) {
854
884
  return {
855
885
  action: "deny",