@namzu/cli 30.0.0 → 32.0.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 (256) hide show
  1. package/CHANGELOG.md +217 -1
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +5 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/acp.d.ts +20 -2
  6. package/dist/commands/acp.d.ts.map +1 -1
  7. package/dist/commands/acp.js +42 -5
  8. package/dist/commands/acp.js.map +1 -1
  9. package/dist/commands/drain.d.ts.map +1 -1
  10. package/dist/commands/drain.js +2 -1
  11. package/dist/commands/drain.js.map +1 -1
  12. package/dist/commands/exec-json.d.ts.map +1 -1
  13. package/dist/commands/exec-json.js +24 -1
  14. package/dist/commands/exec-json.js.map +1 -1
  15. package/dist/commands/exec.d.ts.map +1 -1
  16. package/dist/commands/exec.js +2 -1
  17. package/dist/commands/exec.js.map +1 -1
  18. package/dist/commands/prior-messages.d.ts.map +1 -1
  19. package/dist/commands/prior-messages.js +42 -2
  20. package/dist/commands/prior-messages.js.map +1 -1
  21. package/dist/commands/schedule.d.ts.map +1 -1
  22. package/dist/commands/schedule.js +7 -5
  23. package/dist/commands/schedule.js.map +1 -1
  24. package/dist/config/composer-triggers.d.ts +41 -0
  25. package/dist/config/composer-triggers.d.ts.map +1 -0
  26. package/dist/config/composer-triggers.js +80 -0
  27. package/dist/config/composer-triggers.js.map +1 -0
  28. package/dist/config/load.d.ts.map +1 -1
  29. package/dist/config/load.js +58 -0
  30. package/dist/config/load.js.map +1 -1
  31. package/dist/config/schema.d.ts +34 -0
  32. package/dist/config/schema.d.ts.map +1 -1
  33. package/dist/config/schema.js.map +1 -1
  34. package/dist/context/doctrine.d.ts +3 -3
  35. package/dist/context/doctrine.d.ts.map +1 -1
  36. package/dist/context/doctrine.js +3 -3
  37. package/dist/context/doctrine.js.map +1 -1
  38. package/dist/context/runtime-message.d.ts.map +1 -1
  39. package/dist/context/runtime-message.js +4 -0
  40. package/dist/context/runtime-message.js.map +1 -1
  41. package/dist/integrations/mcp/servers.d.ts +72 -5
  42. package/dist/integrations/mcp/servers.d.ts.map +1 -1
  43. package/dist/integrations/mcp/servers.js +144 -23
  44. package/dist/integrations/mcp/servers.js.map +1 -1
  45. package/dist/integrations/plugins/runtime.d.ts +11 -9
  46. package/dist/integrations/plugins/runtime.d.ts.map +1 -1
  47. package/dist/integrations/plugins/runtime.js +17 -36
  48. package/dist/integrations/plugins/runtime.js.map +1 -1
  49. package/dist/integrations/resident/session-step.d.ts.map +1 -1
  50. package/dist/integrations/resident/session-step.js +2 -1
  51. package/dist/integrations/resident/session-step.js.map +1 -1
  52. package/dist/integrations/sessions/store.d.ts.map +1 -1
  53. package/dist/integrations/sessions/store.js +20 -2
  54. package/dist/integrations/sessions/store.js.map +1 -1
  55. package/dist/integrations/subagents/NamzuCliAgent.d.ts +8 -0
  56. package/dist/integrations/subagents/NamzuCliAgent.d.ts.map +1 -0
  57. package/dist/integrations/subagents/NamzuCliAgent.js +9 -0
  58. package/dist/integrations/subagents/NamzuCliAgent.js.map +1 -0
  59. package/dist/integrations/subagents/runtime.d.ts +5 -4
  60. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  61. package/dist/integrations/subagents/runtime.js +46 -11
  62. package/dist/integrations/subagents/runtime.js.map +1 -1
  63. package/dist/integrations/web/search.d.ts.map +1 -1
  64. package/dist/integrations/web/search.js +8 -6
  65. package/dist/integrations/web/search.js.map +1 -1
  66. package/dist/permissions/rules.d.ts +5 -1
  67. package/dist/permissions/rules.d.ts.map +1 -1
  68. package/dist/permissions/rules.js +85 -3
  69. package/dist/permissions/rules.js.map +1 -1
  70. package/dist/schedule/build.d.ts +19 -2
  71. package/dist/schedule/build.d.ts.map +1 -1
  72. package/dist/schedule/build.js +119 -21
  73. package/dist/schedule/build.js.map +1 -1
  74. package/dist/schedule/changes.d.ts.map +1 -1
  75. package/dist/schedule/changes.js +13 -0
  76. package/dist/schedule/changes.js.map +1 -1
  77. package/dist/schedule/commands/add.d.ts +1 -1
  78. package/dist/schedule/commands/add.d.ts.map +1 -1
  79. package/dist/schedule/commands/add.js +186 -40
  80. package/dist/schedule/commands/add.js.map +1 -1
  81. package/dist/schedule/commands/list.d.ts +2 -0
  82. package/dist/schedule/commands/list.d.ts.map +1 -1
  83. package/dist/schedule/commands/list.js +64 -19
  84. package/dist/schedule/commands/list.js.map +1 -1
  85. package/dist/schedule/daemon/daemon.d.ts +1 -1
  86. package/dist/schedule/daemon/daemon.d.ts.map +1 -1
  87. package/dist/schedule/daemon/daemon.js +17 -5
  88. package/dist/schedule/daemon/daemon.js.map +1 -1
  89. package/dist/schedule/daemon/notify.d.ts +21 -1
  90. package/dist/schedule/daemon/notify.d.ts.map +1 -1
  91. package/dist/schedule/daemon/notify.js +43 -0
  92. package/dist/schedule/daemon/notify.js.map +1 -1
  93. package/dist/schedule/fire/fire.d.ts +1 -1
  94. package/dist/schedule/fire/fire.d.ts.map +1 -1
  95. package/dist/schedule/fire/fire.js +200 -14
  96. package/dist/schedule/fire/fire.js.map +1 -1
  97. package/dist/schedule/fire/result.d.ts +1 -1
  98. package/dist/schedule/fire/result.d.ts.map +1 -1
  99. package/dist/schedule/fire/result.js +2 -1
  100. package/dist/schedule/fire/result.js.map +1 -1
  101. package/dist/schedule/fire/run-script.d.ts +46 -0
  102. package/dist/schedule/fire/run-script.d.ts.map +1 -0
  103. package/dist/schedule/fire/run-script.js +81 -0
  104. package/dist/schedule/fire/run-script.js.map +1 -0
  105. package/dist/schedule/fire/unattended-note.d.ts +8 -0
  106. package/dist/schedule/fire/unattended-note.d.ts.map +1 -1
  107. package/dist/schedule/fire/unattended-note.js +5 -0
  108. package/dist/schedule/fire/unattended-note.js.map +1 -1
  109. package/dist/schedule/fire/wake-gate.d.ts +39 -0
  110. package/dist/schedule/fire/wake-gate.d.ts.map +1 -0
  111. package/dist/schedule/fire/wake-gate.js +90 -0
  112. package/dist/schedule/fire/wake-gate.js.map +1 -0
  113. package/dist/schedule/floor.d.ts +1 -1
  114. package/dist/schedule/floor.d.ts.map +1 -1
  115. package/dist/schedule/floor.js +288 -16
  116. package/dist/schedule/floor.js.map +1 -1
  117. package/dist/schedule/policy.d.ts +40 -0
  118. package/dist/schedule/policy.d.ts.map +1 -1
  119. package/dist/schedule/policy.js +36 -0
  120. package/dist/schedule/policy.js.map +1 -1
  121. package/dist/schedule/script-check.d.ts +53 -0
  122. package/dist/schedule/script-check.d.ts.map +1 -0
  123. package/dist/schedule/script-check.js +145 -0
  124. package/dist/schedule/script-check.js.map +1 -0
  125. package/dist/schedule/script-shell.d.ts +3 -0
  126. package/dist/schedule/script-shell.d.ts.map +1 -0
  127. package/dist/schedule/script-shell.js +5 -0
  128. package/dist/schedule/script-shell.js.map +1 -0
  129. package/dist/schedule/service/manifest.js +1 -1
  130. package/dist/schedule/service/manifest.js.map +1 -1
  131. package/dist/schedule/store/atomic.d.ts +5 -2
  132. package/dist/schedule/store/atomic.d.ts.map +1 -1
  133. package/dist/schedule/store/atomic.js +7 -4
  134. package/dist/schedule/store/atomic.js.map +1 -1
  135. package/dist/schedule/store/claims.js +1 -1
  136. package/dist/schedule/store/claims.js.map +1 -1
  137. package/dist/schedule/store/history.d.ts +1 -1
  138. package/dist/schedule/store/history.d.ts.map +1 -1
  139. package/dist/schedule/store/history.js +4 -1
  140. package/dist/schedule/store/history.js.map +1 -1
  141. package/dist/schedule/store/jobs.d.ts +19 -3
  142. package/dist/schedule/store/jobs.d.ts.map +1 -1
  143. package/dist/schedule/store/jobs.js +83 -16
  144. package/dist/schedule/store/jobs.js.map +1 -1
  145. package/dist/schedule/store/state.js +1 -1
  146. package/dist/schedule/store/state.js.map +1 -1
  147. package/dist/schedule/types.d.ts +71 -6
  148. package/dist/schedule/types.d.ts.map +1 -1
  149. package/dist/schedule/types.js +25 -1
  150. package/dist/schedule/types.js.map +1 -1
  151. package/dist/schedule/visible-source.d.ts +18 -0
  152. package/dist/schedule/visible-source.d.ts.map +1 -0
  153. package/dist/schedule/visible-source.js +59 -0
  154. package/dist/schedule/visible-source.js.map +1 -0
  155. package/dist/test-support/toolset.d.ts +23 -0
  156. package/dist/test-support/toolset.d.ts.map +1 -0
  157. package/dist/test-support/toolset.js +27 -0
  158. package/dist/test-support/toolset.js.map +1 -0
  159. package/dist/tui/App.d.ts.map +1 -1
  160. package/dist/tui/App.js +323 -60
  161. package/dist/tui/App.js.map +1 -1
  162. package/dist/tui/ChoicePicker.d.ts +2 -0
  163. package/dist/tui/ChoicePicker.d.ts.map +1 -1
  164. package/dist/tui/ChoicePicker.js +10 -2
  165. package/dist/tui/ChoicePicker.js.map +1 -1
  166. package/dist/tui/Composer.d.ts +62 -3
  167. package/dist/tui/Composer.d.ts.map +1 -1
  168. package/dist/tui/Composer.js +162 -25
  169. package/dist/tui/Composer.js.map +1 -1
  170. package/dist/tui/ComposerFrame.d.ts +1 -1
  171. package/dist/tui/ComposerFrame.js +3 -3
  172. package/dist/tui/ComposerFrame.js.map +1 -1
  173. package/dist/tui/EffortSlider.d.ts +8 -9
  174. package/dist/tui/EffortSlider.d.ts.map +1 -1
  175. package/dist/tui/EffortSlider.js +15 -12
  176. package/dist/tui/EffortSlider.js.map +1 -1
  177. package/dist/tui/StatusBar.d.ts +12 -12
  178. package/dist/tui/StatusBar.d.ts.map +1 -1
  179. package/dist/tui/StatusBar.js +15 -15
  180. package/dist/tui/StatusBar.js.map +1 -1
  181. package/dist/tui/agent.d.ts +46 -7
  182. package/dist/tui/agent.d.ts.map +1 -1
  183. package/dist/tui/agent.js +386 -178
  184. package/dist/tui/agent.js.map +1 -1
  185. package/dist/tui/hypermode.d.ts +37 -0
  186. package/dist/tui/hypermode.d.ts.map +1 -0
  187. package/dist/tui/hypermode.js +58 -0
  188. package/dist/tui/hypermode.js.map +1 -0
  189. package/dist/tui/permission-review.d.ts +1 -1
  190. package/dist/tui/permission-review.js +1 -1
  191. package/dist/tui/schedule/host-commands.d.ts.map +1 -1
  192. package/dist/tui/schedule/host-commands.js +94 -33
  193. package/dist/tui/schedule/host-commands.js.map +1 -1
  194. package/dist/tui/schedule/integration.d.ts +6 -2
  195. package/dist/tui/schedule/integration.d.ts.map +1 -1
  196. package/dist/tui/schedule/integration.js +3 -1
  197. package/dist/tui/schedule/integration.js.map +1 -1
  198. package/dist/tui/schedule/resume.d.ts.map +1 -1
  199. package/dist/tui/schedule/resume.js +10 -1
  200. package/dist/tui/schedule/resume.js.map +1 -1
  201. package/dist/tui/schedule/tool-host.d.ts +4 -1
  202. package/dist/tui/schedule/tool-host.d.ts.map +1 -1
  203. package/dist/tui/schedule/tool-host.js +155 -40
  204. package/dist/tui/schedule/tool-host.js.map +1 -1
  205. package/dist/tui/slashCommands.d.ts +23 -5
  206. package/dist/tui/slashCommands.d.ts.map +1 -1
  207. package/dist/tui/slashCommands.js +80 -31
  208. package/dist/tui/slashCommands.js.map +1 -1
  209. package/dist/tui/theme.d.ts +10 -4
  210. package/dist/tui/theme.d.ts.map +1 -1
  211. package/dist/tui/theme.js +4 -3
  212. package/dist/tui/theme.js.map +1 -1
  213. package/dist/tui/triggers/analyze.d.ts +57 -0
  214. package/dist/tui/triggers/analyze.d.ts.map +1 -0
  215. package/dist/tui/triggers/analyze.js +221 -0
  216. package/dist/tui/triggers/analyze.js.map +1 -0
  217. package/dist/tui/triggers/context-text.d.ts +21 -0
  218. package/dist/tui/triggers/context-text.d.ts.map +1 -0
  219. package/dist/tui/triggers/context-text.js +44 -0
  220. package/dist/tui/triggers/context-text.js.map +1 -0
  221. package/dist/tui/triggers/copy.d.ts +38 -0
  222. package/dist/tui/triggers/copy.d.ts.map +1 -0
  223. package/dist/tui/triggers/copy.js +163 -0
  224. package/dist/tui/triggers/copy.js.map +1 -0
  225. package/dist/tui/triggers/detect.d.ts +83 -0
  226. package/dist/tui/triggers/detect.d.ts.map +1 -0
  227. package/dist/tui/triggers/detect.js +295 -0
  228. package/dist/tui/triggers/detect.js.map +1 -0
  229. package/dist/tui/triggers/fold.d.ts +34 -0
  230. package/dist/tui/triggers/fold.d.ts.map +1 -0
  231. package/dist/tui/triggers/fold.js +44 -0
  232. package/dist/tui/triggers/fold.js.map +1 -0
  233. package/dist/tui/triggers/pattern.d.ts +66 -0
  234. package/dist/tui/triggers/pattern.d.ts.map +1 -0
  235. package/dist/tui/triggers/pattern.js +243 -0
  236. package/dist/tui/triggers/pattern.js.map +1 -0
  237. package/dist/tui/triggers/provenance.d.ts +58 -0
  238. package/dist/tui/triggers/provenance.d.ts.map +1 -0
  239. package/dist/tui/triggers/provenance.js +98 -0
  240. package/dist/tui/triggers/provenance.js.map +1 -0
  241. package/dist/tui/triggers/registry.d.ts +113 -0
  242. package/dist/tui/triggers/registry.d.ts.map +1 -0
  243. package/dist/tui/triggers/registry.js +164 -0
  244. package/dist/tui/triggers/registry.js.map +1 -0
  245. package/dist/tui/triggers/setting.d.ts +18 -0
  246. package/dist/tui/triggers/setting.d.ts.map +1 -0
  247. package/dist/tui/triggers/setting.js +61 -0
  248. package/dist/tui/triggers/setting.js.map +1 -0
  249. package/dist/tui/triggers/verbs.d.ts +29 -0
  250. package/dist/tui/triggers/verbs.d.ts.map +1 -0
  251. package/dist/tui/triggers/verbs.js +140 -0
  252. package/dist/tui/triggers/verbs.js.map +1 -0
  253. package/dist/tui/types.d.ts +3 -1
  254. package/dist/tui/types.d.ts.map +1 -1
  255. package/package.json +12 -12
  256. package/skills/schedule-task/SKILL.md +115 -3
@@ -1 +1 @@
1
- {"version":3,"file":"unattended-note.js","sourceRoot":"","sources":["../../../src/schedule/fire/unattended-note.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,GAAS,EAAE,EAAU;IAClD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;QAC7C,OAAO,EAAE,MAAM;QACf,GAAG,EAAE,SAAS;QACd,KAAK,EAAE,MAAM;QACb,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,SAAS;QACjB,SAAS,EAAE,KAAK;QAChB,QAAQ,EAAE,EAAE;QACZ,YAAY,EAAE,YAAY;KAC1B,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IACd,OAAO,GAAG,IAAI,KAAK,EAAE,GAAG,CAAA;AACzB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,GAAS,EAAE,EAAU;IACpD,OAAO,aAAa,aAAa,CAAC,GAAG,EAAE,EAAE,CAAC,iIAAiI,CAAA;AAC5K,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAC7B,OAAe,EACf,UAKI,EAAE;IAEN,OAAO;QACN,4CAA4C,OAAO,2BAA2B;QAC9E,GAAG,CAAC,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACvF,qIAAqI;QACrI,uKAAuK;QACvK,uGAAuG;QACvG,8LAA8L;QAC9L,GAAG,CAAC,OAAO,CAAC,OAAO;YAClB,CAAC,CAAC;gBACA,2QAA2Q;aAC3Q;YACF,CAAC,CAAC,EAAE,CAAC;KACN,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACb,CAAC"}
1
+ {"version":3,"file":"unattended-note.js","sourceRoot":"","sources":["../../../src/schedule/fire/unattended-note.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,GAAS,EAAE,EAAU;IAClD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;QAC7C,OAAO,EAAE,MAAM;QACf,GAAG,EAAE,SAAS;QACd,KAAK,EAAE,MAAM;QACb,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,SAAS;QACjB,SAAS,EAAE,KAAK;QAChB,QAAQ,EAAE,EAAE;QACZ,YAAY,EAAE,YAAY;KAC1B,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IACd,OAAO,GAAG,IAAI,KAAK,EAAE,GAAG,CAAA;AACzB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,GAAS,EAAE,EAAU;IACpD,OAAO,aAAa,aAAa,CAAC,GAAG,EAAE,EAAE,CAAC,iIAAiI,CAAA;AAC5K,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAC7B,OAAe,EACf,UAaI,EAAE;IAEN,OAAO;QACN,4CAA4C,OAAO,2BAA2B;QAC9E,GAAG,CAAC,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACvF,qIAAqI;QACrI,uKAAuK;QACvK,uGAAuG;QACvG,8LAA8L;QAC9L,GAAG,CAAC,OAAO,CAAC,OAAO;YAClB,CAAC,CAAC;gBACA,2QAA2Q;aAC3Q;YACF,CAAC,CAAC,EAAE,CAAC;QACN,GAAG,CAAC,OAAO,CAAC,eAAe,KAAK,SAAS;YACxC,CAAC,CAAC;gBACA,sOAAsO,OAAO,CAAC,eAAe,EAAE;aAC/P;YACF,CAAC,CAAC,EAAE,CAAC;KACN,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACb,CAAC"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The wake-gate output contract for a `script+agent` job: the gate script's
3
+ * stdout must be exactly one JSON line — its own last line, or its only
4
+ * line (trailing blank lines tolerated) — of the shape `{"wake": boolean,
5
+ * "context": string}`. Everything else the script writes is captured for
6
+ * the run record (`fire/run-script.ts`'s output, separately) but is never
7
+ * parsed as the contract.
8
+ *
9
+ * Parse failures are mechanical, not a judgement about whether the AGENT
10
+ * half would have succeeded: non-JSON stdout, a JSON value that is not an
11
+ * object, a missing/non-boolean `wake`, a non-string `context`, or more
12
+ * than one line of stdout that independently parses as JSON (ambiguous —
13
+ * which one is the answer?) are all refused the same way, by the caller,
14
+ * as `check-failed`.
15
+ *
16
+ * The gate script's stdout is UNTRUSTED — it is runtime output, not
17
+ * anything an operator confirmed — and a parse failure quotes a slice of
18
+ * it in the `reason` this module returns, which reaches the operator on
19
+ * `schedule show`/`history` and a desktop notification. `shown()` runs it
20
+ * through the same `sanitizeLine()` a notification already uses for the
21
+ * model's own text, so a gate script cannot put control characters or a
22
+ * terminal escape sequence on the operator's screen through a malformed
23
+ * contract line.
24
+ */
25
+ export interface WakeGateResult {
26
+ readonly wake: boolean;
27
+ /** Cut to the job's `wakeGate.maxContextChars`, with an explicit marker — never silently. */
28
+ readonly context: string;
29
+ }
30
+ export type WakeGateOutcome = {
31
+ readonly ok: true;
32
+ readonly result: WakeGateResult;
33
+ } | {
34
+ readonly ok: false;
35
+ readonly reason: string;
36
+ };
37
+ /** Parse a wake-gate's stdout against the contract, capping `context` at `maxContextChars`. */
38
+ export declare function parseWakeGateOutput(stdout: string, maxContextChars: number): WakeGateOutcome;
39
+ //# sourceMappingURL=wake-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wake-gate.d.ts","sourceRoot":"","sources":["../../../src/schedule/fire/wake-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,6FAA6F;IAC7F,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,MAAM,eAAe,GACxB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;CAAE,GACtD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAiBlD,+FAA+F;AAC/F,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,eAAe,CAkD5F"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The wake-gate output contract for a `script+agent` job: the gate script's
3
+ * stdout must be exactly one JSON line — its own last line, or its only
4
+ * line (trailing blank lines tolerated) — of the shape `{"wake": boolean,
5
+ * "context": string}`. Everything else the script writes is captured for
6
+ * the run record (`fire/run-script.ts`'s output, separately) but is never
7
+ * parsed as the contract.
8
+ *
9
+ * Parse failures are mechanical, not a judgement about whether the AGENT
10
+ * half would have succeeded: non-JSON stdout, a JSON value that is not an
11
+ * object, a missing/non-boolean `wake`, a non-string `context`, or more
12
+ * than one line of stdout that independently parses as JSON (ambiguous —
13
+ * which one is the answer?) are all refused the same way, by the caller,
14
+ * as `check-failed`.
15
+ *
16
+ * The gate script's stdout is UNTRUSTED — it is runtime output, not
17
+ * anything an operator confirmed — and a parse failure quotes a slice of
18
+ * it in the `reason` this module returns, which reaches the operator on
19
+ * `schedule show`/`history` and a desktop notification. `shown()` runs it
20
+ * through the same `sanitizeLine()` a notification already uses for the
21
+ * model's own text, so a gate script cannot put control characters or a
22
+ * terminal escape sequence on the operator's screen through a malformed
23
+ * contract line.
24
+ */
25
+ import { sanitizeLine } from '../../integrations/notifications/desktop/sanitize.js';
26
+ /** Untrusted text as a refusal quotes it: sanitized, one line, cut to fit, in backticks. */
27
+ function shown(text, max = 200) {
28
+ const cut = sanitizeLine(text, max);
29
+ return cut.includes('`') ? `\`\` ${cut} \`\`` : `\`${cut}\``;
30
+ }
31
+ function isJson(line) {
32
+ try {
33
+ JSON.parse(line);
34
+ return true;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ /** Parse a wake-gate's stdout against the contract, capping `context` at `maxContextChars`. */
41
+ export function parseWakeGateOutput(stdout, maxContextChars) {
42
+ const lines = stdout.split(/\r?\n/);
43
+ while (lines.length > 0 && lines.at(-1).trim() === '')
44
+ lines.pop();
45
+ if (lines.length === 0) {
46
+ return {
47
+ ok: false,
48
+ reason: 'the wake-gate script produced no output; it must print exactly one JSON line, {"wake": boolean, "context": string}',
49
+ };
50
+ }
51
+ const last = lines.at(-1);
52
+ if (!isJson(last)) {
53
+ return {
54
+ ok: false,
55
+ reason: `the wake-gate script's stdout does not end with JSON: ${shown(last)}`,
56
+ };
57
+ }
58
+ const earlierJsonLines = lines.slice(0, -1).filter((line) => line.trim() !== '' && isJson(line));
59
+ if (earlierJsonLines.length > 0) {
60
+ return {
61
+ ok: false,
62
+ reason: 'the wake-gate script printed more than one line of JSON stdout; it must print exactly one, as its last line',
63
+ };
64
+ }
65
+ const value = JSON.parse(last);
66
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
67
+ return {
68
+ ok: false,
69
+ reason: `the wake-gate script's JSON line is not an object: ${shown(last)}`,
70
+ };
71
+ }
72
+ const record = value;
73
+ if (typeof record.wake !== 'boolean') {
74
+ return {
75
+ ok: false,
76
+ reason: `the wake-gate script's JSON has no boolean "wake": ${shown(last)}`,
77
+ };
78
+ }
79
+ if (typeof record.context !== 'string') {
80
+ return {
81
+ ok: false,
82
+ reason: `the wake-gate script's JSON has no string "context": ${shown(last)}`,
83
+ };
84
+ }
85
+ const context = record.context.length > maxContextChars
86
+ ? `${record.context.slice(0, maxContextChars)}\n...(truncated)`
87
+ : record.context;
88
+ return { ok: true, result: { wake: record.wake, context } };
89
+ }
90
+ //# sourceMappingURL=wake-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wake-gate.js","sourceRoot":"","sources":["../../../src/schedule/fire/wake-gate.ts"],"names":[],"mappings":"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"}
@@ -106,7 +106,7 @@ export interface FloorOptions {
106
106
  /** What the floor keeps a run's paths out of. */
107
107
  export type FloorProtected = 'NAMZU_HOME' | 'the browser profiles';
108
108
  /** Why the floor denied a call: for tests and diagnostics. */
109
- export type FloorReason = `names ${FloorProtected}` | 'scheduler command' | `word names ${FloorProtected}` | `assigned value names ${FloorProtected}` | `redirection names ${FloorProtected}` | `loop or case word names ${FloorProtected}` | 'opaque line mentions a protected name' | 'unread text mentions a protected name' | 'too many spellings';
109
+ export type FloorReason = `names ${FloorProtected}` | 'scheduler command' | `word names ${FloorProtected}` | `assigned value names ${FloorProtected}` | `redirection names ${FloorProtected}` | `loop or case word names ${FloorProtected}` | 'opaque line mentions a protected name' | 'unread text mentions a protected name' | 'encoded command cannot be read at all' | 'find -exec reaches a protected target' | 'runs a file the script wrote earlier' | 'too many spellings';
110
110
  /** Why the floor denied a call, and what in it matched. */
111
111
  export interface FloorFinding {
112
112
  readonly reason: FloorReason;
@@ -1 +1 @@
1
- {"version":3,"file":"floor.d.ts","sourceRoot":"","sources":["../../src/schedule/floor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoFG;AAEH,OAAO,EACN,KAAK,0BAA0B,EAC/B,KAAK,iBAAiB,EAStB,MAAM,YAAY,CAAA;AAEnB,qEAAqE;AACrE,eAAO,MAAM,eAAe,EAAE,SAAS,MAAM,EAAkD,CAAA;AAE/F,MAAM,WAAW,YAAY;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,+CAA+C;IAC/C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CACnC;AAED,iDAAiD;AACjD,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,sBAAsB,CAAA;AAElE,8DAA8D;AAC9D,MAAM,MAAM,WAAW,GACpB,SAAS,cAAc,EAAE,GACzB,mBAAmB,GACnB,cAAc,cAAc,EAAE,GAC9B,wBAAwB,cAAc,EAAE,GACxC,qBAAqB,cAAc,EAAE,GACrC,2BAA2B,cAAc,EAAE,GAC3C,uCAAuC,GACvC,uCAAuC,GACvC,oBAAoB,CAAA;AAEvB,2DAA2D;AAC3D,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACvB;AAED,sFAAsF;AACtF,wBAAgB,wBAAwB,CACvC,OAAO,EAAE,YAAY,GACnB,CAAC,IAAI,EAAE,0BAA0B,KAAK,WAAW,GAAG,IAAI,CAG1D;AAED,4EAA4E;AAC5E,wBAAgB,wBAAwB,CACvC,OAAO,EAAE,YAAY,GACnB,CAAC,IAAI,EAAE,0BAA0B,KAAK,YAAY,GAAG,IAAI,CAG3D;AAED,0FAA0F;AAC1F,wBAAgB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAE1D;AAQD,2DAA2D;AAC3D,wBAAgB,WAAW,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAE5D;AAED,kCAAkC;AAClC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,GAAG,iBAAiB,CAe9E;AAimCD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAStD"}
1
+ {"version":3,"file":"floor.d.ts","sourceRoot":"","sources":["../../src/schedule/floor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoFG;AAEH,OAAO,EACN,KAAK,0BAA0B,EAC/B,KAAK,iBAAiB,EAUtB,MAAM,YAAY,CAAA;AAEnB,qEAAqE;AACrE,eAAO,MAAM,eAAe,EAAE,SAAS,MAAM,EAAkD,CAAA;AAE/F,MAAM,WAAW,YAAY;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,+CAA+C;IAC/C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CACnC;AAED,iDAAiD;AACjD,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,sBAAsB,CAAA;AAElE,8DAA8D;AAC9D,MAAM,MAAM,WAAW,GACpB,SAAS,cAAc,EAAE,GACzB,mBAAmB,GACnB,cAAc,cAAc,EAAE,GAC9B,wBAAwB,cAAc,EAAE,GACxC,qBAAqB,cAAc,EAAE,GACrC,2BAA2B,cAAc,EAAE,GAC3C,uCAAuC,GACvC,uCAAuC,GACvC,uCAAuC,GACvC,uCAAuC,GACvC,sCAAsC,GACtC,oBAAoB,CAAA;AAEvB,2DAA2D;AAC3D,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACvB;AAED,sFAAsF;AACtF,wBAAgB,wBAAwB,CACvC,OAAO,EAAE,YAAY,GACnB,CAAC,IAAI,EAAE,0BAA0B,KAAK,WAAW,GAAG,IAAI,CAG1D;AAED,4EAA4E;AAC5E,wBAAgB,wBAAwB,CACvC,OAAO,EAAE,YAAY,GACnB,CAAC,IAAI,EAAE,0BAA0B,KAAK,YAAY,GAAG,IAAI,CAG3D;AAED,0FAA0F;AAC1F,wBAAgB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAE1D;AAQD,2DAA2D;AAC3D,wBAAgB,WAAW,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAE5D;AAED,kCAAkC;AAClC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,GAAG,iBAAiB,CAe9E;AAy3CD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAStD"}
@@ -83,7 +83,7 @@
83
83
  *
84
84
  * Everything here is linear in the input.
85
85
  */
86
- import { builtinCommandArguments, commandArgumentOf, lexShellCommandLine, nestedShellCommand, } from '@namzu/sdk';
86
+ import { builtinCommandArguments, commandArgumentOf, lexShellCommandLine, nestedShellCommand, programPositions, } from '@namzu/sdk';
87
87
  /** Scheduler verbs a run may use: they read, they change nothing. */
88
88
  export const READ_ONLY_VERBS = ['list', 'show', 'status', 'history', 'logs'];
89
89
  /** The floor's reason for denying a call, or null: what the rule below decides on. */
@@ -185,6 +185,17 @@ const STRING_OPTIONS = {
185
185
  sudo: /^-(?:[a-z]*[si][a-z]*|-shell|-login)$/,
186
186
  env: /^-(?:[a-z]*s[a-z]*|-split-string(?:=.*)?)$/i,
187
187
  };
188
+ /** PowerShell's two names, wherever the floor treats them as an unread shell. */
189
+ const POWERSHELL_NAMES = new Set(['powershell', 'pwsh']);
190
+ /**
191
+ * `-EncodedCommand` and every unambiguous prefix PowerShell itself accepts
192
+ * (`-e`, `-en`, `-enc`, …, up to the full name), case-insensitive. Unlike
193
+ * `-Command`/`-c`, whose literal text the tripwire can still read, the
194
+ * argument here is base64 of UTF-16LE: nothing in it can be read as text, so
195
+ * a line the tripwire finds nothing in front of proves nothing about what it
196
+ * runs.
197
+ */
198
+ const ENCODED_COMMAND_FLAG = /^-e(?:n(?:c(?:o(?:d(?:e(?:d(?:c(?:o(?:m(?:m(?:a(?:n(?:d)?)?)?)?)?)?)?)?)?)?)?)?)?$/i;
188
199
  /** What a refusal says about a `schedule` verb that is not read-only. */
189
200
  const NOT_READ_ONLY = `a \`namzu schedule\` subcommand other than ${READ_ONLY_VERBS.join(', ')}`;
190
201
  const NAME_CHAR = /[a-z0-9._-]/;
@@ -235,15 +246,51 @@ function wordsOf(command, catalogue) {
235
246
  * before; for a question of "is there such a subsequence" that loses
236
247
  * nothing, and it is one pass.
237
248
  */
238
- function reachesScheduler(words, daemonCommandLine) {
249
+ function reachesScheduler(command, words, daemonCommandLine, dialect) {
239
250
  const n = words.length;
240
251
  const find = (from, test) => {
241
252
  for (let i = from; i < n; i++)
242
- if (test(words[i]))
253
+ if (test(words[i], i))
243
254
  return i;
244
255
  return -1;
245
256
  };
246
- const named = (name) => (w) => w.wild ? w.text.includes(name) : commandName(w.text) === name;
257
+ // Where a program name could stand: `programPositions` — the SDK's one
258
+ // answer to "where does this command actually exec a program" — unwraps
259
+ // a chain of re-exec wrappers (`sudo`, `env`, `nice`, `timeout`, …) with
260
+ // each one's own real option grammar, rather than assuming the program
261
+ // sits right after the wrapper's name (`timeout 5 prog`, `stdbuf -oL
262
+ // prog` and `ionice -c2 -n7 prog` all take at least one word of their
263
+ // own first). `words`/`command.words` share indices, so a resolved
264
+ // position's word maps straight back to one. A position `programPositions`
265
+ // could not resolve because that word ITSELF expands (`sudo
266
+ // $(echo systemctl) stop x`) still carries the word, so it becomes a
267
+ // head here too — same as before, just correctly placed after however
268
+ // many wrapper words came first. A literal `source`/`.` with a literal
269
+ // path, or a literal `eval` whose literal payload lexes cleanly, is read
270
+ // the same way and adds its own resolved position(s) here too. A
271
+ // position it could not resolve for a structural reason with no wild
272
+ // word of its own (an option a wrapper does not recognise, too many
273
+ // wrappers, an opaque `eval` payload) carries no word and adds nothing
274
+ // here; that gap is not this fix's scope — an interpreter or unread
275
+ // construct is the tripwire's and `runsUnreadText`'s job, not this one's.
276
+ const heads = new Set();
277
+ for (const position of programPositions(command, dialect)) {
278
+ if (position.word === undefined)
279
+ continue;
280
+ const idx = command.words.indexOf(position.word);
281
+ if (idx >= 0)
282
+ heads.add(idx);
283
+ }
284
+ // A wild (expanding) word in one of those positions cannot be read as
285
+ // NOT being the tool in question — a substring check on its raw,
286
+ // unevaluated text (`$(echo pk)ill`) is defeated by gluing a literal
287
+ // suffix onto the substitution, which never contains the tool's name as
288
+ // one contiguous run. Unknown never passes: such a word counts as being
289
+ // every name checked here, the same way `namesNamzu`/a verb check
290
+ // already treats a wild argument. A wild word elsewhere (an argument, a
291
+ // path — `find ~/.namzu -exec …`'s `~/.namzu`) is not a program name and
292
+ // is read as what it actually is, an argument, by the checks below.
293
+ const named = (name) => (w, i) => (w.wild && heads.has(i)) || commandName(w.text) === name;
247
294
  const namesNamzu = (w) => w.wild || w.text.includes('namzu');
248
295
  // systemctl <verb> <unit>
249
296
  const systemctl = find(0, named('systemctl'));
@@ -270,7 +317,7 @@ function reachesScheduler(words, daemonCommandLine) {
270
317
  return "deletes, changes or ends the scheduler's task";
271
318
  }
272
319
  // pkill / killall: a pattern that could match the daemon's process.
273
- const killer = find(0, (w) => named('pkill')(w) || named('killall')(w));
320
+ const killer = find(0, (w, i) => named('pkill')(w, i) || named('killall')(w, i));
274
321
  if (killer >= 0) {
275
322
  const kills = "has a pattern that could match the scheduler's process";
276
323
  const full = words
@@ -291,7 +338,7 @@ function reachesScheduler(words, daemonCommandLine) {
291
338
  }
292
339
  }
293
340
  // A D-Bus call to systemd naming the unit.
294
- const dbus = find(0, (w) => DBUS_TOOLS.has(commandName(w.text)));
341
+ const dbus = find(0, (w, i) => (w.wild && heads.has(i)) || DBUS_TOOLS.has(commandName(w.text)));
295
342
  if (dbus >= 0 && find(dbus + 1, namesNamzu) >= 0)
296
343
  return 'calls systemd about a namzu unit';
297
344
  // The CLI, by name (`namzu`, a path to it, `npx @namzu/cli`) or through
@@ -344,6 +391,17 @@ function runsUnreadText(command) {
344
391
  return `${program} runs text as code`;
345
392
  if (i === head && name === '.')
346
393
  return '`.` runs a file as commands';
394
+ if (i === head && name === 'trap') {
395
+ // `trap [-lp] [action] [sigspec...]`: the action, when given, runs as
396
+ // a command line when the signal fires — read the same way a nested
397
+ // shell's `-c` payload is, just later. `-l`/`-p`/`--` alone print or
398
+ // reset and run nothing.
399
+ const action = words
400
+ .slice(i + 1)
401
+ .find((w) => w.expands || (w.value !== '-l' && w.value !== '-p' && w.value !== '--'));
402
+ if (action !== undefined)
403
+ return `${program} runs its action as a command when the signal fires`;
404
+ }
347
405
  if (SHELLS.has(name) && !(i >= head && i <= readTo))
348
406
  return `${program} runs commands the floor does not read`;
349
407
  }
@@ -354,6 +412,22 @@ function runsUnreadText(command) {
354
412
  }
355
413
  return null;
356
414
  }
415
+ /**
416
+ * `powershell`/`pwsh`, anywhere in the command, given `-EncodedCommand` (or
417
+ * an unambiguous prefix of it): the program word and the flag, or null.
418
+ */
419
+ function encodedCommandCall(command) {
420
+ const words = command.words;
421
+ for (let i = command.assignments; i < words.length; i++) {
422
+ const word = words[i];
423
+ if (word.expands || !POWERSHELL_NAMES.has(commandName(word.value)))
424
+ continue;
425
+ const flag = words.slice(i + 1).find((w) => !w.expands && ENCODED_COMMAND_FLAG.test(w.value));
426
+ if (flag !== undefined)
427
+ return { program: word, flag };
428
+ }
429
+ return null;
430
+ }
357
431
  // ---------------------------------------------------------------------------
358
432
  // Paths
359
433
  /**
@@ -649,17 +723,14 @@ class Floor {
649
723
  lineVerdict(line, dialect, depth = 0) {
650
724
  // A line for a shell that may be bash or a POSIX shell is read both
651
725
  // ways, and denied if either reading denies it.
652
- const readings = dialect === 'bash'
653
- ? [lexShellCommandLine(line, { dialect: 'bash' })]
654
- : [
655
- lexShellCommandLine(line, { dialect: 'sh' }),
656
- lexShellCommandLine(line, { dialect: 'bash' }),
657
- ];
726
+ const dialects = dialect === 'bash' ? ['bash'] : ['sh', 'bash'];
727
+ const readings = dialects.map((d) => lexShellCommandLine(line, { dialect: d }));
658
728
  const catalogue = /textdomain/i.test(line);
659
729
  const loops = /\b(?:while|until|for|select)\b/.test(line);
660
730
  let unaccounted = null;
661
- for (const reading of readings) {
662
- const finding = this.readingVerdict(reading, catalogue, loops);
731
+ for (let i = 0; i < readings.length; i += 1) {
732
+ const reading = readings[i];
733
+ const finding = this.readingVerdict(reading, catalogue, loops, dialects[i]);
663
734
  if (finding !== null)
664
735
  return finding;
665
736
  if (reading.opaque) {
@@ -793,9 +864,17 @@ class Floor {
793
864
  }
794
865
  return null;
795
866
  }
796
- readingVerdict(reading, catalogue, loops) {
867
+ readingVerdict(reading, catalogue, loops, dialect) {
868
+ for (const command of reading.commands) {
869
+ const encoded = encodedCommandCall(command);
870
+ if (encoded !== null)
871
+ return {
872
+ reason: 'encoded command cannot be read at all',
873
+ detail: `${shown(encoded.program.value)} ${shown(encoded.flag.value)} runs a base64-encoded script the floor cannot read at all, so it is refused outright whatever it decodes to; use -Command '<literal text>' instead`,
874
+ };
875
+ }
797
876
  for (const command of reading.commands) {
798
- const does = reachesScheduler(wordsOf(command, catalogue), this.daemonCommandLine);
877
+ const does = reachesScheduler(command, wordsOf(command, catalogue), this.daemonCommandLine, dialect);
799
878
  if (does !== null)
800
879
  return {
801
880
  reason: 'scheduler command',
@@ -819,8 +898,32 @@ class Floor {
819
898
  for (const command of reading.commands)
820
899
  cwd.after(command, (target) => this.spell(target, variables, cwd.current()), this.userHome);
821
900
  const seen = new Set();
901
+ /** Files an earlier command in this line wrote, resolved and normalized; content unverifiable. */
902
+ const written = new Set();
822
903
  for (const command of reading.commands) {
823
904
  const here = cwd.current();
905
+ const findExec = this.findExecFinding(command, variables, here, dialect);
906
+ if (findExec !== null)
907
+ return findExec;
908
+ const executed = executedPath(command);
909
+ if (executed !== null && !executed.expands) {
910
+ for (const base of isAbsolute(executed.value) ? [''] : here.dirs) {
911
+ const path = base === '' ? normalize(executed.value) : join(base, executed.value);
912
+ if (written.has(path)) {
913
+ return {
914
+ reason: 'runs a file the script wrote earlier',
915
+ detail: `${shown(command.text)} runs ${shown(executed.text)}, which an earlier command in this script wrote; its content cannot be verified, so it is refused rather than run unattended`,
916
+ };
917
+ }
918
+ }
919
+ }
920
+ for (const target of writeTargets(command)) {
921
+ if (target.expands)
922
+ continue;
923
+ for (const base of isAbsolute(target.value) ? [''] : here.dirs) {
924
+ written.add(base === '' ? normalize(target.value) : join(base, target.value));
925
+ }
926
+ }
824
927
  const check = (word) => this.wordNames(word, variables, here, expands);
825
928
  const standalone = command.words.length === command.assignments && !exported;
826
929
  for (const [i, word] of command.words.entries()) {
@@ -1129,6 +1232,174 @@ class Floor {
1129
1232
  }
1130
1233
  return false;
1131
1234
  }
1235
+ /**
1236
+ * `find … -exec|-execdir|-ok|-okdir … ;|+`: the clause runs on every path
1237
+ * `find` matches, substituted for `{}` — a placeholder no static reading
1238
+ * can resolve. Refused when `{}` stands in that clause AND the search
1239
+ * could plausibly reach a protected target: the root is unknown or is
1240
+ * NAMZU_HOME itself or an ancestor of it (so `find` would recurse THROUGH
1241
+ * it), a `-name`/`-iname`/`-path`/`-ipath` pattern could match NAMZU_HOME's
1242
+ * own folder name, or the clause's own program is a tool that can stop or
1243
+ * remove a service (`{}` there could name the scheduler's unit without
1244
+ * ever spelling it in the line the floor can read).
1245
+ */
1246
+ findExecFinding(command, variables, cwd, dialect) {
1247
+ const words = command.words;
1248
+ const findAt = programPositions(command, dialect)
1249
+ .map((position) => position.word && !position.word.expands && commandName(position.word.value) === 'find'
1250
+ ? words.indexOf(position.word)
1251
+ : -1)
1252
+ .find((index) => index >= 0);
1253
+ if (findAt === undefined)
1254
+ return null;
1255
+ const head = words[findAt];
1256
+ const rootWords = [];
1257
+ let i = findAt + 1;
1258
+ while (i < words.length) {
1259
+ const w = words[i];
1260
+ if (!w.expands && (w.value.startsWith('-') || w.value === '(' || w.value === '!'))
1261
+ break;
1262
+ rootWords.push(w);
1263
+ i++;
1264
+ }
1265
+ const roots = rootWords.length > 0
1266
+ ? rootWords
1267
+ : [{ text: '.', value: '.', expands: false, quoted: false, substitutes: false }];
1268
+ const reachesHome = roots.some((root) => {
1269
+ if (root.expands)
1270
+ return true;
1271
+ for (const spelling of this.spell(root, variables, cwd)) {
1272
+ if (spelling.stop !== 'end')
1273
+ return true;
1274
+ const bases = isAbsolute(spelling.known) ? [''] : cwd.dirs.length > 0 ? cwd.dirs : [''];
1275
+ for (const base of bases) {
1276
+ const path = base === '' ? normalize(spelling.known) : join(base, spelling.known);
1277
+ if (this.inside(path) || this.belowAncestor(path) !== null)
1278
+ return true;
1279
+ }
1280
+ }
1281
+ return false;
1282
+ });
1283
+ let filterMatchesHome = false;
1284
+ for (let j = findAt; j < words.length; j++) {
1285
+ const flag = words[j];
1286
+ if (flag.expands || !/^-i?(?:name|path)$/.test(flag.value))
1287
+ continue;
1288
+ const pattern = words[j + 1];
1289
+ if (!pattern || pattern.expands)
1290
+ continue;
1291
+ if (this.homeSegments.some((seg) => mayBe(lower(pattern.value), seg.name))) {
1292
+ filterMatchesHome = true;
1293
+ break;
1294
+ }
1295
+ }
1296
+ for (let j = findAt + 1; j < words.length; j++) {
1297
+ const flag = words[j];
1298
+ if (flag.expands || !/^-(?:exec|execdir|ok|okdir)$/.test(flag.value))
1299
+ continue;
1300
+ let k = j + 1;
1301
+ let hasPlaceholder = false;
1302
+ const clauseStart = k;
1303
+ while (k < words.length) {
1304
+ const w = words[k];
1305
+ if (!w.expands && (w.value === ';' || w.value === '+'))
1306
+ break;
1307
+ if (!w.expands) {
1308
+ if (w.value.includes('{}'))
1309
+ hasPlaceholder = true;
1310
+ }
1311
+ k++;
1312
+ }
1313
+ // Ask the shared resolver what this clause actually runs. The
1314
+ // first word may be `env`/`sudo`/another wrapper, and a shell -c
1315
+ // payload can itself name a service tool. Reusing a one-clause
1316
+ // find command keeps the answer tied to this clause.
1317
+ const clauseCommand = {
1318
+ ...command,
1319
+ words: [head, flag, ...words.slice(clauseStart, k)],
1320
+ assignments: 0,
1321
+ };
1322
+ const toolIsSchedulerTool = programPositions(clauseCommand, dialect)
1323
+ .slice(1)
1324
+ .some((position) => position.unknown !== undefined ||
1325
+ (position.word !== undefined &&
1326
+ SCHEDULER_EXEC_TOOLS.has(commandName(position.word.value))));
1327
+ if (hasPlaceholder && (reachesHome || filterMatchesHome || toolIsSchedulerTool)) {
1328
+ const clauseText = words
1329
+ .slice(clauseStart, k)
1330
+ .map((w) => w.text)
1331
+ .join(' ');
1332
+ const why = toolIsSchedulerTool
1333
+ ? "its clause's own program can stop or remove a service"
1334
+ : filterMatchesHome
1335
+ ? 'a -name/-iname/-path/-ipath pattern could match NAMZU_HOME’s own folder name'
1336
+ : 'its search root is unknown, is NAMZU_HOME, or is an ancestor of it';
1337
+ return {
1338
+ reason: 'find -exec reaches a protected target',
1339
+ detail: `${shown(command.text)} runs \`${flag.value} ${clauseText}\` on whatever it finds, and ${why}; the floor cannot verify what {} will stand for`,
1340
+ };
1341
+ }
1342
+ j = k;
1343
+ }
1344
+ return null;
1345
+ }
1346
+ }
1347
+ /** Tools whose `find -exec`/`-execdir`/`-ok`/`-okdir` clause the floor refuses outright: each can stop or remove a service. */
1348
+ const SCHEDULER_EXEC_TOOLS = new Set([
1349
+ 'systemctl',
1350
+ 'launchctl',
1351
+ 'schtasks',
1352
+ 'pkill',
1353
+ 'killall',
1354
+ 'busctl',
1355
+ 'dbus-send',
1356
+ 'gdbus',
1357
+ ]);
1358
+ /** A write target this command names: `>`/`>>` redirections, `tee`'s arguments, `cp`/`mv`'s destination. */
1359
+ function writeTargets(command) {
1360
+ const targets = [];
1361
+ for (const r of command.redirections) {
1362
+ if (r.operator === '>' || r.operator === '>>')
1363
+ targets.push(r.target);
1364
+ }
1365
+ const words = command.words;
1366
+ const head = words[command.assignments];
1367
+ if (!head || head.expands)
1368
+ return targets;
1369
+ const name = commandName(head.value);
1370
+ const rest = words
1371
+ .slice(command.assignments + 1)
1372
+ .filter((w) => w.expands || !w.value.startsWith('-'));
1373
+ if (name === 'tee')
1374
+ targets.push(...rest);
1375
+ else if (name === 'cp' || name === 'mv') {
1376
+ const last = rest.at(-1);
1377
+ if (last)
1378
+ targets.push(last);
1379
+ }
1380
+ return targets;
1381
+ }
1382
+ /**
1383
+ * The path this command executes as a FILE — `./x.sh`, `some/dir/x.sh`,
1384
+ * `sh x.sh`, `bash x.sh`, `. x.sh`, `source x.sh` — or null. A bare name on
1385
+ * `PATH` (`x.sh` alone, no `/`) is not: that resolves to whatever `PATH`
1386
+ * finds, not a file this line can be shown to have written.
1387
+ */
1388
+ function executedPath(command) {
1389
+ const words = command.words;
1390
+ const head = words[command.assignments];
1391
+ if (!head || head.expands)
1392
+ return null;
1393
+ const name = commandName(head.value);
1394
+ if (name === 'sh' || name === 'bash' || name === '.' || name === 'source') {
1395
+ const rest = words
1396
+ .slice(command.assignments + 1)
1397
+ .filter((w) => w.expands || !w.value.startsWith('-'));
1398
+ return rest[0] ?? null;
1399
+ }
1400
+ if (!head.expands && head.value.includes('/'))
1401
+ return head;
1402
+ return null;
1132
1403
  }
1133
1404
  /** `NAME=value`, `NAME+=value` or `--opt=value`: the value as a word of its own. */
1134
1405
  function assignedValue(word) {
@@ -1146,6 +1417,7 @@ function assignedValue(word) {
1146
1417
  value: word.value.slice(match[0].length),
1147
1418
  expands: word.expands,
1148
1419
  quoted: word.quoted,
1420
+ substitutes: word.substitutes,
1149
1421
  },
1150
1422
  };
1151
1423
  }