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
@@ -1,16 +1,29 @@
1
- import { detectProject as memoryProject } from '../../mcp-server/dist/config.js';
1
+ import { mkdtempSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { externalServerInventory, selectExternalServers, codexMcpEnabledOverride, referenceClaudeMap, waitForClaudeMcpScope, redactMcpValues, writePrivateMcpConfig, mcpDefinitionValues } from './mcp-external-servers.js';
4
+ import { resolveStoredTools, runtimeToolsValue, readDefaultTools } from './tool-lists.js';
5
+ import { waitForSynabunTools, synabunToolsTimeoutText } from './opencode-mcp-ready.js';
6
+ import { config as memoryConfig, detectProject as memoryProject } from '../../mcp-server/dist/config.js';
2
7
  import { Codex } from '@openai/codex-sdk';
8
+ import { CodexWorkerAppServer } from './codex-worker-app-server.js';
3
9
  import { query as claudeQuery } from '@anthropic-ai/claude-agent-sdk';
4
10
  import { CLAUDE_EFFORT_LEVELS } from './claude-model-catalog.js';
5
11
  import { CLAUDE_NOT_INSTALLED, resolveClaudeSdkExecutable } from './claude-executable.js';
6
12
  import { extractOpenCodeText } from './session-title-generator.js';
13
+ import { withServeEnv } from './opencode-protocol.js';
7
14
  import { patchCodexLineSplitting } from './jsonl-lines.js';
8
- import { dirname } from 'node:path';
15
+ import { dirname, join } from 'node:path';
9
16
  import { codexReasoningEffort } from './effort-levels.js';
17
+ import { spawn as spawnProcess } from 'node:child_process';
10
18
  import { codexStreamMedia, collectCodexMedia, openCodeMediaPart, ownCodexImages } from './assistant-media.js';
11
19
  import { browserToolDenial } from './browser-tool-policy.js';
12
20
  import { CODEX_BROWSER_POLICY_UNTRUSTED_NOTE, codexBrowserPolicyHook } from './assistant-route-gate-codex.js';
13
21
  import { codexWindowsSandbox, codexWindowsSandboxConfig } from './agent-sandbox-elevation.js';
22
+ import { DRAFT_DISALLOWED_TOOLS, DRAFT_TOOLS, codexDraftOptions, draftOpenCodeDecision, draftPreToolUseHook, draftToolDecision, openCodeDraftConfig, resolveDraftConfinement } from './skills/draft-confinement.js';
23
+ import { EVAL_CONFINEMENT, codexEvalOptions, evalClaudeOptions, evalGitSafetyEnv, evalOpenCodeDecision, evalPreToolUseHook, evalToolDecision, openCodeEvalConfig, openCodeEvalEnv, placeOpenCodeEvalSkill, resolveEvalConfinement } from './skills/eval-confinement.js';
24
+ import { WORKER_GUARD_DEFAULT, codexSandboxFor, confinementEvent, openCodeCodeModeDecision, openCodeCodeModeNote, openCodePermissionDecision, openCodePermissionsFor, resolveWorkerConfinement, workerShellDecision, workerToolDecision } from './worker-confinement.js';
25
+
26
+ function nativeToolSelection(options, provider) { return resolveStoredTools(options, { source: options.runMode === 'task' ? 'dispatch' : 'launch', provider, defaultTools: options.runMode === 'task' ? undefined : readDefaultTools(memoryConfig.dataHome) }); }
14
27
 
15
28
  function stringEnv(source) {
16
29
  const out = {};
@@ -112,6 +125,12 @@ export function normalizeOpenCodeModel(model) {
112
125
  export const PERMISSION_POLICIES = new Set(['auto', 'ask', 'restricted']);
113
126
  export const CAPABILITIES = new Set(['read-only', 'workspace', 'full']);
114
127
  export const CLAUDE_READ_ONLY_DISALLOWED_TOOLS = ['Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'Bash'];
128
+ // The owner's live data home (its real memory DB). A worker's shell must never inherit these: a temp server or a
129
+ // test suite started from the worker would otherwise open the owner's real store (a reviewer's temp boot did,
130
+ // briefly). The worker's own SynaBun MCP child gets the DB through its own env block / DOTENV_PATH (server.js
131
+ // buildCodexMcpEnv, the OpenCode serve config), not from the shell; the Claude worker's MCP is in-process HTTP and
132
+ // needs nothing here. Stripped for every worker (guarded and full alike): a worker never opens the live DB directly.
133
+ export const LIVE_DATA_ENV = Object.freeze(['SQLITE_DB_PATH', 'MEMORY_DATA_DIR']);
115
134
 
116
135
  export function normalizePermissionPolicy(policy) {
117
136
  const value = String(policy || 'auto').trim().toLowerCase();
@@ -136,6 +155,99 @@ function policyUnsupportedError(provider, policy) {
136
155
  return error;
137
156
  }
138
157
 
158
+ /** A confined run (lib/skills/draft-confinement.js) that cannot be set up on its provider: it never starts unconfined. */
159
+ function confinementUnsupportedError(provider, reason, what = 'a drafting folder') {
160
+ const error = new Error(`${provider} cannot run confined to ${what}${reason ? ` (${reason})` : ''}: the run did not start.`);
161
+ error.code = 'CONFINEMENT_UNSUPPORTED';
162
+ return error;
163
+ }
164
+
165
+ /** Claude's built-in writers, kept off the tool list of an enforced read-only run (the hook refuses them too; Bash is the hook's). */
166
+ export const CLAUDE_READ_ONLY_WRITE_TOOLS = Object.freeze(['Write', 'Edit', 'MultiEdit', 'NotebookEdit']);
167
+
168
+ /**
169
+ * The worker confinement of a run (lib/worker-confinement.js), or null: capability full, the Protect workers
170
+ * setting off, or a skill-drafting run (which has its own, stricter confinement). Throws when the working
171
+ * directory cannot be resolved: the run must not start on a guess.
172
+ */
173
+ function workerGuardFor({ capability, cwd, guardMode, scratchDir, confinement, serverPort = null, localPorts = null, localPortRange = null }) {
174
+ if (confinement) return null;
175
+ // Never rooted where the server runs: a run without a cwd is rooted at its scratch folder, or does not start.
176
+ return resolveWorkerConfinement({ capability, cwd: cwd || null, mode: guardMode, scratchDir: scratchDir || null, serverPort, localPorts, localPortRange });
177
+ }
178
+ /** The port of SynaBun's own server, from a run's MCP address (a guarded worker's shell may not call it). */
179
+ function serverPortOf(mcpUrl, fallback = null) {
180
+ try { const port = Number(new URL(String(mcpUrl)).port); if (port > 0) return port; } catch { /* no address */ }
181
+ return Number(fallback) > 0 ? Number(fallback) : null;
182
+ }
183
+
184
+ /**
185
+ * What an OpenCode run's own config carries for its confinement (server.js setupOpencodeLoopConfig): null for an
186
+ * unguarded run (today's rules), else { permissions (OpenCode 2's rules), mcp: 'off'?, directMcp: true? }. A
187
+ * skill-drafting run has no shell, web, subagent or skill tool and no MCP server; a guarded worker has the rules its
188
+ * adapter answers by policy, and under Enforce no Code Mode: `directMcp` sets `codemode: false` on every MCP server of
189
+ * the run's config, which OpenCode 2.1.4 otherwise reaches only through Code Mode's `execute` (denied by the rules).
190
+ */
191
+ export function openCodeRunConfigFor(state = {}) {
192
+ if (state.confinement?.kind === EVAL_CONFINEMENT) return openCodeEvalConfig({ role: state.confinement.role, shell: state.confinement.shell === true });
193
+ if (state.confinement) return openCodeDraftConfig();
194
+ const guard = workerGuardFor({ capability: normalizeCapability(state.capability), cwd: state.cwd, guardMode: state.guardMode ?? WORKER_GUARD_DEFAULT, scratchDir: state.scratchDir, confinement: null, serverPort: state.serverPort || null });
195
+ if (!guard) return null;
196
+ return { permissions: openCodePermissionsFor(guard), ...(guard.enforced ? { directMcp: true } : {}) };
197
+ }
198
+
199
+ /**
200
+ * Whether an OpenCode run is a protected worker: an Assistant task worker with capability read-only or workspace
201
+ * under Protect workers = Enforce (never a drafting or evaluation run, which have confinements of their own; never
202
+ * Warn, Off or full). Its serve reads no project configuration (openCodeRunEnvFor) and keeps its password out of every
203
+ * environment (server.js ensureIsolatedServe `hidePassword`: lib/opencode-protocol.js serveSpecFor passwordFrom
204
+ * 'stdout'). Throws like resolveWorkerConfinement when a guarded run has no folder.
205
+ */
206
+ export function openCodeProtectedWorker(state = {}) {
207
+ if (state.confinement) return false;
208
+ const guard = workerGuardFor({ capability: normalizeCapability(state.capability), cwd: state.cwd, guardMode: state.guardMode ?? WORKER_GUARD_DEFAULT, scratchDir: state.scratchDir, confinement: null, serverPort: state.serverPort || null });
209
+ return !!guard?.enforced;
210
+ }
211
+
212
+ /**
213
+ * What a protected worker's serve environment holds (openCodeProtectedWorker): no project configuration at all, so
214
+ * nothing the worker writes in its folder runs in the serve (no opencode.json with plugins or MCP servers, no
215
+ * .opencode plugin, agent, command or tool, no project AGENTS.md: OPENCODE_DISABLE_PROJECT_CONFIG, and
216
+ * OPENCODE_CONFIG_PROJECT_DISABLE too, which OpenCode 2 reads first), and no inherited OPENCODE_CONFIG_DIR (OpenCode
217
+ * reads that folder in place of the run's own configuration, whose rules, `codemode: false` and SynaBun MCP pin would
218
+ * then be gone). Every inherited GIT_* variable is removed too: repository, index and object paths could point git
219
+ * outside the folder, and GIT_CONFIG_PARAMETERS or GIT_CONFIG_COUNT/KEY_n/VALUE_n could run an fsmonitor or pager.
220
+ * `GIT_*: null` clears the prefix before deliberate settings are applied (lib/opencode-protocol.js withServeEnv).
221
+ */
222
+ export const OPENCODE_PROTECTED_WORKER_ENV = Object.freeze({
223
+ OPENCODE_DISABLE_PROJECT_CONFIG: '1', OPENCODE_CONFIG_PROJECT_DISABLE: '1', OPENCODE_CONFIG_DIR: null,
224
+ 'GIT_*': null,
225
+ });
226
+
227
+ /**
228
+ * What an OpenCode run's serve environment carries for its confinement (server.js ensureIsolatedServe): an evaluation
229
+ * run's (no project configuration, no file watcher, git's safety settings: lib/skills/eval-confinement.js
230
+ * openCodeEvalEnv), a protected worker's (OPENCODE_PROTECTED_WORKER_ENV), else null. A value of null removes that
231
+ * variable from the serve's environment. Throws when the evaluation folder cannot be resolved: the serve must not start
232
+ * unconfined.
233
+ */
234
+ export function openCodeRunEnvFor(state = {}) {
235
+ if (state.confinement?.kind === EVAL_CONFINEMENT) return openCodeEvalEnv(resolveEvalConfinement(state.confinement, state.cwd));
236
+ if (openCodeProtectedWorker(state)) return { ...OPENCODE_PROTECTED_WORKER_ENV };
237
+ return null;
238
+ }
239
+
240
+ /**
241
+ * An evaluation trial with the skill: its copy placed in the run's own OpenCode configuration folder (`configDir`,
242
+ * server.js setupOpencodeLoopConfig's, removed with the run's serve), where OpenCode reads it as a global skill
243
+ * (lib/skills/eval-confinement.js placeOpenCodeEvalSkill). → the folder, or null (not an evaluation, a baseline, a
244
+ * grader). Throws when it cannot be placed: the run must not start without the skill it tests.
245
+ */
246
+ export function openCodeEvalSkillFor(state = {}, configDir) {
247
+ if (state.confinement?.kind !== EVAL_CONFINEMENT) return null;
248
+ return placeOpenCodeEvalSkill(resolveEvalConfinement(state.confinement, state.cwd), configDir);
249
+ }
250
+
139
251
  /** Final assistant text carried by a Codex SDK stream event, or null. */
140
252
  export function codexAgentMessageText(event) {
141
253
  if (!event) return null;
@@ -241,23 +353,47 @@ function codexStructuredOutput(text, schema) {
241
353
  export async function createCodexNativeLoopAdapter(options = {}) {
242
354
  const {
243
355
  runId, cwd, model, effort, codexHome, codexPath, browserSessionId,
244
- browserTabId, mcpProfile, onIdentity = () => {}, onEvent = () => {},
356
+ browserTabId, onIdentity = () => {}, onEvent = () => {},
245
357
  CodexClass = Codex,
246
358
  extraEnv = {}, permissionPolicy: rawPolicy = 'auto', capability: rawCapability = 'full',
247
359
  outputSchema = null, desktopGrantProvider = null, modelContextWindow = null, contextMode = null,
248
360
  // Image creation runs (assistant dispatch): 'image' → each turn returns the images it generated.
249
361
  collectMedia = null,
250
- // Assistant task runs: the browser policy's hook and the run's notes (a function: never in the state file).
251
- runMode = 'loop', onNote = null, browserPolicyTrust = null,
362
+ // Assistant task runs: the browser policy's hook and the run's notes (a function: never in the state file), and
363
+ // the registered code projects its test runners may also run in (lib/browser-policy-roots.js, read as the run starts).
364
+ runMode = 'loop', onNote = null, browserPolicyTrust = null, policyRoots = [],
252
365
  // Inherited variables to remove before extraEnv applies (the Codex account's launch:
253
366
  // lib/codex-accounts.js launchFor() — inherited OpenAI/Codex credentials never reach the run).
254
367
  unsetEnv = [],
255
368
  // The Admin sandbox setting when the run started (lib/agent-sandbox-elevation.js): the Windows
256
369
  // sandbox every `codex exec` of the thread may use. The run keeps it while it lives.
257
370
  adminSandbox = false,
371
+ // A skill-drafting run's confinement (lib/skills/draft-confinement.js codexDraftOptions): the sandbox around the
372
+ // workspace, no network, no MCP server.
373
+ confinement = null,
374
+ // The Protect workers setting as the run starts (lib/worker-confinement.js), the run's own scratch folder and
375
+ // SynaBun's own port (a guarded worker's shell may not call it).
376
+ guardMode = WORKER_GUARD_DEFAULT, scratchDir = null, serverPort = null, localPortRange = null,
258
377
  } = options;
259
378
  const permissionPolicy = normalizePermissionPolicy(rawPolicy);
260
379
  const capability = normalizeCapability(rawCapability);
380
+ // Resolved before anything starts: a confinement that cannot be set up stops the run (never an unconfined start).
381
+ let drafted = null;
382
+ let evaluated = null;
383
+ if (confinement?.kind === EVAL_CONFINEMENT) {
384
+ // A skill-evaluation run (lib/skills/eval-confinement.js): the sandbox around its job folder (read-only for a
385
+ // grader, a judge or a proposer), no network, no web search, every MCP server disabled.
386
+ let held;
387
+ try { held = resolveEvalConfinement(confinement, cwd); } catch (error) { throw confinementUnsupportedError('Codex', error?.message || error, 'an evaluation folder'); }
388
+ evaluated = { held, ...codexEvalOptions(held, { codexHome, projectDir: cwd }) };
389
+ } else if (confinement) {
390
+ let confined;
391
+ try { confined = resolveDraftConfinement(confinement, cwd); } catch (error) { throw confinementUnsupportedError('Codex', error?.message || error); }
392
+ drafted = codexDraftOptions(confined, { codexHome, projectDir: cwd });
393
+ }
394
+ // Either confinement's options (the thread's sandbox and the config's MCP switches): one shape.
395
+ const boxed = drafted || evaluated;
396
+ const guard = boxed ? null : workerGuardFor({ capability, cwd, guardMode, scratchDir, confinement: null, serverPort, localPortRange });
261
397
  // "<id>[extended]" (the Assistant's extended-context row) runs <id> with the
262
398
  // larger window the dispatcher read from the catalog; never silently at 272k.
263
399
  const legacyExtended = /\[extended\]$/i.test(String(model || ''));
@@ -283,16 +419,37 @@ export async function createCodexNativeLoopAdapter(options = {}) {
283
419
  SYNABUN_PROJECT: cwd ? memoryProject(cwd) : 'global',
284
420
  SYNABUN_BROWSER_SESSION: browserSessionId,
285
421
  SYNABUN_BROWSER_TAB: browserTabId,
286
- SYNABUN_PROFILE: mcpProfile,
422
+ SYNABUN_TOOLS: runtimeToolsValue((() => { const list = nativeToolSelection(options, 'codex'); return list.items.includes('all') ? list.items : [...list.groups, ...list.servers]; })()),
287
423
  SYNABUN_TOOL_CATALOG_MODE: 'deferred',
288
424
  // Computer-use workers: the grant unlocks the `computer` group in the MCP child.
289
425
  SYNABUN_DESKTOP_GRANT: typeof desktopGrantProvider === 'function' ? desktopGrantProvider() || undefined : undefined,
290
426
  });
291
427
  const inherited = { ...process.env };
428
+ for (const key of ['SYNABUN_PROFILE', 'SYNABUN_RUNTIME_PROFILE_PATH', 'SYNABUN_TOOLS', 'SYNABUN_TOOLS_PATH']) delete inherited[key];
292
429
  for (const key of Array.isArray(unsetEnv) ? unsetEnv : []) delete inherited[key];
293
- const env = stringEnv({ ...inherited, ...pins, CODEX_HOME: codexHome, ...(browserPolicy ? { SYNABUN_BROWSER_POLICY_CWD: cwd || process.cwd() } : {}), ...stringEnv(extraEnv) });
430
+ // A worker never holds a brain's launch credential (the Assistant's own identity) or an owner's page secret.
431
+ delete inherited.SYNABUN_BRAIN_TOKEN;
432
+ // The worker's shell must not inherit the owner's live-data paths (its MCP child gets them through its own
433
+ // config: server.js buildCodexMcpEnv). A worker that boots a temp server or runs the suite stays off the live DB.
434
+ for (const key of LIVE_DATA_ENV) delete inherited[key];
435
+ // The hook reads the run's project and the other projects its test runners may run in (a JSON list) from its env.
436
+ const policyEnv = browserPolicy ? { SYNABUN_BROWSER_POLICY_CWD: cwd || process.cwd(), SYNABUN_BROWSER_POLICY_ROOTS: JSON.stringify(Array.isArray(policyRoots) ? policyRoots : []) } : {};
437
+ const env = withServeEnv(stringEnv({ ...inherited, ...pins, CODEX_HOME: codexHome, ...policyEnv, ...stringEnv(extraEnv) }), evaluated ? evalGitSafetyEnv(evaluated.held.root) : null);
294
438
  const config = { ...codexWindowsSandboxConfig(adminSandbox) };
295
439
  if (Object.keys(pins).length) config.mcp_servers = { SynaBun: { env: pins } };
440
+ if (!boxed) {
441
+ const inventory = externalServerInventory({ provider: 'codex', cwd, codexHome, env, home: env.HOME, dataHome: memoryConfig.dataHome, ...options.externalMcpContext });
442
+ const list = nativeToolSelection(options, 'codex');
443
+ const scope = selectExternalServers(list.servers, inventory);
444
+ // Leaf overrides preserve native transports/env. Codex treats quotes in a
445
+ // path literally; reject unaddressable names rather than launch a wider scope.
446
+ for (const name of scope.disabled) {
447
+ if (inventory.servers.find(row => row.name === name)?.reason === 'disabled-in-provider') continue;
448
+ config[codexMcpEnabledOverride(name)] = false;
449
+ }
450
+ }
451
+ // A drafting or evaluation run: every MCP server disabled (SynaBun's keeps its pins beside the switch; they never start).
452
+ if (boxed) config.mcp_servers = { ...(config.mcp_servers || {}), ...Object.fromEntries(Object.entries(boxed.config.mcp_servers).map(([name, value]) => [name, { ...(config.mcp_servers?.[name] || {}), ...value }])) };
296
453
  if (extended) config.model_context_window = Math.floor(Number(modelContextWindow));
297
454
  if (browserPolicy) Object.assign(config, browserPolicy.config);
298
455
  // The SDK is always told which Codex to start: the user's own installation.
@@ -303,7 +460,9 @@ export async function createCodexNativeLoopAdapter(options = {}) {
303
460
  error.code = 'CODEX_NOT_INSTALLED';
304
461
  throw error;
305
462
  }
306
- const codex = new CodexClass({
463
+ const WorkerCodex = task && !boxed && CodexClass === Codex ? CodexWorkerAppServer : CodexClass;
464
+ const codex = new WorkerCodex({
465
+ onProcess: options.onProcess, signal: options.signal, apiKey: options.apiKey, configArgs: options.configArgs,
307
466
  ...(codexPath ? { codexPathOverride: codexPath } : {}),
308
467
  env,
309
468
  config,
@@ -312,23 +471,67 @@ export async function createCodexNativeLoopAdapter(options = {}) {
312
471
  // U+2028 / U+2029: a command printing one ended the run with "Failed to parse item".
313
472
  patchCodexLineSplitting(codex);
314
473
  const parent = dirname(cwd || process.cwd());
315
- const thread = codex.startThread({
474
+ // The sandbox: a drafting run's (the workspace, no network); an enforced guard's (lib/worker-confinement.js
475
+ // codexSandboxFor: read-only, or workspace-write with the working directory and the scratch folder, never the
476
+ // parent folder); else today's (the capability's mode, the parent folder writable too).
477
+ const sandboxed = guard?.enforced ? codexSandboxFor(guard, { parentDir: parent && parent !== cwd ? parent : null, scratchDir }) : null;
478
+ // A guarded Codex worker (Protect workers = Enforce) runs with network OFF in Codex's own sandbox. Codex's
479
+ // sandbox network is all-or-nothing (no per-host/loopback denial on this CLI), so SynaBun cannot wrap it in an
480
+ // outer Seatbelt (nested sandbox-exec is refused): turning the network off is the only way its shell commands
481
+ // (node -e, curl, a script) cannot reach SynaBun's own control port or any other local endpoint. The owner has
482
+ // accepted that read-only / workspace Codex workers lose internet (HTTPS, npm, git fetch) for their shell. The
483
+ // worker's SynaBun MCP child runs outside this command sandbox, so remember / recall / browser tools still work.
484
+ const codexNetworkOff = !!(boxed || guard?.enforced);
485
+ const threadOptions = {
316
486
  workingDirectory: cwd || process.cwd(),
317
487
  model: runModel || undefined,
318
488
  modelReasoningEffort: normalizeCodexEffort(effort),
319
489
  approvalPolicy: 'never',
320
- sandboxMode: codexSandboxModeFor({ permissionPolicy, capability }),
490
+ sandboxMode: boxed ? boxed.thread.sandboxMode : sandboxed ? sandboxed.sandboxMode : codexSandboxModeFor({ permissionPolicy, capability }),
321
491
  skipGitRepoCheck: true,
322
- networkAccessEnabled: true,
323
- ...(task ? { webSearchMode: 'disabled' } : {}),
324
- additionalDirectories: parent && parent !== cwd ? [parent] : undefined,
325
- });
492
+ networkAccessEnabled: !codexNetworkOff,
493
+ ...(task || boxed ? { webSearchMode: 'disabled' } : {}),
494
+ additionalDirectories: boxed ? undefined : sandboxed ? sandboxed.additionalDirectories : parent && parent !== cwd ? [parent] : undefined,
495
+ };
496
+ const thread = options.resumeThreadId ? codex.resumeThread(options.resumeThreadId, threadOptions) : codex.startThread(threadOptions);
497
+ try { await thread.ready?.(); } catch (error) { await codex.dispose?.(); throw error; }
498
+ if (thread.id) onIdentity({ providerThreadId: thread.id, providerSessionId: thread.id });
326
499
  let alive = true;
327
500
  let activeAbort = null;
328
501
  let activeTurn = null;
329
- // Every runTurn is its own `codex exec`, which numbers its items from item_0
330
- // again: the sidepanel keys a turn's items by this sequence.
331
- let providerTurn = 0;
502
+ // A guarded run says at its start what holds it: Codex's sandbox never reports to SynaBun by itself, so this is
503
+ // the one place the note is written before any command (the run's notes, then its events).
504
+ if (guard) {
505
+ try { onNote?.(`worker confinement (${guard.mode}, capability ${guard.capability}): Codex sandbox ${sandboxed ? sandboxed.sandboxMode : codexSandboxModeFor({ permissionPolicy, capability })}${guard.enforced ? `, writable: ${guard.writable.join(', ')}` : ' as before (warn: what Enforce would refuse is noted)'}`); } catch {}
506
+ // Network-off is the one way a Codex worker's shell cannot reach SynaBun's control port (Codex runs its own
507
+ // sandbox, which SynaBun cannot wrap, and its network is all-or-nothing). Say it plainly, once, at the start.
508
+ if (guard.enforced) { try { onNote?.('This protected Codex worker has NO internet for its shell commands: Codex\'s sandbox network is all-or-nothing, so to keep its commands off SynaBun\'s control port (and every local port) the network is off. HTTPS, npm install and git fetch will fail in the shell; your SynaBun MCP tools (remember, recall, browser) still work. A run that needs internet in its shell must use capability "full".'); } catch {} }
509
+ else { try { onNote?.('warn mode: Enforce would also turn this Codex worker\'s shell network off (no internet), so its commands could not reach SynaBun\'s control port.'); } catch {} }
510
+ }
511
+ if (drafted) { try { onNote?.('skill-draft confinement: Codex sandbox workspace-write around the drafting folder, no network, no web search, every MCP server disabled; the shell stays, inside the sandbox'); } catch {} }
512
+ if (evaluated) { try { onNote?.(`skill-eval confinement (${evaluated.held.role}): Codex sandbox ${evaluated.thread.sandboxMode} around the evaluation folder, no network, no web search, every MCP server disabled; the shell stays, inside the sandbox`); } catch {} }
513
+ /**
514
+ * A command Codex ran (item.completed command_execution): under warn, what Enforce would have refused is noted;
515
+ * under enforce, a sandbox denial in its output is explained in SynaBun's words (Codex's sandbox refuses by itself
516
+ * and says nothing to SynaBun).
517
+ */
518
+ const noteCommand = (item, meta) => {
519
+ if (!guard || item?.type !== 'command_execution') return;
520
+ const command = typeof item.command === 'string' ? item.command : Array.isArray(item.command) ? item.command.join(' ') : '';
521
+ if (!command) return;
522
+ const decision = workerShellDecision(command, guard);
523
+ const output = String(item.aggregated_output || item.output || '');
524
+ const denied = Number(item.exit_code) !== 0 && /operation not permitted|permission denied|read-only file system|sandbox|EPERM|EACCES|EROFS/i.test(output);
525
+ if (!decision && !denied) return;
526
+ const event = decision
527
+ ? confinementEvent(decision, { tool: 'shell', confinement: guard, provider: 'codex' })
528
+ : { type: 'synabun.confinement', action: 'refused', capability: guard.capability, tool: 'shell', rule: 'sandbox', reason: `Codex's sandbox refused "${command.slice(0, 160)}": this run (capability ${guard.capability}) writes inside ${guard.writable.join(', ')} only${guard.capability === 'read-only' ? ' (read-only: nothing at all, the temp folder included)' : ''}.`, provider: 'codex' };
529
+ if (decision && guard.enforced && !denied) event.action = 'noted'; // the sandbox decides; the heuristic's view is a note
530
+ onEvent({ provider: 'codex', runId, iteration: meta?.iteration, event });
531
+ };
532
+ // The sidepanel keys a turn's items by this sequence, including across a restart.
533
+ // Scheduled SDK turns restart their item IDs; app-server workers keep their thread IDs.
534
+ let providerTurn = Number(options.providerTurnBase) || 0;
332
535
  // Generated images already returned by an earlier turn, and when the thread began (its rollout's day).
333
536
  const mediaSeen = new Set();
334
537
  let threadStartedMs = 0;
@@ -337,9 +540,13 @@ export async function createCodexNativeLoopAdapter(options = {}) {
337
540
  identity: () => ({ providerThreadId: thread.id, providerSessionId: thread.id }),
338
541
  isAlive: () => alive,
339
542
  describe: () => ({
340
- provider: 'codex', permissionPolicy, capability, sandboxMode: codexSandboxModeFor({ permissionPolicy, capability }),
543
+ provider: 'codex', permissionPolicy, capability, sandboxMode: boxed ? boxed.thread.sandboxMode : sandboxed ? sandboxed.sandboxMode : codexSandboxModeFor({ permissionPolicy, capability }),
544
+ networkOff: codexNetworkOff,
341
545
  windowsSandbox: codexWindowsSandbox(adminSandbox),
342
546
  ...(task ? { browserPolicy: browserPolicy ? 'hook' : 'instructions' } : {}),
547
+ ...(drafted ? { confinement: 'skill-draft', root: cwd } : {}),
548
+ ...(evaluated ? { confinement: EVAL_CONFINEMENT, role: evaluated.held.role, root: cwd } : {}),
549
+ ...(guard ? { guard: { mode: guard.mode, capability: guard.capability, writable: [...guard.writable] } } : {}),
343
550
  }),
344
551
  async runTurn(prompt, meta = {}) {
345
552
  if (!alive) throw new Error('Codex native loop is closed');
@@ -377,6 +584,7 @@ export async function createCodexNativeLoopAdapter(options = {}) {
377
584
  if (agentText !== null) text = agentText;
378
585
  const saved = codexStreamMedia(event);
379
586
  if (saved) media.push(saved);
587
+ if (event?.type === 'item.completed') { try { noteCommand(event.item, meta); } catch { /* a note, never the turn */ } }
380
588
  if (event?.type === 'turn.failed') failure = codexFailureMessage(event.error);
381
589
  if (event?.type === 'turn.completed') { completed = true; if (event.usage) usage = event.usage; }
382
590
  if (event?.type === 'error') failure = codexFailureMessage(event);
@@ -415,6 +623,7 @@ export async function createCodexNativeLoopAdapter(options = {}) {
415
623
  async dispose() {
416
624
  alive = false;
417
625
  if (activeAbort) activeAbort.abort(asAbortError('disposed'));
626
+ await codex.dispose?.();
418
627
  },
419
628
  };
420
629
  }
@@ -438,28 +647,61 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
438
647
  unsetEnv = [],
439
648
  // The host's last word on the environment: on Windows the Git Bash it names (lib/git-bash/).
440
649
  childEnv = null,
650
+ // A skill-drafting run (lib/skills/draft-confinement.js): file tools in its folder only, nothing else.
651
+ confinement = null,
652
+ // The Protect workers setting as the run starts (lib/worker-confinement.js), the run's own scratch folder and
653
+ // SynaBun's own port (a guarded worker's shell may not call it; read from mcpUrl when not given).
654
+ guardMode = WORKER_GUARD_DEFAULT, scratchDir = null, serverPort = null, localPortRange = null,
655
+ // Assistant task runs: the registered code projects the browser policy lets test runners run in besides the
656
+ // run's own (lib/browser-policy-roots.js, read as the run starts).
657
+ policyRoots = [],
658
+ // A run resumed after a server restart (assistant-dispatch.js resumeRun): the Claude session to continue, and the
659
+ // cost counter the resumed CLI starts from (it restores its total from the transcript's last `cost-state` line,
660
+ // lib/worker-resume.js readCostState), so the first turn after it is charged only for itself.
661
+ resumeSessionId = null, costBaseUsd = 0,
662
+ // Told the CLI's process once it is spawned ({ pid, startedAt, command }): a run that outlives this server is
663
+ // found by it (the next start ends an orphan before it resumes the session).
664
+ onProcess = null,
441
665
  } = options;
442
666
  const permissionPolicy = normalizePermissionPolicy(rawPolicy);
443
667
  const capability = normalizeCapability(rawCapability);
444
- const askMode = permissionPolicy === 'ask' && typeof permissionBroker?.request === 'function';
668
+ // Resolved before anything starts: a confinement that cannot be set up stops the run (never an unconfined start).
669
+ let evaluated = null;
670
+ if (confinement?.kind === EVAL_CONFINEMENT) {
671
+ try { evaluated = resolveEvalConfinement(confinement, cwd); } catch (error) { throw confinementUnsupportedError('Claude Code', error?.message || error, 'an evaluation folder'); }
672
+ }
673
+ const confined = confinement && !evaluated ? resolveDraftConfinement(confinement, cwd) : null;
674
+ // Either confinement (drafting, evaluation): no worker guard, nothing asked, never bypassPermissions.
675
+ const held = confined || evaluated;
676
+ const evalOptions = evaluated ? evalClaudeOptions(evaluated) : null;
677
+ // The worker confinement of a read-only / workspace run: a PreToolUse hook on every call (subagents' included),
678
+ // which Claude Code runs before any permission mode, so the run keeps bypassPermissions and nothing waits.
679
+ const guard = workerGuardFor({ capability, cwd, guardMode, scratchDir, confinement: held, serverPort: serverPortOf(mcpUrl, serverPort), localPortRange });
680
+ const askMode = !held && permissionPolicy === 'ask' && typeof permissionBroker?.request === 'function';
445
681
  const input = createInputQueue();
446
682
  const abortController = new AbortController();
447
683
  let alive = true;
448
684
  let sessionId = null;
449
685
  let activeTurn = null;
450
686
  const inherited = cleanClaudeEnvironment();
687
+ for (const key of ['SYNABUN_PROFILE', 'SYNABUN_RUNTIME_PROFILE_PATH', 'SYNABUN_TOOLS', 'SYNABUN_TOOLS_PATH']) delete inherited[key];
451
688
  for (const key of Array.isArray(unsetEnv) ? unsetEnv : []) delete inherited[key];
689
+ // A worker never holds a brain's launch credential (the Assistant's own identity) or an owner's page secret.
690
+ delete inherited.SYNABUN_BRAIN_TOKEN;
691
+ // The worker's shell must not inherit the owner's live-data paths: a Claude worker's MCP is in-process HTTP (it
692
+ // reaches the DB through the Neural Interface, never from this env), so nothing here needs them.
693
+ for (const key of LIVE_DATA_ENV) delete inherited[key];
452
694
  const ownEnv = stringEnv({
453
695
  ...inherited,
454
696
  SYNABUN_TERMINAL_SESSION: runId,
455
697
  SYNABUN_PROJECT: cwd ? memoryProject(cwd) : 'global',
456
- SYNABUN_PROFILE: 'full',
457
698
  SYNABUN_BROWSER_SESSION: browserSessionId,
458
699
  SYNABUN_BROWSER_TAB: browserTabId,
459
700
  SYNABUN_RUN_MODE: runMode === 'task' ? 'task' : undefined,
460
701
  ...stringEnv(extraEnv),
461
702
  });
462
- const env = typeof childEnv === 'function' ? stringEnv(childEnv(ownEnv)) : ownEnv;
703
+ // Apply after every inherited/child environment overlay: drop ALL GIT_* first, then SynaBun's own safety entries.
704
+ const env = withServeEnv(typeof childEnv === 'function' ? stringEnv(childEnv(ownEnv)) : ownEnv, evalOptions?.env);
463
705
  const brokerRequest = async (request) => {
464
706
  try {
465
707
  const reply = await permissionBroker.request({
@@ -474,6 +716,7 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
474
716
  'X-Synabun-Terminal': runId,
475
717
  'X-Synabun-Project': cwd ? memoryProject(cwd) : 'global',
476
718
  'X-Synabun-Memory-Session': runId,
719
+ 'X-Synabun-Tools': runtimeToolsValue(resolveStoredTools(options, { source: options.runMode === 'task' ? 'dispatch' : 'launch', provider: 'claude-code' }).groups),
477
720
  'X-Synabun-Browser-Session': browserSessionId,
478
721
  'X-Synabun-Browser-Tab': browserTabId,
479
722
  // Computer-use workers: the grant unlocks the `computer` MCP group.
@@ -486,15 +729,35 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
486
729
  const denyMessage = askMode
487
730
  ? 'Dispatched workers do not use plan mode.'
488
731
  : 'This unattended automation cannot wait for interactive input.';
732
+ // A confined run asks canUseTool for every write (permission mode 'default') on top of its PreToolUse hook, and
733
+ // loads no settings file: no user hooks (they would ask it for memories it has no tool to write), no allow rules.
734
+ const bypass = !askMode && !held;
735
+ // The guard's decision on a tool call: refused (enforce) or noted (warn), always written into the run's events.
736
+ const guardHook = guard ? async (input) => {
737
+ const decision = workerToolDecision(input?.tool_name, input?.tool_input || {}, guard);
738
+ if (!decision) return {};
739
+ onEvent({ provider: 'claude-code', runId, event: confinementEvent(decision, { tool: input?.tool_name, confinement: guard, provider: 'claude-code' }) });
740
+ if (!guard.enforced) return {};
741
+ return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: decision.reason } };
742
+ } : null;
489
743
  const queryOptions = {
490
744
  cwd: cwd || process.cwd(),
491
- permissionMode: askMode ? 'default' : 'bypassPermissions',
492
- ...(askMode ? {} : { allowDangerouslySkipPermissions: true }),
745
+ permissionMode: bypass ? 'bypassPermissions' : 'default',
746
+ ...(bypass ? { allowDangerouslySkipPermissions: true } : {}),
493
747
  includePartialMessages: !!includePartialMessages,
494
748
  systemPrompt: { type: 'preset', preset: 'claude_code' },
495
- settingSources: ['user', 'project', 'local'],
749
+ // A drafting run and every evaluation run read no settings file at all (an evaluation's skill under test comes as
750
+ // its one plugin, below); everything else the user's, the project's and the local ones.
751
+ settingSources: evalOptions ? evalOptions.settingSources : confined ? [] : ['user', 'project', 'local'],
496
752
  hooks: {
497
- PreToolUse: [{
753
+ PreToolUse: [
754
+ // A skill-drafting run: every call, before any permission mode (lib/skills/draft-confinement.js).
755
+ ...(confined ? [{ hooks: [draftPreToolUseHook(confined)] }] : []),
756
+ // A skill-evaluation run: every call, the same way (lib/skills/eval-confinement.js).
757
+ ...(evaluated ? [{ hooks: [evalPreToolUseHook(evaluated)] }] : []),
758
+ // A guarded worker (capability read-only / workspace): every call, subagents' included (lib/worker-confinement.js).
759
+ ...(guardHook ? [{ hooks: [guardHook] }] : []),
760
+ {
498
761
  matcher: denyMatcher,
499
762
  hooks: [async () => ({
500
763
  hookSpecificOutput: {
@@ -508,7 +771,7 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
508
771
  // on every call, subagents' included, before the bypass and canUseTool.
509
772
  ...(runMode === 'task' ? [{
510
773
  hooks: [async (input) => {
511
- const reason = browserToolDenial(input?.tool_name, input?.tool_input || {}, { host: 'claude', cwd: cwd || process.cwd() });
774
+ const reason = browserToolDenial(input?.tool_name, input?.tool_input || {}, { host: 'claude', cwd: cwd || process.cwd(), roots: policyRoots });
512
775
  return reason ? { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason } } : {};
513
776
  }],
514
777
  }] : [])],
@@ -516,6 +779,14 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
516
779
  env,
517
780
  abortController,
518
781
  canUseTool: async (toolName, toolInput) => {
782
+ if (confined) {
783
+ const reason = draftToolDecision(toolName, toolInput, confined);
784
+ return reason ? { behavior: 'deny', message: reason } : { behavior: 'allow', updatedInput: toolInput };
785
+ }
786
+ if (evaluated) {
787
+ const reason = evalToolDecision(toolName, toolInput, evaluated);
788
+ return reason ? { behavior: 'deny', message: reason } : { behavior: 'allow', updatedInput: toolInput };
789
+ }
519
790
  if (toolName === 'ExitPlanMode') return { behavior: 'deny', message: denyMessage };
520
791
  if (toolName === 'AskUserQuestion') {
521
792
  if (!askMode) return { behavior: 'deny', message: denyMessage };
@@ -541,12 +812,105 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
541
812
  return { behavior: 'allow', updatedInput: toolInput };
542
813
  },
543
814
  };
544
- if (mcpUrl) queryOptions.mcpServers = { SynaBun: { type: 'http', url: mcpUrl, headers } };
815
+ if (confined) {
816
+ // No MCP server at all (SynaBun's or the user's: strict), and only the file tools.
817
+ queryOptions.mcpServers = {};
818
+ queryOptions.strictMcpConfig = true;
819
+ queryOptions.tools = [...DRAFT_TOOLS];
820
+ queryOptions.disallowedTools = [...DRAFT_DISALLOWED_TOOLS];
821
+ } else if (evalOptions) {
822
+ // No MCP server at all, the role's tools only, and the skills it may use: the one under test (a baseline: none),
823
+ // loaded as the run's one local plugin; no hook of any settings file, plugin or skill and no inline shell.
824
+ queryOptions.mcpServers = {};
825
+ queryOptions.strictMcpConfig = true;
826
+ queryOptions.tools = [...evalOptions.tools];
827
+ queryOptions.disallowedTools = [...evalOptions.disallowedTools];
828
+ queryOptions.skills = [...evalOptions.skills];
829
+ queryOptions.settings = { ...evalOptions.settings };
830
+ if (evalOptions.plugins.length) queryOptions.plugins = evalOptions.plugins.map((plugin) => ({ ...plugin }));
831
+ } else if (mcpUrl) queryOptions.mcpServers = { SynaBun: { type: 'http', url: mcpUrl, headers } };
832
+ let externalScopeNames = [];
833
+ let externalValues = [];
834
+ let privateMcpServers = null;
835
+ let privateMcpDir = null;
836
+ if (!held) {
837
+ const inventory = externalServerInventory({ provider: 'claude-code', cwd, env, home: env.HOME, dataHome: memoryConfig.dataHome, ...options.externalMcpContext });
838
+ const list = nativeToolSelection(options, 'claude-code');
839
+ const scope = selectExternalServers(list.servers, inventory);
840
+ const definitions = Object.fromEntries(scope.names.map(name => [name, inventory.definitionFor(name)]));
841
+ if (mcpUrl) definitions.SynaBun = { type: 'http', url: mcpUrl, headers };
842
+ const referenced = referenceClaudeMap(definitions, env);
843
+ queryOptions.mcpServers = referenced.mcpServers;
844
+ queryOptions.strictMcpConfig = true;
845
+ Object.assign(env, referenced.env);
846
+ // OAuth and dynamic header helpers are not expanded by the CLI's generic ${VAR} pass.
847
+ // Keep them intact in a private file; the SDK's JSON is replaced before the OS spawn boundary.
848
+ privateMcpServers = structuredClone(referenced.mcpServers);
849
+ for (const [name, definition] of Object.entries(definitions)) {
850
+ if (definition.oauth) privateMcpServers[name].oauth = structuredClone(definition.oauth);
851
+ if (definition.headersHelper) privateMcpServers[name].headersHelper = definition.headersHelper;
852
+ }
853
+ externalValues = [...Object.values(referenced.env), ...mcpDefinitionValues(definitions)];
854
+ externalScopeNames = Object.keys(definitions);
855
+ }
545
856
  if (model) queryOptions.model = model;
546
857
  if (effort && CLAUDE_EFFORT_LEVELS.includes(effort)) queryOptions.effort = effort;
547
- if (capability === 'read-only') queryOptions.disallowedTools = [...CLAUDE_READ_ONLY_DISALLOWED_TOOLS];
858
+ // Read-only: with the guard enforced the shell stays, limited to read-only commands by the hook, and only the
859
+ // built-in writers are off the list; otherwise (warn, off) today's list, Bash included.
860
+ if (capability === 'read-only' && !held) queryOptions.disallowedTools = guard?.enforced ? [...CLAUDE_READ_ONLY_WRITE_TOOLS] : [...CLAUDE_READ_ONLY_DISALLOWED_TOOLS];
548
861
  if (Number(maxBudgetUsd) > 0) queryOptions.maxBudgetUsd = Number(maxBudgetUsd);
549
862
  if (outputSchema && typeof outputSchema === 'object') queryOptions.outputFormat = { type: 'json_schema', schema: outputSchema };
863
+ // A run resumed after a server restart continues its own Claude session. A tool call the restart cut off is in its
864
+ // transcript without a result: the CLI records it as interrupted (outcome unknown) and the next call goes through.
865
+ if (resumeSessionId) queryOptions.resume = String(resumeSessionId);
866
+ // The CLI's process is spawned here, not inside the SDK, so its pid is known: a server shutdown ends it at once
867
+ // (endProcess), and the next start can tell an orphan from another program. Left to the SDK, a CLI whose server
868
+ // went down only gets its stdin closed: it finishes the tool call in progress (a minute of `sleep` measured) and
869
+ // keeps writing to its transcript, because the SDK's SIGTERM waits 2 s on a timer that dies with the server. The
870
+ // SDK reads no stderr from a process it did not spawn: it is drained here, its tail kept for a failure's message.
871
+ let child = null;
872
+ let stderrTail = '';
873
+ let terminating = false;
874
+ queryOptions.spawnClaudeCodeProcess = (spawnOptions) => {
875
+ if (!alive || abortController.signal.aborted) throw Object.assign(new Error('Claude MCP launch was stopped'), { code: 'MCP_SCOPE_UNENFORCED' });
876
+ const args = [...spawnOptions.args];
877
+ if (privateMcpServers) for (let index = 0; index < args.length - 1; index++) if (args[index] === '--mcp-config') {
878
+ privateMcpDir ||= mkdtempSync(join(tmpdir(), 'synabun-claude-mcp-'));
879
+ args[index + 1] = writePrivateMcpConfig(join(privateMcpDir, 'mcp.json'), privateMcpServers);
880
+ }
881
+ const proc = (options.spawnFactory || spawnProcess)(spawnOptions.command, args, {
882
+ cwd: spawnOptions.cwd, env: spawnOptions.env, signal: spawnOptions.signal,
883
+ stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true,
884
+ });
885
+ child = proc;
886
+ proc.stderr?.setEncoding?.('utf8');
887
+ proc.stderr?.on('data', (chunk) => { stderrTail = redactMcpValues(`${stderrTail}${chunk}`, externalValues).slice(-2048); });
888
+ proc.stderr?.on('error', () => {});
889
+ if (proc.pid) { try { onProcess?.({ pid: proc.pid, startedAt: new Date().toISOString(), command: String(spawnOptions.command || '') }); } catch { /* a note, never the run */ } }
890
+ return proc;
891
+ };
892
+ const processEnded = () => !child || child.exitCode !== null || (child.signalCode !== null && child.signalCode !== undefined);
893
+ /**
894
+ * End the CLI now (a server shutdown): SIGTERM, which makes it stop the tool call in progress (its process tree
895
+ * included), record that call's result and its cost state, and exit within a second; SIGKILL when it has not exited
896
+ * by `graceMs` (a killed CLI leaves its tool's processes behind, so it is the fallback only). Windows has no SIGTERM
897
+ * (a signal stops the CLI alone, and a dev server its tool started keeps its port): the tree is ended with taskkill.
898
+ * The CLI writes no cost state then; the resume reads what the CLI restores either way (lib/worker-resume.js).
899
+ */
900
+ async function endProcess(graceMs = 1500) {
901
+ const proc = child;
902
+ if (!proc || processEnded()) return;
903
+ const exited = new Promise((resolveExit) => { proc.once('exit', () => resolveExit()); });
904
+ if (process.platform === 'win32') {
905
+ try { spawnProcess('taskkill', ['/pid', String(proc.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }).on?.('error', () => {}); } catch { /* gone */ }
906
+ } else {
907
+ try { proc.kill('SIGTERM'); } catch { /* gone */ }
908
+ }
909
+ await Promise.race([exited, new Promise((resolveWait) => setTimeout(resolveWait, Math.max(100, graceMs)))]);
910
+ if (processEnded()) return;
911
+ try { proc.kill('SIGKILL'); } catch { /* gone */ }
912
+ await Promise.race([exited, new Promise((resolveWait) => setTimeout(resolveWait, 500))]);
913
+ }
550
914
  // The loop runs the user's own Claude Code: SynaBun carries none, and the SDK
551
915
  // is never left to look for one. The SDK uses pathToClaudeCodeExecutable
552
916
  // verbatim with no validation, so the path was vetted where it was resolved
@@ -565,9 +929,11 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
565
929
  throw error;
566
930
  }
567
931
 
568
- const q = queryFactory({ prompt: input, options: queryOptions });
932
+ let q;
933
+ try { q = queryFactory({ prompt: input, options: queryOptions }); }
934
+ catch (error) { if (privateMcpDir) rmSync(privateMcpDir, { recursive: true, force: true }); throw error; }
569
935
  const deltaBuffer = new Map();
570
- const forwardEvent = (event) => onEvent({ provider: 'claude-code', runId, event });
936
+ const forwardEvent = (event) => onEvent({ provider: 'claude-code', runId, event: redactMcpValues(event, externalValues) });
571
937
  const flushDelta = (key) => {
572
938
  const slot = deltaBuffer.get(key);
573
939
  if (!slot) return;
@@ -605,8 +971,9 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
605
971
  };
606
972
  // total_cost_usd is this CLI process's running total, not the turn's charge:
607
973
  // a warm task run reported $7.87 after turn 1 and $7.99 after a $0.12 turn 2.
608
- // A turn resolves with its own charge (costUsd) beside the total.
609
- let reportedCostUsd = 0;
974
+ // A turn resolves with its own charge (costUsd) beside the total. A resumed
975
+ // session's CLI starts from the total its transcript recorded (costBaseUsd).
976
+ let reportedCostUsd = Math.max(0, Number(costBaseUsd) || 0);
610
977
  const turnCost = (event) => {
611
978
  const total = Number(event?.total_cost_usd);
612
979
  if (!Number.isFinite(total) || total <= 0) return { costUsd: 0, totalCostUsd: reportedCostUsd };
@@ -618,7 +985,8 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
618
985
  let pumpError = null;
619
986
  const pump = (async () => {
620
987
  try {
621
- for await (const event of q) {
988
+ for await (const rawEvent of q) {
989
+ const event = redactMcpValues(rawEvent, externalValues);
622
990
  if (!alive) break;
623
991
  const eventSessionId = event?.session_id || null;
624
992
  if (eventSessionId && eventSessionId !== sessionId) {
@@ -653,6 +1021,12 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
653
1021
  }
654
1022
  }
655
1023
  } catch (error) {
1024
+ // A process that exited is reported without what it said on stderr (the SDK keeps that only for a process it
1025
+ // spawned itself): its tail goes with the message.
1026
+ const message = String(error?.message || '');
1027
+ if (stderrTail.trim() && /Claude Code process (exited|terminated)/.test(message) && !/stderr:/.test(message)) {
1028
+ try { error.message = `${message}. stderr: ${stderrTail.trim().slice(-1000)}`; } catch { /* read-only message */ }
1029
+ }
656
1030
  pumpError = error;
657
1031
  } finally {
658
1032
  flushDeltas();
@@ -670,7 +1044,8 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
670
1044
  pump.catch((error) => {
671
1045
  // dispose() aborts the query when the run ends, however it ended, and the
672
1046
  // SDK throws "Claude Code process aborted by user" for it: not a failure.
673
- if (abortController.signal.aborted) return;
1047
+ // Neither is the exit of a CLI a server shutdown ended (dispose with terminate).
1048
+ if (abortController.signal.aborted || terminating) return;
674
1049
  onEvent({
675
1050
  provider: 'claude-code',
676
1051
  runId,
@@ -681,8 +1056,14 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
681
1056
  return {
682
1057
  identity: () => ({ providerSessionId: sessionId }),
683
1058
  isAlive: () => alive,
684
- describe: () => ({ provider: 'claude-code', permissionPolicy, capability, askMode }),
685
- runTurn(prompt, meta = {}) {
1059
+ // The CLI's process while it runs ({ pid }), else null.
1060
+ processInfo: () => (child?.pid && !processEnded() ? { pid: child.pid } : null),
1061
+ describe: () => ({ provider: 'claude-code', permissionPolicy, capability, askMode, ...(held ? { confinement: held.kind, root: held.root, ...(evaluated ? { role: evaluated.role, skills: [...evalOptions.skills], plugins: evalOptions.plugins.map((plugin) => plugin.path) } : {}) } : {}), ...(guard ? { guard: { mode: guard.mode, capability: guard.capability, writable: [...guard.writable] } } : {}) }),
1062
+ async runTurn(prompt, meta = {}) {
1063
+ if (!held) {
1064
+ try { await waitForClaudeMcpScope(q, externalScopeNames); }
1065
+ catch { input.close(); q.close?.(); await endProcess(); throw Object.assign(new Error('Claude MCP scope could not be enforced before the first turn'), { code: 'MCP_SCOPE_UNENFORCED' }); }
1066
+ }
686
1067
  if (!alive) return Promise.reject(new Error('Claude native loop is closed'));
687
1068
  if (activeTurn) return Promise.reject(new Error('Claude native loop already has an active turn'));
688
1069
  onEvent({
@@ -718,16 +1099,24 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
718
1099
  } catch {}
719
1100
  return true;
720
1101
  },
721
- async dispose() {
1102
+ // `terminate` (a server shutdown, lib/native-loop-runtime.js): the CLI is ended before the query is closed
1103
+ // (endProcess), so nothing of it outlives the server. The events it sends while it stops (the cut-off tool call's
1104
+ // result) still reach the run; its exit is not reported as a failure.
1105
+ async dispose(options = {}) {
722
1106
  if (activeTurn) {
723
1107
  const turn = activeTurn; activeTurn = null;
724
1108
  turn.reject(asAbortError('disposed'));
725
1109
  }
1110
+ if (options?.terminate === true) {
1111
+ terminating = true;
1112
+ await endProcess(Number(options.graceMs) > 0 ? Number(options.graceMs) : 1500);
1113
+ }
726
1114
  alive = false;
727
1115
  input.close();
728
1116
  flushDeltas();
729
1117
  try { abortController.abort(asAbortError('disposed')); } catch {}
730
1118
  await Promise.race([pump, new Promise((resolveWait) => setTimeout(resolveWait, 1000))]).catch(() => {});
1119
+ if (privateMcpDir) { rmSync(privateMcpDir, { recursive: true, force: true }); privateMcpDir = null; }
731
1120
  },
732
1121
  };
733
1122
  }
@@ -743,13 +1132,89 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
743
1132
  agent = null, effort = null, runMode = 'loop',
744
1133
  // Image / video creation runs: 'image' | 'video' → each turn returns the files its model generated.
745
1134
  collectMedia = null,
1135
+ // A skill-drafting run's confinement (lib/skills/draft-confinement.js openCodeDraftConfig): the serve's config has
1136
+ // no shell, web, subagent or skill tool and no MCP server; every edit is asked and answered inside the folder.
1137
+ confinement = null,
1138
+ // The Protect workers setting as the run starts (lib/worker-confinement.js), the run's own scratch folder,
1139
+ // SynaBun's own port, the run's own serve port and the band SynaBun keeps for its serves (a guarded worker's shell
1140
+ // may call none of them).
1141
+ guardMode = WORKER_GUARD_DEFAULT, scratchDir = null, serverPort = null, localPortRange = null, servePort = null,
1142
+ onNote = null,
746
1143
  } = options;
747
1144
  if (!client?.session?.create || (!client?.session?.promptAsync && !client?.session?.prompt)) {
748
1145
  throw new Error('OpenCode native loop requires an isolated SDK client');
749
1146
  }
750
1147
  const permissionPolicy = normalizePermissionPolicy(rawPolicy);
751
1148
  const capability = normalizeCapability(rawCapability);
752
- const askMode = permissionPolicy === 'ask' && typeof permissionBroker?.request === 'function';
1149
+ // Resolved before anything starts: a confinement that cannot be set up stops the run (never an unconfined start).
1150
+ let confined = null;
1151
+ let evaluated = null;
1152
+ if (confinement?.kind === EVAL_CONFINEMENT) {
1153
+ try { evaluated = resolveEvalConfinement(confinement, cwd); } catch (error) { throw confinementUnsupportedError('OpenCode', error?.message || error, 'an evaluation folder'); }
1154
+ } else if (confinement) {
1155
+ try { confined = resolveDraftConfinement(confinement, cwd); } catch (error) { throw confinementUnsupportedError('OpenCode', error?.message || error); }
1156
+ }
1157
+ const held = confined || evaluated;
1158
+ const guard = workerGuardFor({ capability, cwd, guardMode, scratchDir, confinement: held, serverPort, localPorts: servePort ? [servePort] : null, localPortRange });
1159
+ const askMode = !held && permissionPolicy === 'ask' && typeof permissionBroker?.request === 'function';
1160
+ // Code Mode (lib/worker-confinement.js OPENCODE_CODE_MODE_TOOL): off under Enforce, noted call by call under Warn.
1161
+ if (guard) { try { onNote?.(openCodeCodeModeNote(guard)); } catch {} }
1162
+ const codeModeCalls = new Set();
1163
+ const noteCodeModeCall = (part) => {
1164
+ if (!guard || part?.type !== 'tool') return;
1165
+ const decision = openCodeCodeModeDecision(part.tool, guard);
1166
+ const callID = part.callID || part.id || '';
1167
+ if (!decision || codeModeCalls.has(callID)) return;
1168
+ codeModeCalls.add(callID);
1169
+ onEvent({ provider: 'opencode', runId, eventType: 'synabun.confinement', event: confinementEvent(decision, { tool: part.tool, confinement: guard, provider: 'opencode' }) });
1170
+ };
1171
+ // An external_directory ask identifies its originating call, not whether it reads or writes. Resolve that exact
1172
+ // message/call pair from SSE or the message API; never infer a read from the requested path or arbitrary metadata.
1173
+ const permissionTools = new Map();
1174
+ const rememberPermissionTool = (part) => {
1175
+ if (part?.type === 'tool' && part.messageID && part.callID && part.tool) permissionTools.set(`${part.messageID}:${part.callID}`, part.tool);
1176
+ };
1177
+ const permissionToolName = async (event) => {
1178
+ const { messageID, callID } = event?.tool || {};
1179
+ if (!messageID || !callID) return null;
1180
+ const key = `${messageID}:${callID}`;
1181
+ if (!permissionTools.has(key) && typeof client.session?.message === 'function') {
1182
+ try {
1183
+ const row = await settleWithin(client.session.message({ sessionID: sessionId, messageID, directory: cwd || undefined }), 5000);
1184
+ for (const part of row?.data?.parts || []) if (part.messageID === messageID) rememberPermissionTool(part);
1185
+ } catch { /* unknown calls fail closed */ }
1186
+ }
1187
+ return permissionTools.get(key) || null;
1188
+ };
1189
+ /**
1190
+ * A permission ask under the run's confinement: { reply: 'once' | 'reject', message }, the decision written into
1191
+ * the run's events. A refusal carries its reason as the reply's message: OpenCode hands it to the model as the
1192
+ * tool's error (a reject without a message is what OpenCode's own client follows with an interrupt).
1193
+ */
1194
+ const policyReply = async (event) => {
1195
+ if (evaluated) {
1196
+ const reason = evalOpenCodeDecision(event, evaluated);
1197
+ if (!reason) return { reply: 'once' };
1198
+ onEvent({ provider: 'opencode', runId, eventType: 'synabun.confinement', event: { type: 'synabun.confinement', action: 'refused', capability: 'workspace', tool: event.permission || event.type || 'permission', rule: 'skill-eval', reason, provider: 'opencode' } });
1199
+ return { reply: 'reject', message: reason };
1200
+ }
1201
+ if (confined) {
1202
+ const reason = draftOpenCodeDecision(event, confined);
1203
+ if (!reason) return { reply: 'once' };
1204
+ onEvent({ provider: 'opencode', runId, eventType: 'synabun.confinement', event: { type: 'synabun.confinement', action: 'refused', capability: 'workspace', tool: event.permission || event.type || 'permission', rule: 'skill-draft', reason, provider: 'opencode' } });
1205
+ return { reply: 'reject', message: reason };
1206
+ }
1207
+ if (guard) {
1208
+ const toolName = String(event?.permission || event?.type || '').toLowerCase() === 'external_directory' ? await permissionToolName(event) : null;
1209
+ const decision = openCodePermissionDecision(event, guard, { toolName });
1210
+ if (!decision) return { reply: 'once' };
1211
+ onEvent({ provider: 'opencode', runId, eventType: 'synabun.confinement', event: confinementEvent(decision, { tool: event.permission || event.type || 'permission', confinement: guard, provider: 'opencode' }) });
1212
+ return guard.enforced ? { reply: 'reject', message: decision.reason } : { reply: 'once' };
1213
+ }
1214
+ // Unattended and unguarded: allowed, one call at a time. Never "always": OpenCode 2 saves an "always" answer for
1215
+ // the whole project (GET /api/permission/saved), where it would stop the user's own sessions there from asking.
1216
+ return { reply: 'once' };
1217
+ };
753
1218
  let alive = true;
754
1219
  let activeAbort = null;
755
1220
  let activeTurn = null;
@@ -845,7 +1310,7 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
845
1310
  };
846
1311
  const finishTurnWithText = () => {
847
1312
  const turn = activeTurn;
848
- if (!turn || turn.mode !== 'async' || turn.settled || turn.finishing) return;
1313
+ if (!turn || turn.mode !== 'async' || turn.sent === false || turn.settled || turn.finishing) return;
849
1314
  turn.finishing = true;
850
1315
  fetchAssistantText()
851
1316
  .then((text) => settleActiveTurn(null, { text, result: { text }, ...turnMedia() }))
@@ -867,7 +1332,7 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
867
1332
  return;
868
1333
  }
869
1334
  if (/^message[.:]updated$/i.test(eventType)) rememberMessage(event.info || event.message?.info || null);
870
- if (/^message[.:]part[.:]updated$/i.test(eventType)) rememberPart(event.part || null);
1335
+ if (/^message[.:]part[.:]updated$/i.test(eventType)) { rememberPart(event.part || null); rememberPermissionTool(event.part); noteCodeModeCall(event.part); }
871
1336
  if (/^message[.:]part[.:]delta$/i.test(eventType)) rememberDelta(event);
872
1337
  if (/question\.asked/i.test(eventType)) {
873
1338
  const requestID = event.id || event.requestID;
@@ -894,7 +1359,11 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
894
1359
  })
895
1360
  .catch(() => {});
896
1361
  } else if (requestID && client.permission?.reply) {
897
- client.permission.reply({ requestID, reply: 'always', directory: cwd || undefined }).catch(() => {});
1362
+ // Unattended: by the run's confinement (a drafting run, a guarded worker), else allowed once.
1363
+ policyReply(event).then((answer) => {
1364
+ if (!alive) return;
1365
+ return client.permission.reply({ requestID, reply: answer.reply, ...(answer.message ? { message: answer.message } : {}), directory: cwd || undefined });
1366
+ }).catch(() => {});
898
1367
  }
899
1368
  }
900
1369
  // promptAsync acknowledges immediately, so the durable SSE stream is the
@@ -912,11 +1381,23 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
912
1381
  }
913
1382
  onEvent({ provider: 'opencode', runId, eventType, event });
914
1383
  }) || (() => {});
1384
+ // The rules of a confined run ride the session too (lib/opencode2-client.js session.create sends them as the
1385
+ // session's own `permissions`), on top of the serve's config.
1386
+ const sessionRules = confined ? openCodeDraftConfig().permissions : evaluated ? openCodeEvalConfig(evaluated).permissions : guard ? openCodePermissionsFor(guard) : null;
915
1387
  try {
916
1388
  await client.waitUntilConnected?.(10_000);
917
- const created = await client.session.create({ directory: cwd || undefined, title: title || 'SynaBun automation' });
1389
+ const created = options.resumeSessionId
1390
+ ? await client.session.get({ sessionID: options.resumeSessionId })
1391
+ : await client.session.create({ directory: cwd || undefined, title: title || 'SynaBun automation', ...(sessionRules ? { permission: sessionRules } : {}) });
1392
+ if (created?.error) throw new Error(`OpenCode session unavailable: ${providerErrorMessage(created.error)}`);
918
1393
  sessionId = created?.data?.id || created?.data?.sessionID || created?.id || created?.sessionID;
919
1394
  if (!sessionId) throw new Error('OpenCode did not return a session id');
1395
+ if (options.resumeSessionId && sessionId !== options.resumeSessionId) throw new Error('OpenCode returned a different session on resume');
1396
+ if (options.resumeSessionId && sessionRules) {
1397
+ if (!client.session.update) throw new Error('OpenCode cannot restore the worker permission rules');
1398
+ const updated = await client.session.update({ sessionID: sessionId, permission: sessionRules });
1399
+ if (!updated || updated.error) throw new Error('OpenCode could not restore the worker permission rules');
1400
+ }
920
1401
  onIdentity({ providerSessionId: sessionId });
921
1402
  } catch (error) {
922
1403
  try { unsubscribe(); } catch {}
@@ -943,7 +1424,7 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
943
1424
  return {
944
1425
  identity: () => ({ providerSessionId: sessionId }),
945
1426
  isAlive: () => alive,
946
- describe: () => ({ provider: 'opencode', permissionPolicy, capability, askMode }),
1427
+ describe: () => ({ provider: 'opencode', permissionPolicy, capability, askMode, ...(held ? { confinement: held.kind, root: held.root, ...(evaluated ? { role: evaluated.role } : {}) } : {}), ...(guard ? { guard: { mode: guard.mode, capability: guard.capability, writable: [...guard.writable] } } : {}) }),
947
1428
  /** { children(sessionId) → sessions, messages(sessionId) → [{ info }] } on this run's own serve. */
948
1429
  usageFetchers: () => ({
949
1430
  children: (id) => storedRows('children', id),
@@ -952,13 +1433,29 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
952
1433
  async runTurn(prompt, meta = {}) {
953
1434
  if (!alive) throw new Error('OpenCode native loop is closed');
954
1435
  if (activeTurn) throw new Error('OpenCode native loop already has an active turn');
955
- await client.waitUntilConnected?.(10_000);
956
- activeAbort = new AbortController();
1436
+ const controller = new AbortController();
1437
+ activeAbort = controller;
1438
+ const checkReady = async () => {
1439
+ await client.waitUntilConnected?.(10_000);
1440
+ if (!alive || controller.signal.aborted) throw asAbortError('aborted');
1441
+ const ready = await waitForSynabunTools(client, {
1442
+ ...(options.mcpWait || {}), directory: cwd || undefined,
1443
+ expected: options.synabunExpected !== false && !held, signal: controller.signal,
1444
+ stopped: () => !alive,
1445
+ onTimeout: (ms) => {
1446
+ const text = synabunToolsTimeoutText(ms, 'worker');
1447
+ onNote?.(text);
1448
+ onEvent({ provider: 'opencode', runId, eventType: 'synabun.mcp.status', event: { sessionID: sessionId, text } });
1449
+ },
1450
+ });
1451
+ if (!ready || controller.signal.aborted || !alive) throw asAbortError('aborted');
1452
+ };
957
1453
  const request = {
958
1454
  sessionID: sessionId,
959
1455
  parts: [{ type: 'text', text: prompt }],
960
1456
  model: normalizeOpenCodeModel(model),
961
- agent: capability === 'read-only' ? 'plan' : (agent ? String(agent) : 'build'),
1457
+ // Capability is enforced by session rules, the adapter and sandbox, not OpenCode's planning workflow.
1458
+ agent: agent ? String(agent) : 'build',
962
1459
  directory: cwd || undefined,
963
1460
  signal: activeAbort.signal,
964
1461
  };
@@ -994,11 +1491,13 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
994
1491
  // reported as an unhandled promise before runTurn begins awaiting it.
995
1492
  completion.catch(() => {});
996
1493
  const turn = {
997
- mode: 'async', completion,
1494
+ mode: 'async', completion, sent: false,
998
1495
  resolve: resolveTurn, reject: rejectTurn, settled: false,
999
1496
  };
1000
1497
  activeTurn = turn;
1001
1498
  try {
1499
+ await checkReady();
1500
+ turn.sent = true;
1002
1501
  const response = await client.session.promptAsync(request);
1003
1502
  validateResponse(response);
1004
1503
  return await completion;
@@ -1011,6 +1510,7 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
1011
1510
  const turn = { mode: 'sync' };
1012
1511
  activeTurn = turn;
1013
1512
  try {
1513
+ await checkReady();
1014
1514
  const response = await client.session.prompt(request);
1015
1515
  validateResponse(response);
1016
1516
  const text = extractOpenCodeText(response);