@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,32 @@
1
+ export const DEFAULT_RESOURCE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
2
+ export const USAGE_WINDOW_REAP_MARGIN_MS = 60 * 60 * 1000;
3
+ export const DEFAULT_UNATTENDED_APPROVAL_TTL_MS = 30 * 24 * 60 * 60 * 1000;
4
+ export function sanitizedTtlMs(ttlMs) {
5
+ if (ttlMs === undefined)
6
+ return undefined;
7
+ return Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : DEFAULT_RESOURCE_TTL_MS;
8
+ }
9
+ export function gatedCallIdOf(p) {
10
+ if (p.suspendRef.token !== undefined)
11
+ return p.suspendRef.gatedCallId;
12
+ if (p.reviewRef.token !== undefined)
13
+ return p.reviewRef.gatedCallId;
14
+ return undefined;
15
+ }
16
+ export function parkContaminationMarker(refs) {
17
+ if (refs.suspendRef.token === undefined && refs.reviewRef.token === undefined)
18
+ return undefined;
19
+ const gatedCallId = gatedCallIdOf(refs);
20
+ return { code: "gate.parked", ...(gatedCallId !== undefined ? { gatedCallId } : {}) };
21
+ }
22
+ export function publishCommittedSuspend(refs, token, gate, scope, remoteHandle, checkpointId, pendingAction) {
23
+ const ref = gate.kind === "needs_review" || gate.kind === "plan_review" ? refs.reviewRef : refs.suspendRef;
24
+ ref.token = token;
25
+ if (checkpointId !== undefined)
26
+ ref.checkpointId = checkpointId;
27
+ ref.gate = gate;
28
+ ref.gatedCallId = pendingAction?.kind === "tool_approval" ? pendingAction.toolCallId : undefined;
29
+ if (remoteHandle !== undefined)
30
+ ref.restoreMode = remoteHandle.restoreMode === "park_only" ? "park_only" : "snapshot";
31
+ ref.scope = scope;
32
+ }
@@ -1,5 +1,6 @@
1
1
  /**
2
- * design/389 — prepare's permission-rule phase: the TWO gate lanes the unified store feeds.
2
+ * design/389 — the permission-rule LANES the unified store feeds (prepare-path machinery, design/390 L1): the two gate
3
+ * lanes, and — since the policy-chain phase took the chain assembly — the rule-offer factory every ask-mint site calls.
3
4
  *
4
5
  * `prepareTask` used to assemble the personal lane inline (a store read + a hand-spliced session
5
6
  * overlay + two admission arms) and the org lane from a second seam. Both now read the ONE store query —
@@ -23,8 +24,8 @@
23
24
  * silence the ungated-write warning for deployments that wired no policy at all.
24
25
  */
25
26
  import type { AskRuleEvidence, ToolCallRequest } from "../tool-policy.js";
26
- import type { OrgGateVerdict, PersistedRuleAnswer, PersistedRuleHit } from "../hooks.js";
27
- import type { PersistedAllowRule } from "../permission-rule-model.js";
27
+ import { persistedRuleMandateOf, type OrgGateVerdict, type PersistedRuleAnswer, type PersistedRuleHit } from "../hooks.js";
28
+ import type { PersistedAllowRule, RuleOffer, SegmentCoverage } from "../permission-rule-model.js";
28
29
  import { type OrgRuleResolution } from "../permission-rule-org.js";
29
30
  import type { PermissionRuleStoreProvider } from "../permission-rule-provider.js";
30
31
  /** The one SHELL tool the persisted-rule lane speaks for. */
@@ -130,3 +131,108 @@ export declare function createPermissionRuleLanes(cfg: {
130
131
  * lane took the read. */
131
132
  onDisclosure: (message: string) => void;
132
133
  }): PermissionRuleLanes;
134
+ /** The closed reason set for the rule-offer factory's empty answer (#490 修②) — the same three
135
+ * spellings the `AskRequest` and durable-row seats declare, kept at the one factory that fills both.
136
+ * Deliberately not exported: this is a wire vocabulary, and its two faces declare it literally (the
137
+ * `previewWithheld` precedent) so a consumer reads the closed set on the type it is holding rather
138
+ * than through an import. */
139
+ type RuleOffersAbsence = "mandated" | "lane_cannot_speak" | "shadowed";
140
+ /** The surviving-ask facts the rule-offer factory reads (see {@link createRuleOffersOf}): the
141
+ * synchronous mint sites spread the surviving decision, which carries them; the park leg threads
142
+ * each as its own parameter. */
143
+ interface RuleOffersAskFacts {
144
+ requiresRealApproval?: boolean;
145
+ persistedRuleShadowed?: string;
146
+ decisionReason?: import("../tool-policy.js").DecisionReason;
147
+ /** #457 ④: an EXPLICIT `ask` permission rule matched this call (design/127 DSL, stamped at
148
+ * the rule policy's three matched-ask exits — bare, covering and param). */
149
+ matchedAskRule?: string;
150
+ /** #502: the engine-stamped probe mandate (the reversibility probe declared this call's
151
+ * demotion structural — for the built-in shell probe, a read outside the session's
152
+ * allowed directories). */
153
+ probeMandated?: boolean;
154
+ inheritedUnresolved?: boolean;
155
+ /** The ask is being resolved at an ANCESTOR's frozen approver (the three inherited-lane
156
+ * mint sites pass it literally) — this task's rule lane never adjudicates it. */
157
+ ancestorResolved?: boolean;
158
+ /** design/375 §5.2②: the surviving ask's engine-stamped per-segment coverage table — the
159
+ * batch offer carries exactly the segments this table calls uncovered. Absent ⇒ all
160
+ * segments read as uncovered (over-offer, the safe direction). */
161
+ segmentCoverage?: readonly SegmentCoverage[];
162
+ }
163
+ /**
164
+ * design/179 §4 — the rule forms that could cover this exact call, for a surface's "stop asking me
165
+ * this" option (extracted from the prepare body, the spliceSessionOverlayRows precedent; the maker
166
+ * captures the per-task wiring once and the returned factory serves all five ask-mint sites — the
167
+ * approval-preview field is a standing example of what happens otherwise: it reached two of the
168
+ * five, so a delegated child's card was silently poorer than the top-level one).
169
+ *
170
+ * Empty unless a rule lane is armed — offering an option that redemption would refuse is worse than
171
+ * offering none — and empty for any call the lane cannot speak for (a compound, a redirection, a
172
+ * substitution, or a tool that is not the shell tool).
173
+ *
174
+ * Empty, too, on any ask a persisted rule could never CLEAR — "allow rules silence the classifier's
175
+ * questions, never a mandated one". Two doors, matching the two halves of that boundary:
176
+ * · the marks half, judged from the SAME resolved sources the gate input is built from, through the
177
+ * gate's own single-source predicate (an operator's shellGate:"always", the tool's own
178
+ * egress/irreversibility marks, and — #502 — the PER-CALL member that predicate also takes: a
179
+ * demotion this call's reversibility probe declared structural, read off the ask the gate
180
+ * stamped rather than off the tool seat, because it is true of one call and not of the seat) —
181
+ * a drift here would offer a rule the lane then refuses to honor;
182
+ * · the decision half (`ask`, REQUIRED at every mint site so a site added later cannot skip it):
183
+ * `requiresRealApproval` (org/governance — only judgment clears it), the shadowed-rule
184
+ * disclosure (the standing proof that a matching rule does not clear THIS ask), a hook-raised
185
+ * ask (`decisionReason === "hook"` — the lane's own clearing conjuncts refuse hook asks, so a
186
+ * rule minted from one would never silence it), an EXPLICIT `ask` rule match (`matchedAskRule`
187
+ * — see below), and an inherited-unresolved marked call (the
188
+ * ask is an ANCESTOR's authority, which this task's rule lane never adjudicates). Offering
189
+ * "stop asking me this" on a card that will keep asking lets a person mint a rule that never
190
+ * takes effect where they minted it.
191
+ *
192
+ * #457 ④ (CC 2.1.245 toolPolicy 对表, F11: "wildcard `ask` … keeps every matching tool behind a
193
+ * per-call prompt (no persistent always-allow)") — `matchedAskRule` was the one conjunct of the
194
+ * lane's own clearing predicate this door did not mirror, and the asymmetry was live: the
195
+ * persisted-rule lane refuses to clear ANY ask carrying it (`hooks.ts`, the
196
+ * `decision.matchedAskRule === undefined` conjunct on its allow arm), so a person who wrote a
197
+ * standing `Bash` `ask` rule and had no allow rule yet was still offered "allow, and stop asking
198
+ * me this" — and the rule they minted came back on the very next call as a SHADOWED match
199
+ * (`persistedRuleShadowed`, which this door was already refusing). The offer was only ever
200
+ * suppressed one call too late, after the useless rule existed. The shadowed conjunct stays: it
201
+ * covers the person who ALREADY has the allow rule; this one covers the person about to mint it.
202
+ *
203
+ * #490 修② — every door above that answers "no offers" now also names WHICH door, on the
204
+ * `ruleOffersAbsence` seat (`AskRequest`'s and the durable row's, ONE factory so the two faces
205
+ * cannot disagree). The doctrine it serves is the loud-bad-value one, applied to an absence: a
206
+ * surface reading an empty card could not tell "there is nothing this lane could offer" from
207
+ * "offers were deliberately suppressed", and a person reading it could not tell "write the rule
208
+ * yourself" from "no rule can excuse this approval". The seat is CLOSED
209
+ * (`mandated | lane_cannot_speak | shadowed`), mutually exclusive with `ruleOffers` by
210
+ * construction, and structural-door silent (see the first two guards).
211
+ */
212
+ export declare function createRuleOffersOf(cfg: {
213
+ /** `permissionRuleLane !== undefined` at wiring time — the armed-lane structural door. */
214
+ laneArmed: boolean;
215
+ principal: string | undefined;
216
+ /** `deps.localOwnerRules === true` — the declared local-owner exception to the anonymous floor. */
217
+ localOwnerDeclared: boolean;
218
+ /** The live mark sources the gate input is built from — passed as REFERENCES (read per call, so a
219
+ * late-registered tool seat is seen exactly as the gate sees it). */
220
+ egressTools: {
221
+ has(name: string): boolean;
222
+ };
223
+ irreversibilityTier: {
224
+ get(name: string): Parameters<typeof persistedRuleMandateOf>[0]["irreversibility"];
225
+ };
226
+ shellGatedBash: boolean;
227
+ taskRoot: string | undefined;
228
+ /** The live tracked-cwd ref (read per call — the relative-cd resolution base, adversarial-review P1/r3). */
229
+ cwdRef: {
230
+ current: string;
231
+ } | undefined;
232
+ deniesDirectoryRead: ((directory: string) => boolean) | undefined;
233
+ }): (toolName: string, args: unknown, ask: RuleOffersAskFacts | undefined) => {
234
+ ruleOffers?: readonly RuleOffer[];
235
+ ruleOffersAbsence?: RuleOffersAbsence;
236
+ execCwd?: string;
237
+ };
238
+ export {};
@@ -1,4 +1,5 @@
1
- import { directoryRuleAdmits, eligiblePersisted, findAdmittingRule, lexicalNormalAbsolutePathOf, segmentCoverageOf } from "../permission-rule-model.js";
1
+ import { persistedRuleMandateOf } from "../hooks.js";
2
+ import { directoryRuleAdmits, eligiblePersisted, findAdmittingRule, lexicalNormalAbsolutePathOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
2
3
  import { orgRuleVerdictFor } from "../permission-rule-org.js";
3
4
  export const PERSISTED_RULE_TOOL = "Bash";
4
5
  export const DIRECTORY_RULE_TOOL = "Read";
@@ -138,3 +139,48 @@ export function createPermissionRuleLanes(cfg) {
138
139
  : undefined;
139
140
  return { personal, org };
140
141
  }
142
+ export function createRuleOffersOf(cfg) {
143
+ return (toolName, args, ask) => {
144
+ if (!cfg.laneArmed || toolName !== PERSISTED_RULE_TOOL)
145
+ return {};
146
+ if ((cfg.principal === undefined || cfg.principal === "") && !cfg.localOwnerDeclared)
147
+ return {};
148
+ const closedDoor = (() => {
149
+ if (ask?.requiresRealApproval === true)
150
+ return "mandated";
151
+ if (ask?.persistedRuleShadowed !== undefined)
152
+ return "shadowed";
153
+ if (ask?.decisionReason === "hook")
154
+ return "mandated";
155
+ if (ask?.matchedAskRule !== undefined)
156
+ return "shadowed";
157
+ if (ask?.inheritedUnresolved === true)
158
+ return "mandated";
159
+ if (ask?.ancestorResolved === true)
160
+ return "mandated";
161
+ return undefined;
162
+ })();
163
+ if (closedDoor !== undefined)
164
+ return { ruleOffersAbsence: closedDoor };
165
+ if (persistedRuleMandateOf({
166
+ egress: cfg.egressTools.has(toolName),
167
+ irreversibility: cfg.irreversibilityTier.get(toolName),
168
+ shellGated: cfg.shellGatedBash,
169
+ probeMandated: ask?.probeMandated === true,
170
+ }) !== undefined) {
171
+ return { ruleOffersAbsence: "mandated" };
172
+ }
173
+ const command = args?.command;
174
+ if (typeof command !== "string")
175
+ return { ruleOffersAbsence: "lane_cannot_speak" };
176
+ const offers = suggestRulesForCommand(command, {
177
+ ...(ask?.segmentCoverage !== undefined ? { coverage: ask.segmentCoverage } : {}),
178
+ ...(cfg.taskRoot !== undefined ? { scope: { kind: "project", root: cfg.taskRoot }, cwd: cfg.taskRoot } : {}),
179
+ ...(cfg.cwdRef?.current !== undefined ? { execCwd: cfg.cwdRef.current } : {}),
180
+ ...(cfg.deniesDirectoryRead !== undefined ? { deniesDirectoryRead: cfg.deniesDirectoryRead } : {}),
181
+ });
182
+ return offers.length > 0
183
+ ? { ruleOffers: offers, ...(cfg.cwdRef?.current !== undefined ? { execCwd: cfg.cwdRef.current } : {}) }
184
+ : { ruleOffersAbsence: "lane_cannot_speak" };
185
+ };
186
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * design/390 §1.2 M19 (ask lane) — prepareTask's IN-STREAM ASK LANE, verbatim from the driver's gate-machinery block:
3
+ * the per-call signal composition bound to this task's run signal, the abort-bound `adjudicate` wrapper (the budget
4
+ * snapshot and the live tracked cwd stamped onto every request), the approval-preview projection over the live roster,
5
+ * the `resolveAskBound` mint (the inherited-unavailable intercept, the duplicate-frame grant reuse, the human-review
6
+ * ledger, the bare-human-rejection record), the pre-wrapped deny observer and the hook-crash notifier. The three
7
+ * module-scope helpers only this segment called moved with it (the deny-observer factory, the preview sanitizer and
8
+ * resolver). The interface is the dependency list the segment had implicitly (design/238 R-1) — the closures below
9
+ * used to capture every seat here off the driver's scope.
10
+ *
11
+ * SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await of its own (every await lives inside the closures
12
+ * it builds, which run per tool call), so the phase adds no yield between the wiring-manifest station before it and
13
+ * the suspend saga after it (design/390 §1.5: a stretch with no await is extracted as a sync function). Read-stability
14
+ * of the host handles for the call: {@link RunInternals} (`@contract prepare.deps-read-stable`).
15
+ *
16
+ * The lane exists exactly when the wiring-manifest phase's `gateMachineryActive` holds — the driver's `if` around the
17
+ * whole block became this phase's one early return (the block's other four phases key off the lane's presence), so
18
+ * the orchestrator grows no branch.
19
+ */
20
+ import type { AgentTool } from "../../internal/harness.js";
21
+ import type { AutoModeDenialTracker } from "../auto-mode.js";
22
+ import { type Hooks } from "../hooks.js";
23
+ import { type OnAsk, type ToolCallRequest, type ToolPolicy } from "../tool-policy.js";
24
+ import type { RunnerDeps, TaskSpec } from "../types.js";
25
+ import type { CwdRef } from "../../tools/fs/fs-shared.js";
26
+ import type { AskLane, Prepared } from "./contracts.js";
27
+ import { consumeInheritedAskGrant } from "./inherited-ask-grants.js";
28
+ import type { createRuleOffersOf } from "./permission-rule-lanes.js";
29
+ export interface PrepareAskLaneInput {
30
+ /** borrowed-readonly — the wiring-manifest phase's ONE activation predicate; false ⇒ this phase builds nothing and
31
+ * returns an absent lane. */
32
+ gateMachineryActive: boolean;
33
+ /** borrowed-readonly — prepare's abort seat; only `.signal` is read (composed into every per-call signal and bound
34
+ * onto the deny observer). Never aborted here. */
35
+ abortController: {
36
+ readonly signal: AbortSignal;
37
+ };
38
+ /** borrowed-readonly — the policy chain's effective policy, or undefined (then no `adjudicate` wrapper is built and
39
+ * the gate reads a missing policy as allow). Called at RUN time, per tool call, through the abort-bound wrapper. */
40
+ effectivePolicy: ToolPolicy | undefined;
41
+ /** borrowed-readonly — the per-leg-immutable durable budget snapshot stamped onto EVERY adjudicated request. Same
42
+ * object every call; the policy cannot write it back. */
43
+ budgetSnapshot: NonNullable<ToolCallRequest["budget"]>;
44
+ /** borrowed-readonly — the hand's live tracked cwd, or undefined (hands-less). Read per call at RUN time (`current`),
45
+ * never captured: the fs hand tools resolve their relative paths against this very ref. Not written here. */
46
+ handsCwdRef: CwdRef | undefined;
47
+ /** borrowed-readonly — the shared mount roster; read at RUN time by the approval-preview projection (alias-aware
48
+ * lookup over whatever the roster holds when the ask is minted). Never mutated here. */
49
+ tools: AgentTool[];
50
+ /** borrowed-mutable — the inherited-unavailable marker set. Writer here: `resolveAskBound` consumes (`delete`) a
51
+ * marked call at its fail-closed intercept; the policy-chain phase records, the gate station sweeps. */
52
+ inheritedUnavailableAsks: Set<string>;
53
+ /** borrowed-mutable — the duplicate-frame ask-grant record. Writer here: the grant-reuse consume half
54
+ * (`consumeInheritedAskGrant`); the policy-chain phase records, the gate station sweeps. */
55
+ inheritedAskGrants: Parameters<typeof consumeInheritedAskGrant>[0];
56
+ /** borrowed-readonly — the run's own approver seat (spec over deps), or undefined: the resolve target, the
57
+ * live-approver test the carry mint reads, and the grant-reuse identity law's approver. */
58
+ onAsk: OnAsk | undefined;
59
+ /** borrowed-mutable — the per-task human-review accumulator (`Prepared.humanReviewRef`). Writers here: every
60
+ * synchronous resolve that a person actually judged (count / totalWaitMs / gates push); runtask adds the durable
61
+ * resume latency. */
62
+ humanReviewRef: Prepared["humanReviewRef"];
63
+ /** borrowed-mutable — the bare-human-rejection sideband. Writer here: a `settledBy:"human"` deny with no note
64
+ * (`add`); the gate station judges and sweeps it. */
65
+ humanBareRejections: Set<string>;
66
+ /** borrowed-readonly — the injectable wall clock every human-time observable reads (`Prepared.now`). */
67
+ now: () => number;
68
+ /** borrowed-readonly — the policy-chain phase's rule-offer factory (the suggestion members the ask mint threads). */
69
+ ruleOffersOf: ReturnType<typeof createRuleOffersOf>;
70
+ /** borrowed-readonly — the policy-chain phase's Runner-filled source identity (one factory, every ask mint site). */
71
+ askSourceIdentity: () => {
72
+ principal?: string;
73
+ sourceTaskId?: string;
74
+ fromSubagent?: true;
75
+ sourceAgentName?: string;
76
+ isDelegatedChild?: true;
77
+ };
78
+ /** borrowed-readonly — the policy-chain phase's per-tool risk-axes projection (`riskAxes` on the ask). */
79
+ riskAxesOf: (toolName: string) => {
80
+ riskAxes?: {
81
+ irreversible?: boolean;
82
+ egress?: boolean;
83
+ };
84
+ };
85
+ /** borrowed-mutable — the auto-mode denial tracker, or undefined. Writer here: the carry mint arms the auto-deny
86
+ * window on it iff the seat about to be called is live (the tracker's one writer). */
87
+ autoModeDenialTracking: AutoModeDenialTracker | undefined;
88
+ /** borrowed-readonly — the REBOUND spec; only `taskId` is read (the late-settlement observer's tag). */
89
+ spec: Pick<TaskSpec, "taskId">;
90
+ /** borrowed-readonly — the deployment seats this lane reports on, as a Pick over the SAME `deps` object (receiver
91
+ * preserved for `deps.onError?.()`): the late-settlement observer's notice/error sinks and the hook-crash notifier. */
92
+ deps: Pick<RunnerDeps, "onNotice" | "onError">;
93
+ /** borrowed-readonly — the acquired session id (the observer's tag, the hook-phase error tag). */
94
+ sessionId: string;
95
+ /** borrowed-readonly — the run's invocation id (the observer's tag). */
96
+ runId: string;
97
+ /** borrowed-readonly — the resolved hook record; only `permissionDenied` is read (the deny observer's seat). */
98
+ hooks: Hooks | undefined;
99
+ /** borrowed-readonly — the validated per-prepare hook seat bound (the deny observer's timeout). */
100
+ hookTimeoutMs: number;
101
+ /** borrowed-readonly — the own hook-crash reporter (the deny observer's swallow-guarded report sink). */
102
+ notifyOwnHookCrash: (err: unknown) => void;
103
+ }
104
+ export interface PrepareAskLaneResult {
105
+ /** owned — the lane, or undefined exactly when `gateMachineryActive` is false (then the suspend saga, the boundary
106
+ * parks, the park closure and the gate station are not built either — they key off this seat). */
107
+ askLane: AskLane | undefined;
108
+ }
109
+ /** The M19 ask-lane phase body — prepareTask's in-stream ask stretch, verbatim (see the module header). */
110
+ export declare function prepareAskLane(input: PrepareAskLaneInput): PrepareAskLaneResult;
@@ -0,0 +1,133 @@
1
+ import { primaryActivityArg } from "../arg-summary.js";
2
+ import { hookSeatExpiredError, runHookSeat } from "../hooks.js";
3
+ import { isLiveApproverSeat, resolveAsk } from "../tool-policy.js";
4
+ import { composeCallSignal, raceAbort } from "./abort-race.js";
5
+ import { gateAskCarry, lateAskSettlementObserver } from "./denial-limit-arms.js";
6
+ import { consumeInheritedAskGrant } from "./inherited-ask-grants.js";
7
+ function createDenyObserverNotifier(hooks, hookTimeoutMs, signal, report) {
8
+ if (hooks?.permissionDenied === undefined)
9
+ return undefined;
10
+ return async (payload) => {
11
+ try {
12
+ const seat = await runHookSeat("permissionDenied", { timeoutMs: hookTimeoutMs, signal }, (sig) => hooks.permissionDenied({ ...payload, signal: sig }));
13
+ if (seat.expired)
14
+ report(hookSeatExpiredError("permissionDenied", hookTimeoutMs, seat.cause, "the deny observation was abandoned; the deny itself is unchanged"));
15
+ }
16
+ catch (err) {
17
+ report(err);
18
+ }
19
+ };
20
+ }
21
+ const sanitizePreview = (node, depth = 0) => {
22
+ if (depth > 6)
23
+ return undefined;
24
+ if (typeof node === "string") {
25
+ return node.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "\u2400");
26
+ }
27
+ if (node === null || typeof node !== "object")
28
+ return node;
29
+ if (Array.isArray(node))
30
+ return node.map((v) => sanitizePreview(v, depth + 1));
31
+ const out = {};
32
+ for (const [k, v] of Object.entries(node)) {
33
+ out[sanitizePreview(k, depth + 1)] = sanitizePreview(v, depth + 1);
34
+ }
35
+ return out;
36
+ };
37
+ function resolveApprovalPreview(tools, toolName, args) {
38
+ const t = tools.find((x) => x.name === toolName || (x.aliases?.includes(toolName) ?? false));
39
+ if (t?.approvalPreview === undefined)
40
+ return {};
41
+ try {
42
+ const raw = t.approvalPreview(args);
43
+ if (raw === undefined)
44
+ return {};
45
+ const bytes = JSON.stringify(raw);
46
+ if (bytes === undefined)
47
+ return { withheld: "unavailable" };
48
+ if (bytes.length > 16_384) {
49
+ return { preview: { truncated: true, note: `approval preview exceeded 16KiB (${bytes.length} chars serialized)` }, withheld: "oversize" };
50
+ }
51
+ return { preview: sanitizePreview(raw) };
52
+ }
53
+ catch {
54
+ return { withheld: "unavailable" };
55
+ }
56
+ }
57
+ export function prepareAskLane(input) {
58
+ const { gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, humanBareRejections, now, ruleOffersOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash } = input;
59
+ if (!gateMachineryActive)
60
+ return { askLane: undefined };
61
+ const composedCallSignal = (callSignal) => composeCallSignal(abortController.signal, callSignal);
62
+ const adjudicate = effectivePolicy
63
+ ? (req, callSignal) => {
64
+ const signal = composedCallSignal(callSignal);
65
+ return raceAbort(Promise.resolve(effectivePolicy.check({ ...req, budget: budgetSnapshot, ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}) }, signal)), signal, () => ({
66
+ action: "deny",
67
+ message: "policy check aborted (task timed out or cancelled)",
68
+ }));
69
+ }
70
+ : undefined;
71
+ const approvalPreviewOf = (toolName, args) => resolveApprovalPreview(tools, toolName, args);
72
+ const resolveAskBound = async (decision, req, callSignal) => {
73
+ if (inheritedUnavailableAsks.delete(req.toolCallId)) {
74
+ return {
75
+ action: "deny",
76
+ message: `approval for "${req.toolName}" requires an ancestor task's approval that cannot be resolved here ` +
77
+ `(no live approver reachable, or a durable-park mandate applies), and no durable approval park is ` +
78
+ `available — denied fail-closed (the inherited constraint stands).`,
79
+ decisionReason: "mode",
80
+ approverUnavailable: true,
81
+ };
82
+ }
83
+ const grantReuse = consumeInheritedAskGrant(inheritedAskGrants, onAsk, humanReviewRef, decision, req);
84
+ if (grantReuse !== undefined)
85
+ return grantReuse;
86
+ const t0 = now();
87
+ const resolved = await resolveAsk({
88
+ toolName: req.toolName,
89
+ toolCallId: req.toolCallId,
90
+ args: req.args,
91
+ ...(() => {
92
+ const p = approvalPreviewOf(req.toolName, req.args);
93
+ return { ...(p.preview !== undefined ? { preview: p.preview } : {}), ...(p.withheld !== undefined ? { previewWithheld: p.withheld } : {}) };
94
+ })(),
95
+ ...ruleOffersOf(req.toolName, req.args, decision.action === "ask" ? decision : undefined),
96
+ message: decision.message ?? `approval required for "${req.toolName}"`,
97
+ ...askSourceIdentity(),
98
+ ...riskAxesOf(req.toolName),
99
+ ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
100
+ ...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
101
+ ...(decision.action === "ask" ? gateAskCarry(decision, isLiveApproverSeat(onAsk), autoModeDenialTracking) : {}),
102
+ ...(decision.action === "ask" && decision.probeReason !== undefined ? { probeReason: decision.probeReason } : {}),
103
+ ...(decision.action === "ask" && decision.probeCause !== undefined ? { probeCause: decision.probeCause } : {}),
104
+ ...(decision.action === "ask" && decision.ruleEvidence !== undefined ? { ruleEvidence: decision.ruleEvidence } : {}),
105
+ }, onAsk, composedCallSignal(callSignal), lateAskSettlementObserver({ toolName: req.toolName, toolCallId: req.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
106
+ const waitMs = Math.max(0, now() - t0);
107
+ if (resolved.approverUnavailable !== true && resolved.resolution !== "task_aborted") {
108
+ humanReviewRef.count += 1;
109
+ humanReviewRef.totalWaitMs += waitMs;
110
+ const toolArg = primaryActivityArg(req.args);
111
+ humanReviewRef.gates.push({
112
+ kind: "human",
113
+ waitMs,
114
+ decision: resolved.action === "allow" ? "allow" : "deny",
115
+ toolName: req.toolName,
116
+ ...(toolArg !== undefined ? { toolArg } : {}),
117
+ });
118
+ }
119
+ if (resolved.action === "deny" && resolved.settledBy === "human" && resolved.humanRefusalNote !== true) {
120
+ humanBareRejections.add(req.toolCallId);
121
+ }
122
+ return resolved;
123
+ };
124
+ const notifyPermissionDenied = createDenyObserverNotifier(hooks, hookTimeoutMs, abortController.signal, notifyOwnHookCrash);
125
+ const notifyHookError = (err) => {
126
+ try {
127
+ deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "hook", sessionId });
128
+ }
129
+ catch {
130
+ }
131
+ };
132
+ return { askLane: { composedCallSignal, adjudicate, approvalPreviewOf, resolveAskBound, notifyPermissionDenied, notifyHookError } };
133
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * design/390 §1.2 M19 (boundary parks) — prepareTask's three BOUNDARY-FAMILY park lanes, verbatim from the driver's
3
+ * gate-machinery block: the ONE resource mint (`mintResourceSuspend`) behind the task-axis `suspendForResource` (exposed
4
+ * under the caller's `resourceSuspend` opt-in) and the infrastructure `suspendForPlatformLimit` (exposed on durable infra
5
+ * alone), and the `plan_review` mint behind `suspendForReview` (exposed with a checkpoint store). Every mint reuses the
6
+ * suspend saga's serializer, commit saga, loop cap and env split — this phase adds no second copy of any of them. The
7
+ * interface is the dependency list the segment had implicitly (design/238 R-1); the three lanes used to be forward-declared
8
+ * `let`s at task scope, assigned inside the block, so the driver's Prepared could expose them — they are this phase's
9
+ * Result now, and the driver binds them as consts.
10
+ *
11
+ * SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await of its own (the mints run at a turn boundary), so the
12
+ * phase adds no yield between the suspend saga before it and the park closure after it (design/390 §1.5). Read-stability
13
+ * of the host handles for the call: {@link RunInternals} (`@contract prepare.deps-read-stable`).
14
+ *
15
+ * The lanes exist exactly when the saga does (the ask lane's presence law) — an absent saga answers three absent lanes,
16
+ * so the orchestrator grows no branch.
17
+ */
18
+ import type { AgentHarness } from "../../internal/harness.js";
19
+ import { type CheckpointStore } from "../checkpoint-store.js";
20
+ import type { SessionStore } from "../session.js";
21
+ import type { RunnerDeps, TaskSpec } from "../types.js";
22
+ import type { Prepared, SuspendSaga } from "./contracts.js";
23
+ export interface PrepareBoundaryParksInput {
24
+ /** borrowed-readonly — the suspend saga's eight seats (the serializer, the commit saga, the loop cap, the env split, the
25
+ * park-only handle, the in-flight spend), or undefined when the gate machinery is inactive (then all three lanes are
26
+ * absent). Called at park time; never rebuilt here. */
27
+ saga: SuspendSaga | undefined;
28
+ /** borrowed-readonly — the REBOUND spec. Read: `resourceSuspend` (the slice scope/ttl/maxSlices), `retainBackgroundProcesses`
29
+ * (the park-only sweep gate), `durableApproval` (the review pause's ttl), `principal` (the rows' identity record and
30
+ * the review scope). Never mutated. */
31
+ spec: Pick<TaskSpec, "resourceSuspend" | "retainBackgroundProcesses" | "durableApproval" | "principal">;
32
+ /** borrowed-readonly — the leg's checkpoint store, or undefined (then the review lane is absent and the resource mint
33
+ * answers false). */
34
+ checkpointStore: CheckpointStore | undefined;
35
+ /** borrowed-mutable — prepare's abort seat. `.signal` is read (a cancelling run mints nothing); `abort()` is called on
36
+ * the review lane's commit (the review pause stops the run the human gate's way). */
37
+ abortController: {
38
+ readonly signal: AbortSignal;
39
+ abort(): void;
40
+ };
41
+ /** borrowed-mutable — the built harness. Writers here: `requestStopAfterTurn()` (the resource lanes' clean stop) and
42
+ * `abort()` (the review lane's stop). */
43
+ harness: Pick<AgentHarness, "abort" | "requestStopAfterTurn">;
44
+ /** borrowed-readonly — the acquired session; only `getLeafId` is read (no committed leaf ⇒ no consistent resume point). */
45
+ session: Pick<Prepared["session"], "getLeafId">;
46
+ /** borrowed-readonly — the session store; only the optional `pin` is called (best-effort, after commit). */
47
+ sessions: Pick<SessionStore, "pin">;
48
+ /** borrowed-readonly — the acquired session id (the rows' session, the source attribution, every disclosure's tag). */
49
+ sessionId: string;
50
+ /** borrowed-readonly — the deployment's error face, as a Pick over the SAME `deps` object (receiver preserved for
51
+ * `deps.onError?.()`): the suspendVM refusals, the skipped-review lines, the pin failures. */
52
+ deps: Pick<RunnerDeps, "onError">;
53
+ /** borrowed-readonly — the cross-slice ledger carried on the resumed checkpoint, or undefined on the first slice. */
54
+ priorLedger: Prepared["resourceLedger"];
55
+ /** borrowed-readonly — the resource-slice cap, or undefined (no slice ceiling). */
56
+ maxSlices: number | undefined;
57
+ /** borrowed-readonly — the restart-loop cap the review lane is bounded by. */
58
+ maxSuspends: number;
59
+ /** borrowed-readonly — the human total the first slice seeds the ledger from, or undefined. */
60
+ resourceTotal: {
61
+ totalBudgetMicroUsd?: number;
62
+ totalTokens?: number;
63
+ } | undefined;
64
+ /** borrowed-readonly — how many times this task already suspended (carried on the resumed checkpoint); a slice carries
65
+ * it UNCHANGED. */
66
+ priorSuspendCount: number;
67
+ /** borrowed-readonly — the restart-loop chain base (resets after an approved execution); the review lane's count. */
68
+ suspendChainBase: () => number;
69
+ /** borrowed-readonly — the per-task human-review accumulator; snapshotted onto every row (never written here). */
70
+ humanReviewRef: Prepared["humanReviewRef"];
71
+ /** borrowed-readonly — the loop's live spend seat; `.get` is read at mint time for the review ledger. */
72
+ liveSpendRef: Prepared["liveSpendRef"];
73
+ /** borrowed-readonly — the injectable wall clock (the review pause's `suspendedAt`). */
74
+ now: () => number;
75
+ /** borrowed-readonly — the inherited-gate phase's three version-stamp predicates (face / F-012 / org admission). */
76
+ faceCheckpointState: () => boolean;
77
+ /** borrowed-readonly — see `faceCheckpointState`. */
78
+ f012CheckpointState: () => boolean;
79
+ /** borrowed-readonly — see `faceCheckpointState`. */
80
+ orgAdmissionCheckpointState: () => boolean;
81
+ /** borrowed-mutable — the durable-suspend holder (`Prepared.suspendRef`). Writer here: the commit-side publication of a
82
+ * committed resource row. */
83
+ suspendRef: Prepared["suspendRef"];
84
+ /** borrowed-mutable — the review holder (`Prepared.reviewRef`). Writer here: the commit-side publication of a committed
85
+ * plan_review row. */
86
+ reviewRef: Prepared["reviewRef"];
87
+ /** borrowed-mutable — the run-scoped remote-lifecycle failure log. Writer here: a refused `suspendVM` (`push`). */
88
+ remoteEnvFailures: Prepared["remoteEnvFailures"];
89
+ /** borrowed-readonly — the wiring-manifest phase's task-axis predicate (opt-in ∧ durable infra): exposes `suspendForResource`. */
90
+ resourceSuspendEligible: boolean;
91
+ /** borrowed-readonly — the wiring-manifest phase's durable-infra predicate: exposes `suspendForPlatformLimit`. */
92
+ durableSuspendInfraReady: boolean;
93
+ /** borrowed-readonly — the restore-incomplete adapter's missing members, or undefined (the review lane refuses loudly). */
94
+ incompleteSuspendAdapter: readonly ("resumeVM" | "postResumeInit")[] | undefined;
95
+ }
96
+ export interface PrepareBoundaryParksResult {
97
+ /** owned — the task-axis resource lane (`Prepared.suspendForResource`), or undefined (no opt-in / no infra / inactive). */
98
+ suspendForResource: Prepared["suspendForResource"];
99
+ /** owned — the platform lane (`Prepared.suspendForPlatformLimit`), or undefined (no durable infra / inactive). */
100
+ suspendForPlatformLimit: Prepared["suspendForPlatformLimit"];
101
+ /** owned — the plan-review lane (`Prepared.suspendForReview`), or undefined (no store / inactive). */
102
+ suspendForReview: Prepared["suspendForReview"];
103
+ }
104
+ /** The M19 boundary-parks phase body — prepareTask's three boundary lanes, verbatim (see the module header). */
105
+ export declare function prepareBoundaryParks(input: PrepareBoundaryParksInput): PrepareBoundaryParksResult;