@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
@@ -22,19 +22,23 @@ import { violationDetails } from "./safety.js";
22
22
  import { type ShellReadPlan } from "./shell-read-plan.js";
23
23
  import { type EstablishedRead, type ShellReadBoundary, type ShellReadDisposition, type ShellReadFinding, type ShellReadSeat } from "./shell-read-judge.js";
24
24
  import type { ReadRootGrantCandidate } from "../../core/tool-spec.js";
25
- /** The ONE mint of the containment card — every arm that refuses or asks on a containment finding (a refusal, a
26
- * mandated card's own card, a station that could not answer, the pre-spawn re-establishment) spells it here. */
25
+ import { type ToolCallLike } from "../../core/tool-registry.js";
26
+ /** The ONE mint of the containment card — the targets a finding established OUTSIDE the roots (an out-of-root read, a
27
+ * target that resolves elsewhere by the spawn). */
27
28
  declare function containmentDetails(paths: readonly string[]): {
28
29
  readonly type: "readonly_out_of_root";
29
30
  readonly code: "readonly_out_of_root";
30
31
  readonly paths: readonly string[];
31
32
  };
32
- /** The structured details a refusal carries — the same two wire discriminants the shell tools' own
33
- * refusals carried before the station existed, so a consumer counting either code counts one thing: the deny
34
- * card is the read faces' own (`violationDetails`, safety.ts), the containment card is minted above. */
35
- export type ShellReadRefusalDetails = Extract<NonNullable<ReturnType<typeof violationDetails>>, {
36
- code: "read_path_denied";
37
- }> | ReturnType<typeof containmentDetails>;
33
+ /** The ONE mint of the card of a finding that established nothing: where the command reads could not be established
34
+ * (an unreadable or re-entered program text, a program named from its input, a station that could not answer). */
35
+ declare function unestablishedDetails(): {
36
+ readonly type: "read_unestablished";
37
+ readonly code: "read_unestablished";
38
+ };
39
+ /** The structured details a refusal or a mandated card carries: the deny card is the read faces' own (`violationDetails`,
40
+ * safety.ts), the containment and the unestablished cards are minted above. */
41
+ export type ShellReadRefusalDetails = NonNullable<ReturnType<typeof violationDetails>> | ReturnType<typeof containmentDetails> | ReturnType<typeof unestablishedDetails>;
38
42
  /** What the station answers for ONE call. */
39
43
  export interface ShellReadVerdict {
40
44
  readonly disposition: ShellReadDisposition;
@@ -43,12 +47,40 @@ export interface ShellReadVerdict {
43
47
  * sentence). `mandated` / `ordinary`: the ACCOUNT of the finding, for the approval card's disclosure seat
44
48
  * (never the ask's own message — that is the auto-mode classifier's input). Absent for `vouch`. */
45
49
  readonly sentence?: string;
46
- /** `refuse`: the refusal's structured details. `mandated` on an `outside_roots` / `unestablishable` finding: the
47
- * same containment card, so an ask a person refuses or nobody can answer still carries WHAT was found on
48
- * its `tool_end` (`readonly_out_of_root`), as the tool's own refusal of that finding did. */
50
+ /** `refuse`: the refusal's structured details. `mandated` on an `outside_roots` / `unestablishable` finding (and
51
+ * `ordinary` on the file tools' out-of-root read): the finding's card, so an ask a person refuses or nobody can answer
52
+ * still carries WHAT was found on its `tool_end` — the targets outside the roots (`readonly_out_of_root`, or the file
53
+ * tools' `path_not_in_root`), or that nothing could be established (`read_unestablished`). */
49
54
  readonly details?: ShellReadRefusalDetails;
50
55
  /** `mandated` on an `outside_roots` finding: the directory whose grant would clear this ask. */
51
56
  readonly readRootCandidate?: ReadRootGrantCandidate;
57
+ /** A FACT beside the disposition, for the mode that answers a file command itself: every write target this command
58
+ * names was established inside the boundary's WRITE roots at every seat the program writes it (the entry, where it
59
+ * lands, or both — `WriteSeat`, shell-read-write-targets.ts), off the write-protection table and off every deployment
60
+ * write guard (`ShellReadBoundary.writeProtectionJudge` / `writeTargetGuard`), no removal (`rm` / `rmdir`) targets a
61
+ * write root or a directory containing one, every program the command runs is in its text (no `sed -f` file, no
62
+ * re-entered shell, no unreadable text), and the line's order keeps each segment's establishment true
63
+ * ({@link lineOrderPlaced}). Absent ⇒ not established — the mode does not answer; the station's own table is unchanged. */
64
+ readonly writesPlaced?: true;
65
+ /** A FACT beside the disposition: a write target this command names lands on the write-protection table — judged by
66
+ * the table's own judge, as a write tool's target is — so the command's ask carries that table's class, whichever
67
+ * mode reads it. Absent ⇒ no target the station established is covered (or no judge is mounted). */
68
+ readonly writeProtected?: true;
69
+ /** A FACT beside `writesPlaced`'s absence: the command is one of the mode's file commands whose writes the station would
70
+ * have placed but for an OPTION WORD on a `cp` / `mv` segment ({@link UNPLACED_OPTIONS}: any word starting with `-` — the
71
+ * reference implementation's rule, "with flags requires manual approval to ensure path safety"). Names the first such
72
+ * program, so the approval card and the headless refusal can say WHY the mode did not answer. Absent ⇒ not that reason
73
+ * (or the writes were placed). */
74
+ readonly unplacedOptions?: {
75
+ readonly program: "cp" | "mv";
76
+ };
77
+ /** A FACT beside the disposition: some read of the command could not be established (an `unestablishable` or `declined`
78
+ * finding anywhere in it, whichever finding won). A mode that releases the call releases it past the read-deny table. */
79
+ readonly unestablished?: true;
80
+ /** The station's admission of this call's judged target, activated only by the caller that APPLIES this verdict (the
81
+ * gate's fold, the resumed leg's belt): a verdict nobody received — a wait that elapsed, a judgment that threw —
82
+ * admits nothing, whenever it completes. */
83
+ readonly admit?: () => void;
52
84
  }
53
85
  /** The station seat one tool call is judged on (the `rejudge` seat is asked for by the caller). */
54
86
  export type ShellReadStationSeat = Exclude<ShellReadSeat, "rejudge">;
@@ -58,14 +90,21 @@ export type ShellReadStationSeat = Exclude<ShellReadSeat, "rejudge">;
58
90
  * is load-bearing for the gate: a call the station does not cover must not cost the fold a suspension
59
91
  * point. `unanswered` is the verdict the fold substitutes when the wait elapsed or threw — fail-closed as
60
92
  * `unestablishable` on the call's own seat.
93
+ *
94
+ * Both take the CALL (`{ toolName, face }`), not a name: a station is a tighten keyed on the engine's own tool names,
95
+ * so it holds a call made under ANY of the call's names (`ruleNamesOf` — a tool that answers to `Bash` under another
96
+ * wire name is judged as `Bash`). The first of the names the station's membership holds keys its seat, its boundary
97
+ * and the admission the engine's tool reads before it spawns; the sentences name the call's wire name.
61
98
  */
62
99
  export interface ShellReadCheck {
63
- judge(toolName: string, args: unknown, cwd: string | undefined, signal: AbortSignal | undefined, opts: {
100
+ judge(call: ReadStationCall, args: unknown, cwd: string | undefined, signal: AbortSignal | undefined, opts: {
64
101
  readonly toolCallId?: string;
65
102
  readonly seat?: "rejudge";
66
103
  }): Promise<ShellReadVerdict> | null;
67
- unanswered(toolName: string, cause: string): ShellReadVerdict;
104
+ unanswered(call: ReadStationCall, cause: string): ShellReadVerdict;
68
105
  }
106
+ /** The call a read station judges: its wire name and the face the gate stamped (whose `names` the station reads). */
107
+ export type ReadStationCall = Pick<ToolCallLike, "toolName" | "face">;
69
108
  /** The pre-spawn admission the shell tools call: re-establishes what the station established for this
70
109
  * call and refuses when any canonical target moved. `undefined` ⇒ spawn. A call the station never judged
71
110
  * (no gate ran) has nothing to re-establish and spawns. */
@@ -1,17 +1,183 @@
1
1
  import { errorResult } from "../../core/tools.js";
2
2
  import { BASH_READONLY_CONFINEMENT_NOTE } from "./fs-shared.js";
3
- import { violationDetails } from "./safety.js";
3
+ import { isAbsoluteForFamily, normalizeAbsPathLexically, pathFamilyOf, violationDetails, withinAnyRoot } from "./safety.js";
4
4
  import { fitMiddle, READ_DENY_SENTENCE_PART_MAX, readDenySentence } from "./read-deny.js";
5
5
  import { NOT_AUTO_ALLOWED, programPositionRefusalSentence, readProgramPosition } from "./bash-program-position.js";
6
6
  import { bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
7
7
  import { planShellReads } from "./shell-read-plan.js";
8
8
  import { createShellReadEstablisher, establishShellReadsLexically } from "./shell-read-establish.js";
9
- import { allFindings, decideShellRead, foldShellReadFindings, judgeEstablishedPlan, readRootGrantCandidateOf, } from "./shell-read-judge.js";
9
+ import { programArgs, spellsOption } from "./shell-read-write-targets.js";
10
+ import { allFindings, decideShellRead, denyRowAtOrBelow, foldShellReadFindings, judgeEstablishedPlan, readRootGrantCandidateOf, } from "./shell-read-judge.js";
11
+ import { ruleNamesOf } from "../../core/tool-registry.js";
10
12
  import { normalizeReadRootCandidate } from "../../core/read-root-candidate.js";
11
13
  import { argvOf, lexShellText } from "../../shell/lex.js";
12
14
  function containmentDetails(paths) {
13
15
  return { type: "readonly_out_of_root", code: "readonly_out_of_root", paths };
14
16
  }
17
+ function unestablishedDetails() {
18
+ return { type: "read_unestablished", code: "read_unestablished" };
19
+ }
20
+ function findingDetails(finding, paths) {
21
+ return finding === "unestablishable" || finding === "declined" ? unestablishedDetails() : containmentDetails(paths);
22
+ }
23
+ const REMOVAL_COMMANDS = new Set(["rm", "rmdir"]);
24
+ const WRITES_BENEATH = {
25
+ cp: () => true,
26
+ mv: () => true,
27
+ rm: (s) => s.argv.some((w) => spellsOption(w.folded, "recursive", "[rR]")),
28
+ };
29
+ const isPattern = (f) => f.read.candidate.expands !== false && f.read.candidate.expands !== "unresolvable";
30
+ const RESHAPES_TREE = new Set(["mv"]);
31
+ const anyOptionWord = (words) => words.some((w) => w.startsWith("-"));
32
+ const UNPLACED_OPTIONS = {
33
+ rmdir: (ws) => ws.some((w) => spellsOption(w, "parents", "p")),
34
+ sed: (ws) => ws.some((w) => spellsOption(w, "file", "f")),
35
+ cp: anyOptionWord,
36
+ mv: anyOptionWord,
37
+ };
38
+ function runOrderOf(judged) {
39
+ const runs = [];
40
+ const plans = [];
41
+ let concurrent = false;
42
+ const walk = (j) => {
43
+ plans.push(j);
44
+ for (const s of j.segments) {
45
+ s.nested.forEach(walk);
46
+ runs.push(s);
47
+ if (j.plan.shape.backgrounded || s.segment.connector === "|")
48
+ concurrent = true;
49
+ }
50
+ };
51
+ walk(judged);
52
+ return { runs, plans, concurrent };
53
+ }
54
+ function lineOrderPlaced(runs, concurrent) {
55
+ const reshapes = (s) => RESHAPES_TREE.has(s.segment.verb?.basename ?? "");
56
+ const writes = (s) => s.findings.some((f) => f.read.candidate.face === "write");
57
+ const stepsUp = (s) => s.findings.some((f) => f.read.candidate.spelled.split(/[\\/]/).includes(".."));
58
+ return runs.every((later, j) => runs.every((earlier, i) => i === j || (i > j && !concurrent) || (!reshapes(earlier) && !(writes(earlier) && stepsUp(later)))));
59
+ }
60
+ function writesPlacedOf(judged, boundary) {
61
+ const writeRoots = boundary.writeRoots;
62
+ if (writeRoots === undefined || writeRoots.length === 0)
63
+ return false;
64
+ const { runs, plans, concurrent } = runOrderOf(judged);
65
+ const placed = (s) => {
66
+ const verb = s.segment.verb?.basename ?? "";
67
+ if (s.segment.reentry === "shell" || s.segment.unreadProgramText !== undefined)
68
+ return false;
69
+ if (UNPLACED_OPTIONS[verb]?.(programArgs(s.segment).map((w) => w.spelled)) === true)
70
+ return false;
71
+ const beneath = WRITES_BENEATH[verb]?.(s.segment) === true;
72
+ for (const f of s.findings) {
73
+ if (f.read.candidate.face !== "write")
74
+ continue;
75
+ const target = f.read.target;
76
+ if (f.read.established === "unestablishable" || target === undefined || isPattern(f) || !withinAnyRoot(writeRoots, target))
77
+ return false;
78
+ if (beneath && denyRowAtOrBelow(f.read, boundary, !REMOVAL_COMMANDS.has(verb)))
79
+ return false;
80
+ if (REMOVAL_COMMANDS.has(verb) && writeRoots.some((root) => withinAnyRoot([target], root)))
81
+ return false;
82
+ }
83
+ return true;
84
+ };
85
+ return plans.every((p) => p.plan.unreadable === undefined) && runs.every(placed) && lineOrderPlaced(runs, concurrent);
86
+ }
87
+ function unplacedOptionsOf(judged) {
88
+ for (const s of judged.segments) {
89
+ const nested = s.nested.map(unplacedOptionsOf).find((u) => u !== undefined);
90
+ if (nested !== undefined)
91
+ return nested;
92
+ const verb = s.segment.verb?.basename;
93
+ if ((verb === "cp" || verb === "mv") && anyOptionWord(programArgs(s.segment).map((w) => w.spelled)))
94
+ return { program: verb };
95
+ }
96
+ return undefined;
97
+ }
98
+ async function writeGuardsOf(judged, boundary, placed, env, signal) {
99
+ const protection = boundary.writeProtectionJudge;
100
+ const guard = placed ? boundary.writeTargetGuard : undefined;
101
+ if (protection === undefined && guard === undefined)
102
+ return { unplaced: false, writeProtected: false };
103
+ const { runs, concurrent } = runOrderOf(judged);
104
+ const asKey = new Map();
105
+ const asSpelling = new Map();
106
+ const landingKeys = new Set();
107
+ const writtenBy = runs.map(() => []);
108
+ const add = (into, view, child, at) => {
109
+ if (!isAbsoluteForFamily(pathFamilyOf({ root: view }), view))
110
+ return;
111
+ const seen = into.get(view);
112
+ into.set(view, seen === undefined ? { child, at } : { child: seen.child && child, at: Math.max(seen.at, at) });
113
+ };
114
+ runs.forEach((s, at) => {
115
+ for (const f of s.findings) {
116
+ const written = f.read.written;
117
+ if (written === undefined || f.read.established === "unestablishable" || f.read.target === undefined)
118
+ continue;
119
+ const child = written.child === true;
120
+ const key = f.read.established === "canonical" || f.read.established === "expansion";
121
+ add(key ? asKey : asSpelling, f.read.target, child, at);
122
+ if (key && written.seat === "landing")
123
+ landingKeys.add(f.read.target);
124
+ if (written.seat === "landing")
125
+ for (const spelling of f.read.spellings)
126
+ add(asSpelling, spelling, child, at);
127
+ writtenBy[at].push(f.read.target, ...f.read.spellings);
128
+ }
129
+ });
130
+ const probes = new Map();
131
+ const probe = (p) => {
132
+ let r = probes.get(p);
133
+ if (r === undefined) {
134
+ r = env.canonicalPath(p, signal).catch(() => undefined);
135
+ probes.set(p, r);
136
+ }
137
+ return r;
138
+ };
139
+ const parentOf = (view) => normalizeAbsPathLexically(`${view}/..`);
140
+ const writable = async (view, v) => {
141
+ const at = await probe(view);
142
+ if (at === undefined || at.ok)
143
+ return true;
144
+ if (at.error.code === "not_directory")
145
+ return false;
146
+ if (!v.child || at.error.code !== "not_found")
147
+ return true;
148
+ const dest = await probe(parentOf(view));
149
+ return dest === undefined || dest.ok || (dest.error.code !== "not_found" && dest.error.code !== "not_directory");
150
+ };
151
+ const changedBefore = (view, at) => {
152
+ const fold = (p) => p.normalize("NFC").toLowerCase();
153
+ const dir = fold(parentOf(view));
154
+ return writtenBy.some((paths, i) => i !== at && (i < at || concurrent) && paths.some((w) => withinAnyRoot([fold(w)], dir) || withinAnyRoot([dir], fold(w))));
155
+ };
156
+ const answer = async (ask) => {
157
+ try {
158
+ return (await ask()) === true;
159
+ }
160
+ catch {
161
+ return undefined;
162
+ }
163
+ };
164
+ const asked = [...[...asKey].map(([view, v]) => [view, v, true]), ...[...asSpelling].filter(([view]) => !landingKeys.has(view)).map(([view, v]) => [view, v, false])];
165
+ let unplaced = false;
166
+ let writeProtected = false;
167
+ for (const [view, v, key] of asked) {
168
+ if (!(await writable(view, v)) && !changedBefore(view, v.at))
169
+ continue;
170
+ const covered = protection === undefined ? false : await answer(() => protection(view, { ...(signal !== undefined ? { signal } : {}), ...(key ? { canonical: true } : {}) }));
171
+ const guarded = guard === undefined ? false : await answer(() => guard(view, signal));
172
+ if (covered === true)
173
+ writeProtected = true;
174
+ if (covered !== false || guarded !== false)
175
+ unplaced = true;
176
+ if (writeProtected)
177
+ break;
178
+ }
179
+ return { unplaced, writeProtected };
180
+ }
15
181
  const ADMISSION_MAX = 1024;
16
182
  function shellCommandOf(args) {
17
183
  const command = args?.command;
@@ -76,14 +242,17 @@ function refusalOf(tool, fold, roots, paths) {
76
242
  const w = fold.witness;
77
243
  if (fold.finding === "deny_hit" && w?.deny !== undefined) {
78
244
  const target = w.read.target ?? w.deny.matchedView;
79
- const core = readDenySentence({ target, pattern: w.deny.pattern, matchedView: w.deny.matchedView });
80
- const sentence = `Error (${tool}): ${core} ${DENY_REFUSAL_NOTE}`;
81
- return { sentence, details: violationDetails({ code: "read_path_denied", message: sentence, target, pattern: w.deny.pattern }) };
245
+ const write = w.deny.face === "write";
246
+ const core = readDenySentence({ target, pattern: w.deny.pattern, matchedView: w.deny.matchedView, ...(write ? { face: "write" } : {}) });
247
+ const sentence = `Error (${tool}): ${core} ${write ? WRITE_DENY_REFUSAL_NOTE : DENY_REFUSAL_NOTE}`;
248
+ return { sentence, ...(write ? {} : { details: violationDetails({ code: "read_path_denied", message: sentence, target, pattern: w.deny.pattern }) }) };
82
249
  }
83
250
  const account = findingAccount(fold, roots);
84
251
  const sentence = `Error (${tool}): ${account}. ${BASH_READONLY_CONFINEMENT_NOTE}`;
85
- return { sentence, details: containmentDetails(paths) };
252
+ return { sentence, details: findingDetails(fold.finding, paths) };
86
253
  }
254
+ const WRITE_DENY_REFUSAL_NOTE = "The boundary judges this command's write targets — its output redirections and the operands of mkdir, touch, rm, " +
255
+ "rmdir, mv, cp, tee and an in-place sed — against the write deny list; a denied write is refused here rather than escalated.";
87
256
  const DENY_REFUSAL_NOTE = "The read boundary judges this command's written spelling and, where a filesystem can be consulted, the " +
88
257
  "paths it would actually read; a deny-listed read is refused here rather than escalated.";
89
258
  function verdictOf(tool, seat, fold, judged, boundary) {
@@ -91,17 +260,19 @@ function verdictOf(tool, seat, fold, judged, boundary) {
91
260
  if (disposition === "vouch")
92
261
  return { disposition, finding: fold.finding };
93
262
  const paths = outsidePathsOf(judged);
263
+ const unestablished = fold.unestablished === true ? { unestablished: true } : {};
94
264
  if (disposition === "refuse") {
95
265
  const r = refusalOf(tool, fold, boundary.roots, paths);
96
- return { disposition, finding: fold.finding, sentence: r.sentence, details: r.details };
266
+ return { disposition, finding: fold.finding, sentence: r.sentence, ...(r.details !== undefined ? { details: r.details } : {}) };
97
267
  }
98
268
  const grant = fold.finding === "outside_roots" ? readRootGrantCandidateOf(judged, boundary) : undefined;
99
269
  const containment = fold.finding === "outside_roots" || fold.finding === "unestablishable";
100
270
  return {
101
271
  disposition,
102
272
  finding: fold.finding,
273
+ ...unestablished,
103
274
  sentence: findingAccount(fold, boundary.roots),
104
- ...(disposition === "mandated" && containment ? { details: containmentDetails(paths) } : {}),
275
+ ...(disposition === "mandated" && containment ? { details: findingDetails(fold.finding, paths) } : {}),
105
276
  ...(grant !== undefined ? { readRootCandidate: { dir: grant.dir, ...(grant.covers !== undefined ? { covers: grant.covers } : {}), clearsThisAsk: true } } : {}),
106
277
  };
107
278
  }
@@ -137,17 +308,22 @@ async function applyAlsoInside(judged, boundary) {
137
308
  });
138
309
  return rewrite(judged);
139
310
  }
140
- async function runShellReadStation(tool, command, declared, seat, readers, establisher, signal) {
311
+ async function runShellReadStation(tool, command, declared, seat, readers, establisher, env, signal) {
141
312
  const plan = planShellReads(command, declarationsOf(declared, readers));
142
313
  const established = await establisher.establish(plan, declared, signal);
143
314
  const boundary = { ...declared, roots: established.roots };
144
315
  const judged = await applyAlsoInside(judgePlan(plan, established, boundary), boundary);
145
- const verdict = verdictOf(tool, seat, foldShellReadFindings(judged), judged, boundary);
316
+ const judgedVerdict = verdictOf(tool, seat, foldShellReadFindings(judged), judged, boundary);
317
+ const unplaced = unplacedOptionsOf(judged);
318
+ const placed = writesPlacedOf(judged, boundary);
319
+ const guarded = judgedVerdict.disposition === "refuse" ? { unplaced: false, writeProtected: false } : await writeGuardsOf(judged, boundary, placed, env, signal);
320
+ const facts = guarded.writeProtected ? { ...judgedVerdict, writeProtected: true } : judgedVerdict;
321
+ const verdict = placed && !guarded.unplaced ? { ...facts, writesPlaced: true } : unplaced !== undefined ? { ...facts, unplacedOptions: unplaced } : facts;
146
322
  return { plan, established, judged, boundary, verdict };
147
323
  }
148
324
  export async function judgeShellRead(input) {
149
325
  const readers = input.readers ?? new Set(bashClassifyDefaultAllow());
150
- const run = await runShellReadStation(input.tool ?? "Bash", input.command, input.boundary, input.seat, readers, createShellReadEstablisher(input.env, input.boundary.roots), input.signal);
326
+ const run = await runShellReadStation(input.tool ?? "Bash", input.command, input.boundary, input.seat, readers, createShellReadEstablisher(input.env, input.boundary.roots), input.env, input.signal);
151
327
  const offer = normalizeReadRootCandidate(run.verdict.readRootCandidate);
152
328
  return {
153
329
  plan: run.plan,
@@ -184,30 +360,33 @@ export function createShellReadStation(opts) {
184
360
  }
185
361
  admissions.set(toolCallId, a);
186
362
  };
363
+ const memberOf = (call) => ruleNamesOf(call).find((n) => opts.tools.has(n));
187
364
  const check = {
188
- judge(toolName, args, cwd, signal, callOpts) {
189
- if (!opts.tools.has(toolName))
365
+ judge(call, args, cwd, signal, callOpts) {
366
+ const member = memberOf(call);
367
+ if (member === undefined)
190
368
  return null;
191
369
  const command = shellCommandOf(args);
192
370
  if (command === undefined)
193
371
  return null;
194
- const boundary = opts.boundaryOf(toolName, cwd);
372
+ const boundary = opts.boundaryOf(member, cwd);
195
373
  if (boundary === undefined)
196
374
  return null;
197
- const seat = callOpts.seat ?? opts.seatOf(toolName);
375
+ const seat = callOpts.seat ?? opts.seatOf(member);
198
376
  return (async () => {
199
- const run = await runShellReadStation(toolName, command, boundary, seat, readers, establisherFor(boundary.roots), signal);
377
+ const run = await runShellReadStation(call.toolName, command, boundary, seat, readers, establisherFor(boundary.roots), opts.env, signal);
200
378
  if (run.verdict.disposition !== "refuse" && callOpts.toolCallId !== undefined)
201
- remember(callOpts.toolCallId, { toolName, boundary: run.boundary, established: run.established, command });
379
+ remember(callOpts.toolCallId, { toolName: member, boundary: run.boundary, established: run.established, command });
202
380
  return run.verdict;
203
381
  })();
204
382
  },
205
- unanswered(toolName, cause) {
206
- const seat = opts.seatOf(toolName);
383
+ unanswered(call, cause) {
384
+ const toolName = call.toolName;
385
+ const seat = opts.seatOf(memberOf(call) ?? toolName);
207
386
  const fold = { finding: "unestablishable" };
208
387
  const disposition = decideShellRead("unestablishable", seat);
209
388
  const account = `${cause}, so where this command reads could not be established ${NOT_AUTO_ALLOWED}`;
210
- const details = containmentDetails([]);
389
+ const details = unestablishedDetails();
211
390
  if (disposition === "refuse")
212
391
  return { disposition, finding: "unestablishable", sentence: `Error (${toolName}): ${account}. ${BASH_READONLY_CONFINEMENT_NOTE}`, details };
213
392
  return { disposition, finding: fold.finding, sentence: account, ...(disposition === "mandated" ? { details } : {}) };
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * ESTABLISHMENT — where a shell command's read candidates LAND once the filesystem has answered, as one
3
3
  * value the gate's shell read station holds per execution environment. The plan (`shell-read-plan.ts`)
4
- * decides WHICH words a command may read; this module decides WHERE each of them lands: the canonical
4
+ * decides WHICH words a command may read, `shell-read-write-targets.ts` which words it writes and at which seat;
5
+ * this module decides WHERE each of them lands: the canonical
5
6
  * target of a spelling (symlinks resolved, a `..` applied the way the kernel applies it), the expansion of
6
7
  * a pattern (shell pathname semantics over `env.listDir`), the members of a traversal root (a bounded
7
8
  * recursive enumeration, dot files included, symlinked directories not followed), and the fail-closed arm
@@ -11,7 +12,10 @@
11
12
  * Properties every consumer relies on:
12
13
  * 1. A NON-EXISTENT target is not a refusal — `cat nope.txt` keeps the shell's own "No such file". The
13
14
  * deepest EXISTING ancestor is canonicalized and the missing tail rejoined, so a symlinked PARENT still
14
- * cannot carry a target out.
15
+ * cannot carry a target out. For a WRITE target a component the resolver cannot find but the filesystem
16
+ * still holds (a link whose target does not exist) is not absent: a write through it creates the link's
17
+ * target, so the target is established where the link lands (read one hop at a time, bounded) — or not at
18
+ * all when the environment cannot read the link.
15
19
  * 2. An unanswerable filesystem manufactures no refusal on its own: a chain no ancestor answered degrades
16
20
  * to the lexical spelling on an environment that declares no authority for its resolver, and is
17
21
  * `unestablishable` on one that does (`canonicalPathIsAuthoritative`).
@@ -35,7 +39,7 @@
35
39
  */
36
40
  import type { ExecutionEnv } from "../../internal/harness-types.js";
37
41
  import type { ShellReadPlan, ShellReadSegment } from "./shell-read-plan.js";
38
- import { type EstablishedRead, type ShellReadBoundary } from "./shell-read-judge.js";
42
+ import type { EstablishedRead, ShellReadBoundary } from "./shell-read-judge.js";
39
43
  /**
40
44
  * How many directories one glob operand (or one traversal root) may enumerate, and how many names it may
41
45
  * match, before the leg gives up and answers `unestablishable`.