@sema-agent/core 7.5.1 → 7.5.2

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 (66) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/core/protocol-table.d.ts +5 -0
  3. package/dist/core/protocol-table.js +1 -0
  4. package/dist/core/runner/abort-race.d.ts +41 -0
  5. package/dist/core/runner/abort-race.js +38 -0
  6. package/dist/core/runner/checkpoint-scope.d.ts +15 -3
  7. package/dist/core/runner/checkpoint-scope.js +3 -0
  8. package/dist/core/runner/compaction-call-options.d.ts +1 -1
  9. package/dist/core/runner/content-ask-bindings.d.ts +27 -0
  10. package/dist/core/runner/content-ask-bindings.js +1 -0
  11. package/dist/core/runner/contracts.d.ts +118 -6
  12. package/dist/core/runner/denial-limit-arms.d.ts +23 -0
  13. package/dist/core/runner/denial-limit-arms.js +21 -0
  14. package/dist/core/runner/inherited-ask-grants.d.ts +46 -0
  15. package/dist/core/runner/inherited-ask-grants.js +29 -0
  16. package/dist/core/runner/park-commit.d.ts +108 -0
  17. package/dist/core/runner/park-commit.js +32 -0
  18. package/dist/core/runner/{prepare-permission-rules.d.ts → permission-rule-lanes.d.ts} +109 -3
  19. package/dist/core/runner/{prepare-permission-rules.js → permission-rule-lanes.js} +47 -1
  20. package/dist/core/runner/prepare-ask-lane.d.ts +110 -0
  21. package/dist/core/runner/prepare-ask-lane.js +133 -0
  22. package/dist/core/runner/prepare-boundary-parks.d.ts +105 -0
  23. package/dist/core/runner/prepare-boundary-parks.js +169 -0
  24. package/dist/core/runner/prepare-context-lane.d.ts +119 -0
  25. package/dist/core/runner/prepare-context-lane.js +230 -0
  26. package/dist/core/runner/prepare-gate-stations.d.ts +177 -0
  27. package/dist/core/runner/prepare-gate-stations.js +290 -0
  28. package/dist/core/runner/prepare-hands-readface.d.ts +4 -4
  29. package/dist/core/runner/prepare-inherited-gate.d.ts +4 -4
  30. package/dist/core/runner/prepare-memory-engine-session.d.ts +84 -0
  31. package/dist/core/runner/prepare-memory-engine-session.js +233 -0
  32. package/dist/core/runner/prepare-park-ask.d.ts +164 -0
  33. package/dist/core/runner/prepare-park-ask.js +377 -0
  34. package/dist/core/runner/prepare-policy-chain.d.ts +208 -0
  35. package/dist/core/runner/prepare-policy-chain.js +584 -0
  36. package/dist/core/runner/prepare-project-context.d.ts +1 -13
  37. package/dist/core/runner/prepare-project-context.js +1 -3
  38. package/dist/core/runner/prepare-prompt-assembly.d.ts +95 -0
  39. package/dist/core/runner/prepare-prompt-assembly.js +162 -0
  40. package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -20
  41. package/dist/core/runner/prepare-protocol-tools.d.ts +3 -3
  42. package/dist/core/runner/prepare-protocol-tools.js +0 -3
  43. package/dist/core/runner/prepare-question-face.d.ts +3 -21
  44. package/dist/core/runner/prepare-question-face.js +2 -1
  45. package/dist/core/runner/prepare-safety-scan.d.ts +0 -5
  46. package/dist/core/runner/prepare-safety-scan.js +1 -2
  47. package/dist/core/runner/prepare-suspend-saga.d.ts +170 -0
  48. package/dist/core/runner/prepare-suspend-saga.js +308 -0
  49. package/dist/core/runner/prepare-task.d.ts +9 -136
  50. package/dist/core/runner/prepare-task.js +44 -2741
  51. package/dist/core/runner/prepare-turn-wiring.d.ts +154 -0
  52. package/dist/core/runner/prepare-turn-wiring.js +201 -0
  53. package/dist/core/runner/prepare-wiring-manifest.d.ts +11 -3
  54. package/dist/core/runner/prepare-wiring-manifest.js +9 -2
  55. package/dist/core/runner/prepare-workspace-restore.d.ts +2 -29
  56. package/dist/core/runner/prepare-workspace-restore.js +3 -16
  57. package/dist/core/runner/prompt-hash-salt.d.ts +1 -0
  58. package/dist/core/runner/prompt-hash-salt.js +2 -0
  59. package/dist/core/runner/remote-env-retry.d.ts +29 -0
  60. package/dist/core/runner/remote-env-retry.js +16 -0
  61. package/dist/core/runner/runtask.js +1 -1
  62. package/dist/core/session.d.ts +12 -0
  63. package/dist/core/session.js +3 -0
  64. package/package.json +1 -1
  65. /package/dist/core/runner/{prepare-announce-once.d.ts → announce-once-ledger.d.ts} +0 -0
  66. /package/dist/core/runner/{prepare-announce-once.js → announce-once-ledger.js} +0 -0
@@ -0,0 +1,177 @@
1
+ /**
2
+ * design/390 §1.2 M20 — prepareTask's two GATE STATIONS, verbatim from the driver: the `tool_call` station (the final-roster
3
+ * effect sync, the write-protection judge compiled once per task, the arming predicate, the compliance and plan-mode
4
+ * short-circuits, the `runToolGate` call with every seat it is handed, the lifecycle sweep, the deferred sandbox-admission
5
+ * disclosure, the PreToolUse context stash, the settlement sideband write and the bare-human-rejection boundary) and the
6
+ * `tool_result` station (the PostToolUse / PostToolUseFailure seats and the PreToolUse context drain). The two module-scope
7
+ * helpers only these stations called moved with them (the settlement screen, the rejection-boundary judgment). The
8
+ * interface is the dependency list the stations had implicitly (design/238 R-1).
9
+ *
10
+ * The stations REGISTER handlers on the harness (the side effect this phase exists for) and report only two observation
11
+ * seats back; the order they register in — after the file-history boundary lease the driver registers, and in this order
12
+ * (call before result) — is the order handlers run in.
13
+ *
14
+ * SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await of its own (the handlers run per tool call / per tool
15
+ * result), so the phase adds no yield between the park closure before it and the context lane after it (design/390 §1.5).
16
+ * Read-stability of the host handles for the call: {@link RunInternals} (`@contract prepare.deps-read-stable`).
17
+ *
18
+ * The `tool_call` station is built exactly when the ask lane and the park closure are (the same predicate as the whole
19
+ * gate-machinery block, kept as the block's one guard here); the `tool_result` station keys off the hook record alone, as
20
+ * it always did.
21
+ *
22
+ * Every station this file holds is a `denySource` / settlement WRITE site the decomposition's next slice maps onto one
23
+ * gate outcome; nothing here changes a word, a source or an order — the stations are moved, not re-adjudicated.
24
+ */
25
+ import type { AgentHarness, AgentTool } from "../../internal/harness.js";
26
+ import type { AskClass } from "../ask-class.js";
27
+ import type { AutoModeDecider, AutoModeDenialTracker } from "../auto-mode.js";
28
+ import { type ComplianceCapability } from "../compliance.js";
29
+ import { type HookEnvCapabilities, type HookInvocationIdentity, type Hooks } from "../hooks.js";
30
+ import { type ToolPolicy } from "../tool-policy.js";
31
+ import type { RunnerDeps, TaskSpec, ToolEffect } from "../types.js";
32
+ import type { CwdRef } from "../../tools/fs/fs-shared.js";
33
+ import type { AskLane, IrreversibilityTier, ParkAsk, Prepared, ReversibilityProbes } from "./contracts.js";
34
+ import type { StopForDenialLimit } from "./denial-limit-arms.js";
35
+ import type { PermissionRuleLanes } from "./permission-rule-lanes.js";
36
+ export interface PrepareGateStationsInput {
37
+ /** borrowed-readonly — the in-stream ask lane, or undefined when the gate machinery is inactive (then no `tool_call`
38
+ * station is built). Read: `adjudicate`, `resolveAskBound`, `notifyPermissionDenied`, `notifyHookError`. */
39
+ askLane: AskLane | undefined;
40
+ /** borrowed-readonly — the park closure and the content-ask routing closure, or undefined (same law). Handed to the gate. */
41
+ parkAsk: ParkAsk | undefined;
42
+ /** borrowed-readonly — the FINAL mount roster: iterated once for the effect sync and the write-protection arming. Never
43
+ * mutated here. */
44
+ tools: AgentTool[];
45
+ /** borrowed-mutable — the effect collection. Writer here: the final-roster sync fills MISSING entries from each
46
+ * first-party tool's declared effect (never overrides a scanned one); the plan-mode deny reads it per call. */
47
+ toolEffects: Map<string, ToolEffect>;
48
+ /** borrowed-readonly — the deployment seats these stations read, as a Pick over the SAME `deps` object (receiver
49
+ * preserved for `deps.onError?.()`): the write-protection table and its data root, the tracer, the error face. */
50
+ deps: Pick<RunnerDeps, "writeProtectedPaths" | "memoryEngineDir" | "tracer" | "onError">;
51
+ /** borrowed-mutable — the protocol-tools phase's arming belt. Writer here: `armed` ← the registration predicate, the
52
+ * one write; the refresh seam reads it. */
53
+ toolCallGateArmedRef: {
54
+ armed: boolean;
55
+ };
56
+ /** borrowed-readonly — the policy chain's effective policy, or undefined; presence only (the arming predicate). */
57
+ effectivePolicy: ToolPolicy | undefined;
58
+ /** borrowed-readonly — the resolved hook record: `preToolUse` presence (arming), the two post seats and the failure seat
59
+ * (the `tool_result` station), called through the record so a class-instance keeps its receiver. */
60
+ hooks: Hooks | undefined;
61
+ /** borrowed-readonly — the egress vocabulary: arming, and the per-call `egress` flag handed to the gate. */
62
+ egressTools: ReadonlySet<string>;
63
+ /** borrowed-readonly — the irreversible vocabulary: arming only. */
64
+ irreversibleTools: ReadonlySet<string>;
65
+ /** borrowed-readonly — the REBOUND spec: `enablePlanMode` (arming) and `signal` (the failure seat's interrupt fact). */
66
+ spec: Pick<TaskSpec, "enablePlanMode" | "signal">;
67
+ /** borrowed-readonly — the compliance denials (the web_fetch veto's call-time lock, and its arming term). */
68
+ complianceDenies: ReadonlySet<ComplianceCapability>;
69
+ /** borrowed-mutable — the built harness. Writer here: `on("tool_call")` / `on("tool_result")` — the registrations this
70
+ * phase exists for, in this order, after the driver's boundary-lease handler. */
71
+ harness: Pick<AgentHarness, "on">;
72
+ /** borrowed-mutable — the blocked/suspended call ids (`Prepared.blockedToolCalls`). Writers here: the head-of-evaluation
73
+ * hygiene `delete`, the two short-circuits' and the gate outcome's `add`, the `tool_result` station's one-shot `delete`. */
74
+ blockedToolCalls: Set<string>;
75
+ /** borrowed-mutable — the duplicate-frame ask-grant record. Writer here: the head-of-evaluation and lifecycle-end `delete`. */
76
+ inheritedAskGrants: Map<string, unknown>;
77
+ /** borrowed-mutable — the bare-human-rejection sideband. Writer here: the head-of-evaluation `delete` and the lifecycle
78
+ * sweep whose `delete` IS the read the boundary judgment consumes. */
79
+ humanBareRejections: Set<string>;
80
+ /** borrowed-mutable — the inherited-unavailable marker set. Writer here: the lifecycle-end `delete`; `has` is handed to
81
+ * the gate as the read-only `isMarkedUnresolvable`. */
82
+ inheritedUnavailableAsks: Set<string>;
83
+ /** borrowed-mutable — the per-call ask-class record the sandbox admission reads. Writer here: the lifecycle-end `delete`. */
84
+ foldAskClasses: Map<string, Array<{
85
+ cls: AskClass;
86
+ layer: string;
87
+ }>>;
88
+ /** borrowed-mutable — the deferred ancestor sandbox admissions. Writer here: the unconditional read-and-`delete` after the
89
+ * fold settles. */
90
+ ancestorSandboxAdmissions: Map<string, Array<{
91
+ cls: AskClass;
92
+ layer: string;
93
+ }>>;
94
+ /** borrowed-mutable — PreToolUse `additionalContext` per call id. Writers here: the `tool_call` station's stash (`set`, only
95
+ * for calls that will execute) and the `tool_result` station's one-shot drain (`delete`). */
96
+ preToolContexts: Map<string, string[]>;
97
+ /** borrowed-mutable — the settlement sideband (`Prepared.approvalSettlement`). Writer here: the ONE screened `set` per
98
+ * gate outcome; the frame mint reads and deletes. */
99
+ approvalSettlement: Prepared["approvalSettlement"];
100
+ /** borrowed-mutable — the turn halt fact (`Prepared.batchHaltRef`). Writer here: the bare-rejection boundary (`current`). */
101
+ batchHaltRef: Prepared["batchHaltRef"];
102
+ /** borrowed-readonly — whether blocked/suspended ids are tracked at all (the post-tool callbacks exist). */
103
+ blockedTracked: boolean;
104
+ /** borrowed-readonly — the leg's identity envelope every HookToolContext and PermissionDeniedPayload carries. */
105
+ hookIdentity: HookInvocationIdentity;
106
+ /** borrowed-readonly — the leg's reminder provenance mark (every model-facing deny reason and hook context carries it). */
107
+ reminderMark: string;
108
+ /** borrowed-readonly — the plan-mode latch (`Prepared.planModeRef`); `active` is read per call. Not written here. */
109
+ planModeRef: Prepared["planModeRef"];
110
+ /** borrowed-readonly — the per-task identity (the trace rows' taskId). */
111
+ hostTaskId: string;
112
+ /** borrowed-readonly — the acquired session id (the settlement defects' error tag). */
113
+ sessionId: string;
114
+ /** borrowed-readonly — the screening face as the gate must see it (the observational wrapper or the face itself). */
115
+ ownGatePreToolUse: Hooks["preToolUse"] | undefined;
116
+ /** borrowed-readonly — the validated per-prepare hook seat bound. */
117
+ hookTimeoutMs: number;
118
+ /** borrowed-readonly — the hand's live tracked cwd, or undefined; `current` is read per screening / per post seat. Not
119
+ * written here. */
120
+ handsCwdRef: CwdRef | undefined;
121
+ /** borrowed-readonly — the read-only env capability face every hook context carries, or undefined. */
122
+ hookEnvFace: HookEnvCapabilities | undefined;
123
+ /** borrowed-readonly — the per-tool irreversibility tier handed to the gate per call. */
124
+ irreversibilityTier: IrreversibilityTier;
125
+ /** borrowed-readonly — the per-tool reversibility probes handed to the gate per call. */
126
+ reversibilityProbes: ReversibilityProbes;
127
+ /** borrowed-readonly — prepare's abort seat; only `.signal` is read (the gate's probe bound, the post seats' bound, the
128
+ * failure seat's interrupt fact). */
129
+ abortController: {
130
+ readonly signal: AbortSignal;
131
+ };
132
+ /** borrowed-readonly — whether Bash is coarsely shell-gated (the gate's `shellGated` attribution). */
133
+ shellGatedBash: boolean;
134
+ /** borrowed-readonly — whether Monitor is coarsely shell-gated (same). */
135
+ shellGatedMonitor: boolean;
136
+ /** borrowed-readonly — the auto-mode decider, or undefined (the classifier lane the gate routes a surviving ask through). */
137
+ autoModeDecider: AutoModeDecider | undefined;
138
+ /** borrowed-readonly — the auto-mode denial tracker, or undefined (handed to the gate beside the decider). */
139
+ autoModeDenialTracking: AutoModeDenialTracker | undefined;
140
+ /** borrowed-readonly — the headless denial-limit stop seat (wired unconditionally). */
141
+ stopForDenialLimit: StopForDenialLimit;
142
+ /** borrowed-readonly — the persisted allow-rule lane, or undefined (absent entirely with no store). */
143
+ permissionRuleLane: PermissionRuleLanes["personal"];
144
+ /** borrowed-readonly — the org rule lane, or undefined (absent on an ungoverned deployment). */
145
+ permissionRuleOrgLane: PermissionRuleLanes["org"];
146
+ /** borrowed-readonly — whether the ENGINE's question tool is what is mounted under the reserved name (the org lane's
147
+ * content-ask exemption predicate). */
148
+ questionToolMounted: boolean;
149
+ /** borrowed-readonly — whether sandbox admission is armed (isolated env + park prerequisites). */
150
+ sandboxAdmissionArmed: boolean;
151
+ /** borrowed-readonly — the per-tool boundary-capability fact the sandbox admission reads. */
152
+ sandboxBoundaryCapable: (toolName: string) => boolean;
153
+ /** borrowed-readonly — the sandbox-admission trace sink (the deferred ancestor disclosure and the gate's own). */
154
+ emitSandboxAdmitted: (info: {
155
+ toolCallId: string;
156
+ toolName: string;
157
+ askClasses: ReadonlyArray<{
158
+ cls: AskClass;
159
+ layer: string;
160
+ }>;
161
+ boundary: "sandbox_internal";
162
+ }) => void;
163
+ /** borrowed-readonly — the effective delegation facts (the rejection boundary's isDelegatedChild). */
164
+ delegation: {
165
+ isDelegatedChild: boolean;
166
+ };
167
+ /** borrowed-readonly — the own hook-crash reporter (an expired post seat is reported through it). */
168
+ notifyOwnHookCrash: (err: unknown) => void;
169
+ }
170
+ export interface PrepareGateStationsResult {
171
+ /** owned — whether the `tool_call` station registered (the arming predicate's value; observation seat only). */
172
+ toolCallStationArmed: boolean;
173
+ /** owned — whether the `tool_result` station registered (a post or screening hook is wired; observation seat only). */
174
+ toolResultStationArmed: boolean;
175
+ }
176
+ /** The M20 phase body — prepareTask's two gate stations, verbatim (see the module header). */
177
+ export declare function prepareGateStations(input: PrepareGateStationsInput): PrepareGateStationsResult;
@@ -0,0 +1,290 @@
1
+ import { SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
2
+ import { complianceCallDenial } from "../compliance.js";
3
+ import { cloneObserverInput, formatHookFeedback, hookSeatExpiredError, runHookSeat, runToolGate } from "../hooks.js";
4
+ import { PRESENT_PLAN_TOOL_NAME } from "../present-plan-tool.js";
5
+ import { isApprovalSettledBy, isAskDenyResolution, screenApproverAttribution } from "../tool-policy.js";
6
+ import { emitTrace } from "../trace.js";
7
+ import { createWriteProtectionCheck } from "../write-protect.js";
8
+ import { PATH_CONFINABLE_WRITE_TOOLS } from "./session-rule-policy.js";
9
+ function screenGateSettlement(result, settling) {
10
+ const defects = [];
11
+ const reportedBy = result.settledBy;
12
+ let settledBy;
13
+ if (reportedBy !== undefined) {
14
+ if (!isApprovalSettledBy(reportedBy) || (!settling && reportedBy !== "human")) {
15
+ defects.push(`a tool-gate settlement reported settledBy "${String(reportedBy)}" on ${settling ? "a blocked" : "an executing"} call — ` +
16
+ `it is one of "human" / "timeout" / "aborted", and only "human" can be the source of a call that runs; the frame carries no source`);
17
+ }
18
+ else
19
+ settledBy = reportedBy;
20
+ }
21
+ const attribution = screenApproverAttribution(result.approver);
22
+ if (attribution.defect !== undefined) {
23
+ defects.push(`a tool-gate settlement reported an attribution this engine refuses: ${attribution.defect}; the frame carries no approver`);
24
+ }
25
+ const reportedResolution = result.resolution;
26
+ let resolution;
27
+ if (reportedResolution !== undefined) {
28
+ if (!isAskDenyResolution(reportedResolution) || !settling) {
29
+ defects.push(`a tool-gate settlement reported a deny resolution "${String(reportedResolution)}" on ${settling ? "a blocked" : "an executing"} call — ` +
30
+ `it is one of the closed ask-deny vocabulary and only a BLOCKED call can carry one; the frame carries no resolution`);
31
+ }
32
+ else
33
+ resolution = reportedResolution;
34
+ }
35
+ const reportedAutoDenied = result.autoDenied;
36
+ let autoDenied;
37
+ if (reportedAutoDenied === true) {
38
+ if (!settling)
39
+ defects.push("a tool-gate settlement reported autoDenied on an executing call — only a BLOCKED call can carry the marker; the frame carries none");
40
+ else
41
+ autoDenied = true;
42
+ }
43
+ const record = {
44
+ ...(settledBy !== undefined ? { settledBy } : {}),
45
+ ...(attribution.approver !== undefined ? { approver: attribution.approver } : {}),
46
+ ...(resolution !== undefined ? { resolution } : {}),
47
+ ...(autoDenied !== undefined ? { autoDenied } : {}),
48
+ };
49
+ return { ...(settledBy !== undefined || attribution.approver !== undefined || resolution !== undefined || autoDenied !== undefined ? { record } : {}), defects };
50
+ }
51
+ function maybeHumanRejectionHalt(input) {
52
+ if (!input.bare || input.settledBy !== "human" || input.isDelegatedChild)
53
+ return undefined;
54
+ return {
55
+ reason: `This tool call was NOT executed: the user rejected the "${input.toolName}" tool call in the same ` +
56
+ `assistant message, which stops the rest of the batch. Nothing was run for this call — ` +
57
+ `re-issue it after the user's direction only if it is still needed.`,
58
+ details: {
59
+ error: "gate.batch_halted",
60
+ code: "gate.batch_halted",
61
+ rejectedToolCallId: input.toolCallId,
62
+ rejectedToolName: input.toolName,
63
+ },
64
+ };
65
+ }
66
+ export function prepareGateStations(input) {
67
+ const { askLane, parkAsk, tools, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, humanBareRejections, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, approvalSettlement, batchHaltRef, blockedTracked, hookIdentity, reminderMark, planModeRef, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, irreversibilityTier, reversibilityProbes, abortController, shellGatedBash, shellGatedMonitor, autoModeDecider, autoModeDenialTracking, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash } = input;
68
+ if (askLane !== undefined && parkAsk !== undefined) {
69
+ const { adjudicate, resolveAskBound, notifyPermissionDenied, notifyHookError } = askLane;
70
+ const { resolveContentAsk, suspendAsk } = parkAsk;
71
+ for (const t of tools) {
72
+ const eff = t.effect;
73
+ if (eff !== undefined && !toolEffects.has(t.name))
74
+ toolEffects.set(t.name, eff);
75
+ }
76
+ const writeProtectionCheck = createWriteProtectionCheck(deps.writeProtectedPaths, deps.memoryEngineDir !== undefined ? { dataRoot: deps.memoryEngineDir } : undefined);
77
+ const writeProtectionArmed = writeProtectionCheck !== undefined && tools.some((t) => PATH_CONFINABLE_WRITE_TOOLS.has(t.name));
78
+ toolCallGateArmedRef.armed =
79
+ effectivePolicy !== undefined ||
80
+ hooks?.preToolUse !== undefined ||
81
+ egressTools.size > 0 ||
82
+ irreversibleTools.size > 0 ||
83
+ spec.enablePlanMode === true ||
84
+ complianceDenies.has("web_fetch") ||
85
+ writeProtectionArmed;
86
+ if (toolCallGateArmedRef.armed) {
87
+ harness.on("tool_call", async (e) => {
88
+ blockedToolCalls.delete(e.toolCallId);
89
+ inheritedAskGrants.delete(e.toolCallId);
90
+ humanBareRejections.delete(e.toolCallId);
91
+ {
92
+ const complianceDeny = complianceCallDenial(complianceDenies, e.toolName);
93
+ if (complianceDeny !== undefined) {
94
+ if (blockedTracked)
95
+ blockedToolCalls.add(e.toolCallId);
96
+ if (notifyPermissionDenied) {
97
+ await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(e.input), toolCallId: e.toolCallId, reason: complianceDeny, source: "safety", identity: hookIdentity });
98
+ }
99
+ return { block: true, reason: formatHookFeedback(complianceDeny, reminderMark), preToolContext: [] };
100
+ }
101
+ }
102
+ if (planModeRef.active && (toolEffects.get(e.toolName) ?? "write") !== "read") {
103
+ if (blockedTracked)
104
+ blockedToolCalls.add(e.toolCallId);
105
+ const planDenyReason = `Plan mode is active — "${e.toolName}" is a write/mutating tool and is read-only-blocked. ` +
106
+ `Research with read-only tools, then call ${PRESENT_PLAN_TOOL_NAME} with your plan to get it approved before acting.`;
107
+ if (notifyPermissionDenied) {
108
+ await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(e.input), toolCallId: e.toolCallId, reason: planDenyReason, source: "planMode", identity: hookIdentity });
109
+ }
110
+ return {
111
+ block: true,
112
+ reason: formatHookFeedback(planDenyReason, reminderMark),
113
+ preToolContext: [],
114
+ };
115
+ }
116
+ let result;
117
+ let humanBareRejection = false;
118
+ try {
119
+ result = await runToolGate({
120
+ onNotifyError: (f) => emitTrace(deps.tracer, () => ({ kind: "observer.notify_failed", version: 1, taskId: hostTaskId, site: f.site, message: f.error.message, ts: Date.now() })),
121
+ event: e,
122
+ ...(e.signal !== undefined ? { callSignal: e.signal } : {}),
123
+ identity: hookIdentity,
124
+ reminderMark,
125
+ preToolUse: ownGatePreToolUse,
126
+ hookTimeoutMs,
127
+ ...(handsCwdRef !== undefined ? { trackedCwd: () => handsCwdRef.current } : {}),
128
+ ...(hookEnvFace !== undefined ? { hookEnv: hookEnvFace } : {}),
129
+ adjudicate,
130
+ resolveAsk: resolveAskBound,
131
+ suspendAsk,
132
+ resolveContentAsk,
133
+ egress: egressTools.has(e.toolName),
134
+ peerMessage: e.toolName === SEND_MESSAGE_TOOL_NAME,
135
+ ...(writeProtectionCheck !== undefined ? { writeProtectionCheck } : {}),
136
+ irreversibility: irreversibilityTier.get(e.toolName),
137
+ reversibilityProbe: reversibilityProbes.get(e.toolName),
138
+ abortSignal: abortController.signal,
139
+ ...(notifyPermissionDenied ? { permissionDenied: notifyPermissionDenied } : {}),
140
+ onHookError: notifyHookError,
141
+ shellGated: (e.toolName === "Bash" && shellGatedBash) || (e.toolName === "Monitor" && shellGatedMonitor),
142
+ ...(autoModeDecider ? { autoMode: { decider: autoModeDecider, ...(autoModeDenialTracking !== undefined ? { denialTracking: autoModeDenialTracking } : {}) } } : {}),
143
+ onHeadlessDenialLimit: stopForDenialLimit,
144
+ ...(permissionRuleLane
145
+ ? {
146
+ persistedRules: {
147
+ admits: permissionRuleLane.admits,
148
+ onResolved: (info) => emitTrace(deps.tracer, () => ({
149
+ kind: "permission.persisted_rule_allowed",
150
+ version: 2,
151
+ taskId: hostTaskId,
152
+ toolName: info.toolName,
153
+ toolCallId: info.toolCallId,
154
+ rules: [...info.rules],
155
+ ts: Date.now(),
156
+ })),
157
+ },
158
+ }
159
+ : {}),
160
+ ...(permissionRuleOrgLane
161
+ ? {
162
+ orgRules: {
163
+ adjudicate: permissionRuleOrgLane.adjudicate,
164
+ contentAskToolMounted: questionToolMounted,
165
+ onUnavailable: (info) => emitTrace(deps.tracer, () => ({
166
+ kind: "permission.org_snapshot_unavailable",
167
+ version: 1,
168
+ taskId: hostTaskId,
169
+ toolName: info.toolName,
170
+ toolCallId: info.toolCallId,
171
+ message: info.message,
172
+ ts: Date.now(),
173
+ })),
174
+ },
175
+ }
176
+ : {}),
177
+ isMarkedUnresolvable: (toolCallId) => inheritedUnavailableAsks.has(toolCallId),
178
+ ...(sandboxAdmissionArmed
179
+ ? {
180
+ sandboxAdmission: {
181
+ askClassesOf: (toolCallId) => foldAskClasses.get(toolCallId),
182
+ boundaryCapable: sandboxBoundaryCapable(e.toolName),
183
+ onAdmitted: emitSandboxAdmitted,
184
+ },
185
+ }
186
+ : {}),
187
+ });
188
+ }
189
+ finally {
190
+ inheritedUnavailableAsks.delete(e.toolCallId);
191
+ inheritedAskGrants.delete(e.toolCallId);
192
+ foldAskClasses.delete(e.toolCallId);
193
+ humanBareRejection = humanBareRejections.delete(e.toolCallId);
194
+ }
195
+ const ancestorAdmitted = ancestorSandboxAdmissions.get(e.toolCallId);
196
+ ancestorSandboxAdmissions.delete(e.toolCallId);
197
+ if (ancestorAdmitted !== undefined && !result.block && !result.suspend) {
198
+ emitSandboxAdmitted({ toolCallId: e.toolCallId, toolName: e.toolName, askClasses: ancestorAdmitted, boundary: "sandbox_internal" });
199
+ result.preToolContext.push(`permission.sandbox_admitted: this call's inherited pending approval was auto-admitted because the deployment declares an isolated sandbox execution env and the request stays inside it. Recorded for audit.`);
200
+ }
201
+ if (!result.block && !result.suspend && result.preToolContext.length > 0) {
202
+ preToolContexts.set(e.toolCallId, result.preToolContext);
203
+ }
204
+ if (blockedTracked && (result.block || result.suspend)) {
205
+ blockedToolCalls.add(e.toolCallId);
206
+ }
207
+ const settlement = screenGateSettlement(result, result.block === true);
208
+ for (const defect of settlement.defects)
209
+ deps.onError?.(new Error(defect), { phase: "config", sessionId });
210
+ if (settlement.record !== undefined)
211
+ approvalSettlement.set(e.toolCallId, settlement.record);
212
+ if (result.block) {
213
+ const haltRemaining = maybeHumanRejectionHalt({
214
+ bare: humanBareRejection,
215
+ settledBy: result.settledBy,
216
+ isDelegatedChild: delegation.isDelegatedChild === true,
217
+ toolName: e.toolName,
218
+ toolCallId: e.toolCallId,
219
+ });
220
+ if (haltRemaining !== undefined) {
221
+ batchHaltRef.current = { rejectedToolCallId: e.toolCallId };
222
+ return { block: true, reason: result.reason, haltRemaining };
223
+ }
224
+ return { block: true, reason: result.reason };
225
+ }
226
+ return result.updatedInput !== undefined ? { updatedInput: result.updatedInput } : undefined;
227
+ });
228
+ }
229
+ }
230
+ const toolResultStationArmed = Boolean(hooks?.postToolUse || hooks?.preToolUse || hooks?.postToolUseFailure);
231
+ if (toolResultStationArmed) {
232
+ harness.on("tool_result", async (e) => {
233
+ if (blockedToolCalls.delete(e.toolCallId)) {
234
+ return undefined;
235
+ }
236
+ let content = e.content;
237
+ let changed = false;
238
+ if (e.isError) {
239
+ if (hooks?.postToolUseFailure) {
240
+ let clonedDetails = e.details;
241
+ try {
242
+ clonedDetails = structuredClone(e.details);
243
+ }
244
+ catch {
245
+ }
246
+ const failureSeat = await runHookSeat("postToolUseFailure", { timeoutMs: hookTimeoutMs, signal: abortController.signal }, (sig) => hooks.postToolUseFailure(e.toolName, e.input, {
247
+ error: e.content
248
+ .filter((c) => c.type === "text")
249
+ .map((c) => c.text)
250
+ .join("\n"),
251
+ isInterrupt: abortController.signal.aborted || spec.signal?.aborted === true,
252
+ content: e.content.map((c) => ({ ...c })),
253
+ details: clonedDetails,
254
+ }, { toolCallId: e.toolCallId, toolName: e.toolName, signal: sig, ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}), ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity }));
255
+ if (failureSeat.expired) {
256
+ notifyOwnHookCrash(hookSeatExpiredError("postToolUseFailure", hookTimeoutMs, failureSeat.cause, "the observation was abandoned and its additionalContext dropped; the failure result itself is unchanged"));
257
+ }
258
+ const patch = failureSeat.expired ? undefined : failureSeat.value;
259
+ if (patch?.additionalContext) {
260
+ content = [...content, { type: "text", text: formatHookFeedback(patch.additionalContext, reminderMark) }];
261
+ changed = true;
262
+ }
263
+ }
264
+ }
265
+ else if (hooks?.postToolUse) {
266
+ const successSeat = await runHookSeat("postToolUse", { timeoutMs: hookTimeoutMs, signal: abortController.signal }, (sig) => hooks.postToolUse(e.toolName, e.input, { content: e.content, details: e.details, isError: e.isError }, { toolCallId: e.toolCallId, toolName: e.toolName, signal: sig, ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}), ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity }));
267
+ if (successSeat.expired) {
268
+ notifyOwnHookCrash(hookSeatExpiredError("postToolUse", hookTimeoutMs, successSeat.cause, "the seat was abandoned; the tool result is delivered to the model UNMODIFIED (no updatedOutput, no additionalContext)"));
269
+ }
270
+ const patch = successSeat.expired ? undefined : successSeat.value;
271
+ if (patch?.updatedOutput) {
272
+ content = patch.updatedOutput;
273
+ changed = true;
274
+ }
275
+ if (patch?.additionalContext) {
276
+ content = [...content, { type: "text", text: formatHookFeedback(patch.additionalContext, reminderMark) }];
277
+ changed = true;
278
+ }
279
+ }
280
+ const pre = preToolContexts.get(e.toolCallId);
281
+ if (pre) {
282
+ preToolContexts.delete(e.toolCallId);
283
+ content = [...content, ...pre.map((t) => ({ type: "text", text: formatHookFeedback(t, reminderMark) }))];
284
+ changed = true;
285
+ }
286
+ return changed ? { content } : undefined;
287
+ });
288
+ }
289
+ return { toolCallStationArmed: toolCallGateArmedRef.armed, toolResultStationArmed };
290
+ }
@@ -160,9 +160,9 @@ export interface PrepareHandsReadFaceInput {
160
160
  * when no file restore ran. Fail-closed by construction — dropping a read record only ever
161
161
  * produces refusals. */
162
162
  restoredFilePaths?: readonly string[];
163
- /** borrowed (ref cell; the REF itself is never replaced) design/138 S2-C late-bound memory
163
+ /** borrowed-readonly (ref cell; the REF itself is never replaced) design/138 S2-C late-bound memory
164
164
  * write gate: the band's beforeWrite closure reads `.current` call-time; prepareMemory (driver,
165
- * after this phase) installs the gate. Declared in the driver because the SAME ref feeds
165
+ * after this phase) installs the gate — the one writer; this phase only reads. Declared in the driver because the SAME ref feeds
166
166
  * prepareMemory. */
167
167
  memoryWriteGateRef: {
168
168
  current?: BeforeWriteHook;
@@ -199,10 +199,10 @@ export interface PrepareHandsReadFaceInput {
199
199
  * band's Read/notebook/PDF trailer outlets bump keys on it at tool-execute time (the driver owns
200
200
  * the object and folds it into result stats). */
201
201
  reminderDisclosureCounts: import("../reminder-disclosure.js").ReminderDisclosureCounts;
202
- /** design/381 the first-touch history hook prepare built (present iff a fileHistoryStore is
202
+ /** borrowed-readonly — design/381: the first-touch history hook prepare built (present iff a fileHistoryStore is
203
203
  * wired and the run mounts a real fs env); threaded to the write band's trackFileEdit seat. */
204
204
  trackFileEdit?: import("../types.js").TrackFileEditHook;
205
- /** borrowed — the mutation lane's LANDED observation seat (the accumulator behind
205
+ /** borrowed-readonly — the mutation lane's LANDED observation seat (the accumulator behind
206
206
  * `TaskResult.editedFiles`); threaded to the write band's onFileEdited seat. Unlike
207
207
  * `trackFileEdit` it does NOT depend on a wired history store: it observes what this run's hands
208
208
  * did, so it is present on every run that mounts the band. */
@@ -238,13 +238,13 @@ export interface PrepareInheritedGateResult {
238
238
  notifyOwnHookCrash: (err: unknown) => void;
239
239
  /** owned — the FROZEN question seat (spec over deps), the content-ask twin of `frozenOnAsk`. */
240
240
  frozenOnQuestion: OnQuestion | undefined;
241
- /** borrowed-mutable — the delegation runtime-provenance arming face. Writers: the memory phase's content-origin wrap section,
242
- * once per leg when armed. Read lazily by delegation tools through the ctx. */
241
+ /** borrowed-mutable — the delegation runtime-provenance arming face. Writer: the memory-engine-session phase
242
+ * (prepareMemoryEngineSession), once per leg when it arms. Read lazily by delegation tools through the ctx. */
243
243
  provenanceForChildrenRef: {
244
244
  current?: DelegationContentSafety;
245
245
  };
246
- /** borrowed-mutable — the delegation-settlement coordinates. Writers: the memory phase, once per leg when a session exists.
247
- * Read lazily by delegation tools through the ctx. */
246
+ /** borrowed-mutable — the delegation-settlement coordinates. Writer: the memory-engine-session phase, once per leg when the
247
+ * mounted session carries a settlement seat. Read lazily by delegation tools through the ctx. */
248
248
  delegationSettlementRef: {
249
249
  current?: {
250
250
  controlDir: string;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * design/390 §1.2 M12 — prepareTask's MEMORY-ENGINE-SESSION phase, verbatim from the driver: the content-origin
3
+ * classification + one-way session pollution wrap section (design/178 §3 / design/180 half A / design/324 /
4
+ * design/336 §3.3 + §5.5 / design/378 D-4 / design/380 O9a). Its arming predicate (a mounted memory session OR an
5
+ * upstream recorder channel), the chain-narrowed classification config, the children's provenance seat, the
6
+ * declared-MCP-class announcement, the settlement seat, the per-tool wrap with its three record verbs (mark /
7
+ * recordExternal / recordIncomplete), the Read tool's recall-taint arm and the delegation arm's three-valued
8
+ * attestation judgment all live here; the announcement helper only this section called moved with it. The
9
+ * interface is the dependency list the segment had implicitly (design/238 R-1).
10
+ *
11
+ * SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await of its own (the awaits sit inside the wrappers it
12
+ * installs, which run at tool-call time), so the phase adds no yield between the deferred classification before it
13
+ * and the prompt assembly after it (design/390 §1.5: a stretch with no await is extracted as a sync function).
14
+ * Read-stability of the host handles for the call: {@link RunInternals} (`@contract prepare.deps-read-stable`).
15
+ *
16
+ * One read changed spelling and nothing else: the declared-class announcement read `mcp?.statuses` while `mcp` had
17
+ * long been the protocol-tools phase's non-null handle; the optional chain was dead and is now the plain member read.
18
+ */
19
+ import type { AgentTool } from "../../internal/harness.js";
20
+ import type { LockedPreflight } from "../locked-config.js";
21
+ import type { MaterializedMcp } from "../mcp.js";
22
+ import { type DelegationContentSafety } from "../memory-engine/delegation-provenance.js";
23
+ import type { RunnerDeps, TaskSpec } from "../types.js";
24
+ import type { Prepared, RunInternals } from "./contracts.js";
25
+ export interface PrepareMemoryEngineSessionInput {
26
+ /** borrowed-readonly — the mounted engine session, or undefined (the ARMING fact's first half). Read: `pollution`
27
+ * (the mark face), `contentSafety` (the own half of the narrowing), `settlement` (the seat + the receipt ledger),
28
+ * `recallTaint` (the Read arm's judge). Never mutated; the wrappers call `pollution.markPolluted` at tool-call time. */
29
+ memoryEngineSession: Prepared["memoryEngineSession"];
30
+ /** borrowed-mutable — the trusted spawn-side channel; only `delegationProvenance` (the ARMING fact's second half: the
31
+ * recorder channel with the chain's frozen `contentSafety`). Writers: the wrappers this phase installs set
32
+ * `delegationProvenance.ref.current.sawExternal` / `.incomplete` at tool-call time (monotonic, never cleared). */
33
+ internals: Pick<RunInternals, "delegationProvenance"> | undefined;
34
+ /** borrowed-readonly — the doors' per-leg SNAPSHOT of `RunnerDeps.memoryDelegationEvidence` (screened in the
35
+ * synchronous pre-await stretch; never a live deps re-read here). */
36
+ memoryDelegationEvidence: "static-face" | "attested-only";
37
+ /** borrowed-readonly — the run's external-content-target fold (live adapter declaration OR the persisted bit). */
38
+ externalContentTargetActive: boolean;
39
+ /** borrowed-readonly — the frozen preflight; only `mcp` (the declared entries the announcement walks). */
40
+ lockedPreflight: Pick<LockedPreflight, "mcp">;
41
+ /** borrowed-readonly — the materialized MCP handle; only `statuses` (the announcement's mounted-tool count). */
42
+ mcp: Pick<MaterializedMcp, "statuses">;
43
+ /** borrowed-readonly — the acquired session id (notice detail). */
44
+ sessionId: string;
45
+ /** borrowed-readonly — the run's invocation id (notice detail; the once-per-leg claims are per-RUN claims). */
46
+ runId: string;
47
+ /** borrowed-readonly — the deployment seats; only `onNotice` (the declared-class line and the waiver notice). */
48
+ deps: Pick<RunnerDeps, "onNotice">;
49
+ /** borrowed-mutable — the children's provenance seat (the inherited-gate phase mints it; `inheritedGateForChildren`
50
+ * reads it late). Writer: THIS phase, once at arming, to the effective (chain-narrowed) config. */
51
+ provenanceForChildrenRef: {
52
+ current?: DelegationContentSafety;
53
+ };
54
+ /** borrowed-mutable — the children's settlement seat (the inherited-gate phase mints it). Writer: THIS phase, once at
55
+ * arming, when the memory session mounted a settlement (memoryProvenance "carry"). */
56
+ delegationSettlementRef: {
57
+ current?: {
58
+ controlDir: string;
59
+ sessionId: string;
60
+ };
61
+ };
62
+ /** borrowed-readonly — the REBOUND spec; only `tools` (the RAW caller specs: caller-tool names and the marker-bearing
63
+ * delegation entries — the assembled roster objects carry neither marker). Never mutated. */
64
+ spec: Pick<TaskSpec, "tools">;
65
+ /** borrowed-readonly — the env-hand roster (membership minted at the mount sites); read by the classification. */
66
+ envHandToolNames: ReadonlySet<string>;
67
+ /** borrowed-mutable — the run's ONE tool roster. Writer here: the wrap pass REPLACES entries in place
68
+ * (`tools[i] = wrapped`, identity-tracked so a re-run never stacks wrappers); see the roster's writer table on
69
+ * {@link Prepared.tools}. */
70
+ tools: AgentTool[];
71
+ /** borrowed-mutable — the re-runnable wrap pass's seat (the protocol-tools phase mints it; the rebuild seam re-runs it
72
+ * after a refresh re-splice). Writer: THIS phase installs the pass and runs it once. */
73
+ contentOriginWrapRef: {
74
+ current?: () => void;
75
+ };
76
+ }
77
+ export interface PrepareMemoryEngineSessionResult {
78
+ /** owned — whether the section ARMED on this leg (a memory session or a recorder channel existed): armed ⇒ every
79
+ * roster tool was classified and the polluting/delegation/Read-plane ones wrapped; not armed ⇒ zero wraps, zero
80
+ * announcements, both children's seats untouched. */
81
+ armed: boolean;
82
+ }
83
+ /** The M12 phase body — prepareTask's content-origin wrap stretch, verbatim (see the module header). */
84
+ export declare function prepareMemoryEngineSession(input: PrepareMemoryEngineSessionInput): PrepareMemoryEngineSessionResult;