@sema-agent/core 7.13.0 → 7.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/agents/subagent.d.ts +4 -9
  3. package/dist/agents/subagent.js +16 -12
  4. package/dist/agents/teacher.js +1 -1
  5. package/dist/brain/reasoning.d.ts +23 -0
  6. package/dist/brain/reasoning.js +35 -4
  7. package/dist/brain/stream-engine.js +35 -17
  8. package/dist/brain/timeout.d.ts +25 -4
  9. package/dist/brain/timeout.js +1 -1
  10. package/dist/core/ask-origin.d.ts +30 -11
  11. package/dist/core/ask-origin.js +10 -5
  12. package/dist/core/auto-mode.d.ts +3 -3
  13. package/dist/core/checkpoint-seat.d.ts +18 -0
  14. package/dist/core/checkpoint-seat.js +6 -0
  15. package/dist/core/checkpoint-store.d.ts +23 -31
  16. package/dist/core/checkpoint-store.js +7 -4
  17. package/dist/core/engine-notice.d.ts +3 -1
  18. package/dist/core/env-knob-announce.d.ts +16 -0
  19. package/dist/core/env-knob-announce.js +14 -0
  20. package/dist/core/gate-fold.js +2 -0
  21. package/dist/core/gate-lanes.js +21 -12
  22. package/dist/core/gate-outcome.d.ts +42 -3
  23. package/dist/core/gate-outcome.js +22 -0
  24. package/dist/core/governance-codes.js +1 -1
  25. package/dist/core/hooks.d.ts +9 -19
  26. package/dist/core/hooks.js +6 -4
  27. package/dist/core/mcp.d.ts +3 -2
  28. package/dist/core/mcp.js +5 -11
  29. package/dist/core/memory-engine/engine.d.ts +2 -2
  30. package/dist/core/runner/advertised-writable-dirs.d.ts +74 -0
  31. package/dist/core/runner/advertised-writable-dirs.js +48 -0
  32. package/dist/core/runner/assemble-result.d.ts +5 -0
  33. package/dist/core/runner/contracts.d.ts +2 -2
  34. package/dist/core/runner/denial-limit-arms.d.ts +7 -9
  35. package/dist/core/runner/denial-limit-arms.js +8 -10
  36. package/dist/core/runner/gate-exit.d.ts +15 -3
  37. package/dist/core/runner/gate-exit.js +6 -4
  38. package/dist/core/runner/permission-rule-lanes.d.ts +3 -1
  39. package/dist/core/runner/permission-rule-lanes.js +1 -1
  40. package/dist/core/runner/prepare-caps-and-workflow.d.ts +34 -4
  41. package/dist/core/runner/prepare-caps-and-workflow.js +47 -12
  42. package/dist/core/runner/prepare-defer-classify.d.ts +1 -1
  43. package/dist/core/runner/prepare-defer-classify.js +12 -10
  44. package/dist/core/runner/prepare-gate-stations.d.ts +4 -5
  45. package/dist/core/runner/prepare-gate-stations.js +2 -2
  46. package/dist/core/runner/prepare-hands-readface.d.ts +18 -12
  47. package/dist/core/runner/prepare-hands-readface.js +32 -43
  48. package/dist/core/runner/prepare-inherited-gate.d.ts +7 -5
  49. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  50. package/dist/core/runner/prepare-memory.d.ts +31 -34
  51. package/dist/core/runner/prepare-memory.js +73 -51
  52. package/dist/core/runner/prepare-park-ask.d.ts +5 -6
  53. package/dist/core/runner/prepare-park-ask.js +3 -3
  54. package/dist/core/runner/prepare-policy-chain.d.ts +2 -2
  55. package/dist/core/runner/prepare-policy-chain.js +7 -4
  56. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -0
  57. package/dist/core/runner/prepare-prompt-inputs.js +2 -2
  58. package/dist/core/runner/prepare-question-face.d.ts +10 -8
  59. package/dist/core/runner/prepare-question-face.js +1 -3
  60. package/dist/core/runner/prepare-safety-scan.js +1 -1
  61. package/dist/core/runner/prepare-task.js +60 -53
  62. package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
  63. package/dist/core/runner/prepare-wiring-manifest.js +1 -1
  64. package/dist/core/runner/run-compaction-machinery.js +2 -0
  65. package/dist/core/runner/run-harness-handlers.js +3 -1
  66. package/dist/core/runner/stream-settle-backstop.js +1 -1
  67. package/dist/core/runner/tool-defer-gate.d.ts +86 -0
  68. package/dist/core/runner/tool-defer-gate.js +57 -0
  69. package/dist/core/runner/tool-disclosure.d.ts +0 -36
  70. package/dist/core/runner/tool-disclosure.js +0 -43
  71. package/dist/core/runner-deps.d.ts +16 -9
  72. package/dist/core/runtime-caps.d.ts +21 -0
  73. package/dist/core/runtime-caps.js +5 -1
  74. package/dist/core/task-event.d.ts +11 -2
  75. package/dist/core/task-registry-shared.js +8 -6
  76. package/dist/core/task-result.d.ts +15 -0
  77. package/dist/core/task-spec.d.ts +8 -2
  78. package/dist/core/terminal-cause.d.ts +6 -2
  79. package/dist/core/tool-policy.d.ts +34 -28
  80. package/dist/core/tool-policy.js +29 -5
  81. package/dist/core/tool-roster.js +2 -0
  82. package/dist/core/tool-spec.d.ts +20 -16
  83. package/dist/core/types.d.ts +3 -2
  84. package/dist/core/types.js +1 -0
  85. package/dist/core/wiring-manifest.d.ts +16 -9
  86. package/dist/core/wiring-manifest.js +8 -3
  87. package/dist/index.d.ts +7 -6
  88. package/dist/index.js +5 -4
  89. package/dist/orchestration/run-workflow-tool.d.ts +8 -8
  90. package/dist/orchestration/run-workflow-tool.js +1 -1
  91. package/dist/orchestration/workflow-primitives.d.ts +4 -3
  92. package/dist/orchestration/workflow-primitives.js +3 -3
  93. package/dist/orchestration/workflow-types.d.ts +14 -0
  94. package/dist/orchestration/workflow.d.ts +41 -4
  95. package/dist/orchestration/workflow.js +23 -5
  96. package/dist/tools/fs/fs-bash.d.ts +41 -0
  97. package/dist/tools/fs/fs-bash.js +86 -29
  98. package/dist/tools/fs/fs-shared.js +9 -3
  99. package/dist/tools/fs/read-deny.d.ts +24 -8
  100. package/dist/tools/fs/read-deny.js +20 -1
  101. package/package.json +2 -1
  102. package/test/export-surface.snapshot.json +29 -1
@@ -2,7 +2,7 @@ import { emitTrace } from "../trace.js";
2
2
  import { StubExecutionEnv } from "../stub-env.js";
3
3
  import { hasBackgroundShell } from "../background-shell.js";
4
4
  import { defaultTaskRegistry } from "../task-registry.js";
5
- import { bashReversibilityProbe, compileReadDeny, createHandsToolkit, deploymentReadFaceClampNotice, pdfModelCapabilitiesOf, resolveReadDenyBuiltins, inheritSessionReadFileState, resolveReadFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, } from "../../tools/fs/index.js";
5
+ import { bashReadBoundaryProbe, bashReversibilityProbe, compileReadDeny, createHandsToolkit, deploymentReadFaceClampNotice, pdfModelCapabilitiesOf, resolveReadDenyBuiltins, inheritSessionReadFileState, resolveReadFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, } from "../../tools/fs/index.js";
6
6
  import { resolveKey, sha256 } from "../../tools/fs/safety.js";
7
7
  import { normalizeFileText } from "../../tools/fs/encoding.js";
8
8
  import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
@@ -117,14 +117,13 @@ export function resolveHandsLessReadFace(input) {
117
117
  attachmentRootCanonical: undefined,
118
118
  readDenyMatcher: undefined,
119
119
  envHandToolNames: new Set(),
120
- shellGatedBash: false,
121
- shellGatedMonitor: false,
120
+ shellGatedTools: new Set(),
122
121
  sealReadStateSeat: undefined,
123
122
  };
124
123
  }
125
124
  export async function prepareHandsMount(input) {
126
125
  const envHandToolNames = new Set();
127
- const { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, firstPartyOffload, roster, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes } = input;
126
+ const { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, advertisedWritableDirs, firstPartyOffload, roster, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes } = input;
128
127
  assertStringArraySeat(spec.additionalDirectories, "TaskSpec.additionalDirectories", "strings");
129
128
  assertStringArraySeat(spec.additionalReadDirectories, "TaskSpec.additionalReadDirectories", "strings");
130
129
  assertStringArraySeat(deps.readDenyPatterns, "RunnerDeps.readDenyPatterns", "compiler-owned");
@@ -140,8 +139,7 @@ export async function prepareHandsMount(input) {
140
139
  let resolvedReadFace;
141
140
  let readDenyAdditionsNormalized = [];
142
141
  let handsLessResolvedFace;
143
- let shellGatedBash = false;
144
- let shellGatedMonitor = false;
142
+ const shellGatedTools = new Set();
145
143
  const additionalRootsCanonical = [];
146
144
  const additionalReadRootsCanonical = [];
147
145
  let attachmentRootCanonical;
@@ -178,19 +176,10 @@ export async function prepareHandsMount(input) {
178
176
  };
179
177
  await canonicalizeExtraDirs(spec.additionalDirectories, "additionalDirectories", additionalRootsCanonical);
180
178
  await canonicalizeExtraDirs(spec.additionalReadDirectories, "additionalReadDirectories", additionalReadRootsCanonical);
181
- if (spec.envFacts?.scratchpadDir) {
182
- let c = await executionEnv.canonicalPath(spec.envFacts.scratchpadDir);
183
- if (!c.ok && c.error.code === "not_found") {
184
- const marker = await executionEnv.joinPath([spec.envFacts.scratchpadDir, ".sema-scratchpad"]);
185
- const exclusiveCreate = executionEnv.writeFileExclusive?.bind(executionEnv);
186
- if (marker.ok && exclusiveCreate) {
187
- const w = await exclusiveCreate(marker.value, "");
188
- if (w.ok || w.error.code === "already_exists")
189
- c = await executionEnv.canonicalPath(spec.envFacts.scratchpadDir);
190
- }
191
- }
192
- if (c.ok)
193
- additionalRootsCanonical.push(c.value);
179
+ if (advertisedWritableDirs !== undefined) {
180
+ if (spec.envFacts?.scratchpadDir)
181
+ await advertisedWritableDirs.admit({ path: spec.envFacts.scratchpadDir, advertiser: "scratchpad", materializeMarker: ".sema-scratchpad" });
182
+ additionalRootsCanonical.push(...advertisedWritableDirs.roots);
194
183
  }
195
184
  for (const a of [additionalRootsCanonical, additionalReadRootsCanonical])
196
185
  a.splice(0, a.length, ...new Set(a));
@@ -354,34 +343,35 @@ export async function prepareHandsMount(input) {
354
343
  }
355
344
  roster.mountAll(band.map((t) => (envHandToolNames.add(t.name), firstPartyOffload(t))), { source: "builtin", mountedBy: "hands" });
356
345
  const shellGate = effectiveShellGate;
357
- if (shellGate === "off" && !(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
358
- deps.onError?.(new Error(`shell gate doctrine is "off" while a real writable shell (Bash) is mounted — no shell safety-axis ` +
359
- `fold applies to this run (commands are adjudicated by the ordinary policy/hook chain only). ` +
360
- `Set spec.shellGate to "classify" or "always" if this deployment expects doctrine-gated shell behavior.`), { phase: "config", sessionId, classification: "shell-gate-off" });
361
- }
362
- if (shellGate !== "off" && !(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
363
- const bashTierBefore = irreversibilityTier.get("Bash");
364
- shellGatedBash = !egressTools.has("Bash") && bashTierBefore !== "always" && bashTierBefore !== "maybe";
365
- const bashEffectiveTier = shellGate === "always" || bashTierBefore === "always" ? "always" : "maybe";
366
- irreversibilityTier.set("Bash", bashEffectiveTier);
367
- irreversibleTools.add("Bash");
346
+ if (!(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
347
+ if (shellGate === "off") {
348
+ deps.onError?.(new Error(`shell gate doctrine is "off" while a real writable shell (Bash) is mounted the read boundary is ` +
349
+ `still judged on every command (a deny-listed or out-of-root read asks; the ask is mandated), but no ` +
350
+ `residual shell-risk classifier is installed: writes, egress and unknown commands are adjudicated by ` +
351
+ `the ordinary policy/hook chain only. Set spec.shellGate to "classify" or "always" if this deployment ` +
352
+ `expects doctrine-gated shell behavior.`), { phase: "config", sessionId, classification: "shell-gate-off" });
353
+ }
368
354
  const shellReadBoundary = () => ({
369
355
  roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
370
356
  ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
371
357
  denyMatch: (p) => readDenyMatcher?.matchPath(p)?.pattern ?? null,
372
358
  ...(resolvedReadFace !== undefined ? { face: resolvedReadFace } : {}),
373
359
  });
374
- if (shellGate === "classify" && shellGatedBash)
375
- reversibilityProbes.set("Bash", bashReversibilityProbe(undefined, shellReadBoundary));
376
- if (backgroundTaskToolsActive) {
377
- const monitorTierBefore = irreversibilityTier.get("Monitor");
378
- shellGatedMonitor = !egressTools.has("Monitor") && monitorTierBefore !== "always" && monitorTierBefore !== "maybe";
379
- const monitorEffectiveTier = shellGate === "always" || monitorTierBefore === "always" ? "always" : "maybe";
380
- irreversibilityTier.set("Monitor", monitorEffectiveTier);
381
- irreversibleTools.add("Monitor");
382
- if (shellGate === "classify" && shellGatedMonitor)
383
- reversibilityProbes.set("Monitor", bashReversibilityProbe(undefined, shellReadBoundary));
384
- }
360
+ const installShellSeat = (name) => {
361
+ const tierBefore = irreversibilityTier.get(name);
362
+ const seatOwned = !egressTools.has(name) && tierBefore !== "always" && tierBefore !== "maybe";
363
+ if (seatOwned)
364
+ shellGatedTools.add(name);
365
+ const tier = shellGate === "always" || tierBefore === "always" ? "always" : "maybe";
366
+ irreversibilityTier.set(name, tier);
367
+ irreversibleTools.add(name);
368
+ if (seatOwned && tier === "maybe") {
369
+ reversibilityProbes.set(name, shellGate === "classify" ? bashReversibilityProbe(undefined, shellReadBoundary) : bashReadBoundaryProbe(shellReadBoundary));
370
+ }
371
+ };
372
+ installShellSeat("Bash");
373
+ if (backgroundTaskToolsActive)
374
+ installShellSeat("Monitor");
385
375
  }
386
376
  }
387
377
  return {
@@ -400,8 +390,7 @@ export async function prepareHandsMount(input) {
400
390
  attachmentRootCanonical,
401
391
  readDenyMatcher,
402
392
  envHandToolNames,
403
- shellGatedBash,
404
- shellGatedMonitor,
393
+ shellGatedTools,
405
394
  sealReadStateSeat,
406
395
  };
407
396
  }
@@ -43,7 +43,7 @@ import type { DelegationContentSafety } from "../memory-engine/delegation-proven
43
43
  import type { SessionPermissionRules } from "../session-policy-store.js";
44
44
  import type { SessionStore } from "../session.js";
45
45
  import { type OnAsk } from "../tool-policy.js";
46
- import type { RunnerDeps, RuntimeCaps, TaskEvent, TaskSpec, ToolExecuteContext } from "../types.js";
46
+ import type { RunnerDeps, RuntimeCaps, ShellGateDoctrine, TaskEvent, TaskSpec, ToolExecuteContext } from "../types.js";
47
47
  import type { ActiveSkillScope } from "./active-skill-scope.js";
48
48
  import type { InheritedGate, Prepared, PrepareResume, RunInternals, ToolFaceSnapshot } from "./contracts.js";
49
49
  import { type StopForDenialLimit } from "./denial-limit-arms.js";
@@ -51,9 +51,11 @@ export interface PrepareInheritedGateInput {
51
51
  /** borrowed-readonly — the REBOUND spec. Read: `shellGate` (the own doctrine the chain fold tightens), `durableApproval`
52
52
  * (the mandate tests + the children's copy), `onAsk`/`onQuestion` (the frozen seats), `handsReadOnly` / `interactiveTools`
53
53
  * / `oneShot` / `clientContext` / `principal` / `additionalDirectories` / `additionalReadDirectories` / `envFacts` /
54
- * `memoryPersistenceCapable` / `getApiKeyAndHeaders` / `checkpointStore` / `backgroundScope` / `enablePlanMode` (the ctx
55
- * enrichment's trusted seats). Never mutated. */
56
- spec: Pick<TaskSpec, "shellGate" | "durableApproval" | "onAsk" | "onQuestion" | "handsReadOnly" | "interactiveTools" | "oneShot" | "clientContext" | "principal" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable" | "getApiKeyAndHeaders" | "checkpointStore" | "backgroundScope" | "enablePlanMode" | "excludeAllTools">;
54
+ * `memoryPersistenceCapable` / `getApiKeyAndHeaders` / `backgroundScope` / `enablePlanMode` (the ctx enrichment's
55
+ * trusted seats). Never mutated. */
56
+ spec: Pick<TaskSpec, "shellGate" | "durableApproval" | "onAsk" | "onQuestion" | "handsReadOnly" | "interactiveTools" | "oneShot" | "clientContext" | "principal" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable" | "getApiKeyAndHeaders" | "backgroundScope" | "enablePlanMode" | "excludeAllTools">;
57
+ /** borrowed-readonly — the seat this run hands its children (`checkpointSeatOf`, minted once by the driver after the config doors): the ctx's `checkpointStoreForChildren`, absent when the run has no seat. */
58
+ checkpointSeat: import("../checkpoint-seat.js").CheckpointSeat | undefined;
57
59
  /** borrowed-readonly — the deployment seats: `autoMode.denialLimit` (the rebuilt trackers' bounds), `memoryScopeAdmission` /
58
60
  * `deploymentMemoryScopes` / `compliancePostureResolver` (the governed-provenance bit), `onError` (the screening-face crash
59
61
  * lane), `onNotice` (the stop seat's notice), `executionEnv` (the hook-env identity predicate), `delegationEntryCaps` (resolved
@@ -192,7 +194,7 @@ export interface PrepareInheritedGateResult {
192
194
  /** owned — the monotonic governed-provenance bit for this leg. */
193
195
  orgGovernedProvenance: boolean;
194
196
  /** owned — the spec's own shellGate tightened by the inherited doctrine. */
195
- effectiveShellGate: "off" | "classify" | "always";
197
+ effectiveShellGate: ShellGateDoctrine;
196
198
  /** borrowed-mutable — THIS task's own session-rule snapshot for its children's chain. Writers (R-3): the policy-chain
197
199
  * station's session-policy read, once per leg, when a store is wired and the task carries its own sessionId. Read by
198
200
  * `inheritedGateForChildren` at spawn time. */
@@ -222,7 +222,7 @@ export function prepareInheritedGate(input) {
222
222
  delegationSettlement: delegationSettlementForChildren,
223
223
  ...(autoModeDecider ? { autoModeReview: { decider: autoModeDecider } } : {}),
224
224
  ...(spec.durableApproval !== undefined ? { durableApprovalForChildren: { ...spec.durableApproval } } : {}),
225
- ...(spec.checkpointStore === "disabled" ? { checkpointStoreDisabledForChildren: true } : {}),
225
+ ...(input.checkpointSeat !== undefined ? { checkpointStoreForChildren: input.checkpointSeat } : {}),
226
226
  taskId: hostTaskId,
227
227
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
228
228
  ...(internals?.parentSessionId !== undefined ? { parentSessionId: internals.parentSessionId } : {}),
@@ -1,6 +1,7 @@
1
1
  import type { BeforeWriteHook, EffectiveMemoryScopes, RunnerDeps, TaskSpec, ToolSpec } from "../types.js";
2
2
  import { type CaptureEntitlementInput, type MemoryCapturePosture } from "./memory-capture-optout.js";
3
3
  import type { Prepared } from "./contracts.js";
4
+ import type { AdvertisedWritableDirs } from "./advertised-writable-dirs.js";
4
5
  export interface PrepareMemoryInput {
5
6
  /** borrowed-readonly — the REBOUND spec. Read: `memory` (the scope declaration this phase
6
7
  * normalizes) and `principal` (the org-admission subject). Never mutated. */
@@ -33,23 +34,23 @@ export interface PrepareMemoryInput {
33
34
  * arms — a hands-less run that mounts its own `Write` counts, and an `excludeTools: ["Write"]`
34
35
  * run reads as unmounted whatever the hands band did. Threaded into `engine.inject` so a run with
35
36
  * a writable memory scope but no write channel is not instructed to call a tool that is not on
36
- * its roster (or cannot reach the store); the RB-276 index seed follows the same gate.
37
+ * its roster (or cannot reach the store); the RB-276 index seed follows the same gate. Since #691 this is
38
+ * the FIRST conjunct of the write channel; the second is the root fence's admission of the write plane's
39
+ * root through {@link advertisedWritableDirs} — the channel is open only when both hold.
37
40
  */
38
41
  writeToolsMounted: boolean;
42
+ /**
43
+ * borrowed-mutable — #691: the root fence's advertised-writable-directory seat (its module header carries the
44
+ * one rule). Present iff the hands band mounts this run (a fence exists between the roster's `Write` and the
45
+ * store); absent ⇒ a `Write` on the roster is a caller tool running in the host process, which no fence
46
+ * stands before, so admission holds trivially. This phase admits the WRITE plane's root when the channel's
47
+ * first conjunct holds (`writeToolsMounted`) and a write face exists; a read-only layering admits nothing.
48
+ */
49
+ advertisedWritableDirs?: AdvertisedWritableDirs;
39
50
  /** borrowed-readonly — design/319 (A ticket): the run's reminder provenance mark: the memory engine's announcement
40
51
  * block is an engine-authored `<system-reminder>`, so its open tag must carry the same mark the
41
52
  * system-prompt declaration names. Threaded into every `engine.inject` call this phase builds. */
42
53
  reminderMark: string;
43
- /**
44
- * borrowed-readonly — the session-wide persistence verdict: `TaskSpec.memoryPersistenceCapable` (the deployment's own
45
- * statement — the only honest channel for a custom memory writer persisting through its closure,
46
- * which no inference can see) when set, else the KNOWN-store-path inference: a mounted,
47
- * non-excluded file-write tool (Write/Edit/NotebookEdit) or a write-capable shell. A generic
48
- * write-effect tool does NOT count — a mail sender's side effect is not a memory store, and
49
- * counting it re-opened the silent-confabulation hole. `false` ⇒ this phase mounts
50
- * {@link MEMORY_READONLY_NOTICE} where the engine is silent.
51
- */
52
- rosterCanPersist: boolean;
53
54
  /**
54
55
  * borrowed-readonly — the raw `TaskSpec.memoryPersistenceCapable` DECLARATION, kept separate from the composed
55
56
  * {@link rosterCanPersist} verdict because the two drive different arms:
@@ -184,35 +185,31 @@ export interface PrepareMemoryInput {
184
185
  governedProvenance: boolean;
185
186
  };
186
187
  }
187
- export interface PrepareMemoryResult {
188
- memoryEngineSession: Prepared["memoryEngineSession"];
188
+ /** #691 — what {@link PrepareMemoryResult.composeInjection} hands back: the composed `# Memory` block (undefined
189
+ * when nothing mounts), the recall-discipline segment EXACTLY as appended (prepare-task's retraction strips this
190
+ * returned string — #493 单铸), and the RB-276 index seed files whose full disk text rides in the block. */
191
+ export interface MemoryInjectionComposition {
189
192
  memoryBlock: string | undefined;
190
- /**
191
- * design/178 ②-1 — the `memory_search`/`memory_get` pair's ToolSpecs, present iff the engine
192
- * session mounted AND `memorySearchToolsPlanned` was true. Built HERE because this phase owns the
193
- * planes (backend + mounted scopes + per-plane retrieved-account recorder); prepare-task mounts
194
- * them (atomic pair) and runs the remaining conjuncts.
195
- */
196
- memoryTools?: ToolSpec[];
197
- /**
198
- * #493 单铸 — the recall-discipline segment EXACTLY as this phase appended it to
199
- * {@link memoryBlock} (present iff {@link memoryTools} is). prepare-task's support-name
200
- * retraction strips THIS string: the segment's composition now depends on the marked-presence
201
- * signal only this phase holds, so a strip-site re-composition could mint a string that was
202
- * never appended and leave the real one standing.
203
- */
204
193
  recallDisciplineSegment?: string;
205
- /**
206
- * RB-276 — engine-materialized files whose FULL disk text rides in {@link memoryBlock}, for the
207
- * caller to pre-mark READ through the parity-204 seeding lane (`prepare-task`'s `seedContextFiles`,
208
- * which applies the SAME containment as a real Read — a memory dir outside the task root is simply
209
- * skipped, exactly as the Write tool would refuse it there). Today this is the materialized
210
- * MEMORY.md index; see `MemoryInjection.indexSeed` for the conditions that qualify it.
211
- */
212
194
  seedFiles?: ReadonlyArray<{
213
195
  path: string;
214
196
  content: string;
215
197
  }>;
198
+ }
199
+ export interface PrepareMemoryResult {
200
+ memoryEngineSession: Prepared["memoryEngineSession"];
201
+ memoryTools?: ToolSpec[];
202
+ /**
203
+ * owned — #691: the injection's COMPOSITION step, run by the driver AFTER the hands phase (the mount above runs
204
+ * BEFORE it). It performs the one destructive read this phase has — `inject()` drains the announcement queue —
205
+ * so a hands-phase refusal cannot consume announcements a later prepare should deliver, and it takes the
206
+ * roster-wide persistence verdict the driver can only read off the MOUNTED roster (`rosterCanPersist`: the
207
+ * `TaskSpec.memoryPersistenceCapable` declaration when set, else the known-store-path inference over the
208
+ * assembled tools). Call exactly once per prepare.
209
+ */
210
+ composeInjection: (facts: {
211
+ rosterCanPersist: boolean;
212
+ }) => MemoryInjectionComposition;
216
213
  /**
217
214
  * The FROZEN org-admission verdict of this prepare: every org scope the task actually mounts
218
215
  * (deployment-origin kept + request-origin admitted). Threaded into the run's child chain
@@ -74,6 +74,7 @@ export async function prepareMemory(input) {
74
74
  let captureLineageIndeterminate = false;
75
75
  let memoryEngineSession;
76
76
  let memoryTools;
77
+ let writeChannelOpen = false;
77
78
  let memoryMarkedEntriesServed = false;
78
79
  let effectiveMemoryScopes;
79
80
  const originOf = (scope) => scopeOriginsSnapshot !== undefined
@@ -373,7 +374,7 @@ export async function prepareMemory(input) {
373
374
  writeHandle = writeIsPersonal ? personalHandle : projectHandle;
374
375
  readOnlyEngine = writeIsPersonal ? projectEngine : personalEngine;
375
376
  readOnlyHandle = writeIsPersonal ? projectHandle : personalHandle;
376
- injectFn = () => mergeInjections(projectEngine.inject(projectHandle, { writeToolMounted: input.writeToolsMounted, reminderMark: input.reminderMark }), personalEngine.inject(personalHandle, { writeToolMounted: input.writeToolsMounted, reminderMark: input.reminderMark }));
377
+ injectFn = () => mergeInjections(projectEngine.inject(projectHandle, { writeToolMounted: writeChannelOpen, reminderMark: input.reminderMark }), personalEngine.inject(personalHandle, { writeToolMounted: writeChannelOpen, reminderMark: input.reminderMark }));
377
378
  toolPlanes = [
378
379
  {
379
380
  backend: retrievalBackend(backend, adoptionRestricted || captureOptedOut || captureIndeterminate || p.writePlane !== "project"),
@@ -417,7 +418,7 @@ export async function prepareMemory(input) {
417
418
  memoryMarkedEntriesServed = handle.markedEntriesPresent === true;
418
419
  writeEngine = personalEngine;
419
420
  writeHandle = handle;
420
- injectFn = () => personalEngine.inject(handle, { writeToolMounted: input.writeToolsMounted, reminderMark: input.reminderMark });
421
+ injectFn = () => personalEngine.inject(handle, { writeToolMounted: writeChannelOpen, reminderMark: input.reminderMark });
421
422
  harvestBoth = async () => personalEngine.harvest(handle, { ...pollutedOpts(personalEngine), sessionId, ...(await admitNothingOptsNow()) });
422
423
  toolPlanes = [
423
424
  {
@@ -445,7 +446,7 @@ export async function prepareMemory(input) {
445
446
  memoryMarkedEntriesServed = handle.markedEntriesPresent === true;
446
447
  writeEngine = engine;
447
448
  writeHandle = handle;
448
- injectFn = () => engine.inject(handle, { writeToolMounted: input.writeToolsMounted, reminderMark: input.reminderMark });
449
+ injectFn = () => engine.inject(handle, { writeToolMounted: writeChannelOpen, reminderMark: input.reminderMark });
449
450
  harvestBoth = async () => engine.harvest(handle, { ...pollutedOpts(engine), sessionId, ...(await admitNothingOptsNow()) });
450
451
  toolPlanes = [
451
452
  {
@@ -457,6 +458,22 @@ export async function prepareMemory(input) {
457
458
  },
458
459
  ];
459
460
  }
461
+ const foreignUnderRoot = [];
462
+ if (input.writeToolsMounted && writeHandle.writeScope !== null && input.advertisedWritableDirs !== undefined) {
463
+ for (const d of [...writeEngine.readonlyDirNamesUnderRoot(writeHandle)].sort())
464
+ foreignUnderRoot.push(`another scope's directory ${d}`);
465
+ const controlPlane = canonicalize(writeEngine.controlPlaneDir);
466
+ if (isContainedIn(writeHandle.writableRoot, controlPlane))
467
+ foreignUnderRoot.push(`the engine's control plane ${controlPlane}`);
468
+ }
469
+ if (foreignUnderRoot.length > 0) {
470
+ deps.onError?.(new Error(`memory advertised writable directory refused: the write scope's root ${writeHandle.writableRoot} contains what is not the model's to touch (${foreignUnderRoot.join("; ")}) — the file tools' root fence is directory-granular, so admitting it would expose those files; memory writes through the file tools stay closed this session and the write instruction is not taught.`), { phase: "config", sessionId });
471
+ }
472
+ writeChannelOpen =
473
+ input.writeToolsMounted &&
474
+ writeHandle.writeScope !== null &&
475
+ foreignUnderRoot.length === 0 &&
476
+ (input.advertisedWritableDirs === undefined || (await input.advertisedWritableDirs.admit({ path: writeHandle.writableRoot, advertiser: "memory", expectCanonical: writeHandle.writableRoot })));
460
477
  memoryWriteGateRef.current = async (w) => {
461
478
  if (readOnlyEngine !== undefined && readOnlyHandle !== undefined) {
462
479
  const ro = readOnlyEngine.gateWrite(readOnlyHandle, w.key, w.content);
@@ -690,56 +707,63 @@ export async function prepareMemory(input) {
690
707
  }
691
708
  }
692
709
  }
693
- let memoryBlock;
694
- let seedFiles;
695
- let recallDisciplineSegment;
696
710
  if (memorySpec && memorySpec.enabled && !useMemoryEngine) {
697
711
  const detail = "TaskSpec.memory is enabled but RunnerDeps.memoryBackend is not configured; memory is inactive for this task (the legacy memoryStore fallback was retired in design/138 S4 and the dep was removed in design/157 B19).";
698
712
  deps.onError?.(new Error(detail), { phase: "config", sessionId });
699
713
  }
700
- if (memoryEngineSession) {
701
- const injection = memoryEngineSession.inject();
702
- let blockBody = injection.block;
703
- if (captureOptedOut) {
704
- blockBody = [MEMORY_CAPTURE_OPTOUT_NOTICE, injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
705
- }
706
- else if (captureIndeterminate && input.rosterCanPersist) {
707
- blockBody = [MEMORY_CAPTURE_INDETERMINATE_NOTICE, injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
708
- }
709
- else if (!input.rosterCanPersist && injection.instruction === "" && injection.readOnlyNotice === undefined) {
710
- blockBody = blockBody.trim() ? `${MEMORY_READONLY_NOTICE}\n\n${blockBody}` : MEMORY_READONLY_NOTICE;
711
- }
712
- else if (!input.rosterCanPersist && injection.instruction !== "") {
713
- blockBody = [MEMORY_READONLY_NOTICE, injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
714
- }
715
- else if (input.memoryPersistenceDeclared === true && injection.readOnlyNotice !== undefined) {
716
- blockBody = [injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
717
- }
718
- if (!input.rosterCanPersist &&
719
- (injection.announceBlock?.trim() ?? "") !== "" &&
720
- blockBody.includes(injection.announceBlock) &&
721
- !blockBody.trimEnd().endsWith(MEMORY_ANNOUNCEMENT_READONLY_CODA)) {
722
- blockBody = `${blockBody}\n\n${MEMORY_ANNOUNCEMENT_READONLY_CODA}`;
723
- }
724
- if (captureOptedOut &&
725
- input.rosterCanPersist &&
726
- (injection.announceBlock?.trim() ?? "") !== "" &&
727
- blockBody.includes(injection.announceBlock) &&
728
- !blockBody.trimEnd().endsWith(MEMORY_ANNOUNCEMENT_CAPTURE_OPTOUT_CODA)) {
729
- blockBody = `${blockBody}\n\n${MEMORY_ANNOUNCEMENT_CAPTURE_OPTOUT_CODA}`;
730
- }
731
- if (blockBody.trim())
732
- memoryBlock = blockBody;
733
- if (memoryTools !== undefined) {
734
- recallDisciplineSegment = memoryRecallDisciplineSegment(input.memoryProvenance, { markedEntriesServed: memoryMarkedEntriesServed });
735
- memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${recallDisciplineSegment}` : recallDisciplineSegment;
736
- }
737
- if (memoryEngineSession.handle.writeScope !== null && input.writeToolsMounted && input.rosterCanPersist && !captureOptedOut && !captureIndeterminate) {
738
- memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${MEMORY_PREFERENCE_DISCIPLINE}` : MEMORY_PREFERENCE_DISCIPLINE;
714
+ const composeInjection = (facts) => {
715
+ let memoryBlock;
716
+ let seedFiles;
717
+ let recallDisciplineSegment;
718
+ if (memoryEngineSession) {
719
+ const injection = memoryEngineSession.inject();
720
+ let blockBody = injection.block;
721
+ if (captureOptedOut) {
722
+ blockBody = [MEMORY_CAPTURE_OPTOUT_NOTICE, injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
723
+ }
724
+ else if (captureIndeterminate && facts.rosterCanPersist) {
725
+ blockBody = [MEMORY_CAPTURE_INDETERMINATE_NOTICE, injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
726
+ }
727
+ else if (!facts.rosterCanPersist && injection.instruction === "" && injection.readOnlyNotice === undefined) {
728
+ blockBody = blockBody.trim() ? `${MEMORY_READONLY_NOTICE}\n\n${blockBody}` : MEMORY_READONLY_NOTICE;
729
+ }
730
+ else if (!facts.rosterCanPersist && injection.instruction !== "") {
731
+ blockBody = [MEMORY_READONLY_NOTICE, injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
732
+ }
733
+ else if (input.memoryPersistenceDeclared === true && injection.readOnlyNotice !== undefined) {
734
+ blockBody = [injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
735
+ }
736
+ if (!facts.rosterCanPersist &&
737
+ (injection.announceBlock?.trim() ?? "") !== "" &&
738
+ blockBody.includes(injection.announceBlock) &&
739
+ !blockBody.trimEnd().endsWith(MEMORY_ANNOUNCEMENT_READONLY_CODA)) {
740
+ blockBody = `${blockBody}\n\n${MEMORY_ANNOUNCEMENT_READONLY_CODA}`;
741
+ }
742
+ if (captureOptedOut &&
743
+ facts.rosterCanPersist &&
744
+ (injection.announceBlock?.trim() ?? "") !== "" &&
745
+ blockBody.includes(injection.announceBlock) &&
746
+ !blockBody.trimEnd().endsWith(MEMORY_ANNOUNCEMENT_CAPTURE_OPTOUT_CODA)) {
747
+ blockBody = `${blockBody}\n\n${MEMORY_ANNOUNCEMENT_CAPTURE_OPTOUT_CODA}`;
748
+ }
749
+ if (blockBody.trim())
750
+ memoryBlock = blockBody;
751
+ if (memoryTools !== undefined) {
752
+ recallDisciplineSegment = memoryRecallDisciplineSegment(input.memoryProvenance, { markedEntriesServed: memoryMarkedEntriesServed });
753
+ memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${recallDisciplineSegment}` : recallDisciplineSegment;
754
+ }
755
+ if (memoryEngineSession.handle.writeScope !== null && writeChannelOpen && facts.rosterCanPersist && !captureOptedOut && !captureIndeterminate) {
756
+ memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${MEMORY_PREFERENCE_DISCIPLINE}` : MEMORY_PREFERENCE_DISCIPLINE;
757
+ }
758
+ if (memoryBlock !== undefined && injection.indexSeed !== undefined && facts.rosterCanPersist && !captureOptedOut && !captureIndeterminate)
759
+ seedFiles = [injection.indexSeed];
739
760
  }
740
- if (memoryBlock !== undefined && injection.indexSeed !== undefined && input.rosterCanPersist && !captureOptedOut && !captureIndeterminate)
741
- seedFiles = [injection.indexSeed];
742
- }
761
+ return {
762
+ memoryBlock,
763
+ ...(recallDisciplineSegment !== undefined ? { recallDisciplineSegment } : {}),
764
+ ...(seedFiles !== undefined ? { seedFiles } : {}),
765
+ };
766
+ };
743
767
  if (effectiveMemoryScopes === undefined) {
744
768
  effectiveMemoryScopes = memorySpec
745
769
  ? memorySpec.enabled
@@ -749,12 +773,10 @@ export async function prepareMemory(input) {
749
773
  }
750
774
  return {
751
775
  memoryEngineSession,
752
- memoryBlock,
753
776
  admittedOrgScopes,
754
777
  ownOrgVerdict,
755
778
  effectiveMemoryScopes,
756
779
  ...(memoryTools !== undefined ? { memoryTools } : {}),
757
- ...(recallDisciplineSegment !== undefined ? { recallDisciplineSegment } : {}),
758
- ...(seedFiles !== undefined ? { seedFiles } : {}),
780
+ composeInjection,
759
781
  };
760
782
  }
@@ -25,7 +25,7 @@ import { type CheckpointStore } from "../checkpoint-store.js";
25
25
  import type { SessionStore } from "../session.js";
26
26
  import { type OnAsk, type ToolCallRequest, type ToolPolicy } from "../tool-policy.js";
27
27
  import { type ToolResultStore } from "../tool-result-store.js";
28
- import type { RunnerDeps, RuntimeCaps, TaskSpec } from "../types.js";
28
+ import type { RunnerDeps, RuntimeCaps, ShellGateDoctrine, TaskSpec } from "../types.js";
29
29
  import type { CwdRef } from "../../tools/fs/fs-shared.js";
30
30
  import { type ContentAskBinding } from "./content-ask-bindings.js";
31
31
  import type { AskLane, ParkAsk, Prepared, SuspendSaga } from "./contracts.js";
@@ -108,12 +108,11 @@ export interface PrepareParkAskInput {
108
108
  maxSuspends: number;
109
109
  /** borrowed-mutable — the run-scoped remote-lifecycle failure log. Writer here: a refused `suspendVM` (`push`). */
110
110
  remoteEnvFailures: Prepared["remoteEnvFailures"];
111
- /** borrowed-readonly — whether Bash is coarsely shell-gated (the risk descriptor's `shellGated`). */
112
- shellGatedBash: boolean;
113
- /** borrowed-readonly — whether Monitor is coarsely shell-gated (same). */
114
- shellGatedMonitor: boolean;
111
+ /** borrowed-readonly — the shell-seat provenance set: the shell-running tools whose tier is the engine's shell seat's, not a
112
+ * deployment mark (the risk descriptor's `shellGated` is the gated tool's membership). */
113
+ shellGatedTools: ReadonlySet<string>;
115
114
  /** borrowed-readonly — the effective shell-gate doctrine persisted on the descriptor. */
116
- effectiveShellGate: "off" | "classify" | "always";
115
+ effectiveShellGate: ShellGateDoctrine;
117
116
  /** borrowed-readonly — the EFFECTIVE durable-approval opt-in (the caller's, or the synthesized mandate scope): picks the
118
117
  * gate kind, the scope and the ttl. */
119
118
  durableApproval: TaskSpec["durableApproval"];
@@ -22,7 +22,7 @@ class ParkRefusal extends Error {
22
22
  }
23
23
  }
24
24
  export function prepareParkAsk(input) {
25
- const { askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedBash, shellGatedMonitor, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef } = input;
25
+ const { askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef } = input;
26
26
  if (askLane === undefined || saga === undefined)
27
27
  return { parkAsk: undefined };
28
28
  const { composedCallSignal, approvalPreviewOf } = askLane;
@@ -232,8 +232,8 @@ export function prepareParkAsk(input) {
232
232
  ...(shadowedRule !== undefined ? { shadowedRule } : {}),
233
233
  ...(probeReason !== undefined ? { probeReason } : {}),
234
234
  ...(probeCause !== undefined ? { probeCause } : {}),
235
- shellGated: (req.toolName === "Bash" && shellGatedBash) || (req.toolName === "Monitor" && shellGatedMonitor),
236
- ...(effectiveShellGate !== "off" ? { shellGateDoctrine: effectiveShellGate } : {}),
235
+ shellGated: shellGatedTools.has(req.toolName),
236
+ shellGateDoctrine: effectiveShellGate,
237
237
  });
238
238
  gate =
239
239
  safety !== undefined || realApproval !== undefined
@@ -78,8 +78,8 @@ export interface PreparePolicyChainInput {
78
78
  /** borrowed-readonly — the caller tools' names, the first-await snapshot (the sandbox boundary predicate: a caller tool
79
79
  * is never contained by the hand tools' env). */
80
80
  ownToolNames: ReadonlySet<string>;
81
- /** borrowed-readonly — whether the shell tool is under an operator `shellGate:"always"` (the offer factory's mandate term). */
82
- shellGatedBash: boolean;
81
+ /** borrowed-readonly — the shell-seat provenance set (the offer factory's `shellGated` mandate term, read per call). */
82
+ shellGatedTools: ReadonlySet<string>;
83
83
  /** borrowed-readonly — the tracked-cwd seat, read per call by the lanes and the offer factory (never written here). */
84
84
  handsCwdRef: CwdRef | undefined;
85
85
  /** borrowed-readonly — the compiled read-deny judge, or undefined (the offer factory's directory negative control). */
@@ -5,7 +5,7 @@ import { resolveCheckpointStore } from "../checkpoint-store.js";
5
5
  import { cloneObserverInput, resolveHookTimeoutMs } from "../hooks.js";
6
6
  import { isNamespacedCoveringRuleName, namespacedRuleNameCovers } from "../permission-rules.js";
7
7
  import { isIsolated } from "../remote-env.js";
8
- import { askApproverIdentity, checkToolPolicyProjection, combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, isLiveApproverSeat, resolveAsk, toolPolicyNameSets } from "../tool-policy.js";
8
+ import { askApproverIdentity, carryEngineAttestations, carryPrecedingSettlement, checkToolPolicyProjection, combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, isLiveApproverSeat, resolveAsk, toolPolicyNameSets } from "../tool-policy.js";
9
9
  import { RETIRED_TOOL_NAMES } from "../tool-name-aliases.js";
10
10
  import { NAMESPACED_NAME_SHAPES, protocolOf } from "../protocol-table.js";
11
11
  import { emitTrace } from "../trace.js";
@@ -16,7 +16,7 @@ import { askGrantShapeOf } from "./inherited-ask-grants.js";
16
16
  import { createPermissionRuleLanes, inheritedAskRuleEvidence, createRuleOffersOf } from "./permission-rule-lanes.js";
17
17
  import { createSessionRulePolicy } from "./session-rule-policy.js";
18
18
  export async function preparePolicyChain(input) {
19
- const { spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, ownToolNames, shellGatedBash, handsCwdRef, readDenyMatcher, questionToolMounted, runtimeCaps, inheritedAncestorRules, inheritedParentConstraints, seedInheritedGate, hasSkillManifest, skillScope, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef } = input;
19
+ const { spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, ownToolNames, shellGatedTools, handsCwdRef, readDenyMatcher, questionToolMounted, runtimeCaps, inheritedAncestorRules, inheritedParentConstraints, seedInheritedGate, hasSkillManifest, skillScope, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef } = input;
20
20
  const policy = lockedPreflight.toolPolicy;
21
21
  const auditPolicyNames = (auditee) => {
22
22
  const nameGroups = toolPolicyNameSets(auditee);
@@ -265,7 +265,7 @@ export async function preparePolicyChain(input) {
265
265
  localOwnerDeclared: deps.localOwnerRules === true,
266
266
  egressTools,
267
267
  irreversibilityTier,
268
- shellGatedBash,
268
+ shellGatedTools,
269
269
  taskRoot: taskRootFinal,
270
270
  cwdRef: handsCwdRef,
271
271
  deniesDirectoryRead: readDenyMatcher !== undefined ? (d) => readDenyMatcher.matchPath(d) !== null : undefined,
@@ -557,7 +557,10 @@ export async function preparePolicyChain(input) {
557
557
  for (const layer of recheckLayers) {
558
558
  const d = await layer.check(finalReq, signal);
559
559
  if (d.action === "deny") {
560
- return { ...d, updatedInput: decision.updatedInput };
560
+ const carried = { ...d, updatedInput: decision.updatedInput };
561
+ carryEngineAttestations(d, carried);
562
+ carryPrecedingSettlement(decision, carried);
563
+ return carried;
561
564
  }
562
565
  if (d.updatedInput !== undefined) {
563
566
  return {
@@ -28,6 +28,7 @@ import type { MaterializedMcp } from "../mcp.js";
28
28
  import type { ResolvedRole } from "../roles.js";
29
29
  import type { RunnerDeps, TaskSpec } from "../types.js";
30
30
  import type { PrepareResume, PromptFeatureFlags, RunInternals, ToolFaceSnapshot } from "./contracts.js";
31
+ import type { AdvertisedWritableDirs } from "./advertised-writable-dirs.js";
31
32
  import { type GitStatusLaneRef } from "./git-status-frame.js";
32
33
  export interface PreparePromptInputsInput {
33
34
  /** borrowed-readonly — the REBOUND spec (`spec′`). Read: `promptProvider`, `hooks` (flag), `systemPrompt`
@@ -90,6 +91,9 @@ export interface PreparePromptInputsInput {
90
91
  additionalRootsCanonical: readonly string[];
91
92
  /** borrowed-readonly — the canonical extra read-only roots (copied into the env facts). */
92
93
  additionalReadRootsCanonical: readonly string[];
94
+ /** borrowed-readonly — #691: the advertised-writable-directory seat (hands only). The scratchpad section is
95
+ * rendered only when the seat did not SKIP the scratchpad — a directory the fence refused is not advertised. */
96
+ advertisedWritableDirs?: AdvertisedWritableDirs;
93
97
  /** borrowed-readonly — the acquired session's id (operator-line context). */
94
98
  sessionId: string;
95
99
  }
@@ -4,7 +4,7 @@ import { probeGitStatusLane } from "./git-status-frame.js";
4
4
  import { fenceMcpServerInstructions } from "./turn-attachments.js";
5
5
  const ULTRA_REASONING_TIERS = new Set(["xhigh", "max"]);
6
6
  export async function preparePromptInputs(input) {
7
- const { spec, deps, failClosedReason, lockedPreflight, selfOrchestrationActive, executionEnv, reminderMark, resolvedReadFace, toolFaceSnapshot, promptProfile, fableMitigations, internals, thinking, ownedEnv, delegation, resolvedRole, mcp, model, externalContentTargetActive, resume, handsEnabled, taskRootFinal, additionalRootsCanonical, additionalReadRootsCanonical, sessionId } = input;
7
+ const { spec, deps, failClosedReason, lockedPreflight, selfOrchestrationActive, executionEnv, reminderMark, resolvedReadFace, toolFaceSnapshot, promptProfile, fableMitigations, internals, thinking, ownedEnv, delegation, resolvedRole, mcp, model, externalContentTargetActive, resume, handsEnabled, taskRootFinal, additionalRootsCanonical, additionalReadRootsCanonical, sessionId, advertisedWritableDirs } = input;
8
8
  const provider = spec.promptProvider ?? deps.promptProvider ?? defaultPromptProvider;
9
9
  const promptPolicyEnabled = Boolean(lockedPreflight.toolPolicy);
10
10
  const promptHooks = spec.hooks ?? deps.hooks;
@@ -51,7 +51,7 @@ export async function preparePromptInputs(input) {
51
51
  envFacts.sandboxPkgSource = spec.envFacts.pkgSource;
52
52
  if (spec.envFacts?.egress === "none" || spec.envFacts?.egress === "allowlist" || spec.envFacts?.egress === "full")
53
53
  envFacts.sandboxEgress = spec.envFacts.egress;
54
- if (spec.envFacts?.scratchpadDir)
54
+ if (spec.envFacts?.scratchpadDir && advertisedWritableDirs?.verdictOf("scratchpad") !== "skipped")
55
55
  envFacts.scratchpadDir = spec.envFacts.scratchpadDir;
56
56
  if (externalContentTargetActive)
57
57
  envFacts.externalContentTarget = true;