synabun 2.1.0 → 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 (317) hide show
  1. package/CHANGELOG.md +121 -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 +501 -39
  71. package/neural-interface/i18n/pt-BR.json +501 -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-budget-planner.js +7 -3
  88. package/neural-interface/lib/assistant-budget-split.js +42 -15
  89. package/neural-interface/lib/assistant-catalog.js +76 -9
  90. package/neural-interface/lib/assistant-config.js +6 -3
  91. package/neural-interface/lib/assistant-dispatch.js +207 -49
  92. package/neural-interface/lib/assistant-envelope.js +153 -1
  93. package/neural-interface/lib/assistant-evidence.js +80 -0
  94. package/neural-interface/lib/assistant-hidden-models.js +7 -5
  95. package/neural-interface/lib/assistant-persona.js +16 -11
  96. package/neural-interface/lib/assistant-plan-permissions.js +15 -0
  97. package/neural-interface/lib/assistant-router.js +237 -35
  98. package/neural-interface/lib/assistant-runtime.js +158 -12
  99. package/neural-interface/lib/assistant-task-prompt.js +10 -8
  100. package/neural-interface/lib/assistant-tools.js +2 -2
  101. package/neural-interface/lib/assistant-usage.js +29 -0
  102. package/neural-interface/lib/backup-service.js +6 -1
  103. package/neural-interface/lib/browser-guard-cdp.js +5 -4
  104. package/neural-interface/lib/browser-network-guard.js +274 -82
  105. package/neural-interface/lib/browser-tool-policy.js +12 -7
  106. package/neural-interface/lib/claude-agent-bridge.js +7 -0
  107. package/neural-interface/lib/claude-history-cache.js +210 -0
  108. package/neural-interface/lib/claude-model-catalog.js +5 -1
  109. package/neural-interface/lib/cli-install-api.js +1 -1
  110. package/neural-interface/lib/cli-install.js +160 -31
  111. package/neural-interface/lib/cli-resolver.js +9 -2
  112. package/neural-interface/lib/cli-update-planner.js +25 -15
  113. package/neural-interface/lib/codex-rollout-reader.js +52 -0
  114. package/neural-interface/lib/codex-worker-app-server.js +25 -6
  115. package/neural-interface/lib/editor-git-api.js +3 -20
  116. package/neural-interface/lib/exec-loop-command.js +9 -2
  117. package/neural-interface/lib/file-explorer-api.js +74 -40
  118. package/neural-interface/lib/file-explorer-watch.js +9 -3
  119. package/neural-interface/lib/git-bash/install.js +52 -6
  120. package/neural-interface/lib/git-bash/manager.js +59 -17
  121. package/neural-interface/lib/git-bash/pins.js +12 -0
  122. package/neural-interface/lib/git-read.js +2 -2
  123. package/neural-interface/lib/git-status-runner.js +390 -0
  124. package/neural-interface/lib/git-status-watchdog.js +40 -0
  125. package/neural-interface/lib/greeting/budgets.js +1 -0
  126. package/neural-interface/lib/greeting/engine.js +2 -2
  127. package/neural-interface/lib/greeting/sources.js +10 -12
  128. package/neural-interface/lib/greeting/writer.js +1 -1
  129. package/neural-interface/lib/mcp-client-config.js +37 -6
  130. package/neural-interface/lib/mcp-external-servers.js +13 -4
  131. package/neural-interface/lib/mcp-installer.js +2 -1
  132. package/neural-interface/lib/memory-session-capture.js +17 -3
  133. package/neural-interface/lib/morelogin.js +147 -25
  134. package/neural-interface/lib/native-loop-providers.js +420 -21
  135. package/neural-interface/lib/native-loop-runtime.js +23 -2
  136. package/neural-interface/lib/opencode-native-boundary.js +11 -2
  137. package/neural-interface/lib/owner-authority.js +1 -1
  138. package/neural-interface/lib/path-confine.js +12 -9
  139. package/neural-interface/lib/peer-identity.js +32 -2
  140. package/neural-interface/lib/pty-host/core.js +28 -1
  141. package/neural-interface/lib/pty-host/lifetime.js +12 -0
  142. package/neural-interface/lib/pty-host/manager.js +12 -1
  143. package/neural-interface/lib/remote-policy.js +1 -1
  144. package/neural-interface/lib/request-gate.js +3 -0
  145. package/neural-interface/lib/rulesets/api.js +6 -5
  146. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  147. package/neural-interface/lib/rulesets/installer.js +89 -10
  148. package/neural-interface/lib/rulesets/render.js +1 -1
  149. package/neural-interface/lib/session-cache-builder.js +64 -1
  150. package/neural-interface/lib/skills/catalog.js +5 -1
  151. package/neural-interface/lib/skills/creation-brief.js +1 -1
  152. package/neural-interface/lib/skills/creation-sources.js +19 -1
  153. package/neural-interface/lib/skills/deploy.js +9 -4
  154. package/neural-interface/lib/skills/draft-confinement.js +3 -1
  155. package/neural-interface/lib/skills/eval-confinement.js +3 -1
  156. package/neural-interface/lib/skills/eval-trace.js +16 -0
  157. package/neural-interface/lib/skills/export.js +5 -2
  158. package/neural-interface/lib/skills/known-bridge-digests.json +58 -4
  159. package/neural-interface/lib/skills/ownership.js +1 -1
  160. package/neural-interface/lib/skills/roots.js +10 -3
  161. package/neural-interface/lib/skills/runtime-checks.js +6 -3
  162. package/neural-interface/lib/skills/service.js +15 -7
  163. package/neural-interface/lib/skills/telemetry.js +6 -0
  164. package/neural-interface/lib/synabun-version.js +1 -1
  165. package/neural-interface/lib/system-activity.js +1 -1
  166. package/neural-interface/lib/whatsapp/brain.js +7 -3
  167. package/neural-interface/lib/whatsapp/cards.js +22 -9
  168. package/neural-interface/lib/whatsapp/config.js +1 -1
  169. package/neural-interface/lib/worker-confinement.js +77 -1
  170. package/neural-interface/lib/worker-question.js +74 -0
  171. package/neural-interface/lib/worker-resume.js +13 -4
  172. package/neural-interface/lib/worker-sandbox.js +46 -16
  173. package/neural-interface/package-lock.json +18 -2
  174. package/neural-interface/package.json +2 -1
  175. package/neural-interface/public/onboarding.html +1 -1
  176. package/neural-interface/public/shared/agent-runtime-options.js +89 -11
  177. package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
  178. package/neural-interface/public/shared/agy/agy-client.js +266 -0
  179. package/neural-interface/public/shared/agy/agy-commands.js +120 -0
  180. package/neural-interface/public/shared/agy/agy-copy.js +326 -0
  181. package/neural-interface/public/shared/agy/agy-diff.js +161 -0
  182. package/neural-interface/public/shared/agy/agy-events.js +469 -0
  183. package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
  184. package/neural-interface/public/shared/agy/agy-options.js +232 -0
  185. package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
  186. package/neural-interface/public/shared/agy/agy-render.js +392 -0
  187. package/neural-interface/public/shared/agy/agy-setup.js +149 -0
  188. package/neural-interface/public/shared/agy/agy-styles.js +604 -0
  189. package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
  190. package/neural-interface/public/shared/api.js +4 -4
  191. package/neural-interface/public/shared/assistant/asst-agents-dock.js +3 -4
  192. package/neural-interface/public/shared/assistant/asst-api.js +1 -1
  193. package/neural-interface/public/shared/assistant/asst-brain-picker.js +56 -9
  194. package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
  195. package/neural-interface/public/shared/assistant/asst-control.js +48 -0
  196. package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
  197. package/neural-interface/public/shared/assistant/asst-limit-dock.js +6 -1
  198. package/neural-interface/public/shared/assistant/asst-limit-wait.js +38 -5
  199. package/neural-interface/public/shared/assistant/asst-panel.js +45 -8
  200. package/neural-interface/public/shared/assistant/asst-render.js +51 -12
  201. package/neural-interface/public/shared/assistant/asst-route.js +237 -57
  202. package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
  203. package/neural-interface/public/shared/assistant/asst-scenes.js +54 -1
  204. package/neural-interface/public/shared/assistant/asst-sidepanel.js +51 -7
  205. package/neural-interface/public/shared/assistant/asst-slash.js +8 -2
  206. package/neural-interface/public/shared/assistant/asst-state.js +76 -9
  207. package/neural-interface/public/shared/assistant/asst-styles.js +34 -0
  208. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +34 -7
  209. package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
  210. package/neural-interface/public/shared/cdx/cdx-output.js +26 -3
  211. package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
  212. package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
  213. package/neural-interface/public/shared/cdx/cdx-protocol.js +11 -10
  214. package/neural-interface/public/shared/cdx/cdx-render.js +10 -9
  215. package/neural-interface/public/shared/cdx/cdx-requests.js +11 -1
  216. package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
  217. package/neural-interface/public/shared/cdx/cdx-tabs.js +75 -5
  218. package/neural-interface/public/shared/cli-install-controller.js +218 -0
  219. package/neural-interface/public/shared/cli-install-view.js +7 -4
  220. package/neural-interface/public/shared/cli-status.js +9 -8
  221. package/neural-interface/public/shared/constants.js +3 -2
  222. package/neural-interface/public/shared/context-menu-model.js +1 -0
  223. package/neural-interface/public/shared/cp/cp-restore.js +162 -0
  224. package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
  225. package/neural-interface/public/shared/desktop-bridge.js +17 -0
  226. package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
  227. package/neural-interface/public/shared/desktop-titlebar.js +13 -4
  228. package/neural-interface/public/shared/desktop-update-view.js +1 -0
  229. package/neural-interface/public/shared/explorer/fx-api.js +4 -4
  230. package/neural-interface/public/shared/explorer/fx-links.js +3 -3
  231. package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
  232. package/neural-interface/public/shared/explorer/fx-main.js +48 -16
  233. package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
  234. package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
  235. package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
  236. package/neural-interface/public/shared/html-shell.js +21 -4
  237. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +27 -7
  238. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
  239. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
  240. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -0
  241. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +46 -3
  242. package/neural-interface/public/shared/provider-icons.js +4 -2
  243. package/neural-interface/public/shared/settings/settings-rules.js +2 -2
  244. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +1 -1
  245. package/neural-interface/public/shared/settings/settings-skill-folders.js +2 -2
  246. package/neural-interface/public/shared/showcase-privacy.js +6 -1
  247. package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
  248. package/neural-interface/public/shared/skills/ss-creation.js +6 -3
  249. package/neural-interface/public/shared/skills/ss-dialog.js +5 -2
  250. package/neural-interface/public/shared/skills/ss-lint.js +9 -1
  251. package/neural-interface/public/shared/skills/ss-profiles.js +44 -6
  252. package/neural-interface/public/shared/skills/ss-review-view.js +2 -2
  253. package/neural-interface/public/shared/skills/studio/sx-actions.js +1 -1
  254. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +1 -1
  255. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +2 -1
  256. package/neural-interface/public/shared/skills/studio/sx-dom.js +2 -1
  257. package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
  258. package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
  259. package/neural-interface/public/shared/skills/studio/sx-inspector.js +1 -1
  260. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +9 -6
  261. package/neural-interface/public/shared/skills/studio/sx-install.js +20 -3
  262. package/neural-interface/public/shared/skills/studio/sx-library-view.js +144 -35
  263. package/neural-interface/public/shared/skills/studio/sx-library.js +6 -2
  264. package/neural-interface/public/shared/skills/studio/sx-main.js +11 -2
  265. package/neural-interface/public/shared/skills/studio/sx-review-view.js +1 -1
  266. package/neural-interface/public/shared/skills/studio/sx-subject.js +7 -6
  267. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +6 -5
  268. package/neural-interface/public/shared/state.js +1 -1
  269. package/neural-interface/public/shared/streaming-markdown.js +58 -0
  270. package/neural-interface/public/shared/styles.css +354 -34
  271. package/neural-interface/public/shared/text-cache.js +29 -0
  272. package/neural-interface/public/shared/tool-list-picker.js +2 -2
  273. package/neural-interface/public/shared/tool-list-view.js +2 -2
  274. package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
  275. package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
  276. package/neural-interface/public/shared/ui-assistant.js +2 -0
  277. package/neural-interface/public/shared/ui-automation-studio.js +20 -12
  278. package/neural-interface/public/shared/ui-claude-panel.js +299 -77
  279. package/neural-interface/public/shared/ui-cli-banner.js +23 -167
  280. package/neural-interface/public/shared/ui-file-explorer.js +48 -6
  281. package/neural-interface/public/shared/ui-file-links.js +2 -2
  282. package/neural-interface/public/shared/ui-games.js +2 -2
  283. package/neural-interface/public/shared/ui-judgments.js +1 -1
  284. package/neural-interface/public/shared/ui-keybinds.js +8 -2
  285. package/neural-interface/public/shared/ui-link.js +3 -3
  286. package/neural-interface/public/shared/ui-menubar.js +2 -2
  287. package/neural-interface/public/shared/ui-native-loop-router.js +4 -0
  288. package/neural-interface/public/shared/ui-navbar.js +45 -0
  289. package/neural-interface/public/shared/ui-notifications.js +1 -0
  290. package/neural-interface/public/shared/ui-resume.js +9 -7
  291. package/neural-interface/public/shared/ui-schedules-studio.js +41 -10
  292. package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
  293. package/neural-interface/public/shared/ui-sessions.js +22 -12
  294. package/neural-interface/public/shared/ui-settings.js +90 -65
  295. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
  296. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -3
  297. package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
  298. package/neural-interface/public/shared/ui-sidepanel-windows.js +3 -3
  299. package/neural-interface/public/shared/ui-terminal.js +146 -10
  300. package/neural-interface/public/shared/ui-tool-updates.js +3 -3
  301. package/neural-interface/public/shared/ui-tooltip.js +71 -46
  302. package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
  303. package/neural-interface/public/shared/ui-whiteboard.js +1 -1
  304. package/neural-interface/public/shared/ui-worker-end.js +1 -1
  305. package/neural-interface/public/shared/whatsapp/wa-view.js +2 -2
  306. package/neural-interface/public/shared/worker-questions.js +34 -0
  307. package/neural-interface/public/sidepanel-session.html +1 -1
  308. package/neural-interface/server.js +644 -426
  309. package/neural-interface/templates/rulesets/README.md +1 -1
  310. package/neural-interface/templates/rulesets/legacy-hashes.json +5 -5
  311. package/neural-interface/templates/rulesets/manifest.json +1 -1
  312. package/npm-shrinkwrap.json +18 -2
  313. package/package.json +2 -1
  314. package/skills/skill-forge/SKILL.md +4 -4
  315. package/skills/synabun/SKILL.md +1 -1
  316. package/skills/synabun/modules/schedule.md +3 -3
  317. /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
@@ -43,6 +43,12 @@ export const SANDBOX_ELEVATION = Object.freeze({
43
43
  changes: false,
44
44
  reason: 'OpenCode has no operating-system sandbox, only permission rules, so nothing it starts needs administrator approval.',
45
45
  }),
46
+ // Antigravity (`agy acp`, lib/native-loop-providers.js) is started with no sandbox setting at all: every edit,
47
+ // command or outside access it wants is a permission request SynaBun answers (lib/worker-confinement.js).
48
+ antigravity: Object.freeze({
49
+ changes: false,
50
+ reason: 'SynaBun starts Antigravity (agy acp) with no sandbox setting and answers each of its permission requests itself, so nothing it starts asks for administrator approval.',
51
+ }),
46
52
  });
47
53
 
48
54
  /** Only `true` turns it on: anything else (missing, a string, null) is the default, off. */
@@ -0,0 +1,554 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun — the client side of ACP's file and terminal methods (Antigravity side panel)
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // An ACP client may offer the agent its own file system and terminals (clientCapabilities
6
+ // `fs` and `terminal`): the agent then reads and writes text files and runs commands through
7
+ // the client instead of by itself. The Antigravity side panel offers them only when the owner
8
+ // turned them on in the panel (its "let Antigravity use SynaBun for files / commands" switches,
9
+ // sent with the socket's hello); lib/antigravity/panel-bridge.js routes the agent's requests
10
+ // here and answers "method not found" for what was not offered. One host per panel socket.
11
+ //
12
+ // fs/read_text_file { sessionId, path, line?, limit? } -> { content }
13
+ // fs/write_text_file { sessionId, path, content } -> {}
14
+ // terminal/create { sessionId, command, args?, env?, cwd?, outputByteLimit? } -> { terminalId }
15
+ // terminal/output { sessionId, terminalId } -> { output, truncated, exitStatus: null | { exitCode, signal } }
16
+ // terminal/wait_for_exit { sessionId, terminalId } -> { exitCode, signal }
17
+ // terminal/kill { sessionId, terminalId } -> {} the terminal stays readable
18
+ // terminal/release { sessionId, terminalId } -> {} killed first when still running, then forgotten
19
+ //
20
+ // Files: an absolute path. A read is of a regular file of at most 10 MB, decoded as UTF-8 (a
21
+ // file that is not UTF-8 is refused; a byte-order mark is kept, so a read written back is the
22
+ // same file); `line` is the 1-based first line, `limit` a number of lines, each line with its
23
+ // own ending. A write of at most 10 MB creates the file and its missing folders, or replaces
24
+ // the content of the regular file that is there, in place: its mode, owner and links stay.
25
+ // A folder, a link to one, a device or a FIFO is refused, and nothing waits on a FIFO.
26
+ //
27
+ // Terminals: the command runs in `cwd` (an absolute folder; the session's own when not given)
28
+ // with the agent's own environment plus the pairs it named, then `terminalEnv`, which the agent
29
+ // cannot change (the panel marks every command as the agent's: SYNABUN_AGENT_TERMINAL, because
30
+ // SynaBun's server, not the agent, is its parent process). Without `args` the command is a
31
+ // command line for the platform's shell (POSIX `/bin/sh -c`, Windows `cmd.exe /d /s /c`);
32
+ // with them it is a program started without a shell. On POSIX it leads its own process group,
33
+ // so a kill reaches what it started (Windows: taskkill /T). stdin is closed; stdout and stderr
34
+ // are one output, kept to `outputByteLimit` bytes (default 1 MiB, at most 16 MiB) by dropping
35
+ // from the beginning at a character boundary (`truncated` says so). At most 16 terminals at a
36
+ // time, released or not: a terminal holds its output until the agent releases it.
37
+ // `onOutput(snapshot)` sees a terminal's whole current output at most every 250 ms while it
38
+ // grows, and once more when the command ends (also after a release).
39
+ //
40
+ // Nothing here confines a path: the agent runs as the owner and reads and writes the same files
41
+ // by itself. What this adds is bounds, the refusals above, and an end for everything it
42
+ // started (`releaseSession`, `releaseAll`).
43
+
44
+ import { spawn as nodeSpawn } from 'node:child_process';
45
+ import { randomBytes } from 'node:crypto';
46
+ import { constants as fsConstants } from 'node:fs';
47
+ import * as nodeFs from 'node:fs/promises';
48
+ import { dirname, isAbsolute } from 'node:path';
49
+ import { RequestError } from '@agentclientprotocol/sdk/unstable';
50
+ import { forgetAgentProcess, registerAgentProcess } from '../peer-identity.js';
51
+
52
+ export const ACP_CLIENT_HOST_LIMITS = Object.freeze({
53
+ maxFileBytes: 10 * 1024 * 1024,
54
+ defaultOutputBytes: 1024 * 1024,
55
+ maxOutputBytes: 16 * 1024 * 1024,
56
+ maxTerminals: 16,
57
+ outputThrottleMs: 250,
58
+ killGraceMs: 2_000,
59
+ // After the command exits, how long its output may still arrive before the end is reported
60
+ // (something it started in the background can hold the pipe open far longer).
61
+ exitDrainMs: 150,
62
+ maxCommandChars: 32 * 1024,
63
+ maxArgs: 1_000,
64
+ maxArgChars: 32 * 1024,
65
+ maxEnv: 200,
66
+ maxEnvValueChars: 32 * 1024,
67
+ });
68
+
69
+ const O_NONBLOCK = fsConstants.O_NONBLOCK || 0;
70
+ const BLOCK_BYTES = 64 * 1024;
71
+ const MAX_ID_CHARS = 500;
72
+ const FS_METHODS = new Set(['fs/read_text_file', 'fs/write_text_file']);
73
+
74
+ const invalid = (message, data) => RequestError.invalidParams(data, message);
75
+ const megabytes = (bytes) => (bytes >= 1048576 ? `${Math.round(bytes / 1048576)} MB` : `${bytes} bytes`);
76
+
77
+ /** The error an fs call failed with, as the agent should read it. */
78
+ function fileError(error, path) {
79
+ switch (error?.code) {
80
+ case 'ENOENT': case 'ENOTDIR': return RequestError.resourceNotFound(path);
81
+ case 'EISDIR': return invalid(`${path} is a folder, not a file.`, { path });
82
+ case 'EACCES': case 'EPERM': return invalid(`Permission denied: ${path}`, { path });
83
+ case 'ELOOP': return invalid(`Too many links on the way to ${path}.`, { path });
84
+ case 'EFBIG': case 'ENOSPC': case 'EDQUOT': case 'EROFS': return invalid(`${path} cannot be written: ${error.code}.`, { path });
85
+ default: return RequestError.internalError({ path, code: error?.code || null }, error?.message || String(error));
86
+ }
87
+ }
88
+
89
+ function absolutePath(value) {
90
+ if (typeof value !== 'string' || !value || value.includes('\0')) throw invalid('`path` must be an absolute path.');
91
+ if (!isAbsolute(value)) throw invalid(`\`path\` must be an absolute path: ${value}`, { path: value });
92
+ return value;
93
+ }
94
+
95
+ function sessionIdOf(params) {
96
+ const id = params?.sessionId;
97
+ if (typeof id !== 'string' || !id || id.length > MAX_ID_CHARS) throw invalid('Unknown session.');
98
+ return id;
99
+ }
100
+
101
+ /** Lines `line` (1-based) … `line + limit - 1` of `text`, each with its own ending; all of it without either. */
102
+ export function sliceLines(text, line, limit) {
103
+ const first = Number.isInteger(line) && line > 1 ? line : 1;
104
+ const count = Number.isInteger(limit) && limit >= 0 ? limit : null;
105
+ if (first === 1 && count === null) return text;
106
+ let start = 0;
107
+ for (let n = 1; n < first; n++) {
108
+ const next = text.indexOf('\n', start);
109
+ if (next === -1) return '';
110
+ start = next + 1;
111
+ }
112
+ if (count === null) return text.slice(start);
113
+ let end = start;
114
+ for (let n = 0; n < count; n++) {
115
+ const next = text.indexOf('\n', end);
116
+ if (next === -1) { end = text.length; break; }
117
+ end = next + 1;
118
+ }
119
+ return text.slice(start, end);
120
+ }
121
+
122
+ /** How much of `buf` holds whole UTF-8 characters: a sequence cut at its end is left for later. */
123
+ export function completeUtf8Length(buf) {
124
+ const n = buf.length;
125
+ for (let i = n - 1, back = 0; i >= 0 && back < 4; i--, back++) {
126
+ const byte = buf[i];
127
+ if ((byte & 0xc0) === 0x80) continue;
128
+ const need = byte >= 0xf0 ? 4 : byte >= 0xe0 ? 3 : byte >= 0xc0 ? 2 : 1;
129
+ return n - i >= need ? n : i;
130
+ }
131
+ return n;
132
+ }
133
+
134
+ /**
135
+ * A command's output, kept to its last `limit` bytes. Written in 64 KB blocks (no copy of what is
136
+ * already kept on every write); read whole, from a character boundary when its start was dropped.
137
+ */
138
+ export function createOutputBuffer(limit) {
139
+ const blocks = [];
140
+ let tailUsed = 0; // bytes written into the last block
141
+ let headOffset = 0; // bytes of the first block already dropped
142
+ let size = 0; // bytes kept
143
+ let truncated = false;
144
+ const usedOf = (index) => (index === blocks.length - 1 ? tailUsed : blocks[index].length) - (index === 0 ? headOffset : 0);
145
+ return {
146
+ push(chunk) {
147
+ if (!chunk?.length) return;
148
+ let at = 0;
149
+ while (at < chunk.length) {
150
+ let tail = blocks[blocks.length - 1];
151
+ if (!tail || tailUsed === tail.length) {
152
+ tail = Buffer.allocUnsafe(BLOCK_BYTES);
153
+ blocks.push(tail);
154
+ tailUsed = 0;
155
+ }
156
+ const copied = chunk.copy(tail, tailUsed, at, Math.min(chunk.length, at + tail.length - tailUsed));
157
+ tailUsed += copied;
158
+ at += copied;
159
+ size += copied;
160
+ }
161
+ while (size > limit) {
162
+ truncated = true;
163
+ const first = usedOf(0);
164
+ const excess = size - limit;
165
+ if (excess >= first && blocks.length > 1) {
166
+ blocks.shift();
167
+ headOffset = 0;
168
+ size -= first;
169
+ continue;
170
+ }
171
+ const drop = Math.min(excess, first);
172
+ headOffset += drop;
173
+ size -= drop;
174
+ break;
175
+ }
176
+ },
177
+ /** `final`: the command ended, so a character cut at the very end is kept (as U+FFFD). */
178
+ read(final = false) {
179
+ const parts = blocks.map((block, index) => block.subarray(index === 0 ? headOffset : 0, index === blocks.length - 1 ? tailUsed : block.length));
180
+ const all = Buffer.concat(parts, size);
181
+ let from = 0;
182
+ if (truncated) while (from < all.length && (all[from] & 0xc0) === 0x80) from++;
183
+ const to = final ? all.length : Math.max(from, completeUtf8Length(all));
184
+ return { output: all.subarray(from, to).toString('utf8'), truncated };
185
+ },
186
+ };
187
+ }
188
+
189
+ function shellCommand(command, platform, env) {
190
+ if (platform === 'win32') {
191
+ return { file: env.ComSpec || env.COMSPEC || 'cmd.exe', args: ['/d', '/s', '/c', `"${command}"`], options: { windowsVerbatimArguments: true } };
192
+ }
193
+ return { file: '/bin/sh', args: ['-c', command], options: {} };
194
+ }
195
+
196
+ /**
197
+ * `childEnv()`: the agent's own environment, which a command starts from. `terminalEnv`: variables
198
+ * every command gets last, over the agent's own pairs. `sessionCwd(sessionId)`: the folder a session
199
+ * runs in, or null (a command without `cwd` in an unknown session is refused).
200
+ * `onOutput({ sessionId, terminalId, output, truncated, exitStatus })`: see the header.
201
+ * `spawnImpl`, `fs` (node:fs/promises' open, stat, mkdir), `killImpl` (process.kill) and `platform`
202
+ * are there for tests.
203
+ */
204
+ export function createAcpClientHost({
205
+ childEnv = () => process.env,
206
+ terminalEnv = {},
207
+ sessionCwd = () => null,
208
+ onOutput = null,
209
+ spawnImpl = nodeSpawn,
210
+ fs = nodeFs,
211
+ killImpl = (pid, signal) => process.kill(pid, signal),
212
+ platform = process.platform,
213
+ limits = ACP_CLIENT_HOST_LIMITS,
214
+ log = () => {},
215
+ // Every command is the agent's: while it runs, its process tree never holds the owner's authority
216
+ // (lib/peer-identity.js; the server, not the agent, is its parent, so no agent is in its tree by name).
217
+ registerProcess = registerAgentProcess,
218
+ forgetProcess = forgetAgentProcess,
219
+ } = {}) {
220
+ const terminals = new Map(); // terminalId -> entry
221
+ let reserved = 0; // terminals being started
222
+
223
+ // ── Files ──────────────────────────────────────────────────────────────
224
+
225
+ async function readTextFile(params) {
226
+ sessionIdOf(params);
227
+ const path = absolutePath(params.path);
228
+ let handle;
229
+ try { handle = await fs.open(path, fsConstants.O_RDONLY | O_NONBLOCK); } catch (error) { throw fileError(error, path); }
230
+ try {
231
+ const stat = await handle.stat();
232
+ if (stat.isDirectory()) throw invalid(`${path} is a folder, not a file.`, { path });
233
+ if (!stat.isFile()) throw invalid(`${path} is not a regular file.`, { path });
234
+ if (stat.size > limits.maxFileBytes) throw invalid(`${path} is larger than ${megabytes(limits.maxFileBytes)}.`, { path, size: stat.size });
235
+ const parts = [];
236
+ let total = 0;
237
+ for (;;) {
238
+ const room = limits.maxFileBytes + 1 - total;
239
+ if (room <= 0) break;
240
+ const chunk = Buffer.allocUnsafe(Math.min(256 * 1024, room));
241
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
242
+ if (!bytesRead) break;
243
+ parts.push(chunk.subarray(0, bytesRead));
244
+ total += bytesRead;
245
+ }
246
+ if (total > limits.maxFileBytes) throw invalid(`${path} is larger than ${megabytes(limits.maxFileBytes)}.`, { path });
247
+ let text;
248
+ try { text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(Buffer.concat(parts, total)); } catch { throw invalid(`${path} is not UTF-8 text.`, { path }); }
249
+ return { content: sliceLines(text, params.line, params.limit) };
250
+ } catch (error) {
251
+ throw error instanceof RequestError ? error : fileError(error, path);
252
+ } finally {
253
+ await handle.close().catch(() => {});
254
+ }
255
+ }
256
+
257
+ async function writeTextFile(params) {
258
+ sessionIdOf(params);
259
+ const path = absolutePath(params.path);
260
+ if (typeof params.content !== 'string') throw invalid('`content` must be text.');
261
+ const data = Buffer.from(params.content, 'utf8');
262
+ if (data.length > limits.maxFileBytes) throw invalid(`The content is larger than ${megabytes(limits.maxFileBytes)}.`, { path, size: data.length });
263
+ let existing = null;
264
+ try { existing = await fs.stat(path); } catch (error) { if (error?.code !== 'ENOENT') throw fileError(error, path); }
265
+ if (existing?.isDirectory()) throw invalid(`${path} is a folder, not a file.`, { path });
266
+ if (existing && !existing.isFile()) throw invalid(`${path} is not a regular file.`, { path });
267
+ if (!existing) {
268
+ try { await fs.mkdir(dirname(path), { recursive: true }); } catch (error) { throw fileError(error, dirname(path)); }
269
+ }
270
+ let handle;
271
+ try { handle = await fs.open(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | O_NONBLOCK, 0o666); } catch (error) { throw fileError(error, path); }
272
+ try {
273
+ // What was opened is what is written: a regular file, emptied only now.
274
+ const stat = await handle.stat();
275
+ if (stat.isDirectory()) throw invalid(`${path} is a folder, not a file.`, { path });
276
+ if (!stat.isFile()) throw invalid(`${path} is not a regular file.`, { path });
277
+ await handle.truncate(0);
278
+ if (data.length) await handle.writeFile(data);
279
+ return {};
280
+ } catch (error) {
281
+ throw error instanceof RequestError ? error : fileError(error, path);
282
+ } finally {
283
+ await handle.close().catch(() => {});
284
+ }
285
+ }
286
+
287
+ // ── Terminals ──────────────────────────────────────────────────────────
288
+
289
+ function argsOf(value) {
290
+ if (value === undefined || value === null) return [];
291
+ if (!Array.isArray(value) || value.length > limits.maxArgs) throw invalid(`\`args\` must be a list of at most ${limits.maxArgs} words.`);
292
+ for (const arg of value) {
293
+ if (typeof arg !== 'string' || arg.length > limits.maxArgChars || arg.includes('\0')) throw invalid('An argument is not text, or too long.');
294
+ }
295
+ return [...value];
296
+ }
297
+
298
+ function envPairsOf(value) {
299
+ if (value === undefined || value === null) return [];
300
+ if (!Array.isArray(value) || value.length > limits.maxEnv) throw invalid(`\`env\` must be a list of at most ${limits.maxEnv} variables.`);
301
+ return value.map((pair) => {
302
+ const name = pair?.name;
303
+ const text = pair?.value;
304
+ if (typeof name !== 'string' || !name || name.length > 256 || /[=\0]/.test(name)) throw invalid(`Not a variable name: ${String(name).slice(0, 80)}`);
305
+ if (typeof text !== 'string' || text.length > limits.maxEnvValueChars || text.includes('\0')) throw invalid(`The value of ${name} is not text, or too long.`);
306
+ return { name, value: text };
307
+ });
308
+ }
309
+
310
+ function environment(pairs) {
311
+ const env = { ...(childEnv() || {}) };
312
+ const set = (name, value) => {
313
+ // Windows reads names in any case: one PATH, never PATH and Path.
314
+ if (platform === 'win32') for (const key of Object.keys(env)) if (key.toUpperCase() === name.toUpperCase()) delete env[key];
315
+ env[name] = value;
316
+ };
317
+ for (const { name, value } of pairs) set(name, value);
318
+ for (const [name, value] of Object.entries(terminalEnv || {})) set(name, String(value));
319
+ return env;
320
+ }
321
+
322
+ async function cwdOf(value, sessionId) {
323
+ let dir = value;
324
+ if (dir === undefined || dir === null || dir === '') dir = sessionCwd(sessionId);
325
+ if (typeof dir !== 'string' || !dir) throw invalid('No folder to run in: give `cwd`, an absolute path.');
326
+ if (!isAbsolute(dir) || dir.includes('\0')) throw invalid(`\`cwd\` must be an absolute path: ${dir}`);
327
+ let stat;
328
+ try { stat = await fs.stat(dir); } catch { throw invalid(`The folder ${dir} does not exist.`, { cwd: dir }); }
329
+ if (!stat.isDirectory()) throw invalid(`${dir} is not a folder.`, { cwd: dir });
330
+ return dir;
331
+ }
332
+
333
+ function outputLimitOf(value) {
334
+ if (value === undefined || value === null) return limits.defaultOutputBytes;
335
+ if (!Number.isInteger(value) || value < 0) throw invalid('`outputByteLimit` must be a whole number of bytes.');
336
+ return Math.min(value, limits.maxOutputBytes);
337
+ }
338
+
339
+ function signalTree(entry, signal) {
340
+ const pid = entry.child?.pid;
341
+ if (!pid) return;
342
+ if (platform === 'win32') {
343
+ try {
344
+ const killer = spawnImpl('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
345
+ killer?.on?.('error', () => {});
346
+ } catch { /* the plain kill below */ }
347
+ try { entry.child.kill(); } catch { /* gone */ }
348
+ return;
349
+ }
350
+ try { killImpl(-pid, signal); } catch { try { entry.child.kill(signal); } catch { /* gone */ } }
351
+ }
352
+
353
+ function snapshotOf(entry) {
354
+ const { output, truncated } = entry.output.read(entry.finished);
355
+ return { output, truncated, exitStatus: entry.finished ? { ...entry.exitStatus } : null };
356
+ }
357
+
358
+ function emit(entry) {
359
+ entry.dirty = false;
360
+ entry.lastEmit = Date.now();
361
+ if (typeof onOutput !== 'function') return;
362
+ try { onOutput({ sessionId: entry.sessionId, terminalId: entry.terminalId, ...snapshotOf(entry) }); } catch (error) { log(`terminal output listener failed: ${error?.message || error}`); }
363
+ }
364
+
365
+ function scheduleEmit(entry) {
366
+ if (entry.timer || entry.released || typeof onOutput !== 'function') return;
367
+ const wait = Math.max(0, entry.lastEmit + limits.outputThrottleMs - Date.now());
368
+ entry.timer = setTimeout(() => {
369
+ entry.timer = null;
370
+ if (entry.dirty && !entry.released) emit(entry);
371
+ }, wait);
372
+ entry.timer.unref?.();
373
+ }
374
+
375
+ function closePipes(entry) {
376
+ try { entry.child.stdout?.destroy(); } catch { /* closed */ }
377
+ try { entry.child.stderr?.destroy(); } catch { /* closed */ }
378
+ }
379
+
380
+ function finish(entry, code, signal) {
381
+ if (entry.finished) return;
382
+ if (!entry.exitStatus) entry.exitStatus = { exitCode: Number.isInteger(code) ? code : null, signal: signal || null };
383
+ entry.finished = true;
384
+ clearTimeout(entry.timer);
385
+ entry.timer = null;
386
+ clearTimeout(entry.drainTimer);
387
+ for (const waiter of entry.waiters.splice(0)) waiter.resolve({ ...entry.exitStatus });
388
+ emit(entry);
389
+ if (entry.released) closePipes(entry);
390
+ }
391
+
392
+ // SIGTERM to the command and what it started, SIGKILL to whatever of it is still there after the grace.
393
+ function kill(entry) {
394
+ if (entry.finished && !entry.pipesOpen) return;
395
+ signalTree(entry, 'SIGTERM');
396
+ if (!entry.killTimer) {
397
+ entry.killTimer = setTimeout(() => { if (!entry.finished || entry.pipesOpen) signalTree(entry, 'SIGKILL'); }, limits.killGraceMs);
398
+ entry.killTimer.unref?.();
399
+ }
400
+ }
401
+
402
+ function release(entry) {
403
+ if (entry.released) return;
404
+ entry.released = true;
405
+ terminals.delete(entry.terminalId);
406
+ clearTimeout(entry.timer);
407
+ entry.timer = null;
408
+ kill(entry);
409
+ // Output of something it left running in the background is not read any more.
410
+ if (entry.finished) closePipes(entry);
411
+ }
412
+
413
+ function terminalOf(params) {
414
+ const sessionId = sessionIdOf(params);
415
+ const id = params?.terminalId;
416
+ const entry = typeof id === 'string' ? terminals.get(id) : null;
417
+ if (!entry || entry.sessionId !== sessionId) throw invalid(`Unknown terminal: ${String(id).slice(0, 80)}`, { terminalId: typeof id === 'string' ? id.slice(0, 200) : null });
418
+ return entry;
419
+ }
420
+
421
+ async function createTerminal(params) {
422
+ const sessionId = sessionIdOf(params);
423
+ if (terminals.size + reserved >= limits.maxTerminals) throw invalid(`At most ${limits.maxTerminals} terminals at a time: release one first.`);
424
+ const command = params.command;
425
+ if (typeof command !== 'string' || !command.trim() || command.length > limits.maxCommandChars || command.includes('\0')) throw invalid('`command` must be a command line of at most 32 KB.');
426
+ const args = argsOf(params.args);
427
+ const pairs = envPairsOf(params.env);
428
+ const outputLimit = outputLimitOf(params.outputByteLimit);
429
+ reserved += 1;
430
+ try {
431
+ const cwd = await cwdOf(params.cwd, sessionId);
432
+ const env = environment(pairs);
433
+ const launch = args.length ? { file: command, args, options: {} } : shellCommand(command, platform, env);
434
+ let child;
435
+ try {
436
+ child = spawnImpl(launch.file, launch.args, {
437
+ cwd, env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached: platform !== 'win32', ...launch.options,
438
+ });
439
+ } catch (error) {
440
+ throw RequestError.internalError({ command: command.slice(0, 200) }, `Could not start ${command.slice(0, 200)}: ${error?.message || error}`);
441
+ }
442
+ // From the start: an error the child reports later is logged, never thrown at the server.
443
+ child?.on?.('error', (error) => log(`terminal (${command.slice(0, 80)}): ${error?.message || error}`));
444
+ await new Promise((resolve, reject) => {
445
+ if (typeof child?.once !== 'function') { reject(new Error('the command did not start')); return; }
446
+ const started = () => { child.off?.('error', failed); resolve(); };
447
+ const failed = (error) => { child.off?.('spawn', started); reject(error); };
448
+ child.once('spawn', started);
449
+ child.once('error', failed);
450
+ }).catch((error) => {
451
+ throw RequestError.internalError({ command: command.slice(0, 200), code: error?.code || null }, `Could not start ${command.slice(0, 200)}: ${error?.message || error}`);
452
+ });
453
+ const entry = {
454
+ terminalId: `term-${randomBytes(8).toString('hex')}`,
455
+ sessionId,
456
+ child,
457
+ output: createOutputBuffer(outputLimit),
458
+ exitStatus: null,
459
+ finished: false,
460
+ released: false,
461
+ pipesOpen: true,
462
+ waiters: [],
463
+ timer: null,
464
+ drainTimer: null,
465
+ killTimer: null,
466
+ dirty: false,
467
+ lastEmit: 0,
468
+ };
469
+ const onData = (chunk) => {
470
+ entry.output.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
471
+ entry.dirty = true;
472
+ scheduleEmit(entry);
473
+ };
474
+ child.stdout?.on('data', onData);
475
+ child.stderr?.on('data', onData);
476
+ try { registerProcess(child.pid, 'antigravity command'); } catch { /* the guard is best effort; the command runs */ }
477
+ child.on('exit', (code, signal) => {
478
+ try { forgetProcess(child.pid); } catch { /* already forgotten */ }
479
+ entry.exitStatus = { exitCode: Number.isInteger(code) ? code : null, signal: signal || null };
480
+ entry.drainTimer = setTimeout(() => finish(entry, code, signal), limits.exitDrainMs);
481
+ entry.drainTimer.unref?.();
482
+ });
483
+ child.on('close', (code, signal) => { entry.pipesOpen = false; finish(entry, code, signal); });
484
+ terminals.set(entry.terminalId, entry);
485
+ return { terminalId: entry.terminalId };
486
+ } finally {
487
+ reserved -= 1;
488
+ }
489
+ }
490
+
491
+ function terminalOutput(params) {
492
+ return snapshotOf(terminalOf(params));
493
+ }
494
+
495
+ function waitForTerminalExit(params, { signal } = {}) {
496
+ const entry = terminalOf(params);
497
+ if (entry.finished) return { ...entry.exitStatus };
498
+ return new Promise((resolve, reject) => {
499
+ const waiter = {
500
+ resolve: (status) => { signal?.removeEventListener?.('abort', onAbort); resolve(status); },
501
+ };
502
+ const onAbort = () => {
503
+ const at = entry.waiters.indexOf(waiter);
504
+ if (at !== -1) entry.waiters.splice(at, 1);
505
+ reject(RequestError.requestCancelled());
506
+ };
507
+ if (signal?.aborted) { reject(RequestError.requestCancelled()); return; }
508
+ signal?.addEventListener?.('abort', onAbort, { once: true });
509
+ entry.waiters.push(waiter);
510
+ });
511
+ }
512
+
513
+ function killTerminal(params) {
514
+ kill(terminalOf(params));
515
+ return {};
516
+ }
517
+
518
+ function releaseTerminal(params) {
519
+ release(terminalOf(params));
520
+ return {};
521
+ }
522
+
523
+ const HANDLERS = {
524
+ 'fs/read_text_file': readTextFile,
525
+ 'fs/write_text_file': writeTextFile,
526
+ 'terminal/create': createTerminal,
527
+ 'terminal/output': terminalOutput,
528
+ 'terminal/wait_for_exit': waitForTerminalExit,
529
+ 'terminal/kill': killTerminal,
530
+ 'terminal/release': releaseTerminal,
531
+ };
532
+
533
+ return {
534
+ /**
535
+ * Answers one client method. `allow` ({ fs, terminal }) is what the client offered the agent
536
+ * that asked: a method outside it is refused as "method not found", as one never offered.
537
+ */
538
+ async handle(method, params, { signal, allow = { fs: true, terminal: true } } = {}) {
539
+ const handler = Object.hasOwn(HANDLERS, method) ? HANDLERS[method] : null;
540
+ if (!handler) throw RequestError.methodNotFound(method);
541
+ if (allow?.[FS_METHODS.has(method) ? 'fs' : 'terminal'] !== true) throw RequestError.methodNotFound(method);
542
+ return handler(params && typeof params === 'object' ? params : {}, { signal });
543
+ },
544
+ /** Kills and forgets every terminal of a session (it closed, or was deleted). */
545
+ releaseSession(sessionId) {
546
+ for (const entry of [...terminals.values()]) if (entry.sessionId === sessionId) release(entry);
547
+ },
548
+ /** Kills and forgets every terminal (the agent restarted or ended, the socket closed). */
549
+ releaseAll() {
550
+ for (const entry of [...terminals.values()]) release(entry);
551
+ },
552
+ liveCount: () => terminals.size,
553
+ };
554
+ }