@sema-agent/core 7.44.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 (159) hide show
  1. package/CHANGELOG.md +418 -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 +65 -38
  6. package/dist/agents/tool-filter.d.ts +82 -5
  7. package/dist/agents/tool-filter.js +134 -4
  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/agent-definition.d.ts +2 -2
  11. package/dist/core/allow-entry.d.ts +30 -4
  12. package/dist/core/allow-entry.js +32 -5
  13. package/dist/core/ask-origin.d.ts +23 -4
  14. package/dist/core/ask-origin.js +11 -5
  15. package/dist/core/checkpoint-store.d.ts +41 -4
  16. package/dist/core/decision-receipt.js +1 -1
  17. package/dist/core/effective-path-target.d.ts +143 -10
  18. package/dist/core/effective-path-target.js +113 -9
  19. package/dist/core/engine-notice.d.ts +25 -3
  20. package/dist/core/engine-notice.js +12 -6
  21. package/dist/core/gate-fold.js +3 -0
  22. package/dist/core/gate-lanes.js +11 -3
  23. package/dist/core/gate-outcome.d.ts +5 -0
  24. package/dist/core/gate-outcome.js +2 -0
  25. package/dist/core/governance-codes.d.ts +3 -1
  26. package/dist/core/governance-codes.js +3 -0
  27. package/dist/core/hooks.d.ts +5 -0
  28. package/dist/core/memory-engine/content-origin.d.ts +0 -34
  29. package/dist/core/memory-engine/content-origin.js +9 -4
  30. package/dist/core/memory-engine/layout.d.ts +5 -1
  31. package/dist/core/memory-engine/layout.js +1 -0
  32. package/dist/core/path-rule-binding.d.ts +65 -0
  33. package/dist/core/path-rule-binding.js +99 -0
  34. package/dist/core/permission-rule-model.d.ts +47 -18
  35. package/dist/core/permission-rule-model.js +38 -42
  36. package/dist/core/permission-rule-provider.d.ts +7 -0
  37. package/dist/core/permission-rule-provider.js +10 -3
  38. package/dist/core/permission-rule-store.d.ts +8 -0
  39. package/dist/core/permission-rule-store.js +6 -2
  40. package/dist/core/permission-rule-sync.js +2 -2
  41. package/dist/core/permission-rules.d.ts +22 -36
  42. package/dist/core/permission-rules.js +50 -40
  43. package/dist/core/real-approval.d.ts +9 -6
  44. package/dist/core/real-approval.js +5 -5
  45. package/dist/core/runner/child-tree.d.ts +110 -0
  46. package/dist/core/runner/child-tree.js +107 -0
  47. package/dist/core/runner/construction-doors.d.ts +5 -0
  48. package/dist/core/runner/construction-doors.js +6 -0
  49. package/dist/core/runner/contracts.d.ts +54 -2
  50. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  51. package/dist/core/runner/permission-rule-lanes.js +18 -12
  52. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  53. package/dist/core/runner/persisted-path-rows.js +31 -12
  54. package/dist/core/runner/prepare-ask-lane.d.ts +6 -0
  55. package/dist/core/runner/prepare-ask-lane.js +4 -2
  56. package/dist/core/runner/prepare-config-doors.js +3 -1
  57. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  58. package/dist/core/runner/prepare-execution-env.js +11 -3
  59. package/dist/core/runner/prepare-gate-stations.d.ts +6 -0
  60. package/dist/core/runner/prepare-gate-stations.js +3 -1
  61. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  62. package/dist/core/runner/prepare-hands-readface.js +6 -6
  63. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  64. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  65. package/dist/core/runner/prepare-listings.d.ts +4 -4
  66. package/dist/core/runner/prepare-listings.js +2 -22
  67. package/dist/core/runner/prepare-park-ask.d.ts +5 -0
  68. package/dist/core/runner/prepare-park-ask.js +6 -4
  69. package/dist/core/runner/prepare-policy-chain.d.ts +16 -2
  70. package/dist/core/runner/prepare-policy-chain.js +14 -5
  71. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  72. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  73. package/dist/core/runner/prepare-task.js +41 -18
  74. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  75. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  76. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  77. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  78. package/dist/core/runner/resume-apply.js +17 -3
  79. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  80. package/dist/core/runner/resume-policy-outcome.js +2 -1
  81. package/dist/core/runner/run-leg.js +5 -1
  82. package/dist/core/runner/run-notification-lane.js +2 -2
  83. package/dist/core/runner/runtask.d.ts +2 -0
  84. package/dist/core/runner/runtask.js +4 -2
  85. package/dist/core/runner/stream-steer-verb.js +2 -2
  86. package/dist/core/runner/sub-agent-deny-lists.d.ts +32 -0
  87. package/dist/core/runner/sub-agent-deny-lists.js +121 -0
  88. package/dist/core/runner/synthetic-tools.d.ts +1 -1
  89. package/dist/core/runner/synthetic-tools.js +3 -2
  90. package/dist/core/runner/write-protection-station.d.ts +3 -2
  91. package/dist/core/runner/write-protection-station.js +1 -0
  92. package/dist/core/runner-deps.d.ts +14 -0
  93. package/dist/core/sensitive-path-policy.d.ts +8 -0
  94. package/dist/core/sensitive-path-policy.js +18 -2
  95. package/dist/core/session-policy-store.d.ts +6 -2
  96. package/dist/core/session-policy-store.js +2 -1
  97. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  98. package/dist/core/shell-wrapper-projection.js +56 -0
  99. package/dist/core/shell-wrapper-table.d.ts +144 -34
  100. package/dist/core/shell-wrapper-table.js +40 -20
  101. package/dist/core/skill-grant.d.ts +10 -2
  102. package/dist/core/skill-grant.js +20 -4
  103. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  104. package/dist/core/synthetic-read-verdict.js +3 -1
  105. package/dist/core/task-notification.d.ts +12 -1
  106. package/dist/core/task-notification.js +13 -1
  107. package/dist/core/task-spec.d.ts +5 -3
  108. package/dist/core/tool-catalog-entries.d.ts +21 -0
  109. package/dist/core/tool-catalog-entries.js +9 -0
  110. package/dist/core/tool-policy.d.ts +45 -12
  111. package/dist/core/tool-policy.js +18 -20
  112. package/dist/core/tool-spec.d.ts +23 -4
  113. package/dist/core/types.d.ts +1 -1
  114. package/dist/core/types.js +1 -1
  115. package/dist/core/write-protect.d.ts +4 -0
  116. package/dist/core/write-protect.js +30 -9
  117. package/dist/index.d.ts +10 -6
  118. package/dist/index.js +9 -5
  119. package/dist/orchestration/run-spec.js +8 -7
  120. package/dist/orchestration/workflow.js +3 -2
  121. package/dist/shell/lex.d.ts +7 -0
  122. package/dist/shell/lex.js +17 -0
  123. package/dist/tools/fs/bash-program-position.d.ts +8 -87
  124. package/dist/tools/fs/bash-program-position.js +33 -69
  125. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  126. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  127. package/dist/tools/fs/index.d.ts +3 -2
  128. package/dist/tools/fs/program-read-model.d.ts +694 -0
  129. package/dist/tools/fs/program-read-model.js +1252 -0
  130. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  131. package/dist/tools/fs/program-read-model.rows.js +613 -0
  132. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  133. package/dist/tools/fs/read-deny-home.js +62 -24
  134. package/dist/tools/fs/read-deny.d.ts +54 -14
  135. package/dist/tools/fs/read-deny.js +104 -50
  136. package/dist/tools/fs/removal-targets.d.ts +83 -0
  137. package/dist/tools/fs/removal-targets.js +236 -0
  138. package/dist/tools/fs/safety.d.ts +3 -0
  139. package/dist/tools/fs/safety.js +10 -3
  140. package/dist/tools/fs/shell-read-argv.d.ts +85 -0
  141. package/dist/tools/fs/shell-read-argv.js +386 -0
  142. package/dist/tools/fs/shell-read-bindings.d.ts +273 -0
  143. package/dist/tools/fs/shell-read-bindings.js +746 -0
  144. package/dist/tools/fs/shell-read-check.js +23 -15
  145. package/dist/tools/fs/shell-read-establish.js +79 -5
  146. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  147. package/dist/tools/fs/shell-read-judge.js +9 -1
  148. package/dist/tools/fs/shell-read-plan.d.ts +31 -7
  149. package/dist/tools/fs/shell-read-plan.js +299 -393
  150. package/dist/tools/fs/shell-read-word.d.ts +29 -26
  151. package/dist/tools/fs/shell-read-word.js +30 -38
  152. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -37
  153. package/dist/tools/fs/shell-read-write-targets.js +83 -136
  154. package/dist/tools/fs/shell-reentry-text.d.ts +38 -6
  155. package/dist/tools/fs/shell-reentry-text.js +90 -53
  156. package/package.json +1 -1
  157. package/test/export-surface.snapshot.json +49 -3
  158. package/wire-changes/core@7.45.0.json +1084 -0
  159. package/wire-changes/core@7.46.0.json +1522 -0
@@ -38,6 +38,24 @@ export declare function readDenyHomeLinksOf(env: ExecutionEnv, entries: readonly
38
38
  readonly linked: Map<string, string[]>;
39
39
  readonly whole: boolean;
40
40
  }>;
41
+ /**
42
+ * Where the rows that open with the configuration directory's name (`~/.sema/…`, {@link agentConfigDirTail}) lead when
43
+ * the host declares the directory's root (`RunnerDeps.agentConfigDir`) — the same `linked` answer
44
+ * {@link readDenyHomeLinksOf} gives, for the same rows: the row's literal head below the directory name is spelled under
45
+ * each spelling of the root and resolved the way the read faces mint their keys. A row is not anchored at the root by
46
+ * its own run (it is anchored at the homes), so every location here — the spelling itself and where it leads — is one the
47
+ * row is placed at in addition; the home spelling stays placed. The row's name in every refusal is unchanged.
48
+ * · a row whose run does not open with the directory's name (`~/.sema.*`, the external tools' files) adds nothing;
49
+ * · a row whose head ends at the directory name (`~/.sema/*.json`, `~/.sema`) is placed at the root itself, the rest of
50
+ * its run matched below it.
51
+ */
52
+ export declare function readDenyConfigRootLinksOf(env: ExecutionEnv, entries: readonly {
53
+ readonly pattern: string;
54
+ readonly caseSensitive?: boolean;
55
+ }[], roots: readonly string[], signal?: AbortSignal, deadline?: number): Promise<{
56
+ readonly linked: Map<string, string[]>;
57
+ readonly whole: boolean;
58
+ }>;
41
59
  /** Where an absolute location LEADS in one environment — resolved the way the read and write faces mint their keys
42
60
  * (`canonicalizeTarget`: the longest existing prefix resolved, the rest rejoined). `undefined` when the environment
43
61
  * does not answer (a refused or failed probe, one that throws). The one resolution a deny row's placement reads: a
@@ -122,7 +140,7 @@ export declare function carryHomeViews<T extends object>(judge: T, source: HomeV
122
140
  * · each AUXILIARY home (see the module head) read once under {@link AUXILIARY_HOME_PROBE_MS} — its spellings, its
123
141
  * chain, its rows' heads — at the same time; whatever of it did not answer costs that home alone and never `whole`.
124
142
  * `compile` is the mount's own compile; the rows' patterns are read off its compile at no home. */
125
- export declare function placeReadDenyAtHome(env: ExecutionEnv, compile: (homes: readonly string[], linked?: ReadonlyMap<string, readonly string[]>) => ReadDenyMatcher, homes?: readonly string[], signal?: AbortSignal): Promise<{
143
+ export declare function placeReadDenyAtHome(env: ExecutionEnv, compile: (homes: readonly string[], linked?: ReadonlyMap<string, readonly string[]>) => ReadDenyMatcher, homes?: readonly string[], signal?: AbortSignal, configRoot?: string): Promise<{
126
144
  readonly matcher: PlacedReadDenyMatcher;
127
145
  readonly homes: readonly string[];
128
146
  readonly linked: ReadonlyMap<string, readonly string[]>;
@@ -2,6 +2,7 @@ import { userInfo } from "node:os";
2
2
  import { canonicalizeTarget, isAbsolutePathForm, joinForFamily, normalizeAbsPathLexically, pathFamilyOf } from "./safety.js";
3
3
  import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, withGateTimeout } from "../../core/gate-bounded-await.js";
4
4
  import { thrownText } from "../../core/thrown-value.js";
5
+ import { agentConfigDirTail } from "../../core/agent-config-dir.js";
5
6
  import { readDenyPatternRun, readDenyRunHead } from "./read-deny.js";
6
7
  export const AUXILIARY_HOME_PROBE_MS = 2_000;
7
8
  const said = new Set();
@@ -97,12 +98,15 @@ export async function readDenyHomesOf(env, signal) {
97
98
  async function until(work, deadline, signal) {
98
99
  return await withGateTimeout(work, Math.max(0, deadline - Date.now()), signal, "auxiliaryHome").catch(() => undefined);
99
100
  }
100
- function sayAuxiliary(what) {
101
+ function sayAuxiliary(what, subject = "auxiliary") {
102
+ if (subject === "config_root") {
103
+ sayOnce("config_root", `read-deny homes: the declared agent configuration root did not answer its probes within ${AUXILIARY_HOME_PROBE_MS} ms (${what}) — the rows that name the configuration directory sit at its spelling only, and a path through an alias of it is judged as spelled, until the next placement.`);
104
+ return;
105
+ }
101
106
  sayOnce("auxiliary", `read-deny homes: a home beside the environment's declared home did not answer its probes within ${AUXILIARY_HOME_PROBE_MS} ms (${what}) — its home-anchored read-deny rows sit at its spelling only, and a path through an alias of it is judged as spelled, until the next placement.`);
102
107
  }
103
108
  export async function readDenyHomeLinksOf(env, entries, homes, signal, deadline) {
104
109
  const rows = [];
105
- const familyOf = new Map();
106
110
  for (const { pattern, caseSensitive } of entries) {
107
111
  const { homeAnchored, run } = readDenyPatternRun(pattern);
108
112
  if (!homeAnchored)
@@ -110,26 +114,51 @@ export async function readDenyHomeLinksOf(env, entries, homes, signal, deadline)
110
114
  const head = run.split("/").filter(Boolean).slice(0, readDenyRunHead(run)).join("/");
111
115
  if (head.length === 0)
112
116
  continue;
113
- const spelled = homes.map((home) => {
114
- const family = pathFamilyOf({ root: home });
115
- const at = normalizeAbsPathLexically(joinForFamily(family, home, head));
116
- familyOf.set(at, family);
117
- return at;
118
- });
119
- rows.push({ pattern, caseSensitive: caseSensitive === true, spelled });
117
+ const spelled = homes.map((home) => normalizeAbsPathLexically(joinForFamily(pathFamilyOf({ root: home }), home, head)));
118
+ rows.push({ pattern, caseSensitive: caseSensitive === true, spelled, anchored: spelled });
120
119
  }
120
+ return await linkedLocationsOf(env, rows, signal, deadline);
121
+ }
122
+ export async function readDenyConfigRootLinksOf(env, entries, roots, signal, deadline) {
123
+ return await linkedLocationsOf(env, configRootRowsOf(entries, roots), signal, deadline);
124
+ }
125
+ function configRootRowsOf(entries, roots) {
126
+ const rows = [];
127
+ for (const { pattern, caseSensitive } of entries) {
128
+ const { homeAnchored, run } = readDenyPatternRun(pattern);
129
+ const tail = homeAnchored ? agentConfigDirTail(run, caseSensitive === true) : undefined;
130
+ if (tail === undefined)
131
+ continue;
132
+ const head = tail.split("/").filter(Boolean).slice(0, readDenyRunHead(tail)).join("/");
133
+ const spelled = roots.map((root) => normalizeAbsPathLexically(head.length === 0 ? root : joinForFamily(pathFamilyOf({ root }), root, head)));
134
+ rows.push({ pattern, caseSensitive: caseSensitive === true, spelled, anchored: [] });
135
+ }
136
+ return rows;
137
+ }
138
+ function configRootSpelledLinks(entries, roots) {
139
+ const out = new Map();
140
+ for (const { pattern, spelled } of configRootRowsOf(entries, roots))
141
+ out.set(pattern, [...new Set(spelled)]);
142
+ return out;
143
+ }
144
+ async function linkedLocationsOf(env, rows, signal, deadline) {
145
+ const familyOf = new Map();
146
+ for (const { spelled } of rows)
147
+ for (const at of spelled)
148
+ familyOf.set(at, pathFamilyOf({ root: at }));
121
149
  const leadOf = async (at, family) => (await whereItLeads(env, at, family, signal)) ?? ((await holdsNothingBelow(env, at, signal)) ? at : undefined);
122
150
  const resolved = new Map(await Promise.all([...familyOf].map(async ([at, family]) => [at, deadline === undefined ? await leadOf(at, family) : await until(leadOf(at, family), deadline, signal)])));
123
151
  const out = new Map();
124
152
  const whole = [...resolved.values()].every((location) => location !== undefined);
125
- for (const { pattern, caseSensitive, spelled } of rows) {
153
+ for (const { pattern, caseSensitive, spelled, anchored } of rows) {
126
154
  const fold = (p) => (caseSensitive ? p : p.replace(/[A-Z]/g, (c) => c.toLowerCase()));
127
- const own = new Set(spelled.map(fold));
155
+ const own = new Set(anchored.map(fold));
128
156
  const locations = [];
129
157
  for (const at of spelled) {
130
- const location = resolved.get(at);
131
- if (location !== undefined && !own.has(fold(location)) && !locations.includes(location))
132
- locations.push(location);
158
+ for (const location of [at, resolved.get(at)]) {
159
+ if (location !== undefined && !own.has(fold(location)) && !locations.includes(location))
160
+ locations.push(location);
161
+ }
133
162
  }
134
163
  if (locations.length > 0)
135
164
  out.set(pattern, locations);
@@ -203,19 +232,24 @@ async function readAuxiliaryHome(env, home, opts, signal) {
203
232
  let reading;
204
233
  if (spelled?.canonical === undefined) {
205
234
  const answered = spelled?.answered === true;
206
- reading = { place: [home], chain: answered ? { ancestors: [], whole: true } : NO_CHAIN, linked: new Map(), answered };
235
+ const linked = opts.configRoot === true && opts.entries !== undefined ? configRootSpelledLinks(opts.entries, [home]) : new Map();
236
+ reading = { place: [home], chain: answered ? { ancestors: [], whole: true } : NO_CHAIN, linked, answered };
207
237
  }
208
238
  else {
209
239
  const place = spelled.spellings;
210
240
  const identity = env.fileIdentity;
211
241
  const [chain, heads] = await Promise.all([
212
242
  typeof identity === "function" ? chainAbove(env, identity, spelled.canonical, signal, deadline) : Promise.resolve({ ancestors: [], whole: true }),
213
- opts.entries !== undefined ? readDenyHomeLinksOf(env, opts.entries, place, signal, deadline) : Promise.resolve({ linked: new Map(), whole: true }),
243
+ opts.entries === undefined
244
+ ? Promise.resolve({ linked: new Map(), whole: true })
245
+ : opts.configRoot === true
246
+ ? readDenyConfigRootLinksOf(env, opts.entries, place, signal, deadline)
247
+ : readDenyHomeLinksOf(env, opts.entries, place, signal, deadline),
214
248
  ]);
215
249
  reading = { place, chain, linked: heads.linked, answered: chain.whole && heads.whole };
216
250
  }
217
251
  if (!reading.answered)
218
- sayAuxiliary("a probe did not answer");
252
+ sayAuxiliary("a probe did not answer", opts.configRoot === true ? "config_root" : "auxiliary");
219
253
  return reading;
220
254
  }
221
255
  function homeViewSourceOver(env, auxiliary) {
@@ -325,31 +359,35 @@ export function homeViewsOf(env, judge, signal) {
325
359
  export function carryHomeViews(judge, source) {
326
360
  return { ...judge, homeViews: source };
327
361
  }
328
- export async function placeReadDenyAtHome(env, compile, homes, signal) {
362
+ export async function placeReadDenyAtHome(env, compile, homes, signal, configRoot) {
329
363
  const entries = compile([]).entries;
330
364
  const declared = declaredHomeOf(env);
331
- const heldAnywhere = (whole) => placementOf(carryHomeViews(compile([]), homeViewSourceOver(env, [])), [], new Map(), whole);
365
+ const relocated = configRoot === undefined ? undefined : readAuxiliaryHome(env, configRoot, { entries, configRoot: true }, signal);
366
+ const heldAnywhere = async (whole) => {
367
+ const linked = (await relocated)?.linked ?? new Map();
368
+ return placementOf(carryHomeViews(linked.size === 0 ? compile([]) : compile([], linked), homeViewSourceOver(env, relocated === undefined ? [] : [relocated])), [], linked, whole);
369
+ };
332
370
  const listed = homes === undefined ? declaredReadDenyHomesAnswered(env) : { homes, whole: true };
333
371
  if (!listed.whole)
334
- return heldAnywhere(false);
372
+ return await heldAnywhere(false);
335
373
  const own = listed.homes.filter((h) => isDeclared(declared, h));
336
374
  let declaredAt = [];
337
375
  if (own.length > 0) {
338
376
  const ownSpellings = await withGateTimeout(homeSpellingsOf(env, own[0], signal), DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "readDenyHomes");
339
377
  if (!ownSpellings.answered)
340
- return heldAnywhere(false);
378
+ return await heldAnywhere(false);
341
379
  declaredAt = [...new Set([...own, ...ownSpellings.spellings])];
342
380
  }
343
381
  const auxiliary = readAuxiliaryHomes(env, listed.homes, { entries }, signal);
344
- const [heads, readings] = await Promise.all([readDenyHomeLinksOf(env, entries, declaredAt, signal), Promise.all(auxiliary)]);
382
+ const [heads, readings, root] = await Promise.all([readDenyHomeLinksOf(env, entries, declaredAt, signal), Promise.all(auxiliary), relocated]);
345
383
  const at = [...new Set([...declaredAt, ...readings.flatMap((r) => r.place)])];
346
384
  const linked = new Map();
347
- for (const map of [heads.linked, ...readings.map((r) => r.linked)]) {
385
+ for (const map of [heads.linked, ...readings.map((r) => r.linked), ...(root === undefined ? [] : [root.linked])]) {
348
386
  for (const [pattern, locations] of map)
349
387
  linked.set(pattern, [...new Set([...(linked.get(pattern) ?? []), ...locations])]);
350
388
  }
351
389
  const matcher = linked.size === 0 ? compile(at) : compile(at, linked);
352
- return placementOf(carryHomeViews(matcher, homeViewSourceOver(env, auxiliary)), at, linked, heads.whole);
390
+ return placementOf(carryHomeViews(matcher, homeViewSourceOver(env, relocated === undefined ? auxiliary : [...auxiliary, relocated])), at, linked, heads.whole);
353
391
  }
354
392
  export function readDenyPlacedLater(unplaced, place) {
355
393
  let current = unplaced;
@@ -70,26 +70,66 @@ export interface PathRuleDenyRow {
70
70
  * row, or one whose head cannot be read: it is judged as spelled. */
71
71
  readonly head?: string;
72
72
  }
73
+ /** A row's reach over ONE absolute spelling, three-valued: `unreadable` = the row cannot be judged there (a base its anchor
74
+ * lacks) — a tightening judgment holds on it, an allow grants nothing on it. */
75
+ export type PathRowReach = {
76
+ readonly reach: "reached" | "not_reached";
77
+ } | {
78
+ readonly reach: "unreadable";
79
+ readonly reason: string;
80
+ };
81
+ /** A rule row as the one judgment reads it: the deny-table row, and — on a row a rule binding produced — its three-valued
82
+ * reach (a row built by hand answers through `reaches` alone: reached or not). */
83
+ export interface JudgedPathRow extends PathRuleDenyRow {
84
+ readonly reach?: (absolutePath: string) => PathRowReach;
85
+ }
86
+ /** A call's target as a judgment reads it: the absolute spellings of its views (lexical, canonical, the leaf chain's hops —
87
+ * any order the caller holds), or `unreadable` when the call names a target this reading cannot locate. */
88
+ export interface TargetViews {
89
+ readonly spellings: readonly string[];
90
+ readonly unreadable?: string;
91
+ }
92
+ /** What {@link pathRowsReach} found: the row, the view it met there (absent on an unreadable hold), and — when nothing was
93
+ * reached and some row could not be judged — the reason of the first such row. */
94
+ export interface PathRowsHit<R> {
95
+ readonly row: R;
96
+ readonly view?: string;
97
+ readonly unreadable?: string;
98
+ }
99
+ /**
100
+ * THE one judgment of path rule rows over a call's target, for the gate's rule lanes and the tool's deny table alike: rows
101
+ * outer, views inner — the first row (in the caller's order) that some view reaches is the answer, so the rule a refusal
102
+ * names does not depend on which view happened to meet it. `tighten` (deny / ask): any view reaching the row reaches it; a
103
+ * row that cannot be judged (or a target that cannot be read) is held and answered only when no row reaches. `allow`: a
104
+ * row admits the call only when it admits EVERY view (a link out of an allowed directory is not admitted through its
105
+ * spelling); an unreadable target is admitted by nothing.
106
+ */
107
+ export declare function pathRowsReach<R extends JudgedPathRow>(rows: readonly R[], views: TargetViews, direction: "tighten" | "allow"): PathRowsHit<R> | undefined;
108
+ /** The locations a row is also judged at (each leading part of its HEAD: the anchor's children down to the head itself,
109
+ * a share's root for a UNC head) — what a leg resolves once, for every row whose head it holds. */
110
+ export declare function ruleHeadParts(rows: readonly Pick<PathRuleDenyRow, "head">[]): string[];
73
111
  /**
74
112
  * The rule rows as a leg judges them: each row as spelled, and — for every location on the way to the row's HEAD (each
75
- * leading part of it: the anchor's children down to the head itself) that the environment resolves to another spelling
76
- * (a link or firmlink: `/tmp` → `/private/tmp`, `/var` → `/private/var`, a project root, a home entry or a directory
77
- * inside the rule reached through a symlink) — the same row at that spelling: a path at or below the resolved location
78
- * is judged as its spelling at or below that part. Every read and write face judges the CANONICAL key of a target beside
79
- * its spelled view, so a row whose head is spelled through a link would otherwise never meet a target named by its real
80
- * location — whether the row was written with the short spelling and the call with the long one, or the reverse. Every
81
- * part, not the head alone: a link inside the rule's literal prefix that is re-pointed after the rows were placed still
82
- * meets the row through the parts above it (`<real root>/alias/**` holds when `alias` moves). One rule for every row
83
- * form (absolute, home, root, working directory, directory): the reference compiles the same "physical twin" of every
84
- * deny / ask rule's literal prefix (cli280.js `ei` 9766583, compiled in `mt` 9767083). `leadsTo` is the leg's resolution,
85
- * the one the faces mint their keys with (`undefined` = could not resolve: no twin there); each location is resolved
86
- * once, and a part that only extends its parent's resolution adds no row.
113
+ * leading part of it) that the environment resolves to another spelling (a link or firmlink: `/tmp` → `/private/tmp`,
114
+ * `/var` → `/private/var`, a project root, a home entry or a directory inside the rule reached through a symlink) — the
115
+ * same row at that spelling, right after it: a path at or below the resolved location is judged as its spelling at or
116
+ * below that part. Every part, not the head alone: a link inside the rule's literal prefix that is re-pointed after the
117
+ * rows were placed still meets the row through the parts above it. One rule for every row form and every judge (the
118
+ * gate's lanes and the deny tables): the reference compiles the same "physical twin" of every deny / ask rule's literal
119
+ * prefix (cli280.js `ei` 9766583, compiled in `mt` 9767083). `leads` is the leg's resolution of each part (absent or
120
+ * `undefined` = no twin there); a part that only extends its parent's resolution adds no row. A twin is the row's own
121
+ * kind (it keeps every member but the head), so a judgment reports the rule the twin stands for.
87
122
  */
88
- export declare function withRuleRowsWhereTheyLead(rows: readonly PathRuleDenyRow[], leadsTo: (head: string) => Promise<string | undefined>): Promise<PathRuleDenyRow[]>;
123
+ export declare function rowsWhereTheyLead<R extends JudgedPathRow>(rows: readonly R[], leads: ReadonlyMap<string, string | undefined>): R[];
124
+ /** Where the location `path` itself leads in a leg's resolution (`leads`, keyed by each leading part — the parts
125
+ * {@link ruleHeadParts} names): `undefined` when the leg did not resolve it or it leads to its own spelling. */
126
+ export declare function locationLeadsTo(path: string, leads: ReadonlyMap<string, string | undefined>): string | undefined;
127
+ /** {@link rowsWhereTheyLead} with each part resolved by `leadsTo` (the leg's resolution; each part once). */
128
+ export declare function withRuleRowsWhereTheyLead<R extends JudgedPathRow>(rows: readonly R[], leadsTo: (head: string) => Promise<string | undefined>): Promise<R[]>;
89
129
  /**
90
130
  * The WRITE face of the deny table: the rule rows whose faces include `write` — the judge the shell station reads for a
91
131
  * command's write targets (a redirection, `tee`, `sed -i`, `cp` / `mv` / `touch` / `mkdir` / `rm` operands). Same row
92
- * type, same judgment (`ruleRowHit`) as the read face's rule rows; a hit names the row by its rule text.
132
+ * type, same judgment ({@link pathRowsReach}) as the read face's rule rows; a hit names the row by its rule text.
93
133
  */
94
134
  export interface WriteDenyMatcher {
95
135
  /** Which face this judge is — the write tools' path resolution reads it to word and code the refusal as a write. */
@@ -12,32 +12,62 @@ export function readDenyRunHead(run) {
12
12
  return wild === -1 ? segments.length : wild;
13
13
  }
14
14
  export const DENY_FACES = Object.freeze(["read", "write"]);
15
- export async function withRuleRowsWhereTheyLead(rows, leadsTo) {
16
- const leads = new Map();
17
- const partsOf = (head) => {
18
- const family = pathFamilyOf({ root: head });
19
- const id = pathIdentityOf(head, family, head);
20
- if (!id.ok)
21
- return [head];
22
- const unc = id.identity.startsWith("//");
23
- const segments = id.identity.split("/").filter((seg) => seg !== "");
24
- const parts = [];
25
- for (let k = unc ? 2 : 1; k <= segments.length; k++)
26
- parts.push(spellingOfIdentity(`${unc ? "//" : "/"}${segments.slice(0, k).join("/")}`, family));
27
- return parts.length > 0 ? parts : [head];
28
- };
29
- const partsByRow = rows.map((row) => (row.head === undefined ? [] : partsOf(row.head)));
30
- for (const parts of partsByRow)
31
- for (const part of parts)
32
- if (!leads.has(part))
33
- leads.set(part, leadsTo(part).catch(() => undefined));
15
+ const REACHED = { reach: "reached" };
16
+ const NOT_REACHED = { reach: "not_reached" };
17
+ const reachOf = (row, spelled) => row.reach?.(spelled) ?? (row.reaches(spelled) ? REACHED : NOT_REACHED);
18
+ export function pathRowsReach(rows, views, direction) {
19
+ if (direction === "allow") {
20
+ if (views.unreadable !== undefined || views.spellings.length === 0)
21
+ return undefined;
22
+ for (const row of rows)
23
+ if (views.spellings.every((v) => reachOf(row, v).reach === "reached"))
24
+ return { row };
25
+ return undefined;
26
+ }
27
+ let held;
28
+ for (const row of rows) {
29
+ if (views.unreadable !== undefined)
30
+ held ??= { row, unreadable: views.unreadable };
31
+ for (const view of views.spellings) {
32
+ const r = reachOf(row, view);
33
+ if (r.reach === "reached")
34
+ return { row, view };
35
+ if (r.reach === "unreadable")
36
+ held ??= { row, unreadable: r.reason };
37
+ }
38
+ }
39
+ return held;
40
+ }
41
+ export function ruleHeadParts(rows) {
42
+ const out = new Set();
43
+ for (const row of rows)
44
+ if (row.head !== undefined)
45
+ for (const part of partsOf(row.head))
46
+ out.add(part);
47
+ return [...out];
48
+ }
49
+ function partsOf(head) {
50
+ const family = pathFamilyOf({ root: head });
51
+ const id = pathIdentityOf(head, family, head);
52
+ if (!id.ok)
53
+ return [head];
54
+ const unc = id.identity.startsWith("//");
55
+ const segments = id.identity.split("/").filter((seg) => seg !== "");
56
+ const parts = [];
57
+ for (let k = unc ? 2 : 1; k <= segments.length; k++)
58
+ parts.push(spellingOfIdentity(`${unc ? "//" : "/"}${segments.slice(0, k).join("/")}`, family));
59
+ return parts.length > 0 ? parts : [head];
60
+ }
61
+ export function rowsWhereTheyLead(rows, leads) {
34
62
  const out = [];
35
- for (const [i, row] of rows.entries()) {
63
+ for (const row of rows) {
36
64
  out.push(row);
65
+ if (row.head === undefined || leads.size === 0)
66
+ continue;
37
67
  let parentLed;
38
68
  let parentPart;
39
- for (const part of partsByRow[i]) {
40
- const led = await leads.get(part);
69
+ for (const part of partsOf(row.head)) {
70
+ const led = leads.get(part);
41
71
  const extendsParent = led !== undefined && parentLed !== undefined && parentPart !== undefined && sameLocation(led, `${parentLed.replace(/[\\/]+$/, "")}/${part.slice(parentPart.replace(/[\\/]+$/, "").length).replace(/^[\\/]+/, "")}`);
42
72
  const twin = extendsParent ? undefined : rowLedTo(row, part, led);
43
73
  if (twin !== undefined)
@@ -48,6 +78,15 @@ export async function withRuleRowsWhereTheyLead(rows, leadsTo) {
48
78
  }
49
79
  return out;
50
80
  }
81
+ export function locationLeadsTo(path, leads) {
82
+ const parts = partsOf(path);
83
+ return leads.get(parts[parts.length - 1]);
84
+ }
85
+ export async function withRuleRowsWhereTheyLead(rows, leadsTo) {
86
+ const parts = ruleHeadParts(rows);
87
+ const led = await Promise.all(parts.map((part) => leadsTo(part).catch(() => undefined)));
88
+ return rowsWhereTheyLead(rows, new Map(parts.map((part, i) => [part, led[i]])));
89
+ }
51
90
  function sameLocation(a, b) {
52
91
  const family = pathFamilyOf({ root: a });
53
92
  const x = pathIdentityOf(a, family, a);
@@ -65,50 +104,60 @@ function rowLedTo(row, head, at) {
65
104
  const base = from.identity;
66
105
  const under = base.endsWith("/") ? base : `${base}/`;
67
106
  const onto = to.identity.endsWith("/") ? to.identity.slice(0, -1) : to.identity;
107
+ const mapped = (absolutePath) => {
108
+ const id = pathIdentityOf(absolutePath, family, head);
109
+ if (!id.ok)
110
+ return undefined;
111
+ if (id.identity === base)
112
+ return to.identity;
113
+ return id.identity.startsWith(under) ? `${onto}/${id.identity.slice(under.length)}` : undefined;
114
+ };
115
+ const { head: _head, ...rest } = row;
116
+ const reach = row.reach;
68
117
  return {
69
- rule: row.rule,
70
- tool: row.tool,
71
- pattern: row.pattern,
72
- faces: row.faces,
118
+ ...rest,
73
119
  reaches: (absolutePath) => {
74
- const id = pathIdentityOf(absolutePath, family, head);
75
- if (!id.ok)
76
- return false;
77
- if (id.identity === base)
78
- return row.reaches(to.identity);
79
- return id.identity.startsWith(under) && row.reaches(`${onto}/${id.identity.slice(under.length)}`);
120
+ const m = mapped(absolutePath);
121
+ return m !== undefined && row.reaches(m);
80
122
  },
123
+ ...(reach !== undefined
124
+ ? {
125
+ reach: (absolutePath) => {
126
+ const m = mapped(absolutePath);
127
+ return m === undefined ? NOT_REACHED : reach(m);
128
+ },
129
+ }
130
+ : {}),
81
131
  };
82
132
  }
83
- function ruleRowHit(rows, face, path) {
84
- for (const r of rows)
85
- if (r.faces.includes(face) && r.reaches(path))
86
- return r;
87
- return undefined;
133
+ function ruleRowsHit(rows, face, spellings) {
134
+ const onFace = rows.filter((r) => r.faces.includes(face));
135
+ return onFace.length === 0 ? undefined : pathRowsReach(onFace, { spellings }, "tighten");
88
136
  }
89
137
  export function compileWriteDeny(rules = []) {
90
138
  const ruleRows = rules.filter((r) => r.faces.includes("write"));
91
139
  const matchPath = (path) => {
92
- const hit = ruleRowHit(ruleRows, "write", path);
93
- return hit === undefined ? null : { pattern: hit.rule };
140
+ const hit = ruleRowsHit(ruleRows, "write", [path]);
141
+ return hit === undefined ? null : { pattern: hit.row.rule };
94
142
  };
95
- return { face: "write", ruleRows, matchPath, matchTarget: (canonicalKey, lexicalView, opts) => matchTargetOver(matchPath, canonicalKey, lexicalView, opts) };
143
+ return { face: "write", ruleRows, matchPath, matchTarget: (canonicalKey, lexicalView, opts) => matchTargetOver(() => null, ruleRows, "write", canonicalKey, lexicalView, opts) };
96
144
  }
97
- function matchTargetOver(matchPath, canonicalKey, lexicalView, opts) {
98
- const canonical = matchPath(canonicalKey, opts);
145
+ function matchTargetOver(segmentPath, ruleRows, face, canonicalKey, lexicalView, opts) {
146
+ const canonical = segmentPath(canonicalKey, opts);
99
147
  if (canonical !== null)
100
148
  return { ...canonical, matchedView: canonicalKey };
101
149
  if (lexicalView !== undefined) {
102
- const lexical = matchPath(lexicalView);
150
+ const lexical = segmentPath(lexicalView);
103
151
  if (lexical !== null)
104
152
  return { ...lexical, matchedView: lexicalView };
105
153
  }
106
154
  for (const hop of opts?.hops ?? []) {
107
- const viaHop = matchPath(hop);
155
+ const viaHop = segmentPath(hop);
108
156
  if (viaHop !== null)
109
157
  return { ...viaHop, matchedView: hop };
110
158
  }
111
- return null;
159
+ const hit = ruleRowsHit(ruleRows, face, [canonicalKey, ...(lexicalView !== undefined ? [lexicalView] : []), ...(opts?.hops ?? [])]);
160
+ return hit === undefined ? null : { pattern: hit.row.rule, matchedView: hit.view ?? canonicalKey };
112
161
  }
113
162
  export function readDenySeat(matcher) {
114
163
  const entries = [...(matcher?.entries ?? []).map((e) => e.pattern), ...(matcher?.ruleRows ?? []).map((r) => r.rule)];
@@ -195,6 +244,7 @@ export const READ_FACE_BUILTIN_DENY_TABLE = [
195
244
  { pattern: "~/.mcp.json", tier: "agent-config" },
196
245
  { pattern: "~/.ai-agent/.env", tier: "agent-config" },
197
246
  { pattern: "~/.sema/engine-data/.env", tier: "agent-config" },
247
+ { pattern: "~/.sema/config-lkg-*.json", tier: "agent-config" },
198
248
  { pattern: "~/.codex/config.toml", tier: "agent-config" },
199
249
  { pattern: "~/.cursor/mcp.json", tier: "agent-config" },
200
250
  { pattern: "~/.continue/config.json", tier: "agent-config" },
@@ -461,18 +511,22 @@ export function compileReadDeny(additions = [], layer = "additions", builtin, ru
461
511
  return { pattern: hit };
462
512
  return ruleRows.length === 0 ? null : { pattern: ruleRows[0].rule };
463
513
  };
464
- const matchPath = (path, opts) => {
514
+ const segmentPath = (path, opts) => {
465
515
  const hit = matchSegmentPatterns(path, judged, opts);
516
+ return hit === null ? null : { pattern: hit };
517
+ };
518
+ const matchPath = (path, opts) => {
519
+ const hit = segmentPath(path, opts);
466
520
  if (hit !== null)
467
- return { pattern: hit };
468
- const rule = ruleRows.length === 0 || opts?.walked === true ? undefined : ruleRowHit(ruleRows, "read", path);
469
- return rule === undefined ? null : { pattern: rule.rule };
521
+ return hit;
522
+ const rule = ruleRows.length === 0 || opts?.walked === true ? undefined : ruleRowsHit(ruleRows, "read", [path]);
523
+ return rule === undefined ? null : { pattern: rule.row.rule };
470
524
  };
471
525
  return {
472
526
  entries: normalized,
473
527
  ...(ruleRows.length > 0 ? { ruleRows } : {}),
474
528
  matchPath,
475
- matchTarget: (canonicalKey, lexicalView, opts) => matchTargetOver(matchPath, canonicalKey, lexicalView, opts),
529
+ matchTarget: (canonicalKey, lexicalView, opts) => matchTargetOver(segmentPath, ruleRows, "read", canonicalKey, lexicalView, opts),
476
530
  rgExclusionGlobs,
477
531
  rgProbeGlobs,
478
532
  unexcludableUnder,
@@ -0,0 +1,83 @@
1
+ import { type EstablishedPlan } from "./shell-read-establish.js";
2
+ import type { ExecutionEnv } from "../../internal/harness-types.js";
3
+ import type { ToolCallRequest } from "../../core/tool-policy.js";
4
+ /** The REMOVAL programs — the reference implementation's dangerous-removal arm reads `rm` and `rmdir`, with or without a
5
+ * recursive option (removing an empty home or the task root is still removing it). */
6
+ export declare const SHELL_REMOVAL_COMMANDS: readonly ["rm", "rmdir"];
7
+ export type ShellRemovalCommand = (typeof SHELL_REMOVAL_COMMANDS)[number];
8
+ /** What makes a removal target dangerous, in the order a card names it. `runtime_script`: a shell's script text is built
9
+ * only when the command runs, and what it may run includes a removal nobody can read before it does. */
10
+ export type DangerousRemovalKind = "filesystem_root" | "home" | "workspace" | "top_level" | "runtime_script";
11
+ /** The protected locations a removal is judged against. `homes`: the environment's declared home and every other spelling
12
+ * of it (its canonical one); `workspace`: the task root, the working directory, the additional working directories. */
13
+ export interface RemovalSeat {
14
+ readonly homes: readonly string[];
15
+ readonly workspace: readonly string[];
16
+ }
17
+ /** One dangerous removal a command makes: the program (the removal program, or the shell a runtime-built script is handed
18
+ * to), the operand as written, the target as established (the script word as written, for `runtime_script`). */
19
+ export interface DangerousRemoval {
20
+ readonly kind: DangerousRemovalKind;
21
+ readonly program: string;
22
+ readonly operand: string;
23
+ readonly target: string;
24
+ }
25
+ /**
26
+ * The ONE dangerous-removal predicate: is removing this absolute path removing a protected location? The reference
27
+ * implementation's set (cli 2.1.291 `dme` / `xW`) — the filesystem root (a drive root, a UNC share root), a home directory
28
+ * (every spelling the seat holds), a workspace directory or any of its ANCESTORS (the task root, the working directory, an
29
+ * additional working directory — whether the home lies inside the task root or the task root inside the home), a top-level
30
+ * directory (one segment below the root). `undefined` for a relative path or any other target.
31
+ */
32
+ export declare function dangerousRemovalOf(path: string, seat: RemovalSeat): DangerousRemovalKind | undefined;
33
+ /** What one removal finding asks: a protected location (`dangerous_delete`), or a target the command builds from a value
34
+ * of its own that the guard cannot read (`unverifiable_delete`). */
35
+ export type RemovalFinding = (DangerousRemoval & {
36
+ readonly guard: "dangerous_delete";
37
+ }) | {
38
+ readonly guard: "unverifiable_delete";
39
+ readonly program: string;
40
+ readonly operand: string;
41
+ readonly name: string;
42
+ };
43
+ /**
44
+ * The removal findings of an established plan — ONE walk, nested plans included (a `bash -c` / `sh -c` text, a substitution
45
+ * body), two words. For every `rm` / `rmdir` segment, each of its OPERANDS (every write target that is not one of its
46
+ * option words: a word some implementation's reading of the program's row writes, {@link writeReadings}) as the plan
47
+ * established it on the WRITE face (the entry the program unlinks, and the deny-floor shadow of a home word the plan did
48
+ * not place) is judged by {@link dangerousRemovalOf}; an operand the plan could not place that expands a name the command
49
+ * itself binds is judged in every spelling those bindings give it ({@link boundSpellingsOf}), and one with a binding this
50
+ * reading cannot read is an unverifiable target (a recursive forced removal, {@link recursiveForce}). A shell handed a
51
+ * script built only when the command runs is a dangerous removal of its own ({@link runtimeScriptOf}). The read-face copy
52
+ * of an operand is not judged: it follows a link the removal does not follow.
53
+ */
54
+ export declare function removalFindingsOf(established: EstablishedPlan, seat: RemovalSeat): RemovalFinding[];
55
+ /** Does any segment of this plan (nested plans included) run a removal program, or hand a shell a script built when it
56
+ * runs? The guard establishes nothing for a command that does neither. */
57
+ export declare function planRunsRemoval(plan: EstablishedPlan["plan"]): boolean;
58
+ /** What the removal guard asks for one command: the guard word and its sentence's account — the first protected location
59
+ * (the program, the location, the target), else the first bound value the guard cannot read; `undefined` when neither. */
60
+ export interface RemovalVerdict {
61
+ readonly guard: RemovalFinding["guard"];
62
+ readonly text: string;
63
+ }
64
+ /**
65
+ * The seat the dangerous-removal arm of the removal guard (`createUnverifiableDeletePolicy`, tool-policy.ts) reads, beside the request's own `root` /
66
+ * `cwd` (the task root and the live working directory): the execution environment (its declared home — the `HOME` the
67
+ * shell child receives, the same seat the read station folds `~` / `$HOME` with — and its resolver, so a target that
68
+ * resolves through a link is judged where it lands), and the leg's additional working directories.
69
+ */
70
+ export interface RemovalGuardSeat {
71
+ readonly env?: ExecutionEnv;
72
+ readonly additionalRoots?: () => readonly string[];
73
+ }
74
+ /**
75
+ * The dangerous-removal READING of one command — undefined when no segment of it (nested shell text included) runs a
76
+ * removal program, so a command that removes nothing costs the gate no suspension. Otherwise `judge` establishes the plan
77
+ * the read station plans (the same planner, the same write-target reading) lexically against the request's `cwd` and the
78
+ * seat's home, and — where an environment is seated — through its resolver too, and returns what either establishment
79
+ * finds ({@link removalFindingsOf}: the protected-location word first, then the unverifiable one).
80
+ */
81
+ export declare function removalReadingOf(command: string, req: Pick<ToolCallRequest, "cwd" | "root">, seat: RemovalGuardSeat | undefined): {
82
+ judge(signal?: AbortSignal): Promise<RemovalVerdict | undefined>;
83
+ } | undefined;