@sema-agent/core 7.45.0 → 7.46.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 (127) hide show
  1. package/CHANGELOG.md +246 -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 +5 -3
  11. package/dist/core/allow-entry.js +3 -1
  12. package/dist/core/ask-origin.js +1 -1
  13. package/dist/core/checkpoint-store.d.ts +39 -2
  14. package/dist/core/effective-path-target.d.ts +136 -4
  15. package/dist/core/effective-path-target.js +110 -2
  16. package/dist/core/engine-notice.d.ts +5 -0
  17. package/dist/core/gate-fold.js +3 -0
  18. package/dist/core/gate-lanes.js +9 -3
  19. package/dist/core/governance-codes.d.ts +3 -1
  20. package/dist/core/governance-codes.js +3 -0
  21. package/dist/core/hooks.d.ts +3 -0
  22. package/dist/core/path-rule-binding.d.ts +65 -0
  23. package/dist/core/path-rule-binding.js +99 -0
  24. package/dist/core/permission-rule-model.d.ts +47 -18
  25. package/dist/core/permission-rule-model.js +38 -42
  26. package/dist/core/permission-rule-provider.d.ts +7 -0
  27. package/dist/core/permission-rule-provider.js +10 -3
  28. package/dist/core/permission-rule-store.d.ts +8 -0
  29. package/dist/core/permission-rule-store.js +6 -2
  30. package/dist/core/permission-rule-sync.js +2 -2
  31. package/dist/core/permission-rules.d.ts +22 -40
  32. package/dist/core/permission-rules.js +50 -43
  33. package/dist/core/runner/child-tree.d.ts +97 -0
  34. package/dist/core/runner/child-tree.js +104 -0
  35. package/dist/core/runner/construction-doors.d.ts +5 -0
  36. package/dist/core/runner/construction-doors.js +6 -0
  37. package/dist/core/runner/contracts.d.ts +46 -0
  38. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  39. package/dist/core/runner/permission-rule-lanes.js +18 -12
  40. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  41. package/dist/core/runner/persisted-path-rows.js +31 -12
  42. package/dist/core/runner/prepare-ask-lane.d.ts +4 -0
  43. package/dist/core/runner/prepare-ask-lane.js +4 -2
  44. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  45. package/dist/core/runner/prepare-execution-env.js +10 -3
  46. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  47. package/dist/core/runner/prepare-gate-stations.js +2 -1
  48. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  49. package/dist/core/runner/prepare-hands-readface.js +6 -6
  50. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  51. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  52. package/dist/core/runner/prepare-park-ask.d.ts +3 -0
  53. package/dist/core/runner/prepare-park-ask.js +5 -3
  54. package/dist/core/runner/prepare-policy-chain.d.ts +14 -2
  55. package/dist/core/runner/prepare-policy-chain.js +11 -4
  56. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  57. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  58. package/dist/core/runner/prepare-task.js +37 -18
  59. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  60. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  61. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  62. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  63. package/dist/core/runner/resume-apply.js +17 -3
  64. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  65. package/dist/core/runner/run-leg.js +5 -1
  66. package/dist/core/runner/runtask.js +2 -2
  67. package/dist/core/runner/sub-agent-deny-lists.d.ts +1 -1
  68. package/dist/core/runner/write-protection-station.d.ts +3 -2
  69. package/dist/core/runner/write-protection-station.js +1 -0
  70. package/dist/core/runner-deps.d.ts +14 -0
  71. package/dist/core/sensitive-path-policy.d.ts +8 -0
  72. package/dist/core/sensitive-path-policy.js +18 -2
  73. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  74. package/dist/core/shell-wrapper-projection.js +56 -0
  75. package/dist/core/shell-wrapper-table.d.ts +144 -34
  76. package/dist/core/shell-wrapper-table.js +40 -20
  77. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  78. package/dist/core/synthetic-read-verdict.js +3 -1
  79. package/dist/core/tool-catalog-entries.d.ts +21 -0
  80. package/dist/core/tool-catalog-entries.js +9 -0
  81. package/dist/core/tool-policy.d.ts +30 -8
  82. package/dist/core/tool-policy.js +9 -7
  83. package/dist/core/tool-spec.d.ts +3 -2
  84. package/dist/core/write-protect.d.ts +4 -0
  85. package/dist/core/write-protect.js +30 -9
  86. package/dist/index.d.ts +5 -3
  87. package/dist/index.js +4 -2
  88. package/dist/orchestration/run-spec.js +8 -7
  89. package/dist/orchestration/workflow.js +1 -1
  90. package/dist/shell/lex.d.ts +7 -0
  91. package/dist/shell/lex.js +17 -0
  92. package/dist/tools/fs/bash-program-position.d.ts +8 -87
  93. package/dist/tools/fs/bash-program-position.js +33 -69
  94. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  95. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  96. package/dist/tools/fs/index.d.ts +3 -2
  97. package/dist/tools/fs/program-read-model.d.ts +694 -0
  98. package/dist/tools/fs/program-read-model.js +1252 -0
  99. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  100. package/dist/tools/fs/program-read-model.rows.js +613 -0
  101. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  102. package/dist/tools/fs/read-deny-home.js +62 -24
  103. package/dist/tools/fs/read-deny.d.ts +54 -14
  104. package/dist/tools/fs/read-deny.js +104 -50
  105. package/dist/tools/fs/removal-targets.d.ts +7 -6
  106. package/dist/tools/fs/removal-targets.js +22 -22
  107. package/dist/tools/fs/safety.d.ts +3 -0
  108. package/dist/tools/fs/safety.js +10 -3
  109. package/dist/tools/fs/shell-read-argv.d.ts +85 -0
  110. package/dist/tools/fs/shell-read-argv.js +386 -0
  111. package/dist/tools/fs/shell-read-bindings.d.ts +273 -0
  112. package/dist/tools/fs/shell-read-bindings.js +746 -0
  113. package/dist/tools/fs/shell-read-check.js +23 -15
  114. package/dist/tools/fs/shell-read-establish.js +79 -5
  115. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  116. package/dist/tools/fs/shell-read-judge.js +9 -1
  117. package/dist/tools/fs/shell-read-plan.d.ts +31 -7
  118. package/dist/tools/fs/shell-read-plan.js +299 -393
  119. package/dist/tools/fs/shell-read-word.d.ts +29 -26
  120. package/dist/tools/fs/shell-read-word.js +30 -38
  121. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -64
  122. package/dist/tools/fs/shell-read-write-targets.js +83 -136
  123. package/dist/tools/fs/shell-reentry-text.d.ts +38 -6
  124. package/dist/tools/fs/shell-reentry-text.js +90 -53
  125. package/package.json +1 -1
  126. package/test/export-surface.snapshot.json +17 -3
  127. package/wire-changes/core@7.46.0.json +1522 -0
@@ -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,9 +2,9 @@ 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";
@@ -220,8 +220,8 @@ export async function prepareHandsMount(input) {
220
220
  const seedReadFaceSection = resume?.seed.readFace;
221
221
  const readDenyAdditions = [...(deps.readDenyPatterns ?? []), ...(spec.readDenyPatterns ?? []), ...(seedReadFaceSection?.denyEntries ?? [])];
222
222
  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));
223
+ const denyRuleRows = rowsWhereTheyLead(input.denyRuleRows, await input.headLeads.resolve(input.denyRuleRows));
224
+ const readDenyPlacement = await placeReadDenyAtHome(executionEnv, (homes, linked) => compileReadDeny(readDenyAdditions, "readDenyPatterns", readDenyBuiltinCfg, denyRuleRows, homes, linked), undefined, undefined, deps.agentConfigDir);
225
225
  readDenyMatcher = readDenyPlacement.matcher;
226
226
  readDenyBuiltinTiersActive = activeReadDenyBuiltinTiers(readDenyBuiltinCfg);
227
227
  readDenyAdditionsNormalized = compileReadDeny(readDenyAdditions, "readDenyPatterns", { tiers: [] }).entries;
@@ -291,7 +291,7 @@ export async function prepareHandsMount(input) {
291
291
  }
292
292
  : undefined;
293
293
  const denySeat = readDenySeat(readDenyMatcher);
294
- const writeDeny = compileWriteDeny(pathDenyRules);
294
+ const writeDeny = compileWriteDeny(denyRuleRows);
295
295
  const writeDenySeat = writeDeny.ruleRows.length > 0 ? { writeDenyMatch: (p) => writeDeny.matchPath(p)?.pattern ?? null } : {};
296
296
  const shellReadBoundary = () => ({
297
297
  roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
@@ -334,7 +334,7 @@ export async function prepareHandsMount(input) {
334
334
  ...(additionalRootsCanonical.length > 0 ? { additionalRoots: additionalRootsCanonical } : {}),
335
335
  additionalReadRoots: additionalReadRootsCanonical,
336
336
  ...(readDenyAdditions.length > 0 ? { readDenyPatterns: readDenyAdditions } : {}),
337
- ...(pathDenyRules.length > 0 ? { pathDenyRules } : {}),
337
+ ...(denyRuleRows.length > 0 ? { pathDenyRules: denyRuleRows } : {}),
338
338
  readDenyHomes: readDenyPlacement.homes,
339
339
  readDenyHomeLinks: readDenyPlacement.linked,
340
340
  ...(readDenyBuiltinCfg.tiers !== undefined ? { readDenyBuiltinTiers: readDenyBuiltinCfg.tiers } : {}),
@@ -43,7 +43,8 @@ import type { OwnOrgAdmissionVerdict } from "../memory-admission.js";
43
43
  import type { DelegationContentSafety } from "../memory-engine/delegation-provenance.js";
44
44
  import { type SessionPermissionRules } from "../session-policy-store.js";
45
45
  import type { SessionStore } from "../session.js";
46
- import { type OnAsk } from "../tool-policy.js";
46
+ import { type OnAsk, type ToolPolicy } from "../tool-policy.js";
47
+ import type { PathRuleBases } from "../permission-rule-model.js";
47
48
  import type { PermissionMode, RunnerDeps, RuntimeCaps, ShellGateDoctrine, TaskEvent, TaskSpec, ToolExecuteContext, ToolSpec } from "../types.js";
48
49
  import { PermissionModeController } from "../mode-policy.js";
49
50
  import { type SkillGrantTable } from "../skill-grant.js";
@@ -89,6 +90,12 @@ export interface PrepareInheritedGateInput {
89
90
  ownedEnv: ExecutionEnv | undefined;
90
91
  /** borrowed-readonly — the frozen preflight; only `toolPolicy` (the RESOLVED caller policy the children's chain carries). */
91
92
  lockedPreflight: Pick<LockedPreflight, "toolPolicy">;
93
+ /** borrowed-readonly — the chain's top leg's anchors as this leg reads them (handed on to children), and how this leg
94
+ * installs the policies its chain hands it (`chainPolicyOnLeg`). */
95
+ pathRuleAnchors: {
96
+ readonly chainTop: PathRuleBases;
97
+ readonly onLeg: (policy: ToolPolicy, position: number) => ToolPolicy;
98
+ };
92
99
  /** borrowed-readonly — the frozen tool-face snapshot (the ctx forwards exclude / defer / alwaysLoad / restoreGated; the
93
100
  * full-shell predicate reads the exclusion of Bash). */
94
101
  toolFaceSnapshot: ToolFaceSnapshot;
@@ -5,6 +5,7 @@ import { createPreToolUseConstraintPolicy } from "../hooks.js";
5
5
  import { continuesLineage } from "../session-policy-store.js";
6
6
  import { StubExecutionEnv } from "../stub-env.js";
7
7
  import { askApproverIdentity, durableMandateSourceOf } from "../tool-policy.js";
8
+ import { chainAnchorRecords } from "./child-tree.js";
8
9
  import { deliverEngineNotice } from "../types.js";
9
10
  import { PermissionModeController, modeRequestAfterResume } from "../mode-policy.js";
10
11
  import { SessionModeSeats } from "./session-mode-seats.js";
@@ -13,7 +14,7 @@ import { frozenSkillGrants } from "../skill-grant.js";
13
14
  import { attachRebuiltDenialTrackers, createDenialLimitStop } from "./denial-limit-arms.js";
14
15
  import { asThrownError } from "../thrown-value.js";
15
16
  export function prepareInheritedGate(input) {
16
- const { spec, deps, askSeat, internals, resume, sessions, sessionId, runId, hostTaskId, taskRootFinal, executionEnv, ownedEnv, lockedPreflight, toolFaceSnapshot, mountsCallerTool, promptProfile, resolvedInteractionPosture, permissionModeSeat, handsEnabled, reportUsage, harnessRef, skillGrants, forwardEvent, subagentRetain, worktreeIsolation, requestReview, requestStopAfterTurn, enterPlanMode, reminderMark, reminderDisclosureCounts, fileHistoryEnabled, historyScope, historyRoot, historyFs, abortRun } = input;
17
+ const { spec, deps, askSeat, internals, resume, sessions, sessionId, runId, hostTaskId, taskRootFinal, executionEnv, ownedEnv, lockedPreflight, pathRuleAnchors, toolFaceSnapshot, mountsCallerTool, promptProfile, resolvedInteractionPosture, permissionModeSeat, handsEnabled, reportUsage, harnessRef, skillGrants, forwardEvent, subagentRetain, worktreeIsolation, requestReview, requestStopAfterTurn, enterPlanMode, reminderMark, reminderDisclosureCounts, fileHistoryEnabled, historyScope, historyRoot, historyFs, abortRun } = input;
17
18
  const liveInheritedGate = internals?.inheritedGate;
18
19
  const seedInheritedGate = resume?.seed.inheritedGate;
19
20
  const inheritedAncestorRules = (() => {
@@ -35,7 +36,11 @@ export function prepareInheritedGate(input) {
35
36
  })();
36
37
  const modeSeed = recordedModeSeed(seedInheritedGate);
37
38
  const inheritedShellGate = liveInheritedGate?.shellGate ?? modeSeed.shellGate;
38
- const chainView = askSeat.reach === "none" ? liveInheritedGate?.parentConstraints?.map((pc) => (typeof pc.onAsk === "function" ? ((({ onAsk: _person, ...rest }) => rest)(pc)) : pc)) : liveInheritedGate?.parentConstraints;
39
+ const askView = askSeat.reach === "none" ? liveInheritedGate?.parentConstraints?.map((pc) => (typeof pc.onAsk === "function" ? ((({ onAsk: _person, ...rest }) => rest)(pc)) : pc)) : liveInheritedGate?.parentConstraints;
40
+ const chainView = askView?.map((pc, position) => {
41
+ const policy = pathRuleAnchors.onLeg(pc.policy, position);
42
+ return policy === pc.policy ? pc : { ...pc, policy };
43
+ });
39
44
  const inheritedParentConstraints = attachRebuiltDenialTrackers(chainView, deps.autoMode?.denialLimit);
40
45
  const parentSeat = liveInheritedGate?.permissionModeCeiling;
41
46
  const ceiling = parentSeat ?? liveInheritedGate?.permissionMode;
@@ -186,6 +191,7 @@ export function prepareInheritedGate(input) {
186
191
  permissionMode: permissionMode.current,
187
192
  permissionModeCeiling: legModeCeiling,
188
193
  ...(parentConstraints.length > 0 ? { parentConstraints } : {}),
194
+ ...chainAnchorRecords(pathRuleAnchors.chainTop, parentConstraints),
189
195
  admittedOrgScopes: memoryAdmittedOrgScopesRef.current,
190
196
  ...(orgGovernedProvenance ? { orgAdmissionGoverned: true } : {}),
191
197
  };
@@ -20,6 +20,7 @@
20
20
  * closures, so the orchestrator grows no branch.
21
21
  */
22
22
  import type { AgentHarness, AgentMessage, AgentTool, ExecutionEnv } from "../../internal/harness.js";
23
+ import type { CallTargetStamp } from "../effective-path-target.js";
23
24
  import { type OnQuestion } from "../ask-question.js";
24
25
  import { type CheckpointStore } from "../checkpoint-store.js";
25
26
  import type { GrantBindingOf } from "../skill-grant-binding.js";
@@ -96,6 +97,8 @@ export interface PrepareParkAskInput {
96
97
  handsCwdRef: CwdRef | undefined;
97
98
  /** borrowed-readonly — the root the live gate stamps beside the cwd ({@link import("./prepare-ask-lane.js").PrepareAskLaneInput.handsRoot}). */
98
99
  handsRoot: string | undefined;
100
+ /** borrowed-readonly — the live gate's target-reading stamp ({@link import("./prepare-ask-lane.js").PrepareAskLaneInput.stampCallTarget}). */
101
+ stampCallTarget: CallTargetStamp | undefined;
99
102
  /** borrowed-readonly — the SAME seat the live gate stamps from ({@link import("./prepare-ask-lane.js").PrepareAskLaneInput.advertisedWritableDirs}): a parked call's projection re-adjudication asks one question about one call, so it must ask it against the same admitted list. Read at park time, not written here. */
100
103
  advertisedWritableDirs: Pick<AdvertisedWritableDirs, "roots"> | undefined;
101
104
  /** borrowed-readonly — the run's offload store, or undefined (a volatile store holding results refuses the park). */
@@ -27,7 +27,7 @@ const wordlessDemand = (toolName) => {
27
27
  throw new Error(`a real-approval demand for "${toolName}" reached the park with no origin word — the origin stamp precedes every park`);
28
28
  };
29
29
  export function prepareParkAsk(input) {
30
- const { askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, handsRoot, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, irreversibilityTier, toolMarkSources, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, grantBindingOf, now, humanReviewRef, abortController, harness, pausedRef } = input;
30
+ const { askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, handsRoot, stampCallTarget, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, irreversibilityTier, toolMarkSources, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, grantBindingOf, now, humanReviewRef, abortController, harness, pausedRef } = input;
31
31
  if (askLane === undefined || saga === undefined)
32
32
  return { parkAsk: undefined };
33
33
  const { composedCallSignal, approvalPreviewOf } = askLane;
@@ -159,7 +159,7 @@ export function prepareParkAsk(input) {
159
159
  if (!presented.ok) {
160
160
  throw new ParkRefusal(`the stored form of "${req.toolName}"'s arguments could not be presented for re-adjudication (${describeThrown(presented.cause)})`, { cause: presented.cause });
161
161
  }
162
- const reprojectedRaced = await raceSettlementAgainstSignal(Promise.resolve(basePolicyForResumeEdit.check({
162
+ const reprojectedReq = () => ({
163
163
  toolName: req.toolName,
164
164
  args: presented.value,
165
165
  toolCallId: req.toolCallId,
@@ -167,7 +167,9 @@ export function prepareParkAsk(input) {
167
167
  ...callFaceSeat(req.face),
168
168
  ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}), ...(handsRoot !== undefined ? { root: handsRoot } : {}),
169
169
  ...advertisedWritableRootsSeat(advertisedWritableDirs),
170
- }, cutSignal)), cutSignal);
170
+ });
171
+ const withReading = stampCallTarget?.(reprojectedReq(), cutSignal);
172
+ const reprojectedRaced = await raceSettlementAgainstSignal(withReading === undefined ? Promise.resolve(basePolicyForResumeEdit.check(reprojectedReq(), cutSignal)) : withReading.then((r) => basePolicyForResumeEdit.check(r, cutSignal)), cutSignal);
171
173
  if (reprojectedRaced.tag === "aborted")
172
174
  return undefined;
173
175
  if (reprojectedRaced.tag === "threw")
@@ -27,6 +27,8 @@ import { type Hooks } from "../hooks.js";
27
27
  import type { LockedPreflight } from "../locked-config.js";
28
28
  import type { SessionPermissionRules } from "../session-policy-store.js";
29
29
  import { type OnAsk, type ToolPolicy } from "../tool-policy.js";
30
+ import type { PathRuleBases } from "../permission-rule-model.js";
31
+ import type { HeadLeads } from "../path-rule-binding.js";
30
32
  import type { RunnerDeps, RuntimeCaps, TaskSpec, ToolEffect } from "../types.js";
31
33
  import type { CwdRef } from "../../tools/fs/fs-shared.js";
32
34
  import type { ReadDenyMatcher } from "../../tools/fs/read-deny.js";
@@ -65,9 +67,17 @@ export interface PreparePolicyChainInput {
65
67
  additionalRootsCanonical: readonly string[];
66
68
  /** borrowed-readonly — the run's resolved env (path rules resolve in it; the sandbox declaration is read off it). */
67
69
  executionEnv: ExecutionEnv;
68
- /** borrowed-readonly — the frozen preflight: `toolPolicy` (the caller slot) and `basePolicyForResumeEdit` (the lock-aware
69
- * resume-edit override). */
70
+ /** borrowed-readonly — the frozen preflight as installed on this leg: `toolPolicy` (the caller slot) and
71
+ * `basePolicyForResumeEdit` (the lock-aware resume-edit override), their path rules anchored where this leg installed them. */
70
72
  lockedPreflight: Pick<LockedPreflight, "toolPolicy" | "basePolicyForResumeEdit">;
73
+ /** borrowed-readonly — the path rules' anchors on this leg: `leg` (this leg's root and environment home — where this
74
+ * leg's own lists are installed) and `chainTop` (the top leg's: the person's global / session rows and every `~/`). */
75
+ pathRuleAnchors: {
76
+ readonly leg: PathRuleBases;
77
+ readonly chainTop: PathRuleBases;
78
+ };
79
+ /** borrowed-readonly — the leg's one resolution of where rule heads lead (the rows' twins). */
80
+ headLeads: HeadLeads;
71
81
  /** borrowed-readonly — the run's ONE tool roster, read only here (the name audit's universe: names + aliases). */
72
82
  tools: ReadonlyArray<AgentTool>;
73
83
  /** borrowed-readonly — the harness list (placeholders keep their deferred names), the audit universe's second half. */
@@ -96,6 +106,8 @@ export interface PreparePolicyChainInput {
96
106
  readDenyMatcher: ReadDenyMatcher | undefined;
97
107
  /** borrowed-readonly — the run's one notice for a person's rule store that could not be read (`ruleStoreUnreadableNotice`). */
98
108
  announceRuleStoreSkipped: (at: "prepare" | "call") => void;
109
+ /** borrowed-readonly — the run's one notice for rows the person's store served that were left out (`ruleRowsDroppedNotice`). */
110
+ announceRuleRowsDropped: (rows: import("../permission-rule-provider.js").EffectivePermissionRules["droppedRows"]) => void;
99
111
  /** borrowed-readonly — whether the question tool mounted (the lanes' question-tool exclusion). */
100
112
  questionToolMounted: boolean;
101
113
  /** borrowed-readonly — the resolved per-principal entitlements; only `forceDurableGate` (the mandate-park facility and