synabun 2.1.0 → 2.2.1

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 (332) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +1 -1
  3. package/docs/whatsapp.md +11 -11
  4. package/lib/client-config-repair.js +13 -9
  5. package/lib/external-tools.js +6 -5
  6. package/lib/update-snapshot.js +77 -3
  7. package/lib/virustotal.mjs +12 -1
  8. package/mcp-server/dist/index.js +1 -1
  9. package/mcp-server/dist/index.js.map +1 -1
  10. package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
  11. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
  12. package/mcp-server/dist/services/budget-judgments.js +5 -3
  13. package/mcp-server/dist/services/budget-judgments.js.map +1 -1
  14. package/mcp-server/dist/services/identity.d.ts +2 -2
  15. package/mcp-server/dist/services/identity.js +2 -2
  16. package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
  17. package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
  18. package/mcp-server/dist/services/memory-sessions.js +28 -2
  19. package/mcp-server/dist/services/memory-sessions.js.map +1 -1
  20. package/mcp-server/dist/services/neural-interface.d.ts +2 -1
  21. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  22. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  23. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  24. package/mcp-server/dist/services/server-instructions.js +2 -1
  25. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  26. package/mcp-server/dist/services/tool-catalog.js +1 -1
  27. package/mcp-server/dist/services/tool-catalog.js.map +1 -1
  28. package/mcp-server/dist/services/tool-list.d.ts +1 -1
  29. package/mcp-server/dist/services/tool-list.d.ts.map +1 -1
  30. package/mcp-server/dist/services/tool-runtime.d.ts +2 -2
  31. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -1
  32. package/mcp-server/dist/services/tool-runtime.js +1 -1
  33. package/mcp-server/dist/services/tool-runtime.js.map +1 -1
  34. package/mcp-server/dist/services/typesafe-config.js +1 -1
  35. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  36. package/mcp-server/dist/tools/agents.d.ts +8 -8
  37. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  38. package/mcp-server/dist/tools/agents.js +10 -9
  39. package/mcp-server/dist/tools/agents.js.map +1 -1
  40. package/mcp-server/dist/tools/choice.d.ts +6 -0
  41. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  42. package/mcp-server/dist/tools/choice.js +18 -13
  43. package/mcp-server/dist/tools/choice.js.map +1 -1
  44. package/mcp-server/dist/tools/git-tools.js +2 -2
  45. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  46. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  47. package/mcp-server/dist/tools/morelogin.js +9 -6
  48. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  49. package/mcp-server/dist/tools/skill-tools.d.ts +8 -8
  50. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -1
  51. package/mcp-server/dist/tools/skill-tools.js +11 -8
  52. package/mcp-server/dist/tools/skill-tools.js.map +1 -1
  53. package/mcp-server/package-lock.json +2 -2
  54. package/mcp-server/package.json +1 -1
  55. package/mcp-server/src/index.ts +1 -1
  56. package/mcp-server/src/services/budget-judgments.ts +5 -3
  57. package/mcp-server/src/services/identity.ts +2 -2
  58. package/mcp-server/src/services/memory-sessions.ts +24 -3
  59. package/mcp-server/src/services/neural-interface.ts +2 -1
  60. package/mcp-server/src/services/server-instructions.ts +2 -1
  61. package/mcp-server/src/services/tool-catalog.ts +1 -1
  62. package/mcp-server/src/services/tool-list.ts +1 -1
  63. package/mcp-server/src/services/tool-runtime.ts +2 -2
  64. package/mcp-server/src/services/typesafe-config.ts +1 -1
  65. package/mcp-server/src/tools/agents.ts +10 -9
  66. package/mcp-server/src/tools/choice.ts +17 -12
  67. package/mcp-server/src/tools/git-tools.ts +2 -2
  68. package/mcp-server/src/tools/morelogin.ts +6 -4
  69. package/mcp-server/src/tools/skill-tools.ts +11 -8
  70. package/neural-interface/i18n/en.json +504 -39
  71. package/neural-interface/i18n/pt-BR.json +504 -39
  72. package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
  73. package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
  74. package/neural-interface/lib/antigravity/acp-connection.js +299 -0
  75. package/neural-interface/lib/antigravity/bypass.js +100 -0
  76. package/neural-interface/lib/antigravity/catalog.js +273 -0
  77. package/neural-interface/lib/antigravity/home.js +260 -0
  78. package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
  79. package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
  80. package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
  81. package/neural-interface/lib/antigravity/policy.js +74 -0
  82. package/neural-interface/lib/antigravity/tool-call.js +178 -0
  83. package/neural-interface/lib/antigravity/transcripts.js +252 -0
  84. package/neural-interface/lib/antigravity/usage.js +43 -0
  85. package/neural-interface/lib/assistant-api.js +1 -0
  86. package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
  87. package/neural-interface/lib/assistant-brains/codex.js +6 -1
  88. package/neural-interface/lib/assistant-budget-planner.js +7 -3
  89. package/neural-interface/lib/assistant-budget-split.js +42 -15
  90. package/neural-interface/lib/assistant-catalog.js +76 -9
  91. package/neural-interface/lib/assistant-config.js +6 -3
  92. package/neural-interface/lib/assistant-dispatch.js +211 -50
  93. package/neural-interface/lib/assistant-envelope.js +153 -1
  94. package/neural-interface/lib/assistant-evidence.js +80 -0
  95. package/neural-interface/lib/assistant-hidden-models.js +7 -5
  96. package/neural-interface/lib/assistant-persona.js +16 -11
  97. package/neural-interface/lib/assistant-plan-permissions.js +15 -0
  98. package/neural-interface/lib/assistant-router.js +237 -35
  99. package/neural-interface/lib/assistant-runtime.js +158 -12
  100. package/neural-interface/lib/assistant-task-prompt.js +10 -8
  101. package/neural-interface/lib/assistant-tools.js +2 -2
  102. package/neural-interface/lib/assistant-usage.js +29 -0
  103. package/neural-interface/lib/backup-service.js +6 -1
  104. package/neural-interface/lib/browser-guard-cdp.js +5 -4
  105. package/neural-interface/lib/browser-network-guard.js +274 -82
  106. package/neural-interface/lib/browser-tool-policy.js +12 -7
  107. package/neural-interface/lib/claude-agent-bridge.js +7 -0
  108. package/neural-interface/lib/claude-history-cache.js +210 -0
  109. package/neural-interface/lib/claude-model-catalog.js +5 -1
  110. package/neural-interface/lib/cli-install-api.js +1 -1
  111. package/neural-interface/lib/cli-install.js +160 -31
  112. package/neural-interface/lib/cli-resolver.js +9 -2
  113. package/neural-interface/lib/cli-update-planner.js +25 -15
  114. package/neural-interface/lib/codex-rollout-reader.js +52 -0
  115. package/neural-interface/lib/codex-worker-app-server.js +25 -6
  116. package/neural-interface/lib/editor-git-api.js +3 -20
  117. package/neural-interface/lib/exec-loop-command.js +9 -2
  118. package/neural-interface/lib/file-explorer-api.js +74 -40
  119. package/neural-interface/lib/file-explorer-watch.js +9 -3
  120. package/neural-interface/lib/git-bash/install.js +52 -6
  121. package/neural-interface/lib/git-bash/manager.js +59 -17
  122. package/neural-interface/lib/git-bash/pins.js +12 -0
  123. package/neural-interface/lib/git-read.js +2 -2
  124. package/neural-interface/lib/git-status-runner.js +390 -0
  125. package/neural-interface/lib/git-status-watchdog.js +40 -0
  126. package/neural-interface/lib/greeting/budgets.js +1 -0
  127. package/neural-interface/lib/greeting/engine.js +2 -2
  128. package/neural-interface/lib/greeting/sources.js +10 -12
  129. package/neural-interface/lib/greeting/writer.js +1 -1
  130. package/neural-interface/lib/mcp-client-config.js +37 -6
  131. package/neural-interface/lib/mcp-external-servers.js +40 -7
  132. package/neural-interface/lib/mcp-installer.js +2 -1
  133. package/neural-interface/lib/memory-session-capture.js +17 -3
  134. package/neural-interface/lib/morelogin.js +147 -25
  135. package/neural-interface/lib/native-loop-providers.js +423 -23
  136. package/neural-interface/lib/native-loop-runtime.js +23 -2
  137. package/neural-interface/lib/opencode-native-boundary.js +165 -22
  138. package/neural-interface/lib/owner-authority.js +1 -1
  139. package/neural-interface/lib/path-confine.js +12 -9
  140. package/neural-interface/lib/peer-identity.js +80 -6
  141. package/neural-interface/lib/pty-host/core.js +28 -1
  142. package/neural-interface/lib/pty-host/lifetime.js +12 -0
  143. package/neural-interface/lib/pty-host/manager.js +12 -1
  144. package/neural-interface/lib/remote-policy.js +1 -1
  145. package/neural-interface/lib/request-gate.js +3 -0
  146. package/neural-interface/lib/rulesets/api.js +6 -5
  147. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  148. package/neural-interface/lib/rulesets/installer.js +89 -10
  149. package/neural-interface/lib/rulesets/render.js +1 -1
  150. package/neural-interface/lib/session-cache-builder.js +64 -1
  151. package/neural-interface/lib/skills/catalog.js +5 -1
  152. package/neural-interface/lib/skills/creation-brief.js +1 -1
  153. package/neural-interface/lib/skills/creation-sources.js +19 -1
  154. package/neural-interface/lib/skills/deploy.js +9 -4
  155. package/neural-interface/lib/skills/draft-confinement.js +3 -1
  156. package/neural-interface/lib/skills/eval-confinement.js +3 -1
  157. package/neural-interface/lib/skills/eval-trace.js +16 -0
  158. package/neural-interface/lib/skills/export.js +5 -2
  159. package/neural-interface/lib/skills/known-bridge-digests.json +58 -4
  160. package/neural-interface/lib/skills/ownership.js +1 -1
  161. package/neural-interface/lib/skills/roots.js +10 -3
  162. package/neural-interface/lib/skills/runtime-checks.js +6 -3
  163. package/neural-interface/lib/skills/service.js +15 -7
  164. package/neural-interface/lib/skills/telemetry.js +6 -0
  165. package/neural-interface/lib/synabun-version.js +1 -1
  166. package/neural-interface/lib/system-activity.js +1 -1
  167. package/neural-interface/lib/whatsapp/brain.js +7 -3
  168. package/neural-interface/lib/whatsapp/cards.js +22 -9
  169. package/neural-interface/lib/whatsapp/config.js +1 -1
  170. package/neural-interface/lib/worker-confinement.js +77 -1
  171. package/neural-interface/lib/worker-question.js +74 -0
  172. package/neural-interface/lib/worker-resume.js +13 -4
  173. package/neural-interface/lib/worker-sandbox.js +46 -16
  174. package/neural-interface/package-lock.json +18 -2
  175. package/neural-interface/package.json +2 -1
  176. package/neural-interface/public/onboarding.html +1 -1
  177. package/neural-interface/public/shared/agent-runtime-options.js +89 -11
  178. package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
  179. package/neural-interface/public/shared/agy/agy-client.js +266 -0
  180. package/neural-interface/public/shared/agy/agy-commands.js +120 -0
  181. package/neural-interface/public/shared/agy/agy-copy.js +326 -0
  182. package/neural-interface/public/shared/agy/agy-diff.js +161 -0
  183. package/neural-interface/public/shared/agy/agy-events.js +469 -0
  184. package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
  185. package/neural-interface/public/shared/agy/agy-options.js +232 -0
  186. package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
  187. package/neural-interface/public/shared/agy/agy-render.js +392 -0
  188. package/neural-interface/public/shared/agy/agy-setup.js +149 -0
  189. package/neural-interface/public/shared/agy/agy-styles.js +604 -0
  190. package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
  191. package/neural-interface/public/shared/api.js +4 -4
  192. package/neural-interface/public/shared/assistant/asst-agents-dock.js +3 -4
  193. package/neural-interface/public/shared/assistant/asst-api.js +15 -3
  194. package/neural-interface/public/shared/assistant/asst-brain-picker.js +56 -9
  195. package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
  196. package/neural-interface/public/shared/assistant/asst-computer.js +26 -2
  197. package/neural-interface/public/shared/assistant/asst-control.js +48 -0
  198. package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
  199. package/neural-interface/public/shared/assistant/asst-limit-dock.js +6 -1
  200. package/neural-interface/public/shared/assistant/asst-limit-wait.js +38 -5
  201. package/neural-interface/public/shared/assistant/asst-motion-gate.js +80 -0
  202. package/neural-interface/public/shared/assistant/asst-panel.js +46 -9
  203. package/neural-interface/public/shared/assistant/asst-render.js +112 -18
  204. package/neural-interface/public/shared/assistant/asst-route.js +237 -57
  205. package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
  206. package/neural-interface/public/shared/assistant/asst-scene-styles.js +55 -23
  207. package/neural-interface/public/shared/assistant/asst-scenes.js +279 -80
  208. package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +4 -4
  209. package/neural-interface/public/shared/assistant/asst-sidepanel.js +51 -7
  210. package/neural-interface/public/shared/assistant/asst-slash.js +8 -2
  211. package/neural-interface/public/shared/assistant/asst-state.js +76 -9
  212. package/neural-interface/public/shared/assistant/asst-style-cache.js +44 -0
  213. package/neural-interface/public/shared/assistant/asst-styles.js +58 -4
  214. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +34 -7
  215. package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
  216. package/neural-interface/public/shared/cdx/cdx-output.js +26 -3
  217. package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
  218. package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
  219. package/neural-interface/public/shared/cdx/cdx-protocol.js +11 -10
  220. package/neural-interface/public/shared/cdx/cdx-render.js +10 -9
  221. package/neural-interface/public/shared/cdx/cdx-requests.js +11 -1
  222. package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
  223. package/neural-interface/public/shared/cdx/cdx-tabs.js +75 -5
  224. package/neural-interface/public/shared/cli-install-controller.js +218 -0
  225. package/neural-interface/public/shared/cli-install-view.js +7 -4
  226. package/neural-interface/public/shared/cli-status.js +9 -8
  227. package/neural-interface/public/shared/constants.js +3 -2
  228. package/neural-interface/public/shared/context-menu-model.js +1 -0
  229. package/neural-interface/public/shared/cp/cp-permissions.js +12 -0
  230. package/neural-interface/public/shared/cp/cp-restore.js +162 -0
  231. package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
  232. package/neural-interface/public/shared/desktop-bridge.js +17 -0
  233. package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
  234. package/neural-interface/public/shared/desktop-titlebar.js +13 -4
  235. package/neural-interface/public/shared/desktop-update-view.js +1 -0
  236. package/neural-interface/public/shared/explorer/fx-api.js +4 -4
  237. package/neural-interface/public/shared/explorer/fx-links.js +3 -3
  238. package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
  239. package/neural-interface/public/shared/explorer/fx-main.js +48 -16
  240. package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
  241. package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
  242. package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
  243. package/neural-interface/public/shared/html-shell.js +21 -4
  244. package/neural-interface/public/shared/loading-teardown.js +34 -0
  245. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +6 -1
  246. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +28 -7
  247. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
  248. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +17 -1
  249. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +6 -0
  250. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +101 -68
  251. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +46 -3
  252. package/neural-interface/public/shared/provider-icons.js +4 -2
  253. package/neural-interface/public/shared/settings/settings-rules.js +2 -2
  254. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +1 -1
  255. package/neural-interface/public/shared/settings/settings-skill-folders.js +2 -2
  256. package/neural-interface/public/shared/showcase-privacy.js +6 -1
  257. package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
  258. package/neural-interface/public/shared/skills/ss-creation.js +6 -3
  259. package/neural-interface/public/shared/skills/ss-dialog.js +5 -2
  260. package/neural-interface/public/shared/skills/ss-lint.js +9 -1
  261. package/neural-interface/public/shared/skills/ss-profiles.js +44 -6
  262. package/neural-interface/public/shared/skills/ss-review-view.js +2 -2
  263. package/neural-interface/public/shared/skills/studio/sx-actions.js +1 -1
  264. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +1 -1
  265. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +2 -1
  266. package/neural-interface/public/shared/skills/studio/sx-dom.js +2 -1
  267. package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
  268. package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
  269. package/neural-interface/public/shared/skills/studio/sx-inspector.js +1 -1
  270. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +9 -6
  271. package/neural-interface/public/shared/skills/studio/sx-install.js +20 -3
  272. package/neural-interface/public/shared/skills/studio/sx-library-view.js +144 -35
  273. package/neural-interface/public/shared/skills/studio/sx-library.js +6 -2
  274. package/neural-interface/public/shared/skills/studio/sx-main.js +11 -2
  275. package/neural-interface/public/shared/skills/studio/sx-review-view.js +1 -1
  276. package/neural-interface/public/shared/skills/studio/sx-subject.js +7 -6
  277. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +6 -5
  278. package/neural-interface/public/shared/state.js +1 -1
  279. package/neural-interface/public/shared/storage-retry.js +47 -0
  280. package/neural-interface/public/shared/storage.js +25 -4
  281. package/neural-interface/public/shared/streaming-markdown.js +58 -0
  282. package/neural-interface/public/shared/styles.css +362 -35
  283. package/neural-interface/public/shared/synabun-mascot.js +27 -8
  284. package/neural-interface/public/shared/synabun-ticker.js +147 -12
  285. package/neural-interface/public/shared/text-cache.js +29 -0
  286. package/neural-interface/public/shared/tool-list-picker.js +2 -2
  287. package/neural-interface/public/shared/tool-list-view.js +2 -2
  288. package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
  289. package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
  290. package/neural-interface/public/shared/ui-assistant.js +2 -0
  291. package/neural-interface/public/shared/ui-automation-studio.js +20 -12
  292. package/neural-interface/public/shared/ui-claude-panel.js +374 -134
  293. package/neural-interface/public/shared/ui-cli-banner.js +23 -167
  294. package/neural-interface/public/shared/ui-file-explorer.js +48 -6
  295. package/neural-interface/public/shared/ui-file-links.js +2 -2
  296. package/neural-interface/public/shared/ui-games.js +2 -2
  297. package/neural-interface/public/shared/ui-judgments.js +1 -1
  298. package/neural-interface/public/shared/ui-keybinds.js +8 -2
  299. package/neural-interface/public/shared/ui-link.js +3 -3
  300. package/neural-interface/public/shared/ui-loading.js +5 -4
  301. package/neural-interface/public/shared/ui-menubar.js +2 -2
  302. package/neural-interface/public/shared/ui-native-loop-router.js +4 -0
  303. package/neural-interface/public/shared/ui-navbar.js +45 -0
  304. package/neural-interface/public/shared/ui-notifications.js +1 -0
  305. package/neural-interface/public/shared/ui-resume.js +9 -7
  306. package/neural-interface/public/shared/ui-schedules-studio.js +41 -10
  307. package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
  308. package/neural-interface/public/shared/ui-sessions.js +22 -12
  309. package/neural-interface/public/shared/ui-settings.js +90 -65
  310. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
  311. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -3
  312. package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
  313. package/neural-interface/public/shared/ui-sidepanel-windows.js +3 -3
  314. package/neural-interface/public/shared/ui-terminal.js +146 -10
  315. package/neural-interface/public/shared/ui-tool-updates.js +3 -3
  316. package/neural-interface/public/shared/ui-tooltip.js +71 -46
  317. package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
  318. package/neural-interface/public/shared/ui-whiteboard.js +1 -1
  319. package/neural-interface/public/shared/ui-worker-end.js +1 -1
  320. package/neural-interface/public/shared/whatsapp/wa-view.js +2 -2
  321. package/neural-interface/public/shared/worker-questions.js +34 -0
  322. package/neural-interface/public/sidepanel-session.html +1 -1
  323. package/neural-interface/server.js +731 -466
  324. package/neural-interface/templates/rulesets/README.md +1 -1
  325. package/neural-interface/templates/rulesets/legacy-hashes.json +5 -5
  326. package/neural-interface/templates/rulesets/manifest.json +1 -1
  327. package/npm-shrinkwrap.json +18 -2
  328. package/package.json +2 -1
  329. package/skills/skill-forge/SKILL.md +4 -4
  330. package/skills/synabun/SKILL.md +1 -1
  331. package/skills/synabun/modules/schedule.md +3 -3
  332. /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
@@ -3,7 +3,7 @@ import { storedToolList, toolListRecord, readStoredToolListRecord } from './tool
3
3
  import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
4
4
  import { dirname, resolve } from 'node:path';
5
5
 
6
- export const NATIVE_LOOP_PROFILES = new Set(['claude-code', 'codex', 'opencode']);
6
+ export const NATIVE_LOOP_PROFILES = new Set(['claude-code', 'codex', 'opencode', 'antigravity']);
7
7
 
8
8
  export function isNativeLoopProfile(profile) {
9
9
  return NATIVE_LOOP_PROFILES.has(String(profile || ''));
@@ -22,6 +22,7 @@ function cleanDescriptor(record) {
22
22
  provider: record.provider,
23
23
  providerSessionId: record.providerSessionId || null,
24
24
  providerThreadId: record.providerThreadId || null,
25
+ ...(record.antigravityHome ? { antigravityHome: record.antigravityHome } : {}),
25
26
  source: record.source,
26
27
  focus: !!record.focus,
27
28
  status: record.status,
@@ -53,6 +54,7 @@ function cleanDescriptor(record) {
53
54
  // existing consumers (router, sidepanels, schedules) are unaffected.
54
55
  runMode: record.runMode || 'loop',
55
56
  turnState: record.turnState || null,
57
+ pending: record.status === 'running' ? (record.pendingRequests || []) : [],
56
58
  // What the last turn's result said (task mode: done, blocked or needs_input; the dispatcher's
57
59
  // lastResult.status): an idle warm worker whose turn ended asking waits on the user.
58
60
  outcome: record.outcome || null,
@@ -184,6 +186,14 @@ export class NativeLoopRuntime {
184
186
  return this._emit('sidepanel:run-updated', record, { reason: 'turn-state' });
185
187
  }
186
188
 
189
+ /** A live worker's requests, durable for a late attach or page reload. Resolvers stay in the dispatcher. */
190
+ setPendingRequests(runId, requests) {
191
+ const record = this.records.get(runId);
192
+ if (!record) return null;
193
+ record.pendingRequests = structuredClone(requests || []);
194
+ return this._emit('sidepanel:run-updated', record, { reason: 'pending-requests' });
195
+ }
196
+
187
197
  /**
188
198
  * Task-mode helper: records what the wrapper's last turn reported ('done' |
189
199
  * 'blocked' | 'needs_input', the dispatcher's lastResult.status) and
@@ -224,6 +234,7 @@ export class NativeLoopRuntime {
224
234
  record.suspendedAt = record.suspendedAt || iso(this.now());
225
235
  record.suspendReason = record.suspendReason || 'server_restart';
226
236
  record.turnState = 'suspended';
237
+ record.pendingRequests = [];
227
238
  record.stopping = false;
228
239
  this.records.set(record.runId, record);
229
240
  this._hold(record);
@@ -314,7 +325,8 @@ export class NativeLoopRuntime {
314
325
  }
315
326
 
316
327
  _writeState(runId, state) {
317
- writeJsonAtomic(this._statePath(runId), toolListRecord(state, this.toolListOptions()));
328
+ const { permissionBroker: _broker, ...stored } = state;
329
+ writeJsonAtomic(this._statePath(runId), toolListRecord(stored, this.toolListOptions()));
318
330
  }
319
331
 
320
332
  _writeTerminalState(record, patch) {
@@ -344,6 +356,7 @@ export class NativeLoopRuntime {
344
356
  _setIdentity(record, identity = {}) {
345
357
  const providerSessionId = identity.providerSessionId || identity.sessionId || null;
346
358
  const providerThreadId = identity.providerThreadId || identity.threadId || null;
359
+ const antigravityHome = typeof identity.antigravityHome === "string" && identity.antigravityHome ? identity.antigravityHome : null;
347
360
  let changed = false;
348
361
  if (providerSessionId && providerSessionId !== record.providerSessionId) {
349
362
  record.providerSessionId = providerSessionId;
@@ -353,6 +366,10 @@ export class NativeLoopRuntime {
353
366
  record.providerThreadId = providerThreadId;
354
367
  changed = true;
355
368
  }
369
+ if (antigravityHome && antigravityHome !== record.antigravityHome) {
370
+ record.antigravityHome = antigravityHome;
371
+ changed = true;
372
+ }
356
373
  if (changed) this._emit('sidepanel:run-updated', record, { reason: 'identity' });
357
374
  if ((record.providerSessionId || record.providerThreadId) && record._resolveIdentity) {
358
375
  record._resolveIdentity(cleanDescriptor(record));
@@ -397,6 +414,7 @@ export class NativeLoopRuntime {
397
414
  provider: state.profile,
398
415
  providerSessionId: null,
399
416
  providerThreadId: null,
417
+ antigravityHome: typeof state.antigravityHome === "string" && state.antigravityHome ? state.antigravityHome : null,
400
418
  source,
401
419
  focus,
402
420
  status: 'starting',
@@ -544,6 +562,7 @@ export class NativeLoopRuntime {
544
562
  runId,
545
563
  emit: (event) => this._emitProviderEvent(record, event),
546
564
  setTurnState: (turnState) => this.setTurnState(runId, turnState),
565
+ setPendingRequests: (requests) => this.setPendingRequests(runId, requests),
547
566
  setOutcome: (outcome) => this.setOutcome(runId, outcome),
548
567
  readState: () => this._readState(runId),
549
568
  })
@@ -639,6 +658,7 @@ export class NativeLoopRuntime {
639
658
  browserTabId: state.browserTabId || null,
640
659
  suspendedAt: null,
641
660
  suspendReason: null,
661
+ ...(typeof state.antigravityHome === "string" && state.antigravityHome ? { antigravityHome: state.antigravityHome } : {}),
642
662
  resumes: (Number(record.resumes) || 0) + 1,
643
663
  resumedAt: iso(this.now()),
644
664
  buildPrompt,
@@ -830,6 +850,7 @@ export class NativeLoopRuntime {
830
850
  record.startupAbortController = null;
831
851
  record.buildPrompt = null;
832
852
  record.wrapAdapter = null;
853
+ record.pendingRequests = [];
833
854
  if (turnState === 'suspended') record.turnState = 'suspended';
834
855
  else if (record.turnState && record.turnState !== 'terminal') record.turnState = 'terminal';
835
856
  }
@@ -2,43 +2,186 @@
2
2
  // listen time. This is a capability allowlist, not a generic OpenCode version
3
3
  // check: 2.1.4's HTTP/upgrade authentication and stdout password path were
4
4
  // inspected and tested. Unknown binaries/command wrappers keep the old guard.
5
+ // 2.2.5 (Homebrew anomalyco/tap/opencode-v2) checked 2026-11-01 against throwaway
6
+ // serves on 127.0.0.1: no password variable → it prints "server password <secret>"
7
+ // on stdout; every /api route (session, config, event, pty, shell, file, mcp, and
8
+ // the new tunnel and pairing) answers 401 without it, both plain and as a
9
+ // WebSocket upgrade, and 200 with it; one listener, bound to 127.0.0.1 only.
10
+ // Non-/api paths serve the static web UI (HTML), as before.
11
+ export const NATIVE_SERVE_VERSIONS = Object.freeze(['2.1.4', '2.2.5']);
12
+
5
13
  export function nativeServeCapability({ version, commandLine = false, passwordFrom } = {}) {
6
- return !commandLine && passwordFrom === 'stdout' && /^(?:opencode\s+)?v?2\.1\.4$/.test(String(version || '').trim());
14
+ const bare = String(version || '').trim().replace(/^opencode\s+/, '').replace(/^v/, '');
15
+ return !commandLine && passwordFrom === 'stdout' && NATIVE_SERVE_VERSIONS.includes(bare);
16
+ }
17
+
18
+ // Why a serve is not native-safe, in words that name what to change.
19
+ export function nativeServeReason({ version, commandLine = false, passwordFrom } = {}) {
20
+ if (commandLine) return 'command-line';
21
+ if (passwordFrom !== 'stdout') return 'password';
22
+ return nativeServeCapability({ version, commandLine, passwordFrom }) ? null : 'version';
23
+ }
24
+
25
+ const BASE = `Native browser WebSockets require authenticated OpenCode ${NATIVE_SERVE_VERSIONS.join(' or ')} serves with private stdout credentials; wait for startup, stop unverified serves or use the verified executable path.`;
26
+
27
+ const MAX_COMMAND = 200;
28
+ const short = (text, max = MAX_COMMAND) => {
29
+ const one = String(text ?? '').replace(/\s+/g, ' ').trim();
30
+ return one.length > max ? `${one.slice(0, max - 1)}…` : one;
31
+ };
32
+ const pidOf = (o) => (Number.isInteger(o?.pid) && o.pid > 0 ? o.pid : null);
33
+
34
+ // The fields of an offender a caller may see (a route's answer, a dispatch error): what names the serve, never its
35
+ // token or the rest of its record. A command line is shortened.
36
+ export function narrowOffenders(offenders = []) {
37
+ return (Array.isArray(offenders) ? offenders : []).map((o) => {
38
+ const out = { reason: String(o?.reason || 'unknown'), pid: pidOf(o), port: Number(o?.port) > 0 ? Number(o.port) : null };
39
+ if (o?.path) out.path = short(o.path, 400);
40
+ if (o?.command) out.command = short(o.command, 400);
41
+ if (o?.version) out.version = String(o.version).trim();
42
+ if (o?.reason === 'record') { out.kind = o.kind || null; out.claimable = !!o.claimable; }
43
+ return out;
44
+ });
7
45
  }
8
46
 
47
+ function describeOffender(o) {
48
+ const pid = pidOf(o);
49
+ const where = [pid ? `pid ${pid}` : null, o.port ? `port ${o.port}` : null].filter(Boolean).join(', ');
50
+ const what = [o.path || null, o.command ? `command ${short(o.command)}` : null, o.version ? `version ${String(o.version).trim()}` : null].filter(Boolean).join(', ');
51
+ const head = `OpenCode serve ${where || '(unknown process)'}${what ? ` (${what})` : ''}`;
52
+ switch (o.reason) {
53
+ case 'version': return `${head}: this OpenCode version is not one whose authentication SynaBun verified (${NATIVE_SERVE_VERSIONS.join(', ')}); install one of them, or stop this serve (close the OpenCode panel${pid ? ` / end pid ${pid}` : ''})`;
54
+ case 'command-line': return `${head}: started from the OpenCode command line set in Settings (its version cannot be read); clear it so SynaBun starts the verified executable, or stop this serve`;
55
+ case 'password': return `${head}: started without a private stdout password`;
56
+ case 'starting': return `${head}: still starting; try again once OpenCode answers`;
57
+ case 'untracked-port': return `${head}: answers on a port SynaBun did not start in this run; stop the process listening on port ${o.port}`;
58
+ case 'record': {
59
+ const kind = o.kind ? `, kind ${o.kind}` : '';
60
+ if (o.claimable) return `${head}: left running by an earlier SynaBun that has ended${kind}; it could not be stopped automatically: end ${pid ? `pid ${pid}` : 'that serve'}`;
61
+ // Not confirmed: the owner may still run, or the OS could not say whether this pid is still that serve (a pid
62
+ // is reused once its process ends). Never ask the user to end a number that may be another program now.
63
+ return `${head}: on record from another SynaBun process, or one whose identity the OS could not confirm${kind}. It is unconfirmed${pid ? ` whether pid ${pid} is still this serve (the number may belong to another program by now)` : ''}: check ${pid ? `that pid ${pid}'s command line is the one shown (ps -o command= -p ${pid}; on Windows, Task Manager → Details → Command line column)` : 'the OpenCode serves that are running'} before ending it, or close the other SynaBun`;
64
+ }
65
+ default: return `${head}: not verified`;
66
+ }
67
+ }
68
+
69
+ export function nativeServeError(offenders = []) {
70
+ const list = offenders.map(describeOffender);
71
+ const error = new Error(list.length ? `${BASE} Blocking: ${list.join('; ')}.` : BASE);
72
+ error.code = 'NATIVE_SERVE_UNVERIFIED';
73
+ error.offenders = narrowOffenders(offenders);
74
+ return error;
75
+ }
76
+
77
+ const nativeContextClosed = () => Object.assign(new Error('The browser closed while SynaBun was checking its OpenCode serves; nothing was attached.'), { code: 'NATIVE_CONTEXT_CLOSED' });
78
+ const nativeAttachAborted = () => Object.assign(new Error('The browser request ended while SynaBun was checking its OpenCode serves; nothing was attached.'), { code: 'NATIVE_ATTACH_ABORTED' });
79
+ const defaultSleep = (ms) => new Promise((done) => { const t = setTimeout(done, ms); t.unref?.(); });
80
+
9
81
  export function createNativeServeBoundary() {
10
82
  const processes = new Map();
83
+ // Every native context that relies on the boundary, attached or still being checked (a reservation): while any is
84
+ // held, assertLaunch refuses an unverified serve. A context is held from before its check until it closes, its
85
+ // browser disconnects or its attachment fails.
11
86
  const contexts = new Map();
12
- const error = () => new Error('Native browser WebSockets require authenticated OpenCode 2.1.4 serves with private stdout credentials; wait for startup, stop unverified serves or use the verified executable path.');
87
+ function offenders({ ports = [], unverifiedRecords = [] } = {}) {
88
+ const out = [];
89
+ const verified = new Set();
90
+ for (const [proc, e] of processes) {
91
+ const base = { pid: proc.pid || null, port: e.port, path: e.path || null, version: e.version || null };
92
+ if (!e.safe) out.push({ ...base, reason: e.reason || 'version' });
93
+ else if (!e.ready) out.push({ ...base, reason: 'starting' });
94
+ else verified.add(e.port);
95
+ }
96
+ for (const port of ports) {
97
+ const n = Number(port);
98
+ if (verified.has(n) || out.some(o => o.port === n)) continue;
99
+ out.push({ pid: null, port: n, reason: 'untracked-port' });
100
+ }
101
+ // A record has the serve's command line (its identity), not a path; never its token.
102
+ for (const r of unverifiedRecords) out.push({ pid: r?.pid || null, port: r?.port || null, kind: r?.kind || null, command: r?.command || null, claimable: !!r?.claimable, reason: 'record' });
103
+ return out;
104
+ }
105
+ function reserve(context) {
106
+ const held = contexts.get(context);
107
+ if (held) return held;
108
+ const browser = context.browser?.() || null;
109
+ const release = () => {
110
+ if (contexts.get(context) !== release) return;
111
+ contexts.delete(context);
112
+ context.removeListener('close', release);
113
+ browser?.removeListener('disconnected', release);
114
+ };
115
+ contexts.set(context, release);
116
+ context.once('close', release);
117
+ browser?.once('disconnected', release);
118
+ return release;
119
+ }
120
+ function check(state) {
121
+ const blocking = offenders(state);
122
+ if (blocking.length) throw nativeServeError(blocking);
123
+ }
13
124
  return {
14
- assertLaunch(safe) { if (!safe && contexts.size) throw error(); },
15
- track(proc, { port, safe = false }) {
125
+ offenders,
126
+ reserve,
127
+ holds: (context) => contexts.has(context),
128
+ assertLaunch(safe, info = {}) {
129
+ if (!safe && contexts.size) {
130
+ const e = nativeServeError([{ pid: null, port: info.port || null, path: info.path || null, version: info.version || null, reason: info.reason || 'version' }]);
131
+ e.message = `A native browser is attached, so this unverified OpenCode serve is not started. ${e.message}`;
132
+ throw e;
133
+ }
134
+ },
135
+ track(proc, { port, safe = false, version = null, path = null, reason = null }) {
16
136
  if (!proc || proc.exitCode != null || proc.signalCode != null) return;
17
- processes.set(proc, { port: Number(port), safe, ready: false });
137
+ processes.set(proc, { port: Number(port), safe, ready: false, version, path, reason: safe ? null : (reason || 'version') });
18
138
  const release = () => processes.delete(proc);
19
139
  proc.once('exit', release);
20
140
  proc.once('error', () => { if (!proc.pid) release(); });
21
141
  },
22
142
  ready(proc) { const entry = processes.get(proc); if (entry) entry.ready = true; },
23
- protect(context, { ports, unverifiedRecords = [] }) {
143
+ // Synchronous: check now, then hold the context. A context already held is returned as it is.
144
+ protect(context, state) {
24
145
  if (contexts.has(context)) return contexts.get(context);
25
- const verified = new Set();
26
- for (const { port, safe, ready } of processes.values()) {
27
- if (!safe || !ready) throw error();
28
- verified.add(port);
146
+ check(state);
147
+ return reserve(context);
148
+ },
149
+ // The context is held (a pending reservation assertLaunch honours) before anything is waited for; then, bounded,
150
+ // SynaBun repairs what it can: a sweep of the records an ended SynaBun left (started at once, in the background,
151
+ // waited for at most `sweepMs`) and SynaBun's own verified serves still starting (waited for at most `waitMs`).
152
+ // Any other blocker refuses at once. `deadline` (an absolute time) and `signal` end the wait early. On every
153
+ // failure the reservation is released; nothing is cached, so the next attempt reads the live state again.
154
+ async attach(context, { state, sweep = null, waitMs = 20_000, sweepMs = 10_000, deadline = Infinity, signal = null, pollMs = 250, now = Date.now, sleep = defaultSleep } = {}) {
155
+ const release = reserve(context);
156
+ const held = () => contexts.get(context) === release;
157
+ try {
158
+ const start = now();
159
+ const end = Number.isFinite(deadline) ? deadline : Infinity;
160
+ const waitEnd = Math.min(start + Math.max(0, waitMs), end);
161
+ const sweepEnd = Math.min(start + Math.max(0, sweepMs), end);
162
+ let current = state();
163
+ let sweeping = false;
164
+ if (sweep && current.unverifiedRecords?.length) {
165
+ sweeping = true;
166
+ Promise.resolve().then(sweep).catch(() => {}).finally(() => { sweeping = false; });
167
+ }
168
+ for (;;) {
169
+ if (!held()) throw nativeContextClosed();
170
+ if (signal?.aborted) throw nativeAttachAborted();
171
+ const blocking = offenders(current);
172
+ if (!blocking.length) return release;
173
+ const t = now();
174
+ const waitable = (o) => (o.reason === 'starting' && t < waitEnd)
175
+ || (o.reason === 'record' && o.claimable && sweeping && t < sweepEnd);
176
+ if (!blocking.every(waitable)) throw nativeServeError(blocking);
177
+ const until = Math.max(...blocking.map(o => (o.reason === 'starting' ? waitEnd : sweepEnd)));
178
+ await sleep(Math.max(1, Math.min(pollMs, until - t)));
179
+ current = state();
180
+ }
181
+ } catch (error) {
182
+ release();
183
+ throw error;
29
184
  }
30
- if ([...ports].some(port => !verified.has(Number(port))) || unverifiedRecords.length) throw error();
31
- const browser = context.browser();
32
- const release = () => {
33
- if (contexts.get(context) !== release) return;
34
- contexts.delete(context);
35
- context.removeListener('close', release);
36
- browser.removeListener('disconnected', release);
37
- };
38
- contexts.set(context, release);
39
- context.once('close', release);
40
- browser.once('disconnected', release);
41
- return release;
42
185
  },
43
186
  };
44
187
  }
@@ -74,7 +74,7 @@ export const AGENT_CALL_KINDS = Object.freeze(['agent', 'synabun-mcp']);
74
74
  */
75
75
  export const OWNER_READ_PREFIXES = Object.freeze([...ADMIN_ONLY_PREFIXES.filter((prefix) => prefix !== '/api/hook-'), '/api/git']);
76
76
  /** The owner's sockets: a terminal, the agent panels, the Assistant, the browser panel. */
77
- export const OWNER_SOCKET_PREFIXES = Object.freeze(['/ws/terminal/', '/ws/claude-skin', '/ws/codex-skin', '/ws/opencode-v2', '/ws/assistant/', '/ws/browser/']);
77
+ export const OWNER_SOCKET_PREFIXES = Object.freeze(['/ws/terminal/', '/ws/claude-skin', '/ws/codex-skin', '/ws/opencode-v2', '/ws/antigravity', '/ws/assistant/', '/ws/browser/']);
78
78
  /** Sockets that stay open for the app's pages to draw, read-only for a peer the owner's powers are refused to. */
79
79
  export const READ_ONLY_SOCKET_PATHS = Object.freeze(['/ws/sync']);
80
80
 
@@ -19,13 +19,15 @@ export class PathConfineError extends Error {
19
19
  export const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
20
20
  export const isSessionId = (id) => typeof id === 'string' && SESSION_ID_RE.test(id);
21
21
 
22
- const fold = process.platform === 'win32' ? (s) => s.toLowerCase() : (s) => s;
22
+ const foldForPlatform = (platform = process.platform) => (platform === 'win32' ? (s) => s.toLowerCase() : (s) => s);
23
+ const fold = foldForPlatform();
23
24
 
24
25
  /** True when `target` is below `root` (or is `root` itself, with allowRoot). Lexical: both are resolved first. */
25
- export function isInside(root, target, { allowRoot = false } = {}) {
26
+ export function isInside(root, target, { allowRoot = false, platform = process.platform } = {}) {
26
27
  if (!root || !target) return false;
27
- const r = fold(resolve(String(root)));
28
- const t = fold(resolve(String(target)));
28
+ const f = platform === process.platform ? fold : foldForPlatform(platform);
29
+ const r = f(resolve(String(root)));
30
+ const t = f(resolve(String(target)));
29
31
  if (t === r) return allowRoot;
30
32
  return t.startsWith(r.endsWith(sep) ? r : r + sep);
31
33
  }
@@ -83,28 +85,29 @@ export function isInsideReal(root, target, { allowRoot = false, fs = realFs } =
83
85
  * @param o.projectsDir <config dir>/projects of the account
84
86
  * @param o.pathToKey (projectPath) => the folder name Claude Code uses for it
85
87
  */
86
- export function findSessionTranscript({ sessionId, project = '', projects = [], projectsDir, pathToKey, exists = existsSync, realpath = realpathSync } = {}) {
88
+ export function findSessionTranscript({ sessionId, project = '', projects = [], projectsDir, pathToKey, exists = existsSync, realpath = realpathSync, platform = process.platform } = {}) {
87
89
  if (!isSessionId(sessionId)) throw new PathConfineError(400, 'Not a session id');
88
90
  if (!projectsDir || typeof pathToKey !== 'function') return null;
91
+ const foldPath = platform === process.platform ? fold : foldForPlatform(platform);
89
92
  const known = (Array.isArray(projects) ? projects : []).filter(p => p?.path);
90
- const wanted = typeof project === 'string' && project ? resolve(project) : '';
93
+ const wanted = typeof project === 'string' && project ? foldPath(resolve(project)) : '';
91
94
  let order = known;
92
95
  if (wanted) {
93
- const named = known.find(p => resolve(p.path) === wanted);
96
+ const named = known.find(p => foldPath(resolve(p.path)) === wanted);
94
97
  if (!named) throw new PathConfineError(400, 'Not a registered project');
95
98
  order = [named];
96
99
  }
97
100
  for (const proj of order) {
98
101
  const key = String(pathToKey(proj.path) || '');
99
102
  // Claude Code has written the drive letter in both cases on Windows.
100
- const keys = [...new Set([key.replace(/^([A-Z])/, m => m.toLowerCase()), key])].filter(isPlainName);
103
+ const keys = [...new Set([key.replace(/^([A-Z])/, m => m.toLowerCase()), key.replace(/^([a-z])/, m => m.toUpperCase()), key])].filter(isPlainName);
101
104
  for (const k of keys) {
102
105
  const folder = join(projectsDir, k);
103
106
  const candidate = join(folder, `${sessionId}.jsonl`);
104
107
  if (!exists(candidate)) continue;
105
108
  let realFile; let realFolder;
106
109
  try { realFile = realpath(candidate); realFolder = realpath(folder); } catch { continue; }
107
- if (!isInside(realFolder, realFile)) continue;
110
+ if (!isInside(realFolder, realFile, { platform })) continue;
108
111
  // `realPath` / `realFolder`: the canonical paths that were just checked.
109
112
  // The transcript is read from those (lib/confined-fs.js), not from the
110
113
  // name it was found under, which could be swapped for a link afterwards.
@@ -244,7 +244,11 @@ export const RELAY_NAMES = Object.freeze([
244
244
  /** Proxy / tunnel apps found by their bundle (their helpers carry other names). */
245
245
  const RELAY_BUNDLE_RE = /\/(?:charles|proxyman|surge(?: mac)?|clashx[^/]*|clash verge|clash for windows|clash nyanpasu|flclash|shadowrocket|shadowsocksx[^/]*|v2rayu|v2rayx|qv2ray|v2rayn|stash|quantumult x|loon|proxifier|mitmproxy|ngrok|tailscale|cloudflare warp)\.app\//i;
246
246
  /** The agents whose process trees never hold the owner's authority (argv[0]'s name). */
247
- export const AGENT_NAMES = Object.freeze(['claude', 'codex', 'opencode', 'gemini', 'cursor-agent', 'aider', 'goose', 'qwen', 'crush']);
247
+ // Antigravity's CLI runs as `antigravity` (the kernel's name for the binary Homebrew links as `agy`). Gemini CLI stays on
248
+ // the list though SynaBun no longer drives it: an agent the user still runs never holds the owner's authority either.
249
+ export const AGENT_NAMES = Object.freeze(['claude', 'codex', 'opencode', 'agy', 'antigravity', 'gemini', 'cursor-agent', 'aider', 'goose', 'qwen', 'crush']);
250
+ /** The agent backend of the Antigravity app and IDE (a `language_server` inside their bundles: Codeium's name, so only by its place). */
251
+ const ANTIGRAVITY_SERVER_RE = /\/antigravity(?: ide)?\.app\/contents\/resources\/(?:app\/extensions\/[^/]+\/)?bin\/language_server/i;
248
252
  /** Browsers the owner browses with (their bundle or their network process): never taken for a relay because they
249
253
  * happen to hold a listening socket; an automation flag on them is still refused. */
250
254
  const BROWSER_BUNDLE_RE = /\/(?:safari|google chrome(?: beta| dev| canary)?|chromium|firefox(?: developer edition| nightly)?|arc|brave browser(?: beta| nightly)?|microsoft edge(?: beta| dev| canary)?|opera(?: gx)?|vivaldi|orion|zen(?: browser)?|thorium|comet|dia|sigmaos|yandex|duckduckgo|sidekick|wavebox|island)\.app\/|\/synabun\.app\/contents\/(?:macos\/synabun(?:\s|$)|frameworks\/synabun helper)/i;
@@ -275,10 +279,12 @@ export function processKinds({ ucomm = '', comm = '', args = '' } = {}) {
275
279
  else if (/(?:^|\s)-juggler(?:-pipe)?(?:\s|$)/.test(la) || /(?:^|\s)--?marionette(?:\s|$)/.test(la)) automation = 'Firefox automation';
276
280
 
277
281
  let agent = has(AGENT_NAMES);
282
+ if (agent === 'agy') agent = 'antigravity';
278
283
  if (!agent) {
279
284
  if (/\/claude\/versions\/[^/\s]+(?:\s|$)/.test(la) || /@anthropic-ai\/claude-code\/cli\.m?js(?:\s|$)/.test(la) || /@anthropic-ai\/claude-agent-sdk[^/\s]*\/claude(?:\s|$)/.test(la)) agent = 'claude';
280
285
  else if (/@openai\/codex\/bin\/codex(?:\.js)?(?:\s|$)/.test(la) || /(?:^|\s|\/)codex\s+(?:app-server|exec|mcp(?:-server)?|proto)\b/.test(la)) agent = 'codex';
281
286
  else if (/(?:^|\s|\/)opencode\s+serve\b/.test(la)) agent = 'opencode';
287
+ else if (/(?:^|\s|\/)(?:agy|antigravity)\s+acp\b/.test(la) || ANTIGRAVITY_SERVER_RE.test(c || a)) agent = 'antigravity';
282
288
  else if (/@google\/gemini-cli\//.test(la)) agent = 'gemini';
283
289
  }
284
290
 
@@ -402,6 +408,27 @@ export function isSynabunMcpServer(proc, { roots = [], cwd = null, realpath = nu
402
408
  return typeof real === 'string' && targets.has(real);
403
409
  }
404
410
 
411
+ // ── Processes SynaBun starts for an agent ────────────────────────────────────
412
+ //
413
+ // A command an agent runs through SynaBun (the Antigravity panel's client terminals, lib/antigravity/acp-client-host.js)
414
+ // is a child of this server, not of the agent, so no agent is in its process tree by name. Its pid is registered here
415
+ // while it runs, and a chain that passes through one is an agent's tree. Like the name rule, a command that detaches
416
+ // from its parent (and so from the chain) is not seen.
417
+ const AGENT_PROCESSES = new Map(); // pid -> label
418
+
419
+ /** A process SynaBun started on an agent's behalf: its tree never holds the owner's authority while it runs. */
420
+ export function registerAgentProcess(pid, label = 'agent command') {
421
+ if (Number.isInteger(pid) && pid > 1) AGENT_PROCESSES.set(pid, String(label || 'agent command').slice(0, 80));
422
+ }
423
+ /** That process ended (its pid can be reused). */
424
+ export function forgetAgentProcess(pid) {
425
+ AGENT_PROCESSES.delete(pid);
426
+ }
427
+ /** The registered processes (for tests and diagnostics). */
428
+ export function agentProcesses() {
429
+ return new Map(AGENT_PROCESSES);
430
+ }
431
+
405
432
  /**
406
433
  * Whether the process at the client end of a connection, and its parents, may hold the owner's authority.
407
434
  * @param {Array<{pid:number, ppid:number, ucomm:string, comm:string, args:string}>} chain the peer first, then its
@@ -409,9 +436,10 @@ export function isSynabunMcpServer(proc, { roots = [], cwd = null, realpath = nu
409
436
  * @param {{ selfPid:number, listening:Set<number>, synabunMcp?:number }} o `synabunMcp`: the peer's pid when it is
410
437
  * SynaBun's own MCP server over HTTP (isSynabunMcpServer): not refused as a relay, refused like an agent's tree
411
438
  * (kind 'synabun-mcp': lib/owner-authority.js takes its pinned calls as an agent's own)
439
+ * `agentPids`: processes SynaBun started for an agent (registerAgentProcess), refused like an agent's tree
412
440
  * @returns {{ self:boolean, refuse: null | { why:string, kind:string, pid:number, name:string } }}
413
441
  */
414
- export function classifyPeerChain(chain, { selfPid = process.pid, listening = new Set(), synabunMcp = 0 } = {}) {
442
+ export function classifyPeerChain(chain, { selfPid = process.pid, listening = new Set(), synabunMcp = 0, agentPids = AGENT_PROCESSES, debugClient = null } = {}) {
415
443
  const list = Array.isArray(chain) ? chain : [];
416
444
  if (!list.length) return { self: false, refuse: null };
417
445
  if (list[0].pid === selfPid) return { self: true, refuse: null };
@@ -419,12 +447,20 @@ export function classifyPeerChain(chain, { selfPid = process.pid, listening = ne
419
447
  const refuse = (p, kind, why) => ({ self: false, refuse: { kind, why, pid: p.pid, name: nameOf(p) } });
420
448
  const peerKinds = processKinds(list[0]);
421
449
  const mcp = Number(synabunMcp) > 0 && list[0].pid === Number(synabunMcp);
450
+ let exempted = false;
422
451
  for (let i = 0; i < list.length; i++) {
423
452
  const p = list[i];
424
453
  if (!p || p.pid === selfPid || p.pid <= 1) break; // this server's own ancestry, and launchd, are not the peer's
425
454
  const k = i === 0 ? peerKinds : processKinds(p);
426
- if (k.automation) return refuse(p, 'automation-browser', `a browser started for automation (${k.automation})`);
455
+ // Local profiling (SYNABUN_ALLOW_DEBUG_CLIENT, debugClientPolicy): only the remote-debugging flag is forgiven, and
456
+ // only on the port the policy names; every other rule below still applies to this process and its parents. The
457
+ // process holds its debug port open itself, so the listener rule is not asked of that one process.
458
+ const debugOk = !!k.automation && debugClientExempt(p, k, debugClient);
459
+ if (debugOk) exempted = true;
460
+ if (k.automation && !debugOk) return refuse(p, 'automation-browser', `a browser started for automation (${k.automation})`);
427
461
  if (k.agent) return refuse(p, 'agent', `an AI agent's process tree (${k.agent})`);
462
+ const startedFor = agentPids?.get?.(p.pid);
463
+ if (startedFor) return refuse(p, 'agent', `an AI agent's process tree (${startedFor}, started by SynaBun)`);
428
464
  if (k.synabun) return refuse(p, 'synabun-instance', 'another SynaBun server');
429
465
  if (k.remoteLogin) return refuse(p, 'remote-login', 'a remote login session (sshd)');
430
466
  // SynaBun's own MCP server over HTTP relays for its MCP clients by design: not refused as a relay or a listener
@@ -434,12 +470,47 @@ export function classifyPeerChain(chain, { selfPid = process.pid, listening = ne
434
470
  // A program that serves TCP itself and calls SynaBun is relaying for whoever reached it (a dev server's proxy,
435
471
  // a small SOCKS / HTTP proxy). The owner's browsers are left out of this one rule (their network process, or a
436
472
  // browser that holds a socket for its own reasons): their automation flags are what is asked of them.
437
- if (i <= 1 && !peerKinds.browser && listening.has(p.pid)) return refuse(p, 'listener', 'a program that also listens for connections (a relay or proxy)');
473
+ if (i <= 1 && !peerKinds.browser && !debugOk && listening.has(p.pid)) return refuse(p, 'listener', 'a program that also listens for connections (a relay or proxy)');
438
474
  }
475
+ if (exempted && !mcp) return { self: false, refuse: null, debugExempt: true };
439
476
  if (mcp) return refuse(list[0], 'synabun-mcp', 'SynaBun\'s own MCP server over HTTP (it calls for whichever MCP client reached it)');
440
477
  return { self: false, refuse: null };
441
478
  }
442
479
 
480
+ // ── Local profiling exemption (off unless the server's own environment asks) ─
481
+ //
482
+ // SYNABUN_ALLOW_DEBUG_CLIENT=1 (any debug port) or =<port> (only --remote-debugging-port=<port>) lets a client process
483
+ // started with --remote-debugging-port hold the owner's authority, so SynaBun's own window can be profiled over CDP.
484
+ // Read from the environment the server was started with only (server.js reads it before DATA_HOME/.env is loaded:
485
+ // never from a file a client can write), loopback peers only (127.0.0.0/8, ::1), and only that flag: a
486
+ // --remote-debugging-pipe, MoreLogin, Playwright WebKit / Firefox automation, agents, relays and every other rule
487
+ // still refuse.
488
+
489
+ /** The policy from an environment object: { enabled, port } (port null = any). Anything but 1/on/true/yes or a port is off. */
490
+ export function debugClientPolicy(env = process.env) {
491
+ const raw = String(env?.SYNABUN_ALLOW_DEBUG_CLIENT ?? '').trim().toLowerCase();
492
+ if (/^(?:1|on|true|yes)$/.test(raw)) return { enabled: true, port: null };
493
+ if (/^[0-9]{4,5}$/.test(raw) && Number(raw) >= 1024 && Number(raw) <= 65535) return { enabled: true, port: Number(raw) };
494
+ return { enabled: false, port: null };
495
+ }
496
+
497
+ /** Whether this process's automation flag is the one the policy forgives. */
498
+ export function debugClientExempt(proc, kinds, policy) {
499
+ if (!policy?.enabled || kinds?.automation !== 'remote debugging') return false;
500
+ const la = lower(proc?.args);
501
+ if (/(?:^|\s)--remote-debugging-pipe(?:[=\s]|$)/.test(la)) return false;
502
+ // The other automation rules are asked as if the flag were not there: MoreLogin, Playwright, Firefox automation.
503
+ if (MORELOGIN_PATH_RE.test(String(proc?.comm ?? '')) || lower(proc?.ucomm).trim().startsWith('morelogin')) return false;
504
+ if (/(?:^|\s)--inspector-pipe(?:\s|$)/.test(la) || /\/pw_run\.sh(?:\s|$)/.test(la) || /(?:^|\s)-juggler(?:-pipe)?(?:\s|$)/.test(la) || /(?:^|\s)--?marionette(?:\s|$)/.test(la)) return false;
505
+ if (/(?:^|\s)--(?:enable-automation|headless)(?:[=\s]|$)/.test(la)) return false;
506
+ const ports = [...la.matchAll(/(?:^|\s)--remote-debugging-port(?:=|\s+)([0-9]+)(?=\s|$)/g)].map((m) => Number(m[1]));
507
+ if (!ports.length) return false;
508
+ return policy.port == null || ports.every((n) => n === policy.port);
509
+ }
510
+
511
+ /** Whether a normalized address is loopback. */
512
+ export const isLoopbackAddress = (addr) => /^127\.\d+\.\d+\.\d+$/.test(String(addr || '')) || String(addr || '') === '::1';
513
+
443
514
  /** The environment part of `ps -E` output for one process, given the same process's line without -E. */
444
515
  export function envPart(withEnv, plain) {
445
516
  const w = String(withEnv ?? '');
@@ -496,8 +567,10 @@ function thisMachineAddresses() {
496
567
  * is told apart from other listening programs (isSynabunMcpServer). None: it is refused as a listener, as any relay.
497
568
  * @param {(path:string) => string} [o.realpath] fs.realpathSync (tests inject one).
498
569
  */
499
- export function createPeerIdentity({ platform = process.platform, selfPid = process.pid, run = defaultRun, localAddresses = null, log = () => {}, now = Date.now, disabled = false, synabunRoots = [], realpath = realpathSync } = {}) {
570
+ export function createPeerIdentity({ platform = process.platform, selfPid = process.pid, run = defaultRun, localAddresses = null, log = () => {}, now = Date.now, disabled = false, synabunRoots = [], realpath = realpathSync, debugClient = null } = {}) {
500
571
  const supported = platform === 'darwin' && !disabled;
572
+ const debugPolicy = debugClient?.enabled ? { enabled: true, port: Number.isInteger(debugClient.port) ? debugClient.port : null } : null;
573
+ if (debugPolicy) { try { log(`[peer-identity] WARNING: SYNABUN_ALLOW_DEBUG_CLIENT is set: a loopback client started with --remote-debugging-port${debugPolicy.port ? `=${debugPolicy.port}` : ''} is taken as the owner (local profiling only; unset it when done)`); } catch { /* logger */ } }
501
574
  if (disabled) { try { log('[peer-identity] off (SYNABUN_PEER_IDENTITY=off): the page secret\'s rules from before apply, with no peer lookup'); } catch { /* logger */ } }
502
575
  const bySocket = new WeakMap();
503
576
  const chains = new Map(); // pid → { at, ucomm, chain }
@@ -649,7 +722,8 @@ export function createPeerIdentity({ platform = process.platform, selfPid = proc
649
722
  if (!chain || !chain.length) return { status: 'gone', reason: 'the client process exited before it was identified', pid: row.pid, ms: now() - started };
650
723
  let synabunMcp = 0;
651
724
  try { synabunMcp = await synabunMcpPid(chain[0], table.listening); } catch { synabunMcp = 0; }
652
- const verdict = classifyPeerChain(chain, { selfPid, listening: table.listening, synabunMcp });
725
+ const verdict = classifyPeerChain(chain, { selfPid, listening: table.listening, synabunMcp, debugClient: isLoopbackAddress(remote) ? debugPolicy : null });
726
+ if (verdict.debugExempt) note(`debug:${row.pid}`, `[peer-identity] WARNING: accepted a debug client as the owner (pid ${row.pid}, ${row.name || chain[0].ucomm}) under SYNABUN_ALLOW_DEBUG_CLIENT`);
653
727
  return { status: 'ok', self: verdict.self, pid: row.pid, name: row.name || chain[0].ucomm, chain, refuse: verdict.refuse, ms: now() - started };
654
728
  }
655
729
 
@@ -19,6 +19,7 @@ import { evaluateFlow } from './flow.js';
19
19
  import { EV, OP, HOST_DEFAULTS } from './protocol.js';
20
20
  import { createEventLoopMonitor } from '../event-loop-monitor.js';
21
21
  import { withPathEnv } from '../augmented-path.js';
22
+ import { killOwnedPty } from './lifetime.js';
22
23
 
23
24
  const IS_WIN = process.platform === 'win32';
24
25
  const IS_LINUX = process.platform === 'linux';
@@ -39,8 +40,14 @@ export function createPtyHostCore({ send, config = {}, pty: injectedPty = null }
39
40
  let pty = injectedPty;
40
41
  let ptyError = null;
41
42
  let shuttingDown = false;
43
+ let shutdownTimer = null;
42
44
 
43
45
  const emit = (msg) => { try { send(msg); } catch {} };
46
+ // Covers process.exit(), including host.js's bounded shutdown, and the
47
+ // in-process transport. This must be synchronous: exit cannot await PTYs.
48
+ const onOwnerExit = () => {
49
+ for (const s of sessions.values()) if (!s.exited) killOwnedPty(s.proc.pid);
50
+ };
44
51
 
45
52
  // ── Startup ──
46
53
  async function init() {
@@ -65,6 +72,10 @@ export function createPtyHostCore({ send, config = {}, pty: injectedPty = null }
65
72
  // ── Spawn ──
66
73
  function spawnSession(spec) {
67
74
  const { id } = spec;
75
+ if (shuttingDown) {
76
+ emit({ t: EV.SPAWN_ERROR, id, message: 'Terminal host is shutting down' });
77
+ return;
78
+ }
68
79
  if (!id || sessions.has(id)) {
69
80
  emit({ t: EV.SPAWN_ERROR, id, message: 'Duplicate or missing session id' });
70
81
  return;
@@ -133,6 +144,7 @@ export function createPtyHostCore({ send, config = {}, pty: injectedPty = null }
133
144
  graceTimer: null,
134
145
  exited: false,
135
146
  };
147
+ if (!sessions.size) process.once('exit', onOwnerExit);
136
148
  sessions.set(id, s);
137
149
  counters.spawns++;
138
150
 
@@ -490,6 +502,11 @@ export function createPtyHostCore({ send, config = {}, pty: injectedPty = null }
490
502
  for (const f of s.tempFiles.splice(0)) unlink(f).catch(() => {});
491
503
  try { s.model.dispose(); } catch {}
492
504
  sessions.delete(s.id);
505
+ if (!sessions.size) {
506
+ process.removeListener('exit', onOwnerExit);
507
+ clearTimeout(shutdownTimer);
508
+ shutdownTimer = null;
509
+ }
493
510
  }
494
511
 
495
512
  function killSession(s, signal) {
@@ -571,7 +588,11 @@ export function createPtyHostCore({ send, config = {}, pty: injectedPty = null }
571
588
  return out;
572
589
  }
573
590
 
574
- function shutdownAll() {
591
+ function shutdownAll({ force = false } = {}) {
592
+ // The server's synchronous exit handler kills itself before later exit
593
+ // listeners can run. Its killNow() path must reap the PTYs right here.
594
+ if (force) onOwnerExit();
595
+ if (shuttingDown) return;
575
596
  shuttingDown = true;
576
597
  for (const s of sessions.values()) {
577
598
  s.draining = true;
@@ -581,6 +602,12 @@ export function createPtyHostCore({ send, config = {}, pty: injectedPty = null }
581
602
  for (const s of sessions.values()) {
582
603
  for (const c of s.clients) { try { c.ws.close(1001, 'terminal host shutting down'); } catch {} }
583
604
  }
605
+ // HUP can be ignored, or arrive while the macOS helper is attaching.
606
+ // Bound the grace period in both transports, even if the owner stays up.
607
+ if (sessions.size) {
608
+ shutdownTimer = setTimeout(onOwnerExit, 100);
609
+ shutdownTimer.unref?.();
610
+ }
584
611
  }
585
612
 
586
613
  function dispose() {
@@ -0,0 +1,12 @@
1
+ // node-pty's macOS helper starts a new session before exec. If its host dies
2
+ // before the helper acquires the controlling terminal, closing the master
3
+ // does not hang it up. Kill the known PTY process group as well as its leader.
4
+ // Callers must forget a PID on EXIT: these are owned children, never a sweep
5
+ // of unrelated processes or a list retained across host restarts.
6
+ export function killOwnedPty(pid, { kill = process.kill, platform = process.platform } = {}) {
7
+ if (!Number.isSafeInteger(pid) || pid <= 1) return;
8
+ if (platform !== 'win32') {
9
+ try { kill(-pid, 'SIGKILL'); } catch {}
10
+ }
11
+ try { kill(pid, 'SIGKILL'); } catch {}
12
+ }