synabun 2.0.10 → 2.2.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 (656) hide show
  1. package/CHANGELOG.md +207 -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 +13 -13
  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 +30 -17
  11. package/lib/desktop-app.js +7 -1
  12. package/lib/external-tools.js +7 -6
  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 +302 -0
  24. package/lib/virustotal.mjs +203 -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/budget-judgments.d.ts +1 -1
  37. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
  38. package/mcp-server/dist/services/budget-judgments.js +5 -3
  39. package/mcp-server/dist/services/budget-judgments.js.map +1 -1
  40. package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
  41. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  42. package/mcp-server/dist/services/codex-config-heal.js +81 -28
  43. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  44. package/mcp-server/dist/services/identity.d.ts +2 -2
  45. package/mcp-server/dist/services/identity.js +2 -2
  46. package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
  47. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
  48. package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
  49. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
  50. package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
  51. package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
  52. package/mcp-server/dist/services/memory-sessions.js +28 -2
  53. package/mcp-server/dist/services/memory-sessions.js.map +1 -1
  54. package/mcp-server/dist/services/neural-interface.d.ts +18 -11
  55. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  56. package/mcp-server/dist/services/neural-interface.js +67 -11
  57. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  58. package/mcp-server/dist/services/server-instructions.d.ts +7 -2
  59. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  60. package/mcp-server/dist/services/server-instructions.js +22 -63
  61. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  62. package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
  63. package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
  64. package/mcp-server/dist/services/tool-catalog.js +339 -0
  65. package/mcp-server/dist/services/tool-catalog.js.map +1 -0
  66. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
  67. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
  68. package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
  69. package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
  70. package/mcp-server/dist/services/tool-list.d.ts +108 -0
  71. package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
  72. package/mcp-server/dist/services/tool-list.js +334 -0
  73. package/mcp-server/dist/services/tool-list.js.map +1 -0
  74. package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
  75. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
  76. package/mcp-server/dist/services/tool-runtime.js +400 -0
  77. package/mcp-server/dist/services/tool-runtime.js.map +1 -0
  78. package/mcp-server/dist/services/typesafe-config.js +1 -1
  79. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  80. package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
  81. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
  82. package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
  83. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
  84. package/mcp-server/dist/tools/agents.d.ts +14 -14
  85. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  86. package/mcp-server/dist/tools/agents.js +29 -19
  87. package/mcp-server/dist/tools/agents.js.map +1 -1
  88. package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
  89. package/mcp-server/dist/tools/choice.d.ts +6 -0
  90. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  91. package/mcp-server/dist/tools/choice.js +18 -13
  92. package/mcp-server/dist/tools/choice.js.map +1 -1
  93. package/mcp-server/dist/tools/git-tools.js +2 -2
  94. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  95. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  96. package/mcp-server/dist/tools/image-tools.js +1 -1
  97. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  98. package/mcp-server/dist/tools/morelogin.js +9 -6
  99. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  100. package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
  101. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
  102. package/mcp-server/dist/tools/skill-tools.js +642 -0
  103. package/mcp-server/dist/tools/skill-tools.js.map +1 -0
  104. package/mcp-server/dist/tools/skill.d.ts +7 -0
  105. package/mcp-server/dist/tools/skill.d.ts.map +1 -0
  106. package/mcp-server/dist/tools/skill.js +11 -0
  107. package/mcp-server/dist/tools/skill.js.map +1 -0
  108. package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
  109. package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
  110. package/mcp-server/dist/tools/tool-groups.js +147 -0
  111. package/mcp-server/dist/tools/tool-groups.js.map +1 -0
  112. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
  113. package/mcp-server/dist/tools/whiteboard-format.js +7 -4
  114. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
  115. package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
  116. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  117. package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
  118. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  119. package/mcp-server/package-lock.json +2 -2
  120. package/mcp-server/package.json +1 -1
  121. package/mcp-server/src/http.ts +75 -12
  122. package/mcp-server/src/index.ts +147 -154
  123. package/mcp-server/src/preload.ts +12 -12
  124. package/mcp-server/src/services/budget-judgments.ts +5 -3
  125. package/mcp-server/src/services/codex-config-heal.ts +80 -29
  126. package/mcp-server/src/services/identity.ts +2 -2
  127. package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
  128. package/mcp-server/src/services/memory-sessions.ts +24 -3
  129. package/mcp-server/src/services/neural-interface.ts +81 -21
  130. package/mcp-server/src/services/server-instructions.ts +29 -64
  131. package/mcp-server/src/services/tool-catalog.ts +403 -0
  132. package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
  133. package/mcp-server/src/services/tool-list.ts +410 -0
  134. package/mcp-server/src/services/tool-runtime.ts +457 -0
  135. package/mcp-server/src/services/typesafe-config.ts +1 -1
  136. package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
  137. package/mcp-server/src/tools/agents.ts +29 -20
  138. package/mcp-server/src/tools/choice.ts +17 -12
  139. package/mcp-server/src/tools/git-tools.ts +2 -2
  140. package/mcp-server/src/tools/image-tools.ts +1 -1
  141. package/mcp-server/src/tools/morelogin.ts +6 -4
  142. package/mcp-server/src/tools/skill-tools.ts +592 -0
  143. package/mcp-server/src/tools/skill.ts +13 -0
  144. package/mcp-server/src/tools/tool-groups.ts +164 -0
  145. package/mcp-server/src/tools/whiteboard-format.ts +9 -3
  146. package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
  147. package/neural-interface/i18n/en.json +3953 -228
  148. package/neural-interface/i18n/pt-BR.json +3901 -179
  149. package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
  150. package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
  151. package/neural-interface/lib/antigravity/acp-connection.js +299 -0
  152. package/neural-interface/lib/antigravity/bypass.js +100 -0
  153. package/neural-interface/lib/antigravity/catalog.js +273 -0
  154. package/neural-interface/lib/antigravity/home.js +260 -0
  155. package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
  156. package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
  157. package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
  158. package/neural-interface/lib/antigravity/policy.js +74 -0
  159. package/neural-interface/lib/antigravity/tool-call.js +178 -0
  160. package/neural-interface/lib/antigravity/transcripts.js +252 -0
  161. package/neural-interface/lib/antigravity/usage.js +43 -0
  162. package/neural-interface/lib/assistant-api.js +128 -28
  163. package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
  164. package/neural-interface/lib/assistant-brains/claude.js +4 -3
  165. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
  166. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
  167. package/neural-interface/lib/assistant-brains/codex.js +9 -5
  168. package/neural-interface/lib/assistant-brains/opencode.js +120 -54
  169. package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +28 -13
  170. package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +59 -19
  171. package/neural-interface/lib/assistant-budget-planner.js +7 -3
  172. package/neural-interface/lib/assistant-budget-split.js +42 -15
  173. package/neural-interface/lib/assistant-catalog.js +81 -15
  174. package/neural-interface/lib/assistant-config.js +6 -3
  175. package/neural-interface/lib/assistant-dispatch.js +1073 -200
  176. package/neural-interface/lib/assistant-envelope.js +156 -2
  177. package/neural-interface/lib/assistant-evidence.js +80 -0
  178. package/neural-interface/lib/assistant-hidden-models.js +7 -5
  179. package/neural-interface/lib/assistant-persona.js +93 -19
  180. package/neural-interface/lib/assistant-plan-permissions.js +25 -9
  181. package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
  182. package/neural-interface/lib/assistant-route-gate.js +6 -4
  183. package/neural-interface/lib/assistant-router.js +245 -38
  184. package/neural-interface/lib/assistant-runtime.js +214 -30
  185. package/neural-interface/lib/assistant-task-prompt.js +87 -21
  186. package/neural-interface/lib/assistant-tools.js +51 -0
  187. package/neural-interface/lib/assistant-usage.js +29 -0
  188. package/neural-interface/lib/backup-service.js +626 -4
  189. package/neural-interface/lib/backup-zip-reader.js +10 -5
  190. package/neural-interface/lib/backup-zip-writer.js +257 -0
  191. package/neural-interface/lib/browser-execution.js +10 -2
  192. package/neural-interface/lib/browser-guard-cdp.js +84 -0
  193. package/neural-interface/lib/browser-network-guard.js +476 -0
  194. package/neural-interface/lib/browser-policy-roots.js +64 -0
  195. package/neural-interface/lib/browser-tool-policy.js +198 -38
  196. package/neural-interface/lib/claude-agent-bridge.js +11 -2
  197. package/neural-interface/lib/claude-history-cache.js +210 -0
  198. package/neural-interface/lib/claude-hooks-status.js +103 -0
  199. package/neural-interface/lib/claude-model-catalog.js +5 -1
  200. package/neural-interface/lib/claude-temporary.js +5 -11
  201. package/neural-interface/lib/cli-install-api.js +1 -1
  202. package/neural-interface/lib/cli-install.js +163 -154
  203. package/neural-interface/lib/cli-resolver.js +13 -3
  204. package/neural-interface/lib/cli-update-planner.js +52 -41
  205. package/neural-interface/lib/cloudflared/tunnel.js +42 -2
  206. package/neural-interface/lib/codex-mcp-approvals.js +11 -0
  207. package/neural-interface/lib/codex-rollout-reader.js +52 -0
  208. package/neural-interface/lib/codex-worker-app-server.js +191 -0
  209. package/neural-interface/lib/confined-fs.js +30 -3
  210. package/neural-interface/lib/crash-notice-api.js +98 -0
  211. package/neural-interface/lib/editor-completion-opencode.js +68 -68
  212. package/neural-interface/lib/editor-git-api.js +3 -20
  213. package/neural-interface/lib/exec-loop-command.js +13 -10
  214. package/neural-interface/lib/file-explorer-api.js +74 -40
  215. package/neural-interface/lib/file-explorer-watch.js +9 -3
  216. package/neural-interface/lib/git-bash/install.js +52 -6
  217. package/neural-interface/lib/git-bash/manager.js +59 -17
  218. package/neural-interface/lib/git-bash/pins.js +12 -0
  219. package/neural-interface/lib/git-read.js +2 -2
  220. package/neural-interface/lib/git-status-runner.js +390 -0
  221. package/neural-interface/lib/git-status-watchdog.js +40 -0
  222. package/neural-interface/lib/greeting/budgets.js +1 -0
  223. package/neural-interface/lib/greeting/engine.js +2 -2
  224. package/neural-interface/lib/greeting/sources.js +10 -12
  225. package/neural-interface/lib/greeting/writer.js +1 -1
  226. package/neural-interface/lib/http-guards.js +8 -2
  227. package/neural-interface/lib/install-details.js +4 -1
  228. package/neural-interface/lib/mcp-agent-servers.js +67 -0
  229. package/neural-interface/lib/mcp-client-config.js +35 -33
  230. package/neural-interface/lib/mcp-external-servers.js +332 -0
  231. package/neural-interface/lib/mcp-installer.js +2 -1
  232. package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
  233. package/neural-interface/lib/memory-session-capture.js +17 -3
  234. package/neural-interface/lib/morelogin.js +177 -25
  235. package/neural-interface/lib/native-loop-providers.js +965 -66
  236. package/neural-interface/lib/native-loop-runtime.js +290 -42
  237. package/neural-interface/lib/opencode-config.js +557 -0
  238. package/neural-interface/lib/opencode-db-schema.js +17 -34
  239. package/neural-interface/lib/opencode-history.js +48 -218
  240. package/neural-interface/lib/opencode-mcp-ready.js +108 -0
  241. package/neural-interface/lib/opencode-native-boundary.js +53 -0
  242. package/neural-interface/lib/opencode-protocol.js +249 -112
  243. package/neural-interface/lib/opencode-runtime-config.js +32 -22
  244. package/neural-interface/lib/opencode-serve-records.js +39 -21
  245. package/neural-interface/lib/opencode-v2-client.js +48 -153
  246. package/neural-interface/lib/opencode-v2-ws-requests.js +36 -210
  247. package/neural-interface/lib/opencode-worker-data.js +27 -0
  248. package/neural-interface/lib/opencode2-client.js +73 -37
  249. package/neural-interface/lib/opencode2-proxy.js +23 -63
  250. package/neural-interface/lib/opencode2-translate.js +51 -2
  251. package/neural-interface/lib/owner-authority.js +249 -0
  252. package/neural-interface/lib/page-session.js +275 -0
  253. package/neural-interface/lib/path-confine.js +12 -9
  254. package/neural-interface/lib/peer-identity.js +793 -0
  255. package/neural-interface/lib/pty-host/core.js +28 -1
  256. package/neural-interface/lib/pty-host/lifetime.js +12 -0
  257. package/neural-interface/lib/pty-host/manager.js +12 -1
  258. package/neural-interface/lib/remote-policy.js +1 -1
  259. package/neural-interface/lib/request-gate.js +20 -2
  260. package/neural-interface/lib/rulesets/api.js +7 -6
  261. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  262. package/neural-interface/lib/rulesets/installer.js +180 -36
  263. package/neural-interface/lib/rulesets/json-edit.js +1 -1
  264. package/neural-interface/lib/rulesets/render.js +1 -1
  265. package/neural-interface/lib/session-cache-builder.js +81 -31
  266. package/neural-interface/lib/session-title-generator.js +58 -36
  267. package/neural-interface/lib/skills/api-v2.js +495 -0
  268. package/neural-interface/lib/skills/bundle.js +418 -0
  269. package/neural-interface/lib/skills/catalog.js +298 -0
  270. package/neural-interface/lib/skills/creation-api.js +191 -0
  271. package/neural-interface/lib/skills/creation-brief.js +247 -0
  272. package/neural-interface/lib/skills/creation-sources.js +285 -0
  273. package/neural-interface/lib/skills/creation.js +843 -0
  274. package/neural-interface/lib/skills/deploy.js +569 -0
  275. package/neural-interface/lib/skills/digest.js +119 -0
  276. package/neural-interface/lib/skills/discover.js +111 -0
  277. package/neural-interface/lib/skills/draft-confinement.js +216 -0
  278. package/neural-interface/lib/skills/eval-confinement.js +399 -0
  279. package/neural-interface/lib/skills/eval-sandbox.js +280 -0
  280. package/neural-interface/lib/skills/eval-trace.js +135 -0
  281. package/neural-interface/lib/skills/evals-api.js +212 -0
  282. package/neural-interface/lib/skills/evals.js +1573 -0
  283. package/neural-interface/lib/skills/export.js +203 -0
  284. package/neural-interface/lib/skills/frontmatter.js +64 -0
  285. package/neural-interface/lib/skills/health.js +75 -0
  286. package/neural-interface/lib/skills/json-edit.js +165 -0
  287. package/neural-interface/lib/skills/known-bridge-digests.json +441 -0
  288. package/neural-interface/lib/skills/known-bridges.js +96 -0
  289. package/neural-interface/lib/skills/library.js +823 -0
  290. package/neural-interface/lib/skills/local-store.js +21 -0
  291. package/neural-interface/lib/skills/migration.js +433 -0
  292. package/neural-interface/lib/skills/operations.js +45 -0
  293. package/neural-interface/lib/skills/ownership.js +427 -0
  294. package/neural-interface/lib/skills/packages.js +88 -0
  295. package/neural-interface/lib/skills/reviews.js +212 -0
  296. package/neural-interface/lib/skills/revision.js +80 -0
  297. package/neural-interface/lib/skills/roots.js +269 -0
  298. package/neural-interface/lib/skills/runtime-checks.js +358 -0
  299. package/neural-interface/lib/skills/scan-assets.js +252 -0
  300. package/neural-interface/lib/skills/scan-context.js +26 -0
  301. package/neural-interface/lib/skills/scan-shell.js +324 -0
  302. package/neural-interface/lib/skills/scan.js +364 -0
  303. package/neural-interface/lib/skills/scanners.js +54 -0
  304. package/neural-interface/lib/skills/service.js +530 -0
  305. package/neural-interface/lib/skills/telemetry.js +119 -0
  306. package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
  307. package/neural-interface/lib/synabun-version.js +1 -1
  308. package/neural-interface/lib/system-activity.js +1 -1
  309. package/neural-interface/lib/tool-lists.js +124 -0
  310. package/neural-interface/lib/whatsapp/brain.js +7 -3
  311. package/neural-interface/lib/whatsapp/bridge.js +6 -3
  312. package/neural-interface/lib/whatsapp/cards.js +22 -9
  313. package/neural-interface/lib/whatsapp/config.js +1 -1
  314. package/neural-interface/lib/worker-confinement.js +1268 -0
  315. package/neural-interface/lib/worker-guard-setting.js +56 -0
  316. package/neural-interface/lib/worker-mcp-channel.js +175 -0
  317. package/neural-interface/lib/worker-question.js +74 -0
  318. package/neural-interface/lib/worker-relay-ports.js +227 -0
  319. package/neural-interface/lib/worker-resume.js +238 -0
  320. package/neural-interface/lib/worker-sandbox.js +329 -0
  321. package/neural-interface/package-lock.json +34 -12
  322. package/neural-interface/package.json +3 -2
  323. package/neural-interface/public/index.html +3 -1
  324. package/neural-interface/public/index2d.html +19 -11
  325. package/neural-interface/public/onboarding.html +3 -3
  326. package/neural-interface/public/shared/account-menu.js +8 -5
  327. package/neural-interface/public/shared/agent-runtime-options.js +125 -31
  328. package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
  329. package/neural-interface/public/shared/agy/agy-client.js +266 -0
  330. package/neural-interface/public/shared/agy/agy-commands.js +120 -0
  331. package/neural-interface/public/shared/agy/agy-copy.js +326 -0
  332. package/neural-interface/public/shared/agy/agy-diff.js +161 -0
  333. package/neural-interface/public/shared/agy/agy-events.js +469 -0
  334. package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
  335. package/neural-interface/public/shared/agy/agy-options.js +232 -0
  336. package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
  337. package/neural-interface/public/shared/agy/agy-render.js +392 -0
  338. package/neural-interface/public/shared/agy/agy-setup.js +149 -0
  339. package/neural-interface/public/shared/agy/agy-styles.js +604 -0
  340. package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
  341. package/neural-interface/public/shared/api.js +1527 -1336
  342. package/neural-interface/public/shared/app-frame.js +192 -0
  343. package/neural-interface/public/shared/assistant/asst-agents-dock.js +36 -6
  344. package/neural-interface/public/shared/assistant/asst-api.js +1 -1
  345. package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
  346. package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
  347. package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
  348. package/neural-interface/public/shared/assistant/asst-brain-picker.js +105 -44
  349. package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
  350. package/neural-interface/public/shared/assistant/asst-control.js +205 -26
  351. package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
  352. package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
  353. package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
  354. package/neural-interface/public/shared/assistant/asst-limit-dock.js +45 -0
  355. package/neural-interface/public/shared/assistant/asst-limit-wait.js +327 -0
  356. package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
  357. package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
  358. package/neural-interface/public/shared/assistant/asst-menu.js +7 -6
  359. package/neural-interface/public/shared/assistant/asst-panel.js +151 -19
  360. package/neural-interface/public/shared/assistant/asst-rack-styles.js +66 -49
  361. package/neural-interface/public/shared/assistant/asst-render.js +445 -82
  362. package/neural-interface/public/shared/assistant/asst-route.js +304 -53
  363. package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
  364. package/neural-interface/public/shared/assistant/asst-scenes.js +56 -1
  365. package/neural-interface/public/shared/assistant/asst-sidepanel.js +117 -10
  366. package/neural-interface/public/shared/assistant/asst-slash.js +17 -9
  367. package/neural-interface/public/shared/assistant/asst-state.js +87 -24
  368. package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
  369. package/neural-interface/public/shared/assistant/asst-styles.js +93 -21
  370. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +190 -23
  371. package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
  372. package/neural-interface/public/shared/automation-context.js +5 -2
  373. package/neural-interface/public/shared/cc-dropdown.js +89 -19
  374. package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
  375. package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
  376. package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
  377. package/neural-interface/public/shared/cdx/cdx-output.js +34 -10
  378. package/neural-interface/public/shared/cdx/cdx-panel.js +59 -151
  379. package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
  380. package/neural-interface/public/shared/cdx/cdx-protocol.js +39 -11
  381. package/neural-interface/public/shared/cdx/cdx-render.js +57 -38
  382. package/neural-interface/public/shared/cdx/cdx-requests.js +14 -4
  383. package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
  384. package/neural-interface/public/shared/cdx/cdx-styles.js +15 -3
  385. package/neural-interface/public/shared/cdx/cdx-tabs.js +127 -44
  386. package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
  387. package/neural-interface/public/shared/cli-install-controller.js +218 -0
  388. package/neural-interface/public/shared/cli-install-view.js +11 -12
  389. package/neural-interface/public/shared/cli-status.js +11 -10
  390. package/neural-interface/public/shared/constants.js +5 -4
  391. package/neural-interface/public/shared/context-menu-areas.js +34 -0
  392. package/neural-interface/public/shared/context-menu-logic.js +243 -0
  393. package/neural-interface/public/shared/context-menu-model.js +367 -0
  394. package/neural-interface/public/shared/context-menu.js +372 -0
  395. package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
  396. package/neural-interface/public/shared/cp/cp-restore.js +162 -0
  397. package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
  398. package/neural-interface/public/shared/crash-notice-view.js +168 -0
  399. package/neural-interface/public/shared/desktop-bridge.js +17 -0
  400. package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
  401. package/neural-interface/public/shared/desktop-titlebar.js +13 -4
  402. package/neural-interface/public/shared/desktop-update-view.js +21 -0
  403. package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
  404. package/neural-interface/public/shared/explorer/fx-api.js +4 -4
  405. package/neural-interface/public/shared/explorer/fx-links.js +3 -3
  406. package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
  407. package/neural-interface/public/shared/explorer/fx-main.js +48 -16
  408. package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
  409. package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
  410. package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
  411. package/neural-interface/public/shared/html-shell.js +30 -15
  412. package/neural-interface/public/shared/i18n.js +2 -0
  413. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
  414. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
  415. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
  416. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +2 -0
  417. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
  418. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
  419. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
  420. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
  421. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
  422. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
  423. package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
  424. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +74 -259
  425. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
  426. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +95 -202
  427. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
  428. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +13 -22
  429. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +4 -4
  430. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
  431. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +2 -91
  432. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
  433. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
  434. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
  435. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
  436. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -21
  437. package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
  438. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +60 -50
  439. package/neural-interface/public/shared/page-session.js +96 -0
  440. package/neural-interface/public/shared/provider-icons.js +4 -2
  441. package/neural-interface/public/shared/registry.js +5 -3
  442. package/neural-interface/public/shared/retired-2d.js +3 -0
  443. package/neural-interface/public/shared/schedules-view.js +195 -61
  444. package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
  445. package/neural-interface/public/shared/settings/settings-data.js +3 -1
  446. package/neural-interface/public/shared/settings/settings-ia.js +6 -6
  447. package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
  448. package/neural-interface/public/shared/settings/settings-rules.js +2 -2
  449. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
  450. package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
  451. package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
  452. package/neural-interface/public/shared/showcase-privacy.js +6 -1
  453. package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
  454. package/neural-interface/public/shared/skills/ss-args.js +96 -0
  455. package/neural-interface/public/shared/skills/ss-creation.js +222 -0
  456. package/neural-interface/public/shared/skills/ss-dialog.js +133 -0
  457. package/neural-interface/public/shared/skills/ss-doc.js +173 -0
  458. package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
  459. package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
  460. package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
  461. package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
  462. package/neural-interface/public/shared/skills/ss-evals.js +417 -0
  463. package/neural-interface/public/shared/skills/ss-form.js +159 -0
  464. package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
  465. package/neural-interface/public/shared/skills/ss-ids.js +32 -0
  466. package/neural-interface/public/shared/skills/ss-lint.js +149 -0
  467. package/neural-interface/public/shared/skills/ss-presets.js +117 -0
  468. package/neural-interface/public/shared/skills/ss-profiles.js +513 -0
  469. package/neural-interface/public/shared/skills/ss-resources.js +165 -0
  470. package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
  471. package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
  472. package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
  473. package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
  474. package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
  475. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
  476. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +237 -0
  477. package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
  478. package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
  479. package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
  480. package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
  481. package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
  482. package/neural-interface/public/shared/skills/studio/sx-dom.js +230 -0
  483. package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
  484. package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
  485. package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
  486. package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
  487. package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
  488. package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
  489. package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
  490. package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
  491. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +282 -0
  492. package/neural-interface/public/shared/skills/studio/sx-install.js +313 -0
  493. package/neural-interface/public/shared/skills/studio/sx-library-view.js +748 -0
  494. package/neural-interface/public/shared/skills/studio/sx-library.js +479 -0
  495. package/neural-interface/public/shared/skills/studio/sx-main.js +690 -0
  496. package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
  497. package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
  498. package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
  499. package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
  500. package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
  501. package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
  502. package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
  503. package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
  504. package/neural-interface/public/shared/skills/studio/sx-subject.js +249 -0
  505. package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
  506. package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
  507. package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
  508. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +426 -0
  509. package/neural-interface/public/shared/state.js +1 -1
  510. package/neural-interface/public/shared/streaming-markdown.js +58 -0
  511. package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
  512. package/neural-interface/public/shared/styles.css +1822 -1756
  513. package/neural-interface/public/shared/synabun-mascot.js +1115 -9
  514. package/neural-interface/public/shared/text-cache.js +29 -0
  515. package/neural-interface/public/shared/tool-list-picker.js +55 -0
  516. package/neural-interface/public/shared/tool-list-storage.js +24 -0
  517. package/neural-interface/public/shared/tool-list-view.js +112 -0
  518. package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
  519. package/neural-interface/public/shared/tour/tour-actor.js +268 -0
  520. package/neural-interface/public/shared/tour/tour-beats.js +336 -0
  521. package/neural-interface/public/shared/tour/tour-engine.js +884 -0
  522. package/neural-interface/public/shared/tour/tour-ink.js +390 -0
  523. package/neural-interface/public/shared/tour/tour-stage.js +440 -0
  524. package/neural-interface/public/shared/tour/tour-state.js +40 -0
  525. package/neural-interface/public/shared/tour/tour-tips.js +99 -0
  526. package/neural-interface/public/shared/tour/tour.css +144 -0
  527. package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
  528. package/neural-interface/public/shared/ui-assistant.js +2 -0
  529. package/neural-interface/public/shared/ui-automation-studio.js +111 -140
  530. package/neural-interface/public/shared/ui-claude-panel.js +376 -82
  531. package/neural-interface/public/shared/ui-cli-banner.js +23 -167
  532. package/neural-interface/public/shared/ui-command-runner.js +6 -3
  533. package/neural-interface/public/shared/ui-context-menu.js +295 -0
  534. package/neural-interface/public/shared/ui-cost-widget.js +57 -59
  535. package/neural-interface/public/shared/ui-crash-notice.js +187 -0
  536. package/neural-interface/public/shared/ui-detail.js +15 -3
  537. package/neural-interface/public/shared/ui-file-explorer.js +48 -6
  538. package/neural-interface/public/shared/ui-file-links.js +2 -2
  539. package/neural-interface/public/shared/ui-games.js +2 -2
  540. package/neural-interface/public/shared/ui-image-gallery.js +50 -47
  541. package/neural-interface/public/shared/ui-judgments.js +1 -1
  542. package/neural-interface/public/shared/ui-keybinds.js +33 -2
  543. package/neural-interface/public/shared/ui-link.js +3 -3
  544. package/neural-interface/public/shared/ui-menubar.js +10 -20
  545. package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
  546. package/neural-interface/public/shared/ui-native-loop-router.js +11 -1
  547. package/neural-interface/public/shared/ui-navbar.js +49 -24
  548. package/neural-interface/public/shared/ui-notifications.js +1 -0
  549. package/neural-interface/public/shared/ui-panels.js +96 -65
  550. package/neural-interface/public/shared/ui-resume.js +9 -7
  551. package/neural-interface/public/shared/ui-schedules-studio.js +546 -431
  552. package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
  553. package/neural-interface/public/shared/ui-sessions.js +27 -15
  554. package/neural-interface/public/shared/ui-settings.js +683 -526
  555. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
  556. package/neural-interface/public/shared/ui-sidepanel-sessions.js +4 -4
  557. package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
  558. package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
  559. package/neural-interface/public/shared/ui-sidepanel-windows.js +18 -13
  560. package/neural-interface/public/shared/ui-skills.js +33 -2830
  561. package/neural-interface/public/shared/ui-styleguide.js +5 -2
  562. package/neural-interface/public/shared/ui-sync.js +15 -5
  563. package/neural-interface/public/shared/ui-terminal.js +270 -190
  564. package/neural-interface/public/shared/ui-tool-updates.js +6 -4
  565. package/neural-interface/public/shared/ui-tooltip.js +81 -51
  566. package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
  567. package/neural-interface/public/shared/ui-trash.js +6 -4
  568. package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
  569. package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
  570. package/neural-interface/public/shared/ui-tutorial-steps.js +3 -65
  571. package/neural-interface/public/shared/ui-tutorial.css +47 -100
  572. package/neural-interface/public/shared/ui-tutorial.js +285 -509
  573. package/neural-interface/public/shared/ui-update.js +38 -2
  574. package/neural-interface/public/shared/ui-whiteboard.js +312 -59
  575. package/neural-interface/public/shared/ui-worker-end.js +5 -1
  576. package/neural-interface/public/shared/ui-workspaces.js +3 -2
  577. package/neural-interface/public/shared/view-variant.js +66 -0
  578. package/neural-interface/public/shared/whatsapp/wa-view.js +3 -3
  579. package/neural-interface/public/shared/whiteboard-list.js +172 -0
  580. package/neural-interface/public/shared/worker-end.js +10 -3
  581. package/neural-interface/public/shared/worker-questions.js +34 -0
  582. package/neural-interface/public/shared/worker-restart.js +12 -0
  583. package/neural-interface/public/sidepanel-session.html +2 -2
  584. package/neural-interface/public/variant/2d/background.js +2 -1
  585. package/neural-interface/public/variant/2d/context-menu.js +8 -2
  586. package/neural-interface/public/variant/2d/graph.js +42 -20
  587. package/neural-interface/public/variant/2d/hulls.js +14 -9
  588. package/neural-interface/public/variant/2d/lasso.js +11 -7
  589. package/neural-interface/public/variant/2d/main.js +32 -1
  590. package/neural-interface/public/variant/2d/minimap.js +11 -13
  591. package/neural-interface/public/variant/3d/main.js +69 -6
  592. package/neural-interface/public/variant/3d/map-controls.js +24 -9
  593. package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
  594. package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
  595. package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
  596. package/neural-interface/public/vendor/caveat/caveat.css +17 -0
  597. package/neural-interface/public/vendor/caveat/manifest.json +12 -0
  598. package/neural-interface/public/vendor/yaml/LICENSE +13 -0
  599. package/neural-interface/public/vendor/yaml/manifest.json +4 -0
  600. package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
  601. package/neural-interface/scripts/build-vendor.mjs +32 -0
  602. package/neural-interface/server.js +4553 -2855
  603. package/neural-interface/templates/rulesets/README.md +1 -1
  604. package/neural-interface/templates/rulesets/core.md +1 -1
  605. package/neural-interface/templates/rulesets/legacy-hashes.json +23 -5
  606. package/neural-interface/templates/rulesets/manifest.json +8 -8
  607. package/npm-shrinkwrap.json +34 -12
  608. package/package.json +17 -3
  609. package/postinstall.js +9 -4
  610. package/preuninstall.js +1 -1
  611. package/setup.js +97 -322
  612. package/skills/skill-forge/SKILL.md +177 -0
  613. package/skills/skill-forge/evals/evals.json +54 -0
  614. package/skills/skill-forge/references/package.md +105 -0
  615. package/skills/skill-forge/references/review-and-recovery.md +63 -0
  616. package/skills/skill-forge/references/runtime-profiles.md +20 -0
  617. package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
  618. package/skills/synabun/SKILL.md +10 -7
  619. package/skills/synabun/modules/audit.md +1 -5
  620. package/skills/synabun/modules/changelog.md +1 -5
  621. package/skills/synabun/modules/idea.md +1 -1
  622. package/skills/synabun/modules/memorize.md +6 -9
  623. package/skills/synabun/modules/schedule.md +6 -6
  624. package/mcp-server/dist/services/profiles.d.ts +0 -120
  625. package/mcp-server/dist/services/profiles.d.ts.map +0 -1
  626. package/mcp-server/dist/services/profiles.js +0 -387
  627. package/mcp-server/dist/services/profiles.js.map +0 -1
  628. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
  629. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
  630. package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
  631. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
  632. package/mcp-server/dist/tools/leonardo.d.ts +0 -13
  633. package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
  634. package/mcp-server/dist/tools/leonardo.js +0 -21
  635. package/mcp-server/dist/tools/leonardo.js.map +0 -1
  636. package/mcp-server/dist/tools/profile.d.ts +0 -17
  637. package/mcp-server/dist/tools/profile.d.ts.map +0 -1
  638. package/mcp-server/dist/tools/profile.js +0 -114
  639. package/mcp-server/dist/tools/profile.js.map +0 -1
  640. package/mcp-server/src/services/profiles.ts +0 -428
  641. package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
  642. package/mcp-server/src/tools/leonardo.ts +0 -30
  643. package/mcp-server/src/tools/profile.ts +0 -122
  644. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
  645. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
  646. package/neural-interface/lib/mcp-profile-servers.js +0 -120
  647. package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
  648. package/skills/leonardo/SKILL.md +0 -168
  649. package/skills/leonardo/icon.svg +0 -1
  650. package/skills/leonardo/modules/browser-guide.md +0 -378
  651. package/skills/leonardo/modules/image-prompter.md +0 -283
  652. package/skills/leonardo/modules/model-advisor.md +0 -100
  653. package/skills/leonardo/modules/prompt-library.md +0 -110
  654. package/skills/leonardo/modules/style-guide.md +0 -186
  655. package/skills/leonardo/modules/video-prompter.md +0 -317
  656. /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
@@ -0,0 +1,238 @@
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', 'antigravity'].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' : entry.provider === 'antigravity' ? 'Antigravity 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
+ // Antigravity keeps the conversation in the run's own home (lib/antigravity/home.js): a run started before homes
75
+ // existed has its session in the user's own ~/.gemini, which a resume does not use.
76
+ if (entry.provider === 'antigravity' && !entry.resumeInfo?.antigravityHome) return 'its Antigravity session has no SynaBun home to resume from';
77
+ const resumes = Number(entry.resumes) || 0;
78
+ if (resumes >= MAX_RESUMES) return `it was resumed ${resumes} times already`;
79
+ if (!(workLeftMs > 0)) return entry.maxMinutes ? `its ${entry.maxMinutes} minutes of work time are used up` : 'its work time is used up';
80
+ if (atShutdown) return null;
81
+ const since = Date.parse(entry.suspendedAt || '') || Date.parse(entry.updatedAt || '') || 0;
82
+ 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)})`;
83
+ if (!entry.cwd || !isDirectory(entry.cwd)) return `its working directory is gone (${entry.cwd || 'none'})`;
84
+ let unusable = '';
85
+ // OpenCode and Antigravity sign in inside their own CLI: SynaBun keeps no account of theirs.
86
+ try { unusable = entry.provider === 'opencode' || entry.provider === 'antigravity' ? '' : String(accountUnusable((entry.provider === 'codex' ? entry.codexAccountId : entry.claudeAccountId) || 'default', entry.provider, entry) || ''); } catch (error) { unusable = error?.message || 'it could not be read'; }
87
+ if (unusable) return `its ${entry.provider === 'codex' ? 'Codex' : 'Claude'} account (${entry.codexAccountId || entry.claudeAccountId || 'default'}) cannot be used: ${unusable}`;
88
+ return null;
89
+ }
90
+
91
+ /** Whether `path` is `root` or inside it (both resolved; a missing path is compared as written). */
92
+ export function isInside(path, root) {
93
+ if (!path || !root) return false;
94
+ const real = (value) => { try { return realpathSync(value); } catch { return resolve(String(value)); } };
95
+ const rel = relative(real(root), real(path));
96
+ return rel === '' || (!!rel && !rel.startsWith('..') && !isAbsolute(rel));
97
+ }
98
+
99
+ /** "14:05": a time of day on this computer's clock, or '' for no time. */
100
+ export function clockTime(value) {
101
+ const ms = typeof value === 'number' ? value : Date.parse(value || '');
102
+ if (!Number.isFinite(ms) || ms <= 0) return '';
103
+ const date = new Date(ms);
104
+ return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
105
+ }
106
+
107
+ /**
108
+ * The transcript of a Claude session: <config dir>/projects/<project folder>/<sessionId>.jsonl, the config dir being
109
+ * the run's account's (CLAUDE_CONFIG_DIR) or ~/.claude. Every project folder is looked in (the CLI names it after
110
+ * the real path of the working directory). null when there is none.
111
+ */
112
+ export function findClaudeTranscript({ sessionId, configDir = null, home = homedir() } = {}) {
113
+ const id = String(sessionId || '');
114
+ if (!/^[0-9a-f-]{8,64}$/i.test(id)) return null;
115
+ const projects = join(configDir || join(home, '.claude'), 'projects');
116
+ let folders = [];
117
+ try { folders = readdirSync(projects, { withFileTypes: true }).filter((item) => item.isDirectory()).map((item) => item.name); } catch { return null; }
118
+ for (const folder of folders) {
119
+ const path = join(projects, folder, `${id}.jsonl`);
120
+ try { if (statSync(path).isFile()) return path; } catch { /* not here */ }
121
+ }
122
+ return null;
123
+ }
124
+
125
+ /** The last `maxBytes` of a file as text (a partial first line is dropped). */
126
+ function readTail(path, maxBytes = 1024 * 1024) {
127
+ let fd = null;
128
+ try {
129
+ fd = openSync(path, 'r');
130
+ const size = fstatSync(fd).size;
131
+ const length = Math.min(size, maxBytes);
132
+ const buffer = Buffer.alloc(length);
133
+ readSync(fd, buffer, 0, length, size - length);
134
+ const text = buffer.toString('utf8');
135
+ return size > length ? text.slice(text.indexOf('\n') + 1) : text;
136
+ } catch { return ''; } finally { if (fd !== null) { try { closeSync(fd); } catch { /* closed */ } } }
137
+ }
138
+
139
+ /**
140
+ * The cost state a resumed CLI starts from: the last `cost-state` line of the session's transcript, which the CLI
141
+ * writes when it exits cleanly (a SIGTERM included) and reads back when the session is resumed. →
142
+ * { totalCostUsd, modelUsage } (modelUsage as result.modelUsage spells it), or null when there is none (a CLI that
143
+ * was killed outright starts its counters at zero).
144
+ */
145
+ export function readCostState(path, sessionId = null) {
146
+ if (!path || !existsSync(path)) return null;
147
+ const lines = readTail(path).split('\n');
148
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
149
+ const line = lines[i];
150
+ if (!line.includes('"cost-state"')) continue;
151
+ let row = null;
152
+ try { row = JSON.parse(line); } catch { continue; }
153
+ if (row?.type !== 'cost-state') continue;
154
+ if (sessionId && row.sessionId && row.sessionId !== sessionId) continue;
155
+ const total = Number(row.totalCostUSD);
156
+ return {
157
+ totalCostUsd: Number.isFinite(total) && total > 0 ? total : 0,
158
+ modelUsage: row.modelUsage && typeof row.modelUsage === 'object' && !Array.isArray(row.modelUsage) ? row.modelUsage : null,
159
+ };
160
+ }
161
+ return null;
162
+ }
163
+
164
+ /** Whether a process with this pid exists (one of another user's counts as existing). */
165
+ export function processExists(pid) {
166
+ const id = Number(pid);
167
+ if (!Number.isInteger(id) || id <= 0) return false;
168
+ try { process.kill(id, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
169
+ }
170
+
171
+ /** The command line of a process, or null when it cannot be read (no such process, no `ps`, Windows without CIM). */
172
+ export function processCommandLine(pid, { platform = process.platform, run = execFileSync } = {}) {
173
+ const id = Number(pid);
174
+ if (!Number.isInteger(id) || id <= 0) return null;
175
+ try {
176
+ if (platform === 'win32') {
177
+ 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'] });
178
+ return String(out || '').trim() || null;
179
+ }
180
+ const out = run('ps', ['-o', 'command=', '-p', String(id)], { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'] });
181
+ return String(out || '').trim() || null;
182
+ } catch { return null; }
183
+ }
184
+
185
+ /**
186
+ * 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
187
+ * headers (X-Synabun-Terminal). Anything else with the pid is left alone.
188
+ */
189
+ export function isRunCliCommand(command, runId) {
190
+ const text = String(command || '');
191
+ return !!runId && text.includes(String(runId)) && (
192
+ (text.includes('--input-format') && text.includes('stream-json'))
193
+ || (text.includes('app-server') && text.includes('synabun_worker_run='))
194
+ // Antigravity: `agy acp --gemini-dir <data home>/runtime/antigravity/homes/run-<run id>-…` (lib/antigravity/home.js).
195
+ || (/\sacp(?:\s|$)/.test(text) && text.includes('--gemini-dir') && /[\\/]antigravity[\\/]homes[\\/]run-/.test(text))
196
+ );
197
+ }
198
+
199
+ /** Windows: end a process and every process it started (taskkill /T /F, as the server's own shutdown does). */
200
+ export function endProcessTree(pid, { run = execFileSync } = {}) {
201
+ run('taskkill', ['/pid', String(pid), '/T', '/F'], { timeout: 5000, windowsHide: true, stdio: 'ignore' });
202
+ }
203
+
204
+ /**
205
+ * End the CLI a run had before the server went down, when it is still there (a crash ended no process). It is ended
206
+ * only when its command line shows it is that run's CLI: SIGTERM, then SIGKILL after `graceMs` once the pid still
207
+ * shows that command line (it may have gone to another process meanwhile). Windows has no SIGTERM (a signal stops the
208
+ * CLI alone and leaves its tool's processes, a dev server keeps its port): the tree is ended with taskkill.
209
+ * → { state: 'gone' | 'ended' | 'killed' | 'still_running' | 'not_ours' | 'unknown' }.
210
+ * 'unknown': a process has the pid but its command line cannot be read, so it is not touched.
211
+ */
212
+ 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 } = {}) {
213
+ const id = Number(pid);
214
+ if (!Number.isInteger(id) || id <= 0 || id === process.pid || !exists(id)) return { state: 'gone' };
215
+ const command = readCommand(id);
216
+ // No command line: a process that exited meanwhile is gone; one still there is not touched.
217
+ if (command === null) return { state: exists(id) ? 'unknown' : 'gone' };
218
+ if (!isRunCliCommand(command, runId)) return { state: 'not_ours' };
219
+ if (platform === 'win32') {
220
+ try { killTree(id); } catch { /* gone meanwhile, or not allowed: checked below */ }
221
+ for (let waited = 0; waited < graceMs; waited += 100) {
222
+ await sleep(100);
223
+ if (!exists(id)) return { state: 'killed' };
224
+ }
225
+ return { state: 'still_running' };
226
+ }
227
+ try { kill(id, 'SIGTERM'); } catch { /* gone meanwhile */ }
228
+ for (let waited = 0; waited < graceMs; waited += 100) {
229
+ await sleep(100);
230
+ if (!exists(id)) return { state: 'ended' };
231
+ }
232
+ const again = readCommand(id);
233
+ if (again === null) return { state: exists(id) ? 'still_running' : 'ended' };
234
+ if (!isRunCliCommand(again, runId)) return { state: 'ended' };
235
+ try { kill(id, 'SIGKILL'); } catch { /* gone meanwhile */ }
236
+ await sleep(200);
237
+ return { state: exists(id) ? 'still_running' : 'killed' };
238
+ }
@@ -0,0 +1,329 @@
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, with or without a CIDR suffix, is rejected by the parser: "host must be
23
+ // * or localhost in network address"); `*` is the only form that is address-proof.
24
+ // Seatbelt has no port-range syntax (`*:4120-4400` is rejected), so a reserved range is emitted one port a line.
25
+ // • `(deny file-read* file-write* (subpath "<canonical>"))` blocks reads AND writes under a path; a later
26
+ // `(allow file-read* file-write* (subpath "<canonical>"))` wins (Seatbelt is last-match-wins within a filter),
27
+ // so SynaBun's whole data home can be denied and the run's own scratch / cwd / config folder allowed back after
28
+ // it. Seatbelt matches the RESOLVED path, so every literal must be canonicalised (/tmp is a symlink to
29
+ // /private/tmp; an un-resolved literal never matches, and an allow-back must resolve the same way as its deny).
30
+ // • macOS 27 (27.2 / 26B123, confirmed with scratch sandbox-exec probes outside the repo, 2026-11-01): a
31
+ // `(deny network-outbound (remote ip …))` rule placed textually AFTER an `(allow network-outbound (remote ip
32
+ // …))` rule silently defeats that allow — even when the deny's port is completely unrelated to the allow's
33
+ // (e.g. a later `deny *:4096` broke an earlier `allow localhost:9` for a different, unrelated port 9). This
34
+ // held on every literal form tried (`localhost` and `*` alike), so it is not about the host literal at all —
35
+ // it is purely textual position. A deny is never undone by what comes after it, and a later allow for one port
36
+ // never reopens a different, already-denied port — only "deny AFTER allow, different port" misbehaves. A
37
+ // same-port pair (a broad `*:<port>` allow immediately followed by a narrower `localhost:<port>` deny, the
38
+ // INTERNET_PORTS local-redeny below) still narrows correctly on 27.2: last-match-wins for an overlapping
39
+ // filter is unaffected, confirmed by direct probe. The fix is purely about ORDER, so buildWorkerSandboxProfile
40
+ // emits every plain port/range deny FIRST, then the strict profile's own allow-then-narrow pair for
41
+ // INTERNET_PORTS, then the worker-channel allow(s) dead last among network-outbound rules — nothing (deny)
42
+ // ever follows them, on macOS 26 or 27. This does not widen anything: the channel allow keeps the narrow
43
+ // `localhost:<port>` literal (still local-only on both macOS versions), and the reorder also fixes a second,
44
+ // previously-undetected collateral regression — the strict profile's own INTERNET_PORTS allow-back (meant to
45
+ // keep DNS/SSH/HTTP/HTTPS reachable on real remote hosts) was equally defeated by the later control-port and
46
+ // reserved-range denies that used to sit between it and nothing; a guarded worker in the strict fallback had
47
+ // silently lost the public internet too.
48
+ // • The sandbox is inherited by detached / nohup grandchildren, so a worker cannot escape it by backgrounding.
49
+ // • A same-user `KERN_PROCARGS2` read (ps -E / a tiny C program) CANNOT be blocked by Seatbelt (it is a same-uid
50
+ // kernel allowance, not a sandbox-governed operation). So a worker inside the sandbox can still read another
51
+ // same-user process's argv+env — including a brain's SYNABUN_BRAIN_TOKEN, or a relay's password in env. That
52
+ // theft is harmless here because the token/password is useless without reaching a port this profile blocks on
53
+ // every address; a Codex worker (which this module cannot wrap — nested sandbox-exec is refused) is held by
54
+ // running with its network off instead.
55
+ //
56
+ // This module is macOS-only. On every other platform sandboxSupported() is false; and even on macOS the caller
57
+ // gates on sandboxExecAvailable() (a one-time boot probe that /usr/bin/sandbox-exec exists and runs a trivial
58
+ // allow-default profile), falling back to the in-code guard alone when it does not. Pure but for canonicalising
59
+ // paths, the boot probe, and writing the profile / wrapper into a run's own scratch folder; unit-tested in
60
+ // tests/worker-sandbox.test.mjs.
61
+ //
62
+ // NOT EQUIVALENT OFF macOS (stated plainly; HIGH elsewhere). Where this module does not apply — Linux, Windows, or a
63
+ // macOS host whose sandbox-exec is unavailable — a protected (Enforce, read-only/workspace) worker is held only by the
64
+ // in-code guard (the shell heuristic + the tool hooks), which reads the command line only and so is a lexical
65
+ // speed-bump, not a boundary: a script file (`node /path/x.js`), hidden-address inline code, or any program doing its
66
+ // own networking slips past it. Two things the macOS profile closes that nothing else does there: (a) SynaBun's
67
+ // OpenCode serve band 4096-4400 is unblocked, so a worker's script can connect to a serve; (b) a SHARED (non-protected)
68
+ // serve keeps its OPENCODE_PASSWORD in its process environment (lib/opencode-protocol.js serveSpecFor env mode; only a
69
+ // protected worker's OWN isolated serve hides it via passwordFrom 'stdout'), and a same-uid process can read another
70
+ // process's environment (KERN_PROCARGS2 / /proc/<pid>/environ) — a read no sandbox blocks — so a prompt-injected worker
71
+ // off macOS could read that password and open a full-control session on the shared serve. On macOS under Enforce the
72
+ // 4096-4400 deny (plus the data-home/credential denies below) is the real boundary that stops both. The protected-
73
+ // worker IN-FOLDER confinement off macOS is therefore best-effort, not a guarantee. See docs/assistant.md → Worker
74
+ // confinement → "Protected-worker in-folder confinement off macOS is NOT equivalent to macOS".
75
+
76
+ import { chmodSync, existsSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs';
77
+ import { spawnSync } from 'node:child_process';
78
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
79
+
80
+ export const SANDBOX_EXEC = '/usr/bin/sandbox-exec';
81
+ /**
82
+ * The ports the internet needs (SSH, DNS, HTTP, HTTPS). A relay found listening on one of them is denied on this
83
+ * computer's own addresses only (`localhost:<port>`: 127.0.0.1, ::1, every interface address and the hostname —
84
+ * not the IPv4-mapped spelling, which only `*` covers), never with `*:<port>`, which would cut the worker's internet.
85
+ * The strict profile (detection failed) allows exactly these, plus the worker channel.
86
+ */
87
+ export const INTERNET_PORTS = Object.freeze([22, 53, 80, 443]);
88
+ /** Local ports no guarded worker has a reason to open: SSH to this computer (Remote Login) is a shell outside the
89
+ * sandbox. Denied on this computer's own addresses in every guarded profile; ssh to other hosts is untouched. */
90
+ export const ALWAYS_LOCAL_DENY_PORTS = Object.freeze([22]);
91
+
92
+ /** macOS only: the platform where the Seatbelt primitives above hold. A cheap pure check for tests and early gates. */
93
+ export function sandboxSupported(platform = process.platform) {
94
+ return platform === 'darwin';
95
+ }
96
+
97
+ // Whether /usr/bin/sandbox-exec exists AND runs a trivial allow-default profile: probed once (cheap, ~15 ms) and
98
+ // cached, so a host where Seatbelt is missing or broken falls back to the in-code guard instead of failing every
99
+ // guarded worker. null = not yet probed.
100
+ let _sandboxExecProbe = null;
101
+ /**
102
+ * macOS AND sandbox-exec works. Cached after the first probe. The caller (server.js) gates OS isolation on this,
103
+ * never on sandboxSupported() alone, so a missing / broken sandbox-exec is a visible fall-back, not a launch failure.
104
+ */
105
+ export function sandboxExecAvailable({ platform = process.platform, spawn = spawnSync, force = false } = {}) {
106
+ if (!sandboxSupported(platform)) return false;
107
+ if (_sandboxExecProbe !== null && !force) return _sandboxExecProbe;
108
+ let ok = false;
109
+ try {
110
+ if (existsSync(SANDBOX_EXEC)) {
111
+ const r = spawn(SANDBOX_EXEC, ['-p', '(version 1)(allow default)', '/usr/bin/true'], { timeout: 5000, stdio: 'ignore' });
112
+ ok = !r.error && r.status === 0 && !r.signal;
113
+ }
114
+ } catch { ok = false; }
115
+ _sandboxExecProbe = ok;
116
+ return ok;
117
+ }
118
+ /** Tests only: forget the cached probe result. */
119
+ export function _resetSandboxExecProbe() { _sandboxExecProbe = null; }
120
+
121
+ /** A Seatbelt string literal: double-quoted, with `\` and `"` escaped. */
122
+ function sexpr(text) {
123
+ return `"${String(text).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
124
+ }
125
+
126
+ /** Where a path really is (Seatbelt matches the resolved path); the deepest existing ancestor resolved, rest as named. */
127
+ export function canonicalPath(abs) {
128
+ let probe = isAbsolute(abs) ? abs : resolve(abs);
129
+ const rest = [];
130
+ for (let i = 0; i < 256; i++) {
131
+ try { const real = realpathSync(probe); return rest.length ? join(real, ...rest.reverse()) : real; }
132
+ catch { const parent = dirname(probe); if (parent === probe) return resolve(abs); rest.push(probe.slice(parent.length).replace(/^[/\\]+/, '')); probe = parent; }
133
+ }
134
+ return resolve(abs);
135
+ }
136
+
137
+ /** Valid, deduped, sorted ports from a list. */
138
+ function cleanPorts(ports) {
139
+ return [...new Set((ports || []).map(Number).filter((p) => Number.isInteger(p) && p > 0 && p < 65536))].sort((a, b) => a - b);
140
+ }
141
+
142
+ /**
143
+ * The ancestor DIRECTORIES of each allow-back that fall inside a denied tree. `(deny file-read* (subpath X))` denies
144
+ * metadata too, so when a run's scratch folder is allowed back under a denied data home, node cannot even `lstat`
145
+ * the denied ancestors (/dataHome, /dataHome/data, /dataHome/data/scratch) to resolve a path there — it fails EPERM
146
+ * before it runs a script. Granting `file-read-metadata` on those exact directory nodes (a literal, not a subpath:
147
+ * the directory itself, never its contents) lets the lookup through without exposing any file in the denied tree.
148
+ * Canonical in, canonical out (the allow paths are already canonical, so their dirname chain is too).
149
+ */
150
+ function metadataAncestors(allowPaths, denyPaths) {
151
+ const denies = denyPaths.map((p) => (p.endsWith('/') ? p.slice(0, -1) : p)).filter(Boolean);
152
+ const insideADeny = (dir) => denies.some((d) => dir === d || dir.startsWith(d + '/'));
153
+ const out = new Set();
154
+ for (const allow of allowPaths) {
155
+ let dir = dirname(allow);
156
+ for (let i = 0; i < 256; i++) {
157
+ if (insideADeny(dir)) out.add(dir);
158
+ const parent = dirname(dir);
159
+ if (parent === dir) break;
160
+ dir = parent;
161
+ }
162
+ }
163
+ return [...out];
164
+ }
165
+
166
+ /**
167
+ * The Seatbelt profile for a guarded worker tree.
168
+ * @param {object} o
169
+ * @param {number[]} o.denyPorts individual ports to block OUTBOUND to, on EVERY host (NEURAL_PORT + its WebSockets,
170
+ * the launch beacon, the shared OpenCode serve). The worker-only MCP channel port is
171
+ * NOT here — the profile must leave it open.
172
+ * @param {{from:number,to:number}} [o.denyPortRange] a reserved range (SynaBun's isolated OpenCode serves) blocked
173
+ * in FULL, so a relay started after this profile is written is already denied. Each
174
+ * port in it is a line (Seatbelt has no range syntax).
175
+ * @param {string[]} [o.denyPaths] folders / files whose reads AND writes are denied (SynaBun's whole data home, the
176
+ * other runtimes' credential files this run does not use). Each is canonicalised.
177
+ * @param {string[]} [o.allowPaths] folders allowed back AFTER the denies (the run's own scratch / cwd / config), so
178
+ * a path under a denied tree the run truly needs survives. Canonicalised; placed last
179
+ * (last match wins).
180
+ * @param {number[]} [o.denyLocalPorts] ports blocked OUTBOUND on this computer's own addresses only (`localhost:<port>`):
181
+ * a relay found on an internet port (22, 53, 80, 443), and local SSH.
182
+ * @param {{ allowPorts:number[] }} [o.strictLocal] relay detection FAILED for this run: no outbound connection at all
183
+ * but the internet's ports (INTERNET_PORTS, on other hosts) and the worker channel
184
+ * (`allowPorts`, on this computer). `*:*` is the only rule that covers every local
185
+ * spelling (the IPv4-mapped one included); Seatbelt's `localhost` misses [::ffff:…],
186
+ * and mixing `ip6` rules with `localhost` ones silently re-opens IPv4 loopback (verified
187
+ * on macOS 26), so this is written as deny-all-then-allow, never as a loopback deny.
188
+ * On macOS 27 a later deny for an unrelated port defeats an earlier allow (see the
189
+ * module header), so every network-outbound ALLOW rule this emits — the strict
190
+ * profile's INTERNET_PORTS allow-back and the channel allow(s) — is placed after
191
+ * every plain port/range deny, with the channel allow(s) last of all.
192
+ * @param {string} [o.note] a short comment line (the run id), for readability in a log.
193
+ * @returns {string} the profile text (ends with a newline).
194
+ */
195
+ export function buildWorkerSandboxProfile({ denyPorts = [], denyLocalPorts = [], denyPortRange = null, denyPaths = [], allowPaths = [], note = '', strictLocal = null } = {}) {
196
+ const localOnly = new Set(cleanPorts(denyLocalPorts));
197
+ // A port the internet needs is never denied on every host: it goes to the local-only list instead.
198
+ const ports = cleanPorts(denyPorts).filter((p) => { if (INTERNET_PORTS.includes(p)) { localOnly.add(p); return false; } return true; });
199
+ const range = denyPortRange && Number.isInteger(Number(denyPortRange.from)) && Number.isInteger(Number(denyPortRange.to))
200
+ ? { from: Math.max(1, Number(denyPortRange.from)), to: Math.min(65535, Number(denyPortRange.to)) } : null;
201
+ const denies = [...new Set((denyPaths || []).filter((p) => typeof p === 'string' && p).map(canonicalPath))];
202
+ const allows = [...new Set((allowPaths || []).filter((p) => typeof p === 'string' && p).map(canonicalPath))];
203
+ const channelPorts = strictLocal ? cleanPorts(strictLocal.allowPorts) : [];
204
+ const lines = ['(version 1)'];
205
+ if (note) lines.push(`; ${String(note).replace(/[\r\n]+/g, ' ').slice(0, 200)}`);
206
+ lines.push('(allow default)');
207
+ if (strictLocal) {
208
+ // Detection failed: nothing local is known to be safe, so nothing local is reachable but the worker channel.
209
+ lines.push('; strict: the relay detection failed for this run, so no local port is reachable but the worker channel');
210
+ lines.push(`(deny network-outbound (remote ip ${sexpr('*:*')}))`);
211
+ }
212
+ // The control ports and the reserved relay range FIRST, before any allow rule below: on macOS 27 a later deny
213
+ // here would defeat an earlier allow for an unrelated port (module header). A plain deny is never undone by
214
+ // what follows it, so their own position relative to each other never matters.
215
+ const seen = new Set();
216
+ for (const port of ports) { seen.add(port); lines.push(`(deny network-outbound (remote ip ${sexpr(`*:${port}`)}))`); }
217
+ // The reserved relay range in full: every isolated OpenCode serve, whether it started before or after this profile.
218
+ 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}`)}))`); } }
219
+ if (strictLocal) {
220
+ // Allow the internet's ports back on every host, then narrow them to this computer's own addresses right
221
+ // after (same port: last-match-wins narrows it locally, unaffected by the macOS 27 ordering issue above,
222
+ // which is about an unrelated port's deny, not a same-port narrowing pair).
223
+ for (const port of INTERNET_PORTS) if (port !== 53) localOnly.add(port); // a local DNS resolver stays reachable
224
+ for (const port of channelPorts) localOnly.delete(port);
225
+ for (const port of INTERNET_PORTS) lines.push(`(allow network-outbound (remote ip ${sexpr(`*:${port}`)}))`);
226
+ }
227
+ // Local-only denies (an internet port a relay holds here, local SSH): this computer's own addresses, never the
228
+ // internet. In strict mode these sit right after the INTERNET_PORTS allow-back above (the same-port narrowing
229
+ // pair); otherwise there is no preceding allow to narrow and the position carries no ordering requirement.
230
+ for (const port of [...new Set([...localOnly, ...ALWAYS_LOCAL_DENY_PORTS])].sort((x, y) => x - y)) {
231
+ if (channelPorts.includes(port)) continue;
232
+ lines.push(`(deny network-outbound (remote ip ${sexpr(`localhost:${port}`)}))`);
233
+ }
234
+ // The worker channel, allowed dead last among every network-outbound rule above: on macOS 27 any later deny —
235
+ // even for a wholly unrelated port — would silently defeat this allow (module header). Kept local-only
236
+ // (`localhost:<port>`, not `*:<port>`): still unreachable from a remote host on this exact port, on macOS 26 and 27.
237
+ for (const port of channelPorts) lines.push(`(allow network-outbound (remote ip ${sexpr(`localhost:${port}`)}))`);
238
+ // SynaBun's whole data home (its auth secrets, other runs' task text, the memory DB) and other runtimes' creds:
239
+ // a worker neither reads nor writes them.
240
+ for (const path of denies) lines.push(`(deny file-read* file-write* (subpath ${sexpr(path)}))`);
241
+ // Allowed back LAST (last match wins): the run's own scratch folder, its cwd when that is inside the data home
242
+ // (skill-drafting workspaces), and its OpenCode config folder — everything the run legitimately needs there.
243
+ for (const path of allows) lines.push(`(allow file-read* file-write* (subpath ${sexpr(path)}))`);
244
+ // Metadata-only on the denied ancestors of each allow-back, so `node scratch/probe.mjs` can lstat its way in
245
+ // (without this, a denied data-home ancestor fails EPERM before the script runs). Directory nodes only (literal),
246
+ // placed after the file allow-backs so this metadata grant is the last match for those paths.
247
+ for (const dir of metadataAncestors(allows, denies)) lines.push(`(allow file-read-metadata (literal ${sexpr(dir)}))`);
248
+ return `${lines.join('\n')}\n`;
249
+ }
250
+
251
+ /** sandbox-exec argv to run `command args…` under a written profile (OpenCode's serve launch). */
252
+ export function sandboxExecArgv(profilePath, command, args = []) {
253
+ return [SANDBOX_EXEC, '-f', profilePath, command, ...(Array.isArray(args) ? args : [])];
254
+ }
255
+
256
+ /**
257
+ * Write a run's Seatbelt profile and (for a CLI the Agent SDK spawns directly) a wrapper script that execs the real
258
+ * executable under it. The SDK uses pathToClaudeCodeExecutable verbatim with shell:false, so the wrapper is a plain
259
+ * shebang script that forwards its arguments; the sandbox is inherited by everything the CLI then starts.
260
+ *
261
+ * @param {object} o
262
+ * @param {string} o.dir a folder the run owns (its scratch folder): the profile and wrapper live here.
263
+ * @param {string} o.profile the profile text (buildWorkerSandboxProfile).
264
+ * @param {string} [o.realExecutable] the executable the wrapper should exec (for Claude); omit for OpenCode.
265
+ * @param {string} [o.nodeRunner] the node to run a `.js` entry with (default process.execPath); only used when the
266
+ * real executable is a script the Agent SDK would itself have run via node (.js / .mjs / …).
267
+ * @param {string} [o.name] a basename stem (default 'worker').
268
+ * @returns {{ profilePath: string, wrapperPath: string|null }}
269
+ *
270
+ * The wrapper must be named so the Agent SDK spawns it DIRECTLY (not via node): the SDK runs a
271
+ * pathToClaudeCodeExecutable that ends in .js/.mjs/.ts/.tsx/.jsx through `node`, anything else directly. `.sh`
272
+ * is spawned directly, so the wrapper execs sandbox-exec itself. If the REAL executable is such a script, the
273
+ * wrapper runs it under node (what the SDK would have done), so the sandbox still wraps the whole tree.
274
+ */
275
+ export function writeWorkerSandbox({ dir, profile, realExecutable = null, nodeRunner = process.execPath, name = 'worker' }) {
276
+ if (!dir) throw new Error('writeWorkerSandbox needs a directory the run owns');
277
+ mkdirSync(dir, { recursive: true });
278
+ const stem = String(name).replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 64) || 'worker';
279
+ const profilePath = join(dir, `${stem}.sb`);
280
+ writeFileSync(profilePath, profile, { mode: 0o600 });
281
+ let wrapperPath = null;
282
+ if (realExecutable) {
283
+ wrapperPath = join(dir, `${stem}-wrap.sh`);
284
+ // exec, so the wrapper process is replaced by sandbox-exec (no extra shell in the tree). "$@" forwards argv
285
+ // verbatim. The real executable (and node, when the entry is a script) and the profile are single-quoted.
286
+ const sh = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
287
+ const isScript = /\.(?:mjs|cjs|js|ts|tsx|jsx)$/i.test(realExecutable);
288
+ const runTarget = isScript ? `${sh(nodeRunner)} ${sh(realExecutable)}` : sh(realExecutable);
289
+ writeFileSync(wrapperPath, `#!/bin/sh\nexec ${sh(SANDBOX_EXEC)} -f ${sh(profilePath)} ${runTarget} "$@"\n`, { mode: 0o700 });
290
+ chmodSync(wrapperPath, 0o700);
291
+ }
292
+ return { profilePath, wrapperPath };
293
+ }
294
+
295
+ /**
296
+ * Everything a guarded Claude / OpenCode run needs for OS isolation, or null when it does not apply (not macOS, no
297
+ * guard, warn / off, or the capability is full). The caller gathers the data-home trees and other-runtime creds to
298
+ * deny and the run's own folders to allow back; this assembles the port set and gates on an enforced guard. Writing
299
+ * is left to the caller (writeWorkerSandbox) so a failure to write never throws on the launch path — the caller
300
+ * falls back to the in-code guard and says so.
301
+ * @param {{ allowPorts:number[] }} [o.strictLocal] the relay detection failed: the strict profile (see
302
+ * buildWorkerSandboxProfile), with `allowPorts` the worker channel.
303
+ * @returns {{ denyPorts:number[], denyLocalPorts:number[], denyPortRange:{from,to}|null, denyPaths:string[], allowPaths:string[], note:string, strictLocal:object|null } | null}
304
+ */
305
+ export function workerSandboxPlan({ confinement, serverPort = null, reservedPortRange = null, extraDenyPorts = [], denyPaths = [], allowPaths = [], strictLocal = null, platform = process.platform } = {}) {
306
+ if (!sandboxSupported(platform)) return null;
307
+ if (!confinement || !confinement.enforced) return null; // only an enforced guard (read-only / workspace) is wrapped
308
+ const denyPorts = [];
309
+ const denyLocalPorts = [];
310
+ const own = Number(serverPort || confinement.serverPort) || null;
311
+ if (own) denyPorts.push(own);
312
+ for (const p of extraDenyPorts || []) {
313
+ const n = Number(p);
314
+ if (!(n > 0)) continue;
315
+ // A relay on an internet port is denied on this computer's addresses only; the internet keeps the port.
316
+ if (INTERNET_PORTS.includes(n)) denyLocalPorts.push(n); else denyPorts.push(n);
317
+ }
318
+ const strictAllow = strictLocal ? cleanPorts(strictLocal.allowPorts) : [];
319
+ return {
320
+ denyPorts: [...new Set(denyPorts)],
321
+ denyLocalPorts: [...new Set(denyLocalPorts)],
322
+ denyPortRange: reservedPortRange && Number(reservedPortRange.from) > 0 && Number(reservedPortRange.to) > 0
323
+ ? { from: Number(reservedPortRange.from), to: Number(reservedPortRange.to) } : null,
324
+ denyPaths: [...new Set((denyPaths || []).filter(Boolean))],
325
+ allowPaths: [...new Set((allowPaths || []).filter(Boolean))],
326
+ note: `synabun worker ${confinement.capability} (${confinement.mode})${strictLocal ? ' strict' : ''}`,
327
+ strictLocal: strictLocal ? { allowPorts: strictAllow } : null,
328
+ };
329
+ }