@sema-agent/core 7.43.0 → 7.45.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 (98) hide show
  1. package/CHANGELOG.md +245 -0
  2. package/dist/agents/subagent.d.ts +2 -1
  3. package/dist/agents/subagent.js +33 -14
  4. package/dist/agents/tool-filter.d.ts +65 -4
  5. package/dist/agents/tool-filter.js +127 -4
  6. package/dist/brain/errors.d.ts +23 -2
  7. package/dist/brain/errors.js +26 -2
  8. package/dist/brain/stream-engine.d.ts +2 -1
  9. package/dist/brain/stream-engine.js +10 -9
  10. package/dist/core/agent-definition.d.ts +2 -2
  11. package/dist/core/allow-entry.d.ts +25 -1
  12. package/dist/core/allow-entry.js +29 -4
  13. package/dist/core/ask-origin.d.ts +23 -4
  14. package/dist/core/ask-origin.js +10 -4
  15. package/dist/core/checkpoint-store.d.ts +28 -15
  16. package/dist/core/checkpoint-store.js +4 -13
  17. package/dist/core/decision-receipt.js +1 -1
  18. package/dist/core/effective-path-target.d.ts +7 -6
  19. package/dist/core/effective-path-target.js +3 -7
  20. package/dist/core/engine-notice.d.ts +33 -14
  21. package/dist/core/engine-notice.js +14 -7
  22. package/dist/core/gate-lanes.js +2 -0
  23. package/dist/core/gate-outcome.d.ts +5 -0
  24. package/dist/core/gate-outcome.js +2 -0
  25. package/dist/core/governance-codes.d.ts +4 -3
  26. package/dist/core/governance-codes.js +1 -0
  27. package/dist/core/hooks.d.ts +2 -0
  28. package/dist/core/memory-engine/content-origin.d.ts +0 -34
  29. package/dist/core/memory-engine/content-origin.js +9 -4
  30. package/dist/core/memory-engine/engine.d.ts +2 -2
  31. package/dist/core/memory-engine/engine.js +7 -14
  32. package/dist/core/memory-engine/file-backend.d.ts +2 -2
  33. package/dist/core/memory-engine/file-backend.js +5 -7
  34. package/dist/core/memory-engine/layout.d.ts +41 -4
  35. package/dist/core/memory-engine/layout.js +35 -5
  36. package/dist/core/permission-rules.d.ts +4 -0
  37. package/dist/core/permission-rules.js +5 -2
  38. package/dist/core/real-approval.d.ts +9 -6
  39. package/dist/core/real-approval.js +5 -5
  40. package/dist/core/runner/child-tree.d.ts +13 -0
  41. package/dist/core/runner/child-tree.js +3 -0
  42. package/dist/core/runner/contracts.d.ts +9 -18
  43. package/dist/core/runner/prepare-ask-lane.d.ts +2 -0
  44. package/dist/core/runner/prepare-ask-lane.js +2 -2
  45. package/dist/core/runner/prepare-config-doors.js +3 -1
  46. package/dist/core/runner/prepare-execution-env.js +2 -1
  47. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  48. package/dist/core/runner/prepare-gate-stations.js +2 -1
  49. package/dist/core/runner/prepare-listings.d.ts +4 -4
  50. package/dist/core/runner/prepare-listings.js +2 -22
  51. package/dist/core/runner/prepare-park-ask.d.ts +2 -0
  52. package/dist/core/runner/prepare-park-ask.js +2 -2
  53. package/dist/core/runner/prepare-policy-chain.d.ts +2 -0
  54. package/dist/core/runner/prepare-policy-chain.js +4 -2
  55. package/dist/core/runner/prepare-task.js +10 -6
  56. package/dist/core/runner/resume-policy-outcome.js +2 -1
  57. package/dist/core/runner/run-leg.js +1 -1
  58. package/dist/core/runner/run-notification-lane.js +2 -2
  59. package/dist/core/runner/runtask.d.ts +2 -0
  60. package/dist/core/runner/runtask.js +3 -5
  61. package/dist/core/runner/steer-admission.d.ts +10 -11
  62. package/dist/core/runner/steer-admission.js +6 -8
  63. package/dist/core/runner/stream-steer-verb.d.ts +5 -4
  64. package/dist/core/runner/stream-steer-verb.js +8 -10
  65. package/dist/core/runner/sub-agent-deny-lists.d.ts +32 -0
  66. package/dist/core/runner/sub-agent-deny-lists.js +121 -0
  67. package/dist/core/runner/synthetic-tools.d.ts +1 -1
  68. package/dist/core/runner/synthetic-tools.js +3 -2
  69. package/dist/core/session-policy-store.d.ts +6 -2
  70. package/dist/core/session-policy-store.js +2 -1
  71. package/dist/core/skill-grant.d.ts +10 -2
  72. package/dist/core/skill-grant.js +20 -4
  73. package/dist/core/steer-replay.d.ts +46 -0
  74. package/dist/core/steer-replay.js +22 -0
  75. package/dist/core/task-event.d.ts +3 -2
  76. package/dist/core/task-notification.d.ts +12 -1
  77. package/dist/core/task-notification.js +13 -1
  78. package/dist/core/task-spec.d.ts +5 -3
  79. package/dist/core/task-stream.d.ts +12 -6
  80. package/dist/core/tool-errors.d.ts +2 -2
  81. package/dist/core/tool-policy.d.ts +15 -4
  82. package/dist/core/tool-policy.js +9 -13
  83. package/dist/core/tool-spec.d.ts +20 -2
  84. package/dist/core/types.d.ts +2 -2
  85. package/dist/core/types.js +1 -1
  86. package/dist/index.d.ts +7 -4
  87. package/dist/index.js +7 -4
  88. package/dist/orchestration/workflow-types.d.ts +5 -0
  89. package/dist/orchestration/workflow.d.ts +3 -2
  90. package/dist/orchestration/workflow.js +16 -7
  91. package/dist/tools/fs/removal-targets.d.ts +82 -0
  92. package/dist/tools/fs/removal-targets.js +236 -0
  93. package/dist/tools/fs/shell-read-write-targets.d.ts +27 -0
  94. package/dist/tools/fs/shell-read-write-targets.js +1 -1
  95. package/package.json +1 -1
  96. package/test/export-surface.snapshot.json +43 -5
  97. package/wire-changes/core@7.44.0.json +469 -0
  98. package/wire-changes/core@7.45.0.json +1084 -0
@@ -682,9 +682,9 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
682
682
  * let a transient EIO run the unlocked recovery (and its staged sweep) over a live transaction. */
683
683
  private txnInFlight;
684
684
  /**
685
- * The act gate for store paths ({@link storeTargetEscape}) over `ops`' writes and deletes — shadow
685
+ * The act gate for store paths ({@link landsOutsideStore}) over `ops`' writes and deletes — shadow
686
686
  * ops name control-plane files, not store paths. Refuses with `memory.target_outside_store` at the
687
- * first op that would land outside the store, naming the op and where it resolves. Runs over a
687
+ * first op that would land outside the store, naming the op (its path on `detail.path`). Runs over a
688
688
  * whole batch before its commit marker (no journal is written; a caller that staged files removes
689
689
  * them) and per op at the act itself; past the marker a refusal keeps the journal, which replays
690
690
  * once the path resolves inside the store again.
@@ -8,7 +8,7 @@ import { ambiguousOriginRepresentation, committedDistilledOf, committedOriginOf,
8
8
  import { convertV1SidecarKeys } from "./delegation-settlement.js";
9
9
  import { layerObservationLookup, observedDivergenceNotice } from "./layer-observations.js";
10
10
  import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles } from "./entry-scan.js";
11
- import { ControlPlaneCorruptError, CURSORS_FILE, QUARANTINE_DIR, CHALLENGES_FILE, IMPORT_RECEIPTS_DIR, LINEAGE_FILE, SESSION_POLLUTION_DIR, appendChallengeEvents, atomicWriteFileSync, quarantineAndTombstone, captureAndClearLineageForEntries, adoptRootScopeLayout, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, ensureDirExists, enqueueMemoryAnnouncement, importLatchTxnId, importLineageCommitted, importSessionPollution, isContainedIn, permissionRemedyOf, withPermissionRemedy, lineageContributionsOfSession, listSessionPollution, readChallengeEvents, readLineageRecord, registerScope, registeredScopes, releaseImportLatch, layoutNeedsAdoption, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, scopeRegistryShape, stageImportLatch, landingPathOf, storeTargetEscape, TARGET_OUTSIDE_STORE, writeAllSync, } from "./layout.js";
11
+ import { ControlPlaneCorruptError, CURSORS_FILE, QUARANTINE_DIR, CHALLENGES_FILE, IMPORT_RECEIPTS_DIR, LINEAGE_FILE, SESSION_POLLUTION_DIR, appendChallengeEvents, atomicWriteFileSync, quarantineAndTombstone, captureAndClearLineageForEntries, adoptRootScopeLayout, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, ensureDirExists, enqueueMemoryAnnouncement, importLatchTxnId, importLineageCommitted, importSessionPollution, isContainedIn, permissionRemedyOf, withPermissionRemedy, lineageContributionsOfSession, listSessionPollution, readChallengeEvents, readLineageRecord, registerScope, registeredScopes, releaseImportLatch, layoutNeedsAdoption, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, scopeRegistryShape, stageImportLatch, landingPathOf, landsOutsideStore, storeActRefusal, storeRootMovedRefusal, writeAllSync, } from "./layout.js";
12
12
  import { screenInboundEntries } from "./data-plane.js";
13
13
  import { scanMemoryFileName, scanMemoryWrite } from "./scan.js";
14
14
  import { thrownText } from "../thrown-value.js";
@@ -1404,7 +1404,7 @@ export class FileMemoryEngineBackend {
1404
1404
  }
1405
1405
  }
1406
1406
  this.assertTxnLockOwnership(fenceToken);
1407
- const replayAt = `the journal replay (journal kept at ${jp})`;
1407
+ const replayAt = "the journal replay (journal kept)";
1408
1408
  this.refuseEscapingTargets(journal.ops, replayAt);
1409
1409
  for (const op of journal.ops) {
1410
1410
  this.refuseEscapingTargets([op], replayAt);
@@ -1470,10 +1470,8 @@ export class FileMemoryEngineBackend {
1470
1470
  if (op.kind !== "write" && op.kind !== "delete")
1471
1471
  continue;
1472
1472
  for (const path of op.staged === undefined ? [op.target] : [op.target, op.staged]) {
1473
- const escape = storeTargetEscape(this.physicalRoot, path);
1474
- if (escape === undefined)
1475
- continue;
1476
- throw erasureCodedError(`${TARGET_OUTSIDE_STORE}: ${at} refused a ${op.kind} of ${op.target}${path === op.target ? "" : ` (its staged file ${path})`} — ${escape}; not performed`, TARGET_OUTSIDE_STORE);
1473
+ if (landsOutsideStore(this.physicalRoot, path))
1474
+ throw storeActRefusal(`${path === op.target ? "a" : "the staged file of a"} ${op.kind} at ${at}`, path);
1477
1475
  }
1478
1476
  }
1479
1477
  }
@@ -1481,7 +1479,7 @@ export class FileMemoryEngineBackend {
1481
1479
  const now = landingPathOf(this.directoryRoot);
1482
1480
  if (now === this.physicalRoot)
1483
1481
  return;
1484
- throw erasureCodedError(`${TARGET_OUTSIDE_STORE}: the store root ${this.directoryRoot} now resolves to ${now}, not ${this.physicalRoot} where this backend opened it — refused; open a new backend on the directory you mean`, TARGET_OUTSIDE_STORE);
1482
+ throw storeRootMovedRefusal("backend", this.directoryRoot);
1485
1483
  }
1486
1484
  sweepStagedFiles() {
1487
1485
  const walk = (d, depth) => {
@@ -259,8 +259,45 @@ export declare function adoptRootScopeLayout(rootPath: string, controlDir: strin
259
259
  export declare const canonicalize: typeof physicalPathOfExisting;
260
260
  /** True iff `child` (canonicalized) is `root` or inside it. */
261
261
  export declare function isContainedIn(root: string, child: string): boolean;
262
- /** The code an act on a store path is refused with when it would land outside the store ({@link storeTargetEscape}). */
262
+ /** The codes the store's physical form refuses an act with — the whole family, one closed set: {@link TARGET_OUTSIDE_STORE} and
263
+ * {@link SCOPE_HOME_NOT_DIRECTORY}. Each is a plain `Error` carrying `code` and a `detail` ({@link StoreFormRefusalDetail}). */
264
+ export declare const STORE_FORM_REFUSAL_CODES: readonly ["memory.target_outside_store", "memory.scope_home_not_directory"];
265
+ export type StoreFormRefusalCode = (typeof STORE_FORM_REFUSAL_CODES)[number];
266
+ /** The code an act on a store path is refused with when it would land outside the store ({@link landsOutsideStore}). */
263
267
  export declare const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
268
+ /** The code a scope home that is not a real directory is refused with ({@link assertHomePhysicalForm}). */
269
+ export declare const SCOPE_HOME_NOT_DIRECTORY = "memory.scope_home_not_directory";
270
+ /** What a scope home can be besides a real directory (an `lstat` verdict). */
271
+ export declare const SCOPE_HOME_FORMS: readonly ["symlink", "file", "other"];
272
+ export type ScopeHomeForm = (typeof SCOPE_HOME_FORMS)[number];
273
+ /** How a refusal's sentence names each form. */
274
+ declare const SCOPE_HOME_FORM_PHRASE: {
275
+ readonly symlink: "a symbolic link";
276
+ readonly file: "a regular file";
277
+ readonly other: "a special file";
278
+ };
279
+ export type HomeFormPhraseCoversEveryForm = AssertAllKeysHandled<Exclude<ScopeHomeForm, keyof typeof SCOPE_HOME_FORM_PHRASE>>;
280
+ /** What a store-form refusal carries beside its sentence. The ONE host path lives here and never in the
281
+ * sentence; `form` is present on {@link SCOPE_HOME_NOT_DIRECTORY} only. */
282
+ export type StoreFormRefusalDetail = {
283
+ readonly path: string;
284
+ readonly form?: ScopeHomeForm;
285
+ };
286
+ /** A refusal the store's physical form earns: a plain `Error` (never the control-plane-damage class —
287
+ * the engine's own account is intact, the layout it was handed is not the layout it needs) carrying a
288
+ * `code` and a `detail`. */
289
+ export type StoreFormRefusal = Error & {
290
+ code: StoreFormRefusalCode;
291
+ detail: StoreFormRefusalDetail;
292
+ };
293
+ /** Seat 1 — an act the store's physical form refuses: `act` names what was refused (no path), `path` is the
294
+ * target whose directory resolves outside the store ({@link landsOutsideStore}). */
295
+ export declare function storeActRefusal(act: string, path: string): StoreFormRefusal;
296
+ /** Seat 2 — the store root now resolves to another directory than the one the `holder` first opened; `path` is
297
+ * the root's spelling. */
298
+ export declare function storeRootMovedRefusal(holder: "backend" | "engine", path: string): StoreFormRefusal;
299
+ /** Seat 3 — a scope home that is not a real directory. */
300
+ export declare function scopeHomeRefusal(form: ScopeHomeForm, path: string): StoreFormRefusal;
264
301
  /** Where an act at `p` lands: the physical path of its normalized spelling (the spelling a child path
265
302
  * joined under it carries), a MISSING tail rejoined under the deepest existing directory (what the act
266
303
  * creates). Any other resolution fault (EACCES, ELOOP, ENOTDIR) is THROWN as itself: an act that
@@ -268,8 +305,8 @@ export declare const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
268
305
  * (a link whose own mode refuses `realpath` is still followed by `unlink` and `rename`). */
269
306
  export declare function landingPathOf(p: string): string;
270
307
  /**
271
- * Where a write, rename or delete at `target` would physically LAND, judged against the store's
272
- * `physicalRoot`: `undefined` = inside, otherwise the reason. The target's directory, resolved through
308
+ * Whether a write, rename or delete at `target` would physically LAND outside the store, judged against the
309
+ * store's `physicalRoot` (`true` = outside; the refusal is {@link storeActRefusal}). The target's directory, resolved through
273
310
  * every link on its existing prefix (a missing tail is what the act creates, under the deepest existing
274
311
  * directory), must be the root or inside it. `physicalRoot` is the root's physical form as the store
275
312
  * was opened and is not re-resolved here: a store mounted through a link is judged by what the link
@@ -280,7 +317,7 @@ export declare function landingPathOf(p: string): string;
280
317
  * keeps the spelling while the act follows the link out. A directory that cannot be resolved throws its
281
318
  * fault ({@link landingPathOf}).
282
319
  */
283
- export declare function storeTargetEscape(physicalRoot: string, target: string): string | undefined;
320
+ export declare function landsOutsideStore(physicalRoot: string, target: string): boolean;
284
321
  /** mkdir -p convenience used by the engine/backend (0o700 like the file stores). */
285
322
  export declare function ensureDirExists(dir: string): void;
286
323
  /** The one remedy every surfaced permission refusal carries — `undefined` unless `err` is a write
@@ -472,11 +472,18 @@ function assertHomePhysicalForm(home) {
472
472
  return "absent";
473
473
  if (st.isDirectory())
474
474
  return "dir";
475
- throw new ControlPlaneCorruptError(`scope home ${home} is ${st.isSymbolicLink() ? "a symbolic link" : "not a directory"} — a scope home must be a real directory (fail-closed)`);
475
+ throw scopeHomeRefusal(st.isSymbolicLink() ? "symlink" : st.isFile() ? "file" : "other", home);
476
476
  }
477
477
  function ensureScopeHome(home) {
478
- if (assertHomePhysicalForm(home) === "absent")
479
- mkdirSync(home, { recursive: true });
478
+ if (assertHomePhysicalForm(home) === "absent") {
479
+ try {
480
+ mkdirSync(home, { recursive: true });
481
+ }
482
+ catch (err) {
483
+ assertHomePhysicalForm(home);
484
+ throw err;
485
+ }
486
+ }
480
487
  assertHomePhysicalForm(home);
481
488
  }
482
489
  export function registerScope(memoryDir, controlDir, scope, opts) {
@@ -724,7 +731,30 @@ export function isContainedIn(root, child) {
724
731
  const c = canonicalize(child);
725
732
  return c === r || c.startsWith(`${r}${sep}`);
726
733
  }
734
+ export const STORE_FORM_REFUSAL_CODES = Object.freeze(["memory.target_outside_store", "memory.scope_home_not_directory"]);
727
735
  export const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
736
+ export const SCOPE_HOME_NOT_DIRECTORY = "memory.scope_home_not_directory";
737
+ export const SCOPE_HOME_FORMS = Object.freeze(["symlink", "file", "other"]);
738
+ const SCOPE_HOME_FORM_PHRASE = {
739
+ symlink: "a symbolic link",
740
+ file: "a regular file",
741
+ other: "a special file",
742
+ };
743
+ function storeFormRefusal(code, sentence, detail) {
744
+ const e = new Error(`${code}: ${sentence}`);
745
+ e.code = code;
746
+ e.detail = detail;
747
+ return e;
748
+ }
749
+ export function storeActRefusal(act, path) {
750
+ return storeFormRefusal(TARGET_OUTSIDE_STORE, `${act} was refused — its directory resolves outside the store root; not performed`, { path });
751
+ }
752
+ export function storeRootMovedRefusal(holder, path) {
753
+ return storeFormRefusal(TARGET_OUTSIDE_STORE, `the ${holder === "backend" ? "store root" : "memory dir"} now resolves to a different directory than the one this ${holder} ${holder === "backend" ? "opened" : "first mounted"} — refused; open a new ${holder} on the directory you mean`, { path });
754
+ }
755
+ export function scopeHomeRefusal(form, path) {
756
+ return storeFormRefusal(SCOPE_HOME_NOT_DIRECTORY, `a scope home must be a real directory, but this one is ${SCOPE_HOME_FORM_PHRASE[form]} — refused (fail-closed); put the real directory back`, { path, form });
757
+ }
728
758
  export function landingPathOf(p) {
729
759
  const normalized = resolve(p);
730
760
  try {
@@ -736,9 +766,9 @@ export function landingPathOf(p) {
736
766
  return canonicalize(normalized);
737
767
  }
738
768
  }
739
- export function storeTargetEscape(physicalRoot, target) {
769
+ export function landsOutsideStore(physicalRoot, target) {
740
770
  const dir = landingPathOf(dirname(target));
741
- return dir === physicalRoot || dir.startsWith(`${physicalRoot}${sep}`) ? undefined : `its directory resolves to ${dir}, outside the store root ${physicalRoot}`;
771
+ return dir !== physicalRoot && !dir.startsWith(`${physicalRoot}${sep}`);
742
772
  }
743
773
  export function ensureDirExists(dir) {
744
774
  if (!existsSync(dir))
@@ -149,6 +149,10 @@ export interface PermissionRulePolicyOptions {
149
149
  * own; #644). Facts of the DEPLOYMENT and the task, never of the rule — a pattern is stored as spelled.
150
150
  * A rule whose base is absent (or not an absolute path) here is refused at compile time
151
151
  * (`unsupported.path_base`, naming the base — {@link ruleBasesNeeded} is the same classification).
152
+ * A call's own target resolves against these bases as given; when they hold neither `root` nor `cwd`,
153
+ * against the request's (`ToolCallRequest.root` / `.cwd`, stamped by the Runner) — never a mix of the two
154
+ * sources. A target neither supplies the base for is unreadable: every path rule able to reach the call
155
+ * answers it with the fail-closed ask (`ruleUnreadable`).
152
156
  */
153
157
  pathBases?: PathRuleBases;
154
158
  }
@@ -366,10 +366,13 @@ export function createPermissionRulePolicy(rules, opts) {
366
366
  ...(callFace !== undefined ? faceRules[behavior].filter((r) => r.faces.includes(callFace)) : []),
367
367
  ];
368
368
  const commandParam = catalogRuleFaceOf("Bash")?.primaryParams[0] ?? "command";
369
+ const policyNamesTree = pathBases.root !== undefined || pathBases.cwd !== undefined;
370
+ const callBasesOf = (req) => policyNamesTree ? pathBases : { ...pathBases, ...(typeof req.root === "string" ? { root: req.root } : {}), ...(typeof req.cwd === "string" ? { cwd: req.cwd } : {}) };
369
371
  const contentHit = (lane, req) => {
370
372
  if (lane.length === 0)
371
373
  return undefined;
372
- const target = effectivePathTargetOf(req, protectivePathTargetOf(req), pathBases);
374
+ const bases = callBasesOf(req);
375
+ const target = effectivePathTargetOf(req, protectivePathTargetOf(req), bases);
373
376
  const command = isPlainObjectArgs(req.args) ? req.args[commandParam] : undefined;
374
377
  let unreadable;
375
378
  for (const r of lane) {
@@ -386,7 +389,7 @@ export function createPermissionRulePolicy(rules, opts) {
386
389
  unreadable ??= { rule: r, unreadable: target.reason };
387
390
  continue;
388
391
  }
389
- const outcome = pathRuleReachOf(r.parsed, target.identity, pathBases);
392
+ const outcome = pathRuleReachOf(r.parsed, target.identity, bases);
390
393
  if (outcome.reach === "reached")
391
394
  return { rule: r };
392
395
  if (outcome.reach === "unreadable")
@@ -6,6 +6,7 @@
6
6
  * may still refuse it.
7
7
  */
8
8
  import type { Checkpoint } from "./checkpoint-store.js";
9
+ import type { HostDecider } from "./gate-outcome.js";
9
10
  import type { OrgGateVerdict } from "./hooks.js";
10
11
  import type { AskRequest, PermissionResult, ResolvedAsk } from "./tool-policy.js";
11
12
  /** The synchronous seat's refusal of a configuration's allow: one settlement word for both forms
@@ -16,10 +17,12 @@ export declare function unjudgedAllowRefused(req: AskRequest, by: "blanket" | "p
16
17
  /** A durable row demands a person when its gate carries the real-approval bit (the gate's own, which every resume
17
18
  * check reads; the pending action's `requiresRealApproval` is its display twin). */
18
19
  export declare function rowDemandsAPerson(cp: Checkpoint): boolean;
19
- /** A question a durable decide is held against, in each form a station holds one: the parked row, a policy's
20
- * re-check of an approver's edit, and the organization's CURRENT verdict about the call. */
21
- export type HeldQuestion = Checkpoint | PermissionResult | OrgGateVerdict;
22
- /** Whether a durable decide made by `decidedBy` cannot answer the held question: a question demanding a person is not
20
+ /** A question a decide is held against, in each form a station holds one: the parked row, a policy's re-check of an
21
+ * approver's edit, the organization's CURRENT verdict about the call, and the request an approver was handed (the card a
22
+ * live door answers). */
23
+ export type HeldQuestion = Checkpoint | PermissionResult | OrgGateVerdict | AskRequest;
24
+ /** Whether a decide made by `decidedBy` cannot answer the held question: a question demanding a person is not
23
25
  * the policy's to answer. One predicate for every station that holds a decide against such a question — the resume
24
- * entry (the row, an edit's re-check, the org's current verdict) and the execution belt (the last two, re-read). */
25
- export declare function policyCannotAnswer(question: HeldQuestion, decidedBy: "person" | "sla_timeout" | "policy"): boolean;
26
+ * entry (the row, an edit's re-check, the org's current verdict), the execution belt (the last two, re-read) and a live
27
+ * door answering the request an approver was handed. */
28
+ export declare function policyCannotAnswer(question: HeldQuestion, decidedBy: HostDecider): boolean;
@@ -16,11 +16,11 @@ export function rowDemandsAPerson(cp) {
16
16
  return cp.gate.kind === "irreversible_ask" && cp.gate.realApproval !== undefined;
17
17
  }
18
18
  function demandsAPerson(question) {
19
- if ("gate" in question)
20
- return rowDemandsAPerson(question);
21
- if ("action" in question)
22
- return question.action === "ask" && question.requiresRealApproval === true;
23
- return question.status === "available" && question.verdict?.behavior === "ask";
19
+ const gate = Reflect.get(question, "gate");
20
+ const verdict = Reflect.get(question, "verdict");
21
+ const row = typeof gate === "object" && gate !== null && rowDemandsAPerson(question);
22
+ const org = Reflect.get(question, "status") === "available" && typeof verdict === "object" && verdict !== null && Reflect.get(verdict, "behavior") === "ask";
23
+ return row || Reflect.get(question, "requiresRealApproval") === true || org;
24
24
  }
25
25
  export function policyCannotAnswer(question, decidedBy) {
26
26
  return decidedBy === "policy" && demandsAPerson(question);
@@ -0,0 +1,13 @@
1
+ import type { RunnerDeps } from "../types.js";
2
+ /**
3
+ * Whether this deployment can place a child agent in a tree OTHER than the caller's — the one fact behind
4
+ * every request of that shape: an explicit `cwd`, a workflow agent's `isolation: "worktree"`, and the
5
+ * delegation tool's per-call or per-definition `isolation: "worktree"`.
6
+ *
7
+ * Only an `executionEnvFactory` can: it builds one execution env per task, so a child can be rooted
8
+ * wherever the request says. A static `executionEnv` is ONE shared env with ONE cwd — every child that
9
+ * runs on it runs in the caller's root, whatever tree it was asked to work in. A request the deployment
10
+ * cannot honor is refused before the child starts (and, for a worktree, before one is minted); each site
11
+ * keeps its own code and sentence for the refusal, but none decides the condition itself.
12
+ */
13
+ export declare function deploymentCanRerootChild(deps: Pick<RunnerDeps, "executionEnvFactory">): boolean;
@@ -0,0 +1,3 @@
1
+ export function deploymentCanRerootChild(deps) {
2
+ return deps.executionEnvFactory !== undefined;
3
+ }
@@ -31,9 +31,9 @@ import type { MaterializedA2a } from "../a2a.js";
31
31
  import type { CompactionForkContext, CompactionPhaseDurations, MaybeCompactOptions, RapidRefillState } from "../auto-compaction.js";
32
32
  import type { AutoModeArmingRecipe } from "../auto-mode-arming.js";
33
33
  import type { AutoModeDecider, AutoModeDenialTracker, DenialLimitFallbackFace } from "../auto-mode.js";
34
- import type { SettlementKind } from "../gate-outcome.js";
34
+ import type { HostDecider, SettlementKind } from "../gate-outcome.js";
35
35
  import type { CacheBreakDetector, ToolFingerprintInput } from "../cache-break-detector.js";
36
- import type { ActorAssertion, Checkpoint, CheckpointState, CheckpointStore, CheckpointToken, PendingSteerEntry, PlatformLimitReason, ReopenReason, ResourceLedger, ResourceLimitReason, ResumeOutcome, SerializedCheckpointState } from "../checkpoint-store.js";
36
+ import type { Checkpoint, CheckpointState, CheckpointStore, CheckpointToken, PendingSteerEntry, PlatformLimitReason, ReopenReason, ResourceLedger, ResourceLimitReason, ResumeOutcome, SerializedCheckpointState } from "../checkpoint-store.js";
37
37
  import type { ExecutionOutcomeRecordWord } from "../checkpoint-execution-record.js";
38
38
  import type { ClearedProjectionLedger, MicroCompactMachine, OccurrenceIndex } from "../context-edit.js";
39
39
  import type { HookInvocationIdentity, Hooks, OrgGateVerdict, PermissionDeniedPayload, PostToolBatchCall, PostToolBatchResult, ToolGateInput } from "../hooks.js";
@@ -391,6 +391,8 @@ export interface Prepared {
391
391
  /** The per-task logical cwd ref when a real shell is mounted (else undefined). The Runner
392
392
  * reads `cwdRef.current` after each tool to detect a `cd` move and emit `workspace_changed`. */
393
393
  cwdRef?: CwdRef;
394
+ /** The root the hand tools resolve root-relative spellings against (present with hands; read-only hands included). */
395
+ handsRoot?: string;
394
396
  /** The run's TOOL-PROGRESS lane seat, late-bound for the same reason `harnessRef` is (the tool band is
395
397
  * built during prepare; the lane needs `pushContent` and `ident`, which exist only after it returns).
396
398
  * The driver binds `current` right after `pushContent` exists. Always present on a completed prepare —
@@ -442,7 +444,7 @@ export interface Prepared {
442
444
  toolCallId: string;
443
445
  toolName: string;
444
446
  args: unknown;
445
- decidedBy: "person" | "sla_timeout" | "policy";
447
+ decidedBy: HostDecider;
446
448
  }) => Promise<{
447
449
  readonly refused: string;
448
450
  } | {
@@ -2494,21 +2496,6 @@ export interface RunState {
2494
2496
  attachmentsInjected: number;
2495
2497
  };
2496
2498
  }
2497
- /** One accepted live steer's REPLAY identity, the twin of the parked
2498
- * queue's `samePendingSteerPayload`. `payload` is the exact string handed to the harness, so the text,
2499
- * the speaker envelope and the trust framing all fold into it; `trusted` is kept SEPARATELY from those
2500
- * framed bytes so a text that spells its own `<system-reminder>` wrapper can never read as the trusted
2501
- * arm's byte-equal twin. `actor` carries the attribution the ledger frame reports. */
2502
- export interface AcceptedSteerInput {
2503
- payload: string;
2504
- trusted: boolean;
2505
- /** The NORMALIZED priority the call delivered under (absent input ⇒ "next"),
2506
- * the replay identity's sixth axis: same id + same text at a DIFFERENT tier is not a replay —
2507
- * a `next` input re-sent as `now` answering "idempotent success" without the interrupt would be
2508
- * a disposition lie, so it refuses `duplicate_input_id` like any other identity mismatch. */
2509
- priority: SystemInjectionPriority;
2510
- actor?: ActorAssertion;
2511
- }
2512
2499
  /**
2513
2500
  * Config re-supplied to {@link Runner.resume}. A suspended task's tools / model / policy /
2514
2501
  * hooks cannot be reconstructed from a checkpoint token (the session stores neither tool implementations
@@ -2763,6 +2750,10 @@ export interface RunnerSelfSeat {
2763
2750
  toolPolicy?: RunnerDeps["toolPolicy"];
2764
2751
  hooks?: RunnerDeps["hooks"];
2765
2752
  };
2753
+ /** Whether a child task THIS runner runs can be placed in a tree other than the caller's (an explicit cwd, a worktree):
2754
+ * true only when its deps carry an `executionEnvFactory` (live read — `swapDeps` replaces the deps). The delegation tool asks
2755
+ * the runner that will run the child, not the one that ran the parent leg. Absent on a hand-built seat ⇒ read as "cannot say" ⇒ no; a hand-built seat answers for the runner its `runTask` forwards to. */
2756
+ canRerootChild?(): boolean;
2766
2757
  /** Fire `RunnerDeps.onTaskOutcome` through the swallow-guarded chokepoint (thin compositions emit at their terminal state). */
2767
2758
  emitTaskOutcome(outcome: TaskOutcome): void;
2768
2759
  /** Run a task to completion. `internals` is the TRUSTED run-scoped channel (never a TaskSpec field). */
@@ -28,6 +28,8 @@ export interface PrepareAskLaneInput {
28
28
  /** borrowed-readonly — the hand's live tracked cwd, or undefined (hands-less). Read per call at RUN time (`current`),
29
29
  * never captured: the fs hand tools resolve their relative paths against this very ref. Not written here. */
30
30
  handsCwdRef: CwdRef | undefined;
31
+ /** borrowed-readonly — the root the hand tools resolve root-relative spellings against, or undefined (hands-less). */
32
+ handsRoot: string | undefined;
31
33
  /** borrowed-readonly — the run's advertised-writable-directory seat, or undefined (hands-less / nothing advertised). `roots` is read at RUN time, never captured: it is the fence's own admitted list and the write gate's exemption domain must be that same list. Not written here. */
32
34
  advertisedWritableDirs: Pick<AdvertisedWritableDirs, "roots"> | undefined;
33
35
  /** borrowed-readonly — the shared mount roster; read at RUN time by the approval-preview projection (alias-aware
@@ -59,14 +59,14 @@ function resolveApprovalPreview(tools, toolName, args) {
59
59
  }
60
60
  }
61
61
  export function prepareAskLane(input) {
62
- const { gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, advertisedWritableDirs, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, grantBindingOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash, grantReadRoot } = input;
62
+ const { gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, handsRoot, advertisedWritableDirs, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, grantBindingOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash, grantReadRoot } = input;
63
63
  if (!gateMachineryActive)
64
64
  return { askLane: undefined };
65
65
  const composedCallSignal = (callSignal) => composeCallSignal(abortController.signal, callSignal);
66
66
  const adjudicate = effectivePolicy
67
67
  ? (req, callSignal) => {
68
68
  const signal = composedCallSignal(callSignal);
69
- return raceAbort(Promise.resolve(effectivePolicy.check({ ...req, budget: budgetSnapshot, ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}), ...advertisedWritableRootsSeat(advertisedWritableDirs) }, signal)), signal, () => ({
69
+ return raceAbort(Promise.resolve(effectivePolicy.check({ ...req, budget: budgetSnapshot, ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}), ...(handsRoot !== undefined ? { root: handsRoot } : {}), ...advertisedWritableRootsSeat(advertisedWritableDirs) }, signal)), signal, () => ({
70
70
  action: "deny",
71
71
  message: "policy check aborted (task timed out or cancelled)",
72
72
  }));
@@ -14,6 +14,7 @@ import { applyToolModelGate, assertRestoreGatedToolsValue, modelIdTail } from ".
14
14
  import { resolveUsageWindows } from "../usage-window-store.js";
15
15
  import { deriveAskEffective, resolveAskSeat, resolveQuestionSeam } from "../wiring-manifest.js";
16
16
  import { assertPeerPresenceValue } from "../../agents/peer-directory.js";
17
+ import { snapshotDefinitionLists } from "../../agents/tool-filter.js";
17
18
  import { TOTAL_TOKENS_REMINDER_MODES } from "./turn-attachments.js";
18
19
  const TASK_LIMIT_KEY_DICT = {
19
20
  maxTokens: true,
@@ -359,9 +360,10 @@ export function prepareConfigDoors(input) {
359
360
  e.code = "config.agents.no_delegation_tool";
360
361
  throw e;
361
362
  }
362
- const perTask = spec.agents;
363
+ const perTask = spec.agents.map(snapshotDefinitionLists);
363
364
  spec = {
364
365
  ...spec,
366
+ agents: perTask,
365
367
  tools: pool.map((t) => {
366
368
  if (typeof t.withAgents !== "function")
367
369
  return t;
@@ -1,12 +1,13 @@
1
1
  import { StubExecutionEnv } from "../stub-env.js";
2
2
  import { hasDestroy } from "../remote-env.js";
3
3
  import { announceMalformedCapabilities } from "./env-capability-census.js";
4
+ import { deploymentCanRerootChild } from "./child-tree.js";
4
5
  import { settleTeardownLeg } from "./teardown-bounded.js";
5
6
  export async function prepareExecutionEnv(input) {
6
7
  const { deps, spec, internals, resume, placementRootResolved, sessionId, forgetOnThrow, rollback } = input;
7
8
  let ownedEnv;
8
9
  let ownedEnvObservedAt;
9
- if (internals?.requestedCwd !== undefined && deps.executionEnvFactory === undefined) {
10
+ if (internals?.requestedCwd !== undefined && !deploymentCanRerootChild(deps)) {
10
11
  await settleTeardownLeg(() => forgetOnThrow(), "forgetOnThrow (cwd-unsupported leg)", (err) => deps.onError?.(err, { phase: "config", sessionId }));
11
12
  const e = new Error(`Agent cwd "${internals.requestedCwd}" cannot take effect: this deployment has no executionEnvFactory (a static execution environment cannot be re-rooted per agent). Drop the cwd parameter or deploy a factory.`);
12
13
  e.code = "config.cwd_unsupported";
@@ -37,6 +37,7 @@ import { type WiringManifest } from "../wiring-manifest.js";
37
37
  import type { WriteProtectionCheck } from "../write-protect.js";
38
38
  import type { ShellReadCheck } from "../../tools/fs/shell-read-check.js";
39
39
  import type { CwdRef } from "../../tools/fs/fs-shared.js";
40
+ import type { ContainmentFace } from "../../tools/fs/containment-face.js";
40
41
  import type { AskLane, GateSeatInputsFor, IrreversibilityTier, ParkAsk, Prepared, ReversibilityProbes, StopForDenialLimit } from "./contracts.js";
41
42
  import type { PermissionRuleLanes } from "./permission-rule-lanes.js";
42
43
  import { type ToolRosterDeltaSeat } from "../tool-roster.js";
@@ -167,6 +168,8 @@ export interface PrepareGateStationsInput {
167
168
  /** borrowed-readonly — the leg's shell READ STATION (`ToolGateInput.shellReadCheck`), mounted by the hands phase, or
168
169
  * undefined with no real shell: no station, nothing judged. */
169
170
  shellReadCheck: ShellReadCheck | undefined;
171
+ /** borrowed-readonly — the leg's resolved read face (`undefined` with no hands), read per call into the class derivation. */
172
+ readFace: () => ContainmentFace | undefined;
170
173
  /** borrowed-readonly — the per-tool irreversibility tier handed to the gate per call. */
171
174
  irreversibilityTier: IrreversibilityTier;
172
175
  /** borrowed-readonly — the per-tool reversibility probes handed to the gate per call. */
@@ -46,7 +46,7 @@ export function maybeHumanRejectionHalt(input) {
46
46
  };
47
47
  }
48
48
  export function prepareGateStations(input) {
49
- const { askLane, parkAsk, tools, toolRosterDeltas, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, writeProtectionCheck, shellReadCheck, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider, autoModeDenialTracking, askRoute, permissionMode, toolMarkSources, a2aIsolatePeers, ancestorArbitrations, runId, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash, frameSettlement, seatInputsFor } = input;
49
+ const { askLane, parkAsk, tools, toolRosterDeltas, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, writeProtectionCheck, shellReadCheck, readFace, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider, autoModeDenialTracking, askRoute, permissionMode, toolMarkSources, a2aIsolatePeers, ancestorArbitrations, runId, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash, frameSettlement, seatInputsFor } = input;
50
50
  const gatePreToolUse = ownGatePreToolUse !== undefined ? (name, args, ctx) => ownGatePreToolUse(name, args, { ...ctx, permissionMode: permissionMode.observed }) : undefined;
51
51
  const releasesAnnounced = new Map();
52
52
  const onModeReleased = (info) => {
@@ -130,6 +130,7 @@ export function prepareGateStations(input) {
130
130
  peerMessage: heldBy("gate.peerMessage", PEER_MESSAGE_NAMES, call),
131
131
  ...(writeProtectionCheck !== undefined ? { writeProtectionCheck } : {}),
132
132
  ...(shellReadCheck !== undefined ? { shellReadCheck } : {}),
133
+ readFace,
133
134
  irreversibility: tierSeat?.value,
134
135
  reversibilityProbe: tierSeat?.read(reversibilityProbes),
135
136
  abortSignal: abortController.signal,
@@ -1,6 +1,6 @@
1
1
  /**
2
- * PrepareTask's LISTINGS phase: the per-task agent definitions' tool-name audit
3
- * (deny entries fail loud, allow entries advise), the agent-listing announce face read off the
2
+ * PrepareTask's LISTINGS phase: the sub-agent tool lists' audit (`sub-agent-deny-lists.ts`),
3
+ * the agent-listing announce face read off the
4
4
  * delegation tool's roster (with its deferred / excluded / removal-to-zero arms), the two listing
5
5
  * opt-out advisories, the announced-listings mirror seeded from the once-per-session ledger, the
6
6
  * deferred roster's ToolSearch ride, and the date_change seam. The body is the driver's stretch moved
@@ -10,8 +10,8 @@
10
10
  * the disclosure mount before it and the context-lane seats after it (a stretch
11
11
  * with no await is extracted as a sync function — an `async` wrapper would open a microtask window).
12
12
  *
13
- * Throws pass through unchanged: `config.agent.unknown_tool` (a deny entry naming a tool this
14
- * deployment does not mount — a typo there would silently guard nothing).
13
+ * Throws pass through unchanged: `config.agent.unknown_tool` (a denied-tools entry that names no
14
+ * tool — one judgment for every list a deployment writes, in `sub-agent-deny-lists.ts`).
15
15
  */
16
16
  import type { AgentTool } from "../../internal/harness.js";
17
17
  import type { TaskSpec } from "../types.js";
@@ -1,30 +1,10 @@
1
1
  import { formatLocalDate } from "../../prompts/default.js";
2
+ import { auditSubAgentToolLists } from "./sub-agent-deny-lists.js";
2
3
  import { SKILL_TOOL_NAME } from "./synthetic-tools.js";
3
4
  import { renderAgentListingDelta } from "./turn-attachments.js";
4
5
  export function prepareListings(input) {
5
6
  const { spec, tools, onceLedger, sessionId, toolFaceSnapshot, activeTools, resume, skillsListing, listingBudget, listingRideRef, renderWithDate, envFacts, tzValid, userTz, dateAnchor } = input;
6
- if (spec.agents !== undefined && spec.agents.length > 0) {
7
- const known = new Set(tools.flatMap((t) => [t.name, ...(t.aliases ?? [])]));
8
- for (const def of spec.agents) {
9
- const unknownAllow = (def.allowTools ?? []).filter((n) => n !== "*" && !known.has(n));
10
- if (unknownAllow.length > 0) {
11
- try {
12
- onceLedger.onError(new Error(`TaskSpec.agents: agent "${def.name}" allows tool(s) ${unknownAllow.join(", ")} not present in this task's assembled roster — likely a typo (the entry would be item-filtered at spawn; the agent stays usable). Advisory only: the delegation pool can differ from this roster, so a tool mounted only on the delegation tool or produced by per-spawn extraTools makes this spurious.`), { phase: "config", sessionId });
13
- }
14
- catch {
15
- }
16
- }
17
- for (const n of def.denyTools ?? []) {
18
- if (n === "*")
19
- continue;
20
- if (!known.has(n)) {
21
- const e = new Error(`TaskSpec.agents: agent "${def.name}" declares tool "${n}" in its denied tools, but no such tool exists in this deployment — fix the agent's tools list or mount the tool.`);
22
- e.code = "config.agent.unknown_tool";
23
- throw e;
24
- }
25
- }
26
- }
27
- }
7
+ auditSubAgentToolLists({ spec, tools, onceLedger, sessionId });
28
8
  const rosterBearingSpec = (spec.tools ?? []).find((t) => t.agentListing !== undefined && t.agentListing.length > 0);
29
9
  const rosterFaceDeferred = rosterBearingSpec !== undefined &&
30
10
  !(toolFaceSnapshot.exclude?.includes(rosterBearingSpec.name) ?? false) &&
@@ -94,6 +94,8 @@ export interface PrepareParkAskInput {
94
94
  /** borrowed-readonly — the hand's live tracked cwd, or undefined; `current` is read at park time for the re-adjudication.
95
95
  * Not written here. */
96
96
  handsCwdRef: CwdRef | undefined;
97
+ /** borrowed-readonly — the root the live gate stamps beside the cwd ({@link import("./prepare-ask-lane.js").PrepareAskLaneInput.handsRoot}). */
98
+ handsRoot: string | undefined;
97
99
  /** borrowed-readonly — the SAME seat the live gate stamps from ({@link import("./prepare-ask-lane.js").PrepareAskLaneInput.advertisedWritableDirs}): a parked call's projection re-adjudication asks one question about one call, so it must ask it against the same admitted list. Read at park time, not written here. */
98
100
  advertisedWritableDirs: Pick<AdvertisedWritableDirs, "roots"> | undefined;
99
101
  /** borrowed-readonly — the run's offload store, or undefined (a volatile store holding results refuses the park). */
@@ -27,7 +27,7 @@ const wordlessDemand = (toolName) => {
27
27
  throw new Error(`a real-approval demand for "${toolName}" reached the park with no origin word — the origin stamp precedes every park`);
28
28
  };
29
29
  export function prepareParkAsk(input) {
30
- const { askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, irreversibilityTier, toolMarkSources, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, grantBindingOf, now, humanReviewRef, abortController, harness, pausedRef } = input;
30
+ const { askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, handsRoot, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, irreversibilityTier, toolMarkSources, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, grantBindingOf, now, humanReviewRef, abortController, harness, pausedRef } = input;
31
31
  if (askLane === undefined || saga === undefined)
32
32
  return { parkAsk: undefined };
33
33
  const { composedCallSignal, approvalPreviewOf } = askLane;
@@ -165,7 +165,7 @@ export function prepareParkAsk(input) {
165
165
  toolCallId: req.toolCallId,
166
166
  budget: budgetSnapshot,
167
167
  ...callFaceSeat(req.face),
168
- ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}),
168
+ ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}), ...(handsRoot !== undefined ? { root: handsRoot } : {}),
169
169
  ...advertisedWritableRootsSeat(advertisedWritableDirs),
170
170
  }, cutSignal)), cutSignal);
171
171
  if (reprojectedRaced.tag === "aborted")
@@ -140,6 +140,8 @@ export interface PreparePolicyChainInput {
140
140
  /** borrowed-readonly — LATE-BOUND: this task's own classifier instance (the pass-through test's decider term), read at chain
141
141
  * assembly (the arming runs after this phase, and again on a steer into `auto`). */
142
142
  autoModeDecider: () => AutoModeDecider | undefined;
143
+ /** borrowed-readonly — the leg's resolved read face (`undefined` with no hands), read per call into the inherited stations' class derivation. */
144
+ readFace: () => import("../../tools/fs/containment-face.js").ContainmentFace | undefined;
143
145
  /** borrowed-readonly — the leg's live permission-mode seat: the mode gate policy layer and the inherited stations' arbitration
144
146
  * read `current` per call. */
145
147
  permissionMode: PermissionModeController;
@@ -24,7 +24,7 @@ import { createLiveSessionRules, createSessionRulePolicy } from "./session-rule-
24
24
  import { thrownText } from "../thrown-value.js";
25
25
  import { heldBy, heldValues, seatOf, wireOf } from "../tool-name-readings.js";
26
26
  export async function preparePolicyChain(input) {
27
- const { spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, toolMarkSources, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, skillGrants, skillSpecs, allowedToolTables, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, permissionMode, scratchpadDir, toolRosterDeltas, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef } = input;
27
+ const { spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, toolMarkSources, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, skillGrants, skillSpecs, allowedToolTables, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, readFace, permissionMode, scratchpadDir, toolRosterDeltas, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef } = input;
28
28
  const ancestorArbitrations = new Map();
29
29
  const arbitrateInherited = (ask, toolCallId, judged) => {
30
30
  const verdict = arbitrateByMode(permissionMode.current, judged.classification, { origin: judged.origin, ...askMandateMember(ancestorFacts(judged.ask.action === "ask" ? judged.ask : undefined)) });
@@ -305,11 +305,13 @@ export async function preparePolicyChain(input) {
305
305
  const grantBindingOf = (call, args) => skillGrantBindingMembers(call, args, skillSpecs, SKILL_TOOL_NAME);
306
306
  const inheritedStationFacts = (call) => {
307
307
  const seat = seatMarksOf(call);
308
+ const face = readFace();
308
309
  return {
309
310
  ...INHERITED_STATION_FACTS,
310
311
  mandateMarks: { egress: seat.egress, shellGated: false, ...(seat.tier === "always" && !seat.shellGated ? { irreversibility: "always" } : {}) },
311
312
  ...(seat.sources.size > 0 ? { toolMarkSources: seat.sources } : {}),
312
313
  ...(deps.a2a?.isolatePeers === true ? { a2aIsolatePeers: true } : {}),
314
+ ...(face !== undefined ? { readFace: face } : {}),
313
315
  };
314
316
  };
315
317
  const inheritedAskEvidence = inheritedAskRuleEvidence(deps.permissionRuleStore);
@@ -616,7 +618,7 @@ export async function preparePolicyChain(input) {
616
618
  ...instrumentedChain,
617
619
  ...(rewriteCapableLayers ? narrowingLayers : []),
618
620
  ];
619
- const integrityGuardLayers = [createTranscriptIntegrityPolicy(), createUnverifiableDeletePolicy()];
621
+ const integrityGuardLayers = [createTranscriptIntegrityPolicy(), createUnverifiableDeletePolicy({ removalSeat: { env: executionEnv, additionalRoots: () => additionalRootsCanonical } })];
620
622
  const guardedPolicyLayers = rewriteCapableLayers
621
623
  ? [...integrityGuardLayers, ...policyLayers, modeGateLayer, ...integrityGuardLayers]
622
624
  : [...integrityGuardLayers, ...policyLayers, modeGateLayer];