@sema-agent/core 7.33.8 → 7.35.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 (228) hide show
  1. package/CHANGELOG.md +462 -3
  2. package/dist/agents/cross-session-judge.d.ts +16 -7
  3. package/dist/agents/cross-session-judge.js +18 -15
  4. package/dist/agents/peer-session-drain.d.ts +4 -0
  5. package/dist/agents/peer-session-drain.js +5 -2
  6. package/dist/agents/repair-loop.d.ts +1 -1
  7. package/dist/agents/subagent.d.ts +3 -0
  8. package/dist/agents/subagent.js +12 -3
  9. package/dist/brain/anthropic.js +1 -0
  10. package/dist/brain/errors.d.ts +23 -1
  11. package/dist/brain/errors.js +19 -2
  12. package/dist/brain/open-responses.js +1 -0
  13. package/dist/brain/openai.js +1 -0
  14. package/dist/brain/status-sink.d.ts +7 -1
  15. package/dist/brain/stream-engine.d.ts +3 -0
  16. package/dist/brain/stream-engine.js +2 -1
  17. package/dist/brain/stream-shared.d.ts +3 -1
  18. package/dist/brain/stream-shared.js +16 -4
  19. package/dist/core/a2a.d.ts +3 -1
  20. package/dist/core/a2a.js +1 -1
  21. package/dist/core/ask-origin.d.ts +379 -154
  22. package/dist/core/ask-origin.js +224 -45
  23. package/dist/core/ask-question.js +2 -1
  24. package/dist/core/ask-unresolvable-notice.d.ts +5 -2
  25. package/dist/core/ask-unresolvable-notice.js +4 -3
  26. package/dist/core/auto-mode-arming.d.ts +17 -4
  27. package/dist/core/auto-mode-arming.js +31 -6
  28. package/dist/core/auto-mode-defaults.d.ts +16 -7
  29. package/dist/core/auto-mode-defaults.js +2 -1
  30. package/dist/core/auto-mode-directive.d.ts +18 -30
  31. package/dist/core/auto-mode-directive.js +9 -18
  32. package/dist/core/auto-mode-prompt.d.ts +62 -1
  33. package/dist/core/auto-mode-prompt.js +42 -4
  34. package/dist/core/auto-mode-rebuild.d.ts +2 -1
  35. package/dist/core/auto-mode-rebuild.js +1 -0
  36. package/dist/core/auto-mode.d.ts +32 -3
  37. package/dist/core/auto-mode.js +80 -41
  38. package/dist/core/checkpoint-store.d.ts +54 -35
  39. package/dist/core/checkpoint-store.js +58 -3
  40. package/dist/core/child-clamp-seats.js +1 -0
  41. package/dist/core/classifier-unavailable-arm.d.ts +57 -0
  42. package/dist/core/classifier-unavailable-arm.js +17 -0
  43. package/dist/core/effective-path-target.d.ts +12 -0
  44. package/dist/core/effective-path-target.js +18 -7
  45. package/dist/core/engine-notice.d.ts +53 -2
  46. package/dist/core/fs-write-gate-policy.d.ts +5 -1
  47. package/dist/core/fs-write-gate-policy.js +4 -1
  48. package/dist/core/gate-fold.js +19 -3
  49. package/dist/core/gate-lanes.js +116 -50
  50. package/dist/core/gate-outcome.d.ts +40 -7
  51. package/dist/core/gate-outcome.js +26 -4
  52. package/dist/core/governance-codes.d.ts +18 -2
  53. package/dist/core/governance-codes.js +23 -1
  54. package/dist/core/hooks.d.ts +53 -29
  55. package/dist/core/hooks.js +1 -3
  56. package/dist/core/mcp.js +1 -1
  57. package/dist/core/message-utils.d.ts +2 -1
  58. package/dist/core/message-utils.js +2 -1
  59. package/dist/core/mode-policy.d.ts +185 -0
  60. package/dist/core/mode-policy.js +184 -0
  61. package/dist/core/oracle-isolation.d.ts +3 -3
  62. package/dist/core/permission-rule-model.d.ts +20 -3
  63. package/dist/core/permission-rule-model.js +40 -3
  64. package/dist/core/permission-rule-provider.d.ts +22 -11
  65. package/dist/core/permission-rule-provider.js +17 -7
  66. package/dist/core/permission-rules.d.ts +23 -1
  67. package/dist/core/permission-rules.js +56 -9
  68. package/dist/core/persisted-rule-arms.d.ts +14 -10
  69. package/dist/core/persisted-rule-arms.js +13 -6
  70. package/dist/core/present-plan-tool.d.ts +1 -1
  71. package/dist/core/present-plan-tool.js +3 -3
  72. package/dist/core/read-only-shell.d.ts +19 -19
  73. package/dist/core/read-only-shell.js +38 -12
  74. package/dist/core/retired-keys.d.ts +9 -3
  75. package/dist/core/retired-keys.js +5 -1
  76. package/dist/core/runner/active-skill-scope.js +5 -4
  77. package/dist/core/runner/assemble-result.js +2 -2
  78. package/dist/core/runner/classifier-ladder.d.ts +43 -1
  79. package/dist/core/runner/classifier-ladder.js +29 -0
  80. package/dist/core/runner/contracts.d.ts +62 -38
  81. package/dist/core/runner/denial-limit-arms.d.ts +35 -7
  82. package/dist/core/runner/denial-limit-arms.js +15 -9
  83. package/dist/core/runner/env-capability-census.js +1 -1
  84. package/dist/core/runner/file-history-request.d.ts +26 -0
  85. package/dist/core/runner/file-history-request.js +90 -0
  86. package/dist/core/runner/gate-exit.d.ts +30 -4
  87. package/dist/core/runner/gate-exit.js +28 -7
  88. package/dist/core/runner/permission-rule-lanes.d.ts +11 -6
  89. package/dist/core/runner/permission-rule-lanes.js +55 -48
  90. package/dist/core/runner/persisted-path-rows.d.ts +64 -0
  91. package/dist/core/runner/persisted-path-rows.js +89 -0
  92. package/dist/core/runner/prepare-artifact.d.ts +5 -1
  93. package/dist/core/runner/prepare-artifact.js +9 -3
  94. package/dist/core/runner/prepare-ask-lane.d.ts +6 -4
  95. package/dist/core/runner/prepare-ask-lane.js +3 -3
  96. package/dist/core/runner/prepare-caps-and-workflow.d.ts +39 -13
  97. package/dist/core/runner/prepare-caps-and-workflow.js +58 -29
  98. package/dist/core/runner/prepare-file-history.d.ts +9 -3
  99. package/dist/core/runner/prepare-file-history.js +4 -90
  100. package/dist/core/runner/prepare-gate-stations.d.ts +37 -9
  101. package/dist/core/runner/prepare-gate-stations.js +46 -29
  102. package/dist/core/runner/prepare-hands-readface.d.ts +40 -17
  103. package/dist/core/runner/prepare-hands-readface.js +64 -53
  104. package/dist/core/runner/prepare-inherited-gate.d.ts +30 -23
  105. package/dist/core/runner/prepare-inherited-gate.js +45 -25
  106. package/dist/core/runner/prepare-park-ask.d.ts +6 -4
  107. package/dist/core/runner/prepare-park-ask.js +7 -5
  108. package/dist/core/runner/prepare-policy-chain.d.ts +25 -4
  109. package/dist/core/runner/prepare-policy-chain.js +203 -180
  110. package/dist/core/runner/prepare-protocol-tools.d.ts +28 -0
  111. package/dist/core/runner/prepare-protocol-tools.js +13 -3
  112. package/dist/core/runner/prepare-run-refs.d.ts +6 -6
  113. package/dist/core/runner/prepare-run-refs.js +9 -4
  114. package/dist/core/runner/prepare-safety-scan.d.ts +5 -0
  115. package/dist/core/runner/prepare-safety-scan.js +19 -3
  116. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -5
  117. package/dist/core/runner/prepare-suspend-saga.js +6 -6
  118. package/dist/core/runner/prepare-task.js +56 -24
  119. package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -6
  120. package/dist/core/runner/prepare-wiring-manifest.js +5 -3
  121. package/dist/core/runner/read-root-grant.d.ts +9 -0
  122. package/dist/core/runner/read-root-grant.js +12 -0
  123. package/dist/core/runner/retired-runner-deps.js +1 -0
  124. package/dist/core/runner/run-brain-sinks.js +3 -1
  125. package/dist/core/runner/run-identity-wiring.js +1 -1
  126. package/dist/core/runner/run-leg.js +29 -18
  127. package/dist/core/runner/run-notification-lane.d.ts +3 -0
  128. package/dist/core/runner/run-notification-lane.js +3 -1
  129. package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
  130. package/dist/core/runner/run-telemetry-and-budget-seats.js +1 -1
  131. package/dist/core/runner/run-turn-boundary.js +1 -1
  132. package/dist/core/runner/runtask.d.ts +1 -0
  133. package/dist/core/runner/runtask.js +8 -11
  134. package/dist/core/runner/session-mode-seats.d.ts +48 -0
  135. package/dist/core/runner/session-mode-seats.js +73 -0
  136. package/dist/core/runner/session-rule-policy.js +3 -2
  137. package/dist/core/runner/stream-mode-verb.d.ts +27 -0
  138. package/dist/core/runner/stream-mode-verb.js +19 -0
  139. package/dist/core/runner/turn-attachments.d.ts +3 -3
  140. package/dist/core/runner/turn-attachments.js +1 -1
  141. package/dist/core/runner/write-protection-station.d.ts +7 -1
  142. package/dist/core/runner/write-protection-station.js +6 -3
  143. package/dist/core/runner-deps.d.ts +31 -34
  144. package/dist/core/runtime-caps.d.ts +12 -3
  145. package/dist/core/safety-axis-vocab.d.ts +2 -2
  146. package/dist/core/safety-axis-vocab.js +1 -1
  147. package/dist/core/secret-env.d.ts +30 -58
  148. package/dist/core/secret-env.js +149 -586
  149. package/dist/core/sensitive-path-policy.d.ts +4 -2
  150. package/dist/core/sensitive-path-policy.js +41 -31
  151. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
  152. package/dist/core/task-event.d.ts +1 -1
  153. package/dist/core/task-result.d.ts +1 -1
  154. package/dist/core/task-spec.d.ts +41 -89
  155. package/dist/core/task-stream.d.ts +16 -0
  156. package/dist/core/terminal-cause.js +1 -0
  157. package/dist/core/tighten-task-spec.d.ts +2 -2
  158. package/dist/core/tighten-task-spec.js +12 -14
  159. package/dist/core/tool-catalog-entries.js +4 -3
  160. package/dist/core/tool-policy.d.ts +84 -30
  161. package/dist/core/tool-policy.js +76 -16
  162. package/dist/core/tool-registry.d.ts +38 -0
  163. package/dist/core/tool-registry.js +25 -1
  164. package/dist/core/tool-roster.d.ts +17 -2
  165. package/dist/core/tool-roster.js +11 -1
  166. package/dist/core/tool-spec.d.ts +8 -9
  167. package/dist/core/trace.d.ts +18 -6
  168. package/dist/core/types.d.ts +2 -1
  169. package/dist/core/wiring-manifest.d.ts +31 -5
  170. package/dist/core/wiring-manifest.js +28 -1
  171. package/dist/core/write-protect.d.ts +11 -1
  172. package/dist/core/write-protect.js +26 -15
  173. package/dist/engine/execution-env/node-execution-env.d.ts +6 -1
  174. package/dist/engine/execution-env/node-execution-env.js +16 -3
  175. package/dist/engine/harness/agent-harness.js +2 -1
  176. package/dist/engine/harness/types.d.ts +6 -7
  177. package/dist/engine/llm/types.d.ts +2 -4
  178. package/dist/engine/loop/types.d.ts +7 -0
  179. package/dist/engine/lsp/node-lsp-manager.js +1 -1
  180. package/dist/index.d.ts +18 -8
  181. package/dist/index.js +15 -6
  182. package/dist/orchestration/workflow-governance.js +1 -1
  183. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  184. package/dist/orchestration/workflow-size-guideline.js +2 -2
  185. package/dist/stores/file/index.d.ts +5 -3
  186. package/dist/stores/file/session-store.d.ts +8 -2
  187. package/dist/stores/file/session-store.js +5 -5
  188. package/dist/tools/artifact/artifact-tool.d.ts +17 -0
  189. package/dist/tools/artifact/artifact-tool.js +16 -1
  190. package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
  191. package/dist/tools/fs/bash-readonly-classifier.js +88 -0
  192. package/dist/tools/fs/containment-face.d.ts +9 -3
  193. package/dist/tools/fs/containment-face.js +2 -0
  194. package/dist/tools/fs/file-read-check.d.ts +50 -0
  195. package/dist/tools/fs/file-read-check.js +79 -0
  196. package/dist/tools/fs/fs-bash.d.ts +3 -3
  197. package/dist/tools/fs/fs-read.d.ts +1 -0
  198. package/dist/tools/fs/fs-search-tools.d.ts +2 -2
  199. package/dist/tools/fs/fs-search-tools.js +4 -4
  200. package/dist/tools/fs/fs-shared.js +20 -6
  201. package/dist/tools/fs/fs-write.d.ts +4 -3
  202. package/dist/tools/fs/fs-write.js +9 -9
  203. package/dist/tools/fs/index.d.ts +39 -3
  204. package/dist/tools/fs/index.js +38 -15
  205. package/dist/tools/fs/read-deny-home.d.ts +93 -0
  206. package/dist/tools/fs/read-deny-home.js +225 -0
  207. package/dist/tools/fs/read-deny.d.ts +176 -34
  208. package/dist/tools/fs/read-deny.js +268 -61
  209. package/dist/tools/fs/safety.d.ts +6 -1
  210. package/dist/tools/fs/safety.js +18 -5
  211. package/dist/tools/fs/search.d.ts +3 -0
  212. package/dist/tools/fs/search.js +47 -9
  213. package/dist/tools/fs/shell-read-check.d.ts +52 -13
  214. package/dist/tools/fs/shell-read-check.js +199 -20
  215. package/dist/tools/fs/shell-read-establish.d.ts +7 -3
  216. package/dist/tools/fs/shell-read-establish.js +134 -21
  217. package/dist/tools/fs/shell-read-identity.d.ts +7 -0
  218. package/dist/tools/fs/shell-read-identity.js +21 -0
  219. package/dist/tools/fs/shell-read-judge.d.ts +92 -32
  220. package/dist/tools/fs/shell-read-judge.js +43 -18
  221. package/dist/tools/fs/shell-read-plan.d.ts +4 -0
  222. package/dist/tools/fs/shell-read-plan.js +26 -4
  223. package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
  224. package/dist/tools/fs/shell-read-write-targets.js +173 -0
  225. package/package.json +1 -1
  226. package/test/export-surface.snapshot.json +117 -11
  227. package/wire-changes/core@7.34.0.json +2621 -0
  228. package/wire-changes/core@7.35.0.json +584 -0
@@ -2,7 +2,7 @@ import { hasWin32AliasedComponent, normalizeAbsPathLexically, withinAnyRoot } fr
2
2
  import { globInWord } from "./bash-readonly-classifier.js";
3
3
  import { readOfText } from "./shell-read-word.js";
4
4
  export const SHELL_READ_FINDINGS = ["deny_hit", "outside_roots", "unestablishable", "declined", "identity_changed", "none"];
5
- export const SHELL_READ_SEATS = ["bash_off", "bash_classify", "readonly", "rejudge"];
5
+ export const SHELL_READ_SEATS = ["full_shell", "readonly", "rejudge"];
6
6
  export const SHELL_READ_FINDING_RANK = {
7
7
  identity_changed: 6,
8
8
  deny_hit: 5,
@@ -12,27 +12,36 @@ export const SHELL_READ_FINDING_RANK = {
12
12
  none: 0,
13
13
  };
14
14
  export const SHELL_READ_DECISION = {
15
- deny_hit: { bash_off: "refuse", bash_classify: "refuse", readonly: "refuse", rejudge: "refuse" },
16
- outside_roots: { bash_off: "mandated", bash_classify: "mandated", readonly: "refuse", rejudge: "refuse" },
17
- unestablishable: { bash_off: "mandated", bash_classify: "mandated", readonly: "refuse", rejudge: "refuse" },
18
- declined: { bash_off: "mandated", bash_classify: "ordinary", readonly: "refuse", rejudge: "refuse" },
19
- identity_changed: { bash_off: "refuse", bash_classify: "refuse", readonly: "refuse", rejudge: "refuse" },
20
- none: { bash_off: "vouch", bash_classify: "vouch", readonly: "vouch", rejudge: "vouch" },
15
+ deny_hit: { full_shell: "refuse", readonly: "refuse", rejudge: "refuse" },
16
+ outside_roots: { full_shell: "mandated", readonly: "refuse", rejudge: "refuse" },
17
+ unestablishable: { full_shell: "mandated", readonly: "refuse", rejudge: "refuse" },
18
+ declined: { full_shell: "ordinary", readonly: "refuse", rejudge: "refuse" },
19
+ identity_changed: { full_shell: "refuse", readonly: "refuse", rejudge: "refuse" },
20
+ none: { full_shell: "vouch", readonly: "vouch", rejudge: "vouch" },
21
21
  };
22
22
  export function decideShellRead(finding, seat) {
23
- return SHELL_READ_DECISION[finding][seat];
23
+ const row = Object.hasOwn(SHELL_READ_DECISION, finding) ? SHELL_READ_DECISION[finding] : undefined;
24
+ const disposition = row !== undefined && Object.hasOwn(row, seat) ? row[seat] : undefined;
25
+ if (disposition === undefined) {
26
+ throw new Error(`shell read decision: no cell for finding ${JSON.stringify(finding)} on seat ${JSON.stringify(seat)} — the seats are ${SHELL_READ_SEATS.map((w) => JSON.stringify(w)).join(", ")} (a seat names the face the command runs on)`);
27
+ }
28
+ return disposition;
24
29
  }
25
30
  export function judgeRead(read, boundary) {
26
- const deny = boundary.denyMatch;
31
+ const writeTarget = read.candidate.face === "write";
32
+ const deny = writeTarget ? boundary.writeDenyMatch : boundary.denyMatch;
27
33
  if (deny !== undefined) {
34
+ const walked = read.walked === true ? { walked: true } : undefined;
28
35
  const views = [...(read.target !== undefined ? [read.target] : []), ...read.spellings];
29
36
  for (const view of views) {
30
37
  const folded = normalizeAbsPathLexically(view);
31
- const pattern = deny(folded);
38
+ const pattern = writeTarget ? boundary.writeDenyMatch(folded) : boundary.denyMatch(folded, walked);
32
39
  if (pattern !== null)
33
- return { read, kind: "deny_hit", deny: { pattern, matchedView: folded }, containment: containmentOf(read, boundary) };
40
+ return { read, kind: "deny_hit", deny: { pattern, matchedView: folded, ...(writeTarget ? { face: "write" } : {}) }, containment: containmentOf(read, boundary) };
34
41
  }
35
42
  }
43
+ if (writeTarget)
44
+ return { read, kind: "none", containment: containmentOf(read, boundary) };
36
45
  const containment = containmentOf(read, boundary);
37
46
  if (read.candidate.denyOnly === true)
38
47
  return { read, kind: "none", containment };
@@ -43,8 +52,18 @@ export function judgeRead(read, boundary) {
43
52
  return { read, kind: "unestablishable", containment };
44
53
  return { read, kind: containment === "outside" ? "outside_roots" : "none", containment };
45
54
  }
55
+ export function denyRowAtOrBelow(read, boundary, carriesContent) {
56
+ if (boundary.writeDenyMatch !== undefined)
57
+ return true;
58
+ if (!carriesContent)
59
+ return false;
60
+ const atOrBelow = boundary.denyAtOrBelow;
61
+ if (atOrBelow === undefined)
62
+ return false;
63
+ return [...(read.target !== undefined ? [read.target] : []), ...read.spellings].some((view) => atOrBelow(normalizeAbsPathLexically(view)) !== null);
64
+ }
46
65
  export function denyFloorShadowsOf(candidates) {
47
- return candidates.flatMap((c) => (c.floorSpelled === undefined ? [] : [{ spelled: c.floorSpelled, raw: c.raw, provenance: c.provenance, expands: globInWord(readOfText(c.raw).word) ? "glob" : false, bases: [], denyOnly: true }]));
66
+ return candidates.flatMap((c) => (c.floorSpelled === undefined ? [] : [{ spelled: c.floorSpelled, raw: c.raw, provenance: c.provenance, expands: globInWord(readOfText(c.raw).word) ? "glob" : false, bases: [], denyOnly: true, ...(c.face !== undefined ? { face: c.face } : {}) }]));
48
67
  }
49
68
  function containmentOf(read, boundary) {
50
69
  if (boundary.face === "open")
@@ -55,7 +74,10 @@ function containmentOf(read, boundary) {
55
74
  }
56
75
  export function foldShellReadFindings(judged) {
57
76
  let best = { finding: "none" };
77
+ let unestablished = false;
58
78
  const raise = (next) => {
79
+ if (next.finding === "unestablishable" || next.finding === "declined")
80
+ unestablished = true;
59
81
  if (SHELL_READ_FINDING_RANK[next.finding] > SHELL_READ_FINDING_RANK[best.finding])
60
82
  best = next;
61
83
  };
@@ -86,7 +108,7 @@ export function foldShellReadFindings(judged) {
86
108
  }
87
109
  };
88
110
  walk(judged, false);
89
- return best;
111
+ return unestablished ? { ...best, unestablished: true } : best;
90
112
  }
91
113
  export function judgeEstablishedPlan(plan, established, boundary, nestedOf) {
92
114
  return {
@@ -110,7 +132,7 @@ export function allFindings(judged) {
110
132
  walk(judged);
111
133
  return out;
112
134
  }
113
- function parentDirLexically(p) {
135
+ export function parentDirLexically(p) {
114
136
  return normalizeAbsPathLexically(`${p}/..`);
115
137
  }
116
138
  export function deepestCommonDir(a, b) {
@@ -157,16 +179,19 @@ export function readRootGrantCandidateOf(judged, boundary) {
157
179
  }
158
180
  if (candidate === undefined)
159
181
  return undefined;
160
- const tooWide = parentDirLexically(candidate) === candidate ||
161
- (boundary.homeDir !== undefined && withinAnyRoot([candidate], boundary.homeDir)) ||
162
- boundary.roots.some((root) => withinAnyRoot([candidate], root));
163
- const proposed = tooWide ? (paths.length === 1 ? { dir: paths[0], covers: "exact" } : undefined) : { dir: candidate };
182
+ const proposed = grantOfferFor(candidate, paths.length === 1 ? paths[0] : undefined, boundary);
164
183
  if (proposed === undefined)
165
184
  return undefined;
166
185
  const widened = { ...boundary, roots: [...boundary.roots, proposed.dir] };
167
186
  const replay = rejudge(judged, widened);
168
187
  return foldShellReadFindings(replay).finding === "none" ? proposed : undefined;
169
188
  }
189
+ export function grantOfferFor(candidate, single, bounds) {
190
+ const tooWide = parentDirLexically(candidate) === candidate ||
191
+ (bounds.homeDir !== undefined && withinAnyRoot([candidate], bounds.homeDir)) ||
192
+ bounds.roots.some((root) => withinAnyRoot([candidate], root));
193
+ return tooWide ? (single !== undefined ? { dir: single, covers: "exact" } : undefined) : { dir: candidate };
194
+ }
170
195
  export function rejudge(judged, boundary) {
171
196
  return {
172
197
  plan: judged.plan,
@@ -13,6 +13,7 @@
13
13
  * · a LISTED reader hands its positional operands and the declared option-value slots (the verb tables of
14
14
  * `bash-readonly-classifier.ts`, read here through their exported readers, never copied);
15
15
  * · an UNLISTED program hands every word of its argv (a program declared to open no path operand hands none);
16
+ * · an ASSIGNMENT's value is a candidate only in a text that runs a program able to open a file;
16
17
  * · a LAUNCHER chain is peeled to its program and the chain's own words are never candidates; a launcher
17
18
  * option the row cannot read is recorded as a decline and every word after it is handed over;
18
19
  * · a SHELL re-entry (`sh -c S`, `eval S`, a launcher handing text to a shell, `env -S S`, a builtin's code-body
@@ -67,6 +68,9 @@ export interface ReadCandidate {
67
68
  readonly floorSpelled?: string;
68
69
  /** A deny-floor shadow of a `floorSpelled` spelling, established as a read and judged for deny rows only. */
69
70
  readonly denyOnly?: true;
71
+ /** A WRITE target of the command (a `>`-family redirection, a `sed` `w` file, the operands of a file-writing command):
72
+ * established like a read and judged against the deny table's WRITE face only — never for containment, never a read. */
73
+ readonly face?: "write";
70
74
  }
71
75
  /** How the shell reached one segment — what the base-candidate walk needs from the connectors. */
72
76
  export interface SegmentReach {
@@ -1,7 +1,7 @@
1
1
  import { lexShell, PARSE_FAILURE_SENTENCE, substitutionsInHeredocBody, literalTildeSpelling, } from "../../shell/lex.js";
2
2
  import { compoundArrayElements, dropDefinitionHeads, expandsOf, readOfText, readsOf, rewritten, spelledWithHome, isArithmeticBody, unreadableSentenceOf } from "./shell-read-word.js";
3
3
  import { LAUNCHER_TABLE, SHELL_BLOCK_WORDS, programBasename, programPositionRunsShell, readProgramPosition, shellScanDeclineOf, } from "./bash-program-position.js";
4
- import { ASSIGNMENT_VALUE_NOT_PATHS, INTERPRETER_INLINE_PROGRAMS, NO_PATH_OPERAND_COMMANDS, POLL_LOOP_MAX_BEATS, RECURSIVE_READ_FORMS, SHELL_REENTRY_PROGRAM_RE, UNMODELLED_BASE_MOVERS, argvProgramOf, homeMayBeRebound, attachedOptionPayloads, resolveOperandLexically, inlineProgramClause, interpreterKey, isPathShapedToken, readVerbOptionSlot, sedScriptSlotFiles, segmentSelectsRecursiveRead, slotValueCandidates, suppliesFirstOperandSlot, verbFirstOperandSlot, verbOptionsArePositionIndependent, } from "./bash-readonly-classifier.js";
4
+ import { ASSIGNMENT_VALUE_NOT_PATHS, DECLARATION_BUILTINS, INERT_BUILTINS, INTERPRETER_INLINE_PROGRAMS, NO_PATH_OPERAND_COMMANDS, NO_PATH_OPERAND_EVALUATORS, POLL_LOOP_MAX_BEATS, RECURSIVE_READ_FORMS, SHELL_REENTRY_PROGRAM_RE, UNMODELLED_BASE_MOVERS, argvProgramOf, homeMayBeRebound, attachedOptionPayloads, resolveOperandLexically, inlineProgramClause, interpreterKey, isPathShapedToken, messageSlotWords, readVerbOptionSlot, sedScriptSlotFiles, segmentSelectsRecursiveRead, slotValueCandidates, suppliesFirstOperandSlot, verbFirstOperandSlot, verbOptionsArePositionIndependent, } from "./bash-readonly-classifier.js";
5
5
  import { isAbsolutePathForm } from "./safety.js";
6
6
  import { codeBodies, reentryText } from "./shell-reentry-text.js";
7
7
  export const READ_CANDIDATE_PROVENANCES = [
@@ -29,7 +29,9 @@ export function planShellReads(command, decl, depth = 0, homeRebound = false, ar
29
29
  const unreadable = unreadableSentenceOf(lexed, command, !arithmetic);
30
30
  const nestedPlan = (text, arithmetic = false) => planShellReads(text, decl, depth + 1, home === undefined, arithmetic);
31
31
  const projected = lexed.segments.map((segment) => projectSegment(segment));
32
- const facts = projected.map((p, si) => segmentFacts(lexed.segments[si], p, decl, home, nestedPlan));
32
+ const planned = projected.map((p, si) => segmentFacts(lexed.segments[si], p, decl, home, nestedPlan));
33
+ const inert = depth === 0 && unreadable === undefined && planned.every(segmentOpensNothing);
34
+ const facts = inert ? planned.map((f) => ({ ...f, reads: f.reads.filter((c) => c.provenance !== "env_assignment") })) : planned;
33
35
  shape.grouped = projected.some((p) => p.grouped);
34
36
  shape.backgrounded = lexed.segments.some((s) => s.connector === "&") || facts.some((f) => f.backgrounds === true);
35
37
  shape.strayRedirection = projected.some((p) => p.argv.length === 0 && p.redirects.length > 0);
@@ -165,6 +167,14 @@ export function planShellReads(command, decl, depth = 0, homeRebound = false, ar
165
167
  }
166
168
  return { segments, shape, ...(unreadable !== undefined ? { unreadable } : {}) };
167
169
  }
170
+ function segmentOpensNothing(f) {
171
+ if (f.empty)
172
+ return true;
173
+ const program = f.verb;
174
+ const at = program === undefined ? -1 : f.argv.findIndex((w) => w.folded === program.name);
175
+ const inertProgram = program === undefined || (program.name === program.basename && INERT_BUILTINS.has(program.basename) && !(DECLARATION_BUILTINS.has(program.basename) && f.argv.slice(at + 1).some((w) => /^[-+]/.test(w.folded))));
176
+ return inertProgram && f.argv.every((w) => !/[[\]$`]/.test(w.raw)) && f.nested.length === 0 && f.reentry === undefined && f.declines.length === 0 && f.launcherChain.length === 0 && f.redirects.every((r) => r.direction !== "in") && f.reads.every((c) => c.provenance === "env_assignment");
177
+ }
168
178
  function projectSegment(segment) {
169
179
  const tokens = [...segment.tokens];
170
180
  let grouped = tokens.some((t) => t.kind === "group");
@@ -389,6 +399,17 @@ function segmentFactsRead(segment, p, decl, home, nestedPlan) {
389
399
  else if (listed) {
390
400
  listedVerbCandidates(basename, argReads, reads);
391
401
  }
402
+ else if (DECLARATION_BUILTINS.has(basename) && program.name === basename) {
403
+ for (const r of argReads) {
404
+ const a = r.word.assignment;
405
+ if (a !== undefined && (r.expands !== "unresolvable" || a.value.expansions.length > 0))
406
+ pushAssignment(reads, r.word, "env_assignment");
407
+ else if (r.expands === "unresolvable") {
408
+ reads.push(bare(r, "unlisted_word"));
409
+ pushAssignment(reads, r.word, "unlisted_word");
410
+ }
411
+ }
412
+ }
392
413
  else {
393
414
  unlistedWords(reads, toks, program.index, launcherIndices, "unlisted_word", program);
394
415
  }
@@ -518,6 +539,7 @@ function unlistedWords(reads, toks, from, launcherIndices, provenance, program)
518
539
  const argv = program !== undefined ? { program: program.basename, from: program.index } : argvProgramOf(toks.map((r) => r.word));
519
540
  const argvStart = argv?.from ?? toks.length;
520
541
  const slotVerb = argv?.program;
542
+ const message = slotVerb === undefined || argv === undefined ? new Set() : messageSlotWords(slotVerb, toks, argvStart);
521
543
  const declaredNotPaths = ASSIGNMENT_VALUE_NOT_PATHS.has(slotVerb ?? "");
522
544
  const noPathOperands = slotVerb !== undefined && NO_PATH_OPERAND_COMMANDS.has(slotVerb);
523
545
  const permutes = slotVerb === undefined || !verbOptionsArePositionIndependent(slotVerb);
@@ -525,7 +547,7 @@ function unlistedWords(reads, toks, from, launcherIndices, provenance, program)
525
547
  for (let j = from; j < toks.length; j++) {
526
548
  if (launcherIndices.has(j))
527
549
  continue;
528
- if (noPathOperands && j > argvStart)
550
+ if (noPathOperands && j > argvStart && !NO_PATH_OPERAND_EVALUATORS.has(slotVerb))
529
551
  break;
530
552
  if (toks[j].expands !== "unresolvable")
531
553
  continue;
@@ -541,7 +563,7 @@ function unlistedWords(reads, toks, from, launcherIndices, provenance, program)
541
563
  let endOfOptions = false;
542
564
  let sawArgvOperand = false;
543
565
  for (let j = from; j < toks.length; j++) {
544
- if (launcherIndices.has(j) || expanding.has(j))
566
+ if (launcherIndices.has(j) || expanding.has(j) || message.has(j))
545
567
  continue;
546
568
  const r = toks[j];
547
569
  const word = r.folded;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * WRITE TARGETS — which words of a shell segment a program writes, under which names, and at which seat, read off the
3
+ * plan's text alone (no filesystem): the `>`-family redirections and a `sed` script's `w` files, the operands of the
4
+ * write commands ({@link SHELL_WRITE_COMMANDS}), a copy's or a move's destination children (GNU's argument grammar), an
5
+ * in-place `sed`'s backups, and the seat each is written at ({@link WriteSeat}: the directory entry, or where the path
6
+ * lands). The establisher (`shell-read-establish.ts`) places what this module names; the judge reads the result on the
7
+ * deny table's write face; the station's `writesPlaced` fact reads the option readers exported here.
8
+ */
9
+ import type { ReadCandidate, ShellReadSegment } from "./shell-read-plan.js";
10
+ /**
11
+ * The commands whose operands are WRITE targets — the reference implementation's path-checked commands whose operation
12
+ * is a write or a create (`mkdir` / `touch` create; `rm` / `rmdir` / `mv` / `cp` / `tee` write; `sed` writes when it
13
+ * edits in place). Every operand counts (a `cp` source is judged as a write too, as the reference judges it): the write
14
+ * face only ever REFUSES, so a word read as a target it is not costs a refusal of a command that names a denied path.
15
+ */
16
+ export declare const SHELL_WRITE_COMMANDS: readonly ["mkdir", "touch", "rm", "rmdir", "mv", "cp", "tee", "sed"];
17
+ /**
18
+ * Does an option WORD spell this option — the long name or any abbreviation of it GNU getopt accepts (`--in-p`, `--fi`,
19
+ * `--par`: an unambiguous prefix, `=value` allowed), or the short letter alone or inside a bundle (`-i`, `-ni`, `-i.bak`)?
20
+ * Read in the tightening direction: a prefix that another long option shares still counts (the program would refuse it).
21
+ */
22
+ export declare function spellsOption(word: string, long: string, short: string): boolean;
23
+ /** A word of a program's argv: its raw spelling (the plan's candidates are keyed by it) and its folded spelling. */
24
+ export interface ArgWord {
25
+ readonly raw: string;
26
+ readonly spelled: string;
27
+ }
28
+ /** The words a segment hands its program — the argv after the program word (a launcher's own words are not the program's). */
29
+ export declare function programArgs(segment: ShellReadSegment): ArgWord[];
30
+ /** What a `sed` invocation's option words say about writing, under either grammar ({@link SED_GRAMMARS}): whether it edits
31
+ * in place (`-i` / `-I` / `--in-place`), the backup suffixes it is given (their values; an empty value keeps no backup),
32
+ * and whether GNU `--follow-symlinks` is on. */
33
+ export interface SedOptions {
34
+ readonly inPlace: boolean;
35
+ readonly suffixes: readonly string[];
36
+ readonly followsLinks: boolean;
37
+ }
38
+ export declare function sedOptionsOf(segment: ShellReadSegment): SedOptions;
39
+ /**
40
+ * The BACKUP names an in-place `sed` gives the file it knows as `name` — both readings a suffix has: GNU sed replaces every
41
+ * `*` in the suffix with the name (`bak/*` on `d/f` is `bak/d/f`, `/elsewhere/*` is `/elsewhere/d/f`) and appends a suffix
42
+ * without one (`d/f.bak`); the BSD and BusyBox seds append the suffix literally (`d/f` + `bak/*`). A name resolves against
43
+ * the working directory like the operand itself, so a suffix carrying a separator places the backup wherever it points.
44
+ */
45
+ export declare function sedBackupNames(name: string, suffix: string): string[];
46
+ /**
47
+ * WHERE a write reaches a path it names. `landing` — the path followed through every link to where it lands: the program
48
+ * opens the path and writes through it. `entry` — the directory entry itself, the canonical parent joined with the last
49
+ * name: the program renames over the entry, unlinks it or replaces it, and a link there is not followed (a name ending in
50
+ * a separator, `.` or `..` resolves itself, so it lands).
51
+ */
52
+ export type WriteSeat = "landing" | "entry";
53
+ /** One candidate handed to the judge. `seats`: where a write target is established ({@link WriteSeat}; absent ⇒ it lands —
54
+ * every read does). `written`: the seats among `seats` the program itself WRITES the target at (absent ⇒ all of them;
55
+ * `[]` ⇒ none — a copy's source, judged on the write face as a write path and read by the program); the write guards are
56
+ * asked about these alone. `child`: a copy's or a move's destination child, written only where the destination is a
57
+ * directory when the program runs. `followed`: a GNU sed `--follow-symlinks` backup — its names are {@link sedBackupNames}
58
+ * of the operand's FOLLOWED name, which only the filesystem answers (the candidate's own spelling is the operand's as
59
+ * written, the identity the pre-spawn re-check compares). */
60
+ export interface JudgedCandidate {
61
+ readonly candidate: ReadCandidate;
62
+ readonly seats?: readonly WriteSeat[];
63
+ readonly written?: readonly WriteSeat[];
64
+ readonly child?: true;
65
+ readonly followed?: {
66
+ readonly operand: ReadCandidate;
67
+ readonly suffix: string;
68
+ };
69
+ }
70
+ /** Every candidate a segment hands the judge: its reads minus the write targets, plus its `cd` target (the plan already
71
+ * lists it among the reads), plus its WRITE targets for the write face ({@link writeTargetsOf}), plus the deny-floor
72
+ * shadow of every unplaced home word ({@link denyFloorShadowsOf}) on both faces. */
73
+ export declare function judgedCandidatesOf(segment: ShellReadSegment): readonly JudgedCandidate[];
@@ -0,0 +1,173 @@
1
+ import { denyFloorShadowsOf } from "./shell-read-judge.js";
2
+ export const SHELL_WRITE_COMMANDS = ["mkdir", "touch", "rm", "rmdir", "mv", "cp", "tee", "sed"];
3
+ const WRITE_COMMAND_SET = new Set(SHELL_WRITE_COMMANDS);
4
+ export function spellsOption(word, long, short) {
5
+ if (word.startsWith("--")) {
6
+ const name = word.slice(2).split("=")[0];
7
+ return name.length > 0 && long.startsWith(name);
8
+ }
9
+ return new RegExp(`^-[A-Za-z]*${short}`).test(word);
10
+ }
11
+ export function programArgs(segment) {
12
+ const words = segment.argv.map((w) => ({ raw: w.raw, spelled: w.folded }));
13
+ const at = words.findIndex((w) => w.spelled === segment.verb?.name);
14
+ return at < 0 ? [] : words.slice(at + 1);
15
+ }
16
+ const SED_GRAMMARS = [
17
+ { required: "efl", optional: "i", longRequired: ["expression", "file", "line-length"] },
18
+ { required: "efiI", optional: "" },
19
+ ];
20
+ const COPY_GRAMMARS = [
21
+ { required: "St", optional: "", longRequired: ["target-directory", "suffix", "no-preserve", "sparse"] },
22
+ { required: "", optional: "" },
23
+ ];
24
+ function readArgv(words, grammar) {
25
+ const options = [];
26
+ const operands = [];
27
+ let endOfOptions = false;
28
+ for (let k = 0; k < words.length; k++) {
29
+ const w = words[k];
30
+ const s = w.spelled;
31
+ if (endOfOptions || !s.startsWith("-") || s === "-") {
32
+ operands.push(w);
33
+ continue;
34
+ }
35
+ if (s === "--") {
36
+ endOfOptions = true;
37
+ continue;
38
+ }
39
+ if (s.startsWith("--")) {
40
+ const eq = s.indexOf("=");
41
+ const name = s.slice(2, eq < 0 ? undefined : eq);
42
+ if (grammar.longRequired === undefined || name.length === 0)
43
+ continue;
44
+ const value = eq >= 0 ? { raw: w.raw, spelled: s.slice(eq + 1) } : grammar.longRequired.some((long) => long.startsWith(name)) ? words[++k] : undefined;
45
+ options.push({ name, long: true, ...(value !== undefined ? { value } : {}) });
46
+ continue;
47
+ }
48
+ for (let c = 1; c < s.length; c++) {
49
+ const letter = s[c];
50
+ const rest = s.slice(c + 1);
51
+ if (grammar.required.includes(letter)) {
52
+ const value = rest.length > 0 ? { raw: w.raw, spelled: rest } : words[++k];
53
+ options.push({ name: letter, long: false, ...(value !== undefined ? { value } : {}) });
54
+ break;
55
+ }
56
+ if (grammar.optional.includes(letter)) {
57
+ options.push({ name: letter, long: false, ...(rest.length > 0 ? { value: { raw: w.raw, spelled: rest } } : {}) });
58
+ break;
59
+ }
60
+ options.push({ name: letter, long: false });
61
+ }
62
+ }
63
+ return { options, operands };
64
+ }
65
+ export function sedOptionsOf(segment) {
66
+ const words = programArgs(segment);
67
+ let inPlace = false;
68
+ let followsLinks = false;
69
+ const suffixes = new Set();
70
+ for (const grammar of SED_GRAMMARS) {
71
+ for (const o of readArgv(words, grammar).options) {
72
+ if (o.long ? "in-place".startsWith(o.name) : o.name === "i" || o.name === "I") {
73
+ inPlace = true;
74
+ if (o.value !== undefined && o.value.spelled.length > 0)
75
+ suffixes.add(o.value.spelled);
76
+ }
77
+ if (o.long && "follow-symlinks".startsWith(o.name))
78
+ followsLinks = true;
79
+ }
80
+ }
81
+ return { inPlace, suffixes: [...suffixes], followsLinks };
82
+ }
83
+ export function sedBackupNames(name, suffix) {
84
+ const appended = `${name}${suffix}`;
85
+ return suffix.includes("*") ? [suffix.replace(/\*/g, () => name), appended] : [appended];
86
+ }
87
+ const OPERAND_SEATS = {
88
+ touch: (s) => (programArgs(s).some((w) => w.spelled.startsWith("-") && spellsOption(w.spelled, "no-dereference", "h")) ? ["landing", "entry"] : ["landing"]),
89
+ rm: () => ["entry"],
90
+ rmdir: () => ["entry"],
91
+ sed: (s) => (sedOptionsOf(s).followsLinks ? ["landing"] : ["entry"]),
92
+ mv: () => ["landing", "entry"],
93
+ cp: () => ["landing", "entry"],
94
+ };
95
+ const SOURCE_WRITTEN = { cp: [], mv: ["entry"] };
96
+ function sedBackupTargets(segment) {
97
+ const { suffixes, followsLinks } = sedOptionsOf(segment);
98
+ if (suffixes.length === 0)
99
+ return [];
100
+ const files = segment.reads.filter((c) => c.provenance === "operand" && c.expands === false);
101
+ return files.flatMap((f) => suffixes.flatMap((suffix) => followsLinks
102
+ ? [{ candidate: { ...f, spelled: `${f.spelled}${suffix}`, face: "write" }, seats: ["entry"], followed: { operand: f, suffix } }]
103
+ : sedBackupNames(f.spelled, suffix).map((spelled) => ({ candidate: { ...f, spelled, face: "write" }, seats: ["entry"] }))));
104
+ }
105
+ function copyReadingsOf(segment) {
106
+ const out = [];
107
+ for (const grammar of COPY_GRAMMARS) {
108
+ const { options, operands } = readArgv(programArgs(segment), grammar);
109
+ const sources = [...operands];
110
+ const dest = options.findLast((o) => o.value !== undefined && (o.long ? "target-directory".startsWith(o.name) : o.name === "t"))?.value ?? sources.pop();
111
+ if (dest !== undefined)
112
+ out.push({ dest, sources });
113
+ }
114
+ return out;
115
+ }
116
+ function copySourceWordsOf(segment) {
117
+ const readings = copyReadingsOf(segment);
118
+ if (readings.length < COPY_GRAMMARS.length || programArgs(segment).some((w) => w.spelled.startsWith("-")))
119
+ return new Set();
120
+ const [first, ...rest] = readings.map((r) => new Set(r.sources.map((w) => w.raw)));
121
+ const dests = new Set(readings.map((r) => r.dest.raw));
122
+ return new Set([...first].filter((raw) => !dests.has(raw) && rest.every((s) => s.has(raw))));
123
+ }
124
+ function copyDestinationChildren(segment) {
125
+ const candidateOf = (raw) => segment.reads.find((c) => c.raw === raw && (c.provenance === "unlisted_word" || c.provenance === "operand"));
126
+ const children = new Map();
127
+ for (const { dest, sources } of copyReadingsOf(segment)) {
128
+ const destCandidate = candidateOf(dest.raw);
129
+ if (destCandidate !== undefined && destCandidate.expands !== false)
130
+ continue;
131
+ for (const src of sources) {
132
+ const source = candidateOf(src.raw);
133
+ if (source !== undefined && source.expands !== false)
134
+ continue;
135
+ const child = `${dest.spelled.replace(/\/+$/, "")}/${src.spelled.replace(/\/+$/, "").split("/").pop() ?? ""}`;
136
+ if (child.endsWith("/") || children.has(child))
137
+ continue;
138
+ children.set(child, destCandidate !== undefined && source !== undefined ? { ...destCandidate, spelled: child } : { spelled: child, raw: src.raw, provenance: "unlisted_word", expands: "unresolvable", bases: [] });
139
+ }
140
+ }
141
+ return [...children.values()];
142
+ }
143
+ function writeTargetsOf(segment) {
144
+ const out = [];
145
+ const write = (c, seats, written, child) => void out.push({ candidate: { ...c, face: "write" }, ...(seats !== undefined ? { seats } : {}), ...(written !== undefined ? { written } : {}), ...(child === true ? { child } : {}) });
146
+ for (const r of segment.redirects)
147
+ if (r.direction === "out" && r.target !== undefined)
148
+ write(r.target);
149
+ for (const c of segment.reads)
150
+ if (c.provenance === "write_redirect_target")
151
+ write(c);
152
+ const verb = segment.verb?.basename;
153
+ if (verb !== undefined && WRITE_COMMAND_SET.has(verb) && (verb !== "sed" || sedOptionsOf(segment).inPlace)) {
154
+ const seats = OPERAND_SEATS[verb]?.(segment);
155
+ const sourceWritten = SOURCE_WRITTEN[verb];
156
+ const sources = sourceWritten !== undefined ? copySourceWordsOf(segment) : undefined;
157
+ for (const c of segment.reads)
158
+ if (c.provenance === "operand" || c.provenance === "unlisted_word" || c.provenance === "option_value")
159
+ write(c, seats, sources?.has(c.raw) === true ? sourceWritten : undefined);
160
+ if (verb === "cp" || verb === "mv")
161
+ for (const c of copyDestinationChildren(segment))
162
+ write(c, seats, undefined, true);
163
+ if (verb === "sed")
164
+ out.push(...sedBackupTargets(segment));
165
+ }
166
+ return out;
167
+ }
168
+ export function judgedCandidatesOf(segment) {
169
+ const reads = segment.reads.filter((c) => c.provenance !== "write_redirect_target");
170
+ const writes = writeTargetsOf(segment);
171
+ const plain = (cs) => cs.map((candidate) => ({ candidate }));
172
+ return [...plain(reads), ...plain(denyFloorShadowsOf(reads)), ...writes, ...plain(denyFloorShadowsOf(writes.map((w) => w.candidate)))];
173
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.33.8",
3
+ "version": "7.35.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",