@sema-agent/core 7.13.0 → 7.14.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 (85) hide show
  1. package/CHANGELOG.md +80 -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 +28 -9
  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 +9 -16
  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 +5 -16
  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/denial-limit-arms.d.ts +7 -9
  34. package/dist/core/runner/denial-limit-arms.js +8 -10
  35. package/dist/core/runner/gate-exit.d.ts +15 -3
  36. package/dist/core/runner/gate-exit.js +6 -4
  37. package/dist/core/runner/prepare-caps-and-workflow.d.ts +34 -4
  38. package/dist/core/runner/prepare-caps-and-workflow.js +47 -12
  39. package/dist/core/runner/prepare-gate-stations.d.ts +1 -1
  40. package/dist/core/runner/prepare-hands-readface.d.ts +10 -4
  41. package/dist/core/runner/prepare-hands-readface.js +5 -14
  42. package/dist/core/runner/prepare-inherited-gate.d.ts +5 -3
  43. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  44. package/dist/core/runner/prepare-memory.d.ts +31 -34
  45. package/dist/core/runner/prepare-memory.js +73 -51
  46. package/dist/core/runner/prepare-policy-chain.js +5 -2
  47. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -0
  48. package/dist/core/runner/prepare-prompt-inputs.js +2 -2
  49. package/dist/core/runner/prepare-question-face.d.ts +10 -8
  50. package/dist/core/runner/prepare-question-face.js +1 -3
  51. package/dist/core/runner/prepare-safety-scan.js +1 -1
  52. package/dist/core/runner/prepare-task.js +57 -48
  53. package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
  54. package/dist/core/runner/prepare-wiring-manifest.js +1 -1
  55. package/dist/core/runner/run-compaction-machinery.js +2 -0
  56. package/dist/core/runner/run-harness-handlers.js +3 -1
  57. package/dist/core/runner/stream-settle-backstop.js +1 -1
  58. package/dist/core/runtime-caps.d.ts +21 -0
  59. package/dist/core/runtime-caps.js +5 -1
  60. package/dist/core/task-event.d.ts +11 -2
  61. package/dist/core/task-registry-shared.js +8 -6
  62. package/dist/core/task-result.d.ts +15 -0
  63. package/dist/core/terminal-cause.d.ts +6 -2
  64. package/dist/core/tool-policy.d.ts +34 -28
  65. package/dist/core/tool-policy.js +29 -5
  66. package/dist/core/tool-roster.js +2 -0
  67. package/dist/core/tool-spec.d.ts +13 -12
  68. package/dist/core/types.d.ts +2 -1
  69. package/dist/core/types.js +1 -0
  70. package/dist/core/wiring-manifest.d.ts +16 -9
  71. package/dist/core/wiring-manifest.js +8 -3
  72. package/dist/index.d.ts +5 -4
  73. package/dist/index.js +4 -3
  74. package/dist/orchestration/run-workflow-tool.d.ts +8 -8
  75. package/dist/orchestration/run-workflow-tool.js +1 -1
  76. package/dist/orchestration/workflow-primitives.d.ts +4 -3
  77. package/dist/orchestration/workflow-primitives.js +3 -3
  78. package/dist/orchestration/workflow-types.d.ts +14 -0
  79. package/dist/orchestration/workflow.d.ts +41 -4
  80. package/dist/orchestration/workflow.js +23 -5
  81. package/dist/tools/fs/fs-shared.js +9 -3
  82. package/dist/tools/fs/read-deny.d.ts +24 -8
  83. package/dist/tools/fs/read-deny.js +20 -1
  84. package/package.json +2 -1
  85. package/test/export-surface.snapshot.json +25 -1
@@ -3,6 +3,7 @@ import { type ReadFileState } from "../../tools/fs/safety.js";
3
3
  import type { RunnerDeps, TaskSpec } from "../types.js";
4
4
  import type { Model } from "../../internal/llm.js";
5
5
  import type { AgentTool, ExecutionEnv, WorkspaceState } from "../../internal/harness.js";
6
+ import type { AdvertisedWritableDirs } from "./advertised-writable-dirs.js";
6
7
  import type { StoredSession } from "../session.js";
7
8
  import type { IrreversibilityTier, PrepareResume, ReversibilityProbes, RunInternals, ToolFaceSnapshot } from "./contracts.js";
8
9
  import { RosterBuilder } from "../tool-roster.js";
@@ -161,13 +162,18 @@ export interface PrepareHandsReadFaceInput {
161
162
  * when no file restore ran. Fail-closed by construction — dropping a read record only ever
162
163
  * produces refusals. */
163
164
  restoredFilePaths?: readonly string[];
164
- /** borrowed-readonly — (ref cell; the REF itself is never replaced) design/138 S2-C late-bound memory
165
- * write gate: the band's beforeWrite closure reads `.current` call-time; prepareMemory (driver,
166
- * after this phase) installs the gate the one writer; this phase only reads. Declared in the driver because the SAME ref feeds
167
- * prepareMemory. */
165
+ /** borrowed-readonly — (ref cell; the REF itself is never replaced) design/138 S2-C memory write gate:
166
+ * the band's beforeWrite closure reads `.current` call-time; prepareMemory (driver, BEFORE this phase
167
+ * since #691 the memory root must be known before the toolkit compiles) installs the gate the one
168
+ * writer; this phase only reads. Declared in the driver because the SAME ref feeds prepareMemory. */
168
169
  memoryWriteGateRef: {
169
170
  current?: BeforeWriteHook;
170
171
  };
172
+ /** borrowed-mutable — #691: the advertised-writable-directory seat (see its module header for the one
173
+ * rule). Present on the hands leg only (a fence exists). The memory phase, which runs BEFORE this one,
174
+ * has already admitted its write root through it; this phase admits the deployment's scratchpad and
175
+ * folds `roots` into the write-capable containment allowlist before the toolkit compiles. */
176
+ advertisedWritableDirs?: AdvertisedWritableDirs;
171
177
  /** borrowed-readonly — the driver's per-tool offload wrapper (design/158 posture + policy); the
172
178
  * band rides through it exactly as caller/MCP tools do. */
173
179
  firstPartyOffload: (tool: AgentTool) => AgentTool;
@@ -124,7 +124,7 @@ export function resolveHandsLessReadFace(input) {
124
124
  }
125
125
  export async function prepareHandsMount(input) {
126
126
  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;
127
+ 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
128
  assertStringArraySeat(spec.additionalDirectories, "TaskSpec.additionalDirectories", "strings");
129
129
  assertStringArraySeat(spec.additionalReadDirectories, "TaskSpec.additionalReadDirectories", "strings");
130
130
  assertStringArraySeat(deps.readDenyPatterns, "RunnerDeps.readDenyPatterns", "compiler-owned");
@@ -178,19 +178,10 @@ export async function prepareHandsMount(input) {
178
178
  };
179
179
  await canonicalizeExtraDirs(spec.additionalDirectories, "additionalDirectories", additionalRootsCanonical);
180
180
  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);
181
+ if (advertisedWritableDirs !== undefined) {
182
+ if (spec.envFacts?.scratchpadDir)
183
+ await advertisedWritableDirs.admit({ path: spec.envFacts.scratchpadDir, advertiser: "scratchpad", materializeMarker: ".sema-scratchpad" });
184
+ additionalRootsCanonical.push(...advertisedWritableDirs.roots);
194
185
  }
195
186
  for (const a of [additionalRootsCanonical, additionalReadRootsCanonical])
196
187
  a.splice(0, a.length, ...new Set(a));
@@ -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
@@ -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
  }
@@ -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";
@@ -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;
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import type { AgentTool } from "../../internal/harness.js";
20
20
  import { type OnQuestion } from "../ask-question.js";
21
- import { type CheckpointStore } from "../checkpoint-store.js";
21
+ import type { CheckpointStore } from "../checkpoint-store.js";
22
22
  import type { RunnerDeps, RuntimeCaps, TaskSpec } from "../types.js";
23
23
  import type { PrepareResume } from "./contracts.js";
24
24
  import { type ContentAskBinding } from "./content-ask-bindings.js";
@@ -28,13 +28,15 @@ export interface PrepareQuestionFaceInput {
28
28
  /** borrowed-readonly — the run's frozen question seat (spec > deps), resolved once at the top of prepare;
29
29
  * the mount predicate and the live-face derivation both read this one value. */
30
30
  frozenOnQuestion: OnQuestion | undefined;
31
- /** borrowed-readonly — the REBOUND spec. Read: `checkpointStore` (the per-run off switch), `durableApproval`
32
- * (the durable face's opt-in), `interactiveTools` (the override), `principal` (the tool's source identity),
33
- * `interactiveQuestionFallback` (restores the continuation under a resolved posture). Never mutated. */
34
- spec: Pick<TaskSpec, "checkpointStore" | "durableApproval" | "interactiveTools" | "principal" | "interactiveQuestionFallback">;
35
- /** borrowed-readonly — the deployment seats: `checkpointStore` (the store the leg snapshot resolves against)
36
- * and `onError` (the stranded-answer and synthetic-continuation operator lines; swallow-guarded). */
37
- deps: Pick<RunnerDeps, "checkpointStore" | "onError">;
31
+ /** borrowed-readonly — the REBOUND spec. Read: `durableApproval` (the durable face's opt-in), `interactiveTools`
32
+ * (the override), `principal` (the tool's source identity), `interactiveQuestionFallback` (restores the
33
+ * continuation under a resolved posture). Never mutated. */
34
+ spec: Pick<TaskSpec, "durableApproval" | "interactiveTools" | "principal" | "interactiveQuestionFallback">;
35
+ /** borrowed-readonly — the deployment seat `onError` (the stranded-answer and synthetic-continuation operator
36
+ * lines; swallow-guarded). */
37
+ deps: Pick<RunnerDeps, "onError">;
38
+ /** borrowed-readonly — THE leg's checkpoint seat (minted once by the driver after the config doors); the mount face, the routability predicate, the manifest and suspendAsk all read this one value through the result. */
39
+ checkpointStore: CheckpointStore | undefined;
38
40
  /** borrowed-readonly — the principal's resolved runtime caps as of this point (`forceDurableGate` is the
39
41
  * mandate that outranks a live face only where a store exists to park the question). */
40
42
  runtimeCaps: Pick<RuntimeCaps, "forceDurableGate"> | undefined;
@@ -1,16 +1,14 @@
1
1
  import { createAskUserQuestionTool, isLiveQuestionFace } from "../ask-question.js";
2
2
  import { boundInputHashOf } from "../canonical-json.js";
3
- import { resolveCheckpointStore } from "../checkpoint-store.js";
4
3
  import { CONTENT_ASK_BINDING_CAP } from "./content-ask-bindings.js";
5
4
  import { RosterBuilder } from "../tool-roster.js";
6
5
  export { CONTENT_ASK_BINDING_CAP } from "./content-ask-bindings.js";
7
6
  export function prepareQuestionFace(input) {
8
- const { frozenOnQuestion, spec, deps, runtimeCaps, resolvedInteractionPosture, resume, sessionId, roster, inheritedUnavailableAsks } = input;
7
+ const { frozenOnQuestion, spec, deps, checkpointStore, runtimeCaps, resolvedInteractionPosture, resume, sessionId, roster, inheritedUnavailableAsks } = input;
9
8
  const tools = roster.tools;
10
9
  const onQuestion = frozenOnQuestion;
11
10
  const liveQuestionFace = isLiveQuestionFace(onQuestion) ? onQuestion : undefined;
12
11
  const contentAskBindings = new Map();
13
- const checkpointStore = resolveCheckpointStore(spec, deps);
14
12
  const questionParkStoreWired = checkpointStore !== undefined;
15
13
  const questionDurableMandateBinding = runtimeCaps?.forceDurableGate === true && questionParkStoreWired;
16
14
  const contentAskRoutable = (toolCallId) => liveQuestionFace !== undefined &&
@@ -23,7 +23,7 @@ export function prepareSafetyScan(input) {
23
23
  ...(typeof t.modelGate === "string" ? { modelGate: t.modelGate } : {}),
24
24
  });
25
25
  if (bound !== undefined) {
26
- const e = new Error(`Tool "${t.name.slice(0, 40)}" cannot ride a roster row: ${bound.message}.`);
26
+ const e = new Error(`A caller tool cannot ride a roster row: ${bound.message}.`);
27
27
  e.code = bound.code;
28
28
  throw e;
29
29
  }