synabun 2.0.10 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (656) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/README.md +25 -2
  3. package/THIRD-PARTY-LICENSES.md +22 -0
  4. package/bin/synabun.js +71 -24
  5. package/docs/whatsapp.md +13 -13
  6. package/hooks/claude-code/pre-websearch.mjs +12 -1
  7. package/hooks/claude-code/shared.mjs +5 -0
  8. package/hooks/claude-code/stop.mjs +1 -1
  9. package/lib/claude-hooks.js +172 -18
  10. package/lib/client-config-repair.js +30 -17
  11. package/lib/desktop-app.js +7 -1
  12. package/lib/external-tools.js +7 -6
  13. package/lib/first-launch-protection.js +57 -12
  14. package/lib/mcp-dist.js +176 -0
  15. package/lib/release-npm-ci.mjs +138 -0
  16. package/lib/release-publish.mjs +91 -0
  17. package/lib/release-scan.mjs +50 -0
  18. package/lib/server-crash-record.js +123 -0
  19. package/lib/server-exit.js +318 -0
  20. package/lib/server-supervisor.js +508 -0
  21. package/lib/start-launcher.js +37 -4
  22. package/lib/tools-cli.js +45 -0
  23. package/lib/update-snapshot.js +302 -0
  24. package/lib/virustotal.mjs +203 -0
  25. package/lib/windows-crash-event.js +269 -0
  26. package/mcp-server/dist/http.d.ts +20 -1
  27. package/mcp-server/dist/http.d.ts.map +1 -1
  28. package/mcp-server/dist/http.js +59 -12
  29. package/mcp-server/dist/http.js.map +1 -1
  30. package/mcp-server/dist/index.d.ts +33 -27
  31. package/mcp-server/dist/index.d.ts.map +1 -1
  32. package/mcp-server/dist/index.js +128 -143
  33. package/mcp-server/dist/index.js.map +1 -1
  34. package/mcp-server/dist/preload.js +1 -1
  35. package/mcp-server/dist/preload.js.map +1 -1
  36. package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
  37. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
  38. package/mcp-server/dist/services/budget-judgments.js +5 -3
  39. package/mcp-server/dist/services/budget-judgments.js.map +1 -1
  40. package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
  41. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  42. package/mcp-server/dist/services/codex-config-heal.js +81 -28
  43. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  44. package/mcp-server/dist/services/identity.d.ts +2 -2
  45. package/mcp-server/dist/services/identity.js +2 -2
  46. package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
  47. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
  48. package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
  49. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
  50. package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
  51. package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
  52. package/mcp-server/dist/services/memory-sessions.js +28 -2
  53. package/mcp-server/dist/services/memory-sessions.js.map +1 -1
  54. package/mcp-server/dist/services/neural-interface.d.ts +18 -11
  55. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  56. package/mcp-server/dist/services/neural-interface.js +67 -11
  57. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  58. package/mcp-server/dist/services/server-instructions.d.ts +7 -2
  59. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  60. package/mcp-server/dist/services/server-instructions.js +22 -63
  61. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  62. package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
  63. package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
  64. package/mcp-server/dist/services/tool-catalog.js +339 -0
  65. package/mcp-server/dist/services/tool-catalog.js.map +1 -0
  66. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
  67. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
  68. package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
  69. package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
  70. package/mcp-server/dist/services/tool-list.d.ts +108 -0
  71. package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
  72. package/mcp-server/dist/services/tool-list.js +334 -0
  73. package/mcp-server/dist/services/tool-list.js.map +1 -0
  74. package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
  75. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
  76. package/mcp-server/dist/services/tool-runtime.js +400 -0
  77. package/mcp-server/dist/services/tool-runtime.js.map +1 -0
  78. package/mcp-server/dist/services/typesafe-config.js +1 -1
  79. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  80. package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
  81. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
  82. package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
  83. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
  84. package/mcp-server/dist/tools/agents.d.ts +14 -14
  85. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  86. package/mcp-server/dist/tools/agents.js +29 -19
  87. package/mcp-server/dist/tools/agents.js.map +1 -1
  88. package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
  89. package/mcp-server/dist/tools/choice.d.ts +6 -0
  90. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  91. package/mcp-server/dist/tools/choice.js +18 -13
  92. package/mcp-server/dist/tools/choice.js.map +1 -1
  93. package/mcp-server/dist/tools/git-tools.js +2 -2
  94. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  95. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  96. package/mcp-server/dist/tools/image-tools.js +1 -1
  97. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  98. package/mcp-server/dist/tools/morelogin.js +9 -6
  99. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  100. package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
  101. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
  102. package/mcp-server/dist/tools/skill-tools.js +642 -0
  103. package/mcp-server/dist/tools/skill-tools.js.map +1 -0
  104. package/mcp-server/dist/tools/skill.d.ts +7 -0
  105. package/mcp-server/dist/tools/skill.d.ts.map +1 -0
  106. package/mcp-server/dist/tools/skill.js +11 -0
  107. package/mcp-server/dist/tools/skill.js.map +1 -0
  108. package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
  109. package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
  110. package/mcp-server/dist/tools/tool-groups.js +147 -0
  111. package/mcp-server/dist/tools/tool-groups.js.map +1 -0
  112. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
  113. package/mcp-server/dist/tools/whiteboard-format.js +7 -4
  114. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
  115. package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
  116. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  117. package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
  118. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  119. package/mcp-server/package-lock.json +2 -2
  120. package/mcp-server/package.json +1 -1
  121. package/mcp-server/src/http.ts +75 -12
  122. package/mcp-server/src/index.ts +147 -154
  123. package/mcp-server/src/preload.ts +12 -12
  124. package/mcp-server/src/services/budget-judgments.ts +5 -3
  125. package/mcp-server/src/services/codex-config-heal.ts +80 -29
  126. package/mcp-server/src/services/identity.ts +2 -2
  127. package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
  128. package/mcp-server/src/services/memory-sessions.ts +24 -3
  129. package/mcp-server/src/services/neural-interface.ts +81 -21
  130. package/mcp-server/src/services/server-instructions.ts +29 -64
  131. package/mcp-server/src/services/tool-catalog.ts +403 -0
  132. package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
  133. package/mcp-server/src/services/tool-list.ts +410 -0
  134. package/mcp-server/src/services/tool-runtime.ts +457 -0
  135. package/mcp-server/src/services/typesafe-config.ts +1 -1
  136. package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
  137. package/mcp-server/src/tools/agents.ts +29 -20
  138. package/mcp-server/src/tools/choice.ts +17 -12
  139. package/mcp-server/src/tools/git-tools.ts +2 -2
  140. package/mcp-server/src/tools/image-tools.ts +1 -1
  141. package/mcp-server/src/tools/morelogin.ts +6 -4
  142. package/mcp-server/src/tools/skill-tools.ts +592 -0
  143. package/mcp-server/src/tools/skill.ts +13 -0
  144. package/mcp-server/src/tools/tool-groups.ts +164 -0
  145. package/mcp-server/src/tools/whiteboard-format.ts +9 -3
  146. package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
  147. package/neural-interface/i18n/en.json +3953 -228
  148. package/neural-interface/i18n/pt-BR.json +3901 -179
  149. package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
  150. package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
  151. package/neural-interface/lib/antigravity/acp-connection.js +299 -0
  152. package/neural-interface/lib/antigravity/bypass.js +100 -0
  153. package/neural-interface/lib/antigravity/catalog.js +273 -0
  154. package/neural-interface/lib/antigravity/home.js +260 -0
  155. package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
  156. package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
  157. package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
  158. package/neural-interface/lib/antigravity/policy.js +74 -0
  159. package/neural-interface/lib/antigravity/tool-call.js +178 -0
  160. package/neural-interface/lib/antigravity/transcripts.js +252 -0
  161. package/neural-interface/lib/antigravity/usage.js +43 -0
  162. package/neural-interface/lib/assistant-api.js +128 -28
  163. package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
  164. package/neural-interface/lib/assistant-brains/claude.js +4 -3
  165. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
  166. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
  167. package/neural-interface/lib/assistant-brains/codex.js +9 -5
  168. package/neural-interface/lib/assistant-brains/opencode.js +120 -54
  169. package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +28 -13
  170. package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +59 -19
  171. package/neural-interface/lib/assistant-budget-planner.js +7 -3
  172. package/neural-interface/lib/assistant-budget-split.js +42 -15
  173. package/neural-interface/lib/assistant-catalog.js +81 -15
  174. package/neural-interface/lib/assistant-config.js +6 -3
  175. package/neural-interface/lib/assistant-dispatch.js +1073 -200
  176. package/neural-interface/lib/assistant-envelope.js +156 -2
  177. package/neural-interface/lib/assistant-evidence.js +80 -0
  178. package/neural-interface/lib/assistant-hidden-models.js +7 -5
  179. package/neural-interface/lib/assistant-persona.js +93 -19
  180. package/neural-interface/lib/assistant-plan-permissions.js +25 -9
  181. package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
  182. package/neural-interface/lib/assistant-route-gate.js +6 -4
  183. package/neural-interface/lib/assistant-router.js +245 -38
  184. package/neural-interface/lib/assistant-runtime.js +214 -30
  185. package/neural-interface/lib/assistant-task-prompt.js +87 -21
  186. package/neural-interface/lib/assistant-tools.js +51 -0
  187. package/neural-interface/lib/assistant-usage.js +29 -0
  188. package/neural-interface/lib/backup-service.js +626 -4
  189. package/neural-interface/lib/backup-zip-reader.js +10 -5
  190. package/neural-interface/lib/backup-zip-writer.js +257 -0
  191. package/neural-interface/lib/browser-execution.js +10 -2
  192. package/neural-interface/lib/browser-guard-cdp.js +84 -0
  193. package/neural-interface/lib/browser-network-guard.js +476 -0
  194. package/neural-interface/lib/browser-policy-roots.js +64 -0
  195. package/neural-interface/lib/browser-tool-policy.js +198 -38
  196. package/neural-interface/lib/claude-agent-bridge.js +11 -2
  197. package/neural-interface/lib/claude-history-cache.js +210 -0
  198. package/neural-interface/lib/claude-hooks-status.js +103 -0
  199. package/neural-interface/lib/claude-model-catalog.js +5 -1
  200. package/neural-interface/lib/claude-temporary.js +5 -11
  201. package/neural-interface/lib/cli-install-api.js +1 -1
  202. package/neural-interface/lib/cli-install.js +163 -154
  203. package/neural-interface/lib/cli-resolver.js +13 -3
  204. package/neural-interface/lib/cli-update-planner.js +52 -41
  205. package/neural-interface/lib/cloudflared/tunnel.js +42 -2
  206. package/neural-interface/lib/codex-mcp-approvals.js +11 -0
  207. package/neural-interface/lib/codex-rollout-reader.js +52 -0
  208. package/neural-interface/lib/codex-worker-app-server.js +191 -0
  209. package/neural-interface/lib/confined-fs.js +30 -3
  210. package/neural-interface/lib/crash-notice-api.js +98 -0
  211. package/neural-interface/lib/editor-completion-opencode.js +68 -68
  212. package/neural-interface/lib/editor-git-api.js +3 -20
  213. package/neural-interface/lib/exec-loop-command.js +13 -10
  214. package/neural-interface/lib/file-explorer-api.js +74 -40
  215. package/neural-interface/lib/file-explorer-watch.js +9 -3
  216. package/neural-interface/lib/git-bash/install.js +52 -6
  217. package/neural-interface/lib/git-bash/manager.js +59 -17
  218. package/neural-interface/lib/git-bash/pins.js +12 -0
  219. package/neural-interface/lib/git-read.js +2 -2
  220. package/neural-interface/lib/git-status-runner.js +390 -0
  221. package/neural-interface/lib/git-status-watchdog.js +40 -0
  222. package/neural-interface/lib/greeting/budgets.js +1 -0
  223. package/neural-interface/lib/greeting/engine.js +2 -2
  224. package/neural-interface/lib/greeting/sources.js +10 -12
  225. package/neural-interface/lib/greeting/writer.js +1 -1
  226. package/neural-interface/lib/http-guards.js +8 -2
  227. package/neural-interface/lib/install-details.js +4 -1
  228. package/neural-interface/lib/mcp-agent-servers.js +67 -0
  229. package/neural-interface/lib/mcp-client-config.js +35 -33
  230. package/neural-interface/lib/mcp-external-servers.js +332 -0
  231. package/neural-interface/lib/mcp-installer.js +2 -1
  232. package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
  233. package/neural-interface/lib/memory-session-capture.js +17 -3
  234. package/neural-interface/lib/morelogin.js +177 -25
  235. package/neural-interface/lib/native-loop-providers.js +965 -66
  236. package/neural-interface/lib/native-loop-runtime.js +290 -42
  237. package/neural-interface/lib/opencode-config.js +557 -0
  238. package/neural-interface/lib/opencode-db-schema.js +17 -34
  239. package/neural-interface/lib/opencode-history.js +48 -218
  240. package/neural-interface/lib/opencode-mcp-ready.js +108 -0
  241. package/neural-interface/lib/opencode-native-boundary.js +53 -0
  242. package/neural-interface/lib/opencode-protocol.js +249 -112
  243. package/neural-interface/lib/opencode-runtime-config.js +32 -22
  244. package/neural-interface/lib/opencode-serve-records.js +39 -21
  245. package/neural-interface/lib/opencode-v2-client.js +48 -153
  246. package/neural-interface/lib/opencode-v2-ws-requests.js +36 -210
  247. package/neural-interface/lib/opencode-worker-data.js +27 -0
  248. package/neural-interface/lib/opencode2-client.js +73 -37
  249. package/neural-interface/lib/opencode2-proxy.js +23 -63
  250. package/neural-interface/lib/opencode2-translate.js +51 -2
  251. package/neural-interface/lib/owner-authority.js +249 -0
  252. package/neural-interface/lib/page-session.js +275 -0
  253. package/neural-interface/lib/path-confine.js +12 -9
  254. package/neural-interface/lib/peer-identity.js +793 -0
  255. package/neural-interface/lib/pty-host/core.js +28 -1
  256. package/neural-interface/lib/pty-host/lifetime.js +12 -0
  257. package/neural-interface/lib/pty-host/manager.js +12 -1
  258. package/neural-interface/lib/remote-policy.js +1 -1
  259. package/neural-interface/lib/request-gate.js +20 -2
  260. package/neural-interface/lib/rulesets/api.js +7 -6
  261. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  262. package/neural-interface/lib/rulesets/installer.js +180 -36
  263. package/neural-interface/lib/rulesets/json-edit.js +1 -1
  264. package/neural-interface/lib/rulesets/render.js +1 -1
  265. package/neural-interface/lib/session-cache-builder.js +81 -31
  266. package/neural-interface/lib/session-title-generator.js +58 -36
  267. package/neural-interface/lib/skills/api-v2.js +495 -0
  268. package/neural-interface/lib/skills/bundle.js +418 -0
  269. package/neural-interface/lib/skills/catalog.js +298 -0
  270. package/neural-interface/lib/skills/creation-api.js +191 -0
  271. package/neural-interface/lib/skills/creation-brief.js +247 -0
  272. package/neural-interface/lib/skills/creation-sources.js +285 -0
  273. package/neural-interface/lib/skills/creation.js +843 -0
  274. package/neural-interface/lib/skills/deploy.js +569 -0
  275. package/neural-interface/lib/skills/digest.js +119 -0
  276. package/neural-interface/lib/skills/discover.js +111 -0
  277. package/neural-interface/lib/skills/draft-confinement.js +216 -0
  278. package/neural-interface/lib/skills/eval-confinement.js +399 -0
  279. package/neural-interface/lib/skills/eval-sandbox.js +280 -0
  280. package/neural-interface/lib/skills/eval-trace.js +135 -0
  281. package/neural-interface/lib/skills/evals-api.js +212 -0
  282. package/neural-interface/lib/skills/evals.js +1573 -0
  283. package/neural-interface/lib/skills/export.js +203 -0
  284. package/neural-interface/lib/skills/frontmatter.js +64 -0
  285. package/neural-interface/lib/skills/health.js +75 -0
  286. package/neural-interface/lib/skills/json-edit.js +165 -0
  287. package/neural-interface/lib/skills/known-bridge-digests.json +441 -0
  288. package/neural-interface/lib/skills/known-bridges.js +96 -0
  289. package/neural-interface/lib/skills/library.js +823 -0
  290. package/neural-interface/lib/skills/local-store.js +21 -0
  291. package/neural-interface/lib/skills/migration.js +433 -0
  292. package/neural-interface/lib/skills/operations.js +45 -0
  293. package/neural-interface/lib/skills/ownership.js +427 -0
  294. package/neural-interface/lib/skills/packages.js +88 -0
  295. package/neural-interface/lib/skills/reviews.js +212 -0
  296. package/neural-interface/lib/skills/revision.js +80 -0
  297. package/neural-interface/lib/skills/roots.js +269 -0
  298. package/neural-interface/lib/skills/runtime-checks.js +358 -0
  299. package/neural-interface/lib/skills/scan-assets.js +252 -0
  300. package/neural-interface/lib/skills/scan-context.js +26 -0
  301. package/neural-interface/lib/skills/scan-shell.js +324 -0
  302. package/neural-interface/lib/skills/scan.js +364 -0
  303. package/neural-interface/lib/skills/scanners.js +54 -0
  304. package/neural-interface/lib/skills/service.js +530 -0
  305. package/neural-interface/lib/skills/telemetry.js +119 -0
  306. package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
  307. package/neural-interface/lib/synabun-version.js +1 -1
  308. package/neural-interface/lib/system-activity.js +1 -1
  309. package/neural-interface/lib/tool-lists.js +124 -0
  310. package/neural-interface/lib/whatsapp/brain.js +7 -3
  311. package/neural-interface/lib/whatsapp/bridge.js +6 -3
  312. package/neural-interface/lib/whatsapp/cards.js +22 -9
  313. package/neural-interface/lib/whatsapp/config.js +1 -1
  314. package/neural-interface/lib/worker-confinement.js +1268 -0
  315. package/neural-interface/lib/worker-guard-setting.js +56 -0
  316. package/neural-interface/lib/worker-mcp-channel.js +175 -0
  317. package/neural-interface/lib/worker-question.js +74 -0
  318. package/neural-interface/lib/worker-relay-ports.js +227 -0
  319. package/neural-interface/lib/worker-resume.js +238 -0
  320. package/neural-interface/lib/worker-sandbox.js +329 -0
  321. package/neural-interface/package-lock.json +34 -12
  322. package/neural-interface/package.json +3 -2
  323. package/neural-interface/public/index.html +3 -1
  324. package/neural-interface/public/index2d.html +19 -11
  325. package/neural-interface/public/onboarding.html +3 -3
  326. package/neural-interface/public/shared/account-menu.js +8 -5
  327. package/neural-interface/public/shared/agent-runtime-options.js +125 -31
  328. package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
  329. package/neural-interface/public/shared/agy/agy-client.js +266 -0
  330. package/neural-interface/public/shared/agy/agy-commands.js +120 -0
  331. package/neural-interface/public/shared/agy/agy-copy.js +326 -0
  332. package/neural-interface/public/shared/agy/agy-diff.js +161 -0
  333. package/neural-interface/public/shared/agy/agy-events.js +469 -0
  334. package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
  335. package/neural-interface/public/shared/agy/agy-options.js +232 -0
  336. package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
  337. package/neural-interface/public/shared/agy/agy-render.js +392 -0
  338. package/neural-interface/public/shared/agy/agy-setup.js +149 -0
  339. package/neural-interface/public/shared/agy/agy-styles.js +604 -0
  340. package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
  341. package/neural-interface/public/shared/api.js +1527 -1336
  342. package/neural-interface/public/shared/app-frame.js +192 -0
  343. package/neural-interface/public/shared/assistant/asst-agents-dock.js +36 -6
  344. package/neural-interface/public/shared/assistant/asst-api.js +1 -1
  345. package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
  346. package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
  347. package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
  348. package/neural-interface/public/shared/assistant/asst-brain-picker.js +105 -44
  349. package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
  350. package/neural-interface/public/shared/assistant/asst-control.js +205 -26
  351. package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
  352. package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
  353. package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
  354. package/neural-interface/public/shared/assistant/asst-limit-dock.js +45 -0
  355. package/neural-interface/public/shared/assistant/asst-limit-wait.js +327 -0
  356. package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
  357. package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
  358. package/neural-interface/public/shared/assistant/asst-menu.js +7 -6
  359. package/neural-interface/public/shared/assistant/asst-panel.js +151 -19
  360. package/neural-interface/public/shared/assistant/asst-rack-styles.js +66 -49
  361. package/neural-interface/public/shared/assistant/asst-render.js +445 -82
  362. package/neural-interface/public/shared/assistant/asst-route.js +304 -53
  363. package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
  364. package/neural-interface/public/shared/assistant/asst-scenes.js +56 -1
  365. package/neural-interface/public/shared/assistant/asst-sidepanel.js +117 -10
  366. package/neural-interface/public/shared/assistant/asst-slash.js +17 -9
  367. package/neural-interface/public/shared/assistant/asst-state.js +87 -24
  368. package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
  369. package/neural-interface/public/shared/assistant/asst-styles.js +93 -21
  370. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +190 -23
  371. package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
  372. package/neural-interface/public/shared/automation-context.js +5 -2
  373. package/neural-interface/public/shared/cc-dropdown.js +89 -19
  374. package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
  375. package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
  376. package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
  377. package/neural-interface/public/shared/cdx/cdx-output.js +34 -10
  378. package/neural-interface/public/shared/cdx/cdx-panel.js +59 -151
  379. package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
  380. package/neural-interface/public/shared/cdx/cdx-protocol.js +39 -11
  381. package/neural-interface/public/shared/cdx/cdx-render.js +57 -38
  382. package/neural-interface/public/shared/cdx/cdx-requests.js +14 -4
  383. package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
  384. package/neural-interface/public/shared/cdx/cdx-styles.js +15 -3
  385. package/neural-interface/public/shared/cdx/cdx-tabs.js +127 -44
  386. package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
  387. package/neural-interface/public/shared/cli-install-controller.js +218 -0
  388. package/neural-interface/public/shared/cli-install-view.js +11 -12
  389. package/neural-interface/public/shared/cli-status.js +11 -10
  390. package/neural-interface/public/shared/constants.js +5 -4
  391. package/neural-interface/public/shared/context-menu-areas.js +34 -0
  392. package/neural-interface/public/shared/context-menu-logic.js +243 -0
  393. package/neural-interface/public/shared/context-menu-model.js +367 -0
  394. package/neural-interface/public/shared/context-menu.js +372 -0
  395. package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
  396. package/neural-interface/public/shared/cp/cp-restore.js +162 -0
  397. package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
  398. package/neural-interface/public/shared/crash-notice-view.js +168 -0
  399. package/neural-interface/public/shared/desktop-bridge.js +17 -0
  400. package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
  401. package/neural-interface/public/shared/desktop-titlebar.js +13 -4
  402. package/neural-interface/public/shared/desktop-update-view.js +21 -0
  403. package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
  404. package/neural-interface/public/shared/explorer/fx-api.js +4 -4
  405. package/neural-interface/public/shared/explorer/fx-links.js +3 -3
  406. package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
  407. package/neural-interface/public/shared/explorer/fx-main.js +48 -16
  408. package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
  409. package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
  410. package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
  411. package/neural-interface/public/shared/html-shell.js +30 -15
  412. package/neural-interface/public/shared/i18n.js +2 -0
  413. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
  414. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
  415. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
  416. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +2 -0
  417. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
  418. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
  419. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
  420. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
  421. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
  422. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
  423. package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
  424. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +74 -259
  425. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
  426. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +95 -202
  427. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
  428. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +13 -22
  429. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +4 -4
  430. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
  431. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +2 -91
  432. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
  433. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
  434. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
  435. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
  436. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -21
  437. package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
  438. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +60 -50
  439. package/neural-interface/public/shared/page-session.js +96 -0
  440. package/neural-interface/public/shared/provider-icons.js +4 -2
  441. package/neural-interface/public/shared/registry.js +5 -3
  442. package/neural-interface/public/shared/retired-2d.js +3 -0
  443. package/neural-interface/public/shared/schedules-view.js +195 -61
  444. package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
  445. package/neural-interface/public/shared/settings/settings-data.js +3 -1
  446. package/neural-interface/public/shared/settings/settings-ia.js +6 -6
  447. package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
  448. package/neural-interface/public/shared/settings/settings-rules.js +2 -2
  449. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
  450. package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
  451. package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
  452. package/neural-interface/public/shared/showcase-privacy.js +6 -1
  453. package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
  454. package/neural-interface/public/shared/skills/ss-args.js +96 -0
  455. package/neural-interface/public/shared/skills/ss-creation.js +222 -0
  456. package/neural-interface/public/shared/skills/ss-dialog.js +133 -0
  457. package/neural-interface/public/shared/skills/ss-doc.js +173 -0
  458. package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
  459. package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
  460. package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
  461. package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
  462. package/neural-interface/public/shared/skills/ss-evals.js +417 -0
  463. package/neural-interface/public/shared/skills/ss-form.js +159 -0
  464. package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
  465. package/neural-interface/public/shared/skills/ss-ids.js +32 -0
  466. package/neural-interface/public/shared/skills/ss-lint.js +149 -0
  467. package/neural-interface/public/shared/skills/ss-presets.js +117 -0
  468. package/neural-interface/public/shared/skills/ss-profiles.js +513 -0
  469. package/neural-interface/public/shared/skills/ss-resources.js +165 -0
  470. package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
  471. package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
  472. package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
  473. package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
  474. package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
  475. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
  476. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +237 -0
  477. package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
  478. package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
  479. package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
  480. package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
  481. package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
  482. package/neural-interface/public/shared/skills/studio/sx-dom.js +230 -0
  483. package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
  484. package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
  485. package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
  486. package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
  487. package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
  488. package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
  489. package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
  490. package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
  491. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +282 -0
  492. package/neural-interface/public/shared/skills/studio/sx-install.js +313 -0
  493. package/neural-interface/public/shared/skills/studio/sx-library-view.js +748 -0
  494. package/neural-interface/public/shared/skills/studio/sx-library.js +479 -0
  495. package/neural-interface/public/shared/skills/studio/sx-main.js +690 -0
  496. package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
  497. package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
  498. package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
  499. package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
  500. package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
  501. package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
  502. package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
  503. package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
  504. package/neural-interface/public/shared/skills/studio/sx-subject.js +249 -0
  505. package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
  506. package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
  507. package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
  508. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +426 -0
  509. package/neural-interface/public/shared/state.js +1 -1
  510. package/neural-interface/public/shared/streaming-markdown.js +58 -0
  511. package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
  512. package/neural-interface/public/shared/styles.css +1822 -1756
  513. package/neural-interface/public/shared/synabun-mascot.js +1115 -9
  514. package/neural-interface/public/shared/text-cache.js +29 -0
  515. package/neural-interface/public/shared/tool-list-picker.js +55 -0
  516. package/neural-interface/public/shared/tool-list-storage.js +24 -0
  517. package/neural-interface/public/shared/tool-list-view.js +112 -0
  518. package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
  519. package/neural-interface/public/shared/tour/tour-actor.js +268 -0
  520. package/neural-interface/public/shared/tour/tour-beats.js +336 -0
  521. package/neural-interface/public/shared/tour/tour-engine.js +884 -0
  522. package/neural-interface/public/shared/tour/tour-ink.js +390 -0
  523. package/neural-interface/public/shared/tour/tour-stage.js +440 -0
  524. package/neural-interface/public/shared/tour/tour-state.js +40 -0
  525. package/neural-interface/public/shared/tour/tour-tips.js +99 -0
  526. package/neural-interface/public/shared/tour/tour.css +144 -0
  527. package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
  528. package/neural-interface/public/shared/ui-assistant.js +2 -0
  529. package/neural-interface/public/shared/ui-automation-studio.js +111 -140
  530. package/neural-interface/public/shared/ui-claude-panel.js +376 -82
  531. package/neural-interface/public/shared/ui-cli-banner.js +23 -167
  532. package/neural-interface/public/shared/ui-command-runner.js +6 -3
  533. package/neural-interface/public/shared/ui-context-menu.js +295 -0
  534. package/neural-interface/public/shared/ui-cost-widget.js +57 -59
  535. package/neural-interface/public/shared/ui-crash-notice.js +187 -0
  536. package/neural-interface/public/shared/ui-detail.js +15 -3
  537. package/neural-interface/public/shared/ui-file-explorer.js +48 -6
  538. package/neural-interface/public/shared/ui-file-links.js +2 -2
  539. package/neural-interface/public/shared/ui-games.js +2 -2
  540. package/neural-interface/public/shared/ui-image-gallery.js +50 -47
  541. package/neural-interface/public/shared/ui-judgments.js +1 -1
  542. package/neural-interface/public/shared/ui-keybinds.js +33 -2
  543. package/neural-interface/public/shared/ui-link.js +3 -3
  544. package/neural-interface/public/shared/ui-menubar.js +10 -20
  545. package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
  546. package/neural-interface/public/shared/ui-native-loop-router.js +11 -1
  547. package/neural-interface/public/shared/ui-navbar.js +49 -24
  548. package/neural-interface/public/shared/ui-notifications.js +1 -0
  549. package/neural-interface/public/shared/ui-panels.js +96 -65
  550. package/neural-interface/public/shared/ui-resume.js +9 -7
  551. package/neural-interface/public/shared/ui-schedules-studio.js +546 -431
  552. package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
  553. package/neural-interface/public/shared/ui-sessions.js +27 -15
  554. package/neural-interface/public/shared/ui-settings.js +683 -526
  555. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
  556. package/neural-interface/public/shared/ui-sidepanel-sessions.js +4 -4
  557. package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
  558. package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
  559. package/neural-interface/public/shared/ui-sidepanel-windows.js +18 -13
  560. package/neural-interface/public/shared/ui-skills.js +33 -2830
  561. package/neural-interface/public/shared/ui-styleguide.js +5 -2
  562. package/neural-interface/public/shared/ui-sync.js +15 -5
  563. package/neural-interface/public/shared/ui-terminal.js +270 -190
  564. package/neural-interface/public/shared/ui-tool-updates.js +6 -4
  565. package/neural-interface/public/shared/ui-tooltip.js +81 -51
  566. package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
  567. package/neural-interface/public/shared/ui-trash.js +6 -4
  568. package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
  569. package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
  570. package/neural-interface/public/shared/ui-tutorial-steps.js +3 -65
  571. package/neural-interface/public/shared/ui-tutorial.css +47 -100
  572. package/neural-interface/public/shared/ui-tutorial.js +285 -509
  573. package/neural-interface/public/shared/ui-update.js +38 -2
  574. package/neural-interface/public/shared/ui-whiteboard.js +312 -59
  575. package/neural-interface/public/shared/ui-worker-end.js +5 -1
  576. package/neural-interface/public/shared/ui-workspaces.js +3 -2
  577. package/neural-interface/public/shared/view-variant.js +66 -0
  578. package/neural-interface/public/shared/whatsapp/wa-view.js +3 -3
  579. package/neural-interface/public/shared/whiteboard-list.js +172 -0
  580. package/neural-interface/public/shared/worker-end.js +10 -3
  581. package/neural-interface/public/shared/worker-questions.js +34 -0
  582. package/neural-interface/public/shared/worker-restart.js +12 -0
  583. package/neural-interface/public/sidepanel-session.html +2 -2
  584. package/neural-interface/public/variant/2d/background.js +2 -1
  585. package/neural-interface/public/variant/2d/context-menu.js +8 -2
  586. package/neural-interface/public/variant/2d/graph.js +42 -20
  587. package/neural-interface/public/variant/2d/hulls.js +14 -9
  588. package/neural-interface/public/variant/2d/lasso.js +11 -7
  589. package/neural-interface/public/variant/2d/main.js +32 -1
  590. package/neural-interface/public/variant/2d/minimap.js +11 -13
  591. package/neural-interface/public/variant/3d/main.js +69 -6
  592. package/neural-interface/public/variant/3d/map-controls.js +24 -9
  593. package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
  594. package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
  595. package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
  596. package/neural-interface/public/vendor/caveat/caveat.css +17 -0
  597. package/neural-interface/public/vendor/caveat/manifest.json +12 -0
  598. package/neural-interface/public/vendor/yaml/LICENSE +13 -0
  599. package/neural-interface/public/vendor/yaml/manifest.json +4 -0
  600. package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
  601. package/neural-interface/scripts/build-vendor.mjs +32 -0
  602. package/neural-interface/server.js +4553 -2855
  603. package/neural-interface/templates/rulesets/README.md +1 -1
  604. package/neural-interface/templates/rulesets/core.md +1 -1
  605. package/neural-interface/templates/rulesets/legacy-hashes.json +23 -5
  606. package/neural-interface/templates/rulesets/manifest.json +8 -8
  607. package/npm-shrinkwrap.json +34 -12
  608. package/package.json +17 -3
  609. package/postinstall.js +9 -4
  610. package/preuninstall.js +1 -1
  611. package/setup.js +97 -322
  612. package/skills/skill-forge/SKILL.md +177 -0
  613. package/skills/skill-forge/evals/evals.json +54 -0
  614. package/skills/skill-forge/references/package.md +105 -0
  615. package/skills/skill-forge/references/review-and-recovery.md +63 -0
  616. package/skills/skill-forge/references/runtime-profiles.md +20 -0
  617. package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
  618. package/skills/synabun/SKILL.md +10 -7
  619. package/skills/synabun/modules/audit.md +1 -5
  620. package/skills/synabun/modules/changelog.md +1 -5
  621. package/skills/synabun/modules/idea.md +1 -1
  622. package/skills/synabun/modules/memorize.md +6 -9
  623. package/skills/synabun/modules/schedule.md +6 -6
  624. package/mcp-server/dist/services/profiles.d.ts +0 -120
  625. package/mcp-server/dist/services/profiles.d.ts.map +0 -1
  626. package/mcp-server/dist/services/profiles.js +0 -387
  627. package/mcp-server/dist/services/profiles.js.map +0 -1
  628. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
  629. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
  630. package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
  631. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
  632. package/mcp-server/dist/tools/leonardo.d.ts +0 -13
  633. package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
  634. package/mcp-server/dist/tools/leonardo.js +0 -21
  635. package/mcp-server/dist/tools/leonardo.js.map +0 -1
  636. package/mcp-server/dist/tools/profile.d.ts +0 -17
  637. package/mcp-server/dist/tools/profile.d.ts.map +0 -1
  638. package/mcp-server/dist/tools/profile.js +0 -114
  639. package/mcp-server/dist/tools/profile.js.map +0 -1
  640. package/mcp-server/src/services/profiles.ts +0 -428
  641. package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
  642. package/mcp-server/src/tools/leonardo.ts +0 -30
  643. package/mcp-server/src/tools/profile.ts +0 -122
  644. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
  645. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
  646. package/neural-interface/lib/mcp-profile-servers.js +0 -120
  647. package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
  648. package/skills/leonardo/SKILL.md +0 -168
  649. package/skills/leonardo/icon.svg +0 -1
  650. package/skills/leonardo/modules/browser-guide.md +0 -378
  651. package/skills/leonardo/modules/image-prompter.md +0 -283
  652. package/skills/leonardo/modules/model-advisor.md +0 -100
  653. package/skills/leonardo/modules/prompt-library.md +0 -110
  654. package/skills/leonardo/modules/style-guide.md +0 -186
  655. package/skills/leonardo/modules/video-prompter.md +0 -317
  656. /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
@@ -1,16 +1,36 @@
1
- import { detectProject as memoryProject } from '../../mcp-server/dist/config.js';
1
+ import { codexWorkerReply, CODEX_QUESTIONS, elicitationReply, openCodeAnswers, workerQuestions } from './worker-question.js';
2
+ import { mkdtempSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { externalServerInventory, selectExternalServers, codexMcpEnabledOverride, referenceClaudeMap, waitForClaudeMcpScope, redactMcpValues, writePrivateMcpConfig, mcpDefinitionValues } from './mcp-external-servers.js';
5
+ import { resolveStoredTools, runtimeToolsValue, readDefaultTools } from './tool-lists.js';
6
+ import { waitForSynabunTools, synabunToolsTimeoutText } from './opencode-mcp-ready.js';
7
+ import { config as memoryConfig, detectProject as memoryProject } from '../../mcp-server/dist/config.js';
2
8
  import { Codex } from '@openai/codex-sdk';
9
+ import { CodexWorkerAppServer } from './codex-worker-app-server.js';
3
10
  import { query as claudeQuery } from '@anthropic-ai/claude-agent-sdk';
4
11
  import { CLAUDE_EFFORT_LEVELS } from './claude-model-catalog.js';
5
12
  import { CLAUDE_NOT_INSTALLED, resolveClaudeSdkExecutable } from './claude-executable.js';
6
13
  import { extractOpenCodeText } from './session-title-generator.js';
14
+ import { withServeEnv } from './opencode-protocol.js';
7
15
  import { patchCodexLineSplitting } from './jsonl-lines.js';
8
- import { dirname } from 'node:path';
16
+ import { dirname, join } from 'node:path';
9
17
  import { codexReasoningEffort } from './effort-levels.js';
18
+ import { spawn as spawnProcess } from 'node:child_process';
10
19
  import { codexStreamMedia, collectCodexMedia, openCodeMediaPart, ownCodexImages } from './assistant-media.js';
11
20
  import { browserToolDenial } from './browser-tool-policy.js';
12
21
  import { CODEX_BROWSER_POLICY_UNTRUSTED_NOTE, codexBrowserPolicyHook } from './assistant-route-gate-codex.js';
13
22
  import { codexWindowsSandbox, codexWindowsSandboxConfig } from './agent-sandbox-elevation.js';
23
+ import { DRAFT_DISALLOWED_TOOLS, DRAFT_TOOLS, codexDraftOptions, draftOpenCodeDecision, draftPreToolUseHook, draftToolDecision, openCodeDraftConfig, resolveDraftConfinement } from './skills/draft-confinement.js';
24
+ import { EVAL_CONFINEMENT, codexEvalOptions, evalClaudeOptions, evalGitSafetyEnv, evalOpenCodeDecision, evalPreToolUseHook, evalToolDecision, openCodeEvalConfig, openCodeEvalEnv, placeOpenCodeEvalSkill, resolveEvalConfinement } from './skills/eval-confinement.js';
25
+ import { WORKER_GUARD_DEFAULT, acpPermissionDecision, codexSandboxFor, confinementEvent, openCodeCodeModeDecision, openCodeCodeModeNote, openCodePermissionDecision, openCodePermissionsFor, resolveWorkerConfinement, workerShellDecision, workerToolDecision } from './worker-confinement.js';
26
+ import { createAcpConnection } from './antigravity/acp-connection.js';
27
+ import { acpTurnUsage } from './antigravity/usage.js';
28
+ import { assertSafeAntigravityProject, antigravityHomeReadPolicy, antigravityHomeSettings, policyHookCommand, prepareAntigravityHome, toAntigravityMcpEntry, userAntigravityMcpServers, userAntigravitySettings, userGeminiDir } from './antigravity/home.js';
29
+ import { acpCall, acpPolicyInput, acpReadOnlyKind, acpToolName, acpToolPaths, acpWebTool } from './antigravity/tool-call.js';
30
+ import { splitAntigravityModel } from './antigravity/catalog.js';
31
+ import { fileURLToPath } from 'node:url';
32
+
33
+ function nativeToolSelection(options, provider) { return resolveStoredTools(options, { source: options.runMode === 'task' ? 'dispatch' : 'launch', provider, defaultTools: options.runMode === 'task' ? undefined : readDefaultTools(memoryConfig.dataHome) }); }
14
34
 
15
35
  function stringEnv(source) {
16
36
  const out = {};
@@ -112,6 +132,12 @@ export function normalizeOpenCodeModel(model) {
112
132
  export const PERMISSION_POLICIES = new Set(['auto', 'ask', 'restricted']);
113
133
  export const CAPABILITIES = new Set(['read-only', 'workspace', 'full']);
114
134
  export const CLAUDE_READ_ONLY_DISALLOWED_TOOLS = ['Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'Bash'];
135
+ // The owner's live data home (its real memory DB). A worker's shell must never inherit these: a temp server or a
136
+ // test suite started from the worker would otherwise open the owner's real store (a reviewer's temp boot did,
137
+ // briefly). The worker's own SynaBun MCP child gets the DB through its own env block / DOTENV_PATH (server.js
138
+ // buildCodexMcpEnv, the OpenCode serve config), not from the shell; the Claude worker's MCP is in-process HTTP and
139
+ // needs nothing here. Stripped for every worker (guarded and full alike): a worker never opens the live DB directly.
140
+ export const LIVE_DATA_ENV = Object.freeze(['SQLITE_DB_PATH', 'MEMORY_DATA_DIR']);
115
141
 
116
142
  export function normalizePermissionPolicy(policy) {
117
143
  const value = String(policy || 'auto').trim().toLowerCase();
@@ -136,6 +162,99 @@ function policyUnsupportedError(provider, policy) {
136
162
  return error;
137
163
  }
138
164
 
165
+ /** A confined run (lib/skills/draft-confinement.js) that cannot be set up on its provider: it never starts unconfined. */
166
+ function confinementUnsupportedError(provider, reason, what = 'a drafting folder') {
167
+ const error = new Error(`${provider} cannot run confined to ${what}${reason ? ` (${reason})` : ''}: the run did not start.`);
168
+ error.code = 'CONFINEMENT_UNSUPPORTED';
169
+ return error;
170
+ }
171
+
172
+ /** 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). */
173
+ export const CLAUDE_READ_ONLY_WRITE_TOOLS = Object.freeze(['Write', 'Edit', 'MultiEdit', 'NotebookEdit']);
174
+
175
+ /**
176
+ * The worker confinement of a run (lib/worker-confinement.js), or null: capability full, the Protect workers
177
+ * setting off, or a skill-drafting run (which has its own, stricter confinement). Throws when the working
178
+ * directory cannot be resolved: the run must not start on a guess.
179
+ */
180
+ function workerGuardFor({ capability, cwd, guardMode, scratchDir, confinement, serverPort = null, localPorts = null, localPortRange = null }) {
181
+ if (confinement) return null;
182
+ // Never rooted where the server runs: a run without a cwd is rooted at its scratch folder, or does not start.
183
+ return resolveWorkerConfinement({ capability, cwd: cwd || null, mode: guardMode, scratchDir: scratchDir || null, serverPort, localPorts, localPortRange });
184
+ }
185
+ /** The port of SynaBun's own server, from a run's MCP address (a guarded worker's shell may not call it). */
186
+ function serverPortOf(mcpUrl, fallback = null) {
187
+ try { const port = Number(new URL(String(mcpUrl)).port); if (port > 0) return port; } catch { /* no address */ }
188
+ return Number(fallback) > 0 ? Number(fallback) : null;
189
+ }
190
+
191
+ /**
192
+ * What an OpenCode run's own config carries for its confinement (server.js setupOpencodeLoopConfig): null for an
193
+ * unguarded run (today's rules), else { permissions (OpenCode 2's rules), mcp: 'off'?, directMcp: true? }. A
194
+ * skill-drafting run has no shell, web, subagent or skill tool and no MCP server; a guarded worker has the rules its
195
+ * adapter answers by policy, and under Enforce no Code Mode: `directMcp` sets `codemode: false` on every MCP server of
196
+ * the run's config, which OpenCode 2.1.4 otherwise reaches only through Code Mode's `execute` (denied by the rules).
197
+ */
198
+ export function openCodeRunConfigFor(state = {}) {
199
+ if (state.confinement?.kind === EVAL_CONFINEMENT) return openCodeEvalConfig({ role: state.confinement.role, shell: state.confinement.shell === true });
200
+ if (state.confinement) return openCodeDraftConfig();
201
+ 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 });
202
+ if (!guard) return null;
203
+ return { permissions: openCodePermissionsFor(guard), ...(guard.enforced ? { directMcp: true } : {}) };
204
+ }
205
+
206
+ /**
207
+ * Whether an OpenCode run is a protected worker: an Assistant task worker with capability read-only or workspace
208
+ * under Protect workers = Enforce (never a drafting or evaluation run, which have confinements of their own; never
209
+ * Warn, Off or full). Its serve reads no project configuration (openCodeRunEnvFor) and keeps its password out of every
210
+ * environment (server.js ensureIsolatedServe `hidePassword`: lib/opencode-protocol.js serveSpecFor passwordFrom
211
+ * 'stdout'). Throws like resolveWorkerConfinement when a guarded run has no folder.
212
+ */
213
+ export function openCodeProtectedWorker(state = {}) {
214
+ if (state.confinement) return false;
215
+ 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 });
216
+ return !!guard?.enforced;
217
+ }
218
+
219
+ /**
220
+ * What a protected worker's serve environment holds (openCodeProtectedWorker): no project configuration at all, so
221
+ * nothing the worker writes in its folder runs in the serve (no opencode.json with plugins or MCP servers, no
222
+ * .opencode plugin, agent, command or tool, no project AGENTS.md: OPENCODE_DISABLE_PROJECT_CONFIG, and
223
+ * OPENCODE_CONFIG_PROJECT_DISABLE too, which OpenCode 2 reads first), and no inherited OPENCODE_CONFIG_DIR (OpenCode
224
+ * reads that folder in place of the run's own configuration, whose rules, `codemode: false` and SynaBun MCP pin would
225
+ * then be gone). Every inherited GIT_* variable is removed too: repository, index and object paths could point git
226
+ * outside the folder, and GIT_CONFIG_PARAMETERS or GIT_CONFIG_COUNT/KEY_n/VALUE_n could run an fsmonitor or pager.
227
+ * `GIT_*: null` clears the prefix before deliberate settings are applied (lib/opencode-protocol.js withServeEnv).
228
+ */
229
+ export const OPENCODE_PROTECTED_WORKER_ENV = Object.freeze({
230
+ OPENCODE_DISABLE_PROJECT_CONFIG: '1', OPENCODE_CONFIG_PROJECT_DISABLE: '1', OPENCODE_CONFIG_DIR: null,
231
+ 'GIT_*': null,
232
+ });
233
+
234
+ /**
235
+ * What an OpenCode run's serve environment carries for its confinement (server.js ensureIsolatedServe): an evaluation
236
+ * run's (no project configuration, no file watcher, git's safety settings: lib/skills/eval-confinement.js
237
+ * openCodeEvalEnv), a protected worker's (OPENCODE_PROTECTED_WORKER_ENV), else null. A value of null removes that
238
+ * variable from the serve's environment. Throws when the evaluation folder cannot be resolved: the serve must not start
239
+ * unconfined.
240
+ */
241
+ export function openCodeRunEnvFor(state = {}) {
242
+ if (state.confinement?.kind === EVAL_CONFINEMENT) return openCodeEvalEnv(resolveEvalConfinement(state.confinement, state.cwd));
243
+ if (openCodeProtectedWorker(state)) return { ...OPENCODE_PROTECTED_WORKER_ENV };
244
+ return null;
245
+ }
246
+
247
+ /**
248
+ * An evaluation trial with the skill: its copy placed in the run's own OpenCode configuration folder (`configDir`,
249
+ * server.js setupOpencodeLoopConfig's, removed with the run's serve), where OpenCode reads it as a global skill
250
+ * (lib/skills/eval-confinement.js placeOpenCodeEvalSkill). → the folder, or null (not an evaluation, a baseline, a
251
+ * grader). Throws when it cannot be placed: the run must not start without the skill it tests.
252
+ */
253
+ export function openCodeEvalSkillFor(state = {}, configDir) {
254
+ if (state.confinement?.kind !== EVAL_CONFINEMENT) return null;
255
+ return placeOpenCodeEvalSkill(resolveEvalConfinement(state.confinement, state.cwd), configDir);
256
+ }
257
+
139
258
  /** Final assistant text carried by a Codex SDK stream event, or null. */
140
259
  export function codexAgentMessageText(event) {
141
260
  if (!event) return null;
@@ -241,33 +360,55 @@ function codexStructuredOutput(text, schema) {
241
360
  export async function createCodexNativeLoopAdapter(options = {}) {
242
361
  const {
243
362
  runId, cwd, model, effort, codexHome, codexPath, browserSessionId,
244
- browserTabId, mcpProfile, onIdentity = () => {}, onEvent = () => {},
363
+ browserTabId, onIdentity = () => {}, onEvent = () => {},
245
364
  CodexClass = Codex,
246
- extraEnv = {}, permissionPolicy: rawPolicy = 'auto', capability: rawCapability = 'full',
365
+ extraEnv = {}, permissionPolicy: rawPolicy = 'auto', permissionBroker = null, capability: rawCapability = 'full',
247
366
  outputSchema = null, desktopGrantProvider = null, modelContextWindow = null, contextMode = null,
248
367
  // Image creation runs (assistant dispatch): 'image' → each turn returns the images it generated.
249
368
  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,
369
+ // Assistant task runs: the browser policy's hook and the run's notes (a function: never in the state file), and
370
+ // the registered code projects its test runners may also run in (lib/browser-policy-roots.js, read as the run starts).
371
+ runMode = 'loop', onNote = null, browserPolicyTrust = null, policyRoots = [],
252
372
  // Inherited variables to remove before extraEnv applies (the Codex account's launch:
253
373
  // lib/codex-accounts.js launchFor() — inherited OpenAI/Codex credentials never reach the run).
254
374
  unsetEnv = [],
255
375
  // The Admin sandbox setting when the run started (lib/agent-sandbox-elevation.js): the Windows
256
376
  // sandbox every `codex exec` of the thread may use. The run keeps it while it lives.
257
377
  adminSandbox = false,
378
+ // A skill-drafting run's confinement (lib/skills/draft-confinement.js codexDraftOptions): the sandbox around the
379
+ // workspace, no network, no MCP server.
380
+ confinement = null,
381
+ // The Protect workers setting as the run starts (lib/worker-confinement.js), the run's own scratch folder and
382
+ // SynaBun's own port (a guarded worker's shell may not call it).
383
+ guardMode = WORKER_GUARD_DEFAULT, scratchDir = null, serverPort = null, localPortRange = null,
258
384
  } = options;
259
385
  const permissionPolicy = normalizePermissionPolicy(rawPolicy);
260
386
  const capability = normalizeCapability(rawCapability);
387
+ // Resolved before anything starts: a confinement that cannot be set up stops the run (never an unconfined start).
388
+ let drafted = null;
389
+ let evaluated = null;
390
+ if (confinement?.kind === EVAL_CONFINEMENT) {
391
+ // A skill-evaluation run (lib/skills/eval-confinement.js): the sandbox around its job folder (read-only for a
392
+ // grader, a judge or a proposer), no network, no web search, every MCP server disabled.
393
+ let held;
394
+ try { held = resolveEvalConfinement(confinement, cwd); } catch (error) { throw confinementUnsupportedError('Codex', error?.message || error, 'an evaluation folder'); }
395
+ evaluated = { held, ...codexEvalOptions(held, { codexHome, projectDir: cwd }) };
396
+ } else if (confinement) {
397
+ let confined;
398
+ try { confined = resolveDraftConfinement(confinement, cwd); } catch (error) { throw confinementUnsupportedError('Codex', error?.message || error); }
399
+ drafted = codexDraftOptions(confined, { codexHome, projectDir: cwd });
400
+ }
401
+ // Either confinement's options (the thread's sandbox and the config's MCP switches): one shape.
402
+ const boxed = drafted || evaluated;
403
+ const guard = boxed ? null : workerGuardFor({ capability, cwd, guardMode, scratchDir, confinement: null, serverPort, localPortRange });
261
404
  // "<id>[extended]" (the Assistant's extended-context row) runs <id> with the
262
405
  // larger window the dispatcher read from the catalog; never silently at 272k.
263
406
  const legacyExtended = /\[extended\]$/i.test(String(model || ''));
264
407
  const extended = contextMode === 'extended' || legacyExtended;
265
408
  const runModel = legacyExtended ? String(model).replace(/\[extended\]$/i, '') : model;
266
409
  if (extended && !(Number(modelContextWindow) > 0)) throw new Error(`Extended context is unavailable for ${runModel}.`);
267
- // The Codex SDK drives `codex exec`, which has no approval channel: an 'ask'
268
- // policy would hang or fail mid-turn. Refuse up front so the dispatcher can
269
- // offer 'restricted' instead.
270
- if (permissionPolicy === 'ask') throw policyUnsupportedError('Codex', permissionPolicy);
410
+ // Legacy exec loops have no approval channel. Task workers use an app-server.
411
+ if (permissionPolicy === 'ask' && runMode !== 'task') throw policyUnsupportedError('Codex', permissionPolicy);
271
412
  // Assistant task runs: SynaBun's browser policy as a PreToolUse hook the
272
413
  // user's Codex trusts (the brain's route-gate mechanism), and no web search
273
414
  // of Codex's own. Without a trusted hook the policy is instructions only
@@ -280,19 +421,41 @@ export async function createCodexNativeLoopAdapter(options = {}) {
280
421
  }
281
422
  const pins = stringEnv({
282
423
  SYNABUN_TERMINAL_SESSION: runId,
424
+ SYNABUN_RUN_MODE: options.runMode === 'task' ? 'task' : undefined,
283
425
  SYNABUN_PROJECT: cwd ? memoryProject(cwd) : 'global',
284
426
  SYNABUN_BROWSER_SESSION: browserSessionId,
285
427
  SYNABUN_BROWSER_TAB: browserTabId,
286
- SYNABUN_PROFILE: mcpProfile,
428
+ SYNABUN_TOOLS: runtimeToolsValue((() => { const list = nativeToolSelection(options, 'codex'); return list.items.includes('all') ? list.items : [...list.groups, ...list.servers]; })()),
287
429
  SYNABUN_TOOL_CATALOG_MODE: 'deferred',
288
430
  // Computer-use workers: the grant unlocks the `computer` group in the MCP child.
289
431
  SYNABUN_DESKTOP_GRANT: typeof desktopGrantProvider === 'function' ? desktopGrantProvider() || undefined : undefined,
290
432
  });
291
433
  const inherited = { ...process.env };
434
+ for (const key of ['SYNABUN_PROFILE', 'SYNABUN_RUNTIME_PROFILE_PATH', 'SYNABUN_TOOLS', 'SYNABUN_TOOLS_PATH']) delete inherited[key];
292
435
  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) });
436
+ // A worker never holds a brain's launch credential (the Assistant's own identity) or an owner's page secret.
437
+ delete inherited.SYNABUN_BRAIN_TOKEN;
438
+ // The worker's shell must not inherit the owner's live-data paths (its MCP child gets them through its own
439
+ // config: server.js buildCodexMcpEnv). A worker that boots a temp server or runs the suite stays off the live DB.
440
+ for (const key of LIVE_DATA_ENV) delete inherited[key];
441
+ // The hook reads the run's project and the other projects its test runners may run in (a JSON list) from its env.
442
+ const policyEnv = browserPolicy ? { SYNABUN_BROWSER_POLICY_CWD: cwd || process.cwd(), SYNABUN_BROWSER_POLICY_ROOTS: JSON.stringify(Array.isArray(policyRoots) ? policyRoots : []) } : {};
443
+ const env = withServeEnv(stringEnv({ ...inherited, ...pins, CODEX_HOME: codexHome, ...policyEnv, ...stringEnv(extraEnv) }), evaluated ? evalGitSafetyEnv(evaluated.held.root) : null);
294
444
  const config = { ...codexWindowsSandboxConfig(adminSandbox) };
295
- if (Object.keys(pins).length) config.mcp_servers = { SynaBun: { env: pins } };
445
+ if (Object.keys(pins).length) config.mcp_servers = { SynaBun: { env: pins, ...(task ? { tool_timeout_sec: Math.ceil((options.questionTimeoutMs || 30 * 60_000) / 1000) + 30 } : {}) } };
446
+ if (!boxed) {
447
+ const inventory = externalServerInventory({ provider: 'codex', cwd, codexHome, env, home: env.HOME, dataHome: memoryConfig.dataHome, ...options.externalMcpContext });
448
+ const list = nativeToolSelection(options, 'codex');
449
+ const scope = selectExternalServers(list.servers, inventory);
450
+ // Leaf overrides preserve native transports/env. Codex treats quotes in a
451
+ // path literally; reject unaddressable names rather than launch a wider scope.
452
+ for (const name of scope.disabled) {
453
+ if (inventory.servers.find(row => row.name === name)?.reason === 'disabled-in-provider') continue;
454
+ config[codexMcpEnabledOverride(name)] = false;
455
+ }
456
+ }
457
+ // A drafting or evaluation run: every MCP server disabled (SynaBun's keeps its pins beside the switch; they never start).
458
+ 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
459
  if (extended) config.model_context_window = Math.floor(Number(modelContextWindow));
297
460
  if (browserPolicy) Object.assign(config, browserPolicy.config);
298
461
  // The SDK is always told which Codex to start: the user's own installation.
@@ -303,7 +466,16 @@ export async function createCodexNativeLoopAdapter(options = {}) {
303
466
  error.code = 'CODEX_NOT_INSTALLED';
304
467
  throw error;
305
468
  }
306
- const codex = new CodexClass({
469
+ const WorkerCodex = task && !boxed && CodexClass === Codex ? CodexWorkerAppServer : CodexClass;
470
+ const codex = new WorkerCodex({
471
+ permissionPolicy,
472
+ onRequest: permissionBroker && !boxed ? async (packet, signal) => {
473
+ const question = CODEX_QUESTIONS.has(packet.method);
474
+ if (!question && permissionPolicy !== 'ask') return { error: { code: -32601, message: 'Unexpected worker approval request' } };
475
+ const reply = await permissionBroker.request({ kind: question ? 'ask' : 'tool', toolName: packet.method, input: packet.params, questions: workerQuestions(packet.params), protocol: { provider: 'codex', method: packet.method, id: packet.id }, signal });
476
+ return codexWorkerReply(packet, reply);
477
+ } : null,
478
+ onProcess: options.onProcess, signal: options.signal, apiKey: options.apiKey, configArgs: options.configArgs,
307
479
  ...(codexPath ? { codexPathOverride: codexPath } : {}),
308
480
  env,
309
481
  config,
@@ -312,23 +484,67 @@ export async function createCodexNativeLoopAdapter(options = {}) {
312
484
  // U+2028 / U+2029: a command printing one ended the run with "Failed to parse item".
313
485
  patchCodexLineSplitting(codex);
314
486
  const parent = dirname(cwd || process.cwd());
315
- const thread = codex.startThread({
487
+ // The sandbox: a drafting run's (the workspace, no network); an enforced guard's (lib/worker-confinement.js
488
+ // codexSandboxFor: read-only, or workspace-write with the working directory and the scratch folder, never the
489
+ // parent folder); else today's (the capability's mode, the parent folder writable too).
490
+ const sandboxed = guard?.enforced ? codexSandboxFor(guard, { parentDir: parent && parent !== cwd ? parent : null, scratchDir }) : null;
491
+ // A guarded Codex worker (Protect workers = Enforce) runs with network OFF in Codex's own sandbox. Codex's
492
+ // sandbox network is all-or-nothing (no per-host/loopback denial on this CLI), so SynaBun cannot wrap it in an
493
+ // outer Seatbelt (nested sandbox-exec is refused): turning the network off is the only way its shell commands
494
+ // (node -e, curl, a script) cannot reach SynaBun's own control port or any other local endpoint. The owner has
495
+ // accepted that read-only / workspace Codex workers lose internet (HTTPS, npm, git fetch) for their shell. The
496
+ // worker's SynaBun MCP child runs outside this command sandbox, so remember / recall / browser tools still work.
497
+ const codexNetworkOff = !!(boxed || guard?.enforced);
498
+ const threadOptions = {
316
499
  workingDirectory: cwd || process.cwd(),
317
500
  model: runModel || undefined,
318
501
  modelReasoningEffort: normalizeCodexEffort(effort),
319
502
  approvalPolicy: 'never',
320
- sandboxMode: codexSandboxModeFor({ permissionPolicy, capability }),
503
+ sandboxMode: boxed ? boxed.thread.sandboxMode : sandboxed ? sandboxed.sandboxMode : codexSandboxModeFor({ permissionPolicy, capability }),
321
504
  skipGitRepoCheck: true,
322
- networkAccessEnabled: true,
323
- ...(task ? { webSearchMode: 'disabled' } : {}),
324
- additionalDirectories: parent && parent !== cwd ? [parent] : undefined,
325
- });
505
+ networkAccessEnabled: !codexNetworkOff,
506
+ ...(task || boxed ? { webSearchMode: 'disabled' } : {}),
507
+ additionalDirectories: boxed ? undefined : sandboxed ? sandboxed.additionalDirectories : parent && parent !== cwd ? [parent] : undefined,
508
+ };
509
+ const thread = options.resumeThreadId ? codex.resumeThread(options.resumeThreadId, threadOptions) : codex.startThread(threadOptions);
510
+ try { await thread.ready?.(); } catch (error) { await codex.dispose?.(); throw error; }
511
+ if (thread.id) onIdentity({ providerThreadId: thread.id, providerSessionId: thread.id });
326
512
  let alive = true;
327
513
  let activeAbort = null;
328
514
  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;
515
+ // A guarded run says at its start what holds it: Codex's sandbox never reports to SynaBun by itself, so this is
516
+ // the one place the note is written before any command (the run's notes, then its events).
517
+ if (guard) {
518
+ 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 {}
519
+ // Network-off is the one way a Codex worker's shell cannot reach SynaBun's control port (Codex runs its own
520
+ // sandbox, which SynaBun cannot wrap, and its network is all-or-nothing). Say it plainly, once, at the start.
521
+ 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 {} }
522
+ 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 {} }
523
+ }
524
+ 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 {} }
525
+ 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 {} }
526
+ /**
527
+ * A command Codex ran (item.completed command_execution): under warn, what Enforce would have refused is noted;
528
+ * under enforce, a sandbox denial in its output is explained in SynaBun's words (Codex's sandbox refuses by itself
529
+ * and says nothing to SynaBun).
530
+ */
531
+ const noteCommand = (item, meta) => {
532
+ if (!guard || item?.type !== 'command_execution') return;
533
+ const command = typeof item.command === 'string' ? item.command : Array.isArray(item.command) ? item.command.join(' ') : '';
534
+ if (!command) return;
535
+ const decision = workerShellDecision(command, guard);
536
+ const output = String(item.aggregated_output || item.output || '');
537
+ const denied = Number(item.exit_code) !== 0 && /operation not permitted|permission denied|read-only file system|sandbox|EPERM|EACCES|EROFS/i.test(output);
538
+ if (!decision && !denied) return;
539
+ const event = decision
540
+ ? confinementEvent(decision, { tool: 'shell', confinement: guard, provider: 'codex' })
541
+ : { 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' };
542
+ if (decision && guard.enforced && !denied) event.action = 'noted'; // the sandbox decides; the heuristic's view is a note
543
+ onEvent({ provider: 'codex', runId, iteration: meta?.iteration, event });
544
+ };
545
+ // The sidepanel keys a turn's items by this sequence, including across a restart.
546
+ // Scheduled SDK turns restart their item IDs; app-server workers keep their thread IDs.
547
+ let providerTurn = Number(options.providerTurnBase) || 0;
332
548
  // Generated images already returned by an earlier turn, and when the thread began (its rollout's day).
333
549
  const mediaSeen = new Set();
334
550
  let threadStartedMs = 0;
@@ -337,9 +553,13 @@ export async function createCodexNativeLoopAdapter(options = {}) {
337
553
  identity: () => ({ providerThreadId: thread.id, providerSessionId: thread.id }),
338
554
  isAlive: () => alive,
339
555
  describe: () => ({
340
- provider: 'codex', permissionPolicy, capability, sandboxMode: codexSandboxModeFor({ permissionPolicy, capability }),
556
+ provider: 'codex', permissionPolicy, capability, sandboxMode: boxed ? boxed.thread.sandboxMode : sandboxed ? sandboxed.sandboxMode : codexSandboxModeFor({ permissionPolicy, capability }),
557
+ networkOff: codexNetworkOff,
341
558
  windowsSandbox: codexWindowsSandbox(adminSandbox),
342
559
  ...(task ? { browserPolicy: browserPolicy ? 'hook' : 'instructions' } : {}),
560
+ ...(drafted ? { confinement: 'skill-draft', root: cwd } : {}),
561
+ ...(evaluated ? { confinement: EVAL_CONFINEMENT, role: evaluated.held.role, root: cwd } : {}),
562
+ ...(guard ? { guard: { mode: guard.mode, capability: guard.capability, writable: [...guard.writable] } } : {}),
343
563
  }),
344
564
  async runTurn(prompt, meta = {}) {
345
565
  if (!alive) throw new Error('Codex native loop is closed');
@@ -377,6 +597,7 @@ export async function createCodexNativeLoopAdapter(options = {}) {
377
597
  if (agentText !== null) text = agentText;
378
598
  const saved = codexStreamMedia(event);
379
599
  if (saved) media.push(saved);
600
+ if (event?.type === 'item.completed') { try { noteCommand(event.item, meta); } catch { /* a note, never the turn */ } }
380
601
  if (event?.type === 'turn.failed') failure = codexFailureMessage(event.error);
381
602
  if (event?.type === 'turn.completed') { completed = true; if (event.usage) usage = event.usage; }
382
603
  if (event?.type === 'error') failure = codexFailureMessage(event);
@@ -415,6 +636,7 @@ export async function createCodexNativeLoopAdapter(options = {}) {
415
636
  async dispose() {
416
637
  alive = false;
417
638
  if (activeAbort) activeAbort.abort(asAbortError('disposed'));
639
+ await codex.dispose?.();
418
640
  },
419
641
  };
420
642
  }
@@ -438,28 +660,61 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
438
660
  unsetEnv = [],
439
661
  // The host's last word on the environment: on Windows the Git Bash it names (lib/git-bash/).
440
662
  childEnv = null,
663
+ // A skill-drafting run (lib/skills/draft-confinement.js): file tools in its folder only, nothing else.
664
+ confinement = null,
665
+ // The Protect workers setting as the run starts (lib/worker-confinement.js), the run's own scratch folder and
666
+ // SynaBun's own port (a guarded worker's shell may not call it; read from mcpUrl when not given).
667
+ guardMode = WORKER_GUARD_DEFAULT, scratchDir = null, serverPort = null, localPortRange = null,
668
+ // Assistant task runs: the registered code projects the browser policy lets test runners run in besides the
669
+ // run's own (lib/browser-policy-roots.js, read as the run starts).
670
+ policyRoots = [],
671
+ // A run resumed after a server restart (assistant-dispatch.js resumeRun): the Claude session to continue, and the
672
+ // cost counter the resumed CLI starts from (it restores its total from the transcript's last `cost-state` line,
673
+ // lib/worker-resume.js readCostState), so the first turn after it is charged only for itself.
674
+ resumeSessionId = null, costBaseUsd = 0,
675
+ // Told the CLI's process once it is spawned ({ pid, startedAt, command }): a run that outlives this server is
676
+ // found by it (the next start ends an orphan before it resumes the session).
677
+ onProcess = null,
441
678
  } = options;
442
679
  const permissionPolicy = normalizePermissionPolicy(rawPolicy);
443
680
  const capability = normalizeCapability(rawCapability);
444
- const askMode = permissionPolicy === 'ask' && typeof permissionBroker?.request === 'function';
681
+ // Resolved before anything starts: a confinement that cannot be set up stops the run (never an unconfined start).
682
+ let evaluated = null;
683
+ if (confinement?.kind === EVAL_CONFINEMENT) {
684
+ try { evaluated = resolveEvalConfinement(confinement, cwd); } catch (error) { throw confinementUnsupportedError('Claude Code', error?.message || error, 'an evaluation folder'); }
685
+ }
686
+ const confined = confinement && !evaluated ? resolveDraftConfinement(confinement, cwd) : null;
687
+ // Either confinement (drafting, evaluation): no worker guard, nothing asked, never bypassPermissions.
688
+ const held = confined || evaluated;
689
+ const evalOptions = evaluated ? evalClaudeOptions(evaluated) : null;
690
+ // The worker confinement of a read-only / workspace run: a PreToolUse hook on every call (subagents' included),
691
+ // which Claude Code runs before any permission mode, so the run keeps bypassPermissions and nothing waits.
692
+ const guard = workerGuardFor({ capability, cwd, guardMode, scratchDir, confinement: held, serverPort: serverPortOf(mcpUrl, serverPort), localPortRange });
693
+ const askMode = !held && permissionPolicy === 'ask' && typeof permissionBroker?.request === 'function';
445
694
  const input = createInputQueue();
446
695
  const abortController = new AbortController();
447
696
  let alive = true;
448
697
  let sessionId = null;
449
698
  let activeTurn = null;
450
699
  const inherited = cleanClaudeEnvironment();
700
+ for (const key of ['SYNABUN_PROFILE', 'SYNABUN_RUNTIME_PROFILE_PATH', 'SYNABUN_TOOLS', 'SYNABUN_TOOLS_PATH']) delete inherited[key];
451
701
  for (const key of Array.isArray(unsetEnv) ? unsetEnv : []) delete inherited[key];
702
+ // A worker never holds a brain's launch credential (the Assistant's own identity) or an owner's page secret.
703
+ delete inherited.SYNABUN_BRAIN_TOKEN;
704
+ // The worker's shell must not inherit the owner's live-data paths: a Claude worker's MCP is in-process HTTP (it
705
+ // reaches the DB through the Neural Interface, never from this env), so nothing here needs them.
706
+ for (const key of LIVE_DATA_ENV) delete inherited[key];
452
707
  const ownEnv = stringEnv({
453
708
  ...inherited,
454
709
  SYNABUN_TERMINAL_SESSION: runId,
710
+ SYNABUN_RUN_MODE: options.runMode === 'task' ? 'task' : undefined,
455
711
  SYNABUN_PROJECT: cwd ? memoryProject(cwd) : 'global',
456
- SYNABUN_PROFILE: 'full',
457
712
  SYNABUN_BROWSER_SESSION: browserSessionId,
458
713
  SYNABUN_BROWSER_TAB: browserTabId,
459
- SYNABUN_RUN_MODE: runMode === 'task' ? 'task' : undefined,
460
714
  ...stringEnv(extraEnv),
461
715
  });
462
- const env = typeof childEnv === 'function' ? stringEnv(childEnv(ownEnv)) : ownEnv;
716
+ // Apply after every inherited/child environment overlay: drop ALL GIT_* first, then SynaBun's own safety entries.
717
+ const env = withServeEnv(typeof childEnv === 'function' ? stringEnv(childEnv(ownEnv)) : ownEnv, evalOptions?.env);
463
718
  const brokerRequest = async (request) => {
464
719
  try {
465
720
  const reply = await permissionBroker.request({
@@ -474,27 +729,46 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
474
729
  'X-Synabun-Terminal': runId,
475
730
  'X-Synabun-Project': cwd ? memoryProject(cwd) : 'global',
476
731
  'X-Synabun-Memory-Session': runId,
732
+ 'X-Synabun-Tools': runtimeToolsValue(resolveStoredTools(options, { source: options.runMode === 'task' ? 'dispatch' : 'launch', provider: 'claude-code' }).groups),
477
733
  'X-Synabun-Browser-Session': browserSessionId,
478
734
  'X-Synabun-Browser-Tab': browserTabId,
479
735
  // Computer-use workers: the grant unlocks the `computer` MCP group.
480
736
  'X-Synabun-Desktop-Grant': typeof desktopGrantProvider === 'function' ? desktopGrantProvider() || undefined : undefined,
481
737
  });
482
- // Under 'ask' the orchestrator answers permission prompts (and AskUserQuestion
483
- // becomes a relayed question); otherwise the run is unattended and both
484
- // interactive tools stay denied. ExitPlanMode is never useful for a worker.
485
- const denyMatcher = askMode ? 'ExitPlanMode' : 'AskUserQuestion|ExitPlanMode';
486
- const denyMessage = askMode
487
- ? 'Dispatched workers do not use plan mode.'
488
- : 'This unattended automation cannot wait for interactive input.';
738
+ // Questions use the broker under every policy, independently of tool approvals.
739
+ // ExitPlanMode stays denied: asking does not need a separate plan turn.
740
+ const questionMode = !held && typeof permissionBroker?.request === 'function';
741
+ const denyMatcher = questionMode ? 'ExitPlanMode' : 'AskUserQuestion|ExitPlanMode';
742
+ const denyMessage = 'Dispatched workers do not use plan mode; questions use the live run sidepanel.';
743
+ // A confined run asks canUseTool for every write (permission mode 'default') on top of its PreToolUse hook, and
744
+ // loads no settings file: no user hooks (they would ask it for memories it has no tool to write), no allow rules.
745
+ const bypass = !askMode && !held;
746
+ // The guard's decision on a tool call: refused (enforce) or noted (warn), always written into the run's events.
747
+ const guardHook = guard ? async (input) => {
748
+ const decision = workerToolDecision(input?.tool_name, input?.tool_input || {}, guard);
749
+ if (!decision) return {};
750
+ onEvent({ provider: 'claude-code', runId, event: confinementEvent(decision, { tool: input?.tool_name, confinement: guard, provider: 'claude-code' }) });
751
+ if (!guard.enforced) return {};
752
+ return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: decision.reason } };
753
+ } : null;
489
754
  const queryOptions = {
490
755
  cwd: cwd || process.cwd(),
491
- permissionMode: askMode ? 'default' : 'bypassPermissions',
492
- ...(askMode ? {} : { allowDangerouslySkipPermissions: true }),
756
+ permissionMode: bypass ? 'bypassPermissions' : 'default',
757
+ ...(bypass ? { allowDangerouslySkipPermissions: true } : {}),
493
758
  includePartialMessages: !!includePartialMessages,
494
759
  systemPrompt: { type: 'preset', preset: 'claude_code' },
495
- settingSources: ['user', 'project', 'local'],
760
+ // A drafting run and every evaluation run read no settings file at all (an evaluation's skill under test comes as
761
+ // its one plugin, below); everything else the user's, the project's and the local ones.
762
+ settingSources: evalOptions ? evalOptions.settingSources : confined ? [] : ['user', 'project', 'local'],
496
763
  hooks: {
497
- PreToolUse: [{
764
+ PreToolUse: [
765
+ // A skill-drafting run: every call, before any permission mode (lib/skills/draft-confinement.js).
766
+ ...(confined ? [{ hooks: [draftPreToolUseHook(confined)] }] : []),
767
+ // A skill-evaluation run: every call, the same way (lib/skills/eval-confinement.js).
768
+ ...(evaluated ? [{ hooks: [evalPreToolUseHook(evaluated)] }] : []),
769
+ // A guarded worker (capability read-only / workspace): every call, subagents' included (lib/worker-confinement.js).
770
+ ...(guardHook ? [{ hooks: [guardHook] }] : []),
771
+ {
498
772
  matcher: denyMatcher,
499
773
  hooks: [async () => ({
500
774
  hookSpecificOutput: {
@@ -504,11 +778,12 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
504
778
  },
505
779
  })],
506
780
  },
781
+ ...(questionMode ? [{ matcher: 'AskUserQuestion', hooks: [async () => ({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'ask', permissionDecisionReason: 'The user answers in this run’s sidepanel.' } })] }] : []),
507
782
  // Assistant task runs: SynaBun's browser policy (browser-tool-policy.js)
508
783
  // on every call, subagents' included, before the bypass and canUseTool.
509
784
  ...(runMode === 'task' ? [{
510
785
  hooks: [async (input) => {
511
- const reason = browserToolDenial(input?.tool_name, input?.tool_input || {}, { host: 'claude', cwd: cwd || process.cwd() });
786
+ const reason = browserToolDenial(input?.tool_name, input?.tool_input || {}, { host: 'claude', cwd: cwd || process.cwd(), roots: policyRoots });
512
787
  return reason ? { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason } } : {};
513
788
  }],
514
789
  }] : [])],
@@ -516,9 +791,17 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
516
791
  env,
517
792
  abortController,
518
793
  canUseTool: async (toolName, toolInput) => {
794
+ if (confined) {
795
+ const reason = draftToolDecision(toolName, toolInput, confined);
796
+ return reason ? { behavior: 'deny', message: reason } : { behavior: 'allow', updatedInput: toolInput };
797
+ }
798
+ if (evaluated) {
799
+ const reason = evalToolDecision(toolName, toolInput, evaluated);
800
+ return reason ? { behavior: 'deny', message: reason } : { behavior: 'allow', updatedInput: toolInput };
801
+ }
519
802
  if (toolName === 'ExitPlanMode') return { behavior: 'deny', message: denyMessage };
520
803
  if (toolName === 'AskUserQuestion') {
521
- if (!askMode) return { behavior: 'deny', message: denyMessage };
804
+ if (!questionMode) return { behavior: 'deny', message: denyMessage };
522
805
  const reply = await brokerRequest({ kind: 'ask', toolName, input: toolInput, questions: toolInput?.questions || [] });
523
806
  if (reply.behavior === 'allow') {
524
807
  const answers = reply.answers || reply.updatedInput?.answers || {};
@@ -526,6 +809,8 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
526
809
  }
527
810
  return { behavior: 'deny', message: reply.message || 'The question was not answered.' };
528
811
  }
812
+ // Loading a deferred question schema is a read, never a permission decision.
813
+ if (toolName === 'ToolSearch') return { behavior: 'allow', updatedInput: toolInput };
529
814
  // Computer use is fully autonomous: its guards live in the desktop service,
530
815
  // so the orchestrator is never asked to approve individual actions.
531
816
  if (askMode && /^mcp__SynaBun__computer(_apps|_ax|_status)?$/.test(String(toolName))) return { behavior: 'allow', updatedInput: toolInput };
@@ -541,12 +826,105 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
541
826
  return { behavior: 'allow', updatedInput: toolInput };
542
827
  },
543
828
  };
544
- if (mcpUrl) queryOptions.mcpServers = { SynaBun: { type: 'http', url: mcpUrl, headers } };
829
+ if (confined) {
830
+ // No MCP server at all (SynaBun's or the user's: strict), and only the file tools.
831
+ queryOptions.mcpServers = {};
832
+ queryOptions.strictMcpConfig = true;
833
+ queryOptions.tools = [...DRAFT_TOOLS];
834
+ queryOptions.disallowedTools = [...DRAFT_DISALLOWED_TOOLS];
835
+ } else if (evalOptions) {
836
+ // No MCP server at all, the role's tools only, and the skills it may use: the one under test (a baseline: none),
837
+ // loaded as the run's one local plugin; no hook of any settings file, plugin or skill and no inline shell.
838
+ queryOptions.mcpServers = {};
839
+ queryOptions.strictMcpConfig = true;
840
+ queryOptions.tools = [...evalOptions.tools];
841
+ queryOptions.disallowedTools = [...evalOptions.disallowedTools];
842
+ queryOptions.skills = [...evalOptions.skills];
843
+ queryOptions.settings = { ...evalOptions.settings };
844
+ if (evalOptions.plugins.length) queryOptions.plugins = evalOptions.plugins.map((plugin) => ({ ...plugin }));
845
+ } else if (mcpUrl) queryOptions.mcpServers = { SynaBun: { type: 'http', url: mcpUrl, headers } };
846
+ let externalScopeNames = [];
847
+ let externalValues = [];
848
+ let privateMcpServers = null;
849
+ let privateMcpDir = null;
850
+ if (!held) {
851
+ const inventory = externalServerInventory({ provider: 'claude-code', cwd, env, home: env.HOME, dataHome: memoryConfig.dataHome, ...options.externalMcpContext });
852
+ const list = nativeToolSelection(options, 'claude-code');
853
+ const scope = selectExternalServers(list.servers, inventory);
854
+ const definitions = Object.fromEntries(scope.names.map(name => [name, inventory.definitionFor(name)]));
855
+ if (mcpUrl) definitions.SynaBun = { type: 'http', url: mcpUrl, headers };
856
+ const referenced = referenceClaudeMap(definitions, env);
857
+ queryOptions.mcpServers = referenced.mcpServers;
858
+ queryOptions.strictMcpConfig = true;
859
+ Object.assign(env, referenced.env);
860
+ // OAuth and dynamic header helpers are not expanded by the CLI's generic ${VAR} pass.
861
+ // Keep them intact in a private file; the SDK's JSON is replaced before the OS spawn boundary.
862
+ privateMcpServers = structuredClone(referenced.mcpServers);
863
+ for (const [name, definition] of Object.entries(definitions)) {
864
+ if (definition.oauth) privateMcpServers[name].oauth = structuredClone(definition.oauth);
865
+ if (definition.headersHelper) privateMcpServers[name].headersHelper = definition.headersHelper;
866
+ }
867
+ externalValues = [...Object.values(referenced.env), ...mcpDefinitionValues(definitions)];
868
+ externalScopeNames = Object.keys(definitions);
869
+ }
545
870
  if (model) queryOptions.model = model;
546
871
  if (effort && CLAUDE_EFFORT_LEVELS.includes(effort)) queryOptions.effort = effort;
547
- if (capability === 'read-only') queryOptions.disallowedTools = [...CLAUDE_READ_ONLY_DISALLOWED_TOOLS];
872
+ // Read-only: with the guard enforced the shell stays, limited to read-only commands by the hook, and only the
873
+ // built-in writers are off the list; otherwise (warn, off) today's list, Bash included.
874
+ if (capability === 'read-only' && !held) queryOptions.disallowedTools = guard?.enforced ? [...CLAUDE_READ_ONLY_WRITE_TOOLS] : [...CLAUDE_READ_ONLY_DISALLOWED_TOOLS];
548
875
  if (Number(maxBudgetUsd) > 0) queryOptions.maxBudgetUsd = Number(maxBudgetUsd);
549
876
  if (outputSchema && typeof outputSchema === 'object') queryOptions.outputFormat = { type: 'json_schema', schema: outputSchema };
877
+ // A run resumed after a server restart continues its own Claude session. A tool call the restart cut off is in its
878
+ // transcript without a result: the CLI records it as interrupted (outcome unknown) and the next call goes through.
879
+ if (resumeSessionId) queryOptions.resume = String(resumeSessionId);
880
+ // The CLI's process is spawned here, not inside the SDK, so its pid is known: a server shutdown ends it at once
881
+ // (endProcess), and the next start can tell an orphan from another program. Left to the SDK, a CLI whose server
882
+ // went down only gets its stdin closed: it finishes the tool call in progress (a minute of `sleep` measured) and
883
+ // keeps writing to its transcript, because the SDK's SIGTERM waits 2 s on a timer that dies with the server. The
884
+ // SDK reads no stderr from a process it did not spawn: it is drained here, its tail kept for a failure's message.
885
+ let child = null;
886
+ let stderrTail = '';
887
+ let terminating = false;
888
+ queryOptions.spawnClaudeCodeProcess = (spawnOptions) => {
889
+ if (!alive || abortController.signal.aborted) throw Object.assign(new Error('Claude MCP launch was stopped'), { code: 'MCP_SCOPE_UNENFORCED' });
890
+ const args = [...spawnOptions.args];
891
+ if (privateMcpServers) for (let index = 0; index < args.length - 1; index++) if (args[index] === '--mcp-config') {
892
+ privateMcpDir ||= mkdtempSync(join(tmpdir(), 'synabun-claude-mcp-'));
893
+ args[index + 1] = writePrivateMcpConfig(join(privateMcpDir, 'mcp.json'), privateMcpServers);
894
+ }
895
+ const proc = (options.spawnFactory || spawnProcess)(spawnOptions.command, args, {
896
+ cwd: spawnOptions.cwd, env: spawnOptions.env, signal: spawnOptions.signal,
897
+ stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true,
898
+ });
899
+ child = proc;
900
+ proc.stderr?.setEncoding?.('utf8');
901
+ proc.stderr?.on('data', (chunk) => { stderrTail = redactMcpValues(`${stderrTail}${chunk}`, externalValues).slice(-2048); });
902
+ proc.stderr?.on('error', () => {});
903
+ if (proc.pid) { try { onProcess?.({ pid: proc.pid, startedAt: new Date().toISOString(), command: String(spawnOptions.command || '') }); } catch { /* a note, never the run */ } }
904
+ return proc;
905
+ };
906
+ const processEnded = () => !child || child.exitCode !== null || (child.signalCode !== null && child.signalCode !== undefined);
907
+ /**
908
+ * End the CLI now (a server shutdown): SIGTERM, which makes it stop the tool call in progress (its process tree
909
+ * included), record that call's result and its cost state, and exit within a second; SIGKILL when it has not exited
910
+ * by `graceMs` (a killed CLI leaves its tool's processes behind, so it is the fallback only). Windows has no SIGTERM
911
+ * (a signal stops the CLI alone, and a dev server its tool started keeps its port): the tree is ended with taskkill.
912
+ * The CLI writes no cost state then; the resume reads what the CLI restores either way (lib/worker-resume.js).
913
+ */
914
+ async function endProcess(graceMs = 1500) {
915
+ const proc = child;
916
+ if (!proc || processEnded()) return;
917
+ const exited = new Promise((resolveExit) => { proc.once('exit', () => resolveExit()); });
918
+ if (process.platform === 'win32') {
919
+ try { spawnProcess('taskkill', ['/pid', String(proc.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }).on?.('error', () => {}); } catch { /* gone */ }
920
+ } else {
921
+ try { proc.kill('SIGTERM'); } catch { /* gone */ }
922
+ }
923
+ await Promise.race([exited, new Promise((resolveWait) => setTimeout(resolveWait, Math.max(100, graceMs)))]);
924
+ if (processEnded()) return;
925
+ try { proc.kill('SIGKILL'); } catch { /* gone */ }
926
+ await Promise.race([exited, new Promise((resolveWait) => setTimeout(resolveWait, 500))]);
927
+ }
550
928
  // The loop runs the user's own Claude Code: SynaBun carries none, and the SDK
551
929
  // is never left to look for one. The SDK uses pathToClaudeCodeExecutable
552
930
  // verbatim with no validation, so the path was vetted where it was resolved
@@ -565,9 +943,11 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
565
943
  throw error;
566
944
  }
567
945
 
568
- const q = queryFactory({ prompt: input, options: queryOptions });
946
+ let q;
947
+ try { q = queryFactory({ prompt: input, options: queryOptions }); }
948
+ catch (error) { if (privateMcpDir) rmSync(privateMcpDir, { recursive: true, force: true }); throw error; }
569
949
  const deltaBuffer = new Map();
570
- const forwardEvent = (event) => onEvent({ provider: 'claude-code', runId, event });
950
+ const forwardEvent = (event) => onEvent({ provider: 'claude-code', runId, event: redactMcpValues(event, externalValues) });
571
951
  const flushDelta = (key) => {
572
952
  const slot = deltaBuffer.get(key);
573
953
  if (!slot) return;
@@ -605,8 +985,9 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
605
985
  };
606
986
  // total_cost_usd is this CLI process's running total, not the turn's charge:
607
987
  // 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;
988
+ // A turn resolves with its own charge (costUsd) beside the total. A resumed
989
+ // session's CLI starts from the total its transcript recorded (costBaseUsd).
990
+ let reportedCostUsd = Math.max(0, Number(costBaseUsd) || 0);
610
991
  const turnCost = (event) => {
611
992
  const total = Number(event?.total_cost_usd);
612
993
  if (!Number.isFinite(total) || total <= 0) return { costUsd: 0, totalCostUsd: reportedCostUsd };
@@ -618,7 +999,8 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
618
999
  let pumpError = null;
619
1000
  const pump = (async () => {
620
1001
  try {
621
- for await (const event of q) {
1002
+ for await (const rawEvent of q) {
1003
+ const event = redactMcpValues(rawEvent, externalValues);
622
1004
  if (!alive) break;
623
1005
  const eventSessionId = event?.session_id || null;
624
1006
  if (eventSessionId && eventSessionId !== sessionId) {
@@ -653,6 +1035,12 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
653
1035
  }
654
1036
  }
655
1037
  } catch (error) {
1038
+ // A process that exited is reported without what it said on stderr (the SDK keeps that only for a process it
1039
+ // spawned itself): its tail goes with the message.
1040
+ const message = String(error?.message || '');
1041
+ if (stderrTail.trim() && /Claude Code process (exited|terminated)/.test(message) && !/stderr:/.test(message)) {
1042
+ try { error.message = `${message}. stderr: ${stderrTail.trim().slice(-1000)}`; } catch { /* read-only message */ }
1043
+ }
656
1044
  pumpError = error;
657
1045
  } finally {
658
1046
  flushDeltas();
@@ -670,7 +1058,8 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
670
1058
  pump.catch((error) => {
671
1059
  // dispose() aborts the query when the run ends, however it ended, and the
672
1060
  // SDK throws "Claude Code process aborted by user" for it: not a failure.
673
- if (abortController.signal.aborted) return;
1061
+ // Neither is the exit of a CLI a server shutdown ended (dispose with terminate).
1062
+ if (abortController.signal.aborted || terminating) return;
674
1063
  onEvent({
675
1064
  provider: 'claude-code',
676
1065
  runId,
@@ -681,8 +1070,14 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
681
1070
  return {
682
1071
  identity: () => ({ providerSessionId: sessionId }),
683
1072
  isAlive: () => alive,
684
- describe: () => ({ provider: 'claude-code', permissionPolicy, capability, askMode }),
685
- runTurn(prompt, meta = {}) {
1073
+ // The CLI's process while it runs ({ pid }), else null.
1074
+ processInfo: () => (child?.pid && !processEnded() ? { pid: child.pid } : null),
1075
+ 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] } } : {}) }),
1076
+ async runTurn(prompt, meta = {}) {
1077
+ if (!held) {
1078
+ try { await waitForClaudeMcpScope(q, externalScopeNames); }
1079
+ 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' }); }
1080
+ }
686
1081
  if (!alive) return Promise.reject(new Error('Claude native loop is closed'));
687
1082
  if (activeTurn) return Promise.reject(new Error('Claude native loop already has an active turn'));
688
1083
  onEvent({
@@ -718,16 +1113,24 @@ export async function createClaudeNativeLoopAdapter(options = {}) {
718
1113
  } catch {}
719
1114
  return true;
720
1115
  },
721
- async dispose() {
1116
+ // `terminate` (a server shutdown, lib/native-loop-runtime.js): the CLI is ended before the query is closed
1117
+ // (endProcess), so nothing of it outlives the server. The events it sends while it stops (the cut-off tool call's
1118
+ // result) still reach the run; its exit is not reported as a failure.
1119
+ async dispose(options = {}) {
722
1120
  if (activeTurn) {
723
1121
  const turn = activeTurn; activeTurn = null;
724
1122
  turn.reject(asAbortError('disposed'));
725
1123
  }
1124
+ if (options?.terminate === true) {
1125
+ terminating = true;
1126
+ await endProcess(Number(options.graceMs) > 0 ? Number(options.graceMs) : 1500);
1127
+ }
726
1128
  alive = false;
727
1129
  input.close();
728
1130
  flushDeltas();
729
1131
  try { abortController.abort(asAbortError('disposed')); } catch {}
730
1132
  await Promise.race([pump, new Promise((resolveWait) => setTimeout(resolveWait, 1000))]).catch(() => {});
1133
+ if (privateMcpDir) { rmSync(privateMcpDir, { recursive: true, force: true }); privateMcpDir = null; }
731
1134
  },
732
1135
  };
733
1136
  }
@@ -743,13 +1146,89 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
743
1146
  agent = null, effort = null, runMode = 'loop',
744
1147
  // Image / video creation runs: 'image' | 'video' → each turn returns the files its model generated.
745
1148
  collectMedia = null,
1149
+ // A skill-drafting run's confinement (lib/skills/draft-confinement.js openCodeDraftConfig): the serve's config has
1150
+ // no shell, web, subagent or skill tool and no MCP server; every edit is asked and answered inside the folder.
1151
+ confinement = null,
1152
+ // The Protect workers setting as the run starts (lib/worker-confinement.js), the run's own scratch folder,
1153
+ // SynaBun's own port, the run's own serve port and the band SynaBun keeps for its serves (a guarded worker's shell
1154
+ // may call none of them).
1155
+ guardMode = WORKER_GUARD_DEFAULT, scratchDir = null, serverPort = null, localPortRange = null, servePort = null,
1156
+ onNote = null,
746
1157
  } = options;
747
1158
  if (!client?.session?.create || (!client?.session?.promptAsync && !client?.session?.prompt)) {
748
1159
  throw new Error('OpenCode native loop requires an isolated SDK client');
749
1160
  }
750
1161
  const permissionPolicy = normalizePermissionPolicy(rawPolicy);
751
1162
  const capability = normalizeCapability(rawCapability);
752
- const askMode = permissionPolicy === 'ask' && typeof permissionBroker?.request === 'function';
1163
+ // Resolved before anything starts: a confinement that cannot be set up stops the run (never an unconfined start).
1164
+ let confined = null;
1165
+ let evaluated = null;
1166
+ if (confinement?.kind === EVAL_CONFINEMENT) {
1167
+ try { evaluated = resolveEvalConfinement(confinement, cwd); } catch (error) { throw confinementUnsupportedError('OpenCode', error?.message || error, 'an evaluation folder'); }
1168
+ } else if (confinement) {
1169
+ try { confined = resolveDraftConfinement(confinement, cwd); } catch (error) { throw confinementUnsupportedError('OpenCode', error?.message || error); }
1170
+ }
1171
+ const held = confined || evaluated;
1172
+ const guard = workerGuardFor({ capability, cwd, guardMode, scratchDir, confinement: held, serverPort, localPorts: servePort ? [servePort] : null, localPortRange });
1173
+ const askMode = !held && permissionPolicy === 'ask' && typeof permissionBroker?.request === 'function';
1174
+ // Code Mode (lib/worker-confinement.js OPENCODE_CODE_MODE_TOOL): off under Enforce, noted call by call under Warn.
1175
+ if (guard) { try { onNote?.(openCodeCodeModeNote(guard)); } catch {} }
1176
+ const codeModeCalls = new Set();
1177
+ const noteCodeModeCall = (part) => {
1178
+ if (!guard || part?.type !== 'tool') return;
1179
+ const decision = openCodeCodeModeDecision(part.tool, guard);
1180
+ const callID = part.callID || part.id || '';
1181
+ if (!decision || codeModeCalls.has(callID)) return;
1182
+ codeModeCalls.add(callID);
1183
+ onEvent({ provider: 'opencode', runId, eventType: 'synabun.confinement', event: confinementEvent(decision, { tool: part.tool, confinement: guard, provider: 'opencode' }) });
1184
+ };
1185
+ // An external_directory ask identifies its originating call, not whether it reads or writes. Resolve that exact
1186
+ // message/call pair from SSE or the message API; never infer a read from the requested path or arbitrary metadata.
1187
+ const permissionTools = new Map();
1188
+ const rememberPermissionTool = (part) => {
1189
+ if (part?.type === 'tool' && part.messageID && part.callID && part.tool) permissionTools.set(`${part.messageID}:${part.callID}`, part.tool);
1190
+ };
1191
+ const permissionToolName = async (event) => {
1192
+ const { messageID, callID } = event?.tool || {};
1193
+ if (!messageID || !callID) return null;
1194
+ const key = `${messageID}:${callID}`;
1195
+ if (!permissionTools.has(key) && typeof client.session?.message === 'function') {
1196
+ try {
1197
+ const row = await settleWithin(client.session.message({ sessionID: sessionId, messageID, directory: cwd || undefined }), 5000);
1198
+ for (const part of row?.data?.parts || []) if (part.messageID === messageID) rememberPermissionTool(part);
1199
+ } catch { /* unknown calls fail closed */ }
1200
+ }
1201
+ return permissionTools.get(key) || null;
1202
+ };
1203
+ /**
1204
+ * A permission ask under the run's confinement: { reply: 'once' | 'reject', message }, the decision written into
1205
+ * the run's events. A refusal carries its reason as the reply's message: OpenCode hands it to the model as the
1206
+ * tool's error (a reject without a message is what OpenCode's own client follows with an interrupt).
1207
+ */
1208
+ const policyReply = async (event) => {
1209
+ if (evaluated) {
1210
+ const reason = evalOpenCodeDecision(event, evaluated);
1211
+ if (!reason) return { reply: 'once' };
1212
+ 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' } });
1213
+ return { reply: 'reject', message: reason };
1214
+ }
1215
+ if (confined) {
1216
+ const reason = draftOpenCodeDecision(event, confined);
1217
+ if (!reason) return { reply: 'once' };
1218
+ 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' } });
1219
+ return { reply: 'reject', message: reason };
1220
+ }
1221
+ if (guard) {
1222
+ const toolName = String(event?.permission || event?.type || '').toLowerCase() === 'external_directory' ? await permissionToolName(event) : null;
1223
+ const decision = openCodePermissionDecision(event, guard, { toolName });
1224
+ if (!decision) return { reply: 'once' };
1225
+ onEvent({ provider: 'opencode', runId, eventType: 'synabun.confinement', event: confinementEvent(decision, { tool: event.permission || event.type || 'permission', confinement: guard, provider: 'opencode' }) });
1226
+ return guard.enforced ? { reply: 'reject', message: decision.reason } : { reply: 'once' };
1227
+ }
1228
+ // Unattended and unguarded: allowed, one call at a time. Never "always": OpenCode 2 saves an "always" answer for
1229
+ // the whole project (GET /api/permission/saved), where it would stop the user's own sessions there from asking.
1230
+ return { reply: 'once' };
1231
+ };
753
1232
  let alive = true;
754
1233
  let activeAbort = null;
755
1234
  let activeTurn = null;
@@ -828,6 +1307,7 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
828
1307
  } catch {}
829
1308
  return '';
830
1309
  };
1310
+ const pendingQuestions = new Set();
831
1311
  const brokerRequest = async (request) => {
832
1312
  try {
833
1313
  const reply = await permissionBroker.request({ runId, provider: 'opencode', signal: activeAbort?.signal, ...request });
@@ -845,7 +1325,8 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
845
1325
  };
846
1326
  const finishTurnWithText = () => {
847
1327
  const turn = activeTurn;
848
- if (!turn || turn.mode !== 'async' || turn.settled || turn.finishing) return;
1328
+ if (pendingQuestions.size) return;
1329
+ if (!turn || turn.mode !== 'async' || turn.sent === false || turn.settled || turn.finishing) return;
849
1330
  turn.finishing = true;
850
1331
  fetchAssistantText()
851
1332
  .then((text) => settleActiveTurn(null, { text, result: { text }, ...turnMedia() }))
@@ -867,19 +1348,27 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
867
1348
  return;
868
1349
  }
869
1350
  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);
1351
+ if (/^message[.:]part[.:]updated$/i.test(eventType)) { rememberPart(event.part || null); rememberPermissionTool(event.part); noteCodeModeCall(event.part); }
871
1352
  if (/^message[.:]part[.:]delta$/i.test(eventType)) rememberDelta(event);
872
1353
  if (/question\.asked/i.test(eventType)) {
873
1354
  const requestID = event.id || event.requestID;
874
- if (requestID && askMode && client.question?.reply) {
875
- brokerRequest({ kind: 'ask', toolName: 'question', input: event, questions: event.questions || [] })
1355
+ if (requestID && !held && permissionBroker?.request && client.question?.reply) {
1356
+ if (pendingQuestions.has(requestID)) return;
1357
+ pendingQuestions.add(requestID);
1358
+ brokerRequest({ kind: 'ask', toolName: 'question', input: event, questions: event.questions || [], protocol: { provider: 'opencode', id: requestID } })
876
1359
  .then((reply) => {
1360
+ // The tool is now answered; an idle SSE arriving before the HTTP ack is valid.
1361
+ pendingQuestions.delete(requestID);
877
1362
  if (reply.behavior === 'allow') {
878
- return client.question.reply({ requestID, answers: reply.answers || [], directory: cwd || undefined });
1363
+ return client.question.reply({ requestID, answers: openCodeAnswers(event.questions || [], reply.answers), directory: cwd || undefined });
879
1364
  }
880
1365
  return client.question?.reject?.({ requestID, directory: cwd || undefined });
881
1366
  })
882
- .catch(() => {});
1367
+ .catch(async (error) => {
1368
+ try { await client.question?.reject?.({ requestID, directory: cwd || undefined }); } catch {}
1369
+ settleActiveTurn(new Error(`OpenCode question reply failed: ${providerErrorMessage(error)}`));
1370
+ })
1371
+ .finally(() => pendingQuestions.delete(requestID));
883
1372
  } else if (requestID && client.question?.reject) {
884
1373
  client.question.reject({ requestID, directory: cwd || undefined }).catch(() => {});
885
1374
  }
@@ -894,7 +1383,11 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
894
1383
  })
895
1384
  .catch(() => {});
896
1385
  } else if (requestID && client.permission?.reply) {
897
- client.permission.reply({ requestID, reply: 'always', directory: cwd || undefined }).catch(() => {});
1386
+ // Unattended: by the run's confinement (a drafting run, a guarded worker), else allowed once.
1387
+ policyReply(event).then((answer) => {
1388
+ if (!alive) return;
1389
+ return client.permission.reply({ requestID, reply: answer.reply, ...(answer.message ? { message: answer.message } : {}), directory: cwd || undefined });
1390
+ }).catch(() => {});
898
1391
  }
899
1392
  }
900
1393
  // promptAsync acknowledges immediately, so the durable SSE stream is the
@@ -912,11 +1405,24 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
912
1405
  }
913
1406
  onEvent({ provider: 'opencode', runId, eventType, event });
914
1407
  }) || (() => {});
1408
+ // The rules of a confined run ride the session too (lib/opencode2-client.js session.create sends them as the
1409
+ // session's own `permissions`), on top of the serve's config.
1410
+ let sessionRules = confined ? openCodeDraftConfig().permissions : evaluated ? openCodeEvalConfig(evaluated).permissions : guard ? openCodePermissionsFor(guard) : null;
1411
+ if (runMode === 'task' && !held) sessionRules = [...(sessionRules || []), { action: 'question', resource: '*', effect: 'allow' }];
915
1412
  try {
916
1413
  await client.waitUntilConnected?.(10_000);
917
- const created = await client.session.create({ directory: cwd || undefined, title: title || 'SynaBun automation' });
1414
+ const created = options.resumeSessionId
1415
+ ? await client.session.get({ sessionID: options.resumeSessionId })
1416
+ : await client.session.create({ directory: cwd || undefined, title: title || 'SynaBun automation', ...(sessionRules ? { permission: sessionRules } : {}) });
1417
+ if (created?.error) throw new Error(`OpenCode session unavailable: ${providerErrorMessage(created.error)}`);
918
1418
  sessionId = created?.data?.id || created?.data?.sessionID || created?.id || created?.sessionID;
919
1419
  if (!sessionId) throw new Error('OpenCode did not return a session id');
1420
+ if (options.resumeSessionId && sessionId !== options.resumeSessionId) throw new Error('OpenCode returned a different session on resume');
1421
+ if (options.resumeSessionId && sessionRules) {
1422
+ if (!client.session.update) throw new Error('OpenCode cannot restore the worker permission rules');
1423
+ const updated = await client.session.update({ sessionID: sessionId, permission: sessionRules });
1424
+ if (!updated || updated.error) throw new Error('OpenCode could not restore the worker permission rules');
1425
+ }
920
1426
  onIdentity({ providerSessionId: sessionId });
921
1427
  } catch (error) {
922
1428
  try { unsubscribe(); } catch {}
@@ -943,7 +1449,7 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
943
1449
  return {
944
1450
  identity: () => ({ providerSessionId: sessionId }),
945
1451
  isAlive: () => alive,
946
- describe: () => ({ provider: 'opencode', permissionPolicy, capability, askMode }),
1452
+ 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
1453
  /** { children(sessionId) → sessions, messages(sessionId) → [{ info }] } on this run's own serve. */
948
1454
  usageFetchers: () => ({
949
1455
  children: (id) => storedRows('children', id),
@@ -952,13 +1458,29 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
952
1458
  async runTurn(prompt, meta = {}) {
953
1459
  if (!alive) throw new Error('OpenCode native loop is closed');
954
1460
  if (activeTurn) throw new Error('OpenCode native loop already has an active turn');
955
- await client.waitUntilConnected?.(10_000);
956
- activeAbort = new AbortController();
1461
+ const controller = new AbortController();
1462
+ activeAbort = controller;
1463
+ const checkReady = async () => {
1464
+ await client.waitUntilConnected?.(10_000);
1465
+ if (!alive || controller.signal.aborted) throw asAbortError('aborted');
1466
+ const ready = await waitForSynabunTools(client, {
1467
+ ...(options.mcpWait || {}), directory: cwd || undefined,
1468
+ expected: options.synabunExpected !== false && !held, signal: controller.signal,
1469
+ stopped: () => !alive,
1470
+ onTimeout: (ms) => {
1471
+ const text = synabunToolsTimeoutText(ms, 'worker');
1472
+ onNote?.(text);
1473
+ onEvent({ provider: 'opencode', runId, eventType: 'synabun.mcp.status', event: { sessionID: sessionId, text } });
1474
+ },
1475
+ });
1476
+ if (!ready || controller.signal.aborted || !alive) throw asAbortError('aborted');
1477
+ };
957
1478
  const request = {
958
1479
  sessionID: sessionId,
959
1480
  parts: [{ type: 'text', text: prompt }],
960
1481
  model: normalizeOpenCodeModel(model),
961
- agent: capability === 'read-only' ? 'plan' : (agent ? String(agent) : 'build'),
1482
+ // Capability is enforced by session rules, the adapter and sandbox, not OpenCode's planning workflow.
1483
+ agent: agent ? String(agent) : 'build',
962
1484
  directory: cwd || undefined,
963
1485
  signal: activeAbort.signal,
964
1486
  };
@@ -994,11 +1516,13 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
994
1516
  // reported as an unhandled promise before runTurn begins awaiting it.
995
1517
  completion.catch(() => {});
996
1518
  const turn = {
997
- mode: 'async', completion,
1519
+ mode: 'async', completion, sent: false,
998
1520
  resolve: resolveTurn, reject: rejectTurn, settled: false,
999
1521
  };
1000
1522
  activeTurn = turn;
1001
1523
  try {
1524
+ await checkReady();
1525
+ turn.sent = true;
1002
1526
  const response = await client.session.promptAsync(request);
1003
1527
  validateResponse(response);
1004
1528
  return await completion;
@@ -1011,6 +1535,7 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
1011
1535
  const turn = { mode: 'sync' };
1012
1536
  activeTurn = turn;
1013
1537
  try {
1538
+ await checkReady();
1014
1539
  const response = await client.session.prompt(request);
1015
1540
  validateResponse(response);
1016
1541
  const text = extractOpenCodeText(response);
@@ -1038,3 +1563,377 @@ export async function createOpenCodeNativeLoopAdapter(options = {}) {
1038
1563
  },
1039
1564
  };
1040
1565
  }
1566
+
1567
+ // ── Antigravity (Google's `agy` CLI, over ACP) ──────────────────────────────
1568
+ //
1569
+ // One `agy acp` process and one ACP session per run (lib/antigravity/acp-connection.js), every turn a session/prompt,
1570
+ // in the run's own Antigravity home (`--gemini-dir`, lib/antigravity/home.js): its MCP servers are exactly SynaBun's
1571
+ // and the external servers its tool list names, and its PreToolUse hook asks SynaBun about every call before it runs
1572
+ // (lib/antigravity/policy.js → antigravityRunPolicy: the browser policy, the run's MCP scope, the worker's
1573
+ // confinement). A refusal reaches the model with its reason ("tool call denied by pre-tool hook: …") and the turn goes
1574
+ // on; an ACP permission refusal would end the turn without one. Approvals stay with ACP's session/request_permission:
1575
+ // the Assistant's broker answers them under 'ask', else they are allowed once. Never "always": Antigravity would save
1576
+ // it for later sessions. The model and thinking level are the session's own config options (set only to a value the
1577
+ // session offers). The home keeps the conversation, so a run resumes after a restart (session/resume, no replay).
1578
+ // SynaBun's MCP child gets the run's pins from the agent's environment, which agy's MCP children inherit.
1579
+
1580
+ /** The hook script every SynaBun home runs before a tool call. */
1581
+ export const ANTIGRAVITY_POLICY_HOOK = fileURLToPath(new URL('./antigravity/policy-hook.mjs', import.meta.url));
1582
+
1583
+ /** The ACP permission option for an answer: explicit kinds only, never allow-always. */
1584
+ export function acpPermissionOption(options, wanted) {
1585
+ const list = Array.isArray(options) ? options.filter((option) => option && typeof option.optionId === 'string') : [];
1586
+ for (const kind of wanted) {
1587
+ if (kind === 'allow_always') continue;
1588
+ const found = list.find((option) => option.kind === kind);
1589
+ if (found) return found.optionId;
1590
+ }
1591
+ return null;
1592
+ }
1593
+
1594
+ /** The last JSON object in a reply (an output schema's answer, which ACP has no channel for), or null. */
1595
+ export function lastJsonObject(text) {
1596
+ const source = String(text || '');
1597
+ const fenced = [...source.matchAll(/```(?:json)?\s*([\s\S]*?)```/g)].map((m) => m[1]).reverse();
1598
+ for (const candidate of [...fenced, source]) {
1599
+ const trimmed = candidate.trim();
1600
+ for (let start = trimmed.lastIndexOf('{'); start >= 0; start = trimmed.lastIndexOf('{', start - 1)) {
1601
+ const end = trimmed.lastIndexOf('}');
1602
+ if (end <= start) continue;
1603
+ try {
1604
+ const value = JSON.parse(trimmed.slice(start, end + 1));
1605
+ if (value && typeof value === 'object' && !Array.isArray(value)) return value;
1606
+ } catch { /* try an earlier brace */ }
1607
+ }
1608
+ }
1609
+ return null;
1610
+ }
1611
+
1612
+ /**
1613
+ * One Antigravity run's policy for one tool call (acpCall's shape): what its hook answers, and what a permission
1614
+ * request is checked against again. { decision: 'deny', reason, event? } | { decision: 'ask' | 'allow', event? }.
1615
+ * - MCP tool schemas in the run's home may be read; home configuration is refused;
1616
+ * - the browser policy (lib/browser-tool-policy.js: Antigravity's own web search, URL reading and browser, a shell
1617
+ * that starts a browser or browser code, another server's browser tools);
1618
+ * - the run's MCP scope (`allowedServers`: SynaBun and the external servers its tool list names;
1619
+ * enabled project MCP servers are refused before agy starts);
1620
+ * - the worker's confinement (lib/worker-confinement.js acpPermissionDecision; warn mode reports and allows);
1621
+ * - then the approval: 'ask' for anything but a read under the 'ask' policy, else 'allow'.
1622
+ */
1623
+ export function antigravityRunPolicy(call, { homeDir = null, cwd = null, allowedServers = null, guard = null, askMode = false, policyRoots = [] } = {}) {
1624
+ if (!call || (!call.name && !call.title && (!call.kind || call.kind === 'other'))) return { decision: 'deny', reason: 'SynaBun received no valid tool call, so it is refused.' };
1625
+ const approval = () => (askMode && !acpReadOnlyKind(call.kind) ? 'ask' : 'allow');
1626
+ const name = acpToolName(call);
1627
+ const input = acpPolicyInput(call);
1628
+ const browser = browserToolDenial(acpWebTool(call) && !name.startsWith('mcp__') ? (call.name || 'search_web') : name, input, { host: 'antigravity', cwd, roots: policyRoots });
1629
+ if (browser) return { decision: 'deny', reason: browser, event: { type: 'synabun.browser_policy', action: 'refused', tool: name, reason: browser } };
1630
+ if (call.mcp && allowedServers && !allowedServers.has(call.mcp.server)) {
1631
+ const reason = `SynaBun: the MCP server ${call.mcp.server} is not one of this run's servers (${[...allowedServers].join(', ') || 'none'}), so ${call.mcp.tool} is refused. Use the tools this run was given.`;
1632
+ return { decision: 'deny', reason, event: { type: 'synabun.mcp_scope', action: 'refused', server: call.mcp.server, tool: call.mcp.tool, reason } };
1633
+ }
1634
+ const homeRead = antigravityHomeReadPolicy(call, { homeDir, cwd });
1635
+ if (homeRead) return homeRead;
1636
+ if (guard) {
1637
+ const decision = acpPermissionDecision(call, guard);
1638
+ if (decision) {
1639
+ const event = confinementEvent(decision, { tool: call.title || name, confinement: guard, provider: 'antigravity' });
1640
+ if (guard.enforced) return { decision: 'deny', reason: decision.reason, event };
1641
+ return { decision: approval(), event };
1642
+ }
1643
+ }
1644
+ return { decision: approval() };
1645
+ }
1646
+
1647
+ /** The MCP servers of a run's home: SynaBun's own entry, then the external servers its tool list names. */
1648
+ function antigravityRunServers({ tools, synabunMcpEntry, userGemini, onNote }) {
1649
+ const servers = {};
1650
+ let inventory = null;
1651
+ let names = [];
1652
+ try {
1653
+ inventory = externalServerInventory({ provider: 'antigravity', antigravityConfigPath: join(userGemini, 'config', 'mcp_config.json'), dataHome: memoryConfig.dataHome });
1654
+ names = selectExternalServers(tools.items, inventory).names;
1655
+ } catch (error) {
1656
+ if (error?.code === 'TOOLS_INVALID') throw error;
1657
+ onNote?.(`Antigravity: the external MCP servers could not be read (${error?.message || error}); this run has SynaBun's tools only.`);
1658
+ }
1659
+ const own = synabunMcpEntry || userAntigravityMcpServers(userGemini).SynaBun || null;
1660
+ if (own) servers.SynaBun = toAntigravityMcpEntry(own);
1661
+ for (const name of names) {
1662
+ const definition = inventory?.definitionFor(name);
1663
+ if (definition && name !== 'SynaBun') servers[name] = toAntigravityMcpEntry(definition);
1664
+ }
1665
+ return servers;
1666
+ }
1667
+
1668
+ export async function createAntigravityNativeLoopAdapter(options = {}) {
1669
+ const {
1670
+ runId, cwd, model = null, effort = null, launch = null,
1671
+ onIdentity = () => {}, onEvent = () => {}, onProcess = null, onNote = null,
1672
+ permissionPolicy: rawPolicy = 'auto', permissionBroker = null,
1673
+ capability: rawCapability = 'full', outputSchema = null, runMode = 'loop',
1674
+ browserSessionId = null, browserTabId = null, desktopGrantProvider = null,
1675
+ extraEnv = {}, unsetEnv = [], resumeSessionId = null,
1676
+ confinement = null,
1677
+ guardMode = WORKER_GUARD_DEFAULT, scratchDir = null, serverPort = null, localPortRange = null,
1678
+ connect = createAcpConnection,
1679
+ // The run's Antigravity home (lib/antigravity/home.js). null: agy runs in the user's own ~/.gemini, with every
1680
+ // server of theirs and no hook (an older server's wiring, a test of the bare protocol).
1681
+ homeDir = null, synabunMcpEntry = null, userGemini = userGeminiDir(),
1682
+ // The hook's way back (lib/antigravity/policy.js): the registry, the loopback address of POST /api/antigravity/policy.
1683
+ policy = null, policyUrl = null, policyHookScript = ANTIGRAVITY_POLICY_HOOK, policyRoots = [],
1684
+ // ACP counts tokens per session: what the session had counted when the run was suspended (a resume books only
1685
+ // what follows), and where each turn's count is kept for the next resume.
1686
+ sessionUsageBase = null, onSessionUsage = null,
1687
+ } = options;
1688
+ if (!launch?.command) {
1689
+ const error = new Error('The Antigravity CLI (agy) is not installed or could not be resolved.');
1690
+ error.code = 'CLI_MISSING';
1691
+ throw error;
1692
+ }
1693
+ // Skill drafting and evaluation runs hold their own confinement; Antigravity has no setup for one, and a confined
1694
+ // run never starts unconfined.
1695
+ if (confinement) throw confinementUnsupportedError('Antigravity', 'skill drafting and evaluation run on Claude Code, Codex and OpenCode', confinement?.kind === EVAL_CONFINEMENT ? 'an evaluation folder' : 'a drafting folder');
1696
+ assertSafeAntigravityProject(cwd);
1697
+ const permissionPolicy = normalizePermissionPolicy(rawPolicy);
1698
+ const capability = normalizeCapability(rawCapability);
1699
+ const guard = workerGuardFor({ capability, cwd, guardMode, scratchDir, confinement: null, serverPort, localPortRange });
1700
+ const askMode = permissionPolicy === 'ask' && typeof permissionBroker?.request === 'function';
1701
+ const tools = nativeToolSelection(options, 'antigravity');
1702
+
1703
+ // The run's home: its servers, its hook, its settings, the user's rules and skills.
1704
+ let allowedServers = null;
1705
+ let hookCommand = null;
1706
+ if (homeDir) {
1707
+ const servers = antigravityRunServers({ tools, synabunMcpEntry, userGemini, onNote });
1708
+ allowedServers = new Set(Object.keys(servers));
1709
+ if (typeof policy?.register === 'function' && policyUrl) hookCommand = policyHookCommand({ script: policyHookScript });
1710
+ else onNote?.('Antigravity: SynaBun\'s tool-call hook is not available in this server, so the browser policy and the run\'s MCP scope are checked only when Antigravity asks for approval.');
1711
+ prepareAntigravityHome({ dir: homeDir, mcpServers: servers, hookCommand, settings: antigravityHomeSettings({ userSettings: userAntigravitySettings(userGemini) }), userGemini });
1712
+ }
1713
+ const policyOptions = { homeDir, cwd, allowedServers, guard, askMode, policyRoots };
1714
+
1715
+ const pins = stringEnv({
1716
+ SYNABUN_TERMINAL_SESSION: runId,
1717
+ SYNABUN_RUN_MODE: options.runMode === 'task' ? 'task' : undefined,
1718
+ SYNABUN_PROJECT: cwd ? memoryProject(cwd) : 'global',
1719
+ SYNABUN_BROWSER_SESSION: browserSessionId,
1720
+ SYNABUN_BROWSER_TAB: browserTabId,
1721
+ SYNABUN_TOOLS: runtimeToolsValue(tools.items.includes('all') ? tools.items : [...tools.groups, ...tools.servers]),
1722
+ SYNABUN_DESKTOP_GRANT: typeof desktopGrantProvider === 'function' ? desktopGrantProvider() || undefined : undefined,
1723
+ });
1724
+ const inherited = { ...process.env };
1725
+ for (const key of ['SYNABUN_PROFILE', 'SYNABUN_RUNTIME_PROFILE_PATH', 'SYNABUN_TOOLS', 'SYNABUN_TOOLS_PATH', 'SYNABUN_TOOL_CATALOG_MODE', 'SYNABUN_ROLE', 'SYNABUN_AGY_POLICY_URL', 'SYNABUN_AGY_POLICY_TOKEN']) delete inherited[key];
1726
+ for (const key of Array.isArray(unsetEnv) ? unsetEnv : []) delete inherited[key];
1727
+ delete inherited.SYNABUN_BRAIN_TOKEN;
1728
+ for (const key of LIVE_DATA_ENV) delete inherited[key];
1729
+
1730
+ let alive = true;
1731
+ let sessionId = null;
1732
+ let providerTurn = Number(options.providerTurnBase) || 0;
1733
+ let activeTurn = null; // { iteration, providerTurn, text, controller }
1734
+ let lastSessionUsage = sessionUsageBase && typeof sessionUsageBase === 'object' ? sessionUsageBase : null;
1735
+ const openPermissions = new Set(); // resolvers of permission answers still waiting
1736
+ const startedAt = Date.now();
1737
+
1738
+ const emit = (eventType, event, extra = {}) => {
1739
+ try { onEvent({ provider: 'antigravity', runId, eventType, event, ...extra }); } catch { /* observers never break a run */ }
1740
+ };
1741
+ const turnMeta = () => (activeTurn ? { iteration: activeTurn.iteration, providerTurn: activeTurn.providerTurn } : {});
1742
+ // The hook's question (and, without a hook, the permission request's): the answer, its event in the run's events.
1743
+ const checkCall = (call) => {
1744
+ const answer = antigravityRunPolicy(call, policyOptions);
1745
+ if (answer.event) emit(answer.event.type, answer.event, turnMeta());
1746
+ return answer;
1747
+ };
1748
+ const registration = hookCommand ? policy.register((call) => (alive ? checkCall(call) : { decision: 'deny', reason: 'This run has ended.' }), { label: `run ${runId}` }) : null;
1749
+ const env = stringEnv({
1750
+ ...inherited, ...pins, ...stringEnv(extraEnv),
1751
+ ...(registration ? { SYNABUN_AGY_POLICY_URL: policyUrl, SYNABUN_AGY_POLICY_TOKEN: registration.token } : {}),
1752
+ });
1753
+
1754
+ const decidePermission = async (params, signal) => {
1755
+ const call = acpCall(params);
1756
+ const options = params?.options || [];
1757
+ const allowOnce = () => ({ outcome: { outcome: 'selected', optionId: acpPermissionOption(options, ['allow_once']) } });
1758
+ const rejectOnce = () => ({ outcome: { outcome: 'selected', optionId: acpPermissionOption(options, ['reject_once', 'reject_always']) } });
1759
+ const settle = (answer) => (answer.outcome.optionId ? answer : { outcome: { outcome: 'cancelled' } });
1760
+ if (!alive) return { outcome: { outcome: 'cancelled' } };
1761
+ // With the hook the call was checked before Antigravity asked: checked again here without reporting it twice
1762
+ // (a refusal here ends the turn, which only a call the hook never saw can earn).
1763
+ const verdict = registration ? antigravityRunPolicy(call, policyOptions) : checkCall(call);
1764
+ if (verdict.decision === 'deny') return settle(rejectOnce());
1765
+ if (askMode && !acpReadOnlyKind(call.kind)) {
1766
+ try {
1767
+ // Allowed once whatever the answer said about "always": Antigravity would save an "always" for every later
1768
+ // session of the user's, outside this run (the dispatcher drops it for Antigravity too).
1769
+ const reply = await permissionBroker.request({ runId, provider: 'antigravity', signal, kind: 'tool', toolName: call.mcp ? `${call.mcp.server}_${call.mcp.tool}` : (call.title || call.name || call.kind || 'tool'), input: params?.toolCall || {} });
1770
+ if (reply?.behavior === 'allow') return settle(allowOnce());
1771
+ return settle(rejectOnce());
1772
+ } catch {
1773
+ return settle(rejectOnce());
1774
+ }
1775
+ }
1776
+ return settle(allowOnce());
1777
+ };
1778
+
1779
+ const conn = connect({
1780
+ command: launch.command,
1781
+ args: launch.args || [],
1782
+ acpArgs: homeDir ? ['--gemini-dir', homeDir] : [],
1783
+ cwd: cwd || undefined,
1784
+ env,
1785
+ clientInfo: { name: 'synabun', title: 'SynaBun', version: '0' },
1786
+ onUpdate: (params) => {
1787
+ if (!params || params.sessionId !== sessionId || !params.update) return;
1788
+ const update = params.update;
1789
+ if (activeTurn && update.sessionUpdate === 'agent_message_chunk' && update.content?.type === 'text') activeTurn.text += update.content.text || '';
1790
+ emit(update.sessionUpdate || 'session/update', update, turnMeta());
1791
+ },
1792
+ onElicitation: async (params, { signal } = {}) => {
1793
+ if (!alive || !activeTurn || params.sessionId !== sessionId || !permissionBroker?.request) return { action: 'cancel' };
1794
+ const reply = await permissionBroker.request({ runId, provider: 'antigravity', signal: AbortSignal.any([activeTurn?.controller?.signal, signal].filter(Boolean)), kind: 'ask', toolName: 'choice', input: params, questions: workerQuestions(params), protocol: { provider: 'antigravity', method: 'elicitation/create' } });
1795
+ return elicitationReply(params, reply);
1796
+ },
1797
+ onPermission: (params, { signal } = {}) => new Promise((resolve) => {
1798
+ const done = (answer) => { openPermissions.delete(done); resolve(answer); };
1799
+ openPermissions.add(done);
1800
+ decidePermission(params, signal).then(done, () => done({ outcome: { outcome: 'cancelled' } }));
1801
+ }),
1802
+ onExit: (info) => {
1803
+ alive = false;
1804
+ for (const done of [...openPermissions]) done({ outcome: { outcome: 'cancelled' } });
1805
+ if (info?.error || (info?.code && info.code !== 0)) emit('synabun.provider_exit', { type: 'synabun.provider_exit', code: info.code, signal: info.signal, error: info.error || null });
1806
+ },
1807
+ });
1808
+ try { onProcess?.({ pid: conn.pid, startedAt, home: homeDir || null }); } catch { /* informational */ }
1809
+
1810
+ const fail = async (error) => {
1811
+ alive = false;
1812
+ registration?.dispose();
1813
+ try { await conn.close(); } catch { /* gone */ }
1814
+ throw error;
1815
+ };
1816
+ let session;
1817
+ try {
1818
+ const init = await conn.initialize({ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, elicitation: { form: {} } } });
1819
+ if (resumeSessionId) {
1820
+ // session/resume picks the conversation up without replaying it; an agent without it replays (session/load),
1821
+ // which onUpdate drops (sessionId is set only once the load answered).
1822
+ const caps = init?.agentCapabilities?.sessionCapabilities || {};
1823
+ const method = caps.resume ? 'session/resume' : 'session/load';
1824
+ session = await conn.request(method, { sessionId: String(resumeSessionId), cwd, mcpServers: [] });
1825
+ sessionId = String(session?.sessionId || resumeSessionId);
1826
+ } else {
1827
+ session = await conn.request('session/new', { cwd, mcpServers: [] });
1828
+ sessionId = session.sessionId;
1829
+ }
1830
+ } catch (error) {
1831
+ const message = Number(error?.code) === -32000
1832
+ ? 'Antigravity is not signed in: open the Antigravity panel (or run `agy` in a terminal) and sign in, then try again.'
1833
+ : providerErrorMessage(error, resumeSessionId ? 'Antigravity could not resume the run\'s session' : 'Antigravity could not start a session');
1834
+ return fail(Object.assign(new Error(message), { code: Number(error?.code) === -32000 ? 'AUTH_REQUIRED' : (error?.code || 'START_FAILED') }));
1835
+ }
1836
+ onIdentity({ providerSessionId: sessionId, ...(homeDir ? { antigravityHome: homeDir } : {}) });
1837
+
1838
+ // The model and the thinking level, as the session's own config options name them. A model id with its level
1839
+ // (gemini-4-argon-high, as `agy models` and ACP's model list spell it) is that model at that level.
1840
+ let configOptions = Array.isArray(session?.configOptions) ? session.configOptions : [];
1841
+ const offered = (category) => {
1842
+ const option = configOptions.find((o) => o?.category === category);
1843
+ const accepted = [];
1844
+ for (const entry of option?.options || []) for (const v of Array.isArray(entry?.options) ? entry.options : [entry]) if (v && v.value !== undefined) accepted.push(String(v.value));
1845
+ return { option, accepted };
1846
+ };
1847
+ const setOption = async (category, value, label) => {
1848
+ if (!value) return;
1849
+ const { option, accepted } = offered(category);
1850
+ if (!option) {
1851
+ if (category === 'model') {
1852
+ try { await conn.request('session/set_model', { sessionId, modelId: String(value) }); } catch (error) { onNote?.(`Antigravity kept its default model: ${providerErrorMessage(error, 'set_model failed')}`); }
1853
+ }
1854
+ return;
1855
+ }
1856
+ if (!accepted.includes(String(value))) {
1857
+ if (category === 'model') throw Object.assign(new Error(`Antigravity does not offer the model ${value} (it offers ${accepted.join(', ') || 'none'}).`), { code: 'MODEL_UNAVAILABLE' });
1858
+ onNote?.(`Antigravity has no ${label} "${value}" for this model; it keeps ${option.currentValue}.`);
1859
+ return;
1860
+ }
1861
+ if (String(option.currentValue) === String(value)) return;
1862
+ const answer = await conn.request('session/set_config_option', { sessionId, configId: option.id, value: String(value) });
1863
+ if (Array.isArray(answer?.configOptions)) configOptions = answer.configOptions;
1864
+ };
1865
+ const wanted = splitAntigravityModel(model, offered('model').accepted);
1866
+ const level = effort && !['off', 'default', 'auto', 'none'].includes(String(effort).toLowerCase()) ? String(effort) : wanted.effort;
1867
+ try {
1868
+ await setOption('model', wanted.model, 'model');
1869
+ await setOption('thought_level', level, 'thinking level');
1870
+ } catch (error) {
1871
+ return fail(error);
1872
+ }
1873
+ const optionValue = (category) => configOptions.find((o) => o?.category === category)?.currentValue ?? null;
1874
+
1875
+ return {
1876
+ identity: () => ({ providerSessionId: sessionId, ...(homeDir ? { antigravityHome: homeDir } : {}) }),
1877
+ isAlive: () => alive && conn.running,
1878
+ processInfo: () => ({ pid: conn.pid, startedAt, home: homeDir || null }),
1879
+ describe: () => ({
1880
+ provider: 'antigravity', permissionPolicy, capability, askMode, model: optionValue('model'), effort: optionValue('thought_level'),
1881
+ ...(homeDir ? { home: homeDir, servers: [...(allowedServers || [])], hook: !!registration } : {}),
1882
+ ...(guard ? { guard: { mode: guard.mode, capability: guard.capability, writable: [...guard.writable] } } : {}),
1883
+ }),
1884
+ async runTurn(prompt, meta = {}) {
1885
+ if (!alive || !conn.running) throw new Error('Antigravity native loop is closed');
1886
+ if (activeTurn) throw new Error('Antigravity native loop already has an active turn');
1887
+ providerTurn += 1;
1888
+ const turn = { iteration: meta.iteration, providerTurn, text: '', cancelled: false, controller: new AbortController() };
1889
+ activeTurn = turn;
1890
+ emit('synabun.user_prompt', { type: 'synabun.user_prompt', text: String(prompt || '') }, { iteration: meta.iteration, providerTurn });
1891
+ const schema = meta.outputSchema || outputSchema;
1892
+ const body = schema && typeof schema === 'object'
1893
+ ? `${String(prompt || '')}\n\nEnd your reply with one JSON object (in a \`\`\`json block) that matches this JSON Schema:\n${JSON.stringify(schema)}`
1894
+ : String(prompt || '');
1895
+ try {
1896
+ const answer = await conn.request('session/prompt', { sessionId, prompt: [{ type: 'text', text: body }] }, { timeoutMs: 0 });
1897
+ // The turn's own tokens, whatever its end (a stopped turn spent them too).
1898
+ const usage = acpTurnUsage(answer?.usage, lastSessionUsage);
1899
+ if (answer?.usage) {
1900
+ lastSessionUsage = answer.usage;
1901
+ try { onSessionUsage?.(answer.usage); } catch { /* bookkeeping */ }
1902
+ }
1903
+ if (answer?.stopReason === 'cancelled' || turn.cancelled) throw Object.assign(asAbortError('aborted'), { usage, sessionUsage: answer?.usage || null, providerSessionId: sessionId });
1904
+ const text = turn.text;
1905
+ const result = { providerSessionId: sessionId, text, usage, sessionUsage: answer?.usage || null, stopReason: answer?.stopReason || 'end_turn', result: { text, usage } };
1906
+ if (answer?.stopReason === 'refusal') result.refused = true;
1907
+ if (schema && typeof schema === 'object') {
1908
+ const structured = lastJsonObject(text);
1909
+ if (structured) result.structured = structured;
1910
+ }
1911
+ return result;
1912
+ } catch (error) {
1913
+ if (error?.name === 'AbortError') throw error;
1914
+ if (!alive || !conn.running) throw new Error(`Antigravity stopped during the turn${conn.stderrTail() ? `: ${conn.stderrTail().slice(-400)}` : ''}`);
1915
+ throw new Error(providerErrorMessage(error, 'Antigravity turn failed'));
1916
+ } finally {
1917
+ turn.controller.abort();
1918
+ if (activeTurn === turn) activeTurn = null;
1919
+ }
1920
+ },
1921
+ async abort() {
1922
+ if (activeTurn) { activeTurn.cancelled = true; activeTurn.controller.abort(); }
1923
+ for (const done of [...openPermissions]) done({ outcome: { outcome: 'cancelled' } });
1924
+ try { if (sessionId && conn.running) await settleWithin(conn.notify('session/cancel', { sessionId })); } catch {}
1925
+ return true;
1926
+ },
1927
+ // The process is the run's own: it always ends with the run. Its conversation stays in the run's home, where a
1928
+ // resume after a restart finds it (the dispatcher's prune removes the home once the run is gone).
1929
+ async dispose() {
1930
+ const wasAlive = alive;
1931
+ alive = false;
1932
+ activeTurn?.controller.abort();
1933
+ for (const done of [...openPermissions]) done({ outcome: { outcome: 'cancelled' } });
1934
+ if (wasAlive && activeTurn && sessionId && conn.running) { try { await settleWithin(conn.notify('session/cancel', { sessionId })); } catch {} }
1935
+ await settleWithin(conn.close(), 3000);
1936
+ registration?.dispose();
1937
+ },
1938
+ };
1939
+ }