synabun 2.0.10 → 2.1.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 (517) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +25 -2
  3. package/THIRD-PARTY-LICENSES.md +22 -0
  4. package/bin/synabun.js +71 -24
  5. package/docs/whatsapp.md +2 -2
  6. package/hooks/claude-code/pre-websearch.mjs +12 -1
  7. package/hooks/claude-code/shared.mjs +5 -0
  8. package/hooks/claude-code/stop.mjs +1 -1
  9. package/lib/claude-hooks.js +172 -18
  10. package/lib/client-config-repair.js +17 -8
  11. package/lib/desktop-app.js +7 -1
  12. package/lib/external-tools.js +1 -1
  13. package/lib/first-launch-protection.js +57 -12
  14. package/lib/mcp-dist.js +176 -0
  15. package/lib/release-npm-ci.mjs +138 -0
  16. package/lib/release-publish.mjs +91 -0
  17. package/lib/release-scan.mjs +50 -0
  18. package/lib/server-crash-record.js +123 -0
  19. package/lib/server-exit.js +318 -0
  20. package/lib/server-supervisor.js +508 -0
  21. package/lib/start-launcher.js +37 -4
  22. package/lib/tools-cli.js +45 -0
  23. package/lib/update-snapshot.js +228 -0
  24. package/lib/virustotal.mjs +192 -0
  25. package/lib/windows-crash-event.js +269 -0
  26. package/mcp-server/dist/http.d.ts +20 -1
  27. package/mcp-server/dist/http.d.ts.map +1 -1
  28. package/mcp-server/dist/http.js +59 -12
  29. package/mcp-server/dist/http.js.map +1 -1
  30. package/mcp-server/dist/index.d.ts +33 -27
  31. package/mcp-server/dist/index.d.ts.map +1 -1
  32. package/mcp-server/dist/index.js +128 -143
  33. package/mcp-server/dist/index.js.map +1 -1
  34. package/mcp-server/dist/preload.js +1 -1
  35. package/mcp-server/dist/preload.js.map +1 -1
  36. package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
  37. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  38. package/mcp-server/dist/services/codex-config-heal.js +81 -28
  39. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  40. package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
  41. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
  42. package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
  43. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
  44. package/mcp-server/dist/services/neural-interface.d.ts +16 -10
  45. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  46. package/mcp-server/dist/services/neural-interface.js +67 -11
  47. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  48. package/mcp-server/dist/services/server-instructions.d.ts +7 -2
  49. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  50. package/mcp-server/dist/services/server-instructions.js +20 -62
  51. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  52. package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
  53. package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
  54. package/mcp-server/dist/services/tool-catalog.js +339 -0
  55. package/mcp-server/dist/services/tool-catalog.js.map +1 -0
  56. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
  57. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
  58. package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
  59. package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
  60. package/mcp-server/dist/services/tool-list.d.ts +108 -0
  61. package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
  62. package/mcp-server/dist/services/tool-list.js +334 -0
  63. package/mcp-server/dist/services/tool-list.js.map +1 -0
  64. package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
  65. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
  66. package/mcp-server/dist/services/tool-runtime.js +400 -0
  67. package/mcp-server/dist/services/tool-runtime.js.map +1 -0
  68. package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
  69. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
  70. package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
  71. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
  72. package/mcp-server/dist/tools/agents.d.ts +10 -10
  73. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  74. package/mcp-server/dist/tools/agents.js +22 -13
  75. package/mcp-server/dist/tools/agents.js.map +1 -1
  76. package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
  77. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  78. package/mcp-server/dist/tools/image-tools.js +1 -1
  79. package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
  80. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
  81. package/mcp-server/dist/tools/skill-tools.js +639 -0
  82. package/mcp-server/dist/tools/skill-tools.js.map +1 -0
  83. package/mcp-server/dist/tools/skill.d.ts +7 -0
  84. package/mcp-server/dist/tools/skill.d.ts.map +1 -0
  85. package/mcp-server/dist/tools/skill.js +11 -0
  86. package/mcp-server/dist/tools/skill.js.map +1 -0
  87. package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
  88. package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
  89. package/mcp-server/dist/tools/tool-groups.js +147 -0
  90. package/mcp-server/dist/tools/tool-groups.js.map +1 -0
  91. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
  92. package/mcp-server/dist/tools/whiteboard-format.js +7 -4
  93. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
  94. package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
  95. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  96. package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
  97. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  98. package/mcp-server/package-lock.json +2 -2
  99. package/mcp-server/package.json +1 -1
  100. package/mcp-server/src/http.ts +75 -12
  101. package/mcp-server/src/index.ts +147 -154
  102. package/mcp-server/src/preload.ts +12 -12
  103. package/mcp-server/src/services/codex-config-heal.ts +80 -29
  104. package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
  105. package/mcp-server/src/services/neural-interface.ts +79 -20
  106. package/mcp-server/src/services/server-instructions.ts +27 -63
  107. package/mcp-server/src/services/tool-catalog.ts +403 -0
  108. package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
  109. package/mcp-server/src/services/tool-list.ts +410 -0
  110. package/mcp-server/src/services/tool-runtime.ts +457 -0
  111. package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
  112. package/mcp-server/src/tools/agents.ts +22 -14
  113. package/mcp-server/src/tools/image-tools.ts +1 -1
  114. package/mcp-server/src/tools/skill-tools.ts +589 -0
  115. package/mcp-server/src/tools/skill.ts +13 -0
  116. package/mcp-server/src/tools/tool-groups.ts +164 -0
  117. package/mcp-server/src/tools/whiteboard-format.ts +9 -3
  118. package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
  119. package/neural-interface/i18n/en.json +3463 -200
  120. package/neural-interface/i18n/pt-BR.json +3411 -151
  121. package/neural-interface/lib/assistant-api.js +127 -28
  122. package/neural-interface/lib/assistant-brains/claude.js +4 -3
  123. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
  124. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
  125. package/neural-interface/lib/assistant-brains/codex.js +9 -5
  126. package/neural-interface/lib/assistant-brains/opencode.js +120 -54
  127. package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +28 -13
  128. package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +59 -19
  129. package/neural-interface/lib/assistant-catalog.js +6 -7
  130. package/neural-interface/lib/assistant-dispatch.js +871 -156
  131. package/neural-interface/lib/assistant-envelope.js +3 -1
  132. package/neural-interface/lib/assistant-persona.js +80 -11
  133. package/neural-interface/lib/assistant-plan-permissions.js +10 -9
  134. package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
  135. package/neural-interface/lib/assistant-route-gate.js +6 -4
  136. package/neural-interface/lib/assistant-router.js +13 -8
  137. package/neural-interface/lib/assistant-runtime.js +56 -18
  138. package/neural-interface/lib/assistant-task-prompt.js +78 -14
  139. package/neural-interface/lib/assistant-tools.js +51 -0
  140. package/neural-interface/lib/backup-service.js +620 -3
  141. package/neural-interface/lib/backup-zip-reader.js +10 -5
  142. package/neural-interface/lib/backup-zip-writer.js +257 -0
  143. package/neural-interface/lib/browser-execution.js +10 -2
  144. package/neural-interface/lib/browser-guard-cdp.js +83 -0
  145. package/neural-interface/lib/browser-network-guard.js +284 -0
  146. package/neural-interface/lib/browser-policy-roots.js +64 -0
  147. package/neural-interface/lib/browser-tool-policy.js +186 -31
  148. package/neural-interface/lib/claude-agent-bridge.js +4 -2
  149. package/neural-interface/lib/claude-hooks-status.js +103 -0
  150. package/neural-interface/lib/claude-temporary.js +5 -11
  151. package/neural-interface/lib/cli-install.js +18 -138
  152. package/neural-interface/lib/cli-resolver.js +4 -1
  153. package/neural-interface/lib/cli-update-planner.js +32 -31
  154. package/neural-interface/lib/cloudflared/tunnel.js +42 -2
  155. package/neural-interface/lib/codex-mcp-approvals.js +11 -0
  156. package/neural-interface/lib/codex-worker-app-server.js +172 -0
  157. package/neural-interface/lib/confined-fs.js +30 -3
  158. package/neural-interface/lib/crash-notice-api.js +98 -0
  159. package/neural-interface/lib/editor-completion-opencode.js +68 -68
  160. package/neural-interface/lib/exec-loop-command.js +4 -8
  161. package/neural-interface/lib/http-guards.js +8 -2
  162. package/neural-interface/lib/install-details.js +4 -1
  163. package/neural-interface/lib/mcp-agent-servers.js +67 -0
  164. package/neural-interface/lib/mcp-client-config.js +0 -29
  165. package/neural-interface/lib/mcp-external-servers.js +323 -0
  166. package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
  167. package/neural-interface/lib/morelogin.js +30 -0
  168. package/neural-interface/lib/native-loop-providers.js +547 -47
  169. package/neural-interface/lib/native-loop-runtime.js +268 -41
  170. package/neural-interface/lib/opencode-config.js +557 -0
  171. package/neural-interface/lib/opencode-db-schema.js +17 -34
  172. package/neural-interface/lib/opencode-history.js +48 -218
  173. package/neural-interface/lib/opencode-mcp-ready.js +108 -0
  174. package/neural-interface/lib/opencode-native-boundary.js +44 -0
  175. package/neural-interface/lib/opencode-protocol.js +249 -112
  176. package/neural-interface/lib/opencode-runtime-config.js +32 -22
  177. package/neural-interface/lib/opencode-serve-records.js +39 -21
  178. package/neural-interface/lib/opencode-v2-client.js +48 -153
  179. package/neural-interface/lib/opencode-v2-ws-requests.js +36 -210
  180. package/neural-interface/lib/opencode-worker-data.js +27 -0
  181. package/neural-interface/lib/opencode2-client.js +73 -37
  182. package/neural-interface/lib/opencode2-proxy.js +23 -63
  183. package/neural-interface/lib/opencode2-translate.js +51 -2
  184. package/neural-interface/lib/owner-authority.js +249 -0
  185. package/neural-interface/lib/page-session.js +275 -0
  186. package/neural-interface/lib/peer-identity.js +763 -0
  187. package/neural-interface/lib/request-gate.js +17 -2
  188. package/neural-interface/lib/rulesets/api.js +1 -1
  189. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  190. package/neural-interface/lib/rulesets/installer.js +92 -27
  191. package/neural-interface/lib/rulesets/json-edit.js +1 -1
  192. package/neural-interface/lib/session-cache-builder.js +17 -30
  193. package/neural-interface/lib/session-title-generator.js +58 -36
  194. package/neural-interface/lib/skills/api-v2.js +495 -0
  195. package/neural-interface/lib/skills/bundle.js +418 -0
  196. package/neural-interface/lib/skills/catalog.js +294 -0
  197. package/neural-interface/lib/skills/creation-api.js +191 -0
  198. package/neural-interface/lib/skills/creation-brief.js +247 -0
  199. package/neural-interface/lib/skills/creation-sources.js +267 -0
  200. package/neural-interface/lib/skills/creation.js +843 -0
  201. package/neural-interface/lib/skills/deploy.js +564 -0
  202. package/neural-interface/lib/skills/digest.js +119 -0
  203. package/neural-interface/lib/skills/discover.js +111 -0
  204. package/neural-interface/lib/skills/draft-confinement.js +214 -0
  205. package/neural-interface/lib/skills/eval-confinement.js +397 -0
  206. package/neural-interface/lib/skills/eval-sandbox.js +280 -0
  207. package/neural-interface/lib/skills/eval-trace.js +119 -0
  208. package/neural-interface/lib/skills/evals-api.js +212 -0
  209. package/neural-interface/lib/skills/evals.js +1573 -0
  210. package/neural-interface/lib/skills/export.js +200 -0
  211. package/neural-interface/lib/skills/frontmatter.js +64 -0
  212. package/neural-interface/lib/skills/health.js +75 -0
  213. package/neural-interface/lib/skills/json-edit.js +165 -0
  214. package/neural-interface/lib/skills/known-bridge-digests.json +387 -0
  215. package/neural-interface/lib/skills/known-bridges.js +96 -0
  216. package/neural-interface/lib/skills/library.js +823 -0
  217. package/neural-interface/lib/skills/local-store.js +21 -0
  218. package/neural-interface/lib/skills/migration.js +433 -0
  219. package/neural-interface/lib/skills/operations.js +45 -0
  220. package/neural-interface/lib/skills/ownership.js +427 -0
  221. package/neural-interface/lib/skills/packages.js +88 -0
  222. package/neural-interface/lib/skills/reviews.js +212 -0
  223. package/neural-interface/lib/skills/revision.js +80 -0
  224. package/neural-interface/lib/skills/roots.js +262 -0
  225. package/neural-interface/lib/skills/runtime-checks.js +355 -0
  226. package/neural-interface/lib/skills/scan-assets.js +252 -0
  227. package/neural-interface/lib/skills/scan-context.js +26 -0
  228. package/neural-interface/lib/skills/scan-shell.js +324 -0
  229. package/neural-interface/lib/skills/scan.js +364 -0
  230. package/neural-interface/lib/skills/scanners.js +54 -0
  231. package/neural-interface/lib/skills/service.js +522 -0
  232. package/neural-interface/lib/skills/telemetry.js +113 -0
  233. package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
  234. package/neural-interface/lib/tool-lists.js +124 -0
  235. package/neural-interface/lib/whatsapp/bridge.js +6 -3
  236. package/neural-interface/lib/worker-confinement.js +1192 -0
  237. package/neural-interface/lib/worker-guard-setting.js +56 -0
  238. package/neural-interface/lib/worker-mcp-channel.js +175 -0
  239. package/neural-interface/lib/worker-relay-ports.js +227 -0
  240. package/neural-interface/lib/worker-resume.js +229 -0
  241. package/neural-interface/lib/worker-sandbox.js +299 -0
  242. package/neural-interface/package-lock.json +18 -12
  243. package/neural-interface/package.json +2 -2
  244. package/neural-interface/public/index.html +3 -1
  245. package/neural-interface/public/index2d.html +19 -11
  246. package/neural-interface/public/onboarding.html +2 -2
  247. package/neural-interface/public/shared/account-menu.js +8 -5
  248. package/neural-interface/public/shared/agent-runtime-options.js +35 -19
  249. package/neural-interface/public/shared/api.js +1527 -1336
  250. package/neural-interface/public/shared/app-frame.js +192 -0
  251. package/neural-interface/public/shared/assistant/asst-agents-dock.js +33 -2
  252. package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
  253. package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
  254. package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
  255. package/neural-interface/public/shared/assistant/asst-brain-picker.js +49 -35
  256. package/neural-interface/public/shared/assistant/asst-control.js +157 -26
  257. package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
  258. package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
  259. package/neural-interface/public/shared/assistant/asst-limit-dock.js +40 -0
  260. package/neural-interface/public/shared/assistant/asst-limit-wait.js +294 -0
  261. package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
  262. package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
  263. package/neural-interface/public/shared/assistant/asst-menu.js +7 -6
  264. package/neural-interface/public/shared/assistant/asst-panel.js +106 -11
  265. package/neural-interface/public/shared/assistant/asst-rack-styles.js +66 -49
  266. package/neural-interface/public/shared/assistant/asst-render.js +401 -77
  267. package/neural-interface/public/shared/assistant/asst-route.js +83 -12
  268. package/neural-interface/public/shared/assistant/asst-scenes.js +2 -0
  269. package/neural-interface/public/shared/assistant/asst-sidepanel.js +67 -4
  270. package/neural-interface/public/shared/assistant/asst-slash.js +9 -7
  271. package/neural-interface/public/shared/assistant/asst-state.js +11 -15
  272. package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
  273. package/neural-interface/public/shared/assistant/asst-styles.js +59 -21
  274. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +156 -16
  275. package/neural-interface/public/shared/automation-context.js +5 -2
  276. package/neural-interface/public/shared/cc-dropdown.js +89 -19
  277. package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
  278. package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
  279. package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
  280. package/neural-interface/public/shared/cdx/cdx-output.js +8 -7
  281. package/neural-interface/public/shared/cdx/cdx-panel.js +45 -149
  282. package/neural-interface/public/shared/cdx/cdx-protocol.js +28 -1
  283. package/neural-interface/public/shared/cdx/cdx-render.js +47 -29
  284. package/neural-interface/public/shared/cdx/cdx-requests.js +3 -3
  285. package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
  286. package/neural-interface/public/shared/cdx/cdx-styles.js +11 -3
  287. package/neural-interface/public/shared/cdx/cdx-tabs.js +52 -39
  288. package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
  289. package/neural-interface/public/shared/cli-install-view.js +4 -8
  290. package/neural-interface/public/shared/cli-status.js +2 -2
  291. package/neural-interface/public/shared/constants.js +2 -2
  292. package/neural-interface/public/shared/context-menu-areas.js +34 -0
  293. package/neural-interface/public/shared/context-menu-logic.js +243 -0
  294. package/neural-interface/public/shared/context-menu-model.js +366 -0
  295. package/neural-interface/public/shared/context-menu.js +372 -0
  296. package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
  297. package/neural-interface/public/shared/crash-notice-view.js +168 -0
  298. package/neural-interface/public/shared/desktop-update-view.js +20 -0
  299. package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
  300. package/neural-interface/public/shared/html-shell.js +9 -11
  301. package/neural-interface/public/shared/i18n.js +2 -0
  302. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
  303. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
  304. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
  305. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +2 -0
  306. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
  307. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
  308. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
  309. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
  310. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
  311. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
  312. package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
  313. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +47 -252
  314. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
  315. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +95 -202
  316. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +6 -21
  317. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +4 -4
  318. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
  319. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +2 -91
  320. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
  321. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
  322. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
  323. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
  324. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +0 -21
  325. package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
  326. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +14 -47
  327. package/neural-interface/public/shared/page-session.js +96 -0
  328. package/neural-interface/public/shared/registry.js +5 -3
  329. package/neural-interface/public/shared/retired-2d.js +3 -0
  330. package/neural-interface/public/shared/schedules-view.js +195 -61
  331. package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
  332. package/neural-interface/public/shared/settings/settings-data.js +3 -1
  333. package/neural-interface/public/shared/settings/settings-ia.js +6 -6
  334. package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
  335. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
  336. package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
  337. package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
  338. package/neural-interface/public/shared/skills/ss-args.js +96 -0
  339. package/neural-interface/public/shared/skills/ss-creation.js +219 -0
  340. package/neural-interface/public/shared/skills/ss-dialog.js +130 -0
  341. package/neural-interface/public/shared/skills/ss-doc.js +173 -0
  342. package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
  343. package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
  344. package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
  345. package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
  346. package/neural-interface/public/shared/skills/ss-evals.js +417 -0
  347. package/neural-interface/public/shared/skills/ss-form.js +159 -0
  348. package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
  349. package/neural-interface/public/shared/skills/ss-ids.js +32 -0
  350. package/neural-interface/public/shared/skills/ss-lint.js +141 -0
  351. package/neural-interface/public/shared/skills/ss-presets.js +117 -0
  352. package/neural-interface/public/shared/skills/ss-profiles.js +475 -0
  353. package/neural-interface/public/shared/skills/ss-resources.js +165 -0
  354. package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
  355. package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
  356. package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
  357. package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
  358. package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
  359. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
  360. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +236 -0
  361. package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
  362. package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
  363. package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
  364. package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
  365. package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
  366. package/neural-interface/public/shared/skills/studio/sx-dom.js +229 -0
  367. package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
  368. package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
  369. package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
  370. package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
  371. package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
  372. package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
  373. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +279 -0
  374. package/neural-interface/public/shared/skills/studio/sx-install.js +296 -0
  375. package/neural-interface/public/shared/skills/studio/sx-library-view.js +639 -0
  376. package/neural-interface/public/shared/skills/studio/sx-library.js +475 -0
  377. package/neural-interface/public/shared/skills/studio/sx-main.js +681 -0
  378. package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
  379. package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
  380. package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
  381. package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
  382. package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
  383. package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
  384. package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
  385. package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
  386. package/neural-interface/public/shared/skills/studio/sx-subject.js +248 -0
  387. package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
  388. package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
  389. package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
  390. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +425 -0
  391. package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
  392. package/neural-interface/public/shared/styles.css +1525 -1779
  393. package/neural-interface/public/shared/synabun-mascot.js +1115 -9
  394. package/neural-interface/public/shared/tool-list-picker.js +55 -0
  395. package/neural-interface/public/shared/tool-list-storage.js +24 -0
  396. package/neural-interface/public/shared/tool-list-view.js +112 -0
  397. package/neural-interface/public/shared/tour/tour-actor.js +268 -0
  398. package/neural-interface/public/shared/tour/tour-beats.js +336 -0
  399. package/neural-interface/public/shared/tour/tour-engine.js +884 -0
  400. package/neural-interface/public/shared/tour/tour-ink.js +390 -0
  401. package/neural-interface/public/shared/tour/tour-stage.js +440 -0
  402. package/neural-interface/public/shared/tour/tour-state.js +40 -0
  403. package/neural-interface/public/shared/tour/tour-tips.js +99 -0
  404. package/neural-interface/public/shared/tour/tour.css +144 -0
  405. package/neural-interface/public/shared/ui-automation-studio.js +91 -128
  406. package/neural-interface/public/shared/ui-claude-panel.js +77 -5
  407. package/neural-interface/public/shared/ui-command-runner.js +6 -3
  408. package/neural-interface/public/shared/ui-context-menu.js +295 -0
  409. package/neural-interface/public/shared/ui-cost-widget.js +57 -59
  410. package/neural-interface/public/shared/ui-crash-notice.js +187 -0
  411. package/neural-interface/public/shared/ui-detail.js +15 -3
  412. package/neural-interface/public/shared/ui-image-gallery.js +50 -47
  413. package/neural-interface/public/shared/ui-keybinds.js +26 -1
  414. package/neural-interface/public/shared/ui-menubar.js +8 -18
  415. package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
  416. package/neural-interface/public/shared/ui-native-loop-router.js +7 -1
  417. package/neural-interface/public/shared/ui-navbar.js +4 -24
  418. package/neural-interface/public/shared/ui-panels.js +96 -65
  419. package/neural-interface/public/shared/ui-schedules-studio.js +507 -423
  420. package/neural-interface/public/shared/ui-sessions.js +5 -3
  421. package/neural-interface/public/shared/ui-settings.js +594 -462
  422. package/neural-interface/public/shared/ui-sidepanel-sessions.js +1 -1
  423. package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
  424. package/neural-interface/public/shared/ui-sidepanel-windows.js +16 -11
  425. package/neural-interface/public/shared/ui-skills.js +33 -2830
  426. package/neural-interface/public/shared/ui-styleguide.js +5 -2
  427. package/neural-interface/public/shared/ui-sync.js +15 -5
  428. package/neural-interface/public/shared/ui-terminal.js +124 -180
  429. package/neural-interface/public/shared/ui-tool-updates.js +3 -1
  430. package/neural-interface/public/shared/ui-tooltip.js +10 -5
  431. package/neural-interface/public/shared/ui-trash.js +6 -4
  432. package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
  433. package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
  434. package/neural-interface/public/shared/ui-tutorial-steps.js +3 -65
  435. package/neural-interface/public/shared/ui-tutorial.css +47 -100
  436. package/neural-interface/public/shared/ui-tutorial.js +285 -509
  437. package/neural-interface/public/shared/ui-update.js +38 -2
  438. package/neural-interface/public/shared/ui-whiteboard.js +311 -58
  439. package/neural-interface/public/shared/ui-worker-end.js +4 -0
  440. package/neural-interface/public/shared/ui-workspaces.js +3 -2
  441. package/neural-interface/public/shared/view-variant.js +66 -0
  442. package/neural-interface/public/shared/whatsapp/wa-view.js +1 -1
  443. package/neural-interface/public/shared/whiteboard-list.js +172 -0
  444. package/neural-interface/public/shared/worker-end.js +10 -3
  445. package/neural-interface/public/shared/worker-restart.js +12 -0
  446. package/neural-interface/public/sidepanel-session.html +1 -1
  447. package/neural-interface/public/variant/2d/background.js +2 -1
  448. package/neural-interface/public/variant/2d/context-menu.js +8 -2
  449. package/neural-interface/public/variant/2d/graph.js +42 -20
  450. package/neural-interface/public/variant/2d/hulls.js +14 -9
  451. package/neural-interface/public/variant/2d/lasso.js +11 -7
  452. package/neural-interface/public/variant/2d/main.js +32 -1
  453. package/neural-interface/public/variant/2d/minimap.js +11 -13
  454. package/neural-interface/public/variant/3d/main.js +69 -6
  455. package/neural-interface/public/variant/3d/map-controls.js +24 -9
  456. package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
  457. package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
  458. package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
  459. package/neural-interface/public/vendor/caveat/caveat.css +17 -0
  460. package/neural-interface/public/vendor/caveat/manifest.json +12 -0
  461. package/neural-interface/public/vendor/yaml/LICENSE +13 -0
  462. package/neural-interface/public/vendor/yaml/manifest.json +4 -0
  463. package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
  464. package/neural-interface/scripts/build-vendor.mjs +32 -0
  465. package/neural-interface/server.js +3934 -2454
  466. package/neural-interface/templates/rulesets/core.md +1 -1
  467. package/neural-interface/templates/rulesets/legacy-hashes.json +18 -0
  468. package/neural-interface/templates/rulesets/manifest.json +8 -8
  469. package/npm-shrinkwrap.json +18 -12
  470. package/package.json +16 -3
  471. package/postinstall.js +9 -4
  472. package/preuninstall.js +1 -1
  473. package/setup.js +97 -322
  474. package/skills/skill-forge/SKILL.md +177 -0
  475. package/skills/skill-forge/evals/evals.json +54 -0
  476. package/skills/skill-forge/references/package.md +105 -0
  477. package/skills/skill-forge/references/review-and-recovery.md +63 -0
  478. package/skills/skill-forge/references/runtime-profiles.md +20 -0
  479. package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
  480. package/skills/synabun/SKILL.md +10 -7
  481. package/skills/synabun/modules/audit.md +1 -5
  482. package/skills/synabun/modules/changelog.md +1 -5
  483. package/skills/synabun/modules/idea.md +1 -1
  484. package/skills/synabun/modules/memorize.md +6 -9
  485. package/skills/synabun/modules/schedule.md +4 -4
  486. package/mcp-server/dist/services/profiles.d.ts +0 -120
  487. package/mcp-server/dist/services/profiles.d.ts.map +0 -1
  488. package/mcp-server/dist/services/profiles.js +0 -387
  489. package/mcp-server/dist/services/profiles.js.map +0 -1
  490. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
  491. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
  492. package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
  493. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
  494. package/mcp-server/dist/tools/leonardo.d.ts +0 -13
  495. package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
  496. package/mcp-server/dist/tools/leonardo.js +0 -21
  497. package/mcp-server/dist/tools/leonardo.js.map +0 -1
  498. package/mcp-server/dist/tools/profile.d.ts +0 -17
  499. package/mcp-server/dist/tools/profile.d.ts.map +0 -1
  500. package/mcp-server/dist/tools/profile.js +0 -114
  501. package/mcp-server/dist/tools/profile.js.map +0 -1
  502. package/mcp-server/src/services/profiles.ts +0 -428
  503. package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
  504. package/mcp-server/src/tools/leonardo.ts +0 -30
  505. package/mcp-server/src/tools/profile.ts +0 -122
  506. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
  507. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
  508. package/neural-interface/lib/mcp-profile-servers.js +0 -120
  509. package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
  510. package/skills/leonardo/SKILL.md +0 -168
  511. package/skills/leonardo/icon.svg +0 -1
  512. package/skills/leonardo/modules/browser-guide.md +0 -378
  513. package/skills/leonardo/modules/image-prompter.md +0 -283
  514. package/skills/leonardo/modules/model-advisor.md +0 -100
  515. package/skills/leonardo/modules/prompt-library.md +0 -110
  516. package/skills/leonardo/modules/style-guide.md +0 -186
  517. package/skills/leonardo/modules/video-prompter.md +0 -317
@@ -0,0 +1,229 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun — dispatched workers across a server restart
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // A worker's provider process is a child of the Neural Interface, so every
6
+ // restart of the server (`node --watch` on an edit of its code, an in-app
7
+ // restart, an update, Ctrl+C, a crash) used to end the run as interrupted. The
8
+ // dispatcher (assistant-dispatch.js) now suspends such a run at the shutdown and
9
+ // resumes its provider session on the next start, as the same run. This module is
10
+ // what that decision and the resume need besides the dispatcher's own state:
11
+ //
12
+ // resumeRefusal why a run may not be resumed, or null
13
+ // readCostState what the resumed CLI restores from its transcript (its cost
14
+ // counter and per-model usage), so its first turn is charged
15
+ // only for itself
16
+ // endOrphanProcess a CLI that outlived its server (a crash: no shutdown ended
17
+ // it), ended only once its command line shows it is that run's
18
+ //
19
+ // Measured with the SDK and the Claude Code SynaBun runs (0.3.288 / 2.1.310):
20
+ // a session whose CLI was killed in the middle of a tool call resumes, the CLI
21
+ // records that call as interrupted with its outcome unknown, and the next model
22
+ // call goes through; a CLI that gets SIGTERM ends the tool's process tree,
23
+ // records the call's result and its cost state, and exits within a second.
24
+
25
+ import { execFileSync } from 'node:child_process';
26
+ import { closeSync, existsSync, fstatSync, openSync, readSync, readdirSync, realpathSync, statSync } from 'node:fs';
27
+ import { homedir } from 'node:os';
28
+ import { isAbsolute, join, relative, resolve } from 'node:path';
29
+
30
+ /** A run is resumed when the server is back within this long of its last update, and at most this many times. */
31
+ export const RESUME_WINDOW_MS = 15 * 60_000;
32
+ export const MAX_RESUMES = 3;
33
+
34
+ /** Rebuild OS isolation without dropping an earlier denial or relaxing strict local networking.
35
+ * The channel is a fresh listener/token each boot; only that listener replaces the old allow-port.
36
+ */
37
+ export function resumeSandboxPlan(current, previous, channelPort) {
38
+ if (!current || !previous) throw new Error('the original worker confinement is unavailable');
39
+ const paths = (a) => [...new Set(a || [])].sort();
40
+ if (JSON.stringify(paths(current.allowPaths)) !== JSON.stringify(paths(previous.allowPaths))) {
41
+ throw new Error('the worker confinement folders changed');
42
+ }
43
+ const ranges = [current.denyPortRange, previous.denyPortRange].filter(Boolean);
44
+ return {
45
+ ...current,
46
+ denyPorts: [...new Set([...(current.denyPorts || []), ...(previous.denyPorts || [])])],
47
+ denyLocalPorts: [...new Set([...(current.denyLocalPorts || []), ...(previous.denyLocalPorts || [])])],
48
+ denyPaths: [...new Set([...(current.denyPaths || []), ...(previous.denyPaths || [])])],
49
+ denyPortRange: ranges.length ? { from: Math.min(...ranges.map(r => r.from)), to: Math.max(...ranges.map(r => r.to)) } : null,
50
+ strictLocal: previous.strictLocal || current.strictLocal ? { allowPorts: [channelPort].filter(Boolean) } : null,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Why `entry` (a dispatcher registry entry) cannot be resumed, or null. `atShutdown`: only what is known while the
56
+ * server goes down (the rest is checked at the next start: the time it was away, its folder, its account).
57
+ *
58
+ * @param {object} entry
59
+ * @param {object} o
60
+ * @param {number} [o.now]
61
+ * @param {number} [o.workLeftMs] the run's work time left (max_minutes)
62
+ * @param {boolean} [o.atShutdown]
63
+ * @param {(path: string) => boolean} [o.isDirectory]
64
+ * @param {(accountId: string) => string} [o.accountUnusable] '' when the Claude account can be used
65
+ */
66
+ export function resumeRefusal(entry, { now = Date.now(), workLeftMs = Infinity, atShutdown = false, isDirectory = () => true, accountUnusable = () => '' } = {}) {
67
+ if (!entry) return 'the run is not known';
68
+ if (!['claude-code', 'codex', 'opencode'].includes(entry.provider)) return 'this provider cannot continue after a restart';
69
+ if (entry.confinement?.kind === 'skill-eval') return 'a skill evaluation run is a measurement and starts over instead';
70
+ if (entry.confinement) return 'a skill drafting run is not resumed';
71
+ if (entry.removedAt) return 'the run was removed';
72
+ if (!(entry.provider === 'codex' ? entry.providerThreadId : entry.providerSessionId)) return `its ${entry.provider === 'codex' ? 'Codex thread' : entry.provider === 'opencode' ? 'OpenCode session' : 'Claude session'} had not started yet`;
73
+ if (entry.provider === 'opencode' && !entry.resumeInfo?.persistentOpenCode) return 'its OpenCode session has no durable run data';
74
+ const resumes = Number(entry.resumes) || 0;
75
+ if (resumes >= MAX_RESUMES) return `it was resumed ${resumes} times already`;
76
+ if (!(workLeftMs > 0)) return entry.maxMinutes ? `its ${entry.maxMinutes} minutes of work time are used up` : 'its work time is used up';
77
+ if (atShutdown) return null;
78
+ const since = Date.parse(entry.suspendedAt || '') || Date.parse(entry.updatedAt || '') || 0;
79
+ if (since && now - since > RESUME_WINDOW_MS) return `the server was back ${Math.round((now - since) / 60_000)} minutes later (more than ${Math.round(RESUME_WINDOW_MS / 60_000)})`;
80
+ if (!entry.cwd || !isDirectory(entry.cwd)) return `its working directory is gone (${entry.cwd || 'none'})`;
81
+ let unusable = '';
82
+ try { unusable = entry.provider === 'opencode' ? '' : String(accountUnusable((entry.provider === 'codex' ? entry.codexAccountId : entry.claudeAccountId) || 'default', entry.provider, entry) || ''); } catch (error) { unusable = error?.message || 'it could not be read'; }
83
+ if (unusable) return `its ${entry.provider === 'codex' ? 'Codex' : 'Claude'} account (${entry.codexAccountId || entry.claudeAccountId || 'default'}) cannot be used: ${unusable}`;
84
+ return null;
85
+ }
86
+
87
+ /** Whether `path` is `root` or inside it (both resolved; a missing path is compared as written). */
88
+ export function isInside(path, root) {
89
+ if (!path || !root) return false;
90
+ const real = (value) => { try { return realpathSync(value); } catch { return resolve(String(value)); } };
91
+ const rel = relative(real(root), real(path));
92
+ return rel === '' || (!!rel && !rel.startsWith('..') && !isAbsolute(rel));
93
+ }
94
+
95
+ /** "14:05": a time of day on this computer's clock, or '' for no time. */
96
+ export function clockTime(value) {
97
+ const ms = typeof value === 'number' ? value : Date.parse(value || '');
98
+ if (!Number.isFinite(ms) || ms <= 0) return '';
99
+ const date = new Date(ms);
100
+ return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
101
+ }
102
+
103
+ /**
104
+ * The transcript of a Claude session: <config dir>/projects/<project folder>/<sessionId>.jsonl, the config dir being
105
+ * the run's account's (CLAUDE_CONFIG_DIR) or ~/.claude. Every project folder is looked in (the CLI names it after
106
+ * the real path of the working directory). null when there is none.
107
+ */
108
+ export function findClaudeTranscript({ sessionId, configDir = null, home = homedir() } = {}) {
109
+ const id = String(sessionId || '');
110
+ if (!/^[0-9a-f-]{8,64}$/i.test(id)) return null;
111
+ const projects = join(configDir || join(home, '.claude'), 'projects');
112
+ let folders = [];
113
+ try { folders = readdirSync(projects, { withFileTypes: true }).filter((item) => item.isDirectory()).map((item) => item.name); } catch { return null; }
114
+ for (const folder of folders) {
115
+ const path = join(projects, folder, `${id}.jsonl`);
116
+ try { if (statSync(path).isFile()) return path; } catch { /* not here */ }
117
+ }
118
+ return null;
119
+ }
120
+
121
+ /** The last `maxBytes` of a file as text (a partial first line is dropped). */
122
+ function readTail(path, maxBytes = 1024 * 1024) {
123
+ let fd = null;
124
+ try {
125
+ fd = openSync(path, 'r');
126
+ const size = fstatSync(fd).size;
127
+ const length = Math.min(size, maxBytes);
128
+ const buffer = Buffer.alloc(length);
129
+ readSync(fd, buffer, 0, length, size - length);
130
+ const text = buffer.toString('utf8');
131
+ return size > length ? text.slice(text.indexOf('\n') + 1) : text;
132
+ } catch { return ''; } finally { if (fd !== null) { try { closeSync(fd); } catch { /* closed */ } } }
133
+ }
134
+
135
+ /**
136
+ * The cost state a resumed CLI starts from: the last `cost-state` line of the session's transcript, which the CLI
137
+ * writes when it exits cleanly (a SIGTERM included) and reads back when the session is resumed. →
138
+ * { totalCostUsd, modelUsage } (modelUsage as result.modelUsage spells it), or null when there is none (a CLI that
139
+ * was killed outright starts its counters at zero).
140
+ */
141
+ export function readCostState(path, sessionId = null) {
142
+ if (!path || !existsSync(path)) return null;
143
+ const lines = readTail(path).split('\n');
144
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
145
+ const line = lines[i];
146
+ if (!line.includes('"cost-state"')) continue;
147
+ let row = null;
148
+ try { row = JSON.parse(line); } catch { continue; }
149
+ if (row?.type !== 'cost-state') continue;
150
+ if (sessionId && row.sessionId && row.sessionId !== sessionId) continue;
151
+ const total = Number(row.totalCostUSD);
152
+ return {
153
+ totalCostUsd: Number.isFinite(total) && total > 0 ? total : 0,
154
+ modelUsage: row.modelUsage && typeof row.modelUsage === 'object' && !Array.isArray(row.modelUsage) ? row.modelUsage : null,
155
+ };
156
+ }
157
+ return null;
158
+ }
159
+
160
+ /** Whether a process with this pid exists (one of another user's counts as existing). */
161
+ export function processExists(pid) {
162
+ const id = Number(pid);
163
+ if (!Number.isInteger(id) || id <= 0) return false;
164
+ try { process.kill(id, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
165
+ }
166
+
167
+ /** The command line of a process, or null when it cannot be read (no such process, no `ps`, Windows without CIM). */
168
+ export function processCommandLine(pid, { platform = process.platform, run = execFileSync } = {}) {
169
+ const id = Number(pid);
170
+ if (!Number.isInteger(id) || id <= 0) return null;
171
+ try {
172
+ if (platform === 'win32') {
173
+ const out = run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `(Get-CimInstance Win32_Process -Filter "ProcessId=${id}").CommandLine`], { encoding: 'utf8', timeout: 5000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] });
174
+ return String(out || '').trim() || null;
175
+ }
176
+ const out = run('ps', ['-o', 'command=', '-p', String(id)], { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'] });
177
+ return String(out || '').trim() || null;
178
+ } catch { return null; }
179
+ }
180
+
181
+ /**
182
+ * A run's own Claude Code CLI: started by the Agent SDK (stream-json both ways) with the run's id in its SynaBun MCP
183
+ * headers (X-Synabun-Terminal). Anything else with the pid is left alone.
184
+ */
185
+ export function isRunCliCommand(command, runId) {
186
+ const text = String(command || '');
187
+ return !!runId && text.includes(String(runId)) && ((text.includes('--input-format') && text.includes('stream-json')) || (text.includes('app-server') && text.includes('synabun_worker_run=')));
188
+ }
189
+
190
+ /** Windows: end a process and every process it started (taskkill /T /F, as the server's own shutdown does). */
191
+ export function endProcessTree(pid, { run = execFileSync } = {}) {
192
+ run('taskkill', ['/pid', String(pid), '/T', '/F'], { timeout: 5000, windowsHide: true, stdio: 'ignore' });
193
+ }
194
+
195
+ /**
196
+ * End the CLI a run had before the server went down, when it is still there (a crash ended no process). It is ended
197
+ * only when its command line shows it is that run's CLI: SIGTERM, then SIGKILL after `graceMs` once the pid still
198
+ * shows that command line (it may have gone to another process meanwhile). Windows has no SIGTERM (a signal stops the
199
+ * CLI alone and leaves its tool's processes, a dev server keeps its port): the tree is ended with taskkill.
200
+ * → { state: 'gone' | 'ended' | 'killed' | 'still_running' | 'not_ours' | 'unknown' }.
201
+ * 'unknown': a process has the pid but its command line cannot be read, so it is not touched.
202
+ */
203
+ export async function endOrphanProcess({ pid, runId, graceMs = 2000, platform = process.platform, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), readCommand = processCommandLine, exists = processExists, kill = (id, signal) => process.kill(id, signal), killTree = endProcessTree } = {}) {
204
+ const id = Number(pid);
205
+ if (!Number.isInteger(id) || id <= 0 || id === process.pid || !exists(id)) return { state: 'gone' };
206
+ const command = readCommand(id);
207
+ // No command line: a process that exited meanwhile is gone; one still there is not touched.
208
+ if (command === null) return { state: exists(id) ? 'unknown' : 'gone' };
209
+ if (!isRunCliCommand(command, runId)) return { state: 'not_ours' };
210
+ if (platform === 'win32') {
211
+ try { killTree(id); } catch { /* gone meanwhile, or not allowed: checked below */ }
212
+ for (let waited = 0; waited < graceMs; waited += 100) {
213
+ await sleep(100);
214
+ if (!exists(id)) return { state: 'killed' };
215
+ }
216
+ return { state: 'still_running' };
217
+ }
218
+ try { kill(id, 'SIGTERM'); } catch { /* gone meanwhile */ }
219
+ for (let waited = 0; waited < graceMs; waited += 100) {
220
+ await sleep(100);
221
+ if (!exists(id)) return { state: 'ended' };
222
+ }
223
+ const again = readCommand(id);
224
+ if (again === null) return { state: exists(id) ? 'still_running' : 'ended' };
225
+ if (!isRunCliCommand(again, runId)) return { state: 'ended' };
226
+ try { kill(id, 'SIGKILL'); } catch { /* gone meanwhile */ }
227
+ await sleep(200);
228
+ return { state: exists(id) ? 'still_running' : 'killed' };
229
+ }
@@ -0,0 +1,299 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun — macOS Seatbelt isolation for guarded worker trees (pure + small fs)
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // A dispatched worker (capability read-only / workspace, Protect workers = Enforce) can reach SynaBun's own control
6
+ // listener on localhost:<NEURAL_PORT> from any program it runs — node -e, python, a script, a detached process —
7
+ // fetch the owner's page cookie, and act as the owner (dispatch a full worker, turn Protect workers off, install a
8
+ // skill). The in-code shell guard is only a speed bump: arbitrary code is opaque to it.
9
+ //
10
+ // On macOS the real boundary is the OS: run the worker's whole process tree under sandbox-exec with a profile that
11
+ // blocks outbound connections to SynaBun's control port (and the other local endpoints that can relay commands or
12
+ // owner authority), while leaving everything legitimate working — the internet, reads anywhere, writes per
13
+ // capability, the project's tests, and the worker's own SynaBun MCP tools, which reach SynaBun through a worker-only
14
+ // channel on a port the profile leaves open (see lib/worker-mcp-channel.js).
15
+ //
16
+ // Proven on macOS 26 (scratch probes, 2026-10-23 / 2026-10-24), the facts this module relies on:
17
+ // • `(deny network-outbound (remote ip "*:<port>"))` blocks that port on EVERY host — 127.0.0.1, localhost, ::1,
18
+ // [::ffff:127.0.0.1] (the IPv4-mapped IPv6 form a dual-stack app.listen answers on), 0.0.0.0, this Mac's LAN /
19
+ // interface IPs and its hostname — while a different port and the public internet stay open. The earlier
20
+ // `localhost:<port>` literal did NOT cover [::ffff:127.0.0.1] (a verified full bypass of the whole layer on the
21
+ // dual-stack listener): Seatbelt's `localhost` resolves to 127.0.0.1 + ::1 only. The host literal must be `*`
22
+ // or `localhost` (a numeric IP literal is rejected by the parser); `*` is the only form that is address-proof.
23
+ // Seatbelt has no port-range syntax (`*:4120-4400` is rejected), so a reserved range is emitted one port a line.
24
+ // • `(deny file-read* file-write* (subpath "<canonical>"))` blocks reads AND writes under a path; a later
25
+ // `(allow file-read* file-write* (subpath "<canonical>"))` wins (Seatbelt is last-match-wins within a filter),
26
+ // so SynaBun's whole data home can be denied and the run's own scratch / cwd / config folder allowed back after
27
+ // it. Seatbelt matches the RESOLVED path, so every literal must be canonicalised (/tmp is a symlink to
28
+ // /private/tmp; an un-resolved literal never matches, and an allow-back must resolve the same way as its deny).
29
+ // • The sandbox is inherited by detached / nohup grandchildren, so a worker cannot escape it by backgrounding.
30
+ // • A same-user `KERN_PROCARGS2` read (ps -E / a tiny C program) CANNOT be blocked by Seatbelt (it is a same-uid
31
+ // kernel allowance, not a sandbox-governed operation). So a worker inside the sandbox can still read another
32
+ // same-user process's argv+env — including a brain's SYNABUN_BRAIN_TOKEN, or a relay's password in env. That
33
+ // theft is harmless here because the token/password is useless without reaching a port this profile blocks on
34
+ // every address; a Codex worker (which this module cannot wrap — nested sandbox-exec is refused) is held by
35
+ // running with its network off instead.
36
+ //
37
+ // This module is macOS-only. On every other platform sandboxSupported() is false; and even on macOS the caller
38
+ // gates on sandboxExecAvailable() (a one-time boot probe that /usr/bin/sandbox-exec exists and runs a trivial
39
+ // allow-default profile), falling back to the in-code guard alone when it does not. Pure but for canonicalising
40
+ // paths, the boot probe, and writing the profile / wrapper into a run's own scratch folder; unit-tested in
41
+ // tests/worker-sandbox.test.mjs.
42
+ //
43
+ // NOT EQUIVALENT OFF macOS (stated plainly; HIGH elsewhere). Where this module does not apply — Linux, Windows, or a
44
+ // macOS host whose sandbox-exec is unavailable — a protected (Enforce, read-only/workspace) worker is held only by the
45
+ // in-code guard (the shell heuristic + the tool hooks), which reads the command line only and so is a lexical
46
+ // speed-bump, not a boundary: a script file (`node /path/x.js`), hidden-address inline code, or any program doing its
47
+ // own networking slips past it. Two things the macOS profile closes that nothing else does there: (a) SynaBun's
48
+ // OpenCode serve band 4096-4400 is unblocked, so a worker's script can connect to a serve; (b) a SHARED (non-protected)
49
+ // serve keeps its OPENCODE_PASSWORD in its process environment (lib/opencode-protocol.js serveSpecFor env mode; only a
50
+ // protected worker's OWN isolated serve hides it via passwordFrom 'stdout'), and a same-uid process can read another
51
+ // process's environment (KERN_PROCARGS2 / /proc/<pid>/environ) — a read no sandbox blocks — so a prompt-injected worker
52
+ // off macOS could read that password and open a full-control session on the shared serve. On macOS under Enforce the
53
+ // 4096-4400 deny (plus the data-home/credential denies below) is the real boundary that stops both. The protected-
54
+ // worker IN-FOLDER confinement off macOS is therefore best-effort, not a guarantee. See docs/assistant.md → Worker
55
+ // confinement → "Protected-worker in-folder confinement off macOS is NOT equivalent to macOS".
56
+
57
+ import { chmodSync, existsSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs';
58
+ import { spawnSync } from 'node:child_process';
59
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
60
+
61
+ export const SANDBOX_EXEC = '/usr/bin/sandbox-exec';
62
+ /**
63
+ * The ports the internet needs (SSH, DNS, HTTP, HTTPS). A relay found listening on one of them is denied on this
64
+ * computer's own addresses only (`localhost:<port>`: 127.0.0.1, ::1, every interface address and the hostname —
65
+ * not the IPv4-mapped spelling, which only `*` covers), never with `*:<port>`, which would cut the worker's internet.
66
+ * The strict profile (detection failed) allows exactly these, plus the worker channel.
67
+ */
68
+ export const INTERNET_PORTS = Object.freeze([22, 53, 80, 443]);
69
+ /** Local ports no guarded worker has a reason to open: SSH to this computer (Remote Login) is a shell outside the
70
+ * sandbox. Denied on this computer's own addresses in every guarded profile; ssh to other hosts is untouched. */
71
+ export const ALWAYS_LOCAL_DENY_PORTS = Object.freeze([22]);
72
+
73
+ /** macOS only: the platform where the Seatbelt primitives above hold. A cheap pure check for tests and early gates. */
74
+ export function sandboxSupported(platform = process.platform) {
75
+ return platform === 'darwin';
76
+ }
77
+
78
+ // Whether /usr/bin/sandbox-exec exists AND runs a trivial allow-default profile: probed once (cheap, ~15 ms) and
79
+ // cached, so a host where Seatbelt is missing or broken falls back to the in-code guard instead of failing every
80
+ // guarded worker. null = not yet probed.
81
+ let _sandboxExecProbe = null;
82
+ /**
83
+ * macOS AND sandbox-exec works. Cached after the first probe. The caller (server.js) gates OS isolation on this,
84
+ * never on sandboxSupported() alone, so a missing / broken sandbox-exec is a visible fall-back, not a launch failure.
85
+ */
86
+ export function sandboxExecAvailable({ platform = process.platform, spawn = spawnSync, force = false } = {}) {
87
+ if (!sandboxSupported(platform)) return false;
88
+ if (_sandboxExecProbe !== null && !force) return _sandboxExecProbe;
89
+ let ok = false;
90
+ try {
91
+ if (existsSync(SANDBOX_EXEC)) {
92
+ const r = spawn(SANDBOX_EXEC, ['-p', '(version 1)(allow default)', '/usr/bin/true'], { timeout: 5000, stdio: 'ignore' });
93
+ ok = !r.error && r.status === 0 && !r.signal;
94
+ }
95
+ } catch { ok = false; }
96
+ _sandboxExecProbe = ok;
97
+ return ok;
98
+ }
99
+ /** Tests only: forget the cached probe result. */
100
+ export function _resetSandboxExecProbe() { _sandboxExecProbe = null; }
101
+
102
+ /** A Seatbelt string literal: double-quoted, with `\` and `"` escaped. */
103
+ function sexpr(text) {
104
+ return `"${String(text).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
105
+ }
106
+
107
+ /** Where a path really is (Seatbelt matches the resolved path); the deepest existing ancestor resolved, rest as named. */
108
+ export function canonicalPath(abs) {
109
+ let probe = isAbsolute(abs) ? abs : resolve(abs);
110
+ const rest = [];
111
+ for (let i = 0; i < 256; i++) {
112
+ try { const real = realpathSync(probe); return rest.length ? join(real, ...rest.reverse()) : real; }
113
+ catch { const parent = dirname(probe); if (parent === probe) return resolve(abs); rest.push(probe.slice(parent.length).replace(/^[/\\]+/, '')); probe = parent; }
114
+ }
115
+ return resolve(abs);
116
+ }
117
+
118
+ /** Valid, deduped, sorted ports from a list. */
119
+ function cleanPorts(ports) {
120
+ return [...new Set((ports || []).map(Number).filter((p) => Number.isInteger(p) && p > 0 && p < 65536))].sort((a, b) => a - b);
121
+ }
122
+
123
+ /**
124
+ * The ancestor DIRECTORIES of each allow-back that fall inside a denied tree. `(deny file-read* (subpath X))` denies
125
+ * metadata too, so when a run's scratch folder is allowed back under a denied data home, node cannot even `lstat`
126
+ * the denied ancestors (/dataHome, /dataHome/data, /dataHome/data/scratch) to resolve a path there — it fails EPERM
127
+ * before it runs a script. Granting `file-read-metadata` on those exact directory nodes (a literal, not a subpath:
128
+ * the directory itself, never its contents) lets the lookup through without exposing any file in the denied tree.
129
+ * Canonical in, canonical out (the allow paths are already canonical, so their dirname chain is too).
130
+ */
131
+ function metadataAncestors(allowPaths, denyPaths) {
132
+ const denies = denyPaths.map((p) => (p.endsWith('/') ? p.slice(0, -1) : p)).filter(Boolean);
133
+ const insideADeny = (dir) => denies.some((d) => dir === d || dir.startsWith(d + '/'));
134
+ const out = new Set();
135
+ for (const allow of allowPaths) {
136
+ let dir = dirname(allow);
137
+ for (let i = 0; i < 256; i++) {
138
+ if (insideADeny(dir)) out.add(dir);
139
+ const parent = dirname(dir);
140
+ if (parent === dir) break;
141
+ dir = parent;
142
+ }
143
+ }
144
+ return [...out];
145
+ }
146
+
147
+ /**
148
+ * The Seatbelt profile for a guarded worker tree.
149
+ * @param {object} o
150
+ * @param {number[]} o.denyPorts individual ports to block OUTBOUND to, on EVERY host (NEURAL_PORT + its WebSockets,
151
+ * the launch beacon, the shared OpenCode serve). The worker-only MCP channel port is
152
+ * NOT here — the profile must leave it open.
153
+ * @param {{from:number,to:number}} [o.denyPortRange] a reserved range (SynaBun's isolated OpenCode serves) blocked
154
+ * in FULL, so a relay started after this profile is written is already denied. Each
155
+ * port in it is a line (Seatbelt has no range syntax).
156
+ * @param {string[]} [o.denyPaths] folders / files whose reads AND writes are denied (SynaBun's whole data home, the
157
+ * other runtimes' credential files this run does not use). Each is canonicalised.
158
+ * @param {string[]} [o.allowPaths] folders allowed back AFTER the denies (the run's own scratch / cwd / config), so
159
+ * a path under a denied tree the run truly needs survives. Canonicalised; placed last
160
+ * (last match wins).
161
+ * @param {number[]} [o.denyLocalPorts] ports blocked OUTBOUND on this computer's own addresses only (`localhost:<port>`):
162
+ * a relay found on an internet port (22, 53, 80, 443), and local SSH.
163
+ * @param {{ allowPorts:number[] }} [o.strictLocal] relay detection FAILED for this run: no outbound connection at all
164
+ * but the internet's ports (INTERNET_PORTS, on other hosts) and the worker channel
165
+ * (`allowPorts`, on this computer). `*:*` is the only rule that covers every local
166
+ * spelling (the IPv4-mapped one included); Seatbelt's `localhost` misses [::ffff:…],
167
+ * and mixing `ip6` rules with `localhost` ones silently re-opens IPv4 loopback (verified
168
+ * on macOS 26), so this is written as deny-all-then-allow, never as a loopback deny.
169
+ * @param {string} [o.note] a short comment line (the run id), for readability in a log.
170
+ * @returns {string} the profile text (ends with a newline).
171
+ */
172
+ export function buildWorkerSandboxProfile({ denyPorts = [], denyLocalPorts = [], denyPortRange = null, denyPaths = [], allowPaths = [], note = '', strictLocal = null } = {}) {
173
+ const localOnly = new Set(cleanPorts(denyLocalPorts));
174
+ // A port the internet needs is never denied on every host: it goes to the local-only list instead.
175
+ const ports = cleanPorts(denyPorts).filter((p) => { if (INTERNET_PORTS.includes(p)) { localOnly.add(p); return false; } return true; });
176
+ const range = denyPortRange && Number.isInteger(Number(denyPortRange.from)) && Number.isInteger(Number(denyPortRange.to))
177
+ ? { from: Math.max(1, Number(denyPortRange.from)), to: Math.min(65535, Number(denyPortRange.to)) } : null;
178
+ const denies = [...new Set((denyPaths || []).filter((p) => typeof p === 'string' && p).map(canonicalPath))];
179
+ const allows = [...new Set((allowPaths || []).filter((p) => typeof p === 'string' && p).map(canonicalPath))];
180
+ const lines = ['(version 1)'];
181
+ if (note) lines.push(`; ${String(note).replace(/[\r\n]+/g, ' ').slice(0, 200)}`);
182
+ lines.push('(allow default)');
183
+ if (strictLocal) {
184
+ // Detection failed: nothing local is known to be safe, so nothing local is reachable but the worker channel.
185
+ // Deny every outbound IP connection, allow the internet's ports back on every host, take them away again on this
186
+ // computer's own addresses, and allow the channel last (last match wins).
187
+ lines.push('; strict: the relay detection failed for this run, so no local port is reachable but the worker channel');
188
+ lines.push(`(deny network-outbound (remote ip ${sexpr('*:*')}))`);
189
+ for (const port of INTERNET_PORTS) lines.push(`(allow network-outbound (remote ip ${sexpr(`*:${port}`)}))`);
190
+ for (const port of INTERNET_PORTS) if (port !== 53) localOnly.add(port); // a local DNS resolver stays reachable
191
+ for (const port of cleanPorts(strictLocal.allowPorts)) {
192
+ localOnly.delete(port);
193
+ lines.push(`(allow network-outbound (remote ip ${sexpr(`localhost:${port}`)}))`);
194
+ }
195
+ }
196
+ // The control ports: a worker's own programs may not open them (its MCP tools are the way in). `*:<port>` blocks
197
+ // the port on every address (127.0.0.1, ::1, [::ffff:127.0.0.1], 0.0.0.0, the LAN IPs, the hostname); the public
198
+ // internet and every other port stay open.
199
+ const seen = new Set();
200
+ for (const port of ports) { seen.add(port); lines.push(`(deny network-outbound (remote ip ${sexpr(`*:${port}`)}))`); }
201
+ // Local-only denies (an internet port a relay holds here, local SSH): this computer's own addresses, never the internet.
202
+ for (const port of [...new Set([...localOnly, ...ALWAYS_LOCAL_DENY_PORTS])].sort((x, y) => x - y)) {
203
+ if (strictLocal && cleanPorts(strictLocal.allowPorts).includes(port)) continue;
204
+ lines.push(`(deny network-outbound (remote ip ${sexpr(`localhost:${port}`)}))`);
205
+ }
206
+ // The reserved relay range in full: every isolated OpenCode serve, whether it started before or after this profile.
207
+ if (range) for (let p = range.from; p <= range.to; p++) { if (!seen.has(p)) { seen.add(p); lines.push(`(deny network-outbound (remote ip ${sexpr(`*:${p}`)}))`); } }
208
+ // SynaBun's whole data home (its auth secrets, other runs' task text, the memory DB) and other runtimes' creds:
209
+ // a worker neither reads nor writes them.
210
+ for (const path of denies) lines.push(`(deny file-read* file-write* (subpath ${sexpr(path)}))`);
211
+ // Allowed back LAST (last match wins): the run's own scratch folder, its cwd when that is inside the data home
212
+ // (skill-drafting workspaces), and its OpenCode config folder — everything the run legitimately needs there.
213
+ for (const path of allows) lines.push(`(allow file-read* file-write* (subpath ${sexpr(path)}))`);
214
+ // Metadata-only on the denied ancestors of each allow-back, so `node scratch/probe.mjs` can lstat its way in
215
+ // (without this, a denied data-home ancestor fails EPERM before the script runs). Directory nodes only (literal),
216
+ // placed after the file allow-backs so this metadata grant is the last match for those paths.
217
+ for (const dir of metadataAncestors(allows, denies)) lines.push(`(allow file-read-metadata (literal ${sexpr(dir)}))`);
218
+ return `${lines.join('\n')}\n`;
219
+ }
220
+
221
+ /** sandbox-exec argv to run `command args…` under a written profile (OpenCode's serve launch). */
222
+ export function sandboxExecArgv(profilePath, command, args = []) {
223
+ return [SANDBOX_EXEC, '-f', profilePath, command, ...(Array.isArray(args) ? args : [])];
224
+ }
225
+
226
+ /**
227
+ * Write a run's Seatbelt profile and (for a CLI the Agent SDK spawns directly) a wrapper script that execs the real
228
+ * executable under it. The SDK uses pathToClaudeCodeExecutable verbatim with shell:false, so the wrapper is a plain
229
+ * shebang script that forwards its arguments; the sandbox is inherited by everything the CLI then starts.
230
+ *
231
+ * @param {object} o
232
+ * @param {string} o.dir a folder the run owns (its scratch folder): the profile and wrapper live here.
233
+ * @param {string} o.profile the profile text (buildWorkerSandboxProfile).
234
+ * @param {string} [o.realExecutable] the executable the wrapper should exec (for Claude); omit for OpenCode.
235
+ * @param {string} [o.nodeRunner] the node to run a `.js` entry with (default process.execPath); only used when the
236
+ * real executable is a script the Agent SDK would itself have run via node (.js / .mjs / …).
237
+ * @param {string} [o.name] a basename stem (default 'worker').
238
+ * @returns {{ profilePath: string, wrapperPath: string|null }}
239
+ *
240
+ * The wrapper must be named so the Agent SDK spawns it DIRECTLY (not via node): the SDK runs a
241
+ * pathToClaudeCodeExecutable that ends in .js/.mjs/.ts/.tsx/.jsx through `node`, anything else directly. `.sh`
242
+ * is spawned directly, so the wrapper execs sandbox-exec itself. If the REAL executable is such a script, the
243
+ * wrapper runs it under node (what the SDK would have done), so the sandbox still wraps the whole tree.
244
+ */
245
+ export function writeWorkerSandbox({ dir, profile, realExecutable = null, nodeRunner = process.execPath, name = 'worker' }) {
246
+ if (!dir) throw new Error('writeWorkerSandbox needs a directory the run owns');
247
+ mkdirSync(dir, { recursive: true });
248
+ const stem = String(name).replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 64) || 'worker';
249
+ const profilePath = join(dir, `${stem}.sb`);
250
+ writeFileSync(profilePath, profile, { mode: 0o600 });
251
+ let wrapperPath = null;
252
+ if (realExecutable) {
253
+ wrapperPath = join(dir, `${stem}-wrap.sh`);
254
+ // exec, so the wrapper process is replaced by sandbox-exec (no extra shell in the tree). "$@" forwards argv
255
+ // verbatim. The real executable (and node, when the entry is a script) and the profile are single-quoted.
256
+ const sh = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
257
+ const isScript = /\.(?:mjs|cjs|js|ts|tsx|jsx)$/i.test(realExecutable);
258
+ const runTarget = isScript ? `${sh(nodeRunner)} ${sh(realExecutable)}` : sh(realExecutable);
259
+ writeFileSync(wrapperPath, `#!/bin/sh\nexec ${sh(SANDBOX_EXEC)} -f ${sh(profilePath)} ${runTarget} "$@"\n`, { mode: 0o700 });
260
+ chmodSync(wrapperPath, 0o700);
261
+ }
262
+ return { profilePath, wrapperPath };
263
+ }
264
+
265
+ /**
266
+ * Everything a guarded Claude / OpenCode run needs for OS isolation, or null when it does not apply (not macOS, no
267
+ * guard, warn / off, or the capability is full). The caller gathers the data-home trees and other-runtime creds to
268
+ * deny and the run's own folders to allow back; this assembles the port set and gates on an enforced guard. Writing
269
+ * is left to the caller (writeWorkerSandbox) so a failure to write never throws on the launch path — the caller
270
+ * falls back to the in-code guard and says so.
271
+ * @param {{ allowPorts:number[] }} [o.strictLocal] the relay detection failed: the strict profile (see
272
+ * buildWorkerSandboxProfile), with `allowPorts` the worker channel.
273
+ * @returns {{ denyPorts:number[], denyLocalPorts:number[], denyPortRange:{from,to}|null, denyPaths:string[], allowPaths:string[], note:string, strictLocal:object|null } | null}
274
+ */
275
+ export function workerSandboxPlan({ confinement, serverPort = null, reservedPortRange = null, extraDenyPorts = [], denyPaths = [], allowPaths = [], strictLocal = null, platform = process.platform } = {}) {
276
+ if (!sandboxSupported(platform)) return null;
277
+ if (!confinement || !confinement.enforced) return null; // only an enforced guard (read-only / workspace) is wrapped
278
+ const denyPorts = [];
279
+ const denyLocalPorts = [];
280
+ const own = Number(serverPort || confinement.serverPort) || null;
281
+ if (own) denyPorts.push(own);
282
+ for (const p of extraDenyPorts || []) {
283
+ const n = Number(p);
284
+ if (!(n > 0)) continue;
285
+ // A relay on an internet port is denied on this computer's addresses only; the internet keeps the port.
286
+ if (INTERNET_PORTS.includes(n)) denyLocalPorts.push(n); else denyPorts.push(n);
287
+ }
288
+ const strictAllow = strictLocal ? cleanPorts(strictLocal.allowPorts) : [];
289
+ return {
290
+ denyPorts: [...new Set(denyPorts)],
291
+ denyLocalPorts: [...new Set(denyLocalPorts)],
292
+ denyPortRange: reservedPortRange && Number(reservedPortRange.from) > 0 && Number(reservedPortRange.to) > 0
293
+ ? { from: Number(reservedPortRange.from), to: Number(reservedPortRange.to) } : null,
294
+ denyPaths: [...new Set((denyPaths || []).filter(Boolean))],
295
+ allowPaths: [...new Set((allowPaths || []).filter(Boolean))],
296
+ note: `synabun worker ${confinement.capability} (${confinement.mode})${strictLocal ? ' strict' : ''}`,
297
+ strictLocal: strictLocal ? { allowPorts: strictAllow } : null,
298
+ };
299
+ }
@@ -1,19 +1,18 @@
1
1
  {
2
2
  "name": "neural-memory-interface",
3
- "version": "2.0.10",
3
+ "version": "2.1.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "neural-memory-interface",
9
- "version": "2.0.10",
9
+ "version": "2.1.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "Apache-2.0",
12
12
  "dependencies": {
13
13
  "@anthropic-ai/claude-agent-sdk": "0.3.288",
14
14
  "@huggingface/transformers": "^3.0.0",
15
15
  "@openai/codex-sdk": "^0.160.0",
16
- "@opencode-ai/sdk": "1.18.34",
17
16
  "@xterm/addon-fit": "0.11.0",
18
17
  "@xterm/addon-search": "0.16.0",
19
18
  "@xterm/addon-serialize": "0.14.0",
@@ -31,6 +30,7 @@
31
30
  "playwright": "^1.59.1",
32
31
  "undici": "^8.5.0",
33
32
  "ws": "^8.21.0",
33
+ "yaml": "2.9.1",
34
34
  "zod": "^4.4.3"
35
35
  },
36
36
  "devDependencies": {
@@ -2205,15 +2205,6 @@
2205
2205
  "node": ">=18"
2206
2206
  }
2207
2207
  },
2208
- "node_modules/@opencode-ai/sdk": {
2209
- "version": "1.18.34",
2210
- "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.18.34.tgz",
2211
- "integrity": "sha512-R9QWUpcngtdQ6FCixz7NC6n1WsSho6CEuRUzYHz/CzZcp3miSklDzXkm0oDwXVCnuEu+ANoeM0f6RDp51kOMzQ==",
2212
- "license": "MIT",
2213
- "dependencies": {
2214
- "cross-spawn": "7.0.6"
2215
- }
2216
- },
2217
2208
  "node_modules/@pkgjs/parseargs": {
2218
2209
  "version": "0.11.0",
2219
2210
  "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -6391,6 +6382,21 @@
6391
6382
  "node": ">=18"
6392
6383
  }
6393
6384
  },
6385
+ "node_modules/yaml": {
6386
+ "version": "2.9.1",
6387
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz",
6388
+ "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==",
6389
+ "license": "ISC",
6390
+ "bin": {
6391
+ "yaml": "bin.mjs"
6392
+ },
6393
+ "engines": {
6394
+ "node": ">= 14.6"
6395
+ },
6396
+ "funding": {
6397
+ "url": "https://github.com/sponsors/eemeli"
6398
+ }
6399
+ },
6394
6400
  "node_modules/zip-stream": {
6395
6401
  "version": "6.0.1",
6396
6402
  "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neural-memory-interface",
3
- "version": "2.0.10",
3
+ "version": "2.1.0",
4
4
  "private": true,
5
5
  "description": "Private local SynaBun interface for memory visualization and multi-agent workflows",
6
6
  "license": "Apache-2.0",
@@ -24,7 +24,6 @@
24
24
  "@anthropic-ai/claude-agent-sdk": "0.3.288",
25
25
  "@huggingface/transformers": "^3.0.0",
26
26
  "@openai/codex-sdk": "^0.160.0",
27
- "@opencode-ai/sdk": "1.18.34",
28
27
  "@xterm/addon-fit": "0.11.0",
29
28
  "@xterm/addon-search": "0.16.0",
30
29
  "@xterm/addon-serialize": "0.14.0",
@@ -42,6 +41,7 @@
42
41
  "playwright": "^1.59.1",
43
42
  "undici": "^8.5.0",
44
43
  "ws": "^8.21.0",
44
+ "yaml": "2.9.1",
45
45
  "zod": "^4.4.3"
46
46
  },
47
47
  "overrides": {
@@ -13,7 +13,8 @@
13
13
  <link rel="manifest" href="/manifest.json">
14
14
  <link rel="preconnect" href="https://fonts.googleapis.com">
15
15
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
16
- <link href="https://fonts.googleapis.com/css2?family=Caveat:wght@400;700&family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
16
+ <link rel="stylesheet" href="/vendor/caveat/caveat.css">
17
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
17
18
  <link href="https://db.onlinewebfonts.com/c/3284c3a32cf946466fb963369b3c5b2d?family=Disket+Mono" rel="stylesheet">
18
19
  <link rel="stylesheet" href="/shared/styles.css">
19
20
  <!-- three.js is vendored (scripts/build-vendor.mjs); only the map renderer imports it.
@@ -59,6 +60,7 @@
59
60
  <!-- html2canvas for card screenshot MCP tool (vendored: public/vendor/html2canvas/) -->
60
61
  <script src="/vendor/html2canvas/html2canvas-1.4.1.min.js" defer></script>
61
62
 
63
+ <script type="module" src="/shared/page-session.js"></script>
62
64
  <script type="module" src="/variant/3d/main.js"></script>
63
65
  </body>
64
66
  </html>