@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
@@ -28,8 +28,10 @@
28
28
  import type { AskRuleEvidence, ToolCallRequest } from "../tool-policy.js";
29
29
  import { persistedRuleMandateOf, type RuleClearanceFacts } from "../ask-origin.js";
30
30
  import { type OrgGateVerdict, type PersistedRuleAnswer, type PersistedRuleHit, type PersistedRuleUnreadable } from "../hooks.js";
31
- import type { PersistedRule, PersistedRuleVerdict, RuleOffer, SegmentCoverage } from "../permission-rule-model.js";
32
- import { type ToolCallLike } from "../tool-registry.js";
31
+ import type { PersistedRule, PersistedRuleVerdict, RuleOffer, ScopeRootIdentity, SegmentCoverage } from "../permission-rule-model.js";
32
+ import { type PathRuleBases } from "../permission-rule-model.js";
33
+ import { type HeadLeads } from "../path-rule-binding.js";
34
+ import type { ToolCallLike } from "../tool-registry.js";
33
35
  import { type OrgRuleResolution } from "../permission-rule-org.js";
34
36
  import { type EffectivePermissionRules, type PermissionRuleStoreProvider } from "../permission-rule-provider.js";
35
37
  /** The one SHELL tool the persisted-rule lane's COMMAND grammar speaks for (the rule-offer face mints for it and the
@@ -87,28 +89,25 @@ export declare function persistedRuleHitOf(verdict: PersistedRuleVerdict | undef
87
89
  * question nobody can answer. Every other verdict is {@link persistedRuleHitOf}'s hit. */
88
90
  export declare function persistedRuleAnswerOf(verdict: PersistedRuleVerdict | undefined): PersistedRuleHit | PersistedRuleUnreadable | undefined;
89
91
  /**
90
- * Widened to every path-targeting tool — the persisted-rule lane's PATH arm: the call's
91
- * target path in the SAME lexical identity the rule family is defined over, resolved by the ONE reader every
92
- * fence and every tool goes through (`effectivePathTargetOf`), so the lane judges the path the tool will
93
- * really open. The tool's own declaration decides which base a relative spelling takes and what an ABSENT
94
- * slot means — the lane no longer guesses `liveCwd ?? root` for every tool, which was wrong for the two
95
- * search tools in three separate shapes (see effective-path-target.ts). Still zero IO.
92
+ * The persisted-rule lane's PATH arm, for every path-targeting tool (and the entry lists' path rows): the call's target
93
+ * read through the ONE reader every lane shares ({@link callTargetViewsOf} — the reading's views, else the call's own
94
+ * lexical view), its rows anchored by `ctx.anchorsOf` (a stored row: a project row at its scope root, the rest at the
95
+ * chain's top leg; a list's row: where the list was installed) and judged where their heads lead too (`ctx.leads`). The
96
+ * precedence walk, the admission predicates and the Read-only allow are the model's (`adjudicatePersistedPathRules`); which
97
+ * project rows apply is judged against the leg's own root (`ctx.root`). An ALLOW here clears ASKS only — the read fences
98
+ * judge inside the tool exactly as they would with no rule anywhere — and an unresolvable path is no verdict on the allow
99
+ * side and the fail-closed ask on the tightening side.
96
100
  *
97
- * TWO readings of the call's path slot, one per direction: the tightening arms judge
98
- * the PROTECTIVE target (the object face's declaration, the catalog's for the name as the floor — reading
99
- * the catalog for an undeclared same-name tool can only tighten a deny); the allow arm judges the DECLARED
100
- * target (the face's OWN declaration only — a same-name tool that declares no path target is unconfirmable
101
- * and no grant admits it). The precedence walk, the admission predicates and the Read-only allow are the
102
- * model's (`adjudicatePersistedPathRules`). An ALLOW here clears ASKS only — the read fences (workspace
103
- * containment, the sensitive-read deny face) judge inside the tool exactly as they would with no rule
104
- * anywhere — and an unresolvable path is simply no verdict (fail toward asking on the allow side, no
105
- * refusal on the tightening side: a rule about a path speaks about no other shape).
101
+ * TWO readings of the call's path slot, one per direction: the tightening arms judge the PROTECTIVE reading (the object
102
+ * face's declaration, the catalog's for the name as the floor); the allow arm the DECLARED one (the face's OWN
103
+ * declaration only — a same-name tool that declares no path target is unconfirmable and no grant admits it).
106
104
  */
107
- export declare function pathRuleLaneAnswer(table: readonly PersistedRule[], req: Pick<ToolCallRequest, "toolName" | "args" | "face">, ctx: {
105
+ export declare function pathRuleLaneAnswer(table: readonly PersistedRule[], req: Pick<ToolCallRequest, "toolName" | "args" | "face" | "targetReading" | "root" | "cwd">, ctx: {
108
106
  root: string | undefined;
109
107
  sessionId: string | undefined;
110
- liveCwd: string | undefined;
111
- home: string | undefined;
108
+ anchorsOf: (rule: PersistedRule) => PathRuleBases;
109
+ leads: ReadonlyMap<string, string | undefined>;
110
+ scopeIdentity?: ScopeRootIdentity;
112
111
  }): PersistedRuleHit | PersistedRuleUnreadable | undefined;
113
112
  /** The two lanes prepare hands the gate. Each is `undefined` when its partition is not configured, so
114
113
  * a deployment without one keeps a decision path byte-identical to a build without the feature. */
@@ -152,15 +151,23 @@ export declare function createPermissionRuleLanes(cfg: {
152
151
  principal: string | undefined;
153
152
  sessionId: string;
154
153
  /** The isolation-aware task root — the same value the file fence, the prompt cwd and the session rules
155
- * use, so a project-scoped rule agrees with every other place that says "this project". */
154
+ * use, so a project-scoped rule agrees with every other place that says "this project" (which project rows apply). */
156
155
  root: string;
156
+ /** The same root's canonical spelling (`undefined`: none could be read): a project row applies when its scope root covers
157
+ * either spelling of it — the row's root resolved where it leads ({@link projectScopeIdentity}). */
158
+ rootCanonical: string | undefined;
159
+ /** The anchors of the person's global and session rows and of every `~/` row: the root and home of the chain's top leg
160
+ * (a project row anchors at its own scope root). */
161
+ chainTop: PathRuleBases;
162
+ /** The leg's resolution of the locations rule heads pass through (the rows' twins). */
163
+ leads: HeadLeads;
157
164
  /** The LIVE tracked working directory: the base a relative cd resolves against
158
165
  * after an earlier observable `cd`; read per call. */
159
166
  liveCwd: () => string | undefined;
160
- /** The EXECUTION ENVIRONMENT's home directory (`ExecutionEnv.homeDir`, adapter-declared), the base a
161
- * `~/` path rule resolves against. `undefined` = the adapter declares none: a `~/` deny/ask row then reads
162
- * UNREADABLE for every call (a fail-closed ask), never the engine process's home. A declared value that is
163
- * not an absolute path is refused HERE, loudly (a bad declaration is a wiring defect, not an absence). */
167
+ /** The EXECUTION ENVIRONMENT's home directory (`ExecutionEnv.homeDir`, adapter-declared), screened here: a declared
168
+ * value that is not an absolute path is refused, loudly (a bad declaration is a wiring defect, not an absence). The
169
+ * `~/` rows anchor at the chain's top leg's home (`chainTop.home`); `undefined` there = no home was declared: a `~/`
170
+ * deny/ask row then reads UNREADABLE for every call (a fail-closed ask), never the engine process's home. */
164
171
  home: string | undefined;
165
172
  /** True iff the reserved question-tool NAME resolves to the ENGINE's own content-ask tool on this leg. */
166
173
  questionToolMounted: boolean;
@@ -173,6 +180,8 @@ export declare function createPermissionRuleLanes(cfg: {
173
180
  /** The person is told their store (or the named sources of it) was skipped for a call (the run's one notice,
174
181
  * `ruleStoreUnreadableNotice`). */
175
182
  onStoreUnreadable: (at: "call", sources?: EffectivePermissionRules["unreadableSources"]) => void;
183
+ /** The person is told the store served rows that were left out (the run's one `permission.rule_row_dropped`). */
184
+ onRowsDropped: (rows: EffectivePermissionRules["droppedRows"]) => void;
176
185
  /** The run's cut: a read it interrupts is abandoned quietly — a cancelled wait is no fact about the store. */
177
186
  signal?: AbortSignal;
178
187
  /** The allow layer's auto strip over a stored ALLOW row (true ⇒ out of this call's table BEFORE the coverage is chosen). */
@@ -2,8 +2,8 @@ import { persistedRuleMandateOf, ruleClearanceOf } from "../ask-origin.js";
2
2
  import {} from "../hooks.js";
3
3
  import { adjudicatePersistedNameRules, adjudicatePersistedPathRules, adjudicatePersistedRules, callPathFaceOf, faceAskSpeaksFor, laneToolNamesOf, nameRuleSubjectOf, ruleToolGrammarOf, segmentCoverageOf, suggestRuleForName, suggestRulesForCommand } from "../permission-rule-model.js";
4
4
  import { isAbsolutePathForm } from "../../tools/fs/safety.js";
5
- import { effectivePathTargetOf } from "../effective-path-target.js";
6
- import { declaredPathTargetOf, protectivePathTargetOf } from "../tool-registry.js";
5
+ import { callTargetViewsOf } from "../effective-path-target.js";
6
+ import { bindPathRules, persistedRowAnchors, projectScopeIdentity } from "../path-rule-binding.js";
7
7
  import { heldBy, seatOf, wireOf } from "../tool-name-readings.js";
8
8
  import { orgRuleVerdictFor, settleOrgVerdictWithin } from "../permission-rule-org.js";
9
9
  import { ASK_USER_QUESTION_TOOL_NAME } from "../ask-question.js";
@@ -13,6 +13,7 @@ import { strongestAnswer } from "../persisted-rule-arms.js";
13
13
  import { gateWaitFailureOf, withGateTimeout } from "../gate-bounded-await.js";
14
14
  import { ORG_ADJUDICATION_TIMEOUT_MS } from "../permission-rule-org.js";
15
15
  export const PERSISTED_RULE_TOOL = "Bash";
16
+ const NO_LEADS = new Map();
16
17
  const HANDOFF_BOUND = 256;
17
18
  export function inheritedAskRuleEvidence(provider) {
18
19
  const org = provider?.partitions.org === true ? "not_adjudicated" : "not_wired";
@@ -40,12 +41,11 @@ export function persistedRuleAnswerOf(verdict) {
40
41
  return persistedRuleHitOf(verdict);
41
42
  }
42
43
  export function pathRuleLaneAnswer(table, req, ctx) {
43
- const bases = { ...(ctx.root !== undefined ? { root: ctx.root } : {}), ...(ctx.liveCwd !== undefined ? { cwd: ctx.liveCwd } : {}), ...(ctx.home !== undefined ? { home: ctx.home } : {}) };
44
- const tighten = effectivePathTargetOf(req, protectivePathTargetOf(req), bases);
45
- const allow = effectivePathTargetOf(req, declaredPathTargetOf(req), bases);
44
+ const tighten = callTargetViewsOf(req, "tighten");
45
+ const allow = callTargetViewsOf(req, "allow");
46
46
  if (tighten === undefined && allow === undefined)
47
47
  return undefined;
48
- return persistedRuleAnswerOf(adjudicatePersistedPathRules(table, { tool: req.toolName, cwd: ctx.root, sessionId: ctx.sessionId, face: callPathFaceOf(req) }, { ...(tighten !== undefined ? { tighten } : {}), ...(allow !== undefined ? { allow } : {}) }, bases));
48
+ return persistedRuleAnswerOf(adjudicatePersistedPathRules(table, { tool: req.toolName, cwd: ctx.root, sessionId: ctx.sessionId, face: callPathFaceOf(req), ...(ctx.scopeIdentity !== undefined ? { scopeIdentity: ctx.scopeIdentity } : {}) }, { ...(tighten !== undefined ? { tighten } : {}), ...(allow !== undefined ? { allow } : {}) }, ctx.anchorsOf, ctx.leads));
49
49
  }
50
50
  export function orgRuleLane(cfg) {
51
51
  return {
@@ -141,8 +141,12 @@ export function createPermissionRuleLanes(cfg) {
141
141
  }
142
142
  if (view.unreadable !== undefined)
143
143
  skipped(view.unreadable, view.unreadableSources);
144
+ cfg.onRowsDropped(view.droppedRows);
144
145
  try {
145
- const answer = strongestAnswer(judged.map(([n, grammar]) => judgeAgainst(view.rules.filter((r) => r.behavior !== "allow" || cfg.stripsAllow?.(r, req) !== true), { ...req, toolName: n }, grammar)));
146
+ const table = view.rules.filter((r) => r.behavior !== "allow" || cfg.stripsAllow?.(r, req) !== true);
147
+ const leads = judged.some(([, g]) => g === "path") ? await cfg.leads.resolve(bindPathRules(table.filter((r) => r.match === "path" || r.match === "subpath").map((r) => ({ parsed: r, ruleText: r.rule, rule: r })), (spec) => anchorsOf(spec.rule))) : NO_LEADS;
148
+ const scopeIdentity = await projectScopeIdentity(table, cfg.rootCanonical, cfg.leads);
149
+ const answer = strongestAnswer(judged.map(([n, grammar]) => judgeAgainst(table, { ...req, toolName: n }, grammar, leads, scopeIdentity)));
146
150
  return view.unreadable !== undefined && (answer === undefined || "segmentCoverage" in answer) ? { unreadable: true } : answer;
147
151
  }
148
152
  catch (err) {
@@ -151,20 +155,22 @@ export function createPermissionRuleLanes(cfg) {
151
155
  return { unreadable: true, reason: why };
152
156
  }
153
157
  };
154
- const judgeAgainst = (table, req, grammar) => {
158
+ const anchorsOf = (rule) => persistedRowAnchors(rule.scope, cfg.chainTop);
159
+ const judgeAgainst = (table, req, grammar, leads, scopeIdentity) => {
155
160
  const liveCwd = cfg.liveCwd();
161
+ const scope = scopeIdentity !== undefined ? { scopeIdentity } : {};
156
162
  if (grammar === "path")
157
- return pathRuleLaneAnswer(table, req, { root: cfg.root, sessionId: cfg.sessionId, liveCwd, home: cfg.home });
163
+ return pathRuleLaneAnswer(table, req, { root: cfg.root, sessionId: cfg.sessionId, anchorsOf, leads, ...scope });
158
164
  if (grammar === "name")
159
- return persistedRuleAnswerOf(adjudicatePersistedNameRules(table, { tool: req.toolName, name: nameRuleSubjectOf(req.toolName, req.args), cwd: cfg.root, sessionId: cfg.sessionId }));
165
+ return persistedRuleAnswerOf(adjudicatePersistedNameRules(table, { tool: req.toolName, name: nameRuleSubjectOf(req.toolName, req.args), cwd: cfg.root, sessionId: cfg.sessionId, ...scope }));
160
166
  const command = req.args?.command;
161
167
  if (typeof command !== "string")
162
168
  return undefined;
163
169
  const execCwd = liveCwd !== undefined ? { execCwd: liveCwd } : {};
164
- const verdict = adjudicatePersistedRules(table, { tool: req.toolName, command, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
170
+ const verdict = adjudicatePersistedRules(table, { tool: req.toolName, command, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd, ...scope });
165
171
  if (verdict !== undefined)
166
172
  return persistedRuleAnswerOf(verdict);
167
- const coverage = segmentCoverageOf(command, { persisted: table }, { tool: req.toolName, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
173
+ const coverage = segmentCoverageOf(command, { persisted: table }, { tool: req.toolName, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd, ...scope });
168
174
  return coverage !== undefined ? { segmentCoverage: coverage } : undefined;
169
175
  };
170
176
  const skipped = (why, sources) => (cfg.onDisclosure(why), cfg.onStoreUnreadable("call", sources), { unreadable: true });
@@ -3,14 +3,16 @@
3
3
  * per call).
4
4
  */
5
5
  import { type EffectivePermissionRules, type PermissionRuleStoreProvider } from "../permission-rule-provider.js";
6
- import type { PathRuleDenyRow } from "../../tools/fs/read-deny.js";
6
+ import { type PathRuleBases } from "../permission-rule-model.js";
7
+ import { type BoundPathRule, type HeadLeads } from "../path-rule-binding.js";
7
8
  import type { RunnerDeps } from "../runner-deps.js";
8
9
  /** The person's persisted PATH rows a leg's read face takes at prepare ({@link persistedPathRowsOf}). */
9
10
  export interface PersistedPathRows {
10
11
  /** Every live `Read(//dir/**)` allow row's directory (lexical, absolute): a standing yes to reads under it. */
11
12
  readonly readDirectories: readonly string[];
12
- /** Every live face-keyed path DENY row (`Read(...)`, `Edit(...)`), as a deny-table row resolved at the task's bases. */
13
- readonly denyRows: readonly PathRuleDenyRow[];
13
+ /** Every live face-keyed path DENY row (`Read(...)`, `Edit(...)`), bound at its anchors (a project row at its scope root,
14
+ * the rest at the chain's top leg). */
15
+ readonly denyRows: readonly BoundPathRule[];
14
16
  }
15
17
  /**
16
18
  * The person's persisted PATH rows, read ONCE at prepare from the same store query the gate's lanes read per call: a
@@ -22,19 +24,28 @@ export interface PersistedPathRows {
22
24
  * rows. A read that failed (or did not answer in time) ⇒ no rows, and a partition the read could not read ⇒ none of ITS
23
25
  * rows (the partition that was read contributes its own): what could not be read is skipped for this run's tables — its
24
26
  * directories widen nothing and its deny rows judge nothing here — disclosed on the trace and announced to the person
25
- * (`onUnread`); the next prepare reads again, as it would for a row the store gained mid-run. A deny row whose
26
- * pattern needs a base the task cannot supply (`~/` with no declared home) is left to the per-call lane and disclosed.
27
+ * (`onUnread`); the next prepare reads again, as it would for a row the store gained mid-run. A deny row is anchored by
28
+ * its scope (`persistedRowAnchors`: a project row at its own scope root, a global / session row at the chain's top leg,
29
+ * `~/` at the top leg's home); one whose pattern needs an anchor the chain does not supply (`~/` with no declared home) is
30
+ * left to the per-call lane and disclosed.
27
31
  */
28
32
  export declare function persistedPathRowsOf(cfg: {
29
33
  provider: PermissionRuleStoreProvider | undefined;
30
34
  localOwnerDeclared: boolean;
31
35
  principal: string | undefined;
32
36
  sessionId: string;
37
+ /** The leg's own root: which project rows apply to this leg (either spelling of it — {@link projectScopeIdentity}). */
33
38
  root: string;
34
- home: string | undefined;
39
+ rootCanonical: string | undefined;
40
+ /** The leg's resolution of locations (a project row's scope root, where it leads). */
41
+ leads: HeadLeads;
42
+ /** The anchors of the global / session rows and of `~/`: the chain's top leg's root and home. */
43
+ chainTop: PathRuleBases;
35
44
  onDisclosure: (message: string) => void;
36
45
  /** The person is told the store (or the named sources of it) was skipped ({@link ruleStoreUnreadableNotice}). */
37
46
  onUnread: (at: "prepare", sources?: EffectivePermissionRules["unreadableSources"]) => void;
47
+ /** The person is told rows were left out ({@link ruleRowsDroppedNotice}). */
48
+ onRowsDropped: (rows: EffectivePermissionRules["droppedRows"]) => void;
38
49
  /** The run's cut: a read that has not answered by then (or by the gate's await bound) reads as unreadable. */
39
50
  signal?: AbortSignal;
40
51
  }): Promise<PersistedPathRows>;
@@ -48,6 +59,16 @@ export declare function ruleStoreUnreadableNotice(onNotice: RunnerDeps["onNotice
48
59
  readonly sessionId: string;
49
60
  readonly runId: string;
50
61
  }): (at: "prepare" | "call", sources?: EffectivePermissionRules["unreadableSources"]) => void;
62
+ /**
63
+ * The run's ONE `permission.rule_row_dropped` notice (audience user): the person's store served project-scoped rows whose
64
+ * scope root is not a complete absolute path, left out of the checks; minted by whichever read met them first (the
65
+ * prepare read of the path rows, or a call's own read on the rule lane), once per run. The sentence names no path; the
66
+ * rows (bounded) ride the detail. The trace keeps every read's line.
67
+ */
68
+ export declare function ruleRowsDroppedNotice(onNotice: RunnerDeps["onNotice"], ids: {
69
+ readonly sessionId: string;
70
+ readonly runId: string;
71
+ }): (rows: EffectivePermissionRules["droppedRows"]) => void;
51
72
  /** {@link persistedPathRowsOf} for one task's prepare: only where a read face mounts (hands), disclosed on the run's trace,
52
73
  * cut by the run's own signals. `undefined` = nothing to read, returned synchronously: a run with no person's store (or
53
74
  * no hands) gains no suspension point on its prepare. */
@@ -56,9 +77,12 @@ export declare function persistedPathRowsOfTask(t: {
56
77
  principal: string | undefined;
57
78
  sessionId: string;
58
79
  root: string;
59
- home: string | undefined;
80
+ rootCanonical: string | undefined;
81
+ leads: HeadLeads;
82
+ chainTop: PathRuleBases;
60
83
  taskId: string;
61
84
  handsEnabled: boolean;
62
85
  signals: readonly AbortSignal[];
63
86
  onUnread: (at: "prepare", sources?: EffectivePermissionRules["unreadableSources"]) => void;
87
+ onRowsDropped: (rows: EffectivePermissionRules["droppedRows"]) => void;
64
88
  }): Promise<PersistedPathRows> | undefined;
@@ -1,7 +1,6 @@
1
1
  import { ownerStoreOf } from "../permission-rule-provider.js";
2
- import { PATH_RULE_FACES, READ_RULE_TOOL, eligiblePersisted, isUsablePathBase, ruleBasesNeeded } from "../permission-rule-model.js";
3
- import { isAbsolutePathForm, pathFamilyOf } from "../../tools/fs/safety.js";
4
- import { pathRuleDenyRowOf } from "../permission-rules.js";
2
+ import { PATH_RULE_FACES, READ_RULE_TOOL, eligiblePersisted } from "../permission-rule-model.js";
3
+ import { bindPathRules, persistedRowAnchors, projectScopeIdentity } from "../path-rule-binding.js";
5
4
  import { inlineUntrusted } from "../untrusted-text.js";
6
5
  import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, gateWaitFailureOf, withGateTimeout } from "../gate-bounded-await.js";
7
6
  import { thrownText } from "../thrown-value.js";
@@ -21,11 +20,11 @@ export async function persistedPathRowsOf(cfg) {
21
20
  }
22
21
  if (view.unreadable !== undefined)
23
22
  skipped(cfg, view.unreadable, view.unreadableSources);
24
- const call = { tool: READ_RULE_TOOL, cwd: cfg.root, sessionId: cfg.sessionId };
25
- const bases = { root: cfg.root, ...(cfg.home !== undefined && isAbsolutePathForm(cfg.home) ? { home: cfg.home } : {}) };
26
- const family = pathFamilyOf(bases);
23
+ cfg.onRowsDropped(view.droppedRows);
24
+ const scopeIdentity = await projectScopeIdentity(view.rules, cfg.rootCanonical, cfg.leads);
25
+ const call = { tool: READ_RULE_TOOL, cwd: cfg.root, sessionId: cfg.sessionId, ...(scopeIdentity !== undefined ? { scopeIdentity } : {}) };
27
26
  const readDirectories = [];
28
- const denyRows = [];
27
+ const denySpecs = [];
29
28
  for (const rule of view.rules) {
30
29
  if (rule.status !== "live")
31
30
  continue;
@@ -37,12 +36,15 @@ export async function persistedPathRowsOf(cfg) {
37
36
  const faces = PATH_RULE_FACES[rule.tool]?.deny;
38
37
  if (rule.behavior !== "deny" || faces === undefined || (rule.match !== "path" && rule.match !== "subpath") || !eligiblePersisted(rule, { ...call, tool: rule.tool }, "deny"))
39
38
  continue;
40
- const missing = ruleBasesNeeded(rule).filter((b) => !isUsablePathBase(b === "cwd" ? bases.root : bases[b], family));
41
- if (missing.length > 0) {
42
- cfg.onDisclosure(`the persisted deny rule ${inlineUntrusted(rule.rule, 200)} needs a ${missing.join(" / ")} base this task does not declare — the shell's file commands and the search tools do not judge it on this leg`);
39
+ denySpecs.push({ parsed: rule, ruleText: rule.rule, faces, rule });
40
+ }
41
+ const denyRows = [];
42
+ for (const row of bindPathRules(denySpecs, (spec) => persistedRowAnchors(spec.rule.scope, cfg.chainTop))) {
43
+ if (row.unanchored !== undefined) {
44
+ cfg.onDisclosure(`the persisted deny rule ${inlineUntrusted(row.rule, 200)} needs a ${row.unanchored.join(" / ")} base this task does not declare — the shell's file commands and the search tools do not judge it on this leg`);
43
45
  continue;
44
46
  }
45
- denyRows.push(pathRuleDenyRowOf({ parsed: rule, ruleText: rule.rule, faces }, bases));
47
+ denyRows.push(row);
46
48
  }
47
49
  return { readDirectories, denyRows };
48
50
  }
@@ -68,6 +70,20 @@ export function ruleStoreUnreadableNotice(onNotice, ids) {
68
70
  deliverEngineNotice(onNotice, { code: "permission.rule_store_unavailable", message: SKIPPED_STORE_SENTENCE[at](which), detail: { at, sessionId: ids.sessionId, runId: ids.runId } });
69
71
  };
70
72
  }
73
+ export function ruleRowsDroppedNotice(onNotice, ids) {
74
+ let sent = false;
75
+ return (rows) => {
76
+ if (sent || rows === undefined || rows.length === 0)
77
+ return;
78
+ sent = true;
79
+ const one = rows.length === 1;
80
+ deliverEngineNotice(onNotice, {
81
+ code: "permission.rule_row_dropped",
82
+ message: `${rows.length} of your persisted permission rule rows ${one ? "is" : "are"} scoped to a project directory that is not written as a complete absolute path, so ${one ? "it takes" : "they take"} no part in this run's checks — a deny or ask row among them protects nothing. Rewrite ${one ? "it" : "them"} with the project's full path, or remove ${one ? "it" : "them"} (this notice's detail names ${one ? "it" : "them"}).`,
83
+ detail: { rows: rows.slice(0, 8).map((r) => ({ rule: inlineUntrusted(r.rule, 200), scopeRoot: inlineUntrusted(r.scopeRoot, 200) })), count: rows.length, sessionId: ids.sessionId, runId: ids.runId },
84
+ });
85
+ };
86
+ }
71
87
  function readsPersistedPathRows(provider, principal, localOwnerDeclared) {
72
88
  return provider.partitions.durable && ((principal !== undefined && principal !== "") || localOwnerDeclared);
73
89
  }
@@ -81,9 +97,12 @@ export function persistedPathRowsOfTask(t) {
81
97
  principal: t.principal,
82
98
  sessionId: t.sessionId,
83
99
  root: t.root,
84
- home: t.home,
100
+ rootCanonical: t.rootCanonical,
101
+ leads: t.leads,
102
+ chainTop: t.chainTop,
85
103
  onDisclosure: (message) => emitTrace(t.deps.tracer, () => ({ kind: "permission.rule_store_unreadable", version: 1, taskId: t.taskId, message, ts: Date.now() })),
86
104
  onUnread: t.onUnread,
105
+ onRowsDropped: t.onRowsDropped,
87
106
  signal: AbortSignal.any([...t.signals]),
88
107
  });
89
108
  }
@@ -1,3 +1,4 @@
1
+ import type { CallTargetStamp } from "../effective-path-target.js";
1
2
  import type { AgentTool } from "../../internal/harness.js";
2
3
  import type { AdvertisedWritableDirs } from "./advertised-writable-dirs.js";
3
4
  import type { AutoModeDenialTracker } from "../auto-mode.js";
@@ -30,6 +31,9 @@ export interface PrepareAskLaneInput {
30
31
  handsCwdRef: CwdRef | undefined;
31
32
  /** borrowed-readonly — the root the hand tools resolve root-relative spellings against, or undefined (hands-less). */
32
33
  handsRoot: string | undefined;
34
+ /** borrowed-readonly — the leg's target-reading stamp ({@link CallTargetStamp}), or undefined (hands-less): the request a
35
+ * policy judges carries a reading of its own args. Called at RUN time; a seat with nothing to stamp answers undefined. */
36
+ stampCallTarget: CallTargetStamp | undefined;
33
37
  /** 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. */
34
38
  advertisedWritableDirs: Pick<AdvertisedWritableDirs, "roots"> | undefined;
35
39
  /** borrowed-readonly — the shared mount roster; read at RUN time by the approval-preview projection (alias-aware
@@ -59,14 +59,16 @@ function resolveApprovalPreview(tools, toolName, args) {
59
59
  }
60
60
  }
61
61
  export function prepareAskLane(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;
62
+ const { gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, handsRoot, stampCallTarget, 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 } : {}), ...(handsRoot !== undefined ? { root: handsRoot } : {}), ...advertisedWritableRootsSeat(advertisedWritableDirs) }, signal)), signal, () => ({
69
+ const stampedReq = () => ({ ...req, budget: budgetSnapshot, ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}), ...(handsRoot !== undefined ? { root: handsRoot } : {}), ...advertisedWritableRootsSeat(advertisedWritableDirs) });
70
+ const withReading = stampCallTarget?.(stampedReq(), signal);
71
+ return raceAbort(withReading === undefined ? Promise.resolve(effectivePolicy.check(stampedReq(), signal)) : withReading.then((r) => effectivePolicy.check(r, signal)), signal, () => ({
70
72
  action: "deny",
71
73
  message: "policy check aborted (task timed out or cancelled)",
72
74
  }));
@@ -1,6 +1,6 @@
1
1
  import type { ExecutionEnv } from "../../internal/harness.js";
2
2
  import type { RunnerDeps, TaskSpec } from "../types.js";
3
- import type { PrepareResume, RollbackStack, RunInternals } from "./contracts.js";
3
+ import type { ChildPlacement, PrepareResume, RollbackStack, RunInternals } from "./contracts.js";
4
4
  /**
5
5
  * The ONE reference to a factory-minted env, shared by the driver's detach guard and the rollback entry
6
6
  * this phase registers. `current` is the minted env, or `undefined` when the run has none / after the
@@ -19,7 +19,7 @@ export interface PrepareExecutionEnvInput {
19
19
  spec: Pick<TaskSpec, "taskId">;
20
20
  /** borrowed-readonly — the trusted spawn-side requests: the explicit cwd (a contract), the isolation
21
21
  * request and the parent's cwd (both threaded to the factory ctx). */
22
- internals: Pick<RunInternals, "requestedCwd" | "isolation" | "parentCwd"> | undefined;
22
+ internals: Pick<RunInternals, "requestedCwd" | "isolation" | "parentCwd" | "callerRoot" | "worktreeOffered"> | undefined;
23
23
  /** borrowed-readonly — the resume leg, read only to refuse an explicit cwd that a checkpoint workspace
24
24
  * restore would overrule. */
25
25
  resume: Pick<PrepareResume, "workspaceHandle"> | undefined;
@@ -50,5 +50,8 @@ export interface PrepareExecutionEnvResult {
50
50
  /** borrowed-readonly — the INITIAL task root (the workspace-restore phase may re-root it and returns the
51
51
  * settled root under the NEW name `taskRootFinal`). */
52
52
  taskRootInitial: string;
53
+ /** borrowed-readonly — where a CHILD landed against where its caller asked ({@link childPlacementOnLeg}); absent on a leg no
54
+ * delegation caller placed (no `callerRoot`). */
55
+ placement: ChildPlacement | undefined;
53
56
  }
54
57
  export declare function prepareExecutionEnv(input: PrepareExecutionEnvInput): Promise<PrepareExecutionEnvResult>;
@@ -1,7 +1,7 @@
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
+ import { childPlacementOnLeg, deploymentCanRerootChild, worktreeNotHonored } from "./child-tree.js";
5
5
  import { settleTeardownLeg } from "./teardown-bounded.js";
6
6
  export async function prepareExecutionEnv(input) {
7
7
  const { deps, spec, internals, resume, placementRootResolved, sessionId, forgetOnThrow, rollback } = input;
@@ -69,8 +69,15 @@ export async function prepareExecutionEnv(input) {
69
69
  }
70
70
  }
71
71
  const handsEnabled = ownedEnv !== undefined || deps.executionEnv !== undefined;
72
- const taskRootInitial = internals?.isolation === "worktree" || internals?.requestedCwd !== undefined || !deps.rootPath ? executionEnv.cwd : deps.rootPath;
73
- return { executionEnv, ownedEnvSeat, ownedEnvObservedAt, handsEnabled, taskRootInitial };
72
+ const taskRootInitial = internals?.isolation === "worktree" || internals?.requestedCwd !== undefined || internals?.worktreeOffered === true || !deps.rootPath ? executionEnv.cwd : deps.rootPath;
73
+ const placement = internals?.callerRoot === undefined ? undefined : await childPlacementOnLeg(executionEnv, { ...internals, callerRoot: internals.callerRoot }, taskRootInitial);
74
+ if (internals?.worktreeOffered === true && placement === "caller_tree") {
75
+ await settleTeardownLeg(() => forgetOnThrow(), "forgetOnThrow (worktree-not-honored leg)", (err) => deps.onError?.(err, { phase: "config", sessionId }));
76
+ await settleTeardownLeg(() => (ownedEnv && hasDestroy(ownedEnv) ? ownedEnv.destroy() : undefined), "ownedEnv.destroy (worktree-not-honored leg)", (err) => deps.onError?.(err, { phase: "config", sessionId }));
77
+ ownedEnvSeat.current = undefined;
78
+ throw worktreeNotHonored(executionEnv.cwd);
79
+ }
80
+ return { executionEnv, ownedEnvSeat, ownedEnvObservedAt, handsEnabled, taskRootInitial, placement };
74
81
  }
75
82
  function cwdConflictsRestoreError(requestedCwd) {
76
83
  const e = new Error(`RunInternals.requestedCwd ("${requestedCwd}") cannot be combined with a checkpoint workspace restore — ` +
@@ -24,6 +24,7 @@
24
24
  * gate outcome; nothing here changes a word, a source or an order — the stations are moved, not re-adjudicated.
25
25
  */
26
26
  import type { AgentHarness, AgentTool } from "../../internal/harness.js";
27
+ import type { CallTargetStamp } from "../effective-path-target.js";
27
28
  import type { AskClass } from "../ask-class.js";
28
29
  import type { Arbitration, ToolMarkSource } from "../ask-origin.js";
29
30
  import { type PermissionModeController } from "../mode-policy.js";
@@ -152,6 +153,8 @@ export interface PrepareGateStationsInput {
152
153
  /** borrowed-readonly — the hand's live tracked cwd, or undefined; `current` is read per screening / per post seat. Not
153
154
  * written here. */
154
155
  handsCwdRef: CwdRef | undefined;
156
+ /** borrowed-readonly — the leg's target-reading stamp, or undefined (hands-less): the gate stamps its request with it. */
157
+ stampCallTarget: CallTargetStamp | undefined;
155
158
  /** borrowed-readonly — the leg's read-deny table in force, read at a release: the active built-in tiers and the rows
156
159
  * beside them (the deployment's and the task's patterns, the read-face rule rows). Empty ⇒ no table. */
157
160
  readDenyInForce: () => {
@@ -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, 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;
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, stampCallTarget, 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) => {
@@ -120,6 +120,7 @@ export function prepareGateStations(input) {
120
120
  ...(gatePreToolUse !== undefined ? { preToolUse: gatePreToolUse } : {}),
121
121
  hookTimeoutMs,
122
122
  ...(handsCwdRef !== undefined ? { trackedCwd: () => handsCwdRef.current } : {}),
123
+ ...(stampCallTarget !== undefined ? { stampCallTarget } : {}),
123
124
  ...(hookEnvFace !== undefined ? { hookEnv: hookEnvFace } : {}),
124
125
  toolFaceOf: (name) => toolCallFaceOf(toolRosterDeltas.current, name === e.toolName ? called.toolName : name),
125
126
  adjudicate,
@@ -1,6 +1,7 @@
1
1
  import { type BeforeWriteHook, type CwdRef, type PathRuleDenyRow, type ContainmentFace } from "../../tools/fs/index.js";
2
2
  import { type ReadFileState } from "../../tools/fs/safety.js";
3
3
  import { type PlacedReadDenyMatcher } from "../../tools/fs/read-deny-home.js";
4
+ import type { HeadLeads } from "../path-rule-binding.js";
4
5
  import { type ShellReadAdmission, type ShellReadCheck } from "../../tools/fs/shell-read-check.js";
5
6
  import type { ShellReadBoundary } from "../../tools/fs/shell-read-judge.js";
6
7
  import type { RunnerDeps, ShellGateDoctrine, TaskSpec } from "../types.js";
@@ -54,9 +55,12 @@ export interface PrepareHandsReadFaceInput {
54
55
  /** borrowed-readonly — the registry scope (`internals?.registryScope ?? spec.principal ??
55
56
  * "default"`, computed in the driver); the band's task-registry scope. */
56
57
  taskScope: string;
57
- /** borrowed-readonly — the deny tables' permission-rule rows: the path deny rules of the leg's caller policy and of every
58
- * ancestor's (`toolPolicyPathDenyRules`); joined to the read face's matcher and to the shell station's write face. */
59
- pathDenyRules: readonly PathRuleDenyRow[];
58
+ /** borrowed-readonly — the deny tables' permission-rule rows: the face-keyed path deny rules of the leg's caller policy and
59
+ * of every ancestor's, each bound where it was installed (`toolPolicyPathRules`), and the person's deny rows; joined to the
60
+ * read face's matcher and to the shell station's write face. */
61
+ denyRuleRows: readonly PathRuleDenyRow[];
62
+ /** borrowed-readonly — the leg's one resolution of where rule heads lead (the rows' twins, the same the gate's lanes read). */
63
+ headLeads: HeadLeads;
60
64
  /** borrowed-readonly — the two judges a shell command's write targets meet for the mode's file-command placement: the
61
65
  * leg's write-protection judge (read per judgment — it is compiled at the manifest phase, before any gate is mounted)
62
66
  * and the chain's write-target guard (`toolPolicyWriteTargetGuard`). Absent ⇒ neither (a standalone phase drive). */
@@ -93,7 +97,7 @@ export interface PrepareHandsReadFaceInput {
93
97
  * object (R-4: `deps.onError?.()` keeps its receiver; a new deps read must widen this Pick).
94
98
  * `readDenyBuiltinTiers`/`readDenyBuiltinExclude` are SNAPSHOTTED before the first await that
95
99
  * precedes the toolkit compile (the caller retains these arrays by reference). */
96
- deps: Pick<RunnerDeps, "onError" | "onNotice" | "tracer" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "readFace" | "writeFace" | "permissionRuleStore" | "hands">;
100
+ deps: Pick<RunnerDeps, "onError" | "onNotice" | "tracer" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "agentConfigDir" | "readFace" | "writeFace" | "permissionRuleStore" | "hands">;
97
101
  /** borrowed-readonly — the trusted spawn-side seats the band forwards: the finished-command
98
102
  * notification lane, the mid-flight detach hub, and the Runner's per-session
99
103
  * read-state seats (absent on a standalone prepare ⇒ the transcript-replay rung covers alone). */
@@ -2,13 +2,13 @@ import { emitTrace } from "../trace.js";
2
2
  import { StubExecutionEnv } from "../stub-env.js";
3
3
  import { hasBackgroundShell } from "../background-shell.js";
4
4
  import { defaultTaskRegistry } from "../task-registry.js";
5
- import { bashReversibilityProbe, activeReadDenyBuiltinTiers, readDenyBuiltinConfigOf, compileReadDeny, compileWriteDeny, withRuleRowsWhereTheyLead, readDenySeat, createHandsToolkit, deploymentFaceClampNotice, pdfModelCapabilitiesOf, inheritSessionReadFileState, backgroundOutputReadExemption, resolveContainmentFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, } from "../../tools/fs/index.js";
5
+ import { bashReversibilityProbe, activeReadDenyBuiltinTiers, readDenyBuiltinConfigOf, compileReadDeny, compileWriteDeny, rowsWhereTheyLead, readDenySeat, createHandsToolkit, deploymentFaceClampNotice, pdfModelCapabilitiesOf, inheritSessionReadFileState, backgroundOutputReadExemption, resolveContainmentFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, } from "../../tools/fs/index.js";
6
6
  import { resolveKey, sha256 } from "../../tools/fs/safety.js";
7
- import { placeReadDenyAtHome, whereItsAnsweredPrefixLeads } from "../../tools/fs/read-deny-home.js";
7
+ import { placeReadDenyAtHome } from "../../tools/fs/read-deny-home.js";
8
8
  import { createShellOverflowSpoolFence } from "../../tools/fs/fs-shared.js";
9
9
  import { createShellReadStation } from "../../tools/fs/shell-read-check.js";
10
10
  import { composeReadChecks, createFileReadStation } from "../../tools/fs/file-read-check.js";
11
- import { readTargetToolNames } from "../tool-registry.js";
11
+ import { isShellFaceToolName, predictMountedToolNames, readTargetToolNames } from "../tool-registry.js";
12
12
  import { normalizeFileText } from "../../tools/fs/encoding.js";
13
13
  import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
14
14
  import { deliverEngineNotice } from "../types.js";
@@ -203,6 +203,7 @@ export async function prepareHandsMount(input) {
203
203
  const handsIncludeShell = !(executionEnv instanceof StubExecutionEnv);
204
204
  const handsReadOnly = spec.handsReadOnly === true;
205
205
  backgroundTaskToolsActive = handsIncludeShell && !handsReadOnly && hasBackgroundShell(executionEnv);
206
+ const shellFaceHere = [...predictMountedToolNames(new Set(backgroundTaskToolsActive ? ["hands", "backgroundTasks"] : ["hands"]))].filter(isShellFaceToolName);
206
207
  wsSnapshot = resume !== undefined || !(handsIncludeShell && !handsReadOnly) ? undefined : await session.getWorkspaceState().catch(() => undefined);
207
208
  rebaseWsPath = (p) => (wsSnapshot === undefined || wsSnapshot.taskRoot === rootCanonical ? p : rebaseWorkspacePath(p, wsSnapshot.taskRoot, rootCanonical));
208
209
  if (handsIncludeShell && !handsReadOnly) {
@@ -220,8 +221,8 @@ export async function prepareHandsMount(input) {
220
221
  const seedReadFaceSection = resume?.seed.readFace;
221
222
  const readDenyAdditions = [...(deps.readDenyPatterns ?? []), ...(spec.readDenyPatterns ?? []), ...(seedReadFaceSection?.denyEntries ?? [])];
222
223
  const readDenyBuiltinCfg = readDenyBuiltinConfigOf({ tiers: deps.readDenyBuiltinTiers, exclude: deps.readDenyBuiltinExclude });
223
- const pathDenyRules = await withRuleRowsWhereTheyLead(input.pathDenyRules, (head) => whereItsAnsweredPrefixLeads(executionEnv, head));
224
- const readDenyPlacement = await placeReadDenyAtHome(executionEnv, (homes, linked) => compileReadDeny(readDenyAdditions, "readDenyPatterns", readDenyBuiltinCfg, pathDenyRules, homes, linked));
224
+ const denyRuleRows = rowsWhereTheyLead(input.denyRuleRows, await input.headLeads.resolve(input.denyRuleRows));
225
+ const readDenyPlacement = await placeReadDenyAtHome(executionEnv, (homes, linked) => compileReadDeny(readDenyAdditions, "readDenyPatterns", readDenyBuiltinCfg, denyRuleRows, homes, linked), undefined, undefined, deps.agentConfigDir);
225
226
  readDenyMatcher = readDenyPlacement.matcher;
226
227
  readDenyBuiltinTiersActive = activeReadDenyBuiltinTiers(readDenyBuiltinCfg);
227
228
  readDenyAdditionsNormalized = compileReadDeny(readDenyAdditions, "readDenyPatterns", { tiers: [] }).entries;
@@ -291,7 +292,7 @@ export async function prepareHandsMount(input) {
291
292
  }
292
293
  : undefined;
293
294
  const denySeat = readDenySeat(readDenyMatcher);
294
- const writeDeny = compileWriteDeny(pathDenyRules);
295
+ const writeDeny = compileWriteDeny(denyRuleRows);
295
296
  const writeDenySeat = writeDeny.ruleRows.length > 0 ? { writeDenyMatch: (p) => writeDeny.matchPath(p)?.pattern ?? null } : {};
296
297
  const shellReadBoundary = () => ({
297
298
  roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
@@ -307,7 +308,7 @@ export async function prepareHandsMount(input) {
307
308
  if (handsIncludeShell) {
308
309
  const spoolFence = handsReadOnly ? createShellOverflowSpoolFence(executionEnv) : undefined;
309
310
  const readonlyBoundary = () => spoolFence !== undefined ? { roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical], cwd: rootCanonical, ...denySeat, ...writeDenySeat, alsoInside: spoolFence } : undefined;
310
- shellReadTools = new Set(handsReadOnly ? ["Bash"] : backgroundTaskToolsActive ? ["Bash", "Monitor"] : ["Bash"]);
311
+ shellReadTools = new Set(shellFaceHere);
311
312
  shellReadStation = createShellReadStation({
312
313
  env: executionEnv,
313
314
  tools: shellReadTools,
@@ -334,7 +335,7 @@ export async function prepareHandsMount(input) {
334
335
  ...(additionalRootsCanonical.length > 0 ? { additionalRoots: additionalRootsCanonical } : {}),
335
336
  additionalReadRoots: additionalReadRootsCanonical,
336
337
  ...(readDenyAdditions.length > 0 ? { readDenyPatterns: readDenyAdditions } : {}),
337
- ...(pathDenyRules.length > 0 ? { pathDenyRules } : {}),
338
+ ...(denyRuleRows.length > 0 ? { pathDenyRules: denyRuleRows } : {}),
338
339
  readDenyHomes: readDenyPlacement.homes,
339
340
  readDenyHomeLinks: readDenyPlacement.linked,
340
341
  ...(readDenyBuiltinCfg.tiers !== undefined ? { readDenyBuiltinTiers: readDenyBuiltinCfg.tiers } : {}),
@@ -400,9 +401,8 @@ export async function prepareHandsMount(input) {
400
401
  if (seatOwned)
401
402
  shellGatedTools.add(name);
402
403
  };
403
- installShellSeat("Bash");
404
- if (backgroundTaskToolsActive)
405
- installShellSeat("Monitor");
404
+ for (const name of shellFaceHere)
405
+ installShellSeat(name);
406
406
  }
407
407
  }
408
408
  return {