@sema-agent/core 5.34.0 → 5.35.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, checkToolPolicyProjection, combinePolicies, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
23
+ import { askApproverIdentity, checkToolPolicyProjection, combinePolicies, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, screenApproverAttribution, 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";
@@ -88,6 +88,7 @@ import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
88
88
  import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
89
89
  import { boundInputHashOf } from "../canonical-json.js";
90
90
  import { countElicitOptIns, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
91
+ import { durableParkGapFor } from "../park-selfcheck.js";
91
92
  import { GLOBAL_USAGE_KEY, usageRetryAfterMs } from "../usage-window-store.js";
92
93
  import { deliverEngineNotice } from "../types.js";
93
94
  const announcedMaterializeEnv = new Set();
@@ -192,6 +193,46 @@ async function forgetQuietly(sessions, sessionId) {
192
193
  catch {
193
194
  }
194
195
  }
196
+ function screenGateSettlement(result, settling) {
197
+ const defects = [];
198
+ const reportedBy = result.settledBy;
199
+ let settledBy;
200
+ if (reportedBy !== undefined) {
201
+ if (!isApprovalSettledBy(reportedBy) || (!settling && reportedBy !== "human")) {
202
+ defects.push(`a tool-gate settlement reported settledBy "${String(reportedBy)}" on ${settling ? "a blocked" : "an executing"} call — ` +
203
+ `it is one of "human" / "timeout" / "aborted", and only "human" can be the source of a call that runs; the frame carries no source`);
204
+ }
205
+ else
206
+ settledBy = reportedBy;
207
+ }
208
+ const attribution = screenApproverAttribution(result.approver);
209
+ if (attribution.defect !== undefined) {
210
+ defects.push(`a tool-gate settlement reported an attribution this engine refuses: ${attribution.defect}; the frame carries no approver`);
211
+ }
212
+ const record = {
213
+ ...(settledBy !== undefined ? { settledBy } : {}),
214
+ ...(attribution.approver !== undefined ? { approver: attribution.approver } : {}),
215
+ };
216
+ return { ...(settledBy !== undefined || attribution.approver !== undefined ? { record } : {}), defects };
217
+ }
218
+ function inheritedAskRuleEvidence(deps) {
219
+ const org = deps.permissionRuleOrg === undefined ? "not_wired" : "not_adjudicated";
220
+ const personal = deps.permissionRuleStore === undefined ? "not_wired" : "not_adjudicated";
221
+ return Object.freeze({ orgRevisionAbsent: org, orgRuleAbsent: org, personalRuleDotsAbsent: personal });
222
+ }
223
+ function orgRevisionEvidenceOf(resolution, onDefect) {
224
+ const reported = resolution.revision;
225
+ if (reported === undefined)
226
+ return {};
227
+ if (typeof reported === "number" && Number.isFinite(reported))
228
+ return { revision: reported };
229
+ onDefect(`the org rule overlay reported revision ${JSON.stringify(reported)} — a snapshot revision is a finite number; ` +
230
+ `the adjudication stands, but the ask carries no revision evidence for this call`);
231
+ return {};
232
+ }
233
+ function persistedRuleHitOf(admitting) {
234
+ return admitting === undefined ? undefined : { rule: admitting.rule, dots: admitting.adds.map((a) => ({ actor: a.dot.actor, counter: a.dot.counter })) };
235
+ }
195
236
  export async function prepareTask(spec, deps, sessions, resume, internals, runnerSelf) {
196
237
  const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
197
238
  spec = doors.spec;
@@ -204,7 +245,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
204
245
  e.code = "resume.session_not_found";
205
246
  throw e;
206
247
  }
207
- const { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects });
248
+ const { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects, ...(() => { const g = durableParkGapFor(deps, spec); return g !== undefined ? { durableParkGap: g } : {}; })() });
208
249
  const sessionId = acquired.sessionId;
209
250
  const hostTaskId = spec.taskId ?? sessionId;
210
251
  if (compModel !== undefined) {
@@ -2778,6 +2819,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2778
2819
  return {};
2779
2820
  return { riskAxes: { ...(irreversible !== undefined ? { irreversible } : {}), ...(egress !== undefined ? { egress } : {}) } };
2780
2821
  };
2822
+ const inheritedAskEvidence = inheritedAskRuleEvidence(deps);
2781
2823
  const permissionRuleLane = (() => {
2782
2824
  const provider = deps.permissionRuleStore;
2783
2825
  const localOwnerDeclared = deps.localOwnerRules === true;
@@ -2817,9 +2859,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2817
2859
  message: err instanceof Error ? err.message : String(err),
2818
2860
  ts: Date.now(),
2819
2861
  }));
2820
- return undefined;
2862
+ return { unreadable: true };
2821
2863
  }
2822
- return findAdmittingRule(listed.rules, { tool: req.toolName, command, cwd: root })?.rule;
2864
+ return persistedRuleHitOf(findAdmittingRule(listed.rules, { tool: req.toolName, command, cwd: root }));
2823
2865
  },
2824
2866
  };
2825
2867
  })();
@@ -2840,11 +2882,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2840
2882
  }
2841
2883
  if (resolution.status === "unavailable")
2842
2884
  return { status: "unavailable", disclosures: resolution.disclosures };
2885
+ const revisionCell = orgRevisionEvidenceOf(resolution, (message) => deps.onError?.(new Error(message), { phase: "config", sessionId }));
2843
2886
  const command = req.args?.command;
2844
2887
  if (req.toolName !== PERSISTED_RULE_TOOL || typeof command !== "string")
2845
- return { status: "available" };
2888
+ return { status: "available", ...revisionCell };
2846
2889
  const verdict = orgRuleVerdictFor(resolution.rules, { tool: req.toolName, command });
2847
- return verdict === undefined ? { status: "available" } : { status: "available", verdict };
2890
+ return verdict === undefined ? { status: "available", ...revisionCell } : { status: "available", verdict, ...revisionCell };
2848
2891
  },
2849
2892
  };
2850
2893
  })();
@@ -2926,6 +2969,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2926
2969
  ...riskAxesOf(creq.toolName),
2927
2970
  ...(re.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2928
2971
  ...(re.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: re.persistedRuleShadowed } : {}),
2972
+ ruleEvidence: inheritedAskEvidence,
2929
2973
  }, onAskOf, csignal ?? abortController.signal);
2930
2974
  if (rr.action !== "allow")
2931
2975
  return rr;
@@ -3011,6 +3055,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3011
3055
  ...riskAxesOf(creq.toolName),
3012
3056
  ...(first.action === "ask" && first.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3013
3057
  ...(first.action === "ask" && first.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: first.persistedRuleShadowed } : {}),
3058
+ ruleEvidence: inheritedAskEvidence,
3014
3059
  }, pc.onAsk, csignal ?? abortController.signal);
3015
3060
  const askWaitMs = Math.max(0, now() - askT0);
3016
3061
  if (resolved.action === "deny" && resolved.approverUnavailable === true) {
@@ -3098,6 +3143,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3098
3143
  ...riskAxesOf(creq.toolName),
3099
3144
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3100
3145
  ...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
3146
+ ruleEvidence: inheritedAskEvidence,
3101
3147
  }, pc.onAsk, csignal ?? abortController.signal);
3102
3148
  const askWaitMs = Math.max(0, now() - askT0);
3103
3149
  if (resolved.action === "deny" && resolved.approverUnavailable === true) {
@@ -3239,7 +3285,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3239
3285
  }
3240
3286
  const preToolContexts = new Map();
3241
3287
  const blockedToolCalls = new Set();
3242
- const approvalSettledBy = new Map();
3288
+ const approvalSettlement = new Map();
3243
3289
  const blockedTracked = Boolean(hooks?.postToolUse || hooks?.preToolUse || hooks?.postToolUseFailure || hooks?.postToolBatch);
3244
3290
  const restoreSurfaceGap = ownedEnv !== undefined && isRemoteExecutionEnv(ownedEnv) && ownedEnv.capabilities.suspendable ? missingRestoreSurface(ownedEnv) : [];
3245
3291
  const incompleteSuspendAdapter = restoreSurfaceGap.length > 0 ? restoreSurfaceGap : undefined;
@@ -3413,6 +3459,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3413
3459
  ...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
3414
3460
  ...(decision.action === "ask" && decision.probeReason !== undefined ? { probeReason: decision.probeReason } : {}),
3415
3461
  ...(decision.action === "ask" && decision.probeCause !== undefined ? { probeCause: decision.probeCause } : {}),
3462
+ ...(decision.action === "ask" && decision.ruleEvidence !== undefined ? { ruleEvidence: decision.ruleEvidence } : {}),
3416
3463
  }, onAsk, abortController.signal);
3417
3464
  const waitMs = Math.max(0, now() - t0);
3418
3465
  if (resolved.approverUnavailable !== true) {
@@ -4230,17 +4277,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4230
4277
  if (blockedTracked && (result.block || result.suspend)) {
4231
4278
  blockedToolCalls.add(e.toolCallId);
4232
4279
  }
4233
- const reported = result.settledBy;
4234
- if (reported !== undefined) {
4235
- const settling = result.block === true;
4236
- if (!isApprovalSettledBy(reported) || (!settling && reported !== "human")) {
4237
- deps.onError?.(new Error(`a tool-gate settlement reported settledBy "${String(reported)}" on ${settling ? "a blocked" : "an executing"} call — ` +
4238
- `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 });
4239
- }
4240
- else {
4241
- approvalSettledBy.set(e.toolCallId, reported);
4242
- }
4243
- }
4280
+ const settlement = screenGateSettlement(result, result.block === true);
4281
+ for (const defect of settlement.defects)
4282
+ deps.onError?.(new Error(defect), { phase: "config", sessionId });
4283
+ if (settlement.record !== undefined)
4284
+ approvalSettlement.set(e.toolCallId, settlement.record);
4244
4285
  return result.block
4245
4286
  ? { block: true, reason: result.reason }
4246
4287
  : result.updatedInput !== undefined
@@ -4552,7 +4593,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4552
4593
  const effectiveReadFaceObserved = carrierReadFace();
4553
4594
  const effectiveReadDenyObserved = readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized.map((e) => ({ ...e })) : undefined;
4554
4595
  const preparedHolder = {};
4555
- const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettledBy, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), 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 } : {}) });
4596
+ const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), 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 } : {}) });
4556
4597
  const prepared = buildPrepared();
4557
4598
  preparedHolder.current = prepared;
4558
4599
  return prepared;
@@ -80,13 +80,18 @@ declare function toolEndBodyFrom(result: unknown, isError: boolean,
80
80
  * a parameter and never derived from `result`: a tool's own `details` (which post-tool hooks may
81
81
  * also replace) is writable by layers that adjudicate nothing, so reading provenance out of it would
82
82
  * let a failing tool claim a person approved it. Omitted ⇒ this call settled no approval. */
83
- settledBy?: ApprovalSettledBy): {
83
+ settledBy?: ApprovalSettledBy,
84
+ /** design/252 G-7 — WHOSE settlement, from the same caller and the same channel as `settledBy`, and
85
+ * for the same reason it is a parameter: an attribution read out of a tool's own result would let a
86
+ * tool name the person who approved it. Omitted ⇒ this call's settlement named nobody. */
87
+ approver?: string): {
84
88
  output?: unknown;
85
89
  truncated?: boolean;
86
90
  totalChars?: number;
87
91
  structured?: unknown;
88
92
  errorCode?: string;
89
93
  settledBy?: ApprovalSettledBy;
94
+ approver?: string;
90
95
  };
91
96
  /**
92
97
  * scan-1/A1 — the BODY of the synthetic `tool_end` that closes a reconcile-recovered orphan. ONE
@@ -2,7 +2,7 @@ import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
2
2
  import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
3
3
  import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
4
4
  import { snapshotActorAssertion } from "../../internal/llm.js";
5
- import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
5
+ import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, LEGACY_PENDING_STEER_INPUT_ID, MAX_STEER_INPUT_ID_CHARS, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
6
6
  import { engineVersion } from "../version.js";
7
7
  import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
8
8
  import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
@@ -41,7 +41,7 @@ import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntr
41
41
  import { reconcileInterruptedSession } from "../session-reconcile.js";
42
42
  import { RunnerSharedToolResultStore } from "../tool-result-store.js";
43
43
  import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
44
- import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, refuseOutOfContractDecision, toolPolicyNameSets } from "../tool-policy.js";
44
+ import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, refuseOutOfContractDecision, screenApproverAttribution, toolPolicyNameSets } from "../tool-policy.js";
45
45
  import { defaultTaskRegistry } from "../task-registry.js";
46
46
  import { discloseDroppedPending, isDelegatedAgentTerminal, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
47
47
  import { ToolDetachHub } from "../tool-detach.js";
@@ -68,6 +68,13 @@ function nextHumanInputSeq(key) {
68
68
  }
69
69
  return ++box.n;
70
70
  }
71
+ function sameAcceptedSteerInput(a, b) {
72
+ return (a.payload === b.payload &&
73
+ a.trusted === b.trusted &&
74
+ a.actor?.id === b.actor?.id &&
75
+ a.actor?.hostAsserted === b.actor?.hostAsserted &&
76
+ a.actor?.issuer === b.actor?.issuer);
77
+ }
71
78
  const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
72
79
  const STOP_HOOK_BLOCK_CAP = 8;
73
80
  const COMPACTION_REGROWTH_FACTOR = 1.5;
@@ -130,7 +137,7 @@ function resumeDecisionWasNegative(resume) {
130
137
  }
131
138
  const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call that suspended for durable approval, so it was " +
132
139
  "NOT executed on resume. If you still need it, issue it again now.";
133
- function toolEndBodyFrom(result, isError, settledBy) {
140
+ function toolEndBodyFrom(result, isError, settledBy, approver) {
134
141
  const o = toolOutputFrom(result);
135
142
  const st = structuredFrom(result);
136
143
  const det = isError ? result?.details : undefined;
@@ -142,6 +149,7 @@ function toolEndBodyFrom(result, isError, settledBy) {
142
149
  ...(st !== undefined ? { structured: st } : {}),
143
150
  ...(typeof code === "string" ? { errorCode: code } : {}),
144
151
  ...(settledBy !== undefined ? { settledBy } : {}),
152
+ ...(approver !== undefined ? { approver } : {}),
145
153
  };
146
154
  }
147
155
  export function reconciledToolEndBody(orphan) {
@@ -1213,16 +1221,16 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1213
1221
  prepared.lspDiagnostics.nudge(p);
1214
1222
  }
1215
1223
  }
1216
- const settledBy = prepared.approvalSettledBy.get(event.toolCallId);
1217
- if (settledBy !== undefined)
1218
- prepared.approvalSettledBy.delete(event.toolCallId);
1224
+ const settlement = prepared.approvalSettlement.get(event.toolCallId);
1225
+ if (settlement !== undefined)
1226
+ prepared.approvalSettlement.delete(event.toolCallId);
1219
1227
  pushContent({
1220
1228
  type: "tool_end",
1221
1229
  toolCallId: event.toolCallId,
1222
1230
  toolName: event.toolName,
1223
1231
  ...(toolLabels.get(event.toolName) !== undefined ? { label: toolLabels.get(event.toolName) } : {}),
1224
1232
  isError: event.isError,
1225
- ...toolEndBodyFrom(event.result, event.isError, settledBy),
1233
+ ...toolEndBodyFrom(event.result, event.isError, settlement?.settledBy, settlement?.approver),
1226
1234
  ...ident(),
1227
1235
  });
1228
1236
  announceWorkspaceMove();
@@ -1444,6 +1452,7 @@ export class Runner {
1444
1452
  };
1445
1453
  let reapHandle;
1446
1454
  let steerChain = Promise.resolve();
1455
+ const acceptedSteerInputs = new Map();
1447
1456
  const notifyRef = {};
1448
1457
  const manualCompactRef = { requested: false, waiters: [] };
1449
1458
  const drainManualCompactWaiters = (outcome) => {
@@ -1565,6 +1574,7 @@ export class Runner {
1565
1574
  queue.push({ type: "done", result: resultValue });
1566
1575
  queue.close();
1567
1576
  });
1577
+ void settled.then(() => acceptedSteerInputs.clear(), () => acceptedSteerInputs.clear());
1568
1578
  const steeringError = (msg, code = "steering.not_running") => {
1569
1579
  const e = new Error(`cannot steer: ${msg}`);
1570
1580
  e.code = code;
@@ -1613,22 +1623,40 @@ export class Runner {
1613
1623
  return suggestionsDone.catch(() => []);
1614
1624
  },
1615
1625
  steer: async (text, options) => {
1616
- if (options?.trusted && sanitizeUntrustedText(text) !== text) {
1626
+ const trusted = options?.trusted ? true : false;
1627
+ if (trusted && sanitizeUntrustedText(text) !== text) {
1617
1628
  throw steeringError("trusted steering text must not contain a </system-reminder> tag", "steering.invalid_content");
1618
1629
  }
1619
- const actor = options?.actor === undefined ? undefined : snapshotActorAssertion(options.actor);
1630
+ const inputId = options?.inputId;
1631
+ if (inputId !== undefined) {
1632
+ if (typeof inputId !== "string") {
1633
+ throw steeringError("inputId must be a string when supplied", "steering.invalid_content");
1634
+ }
1635
+ if (inputId === "" || inputId.length > MAX_STEER_INPUT_ID_CHARS) {
1636
+ throw steeringError(`inputId must be a non-empty string of at most ${MAX_STEER_INPUT_ID_CHARS} characters`, "steering.invalid_content");
1637
+ }
1638
+ if (inputId === LEGACY_PENDING_STEER_INPUT_ID) {
1639
+ throw steeringError(`inputId "${LEGACY_PENDING_STEER_INPUT_ID}" is reserved for a pre-queue parked steer and cannot be supplied by a caller`, "steering.invalid_content");
1640
+ }
1641
+ }
1642
+ const actorIn = options?.actor;
1643
+ const actor = actorIn === undefined ? undefined : snapshotActorAssertion(actorIn);
1620
1644
  const projected = projectHumanInput({ text, actor, source: "steer" });
1621
- const payload = options?.trusted ? formatHookFeedback(projected) : projected;
1645
+ const payload = trusted ? formatHookFeedback(projected) : projected;
1622
1646
  const mintsAFrame = payload.trim().length !== 0;
1623
- const emitAccepted = (h) => {
1647
+ const replay = { payload, trusted, ...(actor !== undefined ? { actor } : {}) };
1648
+ const noteAccepted = (h) => {
1624
1649
  if (!mintsAFrame)
1625
1650
  return;
1651
+ if (typeof inputId === "string")
1652
+ acceptedSteerInputs.set(inputId, replay);
1626
1653
  queue.push({
1627
1654
  ...buildHumanInputEvent({
1628
1655
  carrier: "steer",
1629
1656
  source: "steer",
1630
1657
  delivery: "queued",
1631
1658
  sessionSeq: nextHumanInputSeq(h.harness),
1659
+ ...(typeof inputId === "string" ? { inputId } : {}),
1632
1660
  ...(actor !== undefined ? { actor } : {}),
1633
1661
  ...(actor?.issuer !== undefined ? { issuer: actor.issuer } : {}),
1634
1662
  ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
@@ -1645,9 +1673,21 @@ export class Runner {
1645
1673
  const h = handle ?? (await orTimeout(ready));
1646
1674
  if (!h)
1647
1675
  throw steeringError("the task is not running");
1676
+ if (typeof inputId === "string") {
1677
+ const prior = acceptedSteerInputs.get(inputId);
1678
+ if (prior !== undefined) {
1679
+ if (resultValue !== undefined || h.loop.ended)
1680
+ throw steeringError("the task is no longer running");
1681
+ if (!sameAcceptedSteerInput(prior, replay)) {
1682
+ throw steeringError("a different steering instruction was already accepted under this inputId — re-issue this one with a fresh inputId " +
1683
+ "(an identical payload would have been an idempotent retry)", "steering.duplicate_input_id");
1684
+ }
1685
+ return;
1686
+ }
1687
+ }
1648
1688
  try {
1649
1689
  await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
1650
- emitAccepted(h);
1690
+ noteAccepted(h);
1651
1691
  return;
1652
1692
  }
1653
1693
  catch (e) {
@@ -1658,7 +1698,7 @@ export class Runner {
1658
1698
  while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
1659
1699
  try {
1660
1700
  await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
1661
- emitAccepted(h);
1701
+ noteAccepted(h);
1662
1702
  return;
1663
1703
  }
1664
1704
  catch (e2) {
@@ -3664,6 +3704,15 @@ export class Runner {
3664
3704
  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; ` +
3665
3705
  "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" });
3666
3706
  }
3707
+ const approver = decide.approver;
3708
+ const attribution = screenApproverAttribution(approver);
3709
+ if (attribution.defect !== undefined) {
3710
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume carries an approver attribution this engine refuses: ${attribution.defect}; refusing pre-CAS, the checkpoint stays pending`, { field: "approver" });
3711
+ }
3712
+ if (attribution.approver !== undefined && settledBy === undefined) {
3713
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume names an approver but no settledBy — an attribution says WHOSE settlement it was and needs the word that says what KIND of end it was beside it; ` +
3714
+ `supply settledBy ("human" / "timeout" / "aborted"), or omit the approver; refusing pre-CAS, the checkpoint stays pending`, { field: "approver" });
3715
+ }
3667
3716
  const reason = decide.reason;
3668
3717
  assertOutcomeText(reason, "reason");
3669
3718
  plainPolicyOutcome = {
@@ -3675,6 +3724,7 @@ export class Runner {
3675
3724
  ...(reason !== undefined ? { reason } : {}),
3676
3725
  ...(redeemedAnswer !== undefined ? { answer: redeemedAnswer } : {}),
3677
3726
  ...(settledBy !== undefined ? { settledBy } : {}),
3727
+ ...(attribution.approver !== undefined ? { approver: attribution.approver } : {}),
3678
3728
  };
3679
3729
  if (plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason && sanitizeUntrustedText(plainPolicyOutcome.reason) !== plainPolicyOutcome.reason) {
3680
3730
  throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
@@ -4037,7 +4087,7 @@ export class Runner {
4037
4087
  const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput !== undefined ? outcome.updatedInput : pendingAction.args);
4038
4088
  const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
4039
4089
  emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
4040
- const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, outcome.settledBy) });
4090
+ const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, outcome.settledBy, outcome.approver) });
4041
4091
  if (outcome.decision === "deny") {
4042
4092
  const defaultDenial = outcome.settledBy === "timeout"
4043
4093
  ? `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.`
@@ -117,8 +117,8 @@ export type WorkerErrorClass = "budget" | "limit" | "output" | "suspend" | "revi
117
117
  * are NOT all terminal-errorCode-shaped — see {@link EXACT_CODE_CLASS}'s comment), plus the flat 1.36 codes.
118
118
  * The `limits.` namespace splits into the `budget` and `limit` classes by exact code — see
119
119
  * {@link EXACT_CODE_CLASS}. Operation-level dotted codes that are surfaced to a *caller* and never become a
120
- * task outcome are deliberately OUT of scope: `steering.*` (`steering.not_running`/`steering.invalid_content`,
121
- * rejected to the `steer()` caller) and `mcp.*` (`mcp.server_unavailable`, an `onWarn` warning code) — neither
120
+ * task outcome are deliberately OUT of scope: `steering.*` (`steering.not_running`/`steering.invalid_content`/
121
+ * `steering.duplicate_input_id`, rejected to the `steer()` caller) and `mcp.*` (`mcp.server_unavailable`, an `onWarn` warning code) — neither
122
122
  * reaches `TaskResult.errorCode`, so a caller will never pass them here. A genuinely unmapped code → `"unknown"` (which therefore means
123
123
  * "known-but-foreign or no code", e.g. a leaked fs/Node code, NOT "an OUR terminal class we forgot to add").
124
124
  */
@@ -118,6 +118,34 @@ export type ApprovalSettledBy = "human" | "timeout" | "aborted";
118
118
  export declare const APPROVAL_SETTLED_BY_VALUES: readonly ApprovalSettledBy[];
119
119
  /** True iff `v` is one of the three {@link ApprovalSettledBy} words. */
120
120
  export declare function isApprovalSettledBy(v: unknown): v is ApprovalSettledBy;
121
+ /**
122
+ * design/252 G-7 — how long an approver-attribution identifier may be.
123
+ *
124
+ * Sized for the identifiers approval channels actually carry (a login, an email, an opaque account id,
125
+ * a queue name) with room to spare, and bounded at all because this value lands on an operator-plane
126
+ * frame that a deployment may persist: an unbounded field on a record channel is its own denial of a
127
+ * readable record. An over-long value is REFUSED, never truncated — a cut identifier names a different
128
+ * party, or nobody, and either is worse than the honest refusal.
129
+ */
130
+ export declare const APPROVER_ATTRIBUTION_MAX_CHARS = 256;
131
+ /**
132
+ * design/252 G-7 — screen a deployment-supplied approver identifier before it becomes an observation.
133
+ *
134
+ * THE POSTURE, stated so it is not mistaken for something stronger: core does not authenticate this
135
+ * value, does not compare it to anything, and never reads it back to decide anything. It is a
136
+ * TRANSCRIPTION of what the approval channel said about its own settlement — the channel (a server's
137
+ * approval card, an HMAC-verified callback, an operator console) is where identity is established, and
138
+ * a library that holds no identity surface cannot second-guess it. What core owns is that the value is
139
+ * a value: a string, bounded, free of control bytes, or else loudly refused.
140
+ *
141
+ * Returns `{}` for "nothing supplied" (absent, and an empty string — an id of no characters is the
142
+ * absence of an id, the same truthiness fold the deny-note seat uses), `{ approver }` for a value to
143
+ * carry, or `{ defect }` with the sentence a caller puts in its own refusal.
144
+ */
145
+ export declare function screenApproverAttribution(v: unknown): {
146
+ approver?: string;
147
+ defect?: string;
148
+ };
121
149
  /**
122
150
  * A three-state permission decision for a tool call (design/37). Upgrades the old two-state
123
151
  * `{allow|deny}`:
@@ -162,6 +190,7 @@ export type PermissionResult = {
162
190
  message?: string;
163
191
  decisionReason?: DecisionReason;
164
192
  settledBy?: Extract<ApprovalSettledBy, "human">;
193
+ approver?: string;
165
194
  } | {
166
195
  action: "ask";
167
196
  updatedInput?: unknown;
@@ -199,13 +228,86 @@ export type PermissionResult = {
199
228
  * when ANY folded-away concurrent ask bore it (monotone, tighten-only); it is consumed inside the
200
229
  * gate and deliberately NOT copied onto the park/approval-card request. */
201
230
  matchedAskRule?: string;
231
+ /** design/252 G-2 (additive): the RULE-PROVENANCE evidence behind this ask — see
232
+ * {@link AskRuleEvidence}. ENGINE-STAMPED inside the gate, once, after the org layer and the
233
+ * persisted-rule lane have both spoken; a policy that self-declares it is overwritten there
234
+ * (the member is a record of what the ENGINE's own governance layers did, so a layer's claim
235
+ * about itself is not evidence). Carried onto the approval request by the gate's own ask mint
236
+ * site. Display/reconciliation metadata, never adjudication input. */
237
+ ruleEvidence?: AskRuleEvidence;
202
238
  } | {
203
239
  action: "deny";
204
240
  updatedInput?: unknown;
205
241
  message?: string;
206
242
  decisionReason?: DecisionReason;
207
243
  settledBy?: ApprovalSettledBy;
244
+ approver?: string;
208
245
  };
246
+ /**
247
+ * design/252 G-2 — WHY a piece of rule-provenance evidence is not on an ask.
248
+ *
249
+ * The vocabulary exists because a bare `undefined` reads the same for facts that are opposite: "no
250
+ * governance layer is wired here, so there is nothing to name" and "the layer ran and we could not
251
+ * read what it said" are not the same audit answer, and collapsing them is how an evidence chain
252
+ * comes to be reconstructed as "nothing governed this call". Every member of {@link AskRuleEvidence}
253
+ * therefore ships as a value OR a named absence, never as silence.
254
+ *
255
+ * - `"not_wired"` — the lane does not exist on this leg (an ungoverned deployment, no rule store).
256
+ * The field cannot apply; nothing was lost.
257
+ * - `"not_adjudicated"` — the lane exists but this call never reached it (an exempt tool, a call
258
+ * the rule grammar cannot describe). Applicable in principle, skipped in fact.
259
+ * - `"unavailable"` — the lane was consulted and could not read its source. The evidence is LOST,
260
+ * not absent, and this is the one member of the vocabulary that means an auditor should treat the
261
+ * chain as broken rather than empty.
262
+ * - `"no_match"` — consulted, readable, and nothing spoke for this call. A real negative answer.
263
+ * - `"not_reported"` — the supplying seam answered without the identity (a foreign overlay/store
264
+ * implementation, or one written before the identity was projected). Evidence lost at the seam.
265
+ */
266
+ export type AskEvidenceAbsence = "not_wired" | "not_adjudicated" | "unavailable" | "no_match" | "not_reported";
267
+ /** The closed set above, for runtime domain checks at the seams that accept a caller-supplied value. */
268
+ export declare const ASK_EVIDENCE_ABSENCE_VALUES: readonly AskEvidenceAbsence[];
269
+ /**
270
+ * design/252 G-2 — the machine-reconcilable provenance of the governance decision behind ONE ask.
271
+ *
272
+ * WHAT THIS IS: a PROJECTION of identity keys the engine's governance layers already hold — the org
273
+ * snapshot's `revision`, the personal rule's add `dot`s — onto the surface a consumer can actually
274
+ * read. It performs no new judgment and changes no verdict; removing it would leave every decision
275
+ * byte-identical. The prose channels ({@link PermissionResult}'s ask `message`,
276
+ * {@link AskRequest.persistedRuleShadowed}) say the same things to a PERSON; those are sanitized,
277
+ * capped, display-shaped strings, and reconciling a decision against a published policy revision by
278
+ * regexing them is not an audit trail. This member is the machine's copy.
279
+ *
280
+ * WHAT THIS IS NOT: an authority channel. Nothing in the engine reads it back to decide anything, and
281
+ * a host that ignores it entirely is governed exactly as before.
282
+ *
283
+ * ABSENCE DISCIPLINE (the reason each member has a `…Absent` twin): see {@link AskEvidenceAbsence}.
284
+ * Exactly one of each pair is present — a member and its absence reason are never both set, and never
285
+ * both missing, on evidence the engine stamped.
286
+ */
287
+ export interface AskRuleEvidence {
288
+ /** The org snapshot `revision` this call was adjudicated against — the published-policy version an
289
+ * auditor reconciles the decision against. Absent ⇒ {@link orgRevisionAbsent} names why. */
290
+ readonly orgRevision?: number;
291
+ /** Present iff {@link orgRevision} is not. `"unavailable"` here is the load-bearing one: the org
292
+ * layer spoke, its answer was "this deployment cannot see the organization's rules", and the ask
293
+ * in hand is the fail-closed tighten that followed — not an ask any published rule asked for. */
294
+ readonly orgRevisionAbsent?: AskEvidenceAbsence;
295
+ /** The org rule that spoke for this call, verbatim as published (org rules are administrator
296
+ * authored and are relayed unmodified — the prose channel's copy is the same text). Absent ⇒
297
+ * {@link orgRuleAbsent} names why. */
298
+ readonly orgRule?: string;
299
+ /** Present iff {@link orgRule} is not. */
300
+ readonly orgRuleAbsent?: AskEvidenceAbsence;
301
+ /** The add dots of the PERSONAL allow rule that matched this call but did not clear the ask (the
302
+ * #144 shadowed arm). The dots are the rule's durable identity — unlike
303
+ * {@link AskRequest.persistedRuleShadowed}, which is a sanitized, length-capped DISPLAY value and
304
+ * deliberately not an identity channel. A rule is a set of adds (concurrent approvals on one text
305
+ * each redeem their own dot), so this is an array by construction: render/reconcile the entries as
306
+ * data, never re-derive one scalar id by joining them. Absent ⇒ {@link personalRuleDotsAbsent}. */
307
+ readonly personalRuleDots?: readonly import("./permission-rule-model.js").RuleDot[];
308
+ /** Present iff {@link personalRuleDots} is not. */
309
+ readonly personalRuleDotsAbsent?: AskEvidenceAbsence;
310
+ }
209
311
  /** The human/model-readable text of a decision. */
210
312
  export declare function decisionText(d: PermissionResult): string | undefined;
211
313
  /**
@@ -682,6 +784,15 @@ export interface AskRequest {
682
784
  * structure: render the entries as data, never re-derive structure by splitting or joining them.
683
785
  * The durable park route carries the same value as `RiskDescriptor.probeCause`. */
684
786
  readonly probeCause?: import("./checkpoint-store.js").ProbeCause;
787
+ /** design/252 G-2: the RULE-PROVENANCE evidence behind this ask — the org snapshot revision and the
788
+ * matched rules' own identity keys, each present as a value or as a named absence (see
789
+ * {@link AskRuleEvidence}). Present on EVERY ask the engine mints, so a consumer never has to tell a
790
+ * missing field from a field that means something: the gate's own mint site fills it from what its
791
+ * layers did, and the three INHERITED-lane sites — which present an ANCESTOR policy's decision, from
792
+ * upstream of both lanes — fill it with an all-`"not_adjudicated"` record rather than a fabricated
793
+ * revision. Optional on the TYPE only because a deployment may invoke its approver directly.
794
+ * RECONCILIATION metadata, never adjudication input. */
795
+ readonly ruleEvidence?: AskRuleEvidence;
685
796
  toolCallId: string;
686
797
  /** The (post-rewrite) args the tool would run with. */
687
798
  args: unknown;
@@ -859,12 +970,26 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
859
970
  * domain the durable leg enforces pre-CAS for the same field), a THROWING read is a fail-closed
860
971
  * deny naming the true cause (never a raw rejection out of the gate), and the empty string reads
861
972
  * as absent (truthiness, the durable consumer's own read).
973
+ *
974
+ * `approver` (design/252 G-7) — the ATTRIBUTION seat: the identifier the approval channel reports for
975
+ * the party that ended this wait. Core does not authenticate it, compare it, or read it back to decide
976
+ * anything; it transcribes it onto the settlement observation next to `settledBy`, so an audit that can
977
+ * already say "a person ended this wait" can also say which one, without core growing an identity
978
+ * surface it deliberately does not have. Read the two together: `settledBy:"timeout"` with an
979
+ * `approver` names the queue whose window elapsed, NOT someone who refused.
980
+ * Screened, not trusted (see {@link screenApproverAttribution}): a non-string, an over-long value or one
981
+ * carrying control characters is a loud fail-closed refusal at {@link resolveAsk}, the same posture this
982
+ * seam takes for every other out-of-contract value — and the same reason, since a defective attribution
983
+ * that were quietly dropped would leave a settlement looking unattributed rather than misreported. It is
984
+ * carried on the arms where the CALLER settled something (its allow, its human/timeout deny) and never
985
+ * on an end the engine produced (an abort, a throw, an unclonable edit): nobody approved those.
862
986
  */
863
987
  export type AskOutcome = boolean | "unavailable" | {
864
988
  allow: boolean;
865
989
  updatedInput?: unknown;
866
990
  settledBy?: Extract<ApprovalSettledBy, "human" | "timeout">;
867
991
  reason?: string;
992
+ approver?: string;
868
993
  };
869
994
  /**
870
995
  * ruled 2026-08-04 — forward an approver into a delegated child, stamping every ask it raises with the