@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,10 +2,10 @@ import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
2
2
  import { splitNamespacedName } from "./tool-name-aliases.js";
3
3
  import { catalogRuleFaceOf, pathTargetOf } from "./tool-registry.js";
4
4
  import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule } from "./permission-rule-syntax.js";
5
- import { NAME_RULE_FACES, PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, nameRuleAdmits, nameRuleSubjectOf, parseRuleText, pathRuleHeadOf, pathRuleReachOf, programRunReachOf, ruleBasesNeeded, ruleToolGrammarOf } from "./permission-rule-model.js";
6
- import { pathFamilyOf, pathIdentityOf, spellingOfIdentity } from "../tools/fs/safety.js";
7
- import { effectivePathTargetOf } from "./effective-path-target.js";
8
- import { protectivePathTargetOf } from "./tool-registry.js";
5
+ import { NAME_RULE_FACES, PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, nameRuleAdmits, nameRuleSubjectOf, parseRuleText, programRunReachOf, ruleBasesNeeded, ruleToolGrammarOf } from "./permission-rule-model.js";
6
+ import { pathFamilyOf } from "../tools/fs/safety.js";
7
+ import { callTargetReadingOf, callTargetViewsOf } from "./effective-path-target.js";
8
+ import { bindPathRules, pathRowsReach, policyAnchors, rowsWhereTheyLead } from "./path-rule-binding.js";
9
9
  import { ruleDenialSentence } from "./rule-denial-sentence.js";
10
10
  import { heldValues } from "./tool-name-readings.js";
11
11
  export { parsePermissionRule };
@@ -93,22 +93,6 @@ export const fileToolParamVocabulary = (canonicalTool) => {
93
93
  const face = catalogRuleFaceOf(canonicalTool);
94
94
  return face?.params === undefined || pathTargetOf(canonicalTool) === undefined ? undefined : new Set(face.params);
95
95
  };
96
- export function pathRuleDenyRowOf(rule, bases) {
97
- const family = pathFamilyOf(bases);
98
- const tree = bases.root ?? bases.cwd;
99
- const head = pathRuleHeadOf(rule.parsed, bases);
100
- return {
101
- rule: rule.ruleText,
102
- tool: rule.parsed.tool,
103
- pattern: parsePermissionRule(rule.ruleText).ruleContent ?? "",
104
- faces: rule.faces,
105
- reaches: (absolutePath) => {
106
- const id = pathIdentityOf(absolutePath, family, tree);
107
- return id.ok && pathRuleReachOf(rule.parsed, id.identity, bases).reach !== "not_reached";
108
- },
109
- ...(head !== undefined ? { head: spellingOfIdentity(head, family) } : {}),
110
- };
111
- }
112
96
  function countStars(s) {
113
97
  let n = 0;
114
98
  for (const c of s)
@@ -233,9 +217,9 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
233
217
  continue;
234
218
  }
235
219
  for (const base of ruleBasesNeeded(parsedPath.rule)) {
236
- const value = base === "cwd" ? (pathBases.cwd ?? pathBases.root) : pathBases[base];
237
- if (!isUsablePathBase(value, pathFamilyOf(pathBases))) {
238
- bad(text, "unsupported.path_base", `"${text}" is relative to ${PATH_RULE_BASE_LABEL[base]}, and this policy was given no absolute \`pathBases.${base}\`${base === "cwd" ? " (or `root`)" : ""} to resolve it against — the rule could not be judged on any call`);
220
+ const declared = base === "cwd" ? (pathBases.cwd ?? pathBases.root) : pathBases[base];
221
+ if (declared !== undefined && !isUsablePathBase(declared, pathFamilyOf(pathBases))) {
222
+ bad(text, "unsupported.path_base", `"${text}" is relative to ${PATH_RULE_BASE_LABEL[base]}, and the \`pathBases.${base === "cwd" && pathBases.cwd === undefined ? "root" : base}\` this policy declares is not an absolute path of its tree's family — the rule could not be judged on any call`);
239
223
  }
240
224
  }
241
225
  if (issues.length > 0 && issues[issues.length - 1].rule === text && issues[issues.length - 1].code === "unsupported.path_base")
@@ -315,6 +299,7 @@ function ruleDenial(req, ruleText, source) {
315
299
  function ruleAskMessage(ruleText, source) {
316
300
  return `flagged by permission rule "${ruleText}"${source ? ` (${source})` : ""}`;
317
301
  }
302
+ const NO_LEADS = new Map();
318
303
  export function createPermissionRulePolicy(rules, opts) {
319
304
  const caps = { ...DEFAULT_CAPS, ...opts?.caps };
320
305
  const pathBases = opts?.pathBases ?? {};
@@ -360,19 +345,55 @@ export function createPermissionRulePolicy(rules, opts) {
360
345
  deny: [...byTool.values()].flatMap((e) => e.content.deny.filter((r) => r.faces !== undefined)),
361
346
  ask: [...byTool.values()].flatMap((e) => e.content.ask.filter((r) => r.faces !== undefined)),
362
347
  };
363
- const pathDenyRules = faceRules.deny.map((r) => pathRuleDenyRowOf({ parsed: r.parsed, ruleText: r.ruleText, faces: r.faces }, pathBases));
364
348
  const contentLaneOf = (behavior, entry, callFace) => [
365
349
  ...(entry?.content[behavior].filter((r) => r.faces === undefined) ?? []),
366
350
  ...(callFace !== undefined ? faceRules[behavior].filter((r) => r.faces.includes(callFace)) : []),
367
351
  ];
368
352
  const commandParam = catalogRuleFaceOf("Bash")?.primaryParams[0] ?? "command";
369
- const policyNamesTree = pathBases.root !== undefined || pathBases.cwd !== undefined;
370
- const callBasesOf = (req) => policyNamesTree ? pathBases : { ...pathBases, ...(typeof req.root === "string" ? { root: req.root } : {}), ...(typeof req.cwd === "string" ? { cwd: req.cwd } : {}) };
353
+ const pathRuleOf = new Map();
354
+ for (const e of byTool.values()) {
355
+ for (const behavior of ["deny", "ask"]) {
356
+ for (const r of e.content[behavior]) {
357
+ if (r.parsed.match !== "path" && r.parsed.match !== "subpath")
358
+ continue;
359
+ pathRuleOf.set(r, { parsed: r.parsed, ruleText: r.ruleText, ...(behavior === "deny" && r.faces !== undefined ? { faces: r.faces } : {}) });
360
+ }
361
+ }
362
+ }
363
+ const pathSpecs = [...pathRuleOf.values()];
364
+ const bound = new Map();
365
+ const boundAt = (anchors) => {
366
+ const key = JSON.stringify([anchors.root ?? null, anchors.cwd ?? null, anchors.home ?? null]);
367
+ const prior = bound.get(key);
368
+ if (prior !== undefined)
369
+ return prior;
370
+ const rows = bindPathRules(pathSpecs, anchors);
371
+ const of = new Map();
372
+ [...pathRuleOf.keys()].forEach((r, i) => of.set(r, rows[i]));
373
+ const out = { rows, of };
374
+ bound.set(key, out);
375
+ return out;
376
+ };
371
377
  const contentHit = (lane, req) => {
372
378
  if (lane.length === 0)
373
379
  return undefined;
374
- const bases = callBasesOf(req);
375
- const target = effectivePathTargetOf(req, protectivePathTargetOf(req), bases);
380
+ const pathLane = lane.filter((r) => pathRuleOf.has(r));
381
+ if (pathLane.length > 0) {
382
+ const views = callTargetViewsOf(req, "tighten", { ...(pathBases.root !== undefined ? { root: pathBases.root } : {}), ...(pathBases.cwd !== undefined ? { cwd: pathBases.cwd } : {}) });
383
+ if (views !== undefined) {
384
+ const { of } = boundAt(policyAnchors(pathBases, req.pathRuleAnchors));
385
+ const rows = rowsWhereTheyLead(pathLane.map((r) => of.get(r)), callTargetReadingOf(req)?.leads ?? NO_LEADS);
386
+ const hit = pathRowsReach(rows, views, "tighten");
387
+ const rule = hit === undefined ? undefined : pathLane.find((r) => of.get(r).spec === hit.row.spec);
388
+ if (hit !== undefined && hit.unreadable === undefined)
389
+ return { rule: rule };
390
+ if (hit !== undefined)
391
+ return judgeOthers(lane.filter((r) => !pathRuleOf.has(r)), req) ?? { rule: rule, unreadable: hit.unreadable };
392
+ }
393
+ }
394
+ return judgeOthers(lane.filter((r) => !pathRuleOf.has(r)), req);
395
+ };
396
+ const judgeOthers = (lane, req) => {
376
397
  const command = isPlainObjectArgs(req.args) ? req.args[commandParam] : undefined;
377
398
  let unreadable;
378
399
  for (const r of lane) {
@@ -382,20 +403,6 @@ export function createPermissionRulePolicy(rules, opts) {
382
403
  return { rule: r };
383
404
  continue;
384
405
  }
385
- if (r.parsed.match === "subpath" || r.parsed.match === "path") {
386
- if (target === undefined || target.kind === "refused")
387
- continue;
388
- if (target.kind === "unreadable") {
389
- unreadable ??= { rule: r, unreadable: target.reason };
390
- continue;
391
- }
392
- const outcome = pathRuleReachOf(r.parsed, target.identity, bases);
393
- if (outcome.reach === "reached")
394
- return { rule: r };
395
- if (outcome.reach === "unreadable")
396
- unreadable ??= { rule: r, unreadable: outcome.reason };
397
- continue;
398
- }
399
406
  if (typeof command !== "string")
400
407
  continue;
401
408
  const outcome = programRunReachOf(r.parsed, command);
@@ -430,7 +437,7 @@ export function createPermissionRulePolicy(rules, opts) {
430
437
  };
431
438
  return {
432
439
  nameSets: [nameSets],
433
- ...(pathDenyRules.length > 0 ? { pathDenyRules } : {}),
440
+ ...(pathSpecs.length > 0 ? { pathRules: (anchors) => boundAt(policyAnchors(pathBases, anchors)).rows } : {}),
434
441
  check(req) {
435
442
  const entry = entryFor(req);
436
443
  const covering = hasCovering && protocolOf(req.toolName) !== undefined;
@@ -1,4 +1,9 @@
1
1
  import type { RunnerDeps } from "../types.js";
2
+ import type { ExecutionEnv } from "../../internal/harness.js";
3
+ import type { PathRuleBases } from "../permission-rule-model.js";
4
+ import type { ChildPlacement, InheritedGate, RunInternals } from "./contracts.js";
5
+ import type { ToolPolicy } from "../tool-policy.js";
6
+ import { CheckpointError } from "../checkpoint-store.js";
2
7
  /**
3
8
  * Whether this deployment can place a child agent in a tree OTHER than the caller's — the one fact behind
4
9
  * every request of that shape: an explicit `cwd`, a workflow agent's `isolation: "worktree"`, and the
@@ -11,3 +16,95 @@ import type { RunnerDeps } from "../types.js";
11
16
  * keeps its own code and sentence for the refusal, but none decides the condition itself.
12
17
  */
13
18
  export declare function deploymentCanRerootChild(deps: Pick<RunnerDeps, "executionEnvFactory">): boolean;
19
+ /**
20
+ * Where a child the caller asked to place at `asked` actually landed (`childRoot`, the root its environment gave it) — the
21
+ * ONE judgment every reader of that fact takes
22
+ * (the delegation tool's worktree request, and the anchors of the rules the child inherits):
23
+ * · `honored` — the environment's root is where the child was asked to be (canonical on both sides);
24
+ * · `caller_tree` — it is not, and it lies inside the caller's own tree (`callerRoot`): the same filesystem, the wrong
25
+ * directory — a worktree request then fails (the child would write the caller's checkout);
26
+ * · `elsewhere` — neither: another filesystem namespace (a sandbox the factory minted). The child runs there, it is not
27
+ * in the worktree it was offered, and it takes the caller's path rules again at its own anchors.
28
+ * A path the environment cannot canonicalize is compared as spelled. Residual: two namespaces that hold the same path
29
+ * string, with a factory claiming to honor it, read `honored`.
30
+ */
31
+ export declare function childPlacementOf(env: Pick<ExecutionEnv, "canonicalPath">, childRoot: string, asked: string | undefined, callerRoot: string): Promise<ChildPlacement>;
32
+ /** A delegated leg's placement: the place asked for is the hint (a minted worktree, an explicit cwd, the caller's own cwd)
33
+ * — or, for a worktree the factory mints itself (`isolation`), that worktree on the caller's host (a remote environment's
34
+ * is another namespace). A leg no delegation caller placed (no `callerRoot`) has no placement and reads nothing. */
35
+ export declare function childPlacementOnLeg(env: ExecutionEnv, internals: Pick<RunInternals, "isolation" | "parentCwd"> & {
36
+ readonly callerRoot: string;
37
+ }, root: string): Promise<ChildPlacement>;
38
+ /** The refusal of a worktree the caller minted for a child its environment placed in the caller's own tree. */
39
+ export declare function worktreeNotHonored(cwd: string): Error & {
40
+ code: "isolation.unavailable";
41
+ };
42
+ /**
43
+ * The chain's top leg's anchors as THIS leg reads them (the person's global / session path rows and every `~/` row anchor
44
+ * there): the chain's own (`live` as recorded, else the checkpoint's `seed` rebased — {@link recordedAnchors}), unless this
45
+ * leg is a child placed `elsewhere` or no chain names one — then this leg is the top. A recorded root that is not an
46
+ * absolute path anchors nothing: the rows it would anchor read `unreadable`.
47
+ */
48
+ export declare function chainTopAnchors(cfg: {
49
+ leg: PathRuleBases;
50
+ live: InheritedGate["pathRuleAnchors"];
51
+ seed: InheritedGate["pathRuleAnchors"];
52
+ placement: ChildPlacement | undefined;
53
+ rebase: (p: string) => string;
54
+ }): PathRuleBases;
55
+ /** The installation records a leg reads: the live chain's (as recorded), else the checkpoint's (through the workspace rebase). */
56
+ export interface ChainInstallations {
57
+ readonly live: InheritedGate["pathRuleInstallations"];
58
+ readonly seed: InheritedGate["pathRuleInstallations"];
59
+ }
60
+ /** Why no usable record says where a chain entry was installed: no record (a chain written before the record existed), a
61
+ * record that does not cover the chain position for position, or a member that names no absolute root. */
62
+ export type ChainInstallationGap = "absent" | "misaligned" | "invalid";
63
+ /**
64
+ * The chain entries a leg could not install where they were installed: each UNINSTALLED entry (a host re-supplying a
65
+ * chain) whose path rules depend on the installing leg, with no usable record of that leg. Such a chain is refused —
66
+ * installing those entries anywhere else (the chain's top leg, this leg) would move what they protect. Entries whose rules
67
+ * do not depend on it, and installed entries, never need a record.
68
+ */
69
+ export declare function chainInstallationGaps(chain: readonly ToolPolicy[], installations: ChainInstallations, rebase: (p: string) => string): Array<{
70
+ readonly position: number;
71
+ readonly gap: ChainInstallationGap;
72
+ }>;
73
+ /** The refusal of a chain with {@link chainInstallationGaps} (`resume.parent_constraint_mismatch`, `detail.reason` naming
74
+ * the first gap's form); `pending` says the checkpoint is still pending (the refusal came before the claim). */
75
+ export declare function chainInstallationRefusal(gaps: ReadonlyArray<{
76
+ readonly position: number;
77
+ readonly gap: ChainInstallationGap;
78
+ }>, pending: boolean): CheckpointError;
79
+ /**
80
+ * How a leg holds a caller policy — its own (`TaskSpec.toolPolicy` / `RunnerDeps.toolPolicy`) or one its delegation chain
81
+ * hands it: a policy is installed where it ENTERS the chain, and a child holds its ancestor's installation (one object,
82
+ * the same anchors) — its own caller slot included when that slot names the policy an ancestor installed. Installed again:
83
+ * on a child placed `elsewhere` (another filesystem namespace — the ancestor's anchors name another namespace's files), at
84
+ * this leg's anchors; a chain entry handed uninstalled (a host re-supplying a chain), where its installing leg recorded it
85
+ * (`InheritedGate.pathRuleInstallations`, live or from the checkpoint). An uninstalled entry whose rules depend on that
86
+ * leg and whose record is not usable refuses the leg ({@link chainInstallationRefusal}); one whose rules do not, reads
87
+ * the same wherever it is installed (the chain's top leg).
88
+ */
89
+ export declare function chainPolicyOnLeg(cfg: {
90
+ leg: PathRuleBases;
91
+ chainTop: PathRuleBases;
92
+ placement: ChildPlacement | undefined;
93
+ chain: readonly ToolPolicy[];
94
+ installations: ChainInstallations;
95
+ rebase: (p: string) => string;
96
+ }): {
97
+ own: (policy: ToolPolicy | undefined) => ToolPolicy | undefined;
98
+ chained: (policy: ToolPolicy, position: number) => ToolPolicy;
99
+ };
100
+ /** The installation record of a chain entry (its root and home), `null` for an entry with no path rules installed. */
101
+ export declare function installationRecordOf(policy: ToolPolicy): {
102
+ readonly root: string;
103
+ readonly home?: string;
104
+ } | null;
105
+ /** What a leg hands its children about anchors (data half): the chain's top leg's anchors — the person's global / session
106
+ * rows and every `~/` row anchor there — and where each entry handed down was installed, position for position (a chain
107
+ * re-supplied uninstalled is installed there again; absent when no entry is an installation). */
108
+ export declare function chainAnchorRecords(chainTop: PathRuleBases, chain: readonly {
109
+ readonly policy: ToolPolicy;
110
+ }[]): Pick<InheritedGate, "pathRuleAnchors" | "pathRuleInstallations">;
@@ -1,3 +1,107 @@
1
+ import { isRemoteExecutionEnv } from "../remote-env.js";
2
+ import { isAbsolutePathForm } from "../../tools/fs/safety.js";
3
+ import { installPolicyAnchors, installationAnchorsOf, installedFrom } from "../path-rule-binding.js";
4
+ import { CheckpointError } from "../checkpoint-store.js";
1
5
  export function deploymentCanRerootChild(deps) {
2
6
  return deps.executionEnvFactory !== undefined;
3
7
  }
8
+ export async function childPlacementOf(env, childRoot, asked, callerRoot) {
9
+ const canon = async (p) => {
10
+ try {
11
+ const r = await env.canonicalPath(p);
12
+ return r.ok ? r.value : p;
13
+ }
14
+ catch {
15
+ return p;
16
+ }
17
+ };
18
+ const root = await canon(childRoot);
19
+ if (asked !== undefined && (await canon(asked)) === root)
20
+ return "honored";
21
+ const tree = await canon(callerRoot);
22
+ const sep = tree.includes("\\") && !tree.includes("/") ? "\\" : "/";
23
+ return root === tree || root.startsWith(tree.endsWith(sep) ? tree : tree + sep) ? "caller_tree" : "elsewhere";
24
+ }
25
+ export function childPlacementOnLeg(env, internals, root) {
26
+ const asked = internals.isolation === "worktree" ? (isRemoteExecutionEnv(env) ? undefined : root) : internals.parentCwd;
27
+ return childPlacementOf(env, root, asked, internals.callerRoot);
28
+ }
29
+ export function worktreeNotHonored(cwd) {
30
+ return Object.assign(new Error(`isolation "worktree" was requested but cannot be honored: the execution environment factory placed the sub-agent at "${cwd}", inside the caller's own tree, instead of the worktree it was offered. The sub-agent was not started (fail-closed — no silent fallback to the shared working tree).`), { code: "isolation.unavailable" });
31
+ }
32
+ const AS_RECORDED = (p) => p;
33
+ function recordedAnchors(r, rebase) {
34
+ if (r === null || r === undefined)
35
+ return undefined;
36
+ const read = (v) => {
37
+ if (typeof v !== "string")
38
+ return undefined;
39
+ const p = rebase(v);
40
+ return isAbsolutePathForm(p) ? p : undefined;
41
+ };
42
+ const [root, home] = [read(r.root), read(r.home)];
43
+ return { ...(root !== undefined ? { root } : {}), ...(home !== undefined ? { home } : {}) };
44
+ }
45
+ export function chainTopAnchors(cfg) {
46
+ if (cfg.placement === "elsewhere")
47
+ return cfg.leg;
48
+ return (cfg.live !== undefined ? recordedAnchors(cfg.live, AS_RECORDED) : recordedAnchors(cfg.seed, cfg.rebase)) ?? cfg.leg;
49
+ }
50
+ const GAP_REASON = { absent: "installation_record_absent", misaligned: "installation_record_misaligned", invalid: "installation_record_invalid" };
51
+ function dependsOnInstallation(policy) {
52
+ if (installedFrom(policy) !== undefined)
53
+ return false;
54
+ return typeof policy.pathRules === "function" && policy.pathRules({}).some((row) => row.unanchored !== undefined);
55
+ }
56
+ function recordedInstallation(installations, chainLength, position, rebase) {
57
+ const [record, read] = installations.live !== undefined ? [installations.live, AS_RECORDED] : [installations.seed, rebase];
58
+ if (record === undefined)
59
+ return "absent";
60
+ if (record.length !== chainLength)
61
+ return "misaligned";
62
+ const a = recordedAnchors(record[position], read);
63
+ return a?.root === undefined ? "invalid" : a;
64
+ }
65
+ export function chainInstallationGaps(chain, installations, rebase) {
66
+ return chain.flatMap((policy, position) => {
67
+ if (!dependsOnInstallation(policy))
68
+ return [];
69
+ const r = recordedInstallation(installations, chain.length, position, rebase);
70
+ return typeof r === "string" ? [{ position, gap: r }] : [];
71
+ });
72
+ }
73
+ export function chainInstallationRefusal(gaps, pending) {
74
+ const first = gaps[0];
75
+ const form = { absent: "absent (a chain written before installation records existed, or one built without them)", misaligned: "misaligned (it does not cover the chain entry for entry)", invalid: "invalid for that entry (it names no absolute root)" }[first.gap];
76
+ return new CheckpointError("resume.parent_constraint_mismatch", `the parent-constraint chain carries ${gaps.length} polic${gaps.length === 1 ? "y" : "ies"} (entry ${gaps.map((g) => g.position).join(", ")}) whose path rules resolve against the leg that installed ${gaps.length === 1 ? "it" : "them"}, and the installation record is ${form} — installing ${gaps.length === 1 ? "it" : "them"} anywhere else would move what ${gaps.length === 1 ? "it protects" : "they protect"}, so the chain is refused` +
77
+ (pending ? " before the claim (the checkpoint stays pending). A task parked before 7.46.0 under such a chain cannot be resumed by this version: let a worker of the version that parked it finish it, or let it expire and submit the task again." : "."), { reason: GAP_REASON[first.gap] });
78
+ }
79
+ export function chainPolicyOnLeg(cfg) {
80
+ if (cfg.placement !== "elsewhere") {
81
+ const gaps = chainInstallationGaps(cfg.chain, cfg.installations, cfg.rebase);
82
+ if (gaps.length > 0)
83
+ throw chainInstallationRefusal(gaps, false);
84
+ }
85
+ const recorded = (position) => {
86
+ const r = recordedInstallation(cfg.installations, cfg.chain.length, position, cfg.rebase);
87
+ return typeof r === "string" ? undefined : r;
88
+ };
89
+ const chained = (policy, position) => (cfg.placement === "elsewhere" ? installPolicyAnchors(policy, cfg.leg) : installedFrom(policy) !== undefined ? policy : installPolicyAnchors(policy, recorded(position) ?? cfg.chainTop)) ?? policy;
90
+ const own = (policy) => {
91
+ if (policy === undefined)
92
+ return undefined;
93
+ const position = cfg.placement === "elsewhere" ? -1 : cfg.chain.findIndex((c) => (installedFrom(c) ?? c) === (installedFrom(policy) ?? policy));
94
+ return position >= 0 ? chained(cfg.chain[position], position) : installPolicyAnchors(policy, cfg.leg);
95
+ };
96
+ return { own, chained };
97
+ }
98
+ export function installationRecordOf(policy) {
99
+ const a = installationAnchorsOf(policy);
100
+ return a?.root === undefined ? null : { root: a.root, ...(a.home !== undefined ? { home: a.home } : {}) };
101
+ }
102
+ export function chainAnchorRecords(chainTop, chain) {
103
+ return {
104
+ ...(chainTop.root !== undefined ? { pathRuleAnchors: { root: chainTop.root, ...(chainTop.home !== undefined ? { home: chainTop.home } : {}) } } : {}),
105
+ ...(chain.some((pc) => installationAnchorsOf(pc.policy)?.root !== undefined) ? { pathRuleInstallations: chain.map((pc) => installationRecordOf(pc.policy)) } : {}),
106
+ };
107
+ }
@@ -0,0 +1,5 @@
1
+ import type { RunnerDeps } from "../types.js";
2
+ /** The retired-seat door ({@link assertNoRetiredRunnerDeps}), then the seats whose value is screened at construction and
3
+ * never again: the declared configuration root must be the absolute path of an existing directory — a root that names no
4
+ * directory guards nothing, so it is refused here rather than read as absent. */
5
+ export declare function assertRunnerDepsDoors(deps: RunnerDeps): void;
@@ -0,0 +1,6 @@
1
+ import { checkedAgentConfigDir } from "../agent-config-dir.js";
2
+ import { assertNoRetiredRunnerDeps } from "./retired-runner-deps.js";
3
+ export function assertRunnerDepsDoors(deps) {
4
+ assertNoRetiredRunnerDeps(deps);
5
+ checkedAgentConfigDir(deps.agentConfigDir, "RunnerDeps.agentConfigDir");
6
+ }
@@ -393,6 +393,8 @@ export interface Prepared {
393
393
  cwdRef?: CwdRef;
394
394
  /** The root the hand tools resolve root-relative spellings against (present with hands; read-only hands included). */
395
395
  handsRoot?: string;
396
+ /** The leg's target-reading stamp (present with hands): the resume belts stamp the executed bytes' request with it. */
397
+ stampCallTarget?: import("../effective-path-target.js").CallTargetStamp;
396
398
  /** The run's TOOL-PROGRESS lane seat, late-bound for the same reason `harnessRef` is (the tool band is
397
399
  * built during prepare; the lane needs `pushContent` and `ident`, which exist only after it returns).
398
400
  * The driver binds `current` right after `pushContent` exists. Always present on a completed prepare —
@@ -1417,6 +1419,29 @@ export interface InheritedGate {
1417
1419
  * had a governance surface (their opaque posture is untouched). Serializable; persists on checkpoints.
1418
1420
  */
1419
1421
  orgAdmissionGoverned?: true;
1422
+ /**
1423
+ * The chain's TOP leg's root and environment home (data half, serializable; persists on the checkpoint's data half):
1424
+ * the anchors of the person's global and session path rows and of every `~/` row on this chain — a child judges them
1425
+ * where the chain was installed, not at its own root (a project row anchors at its own scope root). Emitted by every leg
1426
+ * that hands a chain down (a top leg emits its own). Absent on an older chain ⇒ the child is its own top. A child placed
1427
+ * in ANOTHER filesystem namespace (its environment did not put it where it was asked, outside the caller's tree) ignores
1428
+ * it and takes its own leg as the top.
1429
+ */
1430
+ pathRuleAnchors?: {
1431
+ readonly root: string;
1432
+ readonly home?: string;
1433
+ };
1434
+ /**
1435
+ * Where each `parentConstraints` entry was INSTALLED (data half, serializable; persists on the checkpoint's data half):
1436
+ * position for position with `parentConstraints`, the root and home its path rules are anchored at (`null` for an entry
1437
+ * with no path rules). A chain re-supplied with uninstalled policies (a resume in another process, a host-built chain)
1438
+ * installs each where this records — never at another leg's anchors. Absent on an older chain: an uninstalled entry is
1439
+ * installed at the chain's top leg (`pathRuleAnchors`).
1440
+ */
1441
+ pathRuleInstallations?: ReadonlyArray<{
1442
+ readonly root: string;
1443
+ readonly home?: string;
1444
+ } | null>;
1420
1445
  /** Live ancestor caller-policy constraints (opaque half), ancestors first. `policy` is the ancestor's
1421
1446
  * RESOLVED caller policy (`spec.toolPolicy ?? deps.toolPolicy` — the same slot the ancestor itself
1422
1447
  * enforces, so a deps-level baseline is inherited too).
@@ -2137,6 +2162,20 @@ export interface RunInternals {
2137
2162
  * callers (ctx-injection posture, same as `parentCwd`).
2138
2163
  */
2139
2164
  requestedCwd?: string;
2165
+ /**
2166
+ * The SPAWNING run's own root, filled ONLY by core delegation callers beside `parentCwd` (ctx-injection posture): with
2167
+ * it the child's prepare judges where its environment actually placed it against where it was asked
2168
+ * (`childPlacementOf`, child-tree.ts: honored / inside the caller's tree / elsewhere) — the fact the
2169
+ * worktree request below and the anchors of the rules the child inherits read. Absent ⇒ no placement is judged (a top
2170
+ * leg, a caller that does not delegate through core).
2171
+ */
2172
+ callerRoot?: string;
2173
+ /**
2174
+ * `parentCwd` is a worktree the caller minted for THIS child (the delegation tool's `isolation: "worktree"`): a child
2175
+ * its environment placed inside the caller's own tree instead is refused before it starts (`isolation.unavailable`).
2176
+ * Filled ONLY by the core delegation tool; never a {@link TaskSpec} field.
2177
+ */
2178
+ worktreeOffered?: true;
2140
2179
  /**
2141
2180
  * Subagent steer verb (dogfood finding 2026-07-03, "中途插话"): the host run's opt-in
2142
2181
  * SUBAGENT-STEER-HANDLE sink. When set, `createSubagentTool` runs each child via `runTaskStream`
@@ -2237,6 +2276,10 @@ export interface RunInternals {
2237
2276
  * non-isolated run reports its plain root with `isolated: false` — the observation face stays complete, while
2238
2277
  * the honest non-claim keeps a consumer from mistaking a SHARED tree for its own worktree.
2239
2278
  */
2279
+ /** Where a delegated child's environment placed it against where its caller asked (`childPlacementOf`, child-tree.ts):
2280
+ * `honored` — where it was asked; `caller_tree` — elsewhere inside the caller's own tree; `elsewhere` — another
2281
+ * filesystem namespace. */
2282
+ export type ChildPlacement = "honored" | "caller_tree" | "elsewhere";
2240
2283
  export interface ResolvedWorkspace {
2241
2284
  /** The task's effective working root — the same value every fence / LSP / prompt consumer uses. */
2242
2285
  cwd: string;
@@ -2247,6 +2290,9 @@ export interface ResolvedWorkspace {
2247
2290
  * or names an unrelated directory — on the host. `isolated && !remote` is the only combination under
2248
2291
  * which `cwd` is a host path a consumer may diff / merge / remove. */
2249
2292
  remote: boolean;
2293
+ /** Where a delegated child's environment placed it against where its caller asked; absent on a leg no delegation
2294
+ * caller placed. */
2295
+ placement?: ChildPlacement;
2250
2296
  }
2251
2297
  /**
2252
2298
  * The resolver's answer, with the WITHHELD cause beside the value.
@@ -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). */