@adhdev/daemon-core 0.9.82-rc.35 → 0.9.82-rc.350

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 (318) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +8 -0
  2. package/dist/boot/process-hardening.d.ts +50 -0
  3. package/dist/build-info.d.ts +37 -0
  4. package/dist/chat/source-machine.d.ts +166 -0
  5. package/dist/chat/source-resolver.d.ts +104 -0
  6. package/dist/chat/subscription-updates.d.ts +1 -0
  7. package/dist/cli-adapter-types.d.ts +46 -2
  8. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  9. package/dist/cli-adapters/cli-state-engine.d.ts +198 -0
  10. package/dist/cli-adapters/provider-cli-adapter.d.ts +116 -64
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  12. package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
  13. package/dist/cli-adapters/provider-cli-shared.d.ts +53 -0
  14. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  15. package/dist/cli-adapters/resolve-executable.d.ts +14 -0
  16. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  17. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  18. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  19. package/dist/commands/cli-manager.d.ts +2 -1
  20. package/dist/commands/handler.d.ts +110 -0
  21. package/dist/commands/mesh-coordinator.d.ts +85 -1
  22. package/dist/commands/router.d.ts +324 -1
  23. package/dist/commands/upgrade-helper.d.ts +12 -0
  24. package/dist/config/chat-history.d.ts +9 -0
  25. package/dist/config/config.d.ts +5 -0
  26. package/dist/config/mesh-config.d.ts +77 -1
  27. package/dist/detection/ide-detector.d.ts +13 -0
  28. package/dist/detection/win32-ide-version.d.ts +37 -0
  29. package/dist/git/change-impact-config.d.ts +159 -0
  30. package/dist/git/git-commands.d.ts +11 -1
  31. package/dist/git/git-diff.d.ts +6 -0
  32. package/dist/git/git-executor.d.ts +11 -0
  33. package/dist/git/git-status.d.ts +23 -0
  34. package/dist/git/git-types.d.ts +2 -50
  35. package/dist/git/index.d.ts +2 -0
  36. package/dist/index.d.ts +52 -13
  37. package/dist/index.js +39739 -14302
  38. package/dist/index.js.map +1 -1
  39. package/dist/index.mjs +41369 -16055
  40. package/dist/index.mjs.map +1 -1
  41. package/dist/installer.d.ts +1 -4
  42. package/dist/ipc/local-ipc-server.d.ts +91 -0
  43. package/dist/launch.d.ts +1 -1
  44. package/dist/logging/async-batch-writer.d.ts +10 -0
  45. package/dist/logging/log-redactor.d.ts +24 -0
  46. package/dist/logging/log-tail-reader.d.ts +46 -0
  47. package/dist/mesh/contracts.d.ts +164 -0
  48. package/dist/mesh/coordinator-prompt.d.ts +36 -0
  49. package/dist/mesh/coordinator-registry.d.ts +59 -0
  50. package/dist/mesh/mesh-active-work.d.ts +174 -0
  51. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  52. package/dist/mesh/mesh-events-coordinator.d.ts +169 -0
  53. package/dist/mesh/mesh-events-pending.d.ts +55 -0
  54. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  55. package/dist/mesh/mesh-events-utils.d.ts +62 -0
  56. package/dist/mesh/mesh-events.d.ts +6 -49
  57. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  58. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  59. package/dist/mesh/mesh-init.d.ts +86 -0
  60. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  61. package/dist/mesh/mesh-ledger.d.ts +77 -1
  62. package/dist/mesh/mesh-missions.d.ts +164 -0
  63. package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
  64. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  65. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  66. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  67. package/dist/mesh/mesh-routing.d.ts +70 -0
  68. package/dist/mesh/mesh-runtime-store.d.ts +419 -0
  69. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  70. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
  71. package/dist/mesh/mesh-work-queue.d.ts +218 -5
  72. package/dist/mesh/preview-freshness.d.ts +18 -0
  73. package/dist/mesh/refine-config.d.ts +216 -0
  74. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  75. package/dist/providers/acp-provider-instance.d.ts +2 -0
  76. package/dist/providers/approval-utils.d.ts +13 -0
  77. package/dist/providers/chat-message-normalization.d.ts +12 -0
  78. package/dist/providers/cli-provider-instance.d.ts +77 -3
  79. package/dist/providers/contracts.d.ts +139 -6
  80. package/dist/providers/external-sources.d.ts +71 -0
  81. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  82. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  83. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  84. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  85. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  86. package/dist/providers/native-history/index.d.ts +13 -0
  87. package/dist/providers/provider-instance-manager.d.ts +14 -0
  88. package/dist/providers/provider-instance.d.ts +15 -1
  89. package/dist/providers/provider-loader.d.ts +26 -4
  90. package/dist/providers/provider-trust.d.ts +31 -0
  91. package/dist/providers/read-chat-contract.d.ts +29 -0
  92. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  93. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  94. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  95. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  96. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  97. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  98. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  99. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  100. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  101. package/dist/providers/sdk/v1/index.d.ts +30 -0
  102. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  103. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  104. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  105. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  106. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  107. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  108. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  109. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  110. package/dist/providers/spec/adapter.d.ts +69 -0
  111. package/dist/providers/spec/cli-adapter.d.ts +217 -0
  112. package/dist/providers/spec/evaluator.d.ts +23 -0
  113. package/dist/providers/spec/fsm-driver.d.ts +322 -0
  114. package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
  115. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  116. package/dist/providers/spec/fsm-types.d.ts +144 -0
  117. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  118. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  119. package/dist/providers/spec/route.d.ts +4 -0
  120. package/dist/providers/spec/types.d.ts +180 -0
  121. package/dist/providers/status-monitor.d.ts +7 -7
  122. package/dist/providers/transcript-v2.d.ts +176 -0
  123. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  124. package/dist/providers/working-dir.d.ts +17 -0
  125. package/dist/repo-mesh-types.d.ts +295 -13
  126. package/dist/sessions/registry.d.ts +6 -0
  127. package/dist/shared-types-extra.d.ts +2 -4
  128. package/dist/shared-types.d.ts +59 -2
  129. package/dist/status/normalize.d.ts +1 -1
  130. package/dist/status/normalize.js +1 -0
  131. package/dist/status/normalize.js.map +1 -1
  132. package/dist/status/normalize.mjs +1 -0
  133. package/dist/status/normalize.mjs.map +1 -1
  134. package/dist/status/reporter.d.ts +2 -0
  135. package/dist/status/snapshot.d.ts +26 -0
  136. package/dist/system/load-better-sqlite3.d.ts +21 -0
  137. package/dist/types.d.ts +5 -0
  138. package/package.json +7 -3
  139. package/src/agent-stream/poller.ts +2 -3
  140. package/src/agent-stream/provider-adapter.ts +1 -1
  141. package/src/boot/daemon-lifecycle.ts +55 -12
  142. package/src/boot/process-hardening.ts +89 -0
  143. package/src/build-info.ts +73 -0
  144. package/src/chat/source-machine.ts +534 -0
  145. package/src/chat/source-resolver.ts +0 -0
  146. package/src/chat/subscription-updates.ts +20 -1
  147. package/src/cli-adapter-types.d.ts +1 -0
  148. package/src/cli-adapter-types.ts +47 -2
  149. package/src/cli-adapters/cli-script-runner.ts +421 -0
  150. package/src/cli-adapters/cli-state-engine.ts +1215 -0
  151. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  152. package/src/cli-adapters/provider-cli-adapter.ts +774 -1141
  153. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  154. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  155. package/src/cli-adapters/provider-cli-runtime.ts +56 -7
  156. package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
  157. package/src/cli-adapters/provider-cli-shared.ts +100 -11
  158. package/src/cli-adapters/pty-transport.ts +2 -1
  159. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  160. package/src/cli-adapters/resolve-executable.ts +90 -0
  161. package/src/cli-adapters/session-host-transport.ts +2 -1
  162. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
  163. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  164. package/src/cli-adapters/terminal-screen.ts +16 -81
  165. package/src/commands/chat-commands.ts +1823 -62
  166. package/src/commands/cli-manager.ts +304 -14
  167. package/src/commands/handler.ts +809 -2
  168. package/src/commands/mesh-coordinator.ts +332 -122
  169. package/src/commands/router.ts +7505 -1311
  170. package/src/commands/stream-commands.ts +8 -0
  171. package/src/commands/upgrade-helper.ts +81 -30
  172. package/src/config/chat-history.ts +483 -24
  173. package/src/config/config.ts +12 -0
  174. package/src/config/mesh-config.ts +388 -6
  175. package/src/config/recent-activity.ts +8 -2
  176. package/src/daemon/dev-auto-implement.ts +3 -2
  177. package/src/daemon/dev-cli-debug.ts +10 -1
  178. package/src/detection/cli-detector.ts +28 -9
  179. package/src/detection/ide-detector.ts +55 -16
  180. package/src/detection/win32-ide-version.ts +106 -0
  181. package/src/git/change-impact-config.ts +354 -0
  182. package/src/git/git-commands.ts +55 -14
  183. package/src/git/git-diff.ts +53 -0
  184. package/src/git/git-executor.ts +12 -0
  185. package/src/git/git-status.ts +574 -43
  186. package/src/git/git-types.ts +14 -62
  187. package/src/git/git-worktree.ts +8 -1
  188. package/src/git/index.ts +16 -0
  189. package/src/index.ts +146 -12
  190. package/src/installer.d.ts +1 -1
  191. package/src/installer.ts +8 -6
  192. package/src/ipc/local-ipc-server.ts +278 -0
  193. package/src/launch.d.ts +1 -1
  194. package/src/launch.ts +37 -28
  195. package/src/logging/async-batch-writer.ts +55 -0
  196. package/src/logging/log-redactor.ts +100 -0
  197. package/src/logging/log-tail-reader.ts +220 -0
  198. package/src/logging/logger.ts +2 -1
  199. package/src/mesh/contracts.ts +329 -0
  200. package/src/mesh/coordinator-prompt.ts +234 -31
  201. package/src/mesh/coordinator-registry.ts +121 -0
  202. package/src/mesh/mesh-active-work.ts +623 -0
  203. package/src/mesh/mesh-delivery-policy.ts +315 -0
  204. package/src/mesh/mesh-events-coordinator.ts +2224 -0
  205. package/src/mesh/mesh-events-pending.ts +558 -0
  206. package/src/mesh/mesh-events-stale.ts +290 -0
  207. package/src/mesh/mesh-events-utils.ts +381 -0
  208. package/src/mesh/mesh-events.ts +30 -1035
  209. package/src/mesh/mesh-fast-forward.ts +839 -0
  210. package/src/mesh/mesh-host-ownership.ts +73 -0
  211. package/src/mesh/mesh-init.ts +260 -0
  212. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  213. package/src/mesh/mesh-ledger.ts +590 -102
  214. package/src/mesh/mesh-missions.ts +322 -0
  215. package/src/mesh/mesh-reconcile-loop.ts +989 -0
  216. package/src/mesh/mesh-refine-batch.ts +197 -0
  217. package/src/mesh/mesh-refine-status.ts +231 -0
  218. package/src/mesh/mesh-review-inbox.ts +307 -0
  219. package/src/mesh/mesh-routing.ts +291 -0
  220. package/src/mesh/mesh-runtime-store.ts +1660 -0
  221. package/src/mesh/mesh-task-stats.ts +154 -0
  222. package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
  223. package/src/mesh/mesh-work-queue.ts +901 -141
  224. package/src/mesh/preview-freshness.ts +118 -0
  225. package/src/mesh/refine-config.ts +423 -0
  226. package/src/mesh/worktree-bootstrap-config.ts +331 -0
  227. package/src/providers/acp-provider-instance.ts +25 -9
  228. package/src/providers/approval-utils.d.ts +4 -0
  229. package/src/providers/approval-utils.ts +47 -5
  230. package/src/providers/chat-message-normalization.ts +45 -5
  231. package/src/providers/cli-provider-instance.ts +1041 -99
  232. package/src/providers/contracts.d.ts +55 -0
  233. package/src/providers/contracts.ts +150 -6
  234. package/src/providers/extension-provider-instance.ts +12 -7
  235. package/src/providers/external-sources.ts +218 -0
  236. package/src/providers/ide-provider-instance.ts +35 -12
  237. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  238. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  239. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  240. package/src/providers/native-history/dispatcher.ts +340 -0
  241. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  242. package/src/providers/native-history/index.ts +30 -0
  243. package/src/providers/provider-instance-manager.ts +30 -0
  244. package/src/providers/provider-instance.ts +10 -1
  245. package/src/providers/provider-loader.ts +670 -50
  246. package/src/providers/provider-schema.ts +87 -14
  247. package/src/providers/provider-trust.ts +114 -0
  248. package/src/providers/read-chat-contract.ts +76 -16
  249. package/src/providers/sdk/README.md +49 -0
  250. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  251. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  252. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  253. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  254. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  255. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  256. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  257. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  258. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  259. package/src/providers/sdk/v1/index.ts +152 -0
  260. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  261. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  262. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  263. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  264. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  265. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  266. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  267. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  268. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  269. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  270. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  271. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  272. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  273. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  274. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  275. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  276. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  277. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  278. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  279. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  280. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  281. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  282. package/src/providers/sdk/v1/validators/index.ts +19 -0
  283. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  284. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  285. package/src/providers/spec/adapter.ts +168 -0
  286. package/src/providers/spec/cli-adapter.ts +986 -0
  287. package/src/providers/spec/evaluator.ts +345 -0
  288. package/src/providers/spec/fsm-driver.ts +1065 -0
  289. package/src/providers/spec/fsm-evaluator.ts +255 -0
  290. package/src/providers/spec/fsm-loader.ts +120 -0
  291. package/src/providers/spec/fsm-types.ts +238 -0
  292. package/src/providers/spec/native-history-executor.ts +1136 -0
  293. package/src/providers/spec/pre-launch-trust.ts +104 -0
  294. package/src/providers/spec/route.ts +51 -0
  295. package/src/providers/spec/types.ts +226 -0
  296. package/src/providers/status-monitor.d.ts +7 -7
  297. package/src/providers/status-monitor.ts +37 -22
  298. package/src/providers/transcript-v2.ts +567 -0
  299. package/src/providers/types/interactive-prompt.ts +536 -0
  300. package/src/providers/version-archive.ts +64 -24
  301. package/src/providers/working-dir.ts +23 -0
  302. package/src/repo-mesh-types.ts +378 -14
  303. package/src/sessions/registry.ts +6 -0
  304. package/src/shared-types-extra.ts +2 -4
  305. package/src/shared-types.d.ts +8 -0
  306. package/src/shared-types.ts +64 -1
  307. package/src/status/builders.ts +26 -6
  308. package/src/status/normalize.ts +2 -0
  309. package/src/status/reporter.ts +19 -1
  310. package/src/status/snapshot.ts +95 -26
  311. package/src/system/host-memory.ts +29 -12
  312. package/src/system/load-better-sqlite3.ts +68 -0
  313. package/src/types.ts +5 -0
  314. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  315. package/dist/mesh/mesh-sync.d.ts +0 -53
  316. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  317. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  318. package/src/mesh/mesh-sync.ts +0 -111
@@ -0,0 +1,472 @@
1
+ /**
2
+ * Provider script require() whitelist.
3
+ *
4
+ * Provider scripts (detect_status.js, parse_session.js, the override layer
5
+ * the v1 contract exposes, plus their _shared helpers) execute in the
6
+ * daemon process with full Node access — see sandbox/README-design.ts for
7
+ * the long story. Full isolation via isolated-vm has a real perf cost and
8
+ * a real marshalling-complexity cost, so we instead constrain the most
9
+ * dangerous attack surface: what a provider script can `require()`.
10
+ *
11
+ * The hook wraps Node's CJS `Module._load` and, when the calling file is
12
+ * inside a directory that this loader has registered as a provider script
13
+ * root, blocks every module that isn't on the allowlist below. Relative
14
+ * paths are allowed but only when they resolve back inside the same
15
+ * provider root — providers can require their sibling helpers but they
16
+ * can't reach `../../../../etc/passwd` through a clever segment.
17
+ *
18
+ * The hook installs exactly once per process; subsequent `register()` /
19
+ * `unregister()` calls just adjust the set of roots that are gated.
20
+ *
21
+ * Performance: the hook adds one `path.normalize` + one Set lookup to
22
+ * every require call coming from a registered root. At the measured
23
+ * baseline (~6 script invocations / second / session, each making
24
+ * ≤1 require during initial module evaluation, zero during steady-state
25
+ * calls), this is unmeasurable.
26
+ */
27
+
28
+ import * as path from 'node:path';
29
+ import { createRequire } from 'node:module';
30
+ import * as nodeFs from 'node:fs';
31
+ import * as nodeChildProcess from 'node:child_process';
32
+ import { LOG } from '../../../../logging/logger.js';
33
+
34
+ /**
35
+ * Standard library modules a provider script may import unchanged.
36
+ * Each entry is bare ("crypto") and node-prefixed ("node:crypto") so
37
+ * both spellings work; production scripts use a mix of the two.
38
+ *
39
+ * Anything that touches the network, the process tree, or the V8 isolate
40
+ * is intentionally absent. fs and child_process are NOT here; they're
41
+ * exposed via the shims further down with a reduced surface.
42
+ */
43
+ const SAFE_STDLIB = new Set<string>([
44
+ 'path', 'os', 'util', 'url', 'querystring', 'buffer',
45
+ 'events', 'stream', 'string_decoder', 'assert',
46
+ 'crypto', 'timers', 'punycode', 'zlib',
47
+ ]);
48
+
49
+ /**
50
+ * Standard library modules that need a shim — we expose a deliberately
51
+ * narrowed surface and trap everything else.
52
+ */
53
+ const SHIMMED_STDLIB = new Set<string>(['fs', 'child_process', 'process']);
54
+
55
+ const ALL_GATED_STDLIB = new Set<string>([
56
+ ...SAFE_STDLIB,
57
+ ...SHIMMED_STDLIB,
58
+ ]);
59
+
60
+ /** Strip an optional `node:` prefix for bookkeeping. */
61
+ function normalizeModuleName(request: string): string {
62
+ return request.startsWith('node:') ? request.slice(5) : request;
63
+ }
64
+
65
+ // ─── fs shim — read-only surface ────────────────────────────────────────────
66
+
67
+ /**
68
+ * The set of `fs` members provider scripts are allowed to call. Each is
69
+ * a read-only operation on the local filesystem. Anything that writes,
70
+ * deletes, opens for write, or alters file metadata is omitted.
71
+ *
72
+ * We expose the *real* function reference so behavior is identical to
73
+ * `require('fs')` for the operations that do pass — including streaming
74
+ * iterators, error semantics, and {withFileTypes}-style overloads.
75
+ */
76
+ const FS_READ_ONLY_MEMBERS = [
77
+ 'existsSync', 'statSync', 'lstatSync', 'realpathSync',
78
+ 'readFileSync', 'readdirSync', 'readlinkSync',
79
+ 'accessSync', 'constants',
80
+ // Async equivalents — same read-only set.
81
+ 'exists', 'stat', 'lstat', 'realpath',
82
+ 'readFile', 'readdir', 'readlink', 'access',
83
+ // Allow `fs.promises.{readFile,...}` for the modern async style.
84
+ 'promises',
85
+ ] as const;
86
+
87
+ const FS_PROMISES_READ_ONLY_MEMBERS = [
88
+ 'stat', 'lstat', 'realpath', 'readFile', 'readdir', 'readlink', 'access',
89
+ ] as const;
90
+
91
+ function buildFsShim(): typeof nodeFs {
92
+ const shim: any = {};
93
+ for (const key of FS_READ_ONLY_MEMBERS) {
94
+ if (key === 'promises') continue; // handled below
95
+ const real = (nodeFs as any)[key];
96
+ if (real !== undefined) shim[key] = real;
97
+ }
98
+ // Re-build a minimal `promises` namespace so destructuring works.
99
+ const realPromises = (nodeFs as any).promises || {};
100
+ const promisesShim: any = {};
101
+ for (const key of FS_PROMISES_READ_ONLY_MEMBERS) {
102
+ const real = realPromises[key];
103
+ if (real !== undefined) promisesShim[key] = real;
104
+ }
105
+ shim.promises = promisesShim;
106
+ return shim as typeof nodeFs;
107
+ }
108
+
109
+ const FS_SHIM = Object.freeze(buildFsShim());
110
+
111
+ // ─── child_process shim — execFileSync only ─────────────────────────────────
112
+
113
+ interface NarrowedExecFileSyncOptions {
114
+ cwd?: string;
115
+ timeout?: number;
116
+ encoding?: BufferEncoding | 'buffer';
117
+ maxBuffer?: number;
118
+ input?: string | Buffer | NodeJS.ArrayBufferView;
119
+ }
120
+
121
+ /**
122
+ * `child_process.execFileSync(file, args, opts)` wrapper that strips
123
+ * every option a provider script has no business setting — most
124
+ * importantly `shell`, which would otherwise re-introduce arbitrary
125
+ * command injection. `args` must be a string array; `file` is a
126
+ * single binary name or absolute path; `opts` is narrowed to the
127
+ * fields below.
128
+ *
129
+ * `spawn`, `exec`, `execSync`, `fork` are intentionally absent from
130
+ * the shim. Providers that need them should ship a daemon-side
131
+ * capability instead of running raw subprocesses out of their script.
132
+ */
133
+ function shimmedExecFileSync(
134
+ file: unknown,
135
+ args?: unknown,
136
+ options?: unknown,
137
+ ): Buffer | string {
138
+ if (typeof file !== 'string' || file.length === 0) {
139
+ throw new TypeError('execFileSync: `file` must be a non-empty string');
140
+ }
141
+ let safeArgs: string[] = [];
142
+ if (args !== undefined && args !== null) {
143
+ if (!Array.isArray(args) || !args.every((a) => typeof a === 'string')) {
144
+ throw new TypeError('execFileSync: `args` must be a string[] (no shell expansion).');
145
+ }
146
+ safeArgs = args as string[];
147
+ }
148
+ const safeOpts: NarrowedExecFileSyncOptions = {};
149
+ if (options && typeof options === 'object') {
150
+ const o = options as Record<string, unknown>;
151
+ if (typeof o.cwd === 'string') safeOpts.cwd = o.cwd;
152
+ if (typeof o.timeout === 'number') safeOpts.timeout = o.timeout;
153
+ if (typeof o.encoding === 'string') safeOpts.encoding = o.encoding as BufferEncoding;
154
+ if (typeof o.maxBuffer === 'number') safeOpts.maxBuffer = o.maxBuffer;
155
+ if (typeof o.input === 'string' || o.input instanceof Buffer) {
156
+ safeOpts.input = o.input as any;
157
+ }
158
+ // shell / env / uid / gid / detached / windowsHide / killSignal — all dropped.
159
+ }
160
+ return nodeChildProcess.execFileSync(file, safeArgs, safeOpts as any);
161
+ }
162
+
163
+ const CHILD_PROCESS_SHIM = Object.freeze({
164
+ execFileSync: shimmedExecFileSync,
165
+ });
166
+
167
+ // ─── process shim — block lifecycle/abort/native bridges ────────────────────
168
+
169
+ /**
170
+ * The methods on `globalThis.process` that a provider script must NEVER be
171
+ * able to call. `exit`/`kill`/`abort` would terminate the daemon; `binding`
172
+ * and `dlopen` are escape hatches that re-introduce arbitrary native code
173
+ * (the whole reason the require() whitelist exists in the first place).
174
+ */
175
+ const DANGEROUS_PROCESS_METHODS = new Set<string>([
176
+ 'exit', 'kill', 'abort', 'binding', 'dlopen', '_kill', '_exit',
177
+ 'reallyExit', '_fatalException',
178
+ ]);
179
+
180
+ let _processGloballyHardened = false;
181
+ let _originalProcessMethods: Map<string, unknown> | null = null;
182
+
183
+ /**
184
+ * Decide whether the current call stack passes through a registered provider
185
+ * script root. We use `new Error().stack` (cheaper than `Error.captureStackTrace`
186
+ * for hot paths because we never need a frame object). The stack string format
187
+ * is `at <fn> (<filename>:line:col)` per frame; we scan for any registered
188
+ * provider root substring. This is best-effort: a determined attacker who
189
+ * controls a non-provider module loaded BY a provider could escape this, but
190
+ * the require() whitelist already prevents loading such modules.
191
+ */
192
+ function callStackTouchesProviderRoot(): boolean {
193
+ if (_gatedRoots.length === 0) return false;
194
+ const stack = new Error().stack || '';
195
+ for (const root of _gatedRoots) {
196
+ if (stack.includes(root.rootPath)) return true;
197
+ }
198
+ return false;
199
+ }
200
+
201
+ /**
202
+ * Wrap `globalThis.process` methods so calls from provider script stacks
203
+ * throw, while every other caller (daemon-core itself, npm deps, the test
204
+ * harness) goes through untouched.
205
+ *
206
+ * Trade-off: this is a stack-introspection check on every call to the
207
+ * affected methods. The methods in question (exit/kill/abort/binding/dlopen)
208
+ * are not hot paths — they're either never called or called once at
209
+ * shutdown. The cost is negligible.
210
+ *
211
+ * Alternative considered: wrap only the module-scope `process` binding via
212
+ * Module._extensions. Node's ESM/CJS semantics make this unreliable across
213
+ * dynamic-require paths. Stack introspection is uglier but correct.
214
+ *
215
+ * Caveat: a provider script that grabs `globalThis.process.exit` once and
216
+ * passes the reference to a non-provider callback can still escape — the
217
+ * stack at the actual call site no longer goes through provider code. We
218
+ * accept this gap; the threat model assumes scripts are not actively
219
+ * trying to launder calls through arbitrary daemon callbacks, just that
220
+ * a naive `process.exit(0)` should not silently terminate the daemon.
221
+ */
222
+ export function installProviderProcessShim(): void {
223
+ if (_processGloballyHardened) return;
224
+ _processGloballyHardened = true;
225
+ const proc = globalThis.process as unknown as Record<string, unknown>;
226
+ if (!proc) return;
227
+ _originalProcessMethods = new Map();
228
+ for (const name of DANGEROUS_PROCESS_METHODS) {
229
+ const original = proc[name];
230
+ if (typeof original !== 'function') continue;
231
+ _originalProcessMethods.set(name, original);
232
+ const wrapped = function gatedProcessMethod(this: unknown, ...args: unknown[]): unknown {
233
+ if (callStackTouchesProviderRoot()) {
234
+ const err: any = new Error(
235
+ `process.${name}() denied: provider scripts cannot terminate, signal, or extend the daemon process.`,
236
+ );
237
+ err.code = 'PROVIDER_PROCESS_DENIED';
238
+ err.method = name;
239
+ throw err;
240
+ }
241
+ return (original as (...a: unknown[]) => unknown).apply(this, args);
242
+ };
243
+ try {
244
+ // Preserve `.name` for stack traces / debugging tools.
245
+ Object.defineProperty(wrapped, 'name', { value: name, configurable: true });
246
+ proc[name] = wrapped;
247
+ } catch {
248
+ // Some Node builds mark certain process.* members non-writable.
249
+ // Skip silently; the require('process') gate still applies.
250
+ }
251
+ }
252
+ }
253
+
254
+ /** For tests — restore the original process.* methods. */
255
+ export function _uninstallProviderProcessShimForTest(): void {
256
+ if (!_originalProcessMethods) {
257
+ _processGloballyHardened = false;
258
+ return;
259
+ }
260
+ const proc = globalThis.process as unknown as Record<string, unknown>;
261
+ for (const [name, original] of _originalProcessMethods) {
262
+ try { proc[name] = original; } catch { /* noop */ }
263
+ }
264
+ _originalProcessMethods = null;
265
+ _processGloballyHardened = false;
266
+ }
267
+
268
+ /**
269
+ * Build the `process` shim returned to provider scripts that do
270
+ * `require('process')`. Same surface as `globalThis.process` minus the
271
+ * dangerous methods, which are replaced with throwers regardless of caller
272
+ * (because they got here via the whitelist — caller is definitively a
273
+ * provider script).
274
+ */
275
+ function buildProcessShim(): NodeJS.Process {
276
+ const real = globalThis.process;
277
+ const shim: Record<string, unknown> = Object.create(null);
278
+ for (const key of Object.keys(real) as Array<keyof NodeJS.Process>) {
279
+ if (DANGEROUS_PROCESS_METHODS.has(String(key))) continue;
280
+ try { shim[key as string] = (real as any)[key]; } catch { /* noop */ }
281
+ }
282
+ for (const name of DANGEROUS_PROCESS_METHODS) {
283
+ shim[name] = function blockedProcessMethod(): never {
284
+ const err: any = new Error(
285
+ `process.${name}() denied via require('process'): provider scripts cannot terminate, signal, or extend the daemon.`,
286
+ );
287
+ err.code = 'PROVIDER_PROCESS_DENIED';
288
+ err.method = name;
289
+ throw err;
290
+ };
291
+ }
292
+ return Object.freeze(shim) as unknown as NodeJS.Process;
293
+ }
294
+
295
+ const PROCESS_SHIM = buildProcessShim();
296
+
297
+ // ─── Hook installation ─────────────────────────────────────────────────────
298
+
299
+ interface GatedRoot {
300
+ /** Normalized, realpath-resolved absolute path. */
301
+ rootPath: string;
302
+ }
303
+
304
+ const _gatedRoots: GatedRoot[] = [];
305
+ let _installed = false;
306
+
307
+ /**
308
+ * Register a directory whose `.js` files should be subject to the
309
+ * whitelist when they call require(). Idempotent — repeated registration
310
+ * of the same root is a no-op. Pass the same string back to
311
+ * `unregisterProviderScriptRoot` to remove it.
312
+ */
313
+ export function registerProviderScriptRoot(absRoot: string): void {
314
+ const normalized = canonicalize(absRoot);
315
+ if (!normalized) return;
316
+ if (_gatedRoots.some((g) => g.rootPath === normalized)) return;
317
+ _gatedRoots.push({ rootPath: normalized });
318
+ ensureInstalled();
319
+ }
320
+
321
+ /** Inverse of registerProviderScriptRoot. */
322
+ export function unregisterProviderScriptRoot(absRoot: string): void {
323
+ const normalized = canonicalize(absRoot);
324
+ if (!normalized) return;
325
+ const idx = _gatedRoots.findIndex((g) => g.rootPath === normalized);
326
+ if (idx >= 0) _gatedRoots.splice(idx, 1);
327
+ }
328
+
329
+ /** For tests — drop every registration and reset internal state. */
330
+ export function _resetProviderScriptRoots(): void {
331
+ _gatedRoots.length = 0;
332
+ }
333
+
334
+ /** For tests — peek at the gated root list. */
335
+ export function _getRegisteredRoots(): readonly string[] {
336
+ return _gatedRoots.map((g) => g.rootPath);
337
+ }
338
+
339
+ function canonicalize(p: string): string | null {
340
+ try {
341
+ const resolved = path.resolve(p);
342
+ try {
343
+ return nodeFs.realpathSync.native
344
+ ? nodeFs.realpathSync.native(resolved)
345
+ : nodeFs.realpathSync(resolved);
346
+ } catch {
347
+ // Root may not exist yet (e.g. external dir created lazily);
348
+ // we still register the resolved path so future requires from
349
+ // inside are gated once the directory shows up.
350
+ return resolved;
351
+ }
352
+ } catch {
353
+ return null;
354
+ }
355
+ }
356
+
357
+ function isCallerInsideGatedRoot(callerFilename: string): GatedRoot | null {
358
+ if (!callerFilename) return null;
359
+ let normalized: string;
360
+ try { normalized = canonicalize(callerFilename) || callerFilename; } catch { normalized = callerFilename; }
361
+ for (const root of _gatedRoots) {
362
+ if (normalized === root.rootPath) return root;
363
+ if (normalized.startsWith(root.rootPath + path.sep)) return root;
364
+ }
365
+ return null;
366
+ }
367
+
368
+ /**
369
+ * Wrap Module._load exactly once. Subsequent calls to register/unregister
370
+ * just adjust the set of roots — the hook itself stays installed.
371
+ */
372
+ function ensureInstalled(): void {
373
+ if (_installed) return;
374
+ _installed = true;
375
+
376
+ // CJS-only hook. ESM provider scripts are not currently supported
377
+ // (everything in adhdev-providers/ is CJS), so we don't need a
378
+ // matching `module.register()` ESM loader. If/when we accept ESM
379
+ // provider scripts, this hook becomes a no-op for them and we'll
380
+ // need to extend it.
381
+ const Module = require('module') as any;
382
+ const originalLoad = Module._load as (
383
+ request: string,
384
+ parent: NodeJS.Module | null,
385
+ isMain: boolean,
386
+ ) => unknown;
387
+
388
+ Module._load = function gatedLoad(
389
+ this: unknown,
390
+ request: string,
391
+ parent: NodeJS.Module | null,
392
+ isMain: boolean,
393
+ ): unknown {
394
+ const callerFilename = parent?.filename || '';
395
+ const gated = isCallerInsideGatedRoot(callerFilename);
396
+ if (!gated) {
397
+ // Caller is not provider code — bypass the gate entirely so
398
+ // daemon-core, vendored npm modules, etc. work as before.
399
+ return originalLoad.call(this, request, parent, isMain);
400
+ }
401
+ return gatedRequire.call(this, request, parent, isMain, gated, originalLoad);
402
+ };
403
+ }
404
+
405
+ /**
406
+ * The actual gate. Runs only when the caller lives inside a registered
407
+ * provider script root. `this` is forwarded from Module._load so
408
+ * delegating to originalLoad mirrors Node's own semantics.
409
+ */
410
+ function gatedRequire(
411
+ this: unknown,
412
+ request: string,
413
+ parent: NodeJS.Module | null,
414
+ isMain: boolean,
415
+ gated: GatedRoot,
416
+ originalLoad: (this: unknown, request: string, parent: NodeJS.Module | null, isMain: boolean) => unknown,
417
+ ): unknown {
418
+ // 1. Relative imports — must stay inside the same provider root.
419
+ if (request.startsWith('./') || request.startsWith('../') || path.isAbsolute(request)) {
420
+ let resolved: string;
421
+ try {
422
+ // Reuse the caller's require to mirror Node's resolution rules.
423
+ const callerRequire = parent?.filename ? createRequire(parent.filename) : createRequire(path.join(gated.rootPath, '__entry__.js'));
424
+ resolved = callerRequire.resolve(request);
425
+ } catch {
426
+ // Fall through to the regular loader; it will throw the
427
+ // standard MODULE_NOT_FOUND with the same path Node would.
428
+ return originalLoad.call(this, request, parent, isMain);
429
+ }
430
+ const resolvedCanon = canonicalize(resolved) || resolved;
431
+ if (!(resolvedCanon === gated.rootPath || resolvedCanon.startsWith(gated.rootPath + path.sep))) {
432
+ denyRequire(request, parent, `relative path escapes provider root (resolved to ${resolvedCanon})`);
433
+ }
434
+ // Inside the root — let it through. The hook will gate any
435
+ // subsequent require() calls made by the loaded file too.
436
+ return originalLoad.call(this, request, parent, isMain);
437
+ }
438
+
439
+ // 2. Bare specifier — only the stdlib subset is allowed.
440
+ const normalized = normalizeModuleName(request);
441
+ if (!ALL_GATED_STDLIB.has(normalized)) {
442
+ denyRequire(request, parent, 'module not on the provider script whitelist');
443
+ }
444
+ if (normalized === 'fs') return FS_SHIM;
445
+ if (normalized === 'child_process') return CHILD_PROCESS_SHIM;
446
+ if (normalized === 'process') return PROCESS_SHIM;
447
+ // SAFE_STDLIB — pass through.
448
+ return originalLoad.call(this, request, parent, isMain);
449
+ }
450
+
451
+ function denyRequire(request: string, parent: NodeJS.Module | null, reason: string): never {
452
+ const caller = parent?.filename || '<unknown>';
453
+ const message = `Provider script require('${request}') denied: ${reason}. Caller: ${caller}`;
454
+ LOG.warn('ProviderScript', `[require-whitelist] ${message}`);
455
+ const err: any = new Error(message);
456
+ err.code = 'PROVIDER_REQUIRE_DENIED';
457
+ err.deniedModule = request;
458
+ err.callerFilename = caller;
459
+ throw err;
460
+ }
461
+
462
+ /**
463
+ * Public re-exports — registry consumers want to know what's on the
464
+ * allowlist without parsing this file.
465
+ */
466
+ export const PROVIDER_REQUIRE_POLICY = Object.freeze({
467
+ safeStdlib: Array.from(SAFE_STDLIB).sort(),
468
+ shimmedStdlib: Array.from(SHIMMED_STDLIB).sort(),
469
+ fsAllowedMembers: Array.from(FS_READ_ONLY_MEMBERS),
470
+ childProcessAllowedMembers: ['execFileSync'],
471
+ processDeniedMembers: Array.from(DANGEROUS_PROCESS_METHODS).sort(),
472
+ });
@@ -0,0 +1,150 @@
1
+ /**
2
+ * script-runner.ts — SandboxedScriptRunner interface + DirectEvalRunner stub
3
+ *
4
+ * Phase 5 scaffold: defines the interface that the isolated-vm implementation
5
+ * will satisfy, and provides a DirectEvalRunner that keeps providers working
6
+ * without the native isolated-vm package.
7
+ *
8
+ * See README-design.ts for the full design rationale.
9
+ */
10
+
11
+ import { LOG } from '../../../../logging/logger.js';
12
+
13
+ /** Options controlling the resource limits applied to a sandboxed script call. */
14
+ export interface SandboxOptions {
15
+ /** Maximum CPU time (wall-clock) in milliseconds before the script is killed. Default: 50. */
16
+ cpuTimeLimitMs: number;
17
+ /** Maximum V8 heap size in megabytes for the Isolate. Default: 32. */
18
+ memoryLimitMb: number;
19
+ /**
20
+ * Whitelist of global names the script may access.
21
+ * The full allowed surface is documented in README-design.ts.
22
+ * Default: ['console', 'JSON', 'Math', 'Date', 'String', 'Number', 'Boolean',
23
+ * 'Array', 'Object', 'RegExp']
24
+ */
25
+ allowedGlobals: string[];
26
+ }
27
+
28
+ /** Default resource limits applied when no options are supplied. */
29
+ export const DEFAULT_SANDBOX_OPTIONS: SandboxOptions = {
30
+ cpuTimeLimitMs: 50,
31
+ memoryLimitMb: 32,
32
+ allowedGlobals: ['console', 'JSON', 'Math', 'Date', 'String', 'Number', 'Boolean', 'Array', 'Object', 'RegExp'],
33
+ };
34
+
35
+ /**
36
+ * Contract for sandboxed script execution.
37
+ *
38
+ * Implementations:
39
+ * - DirectEvalRunner — no-op stub; uses eval() with a try/catch (unsafe, no limits).
40
+ * - IsolatedVmRunner — Phase 5 full implementation using the `isolated-vm` package.
41
+ *
42
+ * Usage:
43
+ * const runner = createScriptRunner({ cpuTimeLimitMs: 50, memoryLimitMb: 32 });
44
+ * const result = await runner.run(scriptSource, { input, sdk });
45
+ * runner.dispose();
46
+ */
47
+ export interface SandboxedScriptRunner {
48
+ /**
49
+ * Execute `script` synchronously inside a sandboxed context.
50
+ *
51
+ * @param script JavaScript source string. The script body should
52
+ * evaluate to the desired return value (the last expression
53
+ * is returned, module.exports is NOT consulted).
54
+ * Example: `"input.tail.includes('✓') ? 'idle' : 'generating'"`
55
+ *
56
+ * @param context Plain key/value pairs injected as globals inside the
57
+ * sandbox. All values must be JSON-serializable.
58
+ * Typically: `{ input: <CliStatusInput>, sdk: <sdk facade> }`
59
+ *
60
+ * @param options Optional per-call overrides for resource limits.
61
+ * Merged with the options supplied to createScriptRunner().
62
+ *
63
+ * @returns The value produced by the script (JSON-deserialized from
64
+ * the sandbox). May be `unknown` — callers must validate.
65
+ *
66
+ * @throws Re-throws if the script contains a syntax error.
67
+ * Returns `undefined` (via rejection) on timeout or OOM.
68
+ */
69
+ run(script: string, context: Record<string, unknown>, options?: Partial<SandboxOptions>): Promise<unknown>;
70
+
71
+ /**
72
+ * Release all resources held by this runner (Isolate, Contexts, etc.).
73
+ * After dispose(), calling run() has undefined behavior.
74
+ */
75
+ dispose(): void;
76
+ }
77
+
78
+ /**
79
+ * No-op stub used when isolated-vm is not available (graceful degradation).
80
+ *
81
+ * Falls back to direct `eval()` inside a try/catch — there are no CPU or
82
+ * memory limits. A warning is logged on the first run() call to make the
83
+ * fallback visible in daemon logs.
84
+ *
85
+ * This implementation is suitable for:
86
+ * - Development environments where isolated-vm is not installed.
87
+ * - Unit tests (no native dependency required).
88
+ * - Graceful fallback for existing extended-legacy providers while Phase 5
89
+ * is being rolled out.
90
+ *
91
+ * NOT suitable for production use with untrusted script sources.
92
+ */
93
+ export class DirectEvalRunner implements SandboxedScriptRunner {
94
+ private warned = false;
95
+ private readonly defaultOptions: SandboxOptions;
96
+
97
+ constructor(options?: Partial<SandboxOptions>) {
98
+ this.defaultOptions = { ...DEFAULT_SANDBOX_OPTIONS, ...options };
99
+ }
100
+
101
+ async run(
102
+ script: string,
103
+ context: Record<string, unknown>,
104
+ _options?: Partial<SandboxOptions>,
105
+ ): Promise<unknown> {
106
+ if (!this.warned) {
107
+ LOG.warn('Sandbox', '[DirectEvalRunner] isolated-vm not available — running script without sandboxing. Install isolated-vm for production use.');
108
+ this.warned = true;
109
+ }
110
+
111
+ // Build a function body that exposes context keys as locals, then
112
+ // evaluates the script and returns its result.
113
+ const contextKeys = Object.keys(context);
114
+ const contextValues = contextKeys.map((k) => context[k]);
115
+
116
+ // eslint-disable-next-line no-new-func
117
+ const fn = new Function(...contextKeys, `"use strict";\nreturn (${script});`);
118
+ return fn(...contextValues);
119
+ }
120
+
121
+ dispose(): void {
122
+ // Nothing to release — DirectEvalRunner holds no native resources.
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Factory: returns IsolatedVmRunner if isolated-vm is installed, else DirectEvalRunner.
128
+ *
129
+ * Logs a warning when falling back to DirectEvalRunner so the operator is aware
130
+ * that sandboxing is inactive.
131
+ *
132
+ * @param options Resource limit defaults applied to every run() call unless
133
+ * overridden at the call site. Merged with DEFAULT_SANDBOX_OPTIONS.
134
+ */
135
+ export function createScriptRunner(options?: Partial<SandboxOptions>): SandboxedScriptRunner {
136
+ try {
137
+ // Dynamic require so that the import does not break the module when
138
+ // isolated-vm is absent from node_modules.
139
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
140
+ const isolatedVm = require('isolated-vm');
141
+ if (isolatedVm && typeof isolatedVm.Isolate === 'function') {
142
+ // Phase 5: IsolatedVmRunner — not yet implemented.
143
+ // TODO(phase5): return new IsolatedVmRunner(options);
144
+ LOG.warn('Sandbox', '[createScriptRunner] isolated-vm detected but IsolatedVmRunner is not yet implemented — falling back to DirectEvalRunner.');
145
+ }
146
+ } catch {
147
+ // isolated-vm is not installed — this is the expected state for now.
148
+ }
149
+ return new DirectEvalRunner(options);
150
+ }