@sema-agent/core 7.45.0 → 7.47.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 (139) hide show
  1. package/CHANGELOG.md +298 -1
  2. package/dist/agents/extra-tool-names.d.ts +65 -0
  3. package/dist/agents/extra-tool-names.js +124 -0
  4. package/dist/agents/subagent.d.ts +17 -5
  5. package/dist/agents/subagent.js +33 -25
  6. package/dist/agents/tool-filter.d.ts +17 -1
  7. package/dist/agents/tool-filter.js +7 -0
  8. package/dist/core/agent-config-dir.d.ts +20 -0
  9. package/dist/core/agent-config-dir.js +49 -0
  10. package/dist/core/allow-entry.d.ts +6 -4
  11. package/dist/core/allow-entry.js +6 -5
  12. package/dist/core/ask-origin.d.ts +35 -12
  13. package/dist/core/ask-origin.js +24 -7
  14. package/dist/core/checkpoint-store.d.ts +39 -2
  15. package/dist/core/effective-path-target.d.ts +136 -4
  16. package/dist/core/effective-path-target.js +110 -2
  17. package/dist/core/engine-notice.d.ts +12 -4
  18. package/dist/core/gate-fold.js +5 -2
  19. package/dist/core/gate-lanes.js +11 -4
  20. package/dist/core/gate-outcome.d.ts +4 -2
  21. package/dist/core/gate-outcome.js +2 -2
  22. package/dist/core/governance-codes.d.ts +3 -1
  23. package/dist/core/governance-codes.js +3 -0
  24. package/dist/core/hooks.d.ts +3 -0
  25. package/dist/core/path-rule-binding.d.ts +65 -0
  26. package/dist/core/path-rule-binding.js +99 -0
  27. package/dist/core/permission-rule-model.d.ts +59 -24
  28. package/dist/core/permission-rule-model.js +48 -44
  29. package/dist/core/permission-rule-provider.d.ts +7 -0
  30. package/dist/core/permission-rule-provider.js +10 -3
  31. package/dist/core/permission-rule-store.d.ts +8 -0
  32. package/dist/core/permission-rule-store.js +6 -2
  33. package/dist/core/permission-rule-sync.js +2 -2
  34. package/dist/core/permission-rules.d.ts +26 -45
  35. package/dist/core/permission-rules.js +77 -57
  36. package/dist/core/runner/child-tree.d.ts +97 -0
  37. package/dist/core/runner/child-tree.js +104 -0
  38. package/dist/core/runner/construction-doors.d.ts +5 -0
  39. package/dist/core/runner/construction-doors.js +6 -0
  40. package/dist/core/runner/contracts.d.ts +46 -0
  41. package/dist/core/runner/gate-exit.d.ts +4 -1
  42. package/dist/core/runner/gate-exit.js +5 -1
  43. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  44. package/dist/core/runner/permission-rule-lanes.js +18 -12
  45. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  46. package/dist/core/runner/persisted-path-rows.js +31 -12
  47. package/dist/core/runner/prepare-ask-lane.d.ts +4 -0
  48. package/dist/core/runner/prepare-ask-lane.js +4 -2
  49. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  50. package/dist/core/runner/prepare-execution-env.js +10 -3
  51. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  52. package/dist/core/runner/prepare-gate-stations.js +2 -1
  53. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  54. package/dist/core/runner/prepare-hands-readface.js +11 -11
  55. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  56. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  57. package/dist/core/runner/prepare-park-ask.d.ts +3 -0
  58. package/dist/core/runner/prepare-park-ask.js +5 -3
  59. package/dist/core/runner/prepare-policy-chain.d.ts +14 -2
  60. package/dist/core/runner/prepare-policy-chain.js +13 -5
  61. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  62. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  63. package/dist/core/runner/prepare-task.js +37 -18
  64. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  65. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  66. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  67. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  68. package/dist/core/runner/resume-apply.js +17 -3
  69. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  70. package/dist/core/runner/run-leg.js +5 -1
  71. package/dist/core/runner/runtask.js +2 -2
  72. package/dist/core/runner/sub-agent-deny-lists.d.ts +1 -1
  73. package/dist/core/runner/write-protection-station.d.ts +3 -2
  74. package/dist/core/runner/write-protection-station.js +1 -0
  75. package/dist/core/runner-deps.d.ts +14 -0
  76. package/dist/core/sensitive-path-policy.d.ts +8 -0
  77. package/dist/core/sensitive-path-policy.js +18 -2
  78. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  79. package/dist/core/shell-wrapper-projection.js +56 -0
  80. package/dist/core/shell-wrapper-table.d.ts +144 -34
  81. package/dist/core/shell-wrapper-table.js +40 -20
  82. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  83. package/dist/core/synthetic-read-verdict.js +3 -1
  84. package/dist/core/tool-catalog-entries.d.ts +21 -0
  85. package/dist/core/tool-catalog-entries.js +12 -2
  86. package/dist/core/tool-face.d.ts +10 -1
  87. package/dist/core/tool-name-readings.d.ts +8 -0
  88. package/dist/core/tool-name-readings.js +2 -0
  89. package/dist/core/tool-policy.d.ts +40 -17
  90. package/dist/core/tool-policy.js +11 -9
  91. package/dist/core/tool-registry.d.ts +9 -0
  92. package/dist/core/tool-registry.js +10 -0
  93. package/dist/core/tool-spec.d.ts +3 -2
  94. package/dist/core/write-protect.d.ts +14 -3
  95. package/dist/core/write-protect.js +41 -10
  96. package/dist/index.d.ts +6 -4
  97. package/dist/index.js +5 -3
  98. package/dist/orchestration/run-spec.js +8 -7
  99. package/dist/orchestration/workflow.js +1 -1
  100. package/dist/shell/lex.d.ts +22 -0
  101. package/dist/shell/lex.js +75 -13
  102. package/dist/tools/fs/bash-program-position.d.ts +14 -90
  103. package/dist/tools/fs/bash-program-position.js +41 -74
  104. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  105. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  106. package/dist/tools/fs/index.d.ts +3 -2
  107. package/dist/tools/fs/program-read-model.d.ts +711 -0
  108. package/dist/tools/fs/program-read-model.js +1259 -0
  109. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  110. package/dist/tools/fs/program-read-model.rows.js +624 -0
  111. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  112. package/dist/tools/fs/read-deny-home.js +62 -24
  113. package/dist/tools/fs/read-deny.d.ts +54 -14
  114. package/dist/tools/fs/read-deny.js +104 -50
  115. package/dist/tools/fs/removal-targets.d.ts +7 -6
  116. package/dist/tools/fs/removal-targets.js +34 -24
  117. package/dist/tools/fs/safety.d.ts +3 -0
  118. package/dist/tools/fs/safety.js +10 -3
  119. package/dist/tools/fs/shell-read-argv.d.ts +86 -0
  120. package/dist/tools/fs/shell-read-argv.js +388 -0
  121. package/dist/tools/fs/shell-read-bindings.d.ts +300 -0
  122. package/dist/tools/fs/shell-read-bindings.js +755 -0
  123. package/dist/tools/fs/shell-read-check.js +23 -15
  124. package/dist/tools/fs/shell-read-establish.js +82 -8
  125. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  126. package/dist/tools/fs/shell-read-judge.js +9 -1
  127. package/dist/tools/fs/shell-read-plan.d.ts +33 -8
  128. package/dist/tools/fs/shell-read-plan.js +309 -407
  129. package/dist/tools/fs/shell-read-word.d.ts +60 -26
  130. package/dist/tools/fs/shell-read-word.js +47 -37
  131. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -64
  132. package/dist/tools/fs/shell-read-write-targets.js +85 -137
  133. package/dist/tools/fs/shell-reentry-text.d.ts +136 -6
  134. package/dist/tools/fs/shell-reentry-text.js +203 -53
  135. package/dist/tools/monitor.d.ts +3 -3
  136. package/package.json +1 -1
  137. package/test/export-surface.snapshot.json +25 -5
  138. package/wire-changes/core@7.46.0.json +1522 -0
  139. package/wire-changes/core@7.47.0.json +412 -0
@@ -1,15 +1,14 @@
1
1
  import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
2
2
  import { splitNamespacedName } from "./tool-name-aliases.js";
3
- import { catalogRuleFaceOf, pathTargetOf } from "./tool-registry.js";
3
+ import { catalogRuleFaceOf, isShellFaceToolName, pathTargetOf } from "./tool-registry.js";
4
4
  import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule } from "./permission-rule-syntax.js";
5
- import { NAME_RULE_FACES, PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, nameRuleAdmits, nameRuleSubjectOf, parseRuleText, pathRuleHeadOf, pathRuleReachOf, programRunReachOf, ruleBasesNeeded, ruleToolGrammarOf } from "./permission-rule-model.js";
6
- import { pathFamilyOf, pathIdentityOf, spellingOfIdentity } from "../tools/fs/safety.js";
7
- import { effectivePathTargetOf } from "./effective-path-target.js";
8
- import { protectivePathTargetOf } from "./tool-registry.js";
5
+ import { COMMAND_RULE_TOOL, NAME_RULE_FACES, PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, laneToolNamesOf, nameRuleAdmits, nameRuleSubjectOf, parseRuleText, programRunReachOf, ruleBasesNeeded, ruleToolGrammarOf } from "./permission-rule-model.js";
6
+ import { pathFamilyOf } from "../tools/fs/safety.js";
7
+ import { callTargetReadingOf, callTargetViewsOf } from "./effective-path-target.js";
8
+ import { bindPathRules, pathRowsReach, policyAnchors, rowsWhereTheyLead } from "./path-rule-binding.js";
9
9
  import { ruleDenialSentence } from "./rule-denial-sentence.js";
10
10
  import { heldValues } from "./tool-name-readings.js";
11
11
  export { parsePermissionRule };
12
- export const BASH_GENERIC_PARAMS = new Set(catalogRuleFaceOf("Bash")?.params ?? []);
13
12
  function describeBehavior(v) {
14
13
  switch (typeof v) {
15
14
  case "undefined": return "undefined";
@@ -93,22 +92,6 @@ export const fileToolParamVocabulary = (canonicalTool) => {
93
92
  const face = catalogRuleFaceOf(canonicalTool);
94
93
  return face?.params === undefined || pathTargetOf(canonicalTool) === undefined ? undefined : new Set(face.params);
95
94
  };
96
- export function pathRuleDenyRowOf(rule, bases) {
97
- const family = pathFamilyOf(bases);
98
- const tree = bases.root ?? bases.cwd;
99
- const head = pathRuleHeadOf(rule.parsed, bases);
100
- return {
101
- rule: rule.ruleText,
102
- tool: rule.parsed.tool,
103
- pattern: parsePermissionRule(rule.ruleText).ruleContent ?? "",
104
- faces: rule.faces,
105
- reaches: (absolutePath) => {
106
- const id = pathIdentityOf(absolutePath, family, tree);
107
- return id.ok && pathRuleReachOf(rule.parsed, id.identity, bases).reach !== "not_reached";
108
- },
109
- ...(head !== undefined ? { head: spellingOfIdentity(head, family) } : {}),
110
- };
111
- }
112
95
  function countStars(s) {
113
96
  let n = 0;
114
97
  for (const c of s)
@@ -126,13 +109,14 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
126
109
  return { face, judged: face !== undefined };
127
110
  };
128
111
  const byTool = new Map();
112
+ const command = { deny: [], ask: [] };
129
113
  const namespacedCovering = { deny: [], ask: [] };
130
114
  const issues = [];
131
115
  const bad = (rule, code, message) => {
132
116
  issues.push({ rule, code, message });
133
117
  };
134
118
  if (rules.length > caps.maxRules) {
135
- return { byTool, namespacedCovering, issues: [{ rule: "", code: "invalid.cap_exceeded", message: `${rules.length} rules > maxRules ${caps.maxRules}` }] };
119
+ return { byTool, command, namespacedCovering, issues: [{ rule: "", code: "invalid.cap_exceeded", message: `${rules.length} rules > maxRules ${caps.maxRules}` }] };
136
120
  }
137
121
  for (const r of rules) {
138
122
  const text = r.rule;
@@ -190,17 +174,24 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
190
174
  }
191
175
  const content = parsed.ruleContent;
192
176
  const colon = content.indexOf(":");
193
- if (toolName === "Bash") {
177
+ if (isShellFaceToolName(toolName)) {
194
178
  const paramPart = colon > 0 ? content.slice(0, colon).trim() : "";
195
- const looksLikeParamRule = colon > 0 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(paramPart) && BASH_GENERIC_PARAMS.has(paramPart);
196
- if (!looksLikeParamRule || content.endsWith(":*")) {
179
+ const declaresParam = (tool) => colon > 0 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(paramPart) && (catalogRuleFaceOf(tool)?.params ?? []).includes(paramPart);
180
+ const looksLikeParamRule = declaresParam(toolName);
181
+ if (toolName !== COMMAND_RULE_TOOL && !looksLikeParamRule) {
182
+ const commandWordParam = declaresParam(COMMAND_RULE_TOOL) && !content.endsWith(":*");
183
+ bad(text, "invalid.command_rule", commandWordParam
184
+ ? `"${text}" is a parameter rule on "${paramPart}", which ${toolName} does not declare (its rule parameters: ${(catalogRuleFaceOf(toolName)?.params ?? []).join(", ")}); a parameter rule reaches only the tool that declares the parameter, so ${COMMAND_RULE_TOOL}(${content}) is a rule on ${COMMAND_RULE_TOOL}'s own argument and does not reach ${toolName}. A shell command rule is written ${COMMAND_RULE_TOOL}(<command>) — it reaches ${toolName} and every other tool that runs a shell command by the command it runs.`
185
+ : `a shell command rule is written ${COMMAND_RULE_TOOL}(…) — it reaches ${toolName} and every other tool that runs a shell command by the command it runs; "${text}" names no rule upstream reads. Write ${COMMAND_RULE_TOOL}(${content}).`);
186
+ continue;
187
+ }
188
+ if (toolName === COMMAND_RULE_TOOL && (!looksLikeParamRule || content.endsWith(":*"))) {
197
189
  const parsedCommand = parseRuleText(text, verb);
198
190
  if ("reject" in parsedCommand) {
199
191
  bad(text, "invalid.command_rule", `${parsedCommand.reject.code}: ${parsedCommand.reject.message}`);
200
192
  continue;
201
193
  }
202
- entry.content[verb].push({ parsed: parsedCommand.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}) });
203
- byTool.set(toolName, entry);
194
+ command[verb].push({ parsed: parsedCommand.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}) });
204
195
  continue;
205
196
  }
206
197
  }
@@ -233,9 +224,9 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
233
224
  continue;
234
225
  }
235
226
  for (const base of ruleBasesNeeded(parsedPath.rule)) {
236
- const value = base === "cwd" ? (pathBases.cwd ?? pathBases.root) : pathBases[base];
237
- if (!isUsablePathBase(value, pathFamilyOf(pathBases))) {
238
- bad(text, "unsupported.path_base", `"${text}" is relative to ${PATH_RULE_BASE_LABEL[base]}, and this policy was given no absolute \`pathBases.${base}\`${base === "cwd" ? " (or `root`)" : ""} to resolve it against — the rule could not be judged on any call`);
227
+ const declared = base === "cwd" ? (pathBases.cwd ?? pathBases.root) : pathBases[base];
228
+ if (declared !== undefined && !isUsablePathBase(declared, pathFamilyOf(pathBases))) {
229
+ bad(text, "unsupported.path_base", `"${text}" is relative to ${PATH_RULE_BASE_LABEL[base]}, and the \`pathBases.${base === "cwd" && pathBases.cwd === undefined ? "root" : base}\` this policy declares is not an absolute path of its tree's family — the rule could not be judged on any call`);
239
230
  }
240
231
  }
241
232
  if (issues.length > 0 && issues[issues.length - 1].rule === text && issues[issues.length - 1].code === "unsupported.path_base")
@@ -280,7 +271,7 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
280
271
  entry.param[verb].push({ param, pattern, behavior: verb, ruleText: text, source: r.source });
281
272
  byTool.set(toolName, entry);
282
273
  }
283
- return { byTool, namespacedCovering, issues };
274
+ return { byTool, command, namespacedCovering, issues };
284
275
  }
285
276
  export function validatePermissionRules(rules, opts) {
286
277
  const caps = { ...DEFAULT_CAPS, ...opts?.caps };
@@ -315,10 +306,11 @@ function ruleDenial(req, ruleText, source) {
315
306
  function ruleAskMessage(ruleText, source) {
316
307
  return `flagged by permission rule "${ruleText}"${source ? ` (${source})` : ""}`;
317
308
  }
309
+ const NO_LEADS = new Map();
318
310
  export function createPermissionRulePolicy(rules, opts) {
319
311
  const caps = { ...DEFAULT_CAPS, ...opts?.caps };
320
312
  const pathBases = opts?.pathBases ?? {};
321
- const { byTool, namespacedCovering, issues } = compile(rules, caps, opts?.primaryFieldGeneric ?? "reject", opts?.ruleFaces, pathBases);
313
+ const { byTool, command, namespacedCovering, issues } = compile(rules, caps, opts?.primaryFieldGeneric ?? "reject", opts?.ruleFaces, pathBases);
322
314
  const setLevelCap = issues.some((i) => i.code === "invalid.cap_exceeded" && i.rule === "");
323
315
  if (issues.length > 0 && ((opts?.onInvalidRule ?? "throw") === "throw" || setLevelCap)) {
324
316
  const e = new Error(`createPermissionRulePolicy: ${issues.length} invalid/unsupported rule(s):\n` +
@@ -350,6 +342,9 @@ export function createPermissionRulePolicy(rules, opts) {
350
342
  if (entry.bare.ask !== undefined || entry.param.ask.length > 0 || entry.content.ask.length > 0)
351
343
  nameSets.ask.push(toolName);
352
344
  }
345
+ for (const behavior of ["deny", "ask"])
346
+ if (command[behavior].length > 0 && !nameSets[behavior].includes(COMMAND_RULE_TOOL))
347
+ nameSets[behavior].push(COMMAND_RULE_TOOL);
353
348
  for (const behavior of ["deny", "ask"]) {
354
349
  for (const r of namespacedCovering[behavior])
355
350
  nameSets[behavior].push(r.ruleName);
@@ -360,19 +355,57 @@ export function createPermissionRulePolicy(rules, opts) {
360
355
  deny: [...byTool.values()].flatMap((e) => e.content.deny.filter((r) => r.faces !== undefined)),
361
356
  ask: [...byTool.values()].flatMap((e) => e.content.ask.filter((r) => r.faces !== undefined)),
362
357
  };
363
- const pathDenyRules = faceRules.deny.map((r) => pathRuleDenyRowOf({ parsed: r.parsed, ruleText: r.ruleText, faces: r.faces }, pathBases));
364
- const contentLaneOf = (behavior, entry, callFace) => [
358
+ const contentLaneOf = (behavior, entry, shellCall, callFace) => [
365
359
  ...(entry?.content[behavior].filter((r) => r.faces === undefined) ?? []),
360
+ ...(shellCall ? command[behavior] : []),
366
361
  ...(callFace !== undefined ? faceRules[behavior].filter((r) => r.faces.includes(callFace)) : []),
367
362
  ];
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 } : {}) };
363
+ const hasCommandRules = command.deny.length > 0 || command.ask.length > 0;
364
+ const commandParam = catalogRuleFaceOf(COMMAND_RULE_TOOL)?.primaryParams[0] ?? "command";
365
+ const pathRuleOf = new Map();
366
+ for (const e of byTool.values()) {
367
+ for (const behavior of ["deny", "ask"]) {
368
+ for (const r of e.content[behavior]) {
369
+ if (r.parsed.match !== "path" && r.parsed.match !== "subpath")
370
+ continue;
371
+ pathRuleOf.set(r, { parsed: r.parsed, ruleText: r.ruleText, ...(behavior === "deny" && r.faces !== undefined ? { faces: r.faces } : {}) });
372
+ }
373
+ }
374
+ }
375
+ const pathSpecs = [...pathRuleOf.values()];
376
+ const bound = new Map();
377
+ const boundAt = (anchors) => {
378
+ const key = JSON.stringify([anchors.root ?? null, anchors.cwd ?? null, anchors.home ?? null]);
379
+ const prior = bound.get(key);
380
+ if (prior !== undefined)
381
+ return prior;
382
+ const rows = bindPathRules(pathSpecs, anchors);
383
+ const of = new Map();
384
+ [...pathRuleOf.keys()].forEach((r, i) => of.set(r, rows[i]));
385
+ const out = { rows, of };
386
+ bound.set(key, out);
387
+ return out;
388
+ };
371
389
  const contentHit = (lane, req) => {
372
390
  if (lane.length === 0)
373
391
  return undefined;
374
- const bases = callBasesOf(req);
375
- const target = effectivePathTargetOf(req, protectivePathTargetOf(req), bases);
392
+ const pathLane = lane.filter((r) => pathRuleOf.has(r));
393
+ if (pathLane.length > 0) {
394
+ const views = callTargetViewsOf(req, "tighten", { ...(pathBases.root !== undefined ? { root: pathBases.root } : {}), ...(pathBases.cwd !== undefined ? { cwd: pathBases.cwd } : {}) });
395
+ if (views !== undefined) {
396
+ const { of } = boundAt(policyAnchors(pathBases, req.pathRuleAnchors));
397
+ const rows = rowsWhereTheyLead(pathLane.map((r) => of.get(r)), callTargetReadingOf(req)?.leads ?? NO_LEADS);
398
+ const hit = pathRowsReach(rows, views, "tighten");
399
+ const rule = hit === undefined ? undefined : pathLane.find((r) => of.get(r).spec === hit.row.spec);
400
+ if (hit !== undefined && hit.unreadable === undefined)
401
+ return { rule: rule };
402
+ if (hit !== undefined)
403
+ return judgeOthers(lane.filter((r) => !pathRuleOf.has(r)), req) ?? { rule: rule, unreadable: hit.unreadable };
404
+ }
405
+ }
406
+ return judgeOthers(lane.filter((r) => !pathRuleOf.has(r)), req);
407
+ };
408
+ const judgeOthers = (lane, req) => {
376
409
  const command = isPlainObjectArgs(req.args) ? req.args[commandParam] : undefined;
377
410
  let unreadable;
378
411
  for (const r of lane) {
@@ -382,20 +415,6 @@ export function createPermissionRulePolicy(rules, opts) {
382
415
  return { rule: r };
383
416
  continue;
384
417
  }
385
- if (r.parsed.match === "subpath" || r.parsed.match === "path") {
386
- if (target === undefined || target.kind === "refused")
387
- continue;
388
- if (target.kind === "unreadable") {
389
- unreadable ??= { rule: r, unreadable: target.reason };
390
- continue;
391
- }
392
- const outcome = pathRuleReachOf(r.parsed, target.identity, bases);
393
- if (outcome.reach === "reached")
394
- return { rule: r };
395
- if (outcome.reach === "unreadable")
396
- unreadable ??= { rule: r, unreadable: outcome.reason };
397
- continue;
398
- }
399
418
  if (typeof command !== "string")
400
419
  continue;
401
420
  const outcome = programRunReachOf(r.parsed, command);
@@ -430,7 +449,7 @@ export function createPermissionRulePolicy(rules, opts) {
430
449
  };
431
450
  return {
432
451
  nameSets: [nameSets],
433
- ...(pathDenyRules.length > 0 ? { pathDenyRules } : {}),
452
+ ...(pathSpecs.length > 0 ? { pathRules: (anchors) => boundAt(policyAnchors(pathBases, anchors)).rows } : {}),
434
453
  check(req) {
435
454
  const entry = entryFor(req);
436
455
  const covering = hasCovering && protocolOf(req.toolName) !== undefined;
@@ -443,7 +462,8 @@ export function createPermissionRulePolicy(rules, opts) {
443
462
  return { action: "deny", message: ruleDenial(req, coveringDeny.ruleText, coveringDeny.source) };
444
463
  }
445
464
  const callFace = faceRules.deny.length > 0 || faceRules.ask.length > 0 ? callPathFaceOf(req) : undefined;
446
- const denyLane = contentLaneOf("deny", entry, callFace);
465
+ const shellCall = hasCommandRules && laneToolNamesOf(req).includes(COMMAND_RULE_TOOL);
466
+ const denyLane = contentLaneOf("deny", entry, shellCall, callFace);
447
467
  const contentDeny = denyLane.length > 0 ? contentHit(denyLane, req) : undefined;
448
468
  if (contentDeny !== undefined && contentDeny.unreadable === undefined) {
449
469
  return { action: "deny", message: ruleDenial(req, contentDeny.rule.ruleText, contentDeny.rule.source) };
@@ -466,7 +486,7 @@ export function createPermissionRulePolicy(rules, opts) {
466
486
  if (coveringAsk)
467
487
  return { action: "ask", message: ruleAskMessage(coveringAsk.ruleText, coveringAsk.source), matchedAskRule: coveringAsk.ruleText };
468
488
  }
469
- const askLane = contentLaneOf("ask", entry, callFace);
489
+ const askLane = contentLaneOf("ask", entry, shellCall, callFace);
470
490
  const contentAsk = askLane.length > 0 ? contentHit(askLane, req) : undefined;
471
491
  if (contentAsk !== undefined) {
472
492
  if (contentAsk.unreadable !== undefined)
@@ -1,4 +1,9 @@
1
1
  import type { RunnerDeps } from "../types.js";
2
+ import type { ExecutionEnv } from "../../internal/harness.js";
3
+ import type { PathRuleBases } from "../permission-rule-model.js";
4
+ import type { ChildPlacement, InheritedGate, RunInternals } from "./contracts.js";
5
+ import type { ToolPolicy } from "../tool-policy.js";
6
+ import { CheckpointError } from "../checkpoint-store.js";
2
7
  /**
3
8
  * Whether this deployment can place a child agent in a tree OTHER than the caller's — the one fact behind
4
9
  * every request of that shape: an explicit `cwd`, a workflow agent's `isolation: "worktree"`, and the
@@ -11,3 +16,95 @@ import type { RunnerDeps } from "../types.js";
11
16
  * keeps its own code and sentence for the refusal, but none decides the condition itself.
12
17
  */
13
18
  export declare function deploymentCanRerootChild(deps: Pick<RunnerDeps, "executionEnvFactory">): boolean;
19
+ /**
20
+ * Where a child the caller asked to place at `asked` actually landed (`childRoot`, the root its environment gave it) — the
21
+ * ONE judgment every reader of that fact takes
22
+ * (the delegation tool's worktree request, and the anchors of the rules the child inherits):
23
+ * · `honored` — the environment's root is where the child was asked to be (canonical on both sides);
24
+ * · `caller_tree` — it is not, and it lies inside the caller's own tree (`callerRoot`): the same filesystem, the wrong
25
+ * directory — a worktree request then fails (the child would write the caller's checkout);
26
+ * · `elsewhere` — neither: another filesystem namespace (a sandbox the factory minted). The child runs there, it is not
27
+ * in the worktree it was offered, and it takes the caller's path rules again at its own anchors.
28
+ * A path the environment cannot canonicalize is compared as spelled. Residual: two namespaces that hold the same path
29
+ * string, with a factory claiming to honor it, read `honored`.
30
+ */
31
+ export declare function childPlacementOf(env: Pick<ExecutionEnv, "canonicalPath">, childRoot: string, asked: string | undefined, callerRoot: string): Promise<ChildPlacement>;
32
+ /** A delegated leg's placement: the place asked for is the hint (a minted worktree, an explicit cwd, the caller's own cwd)
33
+ * — or, for a worktree the factory mints itself (`isolation`), that worktree on the caller's host (a remote environment's
34
+ * is another namespace). A leg no delegation caller placed (no `callerRoot`) has no placement and reads nothing. */
35
+ export declare function childPlacementOnLeg(env: ExecutionEnv, internals: Pick<RunInternals, "isolation" | "parentCwd"> & {
36
+ readonly callerRoot: string;
37
+ }, root: string): Promise<ChildPlacement>;
38
+ /** The refusal of a worktree the caller minted for a child its environment placed in the caller's own tree. */
39
+ export declare function worktreeNotHonored(cwd: string): Error & {
40
+ code: "isolation.unavailable";
41
+ };
42
+ /**
43
+ * The chain's top leg's anchors as THIS leg reads them (the person's global / session path rows and every `~/` row anchor
44
+ * there): the chain's own (`live` as recorded, else the checkpoint's `seed` rebased — {@link recordedAnchors}), unless this
45
+ * leg is a child placed `elsewhere` or no chain names one — then this leg is the top. A recorded root that is not an
46
+ * absolute path anchors nothing: the rows it would anchor read `unreadable`.
47
+ */
48
+ export declare function chainTopAnchors(cfg: {
49
+ leg: PathRuleBases;
50
+ live: InheritedGate["pathRuleAnchors"];
51
+ seed: InheritedGate["pathRuleAnchors"];
52
+ placement: ChildPlacement | undefined;
53
+ rebase: (p: string) => string;
54
+ }): PathRuleBases;
55
+ /** The installation records a leg reads: the live chain's (as recorded), else the checkpoint's (through the workspace rebase). */
56
+ export interface ChainInstallations {
57
+ readonly live: InheritedGate["pathRuleInstallations"];
58
+ readonly seed: InheritedGate["pathRuleInstallations"];
59
+ }
60
+ /** Why no usable record says where a chain entry was installed: no record (a chain written before the record existed), a
61
+ * record that does not cover the chain position for position, or a member that names no absolute root. */
62
+ export type ChainInstallationGap = "absent" | "misaligned" | "invalid";
63
+ /**
64
+ * The chain entries a leg could not install where they were installed: each UNINSTALLED entry (a host re-supplying a
65
+ * chain) whose path rules depend on the installing leg, with no usable record of that leg. Such a chain is refused —
66
+ * installing those entries anywhere else (the chain's top leg, this leg) would move what they protect. Entries whose rules
67
+ * do not depend on it, and installed entries, never need a record.
68
+ */
69
+ export declare function chainInstallationGaps(chain: readonly ToolPolicy[], installations: ChainInstallations, rebase: (p: string) => string): Array<{
70
+ readonly position: number;
71
+ readonly gap: ChainInstallationGap;
72
+ }>;
73
+ /** The refusal of a chain with {@link chainInstallationGaps} (`resume.parent_constraint_mismatch`, `detail.reason` naming
74
+ * the first gap's form); `pending` says the checkpoint is still pending (the refusal came before the claim). */
75
+ export declare function chainInstallationRefusal(gaps: ReadonlyArray<{
76
+ readonly position: number;
77
+ readonly gap: ChainInstallationGap;
78
+ }>, pending: boolean): CheckpointError;
79
+ /**
80
+ * How a leg holds a caller policy — its own (`TaskSpec.toolPolicy` / `RunnerDeps.toolPolicy`) or one its delegation chain
81
+ * hands it: a policy is installed where it ENTERS the chain, and a child holds its ancestor's installation (one object,
82
+ * the same anchors) — its own caller slot included when that slot names the policy an ancestor installed. Installed again:
83
+ * on a child placed `elsewhere` (another filesystem namespace — the ancestor's anchors name another namespace's files), at
84
+ * this leg's anchors; a chain entry handed uninstalled (a host re-supplying a chain), where its installing leg recorded it
85
+ * (`InheritedGate.pathRuleInstallations`, live or from the checkpoint). An uninstalled entry whose rules depend on that
86
+ * leg and whose record is not usable refuses the leg ({@link chainInstallationRefusal}); one whose rules do not, reads
87
+ * the same wherever it is installed (the chain's top leg).
88
+ */
89
+ export declare function chainPolicyOnLeg(cfg: {
90
+ leg: PathRuleBases;
91
+ chainTop: PathRuleBases;
92
+ placement: ChildPlacement | undefined;
93
+ chain: readonly ToolPolicy[];
94
+ installations: ChainInstallations;
95
+ rebase: (p: string) => string;
96
+ }): {
97
+ own: (policy: ToolPolicy | undefined) => ToolPolicy | undefined;
98
+ chained: (policy: ToolPolicy, position: number) => ToolPolicy;
99
+ };
100
+ /** The installation record of a chain entry (its root and home), `null` for an entry with no path rules installed. */
101
+ export declare function installationRecordOf(policy: ToolPolicy): {
102
+ readonly root: string;
103
+ readonly home?: string;
104
+ } | null;
105
+ /** What a leg hands its children about anchors (data half): the chain's top leg's anchors — the person's global / session
106
+ * rows and every `~/` row anchor there — and where each entry handed down was installed, position for position (a chain
107
+ * re-supplied uninstalled is installed there again; absent when no entry is an installation). */
108
+ export declare function chainAnchorRecords(chainTop: PathRuleBases, chain: readonly {
109
+ readonly policy: ToolPolicy;
110
+ }[]): Pick<InheritedGate, "pathRuleAnchors" | "pathRuleInstallations">;
@@ -1,3 +1,107 @@
1
+ import { isRemoteExecutionEnv } from "../remote-env.js";
2
+ import { isAbsolutePathForm } from "../../tools/fs/safety.js";
3
+ import { installPolicyAnchors, installationAnchorsOf, installedFrom } from "../path-rule-binding.js";
4
+ import { CheckpointError } from "../checkpoint-store.js";
1
5
  export function deploymentCanRerootChild(deps) {
2
6
  return deps.executionEnvFactory !== undefined;
3
7
  }
8
+ export async function childPlacementOf(env, childRoot, asked, callerRoot) {
9
+ const canon = async (p) => {
10
+ try {
11
+ const r = await env.canonicalPath(p);
12
+ return r.ok ? r.value : p;
13
+ }
14
+ catch {
15
+ return p;
16
+ }
17
+ };
18
+ const root = await canon(childRoot);
19
+ if (asked !== undefined && (await canon(asked)) === root)
20
+ return "honored";
21
+ const tree = await canon(callerRoot);
22
+ const sep = tree.includes("\\") && !tree.includes("/") ? "\\" : "/";
23
+ return root === tree || root.startsWith(tree.endsWith(sep) ? tree : tree + sep) ? "caller_tree" : "elsewhere";
24
+ }
25
+ export function childPlacementOnLeg(env, internals, root) {
26
+ const asked = internals.isolation === "worktree" ? (isRemoteExecutionEnv(env) ? undefined : root) : internals.parentCwd;
27
+ return childPlacementOf(env, root, asked, internals.callerRoot);
28
+ }
29
+ export function worktreeNotHonored(cwd) {
30
+ return Object.assign(new Error(`isolation "worktree" was requested but cannot be honored: the execution environment factory placed the sub-agent at "${cwd}", inside the caller's own tree, instead of the worktree it was offered. The sub-agent was not started (fail-closed — no silent fallback to the shared working tree).`), { code: "isolation.unavailable" });
31
+ }
32
+ const AS_RECORDED = (p) => p;
33
+ function recordedAnchors(r, rebase) {
34
+ if (r === null || r === undefined)
35
+ return undefined;
36
+ const read = (v) => {
37
+ if (typeof v !== "string")
38
+ return undefined;
39
+ const p = rebase(v);
40
+ return isAbsolutePathForm(p) ? p : undefined;
41
+ };
42
+ const [root, home] = [read(r.root), read(r.home)];
43
+ return { ...(root !== undefined ? { root } : {}), ...(home !== undefined ? { home } : {}) };
44
+ }
45
+ export function chainTopAnchors(cfg) {
46
+ if (cfg.placement === "elsewhere")
47
+ return cfg.leg;
48
+ return (cfg.live !== undefined ? recordedAnchors(cfg.live, AS_RECORDED) : recordedAnchors(cfg.seed, cfg.rebase)) ?? cfg.leg;
49
+ }
50
+ const GAP_REASON = { absent: "installation_record_absent", misaligned: "installation_record_misaligned", invalid: "installation_record_invalid" };
51
+ function dependsOnInstallation(policy) {
52
+ if (installedFrom(policy) !== undefined)
53
+ return false;
54
+ return typeof policy.pathRules === "function" && policy.pathRules({}).some((row) => row.unanchored !== undefined);
55
+ }
56
+ function recordedInstallation(installations, chainLength, position, rebase) {
57
+ const [record, read] = installations.live !== undefined ? [installations.live, AS_RECORDED] : [installations.seed, rebase];
58
+ if (record === undefined)
59
+ return "absent";
60
+ if (record.length !== chainLength)
61
+ return "misaligned";
62
+ const a = recordedAnchors(record[position], read);
63
+ return a?.root === undefined ? "invalid" : a;
64
+ }
65
+ export function chainInstallationGaps(chain, installations, rebase) {
66
+ return chain.flatMap((policy, position) => {
67
+ if (!dependsOnInstallation(policy))
68
+ return [];
69
+ const r = recordedInstallation(installations, chain.length, position, rebase);
70
+ return typeof r === "string" ? [{ position, gap: r }] : [];
71
+ });
72
+ }
73
+ export function chainInstallationRefusal(gaps, pending) {
74
+ const first = gaps[0];
75
+ const form = { absent: "absent (a chain written before installation records existed, or one built without them)", misaligned: "misaligned (it does not cover the chain entry for entry)", invalid: "invalid for that entry (it names no absolute root)" }[first.gap];
76
+ return new CheckpointError("resume.parent_constraint_mismatch", `the parent-constraint chain carries ${gaps.length} polic${gaps.length === 1 ? "y" : "ies"} (entry ${gaps.map((g) => g.position).join(", ")}) whose path rules resolve against the leg that installed ${gaps.length === 1 ? "it" : "them"}, and the installation record is ${form} — installing ${gaps.length === 1 ? "it" : "them"} anywhere else would move what ${gaps.length === 1 ? "it protects" : "they protect"}, so the chain is refused` +
77
+ (pending ? " before the claim (the checkpoint stays pending). A task parked before 7.46.0 under such a chain cannot be resumed by this version: let a worker of the version that parked it finish it, or let it expire and submit the task again." : "."), { reason: GAP_REASON[first.gap] });
78
+ }
79
+ export function chainPolicyOnLeg(cfg) {
80
+ if (cfg.placement !== "elsewhere") {
81
+ const gaps = chainInstallationGaps(cfg.chain, cfg.installations, cfg.rebase);
82
+ if (gaps.length > 0)
83
+ throw chainInstallationRefusal(gaps, false);
84
+ }
85
+ const recorded = (position) => {
86
+ const r = recordedInstallation(cfg.installations, cfg.chain.length, position, cfg.rebase);
87
+ return typeof r === "string" ? undefined : r;
88
+ };
89
+ const chained = (policy, position) => (cfg.placement === "elsewhere" ? installPolicyAnchors(policy, cfg.leg) : installedFrom(policy) !== undefined ? policy : installPolicyAnchors(policy, recorded(position) ?? cfg.chainTop)) ?? policy;
90
+ const own = (policy) => {
91
+ if (policy === undefined)
92
+ return undefined;
93
+ const position = cfg.placement === "elsewhere" ? -1 : cfg.chain.findIndex((c) => (installedFrom(c) ?? c) === (installedFrom(policy) ?? policy));
94
+ return position >= 0 ? chained(cfg.chain[position], position) : installPolicyAnchors(policy, cfg.leg);
95
+ };
96
+ return { own, chained };
97
+ }
98
+ export function installationRecordOf(policy) {
99
+ const a = installationAnchorsOf(policy);
100
+ return a?.root === undefined ? null : { root: a.root, ...(a.home !== undefined ? { home: a.home } : {}) };
101
+ }
102
+ export function chainAnchorRecords(chainTop, chain) {
103
+ return {
104
+ ...(chainTop.root !== undefined ? { pathRuleAnchors: { root: chainTop.root, ...(chainTop.home !== undefined ? { home: chainTop.home } : {}) } } : {}),
105
+ ...(chain.some((pc) => installationAnchorsOf(pc.policy)?.root !== undefined) ? { pathRuleInstallations: chain.map((pc) => installationRecordOf(pc.policy)) } : {}),
106
+ };
107
+ }
@@ -0,0 +1,5 @@
1
+ import type { RunnerDeps } from "../types.js";
2
+ /** The retired-seat door ({@link assertNoRetiredRunnerDeps}), then the seats whose value is screened at construction and
3
+ * never again: the declared configuration root must be the absolute path of an existing directory — a root that names no
4
+ * directory guards nothing, so it is refused here rather than read as absent. */
5
+ export declare function assertRunnerDepsDoors(deps: RunnerDeps): void;
@@ -0,0 +1,6 @@
1
+ import { checkedAgentConfigDir } from "../agent-config-dir.js";
2
+ import { assertNoRetiredRunnerDeps } from "./retired-runner-deps.js";
3
+ export function assertRunnerDepsDoors(deps) {
4
+ assertNoRetiredRunnerDeps(deps);
5
+ checkedAgentConfigDir(deps.agentConfigDir, "RunnerDeps.agentConfigDir");
6
+ }
@@ -393,6 +393,8 @@ export interface Prepared {
393
393
  cwdRef?: CwdRef;
394
394
  /** The root the hand tools resolve root-relative spellings against (present with hands; read-only hands included). */
395
395
  handsRoot?: string;
396
+ /** The leg's target-reading stamp (present with hands): the resume belts stamp the executed bytes' request with it. */
397
+ stampCallTarget?: import("../effective-path-target.js").CallTargetStamp;
396
398
  /** The run's TOOL-PROGRESS lane seat, late-bound for the same reason `harnessRef` is (the tool band is
397
399
  * built during prepare; the lane needs `pushContent` and `ident`, which exist only after it returns).
398
400
  * The driver binds `current` right after `pushContent` exists. Always present on a completed prepare —
@@ -1417,6 +1419,29 @@ export interface InheritedGate {
1417
1419
  * had a governance surface (their opaque posture is untouched). Serializable; persists on checkpoints.
1418
1420
  */
1419
1421
  orgAdmissionGoverned?: true;
1422
+ /**
1423
+ * The chain's TOP leg's root and environment home (data half, serializable; persists on the checkpoint's data half):
1424
+ * the anchors of the person's global and session path rows and of every `~/` row on this chain — a child judges them
1425
+ * where the chain was installed, not at its own root (a project row anchors at its own scope root). Emitted by every leg
1426
+ * that hands a chain down (a top leg emits its own). Absent on an older chain ⇒ the child is its own top. A child placed
1427
+ * in ANOTHER filesystem namespace (its environment did not put it where it was asked, outside the caller's tree) ignores
1428
+ * it and takes its own leg as the top.
1429
+ */
1430
+ pathRuleAnchors?: {
1431
+ readonly root: string;
1432
+ readonly home?: string;
1433
+ };
1434
+ /**
1435
+ * Where each `parentConstraints` entry was INSTALLED (data half, serializable; persists on the checkpoint's data half):
1436
+ * position for position with `parentConstraints`, the root and home its path rules are anchored at (`null` for an entry
1437
+ * with no path rules). A chain re-supplied with uninstalled policies (a resume in another process, a host-built chain)
1438
+ * installs each where this records — never at another leg's anchors. Absent on an older chain: an uninstalled entry is
1439
+ * installed at the chain's top leg (`pathRuleAnchors`).
1440
+ */
1441
+ pathRuleInstallations?: ReadonlyArray<{
1442
+ readonly root: string;
1443
+ readonly home?: string;
1444
+ } | null>;
1420
1445
  /** Live ancestor caller-policy constraints (opaque half), ancestors first. `policy` is the ancestor's
1421
1446
  * RESOLVED caller policy (`spec.toolPolicy ?? deps.toolPolicy` — the same slot the ancestor itself
1422
1447
  * enforces, so a deps-level baseline is inherited too).
@@ -2137,6 +2162,20 @@ export interface RunInternals {
2137
2162
  * callers (ctx-injection posture, same as `parentCwd`).
2138
2163
  */
2139
2164
  requestedCwd?: string;
2165
+ /**
2166
+ * The SPAWNING run's own root, filled ONLY by core delegation callers beside `parentCwd` (ctx-injection posture): with
2167
+ * it the child's prepare judges where its environment actually placed it against where it was asked
2168
+ * (`childPlacementOf`, child-tree.ts: honored / inside the caller's tree / elsewhere) — the fact the
2169
+ * worktree request below and the anchors of the rules the child inherits read. Absent ⇒ no placement is judged (a top
2170
+ * leg, a caller that does not delegate through core).
2171
+ */
2172
+ callerRoot?: string;
2173
+ /**
2174
+ * `parentCwd` is a worktree the caller minted for THIS child (the delegation tool's `isolation: "worktree"`): a child
2175
+ * its environment placed inside the caller's own tree instead is refused before it starts (`isolation.unavailable`).
2176
+ * Filled ONLY by the core delegation tool; never a {@link TaskSpec} field.
2177
+ */
2178
+ worktreeOffered?: true;
2140
2179
  /**
2141
2180
  * Subagent steer verb (dogfood finding 2026-07-03, "中途插话"): the host run's opt-in
2142
2181
  * SUBAGENT-STEER-HANDLE sink. When set, `createSubagentTool` runs each child via `runTaskStream`
@@ -2237,6 +2276,10 @@ export interface RunInternals {
2237
2276
  * non-isolated run reports its plain root with `isolated: false` — the observation face stays complete, while
2238
2277
  * the honest non-claim keeps a consumer from mistaking a SHARED tree for its own worktree.
2239
2278
  */
2279
+ /** Where a delegated child's environment placed it against where its caller asked (`childPlacementOf`, child-tree.ts):
2280
+ * `honored` — where it was asked; `caller_tree` — elsewhere inside the caller's own tree; `elsewhere` — another
2281
+ * filesystem namespace. */
2282
+ export type ChildPlacement = "honored" | "caller_tree" | "elsewhere";
2240
2283
  export interface ResolvedWorkspace {
2241
2284
  /** The task's effective working root — the same value every fence / LSP / prompt consumer uses. */
2242
2285
  cwd: string;
@@ -2247,6 +2290,9 @@ export interface ResolvedWorkspace {
2247
2290
  * or names an unrelated directory — on the host. `isolated && !remote` is the only combination under
2248
2291
  * which `cwd` is a host path a consumer may diff / merge / remove. */
2249
2292
  remote: boolean;
2293
+ /** Where a delegated child's environment placed it against where its caller asked; absent on a leg no delegation
2294
+ * caller placed. */
2295
+ placement?: ChildPlacement;
2250
2296
  }
2251
2297
  /**
2252
2298
  * The resolver's answer, with the WITHHELD cause beside the value.
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import { type ClassifierDenyCause, type DeniedBy, type GateOutcome, type ModeRefusal, type ModeRelease, type Settlement } from "../gate-outcome.js";
25
25
  import type { AutoModeClassifierRound } from "../auto-mode.js";
26
- import { type Arbitration, type AskClassification, type AskOrigin, type AskOriginFacts } from "../ask-origin.js";
26
+ import { type Arbitration, type AskClassification, type AskFace, type AskOrigin, type AskOriginFacts } from "../ask-origin.js";
27
27
  import { type PermissionResult, type ResolvedAsk, type ToolCallRequest } from "../tool-policy.js";
28
28
  import type { SafeNotifier } from "../safe-notify.js";
29
29
  import type { HookSeatOutcome, PermissionDeniedPayload, PreToolUseResult, ToolGateInput, ToolGateResult } from "../hooks.js";
@@ -143,6 +143,9 @@ export interface PreToolUseScreening {
143
143
  /** The model-facing text for an answer the decision receipt refused (an ENGINE fail-closed, not the face's deny). */
144
144
  refusedReason(answer: PreToolUseResult): string;
145
145
  }
146
+ /** The FACE a pass's questions are put on ({@link askFaceOf} of the call's wire name) — the one reading of it the
147
+ * write-protection tighten (the fold) and every class derivation (the lanes) make. */
148
+ export declare function passAskFace(pass: Pick<GatePass, "req">): AskFace;
146
149
  /**
147
150
  * ONE gate pass — the record the station groups (`runGateFold`, `runGateLanes`) and the exits read and write,
148
151
  * built once per call by `runToolGate` before the screening pass. What used to be the closure-captured `let`
@@ -1,5 +1,6 @@
1
1
  import { screenGateOutcome, SETTLEMENT_IS_REFUSAL } from "../gate-outcome.js";
2
- import { classifyAsk, headlessApprovalRemedy } from "../ask-origin.js";
2
+ import { askFaceOf, classifyAsk, headlessApprovalRemedy } from "../ask-origin.js";
3
+ import { wireOf } from "../tool-name-readings.js";
3
4
  import { isProtectedWrite } from "../tool-registry.js";
4
5
  import { askUnresolvableCauseOf, classifierDenyCauseOf, classifierRoundOf, decisionText, engineSettlementOf } from "../tool-policy.js";
5
6
  import { askUnresolvableRemedySeat } from "../ask-unresolvable-notice.js";
@@ -97,6 +98,9 @@ function withParkFailureCause(reason, parkFailed) {
97
98
  `(${inlineUntrusted(parkFailed, PARK_FAILURE_CAUSE_MAX)}), so the refusal above is what the fallback had ` +
98
99
  `left to say, not the reason the call stopped.`);
99
100
  }
101
+ export function passAskFace(pass) {
102
+ return askFaceOf(wireOf("gate.askFace", pass.req));
103
+ }
100
104
  export async function engineFailClosedExit(pass, reason) {
101
105
  const { input, toolName, toolCallId, notifyPermissionDenied } = pass;
102
106
  const gate = directDeny("hook");