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.
- package/CHANGELOG.md +121 -0
- package/README.md +1 -1
- package/docs/whatsapp.md +11 -11
- package/lib/client-config-repair.js +13 -9
- package/lib/external-tools.js +6 -5
- package/lib/update-snapshot.js +77 -3
- package/lib/virustotal.mjs +12 -1
- package/mcp-server/dist/index.js +1 -1
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
- package/mcp-server/dist/services/budget-judgments.js +5 -3
- package/mcp-server/dist/services/budget-judgments.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +2 -2
- package/mcp-server/dist/services/identity.js +2 -2
- package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
- package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
- package/mcp-server/dist/services/memory-sessions.js +28 -2
- package/mcp-server/dist/services/memory-sessions.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +2 -1
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
- package/mcp-server/dist/services/server-instructions.js +2 -1
- package/mcp-server/dist/services/server-instructions.js.map +1 -1
- package/mcp-server/dist/services/tool-catalog.js +1 -1
- package/mcp-server/dist/services/tool-catalog.js.map +1 -1
- package/mcp-server/dist/services/tool-list.d.ts +1 -1
- package/mcp-server/dist/services/tool-list.d.ts.map +1 -1
- package/mcp-server/dist/services/tool-runtime.d.ts +2 -2
- package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -1
- package/mcp-server/dist/services/tool-runtime.js +1 -1
- package/mcp-server/dist/services/tool-runtime.js.map +1 -1
- package/mcp-server/dist/services/typesafe-config.js +1 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +8 -8
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +10 -9
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/choice.d.ts +6 -0
- package/mcp-server/dist/tools/choice.d.ts.map +1 -1
- package/mcp-server/dist/tools/choice.js +18 -13
- package/mcp-server/dist/tools/choice.js.map +1 -1
- package/mcp-server/dist/tools/git-tools.js +2 -2
- package/mcp-server/dist/tools/git-tools.js.map +1 -1
- package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
- package/mcp-server/dist/tools/morelogin.js +9 -6
- package/mcp-server/dist/tools/morelogin.js.map +1 -1
- package/mcp-server/dist/tools/skill-tools.d.ts +8 -8
- package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/skill-tools.js +11 -8
- package/mcp-server/dist/tools/skill-tools.js.map +1 -1
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/src/index.ts +1 -1
- package/mcp-server/src/services/budget-judgments.ts +5 -3
- package/mcp-server/src/services/identity.ts +2 -2
- package/mcp-server/src/services/memory-sessions.ts +24 -3
- package/mcp-server/src/services/neural-interface.ts +2 -1
- package/mcp-server/src/services/server-instructions.ts +2 -1
- package/mcp-server/src/services/tool-catalog.ts +1 -1
- package/mcp-server/src/services/tool-list.ts +1 -1
- package/mcp-server/src/services/tool-runtime.ts +2 -2
- package/mcp-server/src/services/typesafe-config.ts +1 -1
- package/mcp-server/src/tools/agents.ts +10 -9
- package/mcp-server/src/tools/choice.ts +17 -12
- package/mcp-server/src/tools/git-tools.ts +2 -2
- package/mcp-server/src/tools/morelogin.ts +6 -4
- package/mcp-server/src/tools/skill-tools.ts +11 -8
- package/neural-interface/i18n/en.json +501 -39
- package/neural-interface/i18n/pt-BR.json +501 -39
- package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
- package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
- package/neural-interface/lib/antigravity/acp-connection.js +299 -0
- package/neural-interface/lib/antigravity/bypass.js +100 -0
- package/neural-interface/lib/antigravity/catalog.js +273 -0
- package/neural-interface/lib/antigravity/home.js +260 -0
- package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
- package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
- package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
- package/neural-interface/lib/antigravity/policy.js +74 -0
- package/neural-interface/lib/antigravity/tool-call.js +178 -0
- package/neural-interface/lib/antigravity/transcripts.js +252 -0
- package/neural-interface/lib/antigravity/usage.js +43 -0
- package/neural-interface/lib/assistant-api.js +1 -0
- package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
- package/neural-interface/lib/assistant-budget-planner.js +7 -3
- package/neural-interface/lib/assistant-budget-split.js +42 -15
- package/neural-interface/lib/assistant-catalog.js +76 -9
- package/neural-interface/lib/assistant-config.js +6 -3
- package/neural-interface/lib/assistant-dispatch.js +207 -49
- package/neural-interface/lib/assistant-envelope.js +153 -1
- package/neural-interface/lib/assistant-evidence.js +80 -0
- package/neural-interface/lib/assistant-hidden-models.js +7 -5
- package/neural-interface/lib/assistant-persona.js +16 -11
- package/neural-interface/lib/assistant-plan-permissions.js +15 -0
- package/neural-interface/lib/assistant-router.js +237 -35
- package/neural-interface/lib/assistant-runtime.js +158 -12
- package/neural-interface/lib/assistant-task-prompt.js +10 -8
- package/neural-interface/lib/assistant-tools.js +2 -2
- package/neural-interface/lib/assistant-usage.js +29 -0
- package/neural-interface/lib/backup-service.js +6 -1
- package/neural-interface/lib/browser-guard-cdp.js +5 -4
- package/neural-interface/lib/browser-network-guard.js +274 -82
- package/neural-interface/lib/browser-tool-policy.js +12 -7
- package/neural-interface/lib/claude-agent-bridge.js +7 -0
- package/neural-interface/lib/claude-history-cache.js +210 -0
- package/neural-interface/lib/claude-model-catalog.js +5 -1
- package/neural-interface/lib/cli-install-api.js +1 -1
- package/neural-interface/lib/cli-install.js +160 -31
- package/neural-interface/lib/cli-resolver.js +9 -2
- package/neural-interface/lib/cli-update-planner.js +25 -15
- package/neural-interface/lib/codex-rollout-reader.js +52 -0
- package/neural-interface/lib/codex-worker-app-server.js +25 -6
- package/neural-interface/lib/editor-git-api.js +3 -20
- package/neural-interface/lib/exec-loop-command.js +9 -2
- package/neural-interface/lib/file-explorer-api.js +74 -40
- package/neural-interface/lib/file-explorer-watch.js +9 -3
- package/neural-interface/lib/git-bash/install.js +52 -6
- package/neural-interface/lib/git-bash/manager.js +59 -17
- package/neural-interface/lib/git-bash/pins.js +12 -0
- package/neural-interface/lib/git-read.js +2 -2
- package/neural-interface/lib/git-status-runner.js +390 -0
- package/neural-interface/lib/git-status-watchdog.js +40 -0
- package/neural-interface/lib/greeting/budgets.js +1 -0
- package/neural-interface/lib/greeting/engine.js +2 -2
- package/neural-interface/lib/greeting/sources.js +10 -12
- package/neural-interface/lib/greeting/writer.js +1 -1
- package/neural-interface/lib/mcp-client-config.js +37 -6
- package/neural-interface/lib/mcp-external-servers.js +13 -4
- package/neural-interface/lib/mcp-installer.js +2 -1
- package/neural-interface/lib/memory-session-capture.js +17 -3
- package/neural-interface/lib/morelogin.js +147 -25
- package/neural-interface/lib/native-loop-providers.js +420 -21
- package/neural-interface/lib/native-loop-runtime.js +23 -2
- package/neural-interface/lib/opencode-native-boundary.js +11 -2
- package/neural-interface/lib/owner-authority.js +1 -1
- package/neural-interface/lib/path-confine.js +12 -9
- package/neural-interface/lib/peer-identity.js +32 -2
- package/neural-interface/lib/pty-host/core.js +28 -1
- package/neural-interface/lib/pty-host/lifetime.js +12 -0
- package/neural-interface/lib/pty-host/manager.js +12 -1
- package/neural-interface/lib/remote-policy.js +1 -1
- package/neural-interface/lib/request-gate.js +3 -0
- package/neural-interface/lib/rulesets/api.js +6 -5
- package/neural-interface/lib/rulesets/fs-safe.js +1 -1
- package/neural-interface/lib/rulesets/installer.js +89 -10
- package/neural-interface/lib/rulesets/render.js +1 -1
- package/neural-interface/lib/session-cache-builder.js +64 -1
- package/neural-interface/lib/skills/catalog.js +5 -1
- package/neural-interface/lib/skills/creation-brief.js +1 -1
- package/neural-interface/lib/skills/creation-sources.js +19 -1
- package/neural-interface/lib/skills/deploy.js +9 -4
- package/neural-interface/lib/skills/draft-confinement.js +3 -1
- package/neural-interface/lib/skills/eval-confinement.js +3 -1
- package/neural-interface/lib/skills/eval-trace.js +16 -0
- package/neural-interface/lib/skills/export.js +5 -2
- package/neural-interface/lib/skills/known-bridge-digests.json +58 -4
- package/neural-interface/lib/skills/ownership.js +1 -1
- package/neural-interface/lib/skills/roots.js +10 -3
- package/neural-interface/lib/skills/runtime-checks.js +6 -3
- package/neural-interface/lib/skills/service.js +15 -7
- package/neural-interface/lib/skills/telemetry.js +6 -0
- package/neural-interface/lib/synabun-version.js +1 -1
- package/neural-interface/lib/system-activity.js +1 -1
- package/neural-interface/lib/whatsapp/brain.js +7 -3
- package/neural-interface/lib/whatsapp/cards.js +22 -9
- package/neural-interface/lib/whatsapp/config.js +1 -1
- package/neural-interface/lib/worker-confinement.js +77 -1
- package/neural-interface/lib/worker-question.js +74 -0
- package/neural-interface/lib/worker-resume.js +13 -4
- package/neural-interface/lib/worker-sandbox.js +46 -16
- package/neural-interface/package-lock.json +18 -2
- package/neural-interface/package.json +2 -1
- package/neural-interface/public/onboarding.html +1 -1
- package/neural-interface/public/shared/agent-runtime-options.js +89 -11
- package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
- package/neural-interface/public/shared/agy/agy-client.js +266 -0
- package/neural-interface/public/shared/agy/agy-commands.js +120 -0
- package/neural-interface/public/shared/agy/agy-copy.js +326 -0
- package/neural-interface/public/shared/agy/agy-diff.js +161 -0
- package/neural-interface/public/shared/agy/agy-events.js +469 -0
- package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
- package/neural-interface/public/shared/agy/agy-options.js +232 -0
- package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
- package/neural-interface/public/shared/agy/agy-render.js +392 -0
- package/neural-interface/public/shared/agy/agy-setup.js +149 -0
- package/neural-interface/public/shared/agy/agy-styles.js +604 -0
- package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
- package/neural-interface/public/shared/api.js +4 -4
- package/neural-interface/public/shared/assistant/asst-agents-dock.js +3 -4
- package/neural-interface/public/shared/assistant/asst-api.js +1 -1
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +56 -9
- package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
- package/neural-interface/public/shared/assistant/asst-control.js +48 -0
- package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
- package/neural-interface/public/shared/assistant/asst-limit-dock.js +6 -1
- package/neural-interface/public/shared/assistant/asst-limit-wait.js +38 -5
- package/neural-interface/public/shared/assistant/asst-panel.js +45 -8
- package/neural-interface/public/shared/assistant/asst-render.js +51 -12
- package/neural-interface/public/shared/assistant/asst-route.js +237 -57
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
- package/neural-interface/public/shared/assistant/asst-scenes.js +54 -1
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +51 -7
- package/neural-interface/public/shared/assistant/asst-slash.js +8 -2
- package/neural-interface/public/shared/assistant/asst-state.js +76 -9
- package/neural-interface/public/shared/assistant/asst-styles.js +34 -0
- package/neural-interface/public/shared/assistant/asst-tool-kinds.js +34 -7
- package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
- package/neural-interface/public/shared/cdx/cdx-output.js +26 -3
- package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
- package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
- package/neural-interface/public/shared/cdx/cdx-protocol.js +11 -10
- package/neural-interface/public/shared/cdx/cdx-render.js +10 -9
- package/neural-interface/public/shared/cdx/cdx-requests.js +11 -1
- package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
- package/neural-interface/public/shared/cdx/cdx-tabs.js +75 -5
- package/neural-interface/public/shared/cli-install-controller.js +218 -0
- package/neural-interface/public/shared/cli-install-view.js +7 -4
- package/neural-interface/public/shared/cli-status.js +9 -8
- package/neural-interface/public/shared/constants.js +3 -2
- package/neural-interface/public/shared/context-menu-model.js +1 -0
- package/neural-interface/public/shared/cp/cp-restore.js +162 -0
- package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
- package/neural-interface/public/shared/desktop-bridge.js +17 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
- package/neural-interface/public/shared/desktop-titlebar.js +13 -4
- package/neural-interface/public/shared/desktop-update-view.js +1 -0
- package/neural-interface/public/shared/explorer/fx-api.js +4 -4
- package/neural-interface/public/shared/explorer/fx-links.js +3 -3
- package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
- package/neural-interface/public/shared/explorer/fx-main.js +48 -16
- package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
- package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
- package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
- package/neural-interface/public/shared/html-shell.js +21 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +27 -7
- package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +46 -3
- package/neural-interface/public/shared/provider-icons.js +4 -2
- package/neural-interface/public/shared/settings/settings-rules.js +2 -2
- package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +1 -1
- package/neural-interface/public/shared/settings/settings-skill-folders.js +2 -2
- package/neural-interface/public/shared/showcase-privacy.js +6 -1
- package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
- package/neural-interface/public/shared/skills/ss-creation.js +6 -3
- package/neural-interface/public/shared/skills/ss-dialog.js +5 -2
- package/neural-interface/public/shared/skills/ss-lint.js +9 -1
- package/neural-interface/public/shared/skills/ss-profiles.js +44 -6
- package/neural-interface/public/shared/skills/ss-review-view.js +2 -2
- package/neural-interface/public/shared/skills/studio/sx-actions.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-create-ai.js +2 -1
- package/neural-interface/public/shared/skills/studio/sx-dom.js +2 -1
- package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
- package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
- package/neural-interface/public/shared/skills/studio/sx-inspector.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-install-flow.js +9 -6
- package/neural-interface/public/shared/skills/studio/sx-install.js +20 -3
- package/neural-interface/public/shared/skills/studio/sx-library-view.js +144 -35
- package/neural-interface/public/shared/skills/studio/sx-library.js +6 -2
- package/neural-interface/public/shared/skills/studio/sx-main.js +11 -2
- package/neural-interface/public/shared/skills/studio/sx-review-view.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-subject.js +7 -6
- package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +6 -5
- package/neural-interface/public/shared/state.js +1 -1
- package/neural-interface/public/shared/streaming-markdown.js +58 -0
- package/neural-interface/public/shared/styles.css +354 -34
- package/neural-interface/public/shared/text-cache.js +29 -0
- package/neural-interface/public/shared/tool-list-picker.js +2 -2
- package/neural-interface/public/shared/tool-list-view.js +2 -2
- package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
- package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
- package/neural-interface/public/shared/ui-assistant.js +2 -0
- package/neural-interface/public/shared/ui-automation-studio.js +20 -12
- package/neural-interface/public/shared/ui-claude-panel.js +299 -77
- package/neural-interface/public/shared/ui-cli-banner.js +23 -167
- package/neural-interface/public/shared/ui-file-explorer.js +48 -6
- package/neural-interface/public/shared/ui-file-links.js +2 -2
- package/neural-interface/public/shared/ui-games.js +2 -2
- package/neural-interface/public/shared/ui-judgments.js +1 -1
- package/neural-interface/public/shared/ui-keybinds.js +8 -2
- package/neural-interface/public/shared/ui-link.js +3 -3
- package/neural-interface/public/shared/ui-menubar.js +2 -2
- package/neural-interface/public/shared/ui-native-loop-router.js +4 -0
- package/neural-interface/public/shared/ui-navbar.js +45 -0
- package/neural-interface/public/shared/ui-notifications.js +1 -0
- package/neural-interface/public/shared/ui-resume.js +9 -7
- package/neural-interface/public/shared/ui-schedules-studio.js +41 -10
- package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
- package/neural-interface/public/shared/ui-sessions.js +22 -12
- package/neural-interface/public/shared/ui-settings.js +90 -65
- package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -3
- package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
- package/neural-interface/public/shared/ui-sidepanel-windows.js +3 -3
- package/neural-interface/public/shared/ui-terminal.js +146 -10
- package/neural-interface/public/shared/ui-tool-updates.js +3 -3
- package/neural-interface/public/shared/ui-tooltip.js +71 -46
- package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
- package/neural-interface/public/shared/ui-whiteboard.js +1 -1
- package/neural-interface/public/shared/ui-worker-end.js +1 -1
- package/neural-interface/public/shared/whatsapp/wa-view.js +2 -2
- package/neural-interface/public/shared/worker-questions.js +34 -0
- package/neural-interface/public/sidepanel-session.html +1 -1
- package/neural-interface/server.js +644 -426
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/neural-interface/templates/rulesets/legacy-hashes.json +5 -5
- package/neural-interface/templates/rulesets/manifest.json +1 -1
- package/npm-shrinkwrap.json +18 -2
- package/package.json +2 -1
- package/skills/skill-forge/SKILL.md +4 -4
- package/skills/synabun/SKILL.md +1 -1
- package/skills/synabun/modules/schedule.md +3 -3
- /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
import { assertSafeAntigravityProject } from './home.js';
|
|
2
|
+
// ═══════════════════════════════════════════
|
|
3
|
+
// SynaBun — the Antigravity side panel's server end (/ws/antigravity)
|
|
4
|
+
// ═══════════════════════════════════════════
|
|
5
|
+
//
|
|
6
|
+
// One socket per panel window, one `agy acp` process per socket (started on the
|
|
7
|
+
// socket's `hello`, again after it ended or on `agent:restart`), one ACP session per
|
|
8
|
+
// tab. The page asks with `{ type, reqId, ... }` and gets
|
|
9
|
+
// `{ type: 'result', reqId, ok, data | error, authRequired }` (an error carries a string
|
|
10
|
+
// `code`); what the agent says on its own comes as events:
|
|
11
|
+
//
|
|
12
|
+
// session:update { sessionId, update, replay } an ACP session/update, as sent
|
|
13
|
+
// permission:request { requestId, sessionId, toolCall, options }
|
|
14
|
+
// permission:resolved { requestId, outcome } answered, or cancelled by a turn's end
|
|
15
|
+
// elicitation:request { requestId, sessionId, toolCallId, message, mode, requestedSchema?, url?, elicitationId? }
|
|
16
|
+
// the agent asks the owner for input: a form (`requestedSchema`), a page to open
|
|
17
|
+
// (`url`: https, or http on this computer; any other is declined and never shown),
|
|
18
|
+
// or a mode the page may not know (it can still decline or cancel it)
|
|
19
|
+
// elicitation:resolved { requestId, sessionId, action } answered (accept / decline / cancel); 'complete' when
|
|
20
|
+
// the agent says an accepted page is done (elicitation/complete); 'cancel' when the turn
|
|
21
|
+
// that asked ends, after 30 minutes, when the session closes or the agent ends
|
|
22
|
+
// terminal:output { sessionId, terminalId, output, truncated, exitStatus } a terminal the agent runs
|
|
23
|
+
// through SynaBun: its whole current output (bounded) at most every 250 ms while it grows,
|
|
24
|
+
// and once when it ends (`exitStatus` { exitCode, signal }, null while it runs); a tool
|
|
25
|
+
// call whose content is { type: 'terminal', terminalId } shows it
|
|
26
|
+
// turn:end { sessionId, stopReason, usage } after every prompt, also failed ones
|
|
27
|
+
// agent:exit { code, signal, error, stderr } the process ended (not after agent:restart)
|
|
28
|
+
//
|
|
29
|
+
// Requests: hello, agent:restart, session:new, session:load, session:resume, session:fork,
|
|
30
|
+
// session:delete, session:list, session:close, prompt, cancel, config:set, mode:set, model:set,
|
|
31
|
+
// permission:answer, elicitation:answer, auth:start, auth:logout, session:bypass (a tab's Bypass: lib/antigravity/bypass.js;
|
|
32
|
+
// every request of that session is answered allow-once and reported as permission:auto { requestId, sessionId,
|
|
33
|
+
// toolCall, decision: 'allowed' | 'refused', rule?, reason? }; the hard rails still refuse). `capabilities` in the hello
|
|
34
|
+
// answer lists what this server does; the page offers a feature only while it is listed (public/
|
|
35
|
+
// reloads at any time, lib/ only with a restart), and resume, fork, delete, other folders and
|
|
36
|
+
// sign-out only while the agent's own `agentCapabilities` say it does them too (otherwise they
|
|
37
|
+
// answer UNSUPPORTED, and other folders are left out of what the agent is sent).
|
|
38
|
+
//
|
|
39
|
+
// What the agent is offered (ACP clientCapabilities): forms and pages to open (elicitation),
|
|
40
|
+
// sign-in methods that run in a terminal, compaction, boolean settings, notices, plan updates and
|
|
41
|
+
// subagent sessions always; the owner's files and terminals only when the page's hello (or
|
|
42
|
+
// agent:restart) asks for them with `clientCapabilities: { fs, terminal }` (both off by default),
|
|
43
|
+
// answered by lib/antigravity/acp-client-host.js. The socket keeps those switches; a running agent
|
|
44
|
+
// keeps what it was started with until agent:restart.
|
|
45
|
+
//
|
|
46
|
+
// What the page sends is never trusted: a cwd must be an absolute folder that exists
|
|
47
|
+
// (`validateCwd` may narrow it further), and so must every other folder of a session (at most 8);
|
|
48
|
+
// attachments are bounded in number, size and type, a path to attach must be an absolute regular
|
|
49
|
+
// file, a permission answer must name one of the options the agent offered for that very request,
|
|
50
|
+
// and an elicitation answer is a flat object of bounded strings, numbers, booleans and string lists.
|
|
51
|
+
//
|
|
52
|
+
// A prompt is also a person's message (`onHumanPrompt`: the greeting notes the activity), may be `/synabun …`
|
|
53
|
+
// (`expandPrompt` turns it into the bundled skill's instructions, as the Codex panel does: capability
|
|
54
|
+
// 'synabun-command') and may carry `greetingId`, the greeting the tab opened with (capability 'greeting'):
|
|
55
|
+
// `greetingNote` gives its note once, sent before the typed words between the markers of
|
|
56
|
+
// public/shared/agy/agy-greeting.js.
|
|
57
|
+
|
|
58
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
59
|
+
import { lstatSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
60
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from 'node:path';
|
|
61
|
+
import { pathToFileURL } from 'node:url';
|
|
62
|
+
import { createAcpClientHost } from './acp-client-host.js';
|
|
63
|
+
import { acpErrorInfo, createAcpConnection, isAuthRequired } from './acp-connection.js';
|
|
64
|
+
import { allowOnceOption, bypassRail, rejectOnceOption } from './bypass.js';
|
|
65
|
+
import { greetingNoteBlock } from '../../public/shared/agy/agy-greeting.js';
|
|
66
|
+
|
|
67
|
+
export const ANTIGRAVITY_WS_PATH = '/ws/antigravity';
|
|
68
|
+
export const ANTIGRAVITY_PANEL_CAPABILITIES = Object.freeze([
|
|
69
|
+
'session-new', 'session-load', 'session-list', 'session-close', 'prompt', 'cancel',
|
|
70
|
+
'config-options', 'modes', 'model-set', 'permissions', 'attachments', 'attach-paths', 'auth', 'usage',
|
|
71
|
+
'session-resume', 'session-fork', 'session-delete', 'additional-directories', 'agent-restart',
|
|
72
|
+
'client-fs', 'client-terminal', 'elicitation', 'audio', 'logout', 'bypass', 'greeting', 'synabun-command', 'run-homes',
|
|
73
|
+
]);
|
|
74
|
+
export const ANTIGRAVITY_LIMITS = Object.freeze({
|
|
75
|
+
maxAttachments: 8,
|
|
76
|
+
maxAttachmentBytes: 15 * 1024 * 1024,
|
|
77
|
+
maxPromptChars: 200_000,
|
|
78
|
+
maxPaths: 20,
|
|
79
|
+
maxAdditionalDirectories: 8,
|
|
80
|
+
permissionTimeoutMs: 30 * 60_000,
|
|
81
|
+
elicitationTimeoutMs: 30 * 60_000,
|
|
82
|
+
authTimeoutMs: 10 * 60_000,
|
|
83
|
+
maxElicitationFields: 50,
|
|
84
|
+
maxElicitationText: 10_000,
|
|
85
|
+
maxElicitationItems: 50,
|
|
86
|
+
maxElicitationMessage: 20_000,
|
|
87
|
+
});
|
|
88
|
+
const IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
|
|
89
|
+
const ATTACHABLE_TYPES = /^(image\/(png|jpeg|gif|webp)|application\/pdf|text\/[a-z0-9.+-]+|application\/(json|xml|x-yaml|yaml)|audio\/(wav|x-wav|mpeg|mp4|aac|flac|ogg|opus|webm))$/;
|
|
90
|
+
const ID_RE = /^[A-Za-z0-9._:@-]{1,200}$/;
|
|
91
|
+
const CAPABILITY_KEY_RE = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
|
|
92
|
+
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
93
|
+
const ELICITATION_ACTIONS = new Set(['accept', 'decline', 'cancel']);
|
|
94
|
+
|
|
95
|
+
/** A safe file name for an attachment: no folders, no control characters, an extension kept. */
|
|
96
|
+
export function safeAttachmentName(name, mimeType = '') {
|
|
97
|
+
const raw = basename(String(name || '').replace(/\\/g, '/')).replace(/[\u0000-\u001f<>:"|?*]/g, '').trim();
|
|
98
|
+
const fallbackExt = mimeType === 'image/jpeg' ? '.jpg' : mimeType.startsWith('image/') ? `.${mimeType.slice(6)}` : '';
|
|
99
|
+
const cleaned = raw.replace(/^\.+/, '').slice(-120) || `attachment${fallbackExt || '.bin'}`;
|
|
100
|
+
return extname(cleaned) ? cleaned : `${cleaned}${fallbackExt}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Turns the page's prompt (text, pasted files, paths) into ACP content blocks. Throws on what it refuses. */
|
|
104
|
+
export function buildPromptBlocks({ text = '', attachments = [], paths = [] } = {}, {
|
|
105
|
+
promptCapabilities = {},
|
|
106
|
+
saveAttachment,
|
|
107
|
+
limits = ANTIGRAVITY_LIMITS,
|
|
108
|
+
statImpl = statSync,
|
|
109
|
+
} = {}) {
|
|
110
|
+
const blocks = [];
|
|
111
|
+
const body = typeof text === 'string' ? text : '';
|
|
112
|
+
if (body.length > limits.maxPromptChars) throw Object.assign(new Error(`The message is longer than ${limits.maxPromptChars} characters.`), { code: 'PROMPT_TOO_LONG' });
|
|
113
|
+
if (body.trim()) blocks.push({ type: 'text', text: body });
|
|
114
|
+
const files = Array.isArray(attachments) ? attachments : [];
|
|
115
|
+
const links = Array.isArray(paths) ? paths : [];
|
|
116
|
+
if (files.length > limits.maxAttachments) throw Object.assign(new Error(`At most ${limits.maxAttachments} attachments per message.`), { code: 'TOO_MANY_ATTACHMENTS' });
|
|
117
|
+
if (links.length > limits.maxPaths) throw Object.assign(new Error(`At most ${limits.maxPaths} files per message.`), { code: 'TOO_MANY_PATHS' });
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
const mimeType = String(file?.mimeType || '').toLowerCase();
|
|
120
|
+
if (!ATTACHABLE_TYPES.test(mimeType)) throw Object.assign(new Error(`Antigravity cannot take a ${mimeType || 'file of unknown type'} attachment.`), { code: 'ATTACHMENT_TYPE' });
|
|
121
|
+
const data = typeof file?.data === 'string' ? file.data : '';
|
|
122
|
+
if (!data || !/^[A-Za-z0-9+/]+={0,2}$/.test(data)) throw Object.assign(new Error('An attachment arrived empty or damaged.'), { code: 'ATTACHMENT_DATA' });
|
|
123
|
+
const size = Math.floor(data.length * 3 / 4) - (data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0);
|
|
124
|
+
if (size > limits.maxAttachmentBytes) throw Object.assign(new Error(`An attachment is larger than ${Math.round(limits.maxAttachmentBytes / 1048576)} MB.`), { code: 'ATTACHMENT_SIZE' });
|
|
125
|
+
const name = safeAttachmentName(file?.name, mimeType);
|
|
126
|
+
if (IMAGE_TYPES.has(mimeType) && promptCapabilities?.image) {
|
|
127
|
+
blocks.push({ type: 'image', mimeType, data });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (mimeType.startsWith('audio/') && promptCapabilities?.audio) {
|
|
131
|
+
blocks.push({ type: 'audio', mimeType, data });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (typeof saveAttachment !== 'function') throw Object.assign(new Error('Attachments are not available.'), { code: 'ATTACHMENT_UNAVAILABLE' });
|
|
135
|
+
const saved = saveAttachment(name, Buffer.from(data, 'base64'));
|
|
136
|
+
blocks.push({ type: 'resource_link', uri: pathToFileURL(saved).href, name, mimeType, size });
|
|
137
|
+
}
|
|
138
|
+
for (const entry of links) {
|
|
139
|
+
const path = typeof entry === 'string' ? entry : '';
|
|
140
|
+
if (!path || !isAbsolute(path)) throw Object.assign(new Error('A file to attach must be an absolute path.'), { code: 'PATH_INVALID' });
|
|
141
|
+
let stat;
|
|
142
|
+
try { stat = statImpl(path); } catch { throw Object.assign(new Error(`No such file: ${path}`), { code: 'PATH_MISSING' }); }
|
|
143
|
+
if (!stat.isFile() && !stat.isDirectory()) throw Object.assign(new Error(`Not a file or folder: ${path}`), { code: 'PATH_INVALID' });
|
|
144
|
+
blocks.push({ type: 'resource_link', uri: pathToFileURL(path).href, name: basename(path) || path, ...(stat.isFile() ? { size: stat.size } : {}) });
|
|
145
|
+
}
|
|
146
|
+
if (!blocks.length) throw Object.assign(new Error('Nothing to send.'), { code: 'EMPTY_PROMPT' });
|
|
147
|
+
return blocks;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The absolute, existing folder a session may run in, by its real path. */
|
|
151
|
+
export function defaultValidateCwd(cwd) {
|
|
152
|
+
if (typeof cwd !== 'string' || !cwd || !isAbsolute(cwd)) throw Object.assign(new Error('Choose a project folder first.'), { code: 'CWD_INVALID' });
|
|
153
|
+
let real;
|
|
154
|
+
try { real = realpathSync(cwd); } catch { throw Object.assign(new Error(`The folder ${cwd} does not exist.`), { code: 'CWD_MISSING' }); }
|
|
155
|
+
if (!statSync(real).isDirectory()) throw Object.assign(new Error(`${cwd} is not a folder.`), { code: 'CWD_INVALID' });
|
|
156
|
+
return real;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const RUN_HOME_NAME_RE = /^run-[A-Za-z0-9_-]+$/;
|
|
160
|
+
export const RUN_HOME_PRUNED_MESSAGE = 'This dispatched run\x27s conversation folder was already cleaned up, so the conversation can no longer be continued here. Start a new tab instead.';
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validates a `geminiDir` sent by the page for a continued dispatched-run tab: null for an ordinary tab
|
|
164
|
+
* (default ~/.gemini), else the real path of an existing SynaBun per-run home directly under `homesRoot`.
|
|
165
|
+
* Never touches paths outside `homesRoot`; throws HOME_INVALID on any path outside it or a symlink, and
|
|
166
|
+
* HOME_PRUNED when a valid run home folder was already cleaned up.
|
|
167
|
+
*/
|
|
168
|
+
export function validateRunHome(geminiDir, homesRoot) {
|
|
169
|
+
if (geminiDir === undefined || geminiDir === null || geminiDir === '') return null;
|
|
170
|
+
const invalid = () => Object.assign(new Error('That Antigravity conversation folder is outside SynaBun\x27s run homes.'), { code: 'HOME_INVALID' });
|
|
171
|
+
if (typeof geminiDir !== 'string' || !isAbsolute(geminiDir)) throw invalid();
|
|
172
|
+
const rawRoot = typeof homesRoot === 'function' ? homesRoot() : homesRoot;
|
|
173
|
+
if (typeof rawRoot !== 'string' || !rawRoot || !isAbsolute(rawRoot)) throw invalid();
|
|
174
|
+
const resolvedRoot = resolve(rawRoot);
|
|
175
|
+
let canonRoot = resolvedRoot;
|
|
176
|
+
try { canonRoot = realpathSync(resolvedRoot); } catch {
|
|
177
|
+
const rootParent = dirname(resolvedRoot);
|
|
178
|
+
if (rootParent && rootParent !== resolvedRoot) {
|
|
179
|
+
try { canonRoot = join(realpathSync(rootParent), basename(resolvedRoot)); } catch { /* keep resolvedRoot */ }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const candidate = resolve(geminiDir);
|
|
183
|
+
const name = basename(candidate);
|
|
184
|
+
if (!RUN_HOME_NAME_RE.test(name)) throw invalid();
|
|
185
|
+
const allowedParents = new Set([resolvedRoot, canonRoot]);
|
|
186
|
+
if (canonRoot.startsWith('/private/')) allowedParents.add(canonRoot.slice('/private'.length));
|
|
187
|
+
if (!allowedParents.has(dirname(candidate))) throw invalid();
|
|
188
|
+
const target = join(canonRoot, name);
|
|
189
|
+
let stat;
|
|
190
|
+
try {
|
|
191
|
+
stat = lstatSync(target);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error?.code === 'ENOENT') throw Object.assign(new Error(RUN_HOME_PRUNED_MESSAGE), { code: 'HOME_PRUNED' });
|
|
194
|
+
throw invalid();
|
|
195
|
+
}
|
|
196
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw invalid();
|
|
197
|
+
let realTarget;
|
|
198
|
+
let realRoot;
|
|
199
|
+
try {
|
|
200
|
+
realTarget = realpathSync(target);
|
|
201
|
+
realRoot = realpathSync(canonRoot);
|
|
202
|
+
} catch {
|
|
203
|
+
throw invalid();
|
|
204
|
+
}
|
|
205
|
+
if (dirname(realTarget) !== realRoot || basename(realTarget) !== name) throw invalid();
|
|
206
|
+
return realTarget;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The page's switches ({ fs, terminal }), read strictly: only `true` turns one on. */
|
|
210
|
+
export function clientSwitches(value) {
|
|
211
|
+
return { fs: value?.fs === true, terminal: value?.terminal === true };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** What the panel offers the agent at `initialize` (ACP clientCapabilities), for the page's switches. */
|
|
215
|
+
export function acpClientCapabilities(switches = {}) {
|
|
216
|
+
const { fs, terminal } = clientSwitches(switches);
|
|
217
|
+
return {
|
|
218
|
+
fs: { readTextFile: fs, writeTextFile: fs },
|
|
219
|
+
terminal,
|
|
220
|
+
auth: { terminal: true },
|
|
221
|
+
elicitation: { form: {}, url: {} },
|
|
222
|
+
session: { compaction: {}, configOptions: { boolean: {} }, notices: {} },
|
|
223
|
+
plan: {},
|
|
224
|
+
subagents: {},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// The names of what an agent advertises in one capability object ({} or true), each as true.
|
|
229
|
+
function advertised(value) {
|
|
230
|
+
const out = {};
|
|
231
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return out;
|
|
232
|
+
for (const [key, entry] of Object.entries(value).slice(0, 64)) {
|
|
233
|
+
if (key === '_meta' || !CAPABILITY_KEY_RE.test(key)) continue;
|
|
234
|
+
if (entry === true || (entry && typeof entry === 'object')) out[key] = true;
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** A plain copy of the agent's capabilities for the page: flags only, nothing else it sent. */
|
|
240
|
+
export function agentCapabilitiesView(capabilities) {
|
|
241
|
+
const caps = capabilities && typeof capabilities === 'object' ? capabilities : {};
|
|
242
|
+
const flags = (value, keys) => Object.fromEntries(keys.map((key) => [key, value?.[key] === true]));
|
|
243
|
+
return {
|
|
244
|
+
loadSession: caps.loadSession === true,
|
|
245
|
+
promptCapabilities: flags(caps.promptCapabilities, ['image', 'audio', 'embeddedContext']),
|
|
246
|
+
mcpCapabilities: flags(caps.mcpCapabilities, ['http', 'sse', 'acp']),
|
|
247
|
+
auth: advertised(caps.auth),
|
|
248
|
+
sessionCapabilities: advertised(caps.sessionCapabilities || caps.session_capabilities),
|
|
249
|
+
providers: !!(caps.providers && typeof caps.providers === 'object'),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The agent's sign-in methods for the page: id, name, description, type ('agent' when it names
|
|
255
|
+
* none), and for a method that runs in a terminal its `args` and the names of its `env` (never
|
|
256
|
+
* their values).
|
|
257
|
+
*/
|
|
258
|
+
export function authMethodsView(methods) {
|
|
259
|
+
const out = [];
|
|
260
|
+
for (const method of Array.isArray(methods) ? methods.slice(0, 32) : []) {
|
|
261
|
+
if (!method || typeof method !== 'object' || typeof method.id !== 'string' || !method.id) continue;
|
|
262
|
+
const view = {
|
|
263
|
+
id: method.id,
|
|
264
|
+
name: typeof method.name === 'string' && method.name ? method.name : method.id,
|
|
265
|
+
description: typeof method.description === 'string' ? method.description : null,
|
|
266
|
+
type: typeof method.type === 'string' && method.type ? method.type : 'agent',
|
|
267
|
+
};
|
|
268
|
+
if (Array.isArray(method.args)) view.args = method.args.filter((arg) => typeof arg === 'string').slice(0, 50).map((arg) => arg.slice(0, 1000));
|
|
269
|
+
if (method.env && typeof method.env === 'object' && !Array.isArray(method.env)) view.env = Object.keys(method.env).slice(0, 50);
|
|
270
|
+
out.push(view);
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** A page the agent asks the owner to open: https anywhere, http on this computer only; null otherwise. */
|
|
276
|
+
export function safeElicitationUrl(value) {
|
|
277
|
+
if (typeof value !== 'string' || !value || value.length > 8192) return null;
|
|
278
|
+
let url;
|
|
279
|
+
try { url = new URL(value); } catch { return null; }
|
|
280
|
+
if (url.protocol === 'https:' || (url.protocol === 'http:' && LOCAL_HOSTS.has(url.hostname))) return url.href;
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* What the page answers an elicitation with (`accept` only): a plain object of at most 50 fields,
|
|
286
|
+
* each a string (≤ 10,000 characters), a number, a boolean or a list of at most 50 such strings.
|
|
287
|
+
* Undefined when there is none; throws ELICITATION_CONTENT_INVALID on anything else.
|
|
288
|
+
*/
|
|
289
|
+
export function elicitationContent(content, limits = ANTIGRAVITY_LIMITS) {
|
|
290
|
+
if (content === undefined || content === null) return undefined;
|
|
291
|
+
const refuse = (why) => Object.assign(new Error(`The answer cannot be sent: ${why}.`), { code: 'ELICITATION_CONTENT_INVALID' });
|
|
292
|
+
if (typeof content !== 'object' || Array.isArray(content)) throw refuse('it is not a set of fields');
|
|
293
|
+
const proto = Object.getPrototypeOf(content);
|
|
294
|
+
if (proto !== Object.prototype && proto !== null) throw refuse('it is not a set of fields');
|
|
295
|
+
const keys = Object.keys(content);
|
|
296
|
+
if (keys.length > limits.maxElicitationFields) throw refuse(`more than ${limits.maxElicitationFields} fields`);
|
|
297
|
+
const entries = [];
|
|
298
|
+
for (const key of keys) {
|
|
299
|
+
if (!key || key.length > 200) throw refuse('a field name is empty or too long');
|
|
300
|
+
const value = content[key];
|
|
301
|
+
if (typeof value === 'string') {
|
|
302
|
+
if (value.length > limits.maxElicitationText) throw refuse(`${key} is longer than ${limits.maxElicitationText} characters`);
|
|
303
|
+
entries.push([key, value]);
|
|
304
|
+
} else if (typeof value === 'number') {
|
|
305
|
+
if (!Number.isFinite(value)) throw refuse(`${key} is not a number`);
|
|
306
|
+
entries.push([key, value]);
|
|
307
|
+
} else if (typeof value === 'boolean') {
|
|
308
|
+
entries.push([key, value]);
|
|
309
|
+
} else if (Array.isArray(value)) {
|
|
310
|
+
if (value.length > limits.maxElicitationItems) throw refuse(`${key} has more than ${limits.maxElicitationItems} choices`);
|
|
311
|
+
if (value.some((item) => typeof item !== 'string' || item.length > limits.maxElicitationText)) throw refuse(`${key} is not a list of choices`);
|
|
312
|
+
entries.push([key, [...value]]);
|
|
313
|
+
} else {
|
|
314
|
+
throw refuse(`${key} is not text, a number, a yes / no or a list of choices`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Built from its entries: a field named __proto__ stays a field.
|
|
318
|
+
return Object.fromEntries(entries);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* `resolveLaunch()`: { command, args, version? } for `agy`, or null when it is not installed.
|
|
323
|
+
* `childEnv()`: the agent process's environment (and the base of every command it runs through the
|
|
324
|
+
* panel). `attachmentsRoot`: a folder of SynaBun's own (under the data home) where attachments are
|
|
325
|
+
* written for the agent to read. `connect` and `createClientHost` are there for tests.
|
|
326
|
+
*/
|
|
327
|
+
// What a process that ended before (or while) it answered `initialize` said, as one error the page can show:
|
|
328
|
+
// the last lines of its stderr (already redacted by the connection), and ACP_UNSUPPORTED for an agy without
|
|
329
|
+
// the `acp` subcommand (1.4.6 answers 'Error: unexpected argument "acp".' and exits 2).
|
|
330
|
+
const STDERR_LINES = 4;
|
|
331
|
+
const STDERR_CHARS = 600;
|
|
332
|
+
export function stderrSummary(stderr) {
|
|
333
|
+
const lines = String(stderr || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
334
|
+
const tail = lines.slice(-STDERR_LINES).join('\n');
|
|
335
|
+
return tail.length > STDERR_CHARS ? `…${tail.slice(-STDERR_CHARS)}` : tail;
|
|
336
|
+
}
|
|
337
|
+
export function agentStartError(error, exit) {
|
|
338
|
+
const stderr = stderrSummary(exit?.stderr || error?.stderr);
|
|
339
|
+
if (/unexpected argument\W+acp\b|unknown (sub)?command\W+acp\b/i.test(stderr)) {
|
|
340
|
+
return Object.assign(new Error('This Antigravity CLI has no ACP server (`agy acp`), which the panel needs. Update it (`agy update`, or `brew upgrade --cask antigravity-cli`), then try again.'), { code: 'ACP_UNSUPPORTED', stderr });
|
|
341
|
+
}
|
|
342
|
+
if (!stderr) return error;
|
|
343
|
+
const base = exit && (exit.code !== null || exit.signal) && !exit.error ? `Antigravity exited (${exit.signal || exit.code})` : (error?.message || 'Antigravity failed to start');
|
|
344
|
+
return Object.assign(new Error(`${base}: ${stderr}`), { code: error?.code, stderr });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function createAntigravityPanelBridge({
|
|
348
|
+
resolveLaunch,
|
|
349
|
+
childEnv = () => process.env,
|
|
350
|
+
connect = createAcpConnection,
|
|
351
|
+
createClientHost = createAcpClientHost,
|
|
352
|
+
attachmentsRoot,
|
|
353
|
+
homesRoot = null,
|
|
354
|
+
isRunHomeActive = null,
|
|
355
|
+
validateCwd = defaultValidateCwd,
|
|
356
|
+
clientInfo = { name: 'synabun', title: 'SynaBun', version: '0' },
|
|
357
|
+
limits = ANTIGRAVITY_LIMITS,
|
|
358
|
+
log = () => {},
|
|
359
|
+
policyRoots = () => [],
|
|
360
|
+
onSessionActivity = null,
|
|
361
|
+
onHumanPrompt = null, // ({ cwd, words }): a person sent a message (lib/greeting noteActivity)
|
|
362
|
+
greetingNote = null, // (greetingId, { skip }) => the note to tell the model once, or ''
|
|
363
|
+
expandPrompt = null, // (text) => the text sent (`/synabun …` → the bundled skill), or the same text
|
|
364
|
+
onTurnEnd = null, // ({ sessionId, cwd }): a prompt ended (its transcript may have changed)
|
|
365
|
+
} = {}) {
|
|
366
|
+
const live = new Set();
|
|
367
|
+
const stopping = new Set();
|
|
368
|
+
const attachmentName = (id) => {
|
|
369
|
+
const safe = id.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
370
|
+
return safe && safe !== '.' && safe !== '..' && safe === id ? safe : `session-${createHash('sha256').update(id).digest('hex')}`;
|
|
371
|
+
};
|
|
372
|
+
function removeAttachments(id) {
|
|
373
|
+
if (!attachmentsRoot) return;
|
|
374
|
+
try { rmSync(join(attachmentsRoot, attachmentName(id)), { recursive: true, force: true }); } catch { /* next prune retries */ }
|
|
375
|
+
}
|
|
376
|
+
function pruneAttachments() {
|
|
377
|
+
if (!attachmentsRoot) return;
|
|
378
|
+
const keep = new Set();
|
|
379
|
+
for (const state of live) for (const id of state.sessionIds()) keep.add(attachmentName(id));
|
|
380
|
+
try {
|
|
381
|
+
for (const item of readdirSync(attachmentsRoot, { withFileTypes: true })) {
|
|
382
|
+
if (!item.isDirectory() || keep.has(item.name)) continue;
|
|
383
|
+
const dir = join(attachmentsRoot, item.name);
|
|
384
|
+
if (Date.now() - lstatSync(dir).mtimeMs > 7 * 86400000) rmSync(dir, { recursive: true, force: true });
|
|
385
|
+
}
|
|
386
|
+
} catch { /* a missing or busy attachments folder is retried on the next save */ }
|
|
387
|
+
}
|
|
388
|
+
pruneAttachments();
|
|
389
|
+
|
|
390
|
+
function handleSocket(ws) {
|
|
391
|
+
let closed = false;
|
|
392
|
+
let wanted = clientSwitches(null); // the page's switches: what the next agent is offered
|
|
393
|
+
let restarts = Promise.resolve();
|
|
394
|
+
const agents = new Map(); // homeKey ('' default, realHome for run tabs) -> { key, home, conn, starting, init, offered }
|
|
395
|
+
const slotOf = (home = null) => {
|
|
396
|
+
const key = home || '';
|
|
397
|
+
let slot = agents.get(key);
|
|
398
|
+
if (!slot) {
|
|
399
|
+
slot = { key, home: home || null, conn: null, starting: null, init: null, offered: null };
|
|
400
|
+
agents.set(key, slot);
|
|
401
|
+
}
|
|
402
|
+
return slot;
|
|
403
|
+
};
|
|
404
|
+
const defaultSlot = slotOf(null);
|
|
405
|
+
const sessions = new Map(); // sessionId -> { cwd, home, tabId, turn: null | { id }, additionalDirectories }
|
|
406
|
+
const permissions = new Map(); // requestId -> { sessionId, options, resolve, timer }
|
|
407
|
+
const elicitations = new Map(); // requestId -> { sessionId, mode, elicitationId, resolve, timer, done }
|
|
408
|
+
const completions = new Map(); // elicitationId -> { requestId, sessionId }: an accepted page, until it is done
|
|
409
|
+
const subagents = new Map(); // child sessionId -> { parent, cwd }: what subagent_update said
|
|
410
|
+
const send = (message) => {
|
|
411
|
+
if (closed || ws.readyState !== 1) return;
|
|
412
|
+
try { ws.send(JSON.stringify(message)); } catch { /* the close handler cleans up */ }
|
|
413
|
+
};
|
|
414
|
+
const clientHost = createClientHost({
|
|
415
|
+
childEnv,
|
|
416
|
+
// A command run for the agent is SynaBun's child, not the agent's: this says whose it is.
|
|
417
|
+
terminalEnv: { SYNABUN_AGENT_TERMINAL: 'antigravity' },
|
|
418
|
+
sessionCwd: (id) => sessionCwd(id),
|
|
419
|
+
onOutput: (snapshot) => send({ type: 'terminal:output', ...snapshot }),
|
|
420
|
+
log: (line) => log(`[antigravity] ${line}`),
|
|
421
|
+
});
|
|
422
|
+
let shutdownPromise = null;
|
|
423
|
+
const state = {
|
|
424
|
+
conn: () => defaultSlot.conn,
|
|
425
|
+
sessionIds: () => sessions.keys(),
|
|
426
|
+
homes: () => {
|
|
427
|
+
const names = new Set();
|
|
428
|
+
for (const slot of agents.values()) if (slot.home && (slot.conn?.running || slot.starting)) names.add(basename(slot.home));
|
|
429
|
+
for (const session of sessions.values()) if (session.home) names.add(basename(session.home));
|
|
430
|
+
return names;
|
|
431
|
+
},
|
|
432
|
+
// Turns that are running on this socket's agents (the desktop updater waits for them: GET /api/system/activity).
|
|
433
|
+
turns: () => { let count = 0; for (const session of sessions.values()) if (session.turn) count += 1; return count; },
|
|
434
|
+
shutdown: () => {
|
|
435
|
+
if (shutdownPromise) return shutdownPromise;
|
|
436
|
+
closed = true;
|
|
437
|
+
live.delete(state);
|
|
438
|
+
shutdownPromise = stopAgent().finally(() => stopping.delete(shutdownPromise));
|
|
439
|
+
stopping.add(shutdownPromise);
|
|
440
|
+
return shutdownPromise;
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
live.add(state);
|
|
444
|
+
|
|
445
|
+
function settlePermission(requestId, outcome) {
|
|
446
|
+
const entry = permissions.get(requestId);
|
|
447
|
+
if (!entry) return false;
|
|
448
|
+
permissions.delete(requestId);
|
|
449
|
+
clearTimeout(entry.timer);
|
|
450
|
+
entry.resolve({ outcome });
|
|
451
|
+
send({ type: 'permission:resolved', requestId, sessionId: entry.sessionId, outcome });
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
function cancelPermissions(sessionId = null) {
|
|
455
|
+
for (const [requestId, entry] of [...permissions]) {
|
|
456
|
+
if (sessionId === null || entry.sessionId === sessionId) settlePermission(requestId, { outcome: 'cancelled' });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// `shown`: what the page is told, when it differs from what the agent is answered.
|
|
461
|
+
function settleElicitation(requestId, answer, shown = answer.action) {
|
|
462
|
+
const entry = elicitations.get(requestId);
|
|
463
|
+
if (!entry) return false;
|
|
464
|
+
elicitations.delete(requestId);
|
|
465
|
+
clearTimeout(entry.timer);
|
|
466
|
+
entry.done?.();
|
|
467
|
+
if (answer.action === 'accept' && entry.mode === 'url' && entry.elicitationId) {
|
|
468
|
+
completions.delete(entry.elicitationId);
|
|
469
|
+
completions.set(entry.elicitationId, { requestId, sessionId: entry.sessionId });
|
|
470
|
+
if (completions.size > 64) completions.delete(completions.keys().next().value);
|
|
471
|
+
}
|
|
472
|
+
entry.resolve(answer);
|
|
473
|
+
send({ type: 'elicitation:resolved', requestId, sessionId: entry.sessionId, action: shown });
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
function cancelElicitations(sessionId = null) {
|
|
477
|
+
for (const [requestId, entry] of [...elicitations]) {
|
|
478
|
+
if (sessionId === null || entry.sessionId === sessionId) settleElicitation(requestId, { action: 'cancel' });
|
|
479
|
+
}
|
|
480
|
+
for (const [elicitationId, entry] of [...completions]) {
|
|
481
|
+
if (sessionId === null || entry.sessionId === sessionId) completions.delete(elicitationId);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function askElicitation(params, { signal } = {}) {
|
|
485
|
+
if (signal?.aborted) return { action: 'cancel' };
|
|
486
|
+
const sessionId = typeof params?.sessionId === 'string' ? params.sessionId : null;
|
|
487
|
+
const mode = typeof params?.mode === 'string' ? params.mode.slice(0, 100) : '';
|
|
488
|
+
const url = mode === 'url' ? safeElicitationUrl(params?.url) : null;
|
|
489
|
+
// A page that is not https (or http on this computer) is never put in front of the owner.
|
|
490
|
+
if (mode === 'url' && !url) return { action: 'decline' };
|
|
491
|
+
const elicitationId = typeof params?.elicitationId === 'string' && params.elicitationId ? params.elicitationId.slice(0, 500) : null;
|
|
492
|
+
return new Promise((resolve) => {
|
|
493
|
+
const requestId = randomBytes(8).toString('hex');
|
|
494
|
+
const timer = setTimeout(() => settleElicitation(requestId, { action: 'cancel' }), limits.elicitationTimeoutMs);
|
|
495
|
+
timer.unref?.();
|
|
496
|
+
const onAbort = () => settleElicitation(requestId, { action: 'cancel' });
|
|
497
|
+
signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
498
|
+
elicitations.set(requestId, { sessionId, mode, elicitationId, resolve, timer, done: () => signal?.removeEventListener?.('abort', onAbort) });
|
|
499
|
+
send({
|
|
500
|
+
type: 'elicitation:request',
|
|
501
|
+
requestId,
|
|
502
|
+
sessionId,
|
|
503
|
+
toolCallId: typeof params?.toolCallId === 'string' ? params.toolCallId : null,
|
|
504
|
+
message: typeof params?.message === 'string' ? params.message.slice(0, limits.maxElicitationMessage) : '',
|
|
505
|
+
mode,
|
|
506
|
+
...(params?.requestedSchema && typeof params.requestedSchema === 'object' ? { requestedSchema: params.requestedSchema } : {}),
|
|
507
|
+
...(url ? { url } : {}),
|
|
508
|
+
...(elicitationId ? { elicitationId } : {}),
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
// elicitation/complete: the page the owner was sent to is done. A question still open is
|
|
513
|
+
// dismissed (the agent is answered `cancel`: nobody chose here); the page hears 'complete'.
|
|
514
|
+
function completeElicitation(params) {
|
|
515
|
+
const elicitationId = typeof params?.elicitationId === 'string' ? params.elicitationId : '';
|
|
516
|
+
if (!elicitationId) return;
|
|
517
|
+
for (const [requestId, entry] of elicitations) {
|
|
518
|
+
if (entry.elicitationId === elicitationId) { settleElicitation(requestId, { action: 'cancel' }, 'complete'); return; }
|
|
519
|
+
}
|
|
520
|
+
const accepted = completions.get(elicitationId);
|
|
521
|
+
if (!accepted) return;
|
|
522
|
+
completions.delete(elicitationId);
|
|
523
|
+
send({ type: 'elicitation:resolved', requestId: accepted.requestId, sessionId: accepted.sessionId, action: 'complete' });
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// A subagent session the agent exposed (subagent_update): where its commands run by default.
|
|
527
|
+
function noteSubagent(parentId, update) {
|
|
528
|
+
if (update?.sessionUpdate !== 'subagent_update') return;
|
|
529
|
+
const id = update.sessionId;
|
|
530
|
+
if (typeof id !== 'string' || !id || id.length > 500 || id === parentId) return;
|
|
531
|
+
const known = subagents.get(id);
|
|
532
|
+
subagents.delete(id);
|
|
533
|
+
subagents.set(id, { parent: parentId, cwd: typeof update.cwd === 'string' && isAbsolute(update.cwd) ? update.cwd : known?.cwd || null });
|
|
534
|
+
if (subagents.size > 256) subagents.delete(subagents.keys().next().value);
|
|
535
|
+
}
|
|
536
|
+
// Bypass is the tab's: a subagent's session follows the session that started it.
|
|
537
|
+
function sessionBypass(id) {
|
|
538
|
+
let at = id;
|
|
539
|
+
for (let depth = 0; depth < 8 && at; depth++) {
|
|
540
|
+
const own = sessions.get(at);
|
|
541
|
+
if (own) return own.bypass === true;
|
|
542
|
+
const child = subagents.get(at);
|
|
543
|
+
if (!child) return false;
|
|
544
|
+
at = child.parent;
|
|
545
|
+
}
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
// Bypass answers an open request itself (lib/antigravity/bypass.js): allow once, unless a hard rail refuses it.
|
|
549
|
+
// False when it cannot (no allow-once option): the owner's card decides.
|
|
550
|
+
function autoAnswer(requestId) {
|
|
551
|
+
const entry = permissions.get(requestId);
|
|
552
|
+
if (!entry) return false;
|
|
553
|
+
const rail = bypassRail(entry.toolCall, { cwd: sessionCwd(entry.sessionId), roots: safeRoots() });
|
|
554
|
+
const base = { type: 'permission:auto', requestId, sessionId: entry.sessionId, toolCall: entry.toolCall };
|
|
555
|
+
if (rail) {
|
|
556
|
+
const reject = rejectOnceOption(entry.options);
|
|
557
|
+
send({ ...base, decision: 'refused', rule: rail.rule, reason: rail.reason });
|
|
558
|
+
settlePermission(requestId, reject ? { outcome: 'selected', optionId: reject.optionId } : { outcome: 'cancelled' });
|
|
559
|
+
log(`[antigravity] bypass refused (${rail.rule}): ${rail.reason.slice(0, 200)}`);
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
const allow = allowOnceOption(entry.options);
|
|
563
|
+
if (!allow) return false;
|
|
564
|
+
send({ ...base, decision: 'allowed', optionId: allow.optionId });
|
|
565
|
+
settlePermission(requestId, { outcome: 'selected', optionId: allow.optionId });
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
function safeRoots() {
|
|
569
|
+
try { const roots = policyRoots(); return Array.isArray(roots) ? roots : []; } catch { return []; }
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function sessionCwd(id) {
|
|
573
|
+
let at = id;
|
|
574
|
+
for (let depth = 0; depth < 8 && at; depth++) {
|
|
575
|
+
const own = sessions.get(at);
|
|
576
|
+
if (own) return own.cwd;
|
|
577
|
+
const child = subagents.get(at);
|
|
578
|
+
if (!child) return null;
|
|
579
|
+
if (child.cwd) return child.cwd;
|
|
580
|
+
at = child.parent;
|
|
581
|
+
}
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
// What a closed or deleted session leaves: its open questions and its terminals, and its subagents'.
|
|
585
|
+
function endSession(id) {
|
|
586
|
+
const ids = [id];
|
|
587
|
+
for (let i = 0; i < ids.length && ids.length < 300; i++) {
|
|
588
|
+
for (const [child, info] of subagents) if (info.parent === ids[i] && !ids.includes(child)) ids.push(child);
|
|
589
|
+
}
|
|
590
|
+
for (const each of ids) {
|
|
591
|
+
cancelPermissions(each);
|
|
592
|
+
cancelElicitations(each);
|
|
593
|
+
clientHost.releaseSession(each);
|
|
594
|
+
removeAttachments(each);
|
|
595
|
+
subagents.delete(each);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async function ensureSlot(home = null) {
|
|
600
|
+
if (closed) throw new Error('The Antigravity panel closed.');
|
|
601
|
+
const slot = slotOf(home);
|
|
602
|
+
if (slot.conn?.running && slot.init) return slot;
|
|
603
|
+
if (slot.starting) return slot.starting;
|
|
604
|
+
slot.starting = (async () => {
|
|
605
|
+
if (slot.home && typeof isRunHomeActive === 'function' && isRunHomeActive(slot.home)) {
|
|
606
|
+
throw Object.assign(new Error('This dispatched run is still running in this folder.'), { code: 'HOME_BUSY' });
|
|
607
|
+
}
|
|
608
|
+
const launch = await resolveLaunch();
|
|
609
|
+
if (closed) throw new Error('The Antigravity panel closed.');
|
|
610
|
+
if (!launch?.command) throw Object.assign(new Error('The Antigravity CLI (agy) is not installed.'), { code: 'CLI_MISSING' });
|
|
611
|
+
if (slot.home) assertSafeAntigravityProject(launch.cwd || process.cwd());
|
|
612
|
+
if (slot.home) {
|
|
613
|
+
// Launch resolution is async: a worker may have acquired this home while we waited.
|
|
614
|
+
if (typeof isRunHomeActive === 'function' && isRunHomeActive(slot.home)) throw Object.assign(new Error('This dispatched run is still running in this folder.'), { code: 'HOME_BUSY' });
|
|
615
|
+
// The worker run's PreToolUse hook checked its per-run policy token, which ended with the run;
|
|
616
|
+
// once the panel takes over, permissions run through session/request_permission and Bypass like ordinary tabs.
|
|
617
|
+
try { rmSync(join(slot.home, 'config', 'hooks.json'), { force: true }); } catch { /* already gone */ }
|
|
618
|
+
}
|
|
619
|
+
const switches = { ...wanted };
|
|
620
|
+
const next = connect({
|
|
621
|
+
command: launch.command,
|
|
622
|
+
args: launch.args || [],
|
|
623
|
+
...(slot.home ? { acpArgs: ['--gemini-dir', slot.home] } : {}),
|
|
624
|
+
cwd: launch.cwd || undefined,
|
|
625
|
+
env: childEnv(),
|
|
626
|
+
clientInfo,
|
|
627
|
+
onLog: (line) => log(`[antigravity] ${line}`),
|
|
628
|
+
onUpdate: (params) => {
|
|
629
|
+
if (!params || typeof params.sessionId !== 'string') return;
|
|
630
|
+
const session = sessions.get(params.sessionId);
|
|
631
|
+
noteSubagent(params.sessionId, params.update);
|
|
632
|
+
send({ type: 'session:update', sessionId: params.sessionId, update: params.update, replay: !!session?.replaying });
|
|
633
|
+
},
|
|
634
|
+
onPermission: (params) => new Promise((resolve) => {
|
|
635
|
+
const requestId = randomBytes(8).toString('hex');
|
|
636
|
+
const options = Array.isArray(params?.options) ? params.options.filter((o) => o && typeof o.optionId === 'string') : [];
|
|
637
|
+
const timer = setTimeout(() => settlePermission(requestId, { outcome: 'cancelled' }), limits.permissionTimeoutMs);
|
|
638
|
+
timer.unref?.();
|
|
639
|
+
permissions.set(requestId, { sessionId: params?.sessionId, options, toolCall: params?.toolCall || null, resolve, timer });
|
|
640
|
+
if (sessionBypass(params?.sessionId) && autoAnswer(requestId)) return;
|
|
641
|
+
send({ type: 'permission:request', requestId, sessionId: params?.sessionId, toolCall: params?.toolCall || null, options });
|
|
642
|
+
}),
|
|
643
|
+
onElicitation: (params, context) => (slot.conn === next ? askElicitation(params, context) : { action: 'cancel' }),
|
|
644
|
+
// Only what this agent was offered, and nothing for an agent that is being replaced.
|
|
645
|
+
onClientRequest: (method, params, context) => clientHost.handle(method, params, { signal: context?.signal, allow: slot.conn === next ? switches : {} }),
|
|
646
|
+
onNotification: (method, params) => {
|
|
647
|
+
if (method === 'elicitation/complete' && slot.conn === next) completeElicitation(params);
|
|
648
|
+
},
|
|
649
|
+
onExit: (info) => {
|
|
650
|
+
if (slot.conn !== next) return;
|
|
651
|
+
if (slot.home === null) {
|
|
652
|
+
for (const [requestId, entry] of [...permissions]) if (!entry.sessionId) settlePermission(requestId, { outcome: 'cancelled' });
|
|
653
|
+
for (const [requestId, entry] of [...elicitations]) if (!entry.sessionId) settleElicitation(requestId, { action: 'cancel' });
|
|
654
|
+
}
|
|
655
|
+
for (const [id, session] of [...sessions.entries()]) {
|
|
656
|
+
if ((session.home || null) !== slot.home) continue;
|
|
657
|
+
endSession(id);
|
|
658
|
+
session.turn = null;
|
|
659
|
+
}
|
|
660
|
+
slot.init = null;
|
|
661
|
+
slot.offered = null;
|
|
662
|
+
send({
|
|
663
|
+
type: 'agent:exit',
|
|
664
|
+
code: info.code,
|
|
665
|
+
signal: info.signal,
|
|
666
|
+
error: info.error || null,
|
|
667
|
+
stderr: info.stderr ? info.stderr.slice(-2000) : '',
|
|
668
|
+
...(slot.home ? { geminiDir: slot.home } : {}),
|
|
669
|
+
});
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
slot.conn = next;
|
|
673
|
+
try {
|
|
674
|
+
slot.init = await next.initialize({ clientCapabilities: acpClientCapabilities(switches) });
|
|
675
|
+
slot.offered = switches;
|
|
676
|
+
} catch (error) {
|
|
677
|
+
next.close();
|
|
678
|
+
if (slot.conn === next) slot.conn = null;
|
|
679
|
+
// The exit (and its stderr) is known at once for a process that is gone; a live one gets a moment.
|
|
680
|
+
const exit = await Promise.race([next.exited, new Promise((resolve) => { setTimeout(resolve, 1000).unref?.(); })]);
|
|
681
|
+
throw agentStartError(error, exit);
|
|
682
|
+
}
|
|
683
|
+
return slot;
|
|
684
|
+
})();
|
|
685
|
+
try { return await slot.starting; } finally { slot.starting = null; }
|
|
686
|
+
}
|
|
687
|
+
async function ensureAgent(home = null) {
|
|
688
|
+
return (await ensureSlot(home)).conn;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// agent:restart: the running agents end (what they asked is cancelled, their terminals killed, their sessions
|
|
692
|
+
// forgotten: the page opens them again) without an agent:exit; the next ensureAgent() starts a new one.
|
|
693
|
+
async function stopAgent() {
|
|
694
|
+
// Closing an initializing connection rejects initialize; waiting for initialize first would deadlock.
|
|
695
|
+
const closing = [...agents.values()].map((slot) => slot.conn?.close()).filter(Boolean);
|
|
696
|
+
let startTimer;
|
|
697
|
+
try {
|
|
698
|
+
await Promise.race([Promise.allSettled([...agents.values()].map((slot) => slot.starting).filter(Boolean)), new Promise((resolve) => { startTimer = setTimeout(resolve, 5000); })]);
|
|
699
|
+
} finally { clearTimeout(startTimer); }
|
|
700
|
+
const running = [];
|
|
701
|
+
for (const slot of agents.values()) {
|
|
702
|
+
if (slot.conn) running.push(slot.conn);
|
|
703
|
+
slot.conn = null;
|
|
704
|
+
slot.init = null;
|
|
705
|
+
slot.offered = null;
|
|
706
|
+
}
|
|
707
|
+
agents.clear();
|
|
708
|
+
agents.set('', defaultSlot);
|
|
709
|
+
cancelPermissions();
|
|
710
|
+
cancelElicitations();
|
|
711
|
+
clientHost.releaseAll();
|
|
712
|
+
sessions.clear();
|
|
713
|
+
subagents.clear();
|
|
714
|
+
let closeTimer;
|
|
715
|
+
try {
|
|
716
|
+
await Promise.race([Promise.allSettled([...closing, ...running.map((agent) => agent.close())]), new Promise((resolve) => { closeTimer = setTimeout(resolve, 5000); })]);
|
|
717
|
+
} finally { clearTimeout(closeTimer); }
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function sessionId(msg) {
|
|
721
|
+
const id = typeof msg?.sessionId === 'string' ? msg.sessionId : '';
|
|
722
|
+
if (!ID_RE.test(id)) throw Object.assign(new Error('Unknown session.'), { code: 'SESSION_INVALID' });
|
|
723
|
+
return id;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function agentView(slot = defaultSlot) {
|
|
727
|
+
return agentCapabilitiesView(slot?.init?.agentCapabilities);
|
|
728
|
+
}
|
|
729
|
+
function requireSessionCapability(key, what, slot = defaultSlot) {
|
|
730
|
+
if (!agentView(slot).sessionCapabilities[key]) throw Object.assign(new Error(`This version of Antigravity cannot ${what}.`), { code: 'UNSUPPORTED' });
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// The other folders of a session (ACP additionalDirectories): only for an agent that takes them,
|
|
734
|
+
// each an existing absolute folder by its real path, the session's own folder and repeats left out.
|
|
735
|
+
function additionalDirectoriesOf(value, cwd, slot = defaultSlot) {
|
|
736
|
+
if (!agentView(slot).sessionCapabilities.additionalDirectories || value === undefined || value === null) return [];
|
|
737
|
+
const refuse = (message) => Object.assign(new Error(message), { code: 'ADDITIONAL_DIRECTORIES_INVALID' });
|
|
738
|
+
if (!Array.isArray(value)) throw refuse('Other folders must be a list of folders.');
|
|
739
|
+
if (value.length > limits.maxAdditionalDirectories) throw refuse(`At most ${limits.maxAdditionalDirectories} other folders per session.`);
|
|
740
|
+
const list = [];
|
|
741
|
+
for (const entry of value) {
|
|
742
|
+
if (typeof entry !== 'string' || !entry || !isAbsolute(entry)) throw refuse(`Another folder must be an absolute path: ${String(entry).slice(0, 200)}`);
|
|
743
|
+
let real;
|
|
744
|
+
try { real = validateCwd(entry); } catch (error) { throw refuse(error?.message || `${entry} is not a folder.`); }
|
|
745
|
+
if (real === cwd || list.includes(real)) continue;
|
|
746
|
+
list.push(real);
|
|
747
|
+
}
|
|
748
|
+
return list;
|
|
749
|
+
}
|
|
750
|
+
const directoriesParam = (list) => (list.length ? { additionalDirectories: list } : {});
|
|
751
|
+
|
|
752
|
+
function helloAnswer() {
|
|
753
|
+
const init = defaultSlot.init;
|
|
754
|
+
return {
|
|
755
|
+
capabilities: [...ANTIGRAVITY_PANEL_CAPABILITIES],
|
|
756
|
+
agent: init?.agentInfo || null,
|
|
757
|
+
authMethods: authMethodsView(init?.authMethods),
|
|
758
|
+
promptCapabilities: init?.agentCapabilities?.promptCapabilities || {},
|
|
759
|
+
sessionCapabilities: init?.agentCapabilities?.sessionCapabilities || init?.agentCapabilities?.session_capabilities || {},
|
|
760
|
+
loadSession: !!init?.agentCapabilities?.loadSession,
|
|
761
|
+
agentCapabilities: agentView(defaultSlot),
|
|
762
|
+
// What the running agent was offered; with none running, what the next one will be.
|
|
763
|
+
clientCapabilities: { ...(defaultSlot.conn?.running && defaultSlot.offered ? defaultSlot.offered : wanted) },
|
|
764
|
+
limits: { maxAttachments: limits.maxAttachments, maxAttachmentBytes: limits.maxAttachmentBytes, maxPaths: limits.maxPaths, maxAdditionalDirectories: limits.maxAdditionalDirectories },
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
async function agentStatus() {
|
|
768
|
+
try {
|
|
769
|
+
await ensureAgent();
|
|
770
|
+
return { ...helloAnswer(), running: true };
|
|
771
|
+
} catch (error) {
|
|
772
|
+
const info = acpErrorInfo(error);
|
|
773
|
+
if (error?.stderr) info.stderr = String(error.stderr).slice(-2000);
|
|
774
|
+
return { ...helloAnswer(), running: false, error: info, cliMissing: error?.code === 'CLI_MISSING', acpUnsupported: error?.code === 'ACP_UNSUPPORTED' };
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function saveAttachmentFor(id) {
|
|
779
|
+
return (name, buffer) => {
|
|
780
|
+
if (!attachmentsRoot) throw Object.assign(new Error('Attachments are not available.'), { code: 'ATTACHMENT_UNAVAILABLE' });
|
|
781
|
+
pruneAttachments();
|
|
782
|
+
const dir = join(attachmentsRoot, attachmentName(id));
|
|
783
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
784
|
+
const file = join(dir, `${Date.now().toString(36)}-${randomBytes(3).toString('hex')}-${name}`);
|
|
785
|
+
writeFileSync(file, buffer, { mode: 0o600, flag: 'wx' });
|
|
786
|
+
return file;
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const handlers = {
|
|
791
|
+
async hello(msg) {
|
|
792
|
+
if (Object.hasOwn(msg, 'clientCapabilities')) wanted = clientSwitches(msg.clientCapabilities);
|
|
793
|
+
return agentStatus();
|
|
794
|
+
},
|
|
795
|
+
'agent:restart'(msg) {
|
|
796
|
+
const run = restarts.then(async () => {
|
|
797
|
+
if ([...sessions.values()].some((session) => session.turn)) throw Object.assign(new Error('A turn is running. Stop it before Antigravity restarts.'), { code: 'TURN_RUNNING' });
|
|
798
|
+
if (Object.hasOwn(msg, 'clientCapabilities')) wanted = clientSwitches(msg.clientCapabilities);
|
|
799
|
+
await stopAgent();
|
|
800
|
+
return agentStatus();
|
|
801
|
+
});
|
|
802
|
+
restarts = run.catch(() => {});
|
|
803
|
+
return run;
|
|
804
|
+
},
|
|
805
|
+
async 'session:new'(msg) {
|
|
806
|
+
// Ordinary tabs always open in the user's default ~/.gemini store; a geminiDir sent here is refused unless empty.
|
|
807
|
+
if (msg.geminiDir !== undefined && msg.geminiDir !== null && msg.geminiDir !== '') {
|
|
808
|
+
throw Object.assign(new Error('New conversations open in the default Antigravity store.'), { code: 'HOME_INVALID' });
|
|
809
|
+
}
|
|
810
|
+
const cwd = validateCwd(msg.cwd);
|
|
811
|
+
const slot = await ensureSlot(null);
|
|
812
|
+
const directories = additionalDirectoriesOf(msg.additionalDirectories, cwd, slot);
|
|
813
|
+
const answer = await slot.conn.request('session/new', { cwd, mcpServers: [], ...directoriesParam(directories) });
|
|
814
|
+
sessions.set(answer.sessionId, { cwd, home: null, tabId: msg.tabId || null, bypass: msg.bypass === true, turn: null, additionalDirectories: directories });
|
|
815
|
+
try { onSessionActivity?.({ kind: 'new', sessionId: answer.sessionId, cwd }); } catch { /* informational */ }
|
|
816
|
+
// `models`: the older ACP model state ({ currentModelId, availableModels }) `agy acp` also answers with.
|
|
817
|
+
return { sessionId: answer.sessionId, cwd, modes: answer.modes || null, models: answer.models || null, configOptions: answer.configOptions || [], additionalDirectories: directories };
|
|
818
|
+
},
|
|
819
|
+
async 'session:load'(msg) {
|
|
820
|
+
const id = sessionId(msg);
|
|
821
|
+
const cwd = validateCwd(msg.cwd);
|
|
822
|
+
const home = validateRunHome(msg.geminiDir, homesRoot);
|
|
823
|
+
if (home) assertSafeAntigravityProject(cwd);
|
|
824
|
+
const slot = await ensureSlot(home);
|
|
825
|
+
const directories = additionalDirectoriesOf(msg.additionalDirectories, cwd, slot);
|
|
826
|
+
const entry = { cwd, home, tabId: msg.tabId || null, bypass: msg.bypass === true, turn: null, replaying: true, additionalDirectories: directories };
|
|
827
|
+
sessions.set(id, entry);
|
|
828
|
+
try {
|
|
829
|
+
const answer = await slot.conn.request('session/load', { sessionId: id, cwd, mcpServers: [], ...directoriesParam(directories) });
|
|
830
|
+
return { sessionId: id, cwd, modes: answer?.modes || null, models: answer?.models || null, configOptions: answer?.configOptions || [], additionalDirectories: directories };
|
|
831
|
+
} catch (error) {
|
|
832
|
+
sessions.delete(id);
|
|
833
|
+
throw error;
|
|
834
|
+
} finally {
|
|
835
|
+
entry.replaying = false;
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
// session/resume: the conversation goes on in this process without its history being replayed.
|
|
839
|
+
async 'session:resume'(msg) {
|
|
840
|
+
const id = sessionId(msg);
|
|
841
|
+
const cwd = validateCwd(msg.cwd);
|
|
842
|
+
const home = validateRunHome(msg.geminiDir ?? sessions.get(id)?.home ?? null, homesRoot);
|
|
843
|
+
if (home) assertSafeAntigravityProject(cwd);
|
|
844
|
+
const slot = await ensureSlot(home);
|
|
845
|
+
requireSessionCapability('resume', 'resume a conversation', slot);
|
|
846
|
+
if (sessions.get(id)?.turn) throw Object.assign(new Error('A turn is already running in this session.'), { code: 'TURN_RUNNING' });
|
|
847
|
+
const directories = additionalDirectoriesOf(msg.additionalDirectories, cwd, slot);
|
|
848
|
+
const answer = await slot.conn.request('session/resume', { sessionId: id, cwd, mcpServers: [], ...directoriesParam(directories) });
|
|
849
|
+
sessions.set(id, { cwd, home, tabId: msg.tabId || null, bypass: msg.bypass === true, turn: null, additionalDirectories: directories });
|
|
850
|
+
return { sessionId: id, cwd, modes: answer?.modes || null, models: answer?.models || null, configOptions: answer?.configOptions || [], additionalDirectories: directories };
|
|
851
|
+
},
|
|
852
|
+
// session/fork: a new session that starts from this one's context; the original is left as it is.
|
|
853
|
+
async 'session:fork'(msg) {
|
|
854
|
+
const id = sessionId(msg);
|
|
855
|
+
const cwd = validateCwd(msg.cwd);
|
|
856
|
+
const home = validateRunHome(msg.geminiDir ?? sessions.get(id)?.home ?? null, homesRoot);
|
|
857
|
+
if (home) assertSafeAntigravityProject(cwd);
|
|
858
|
+
const slot = await ensureSlot(home);
|
|
859
|
+
requireSessionCapability('fork', 'fork a conversation', slot);
|
|
860
|
+
const directories = additionalDirectoriesOf(msg.additionalDirectories, cwd, slot);
|
|
861
|
+
const answer = await slot.conn.request('session/fork', { sessionId: id, cwd, mcpServers: [], ...directoriesParam(directories) });
|
|
862
|
+
const forked = typeof answer?.sessionId === 'string' && answer.sessionId ? answer.sessionId : '';
|
|
863
|
+
if (!forked) throw Object.assign(new Error('Antigravity did not name the new conversation.'), { code: 'FORK_FAILED' });
|
|
864
|
+
sessions.set(forked, { cwd, home, tabId: msg.tabId || null, bypass: msg.bypass === true, turn: null, additionalDirectories: directories });
|
|
865
|
+
try { onSessionActivity?.({ kind: 'new', sessionId: forked, cwd }); } catch { /* informational */ }
|
|
866
|
+
return { sessionId: forked, cwd, modes: answer?.modes || null, models: answer?.models || null, configOptions: answer?.configOptions || [], additionalDirectories: directories };
|
|
867
|
+
},
|
|
868
|
+
// session/delete: gone from the agent's list too. What it asked is cancelled and its terminals killed first.
|
|
869
|
+
async 'session:delete'(msg) {
|
|
870
|
+
const id = sessionId(msg);
|
|
871
|
+
const home = validateRunHome(msg.geminiDir ?? sessions.get(id)?.home ?? null, homesRoot);
|
|
872
|
+
const slot = await ensureSlot(home);
|
|
873
|
+
requireSessionCapability('delete', 'delete a conversation', slot);
|
|
874
|
+
const running = !!sessions.get(id)?.turn;
|
|
875
|
+
endSession(id);
|
|
876
|
+
sessions.delete(id);
|
|
877
|
+
if (running) await slot.conn.notify('session/cancel', { sessionId: id }).catch(() => {});
|
|
878
|
+
await slot.conn.request('session/delete', { sessionId: id }, { timeoutMs: 30_000 });
|
|
879
|
+
return { deleted: true };
|
|
880
|
+
},
|
|
881
|
+
async 'session:list'(msg) {
|
|
882
|
+
const agent = await ensureAgent();
|
|
883
|
+
const params = {};
|
|
884
|
+
if (typeof msg.cwd === 'string' && msg.cwd) params.cwd = validateCwd(msg.cwd);
|
|
885
|
+
if (typeof msg.cursor === 'string' && msg.cursor) params.cursor = msg.cursor;
|
|
886
|
+
const answer = await agent.request('session/list', params);
|
|
887
|
+
return { sessions: Array.isArray(answer?.sessions) ? answer.sessions : [], nextCursor: answer?.nextCursor || null };
|
|
888
|
+
},
|
|
889
|
+
async 'session:close'(msg) {
|
|
890
|
+
const id = sessionId(msg);
|
|
891
|
+
const session = sessions.get(id);
|
|
892
|
+
endSession(id);
|
|
893
|
+
sessions.delete(id);
|
|
894
|
+
const conn = agents.get(session?.home || '')?.conn;
|
|
895
|
+
if (!conn?.running) return { closed: true };
|
|
896
|
+
try { await conn.request('session/close', { sessionId: id }, { timeoutMs: 10_000 }); } catch { /* closing is best effort */ }
|
|
897
|
+
return { closed: true };
|
|
898
|
+
},
|
|
899
|
+
async prompt(msg) {
|
|
900
|
+
const id = sessionId(msg);
|
|
901
|
+
const session = sessions.get(id);
|
|
902
|
+
if (!session) throw Object.assign(new Error('This session is not open. Reload it first.'), { code: 'SESSION_NOT_OPEN' });
|
|
903
|
+
if (session.turn) throw Object.assign(new Error('A turn is already running in this session.'), { code: 'TURN_RUNNING' });
|
|
904
|
+
// The turn is claimed before anything is awaited: two prompts that arrive together must not both reach
|
|
905
|
+
// agy, which folds a second prompt into the running turn (both echoed, one reply).
|
|
906
|
+
const turn = { id: randomBytes(6).toString('hex') };
|
|
907
|
+
session.turn = turn;
|
|
908
|
+
let agent;
|
|
909
|
+
let blocks;
|
|
910
|
+
const typed = typeof msg.text === 'string' ? msg.text : '';
|
|
911
|
+
try {
|
|
912
|
+
const slot = await ensureSlot(session.home || null);
|
|
913
|
+
agent = slot.conn;
|
|
914
|
+
let text = typed;
|
|
915
|
+
if (typeof expandPrompt === 'function') { try { text = String(expandPrompt(typed) ?? typed); } catch { text = typed; } }
|
|
916
|
+
blocks = buildPromptBlocks({ ...msg, text }, { promptCapabilities: slot.init?.agentCapabilities?.promptCapabilities || {}, saveAttachment: saveAttachmentFor(id), limits });
|
|
917
|
+
} catch (error) {
|
|
918
|
+
if (session.turn === turn) session.turn = null;
|
|
919
|
+
throw error;
|
|
920
|
+
}
|
|
921
|
+
try { onHumanPrompt?.({ cwd: session.cwd, words: typed }); } catch { /* informational */ }
|
|
922
|
+
// The greeting is told once, with the prompt that carries its id; a slash command gets none (its own flow
|
|
923
|
+
// comes first), and the greeting is not owed to it later either.
|
|
924
|
+
if (typeof msg.greetingId === 'string' && msg.greetingId && typeof greetingNote === 'function') {
|
|
925
|
+
let note = '';
|
|
926
|
+
try { note = greetingNoteBlock(greetingNote(msg.greetingId, { skip: /^\s*\//.test(typed) })); } catch { note = ''; }
|
|
927
|
+
if (note) blocks = [{ type: 'text', text: note }, ...blocks];
|
|
928
|
+
}
|
|
929
|
+
try { onSessionActivity?.({ kind: 'prompt', sessionId: id, cwd: session.cwd }); } catch { /* informational */ }
|
|
930
|
+
try {
|
|
931
|
+
const answer = await agent.request('session/prompt', { sessionId: id, prompt: blocks }, { timeoutMs: 0 });
|
|
932
|
+
const result = { stopReason: answer?.stopReason || 'end_turn', usage: answer?.usage || null };
|
|
933
|
+
send({ type: 'turn:end', sessionId: id, turnId: turn.id, ...result });
|
|
934
|
+
return { turnId: turn.id, ...result };
|
|
935
|
+
} catch (error) {
|
|
936
|
+
send({ type: 'turn:end', sessionId: id, turnId: turn.id, stopReason: 'error', error: acpErrorInfo(error) });
|
|
937
|
+
throw error;
|
|
938
|
+
} finally {
|
|
939
|
+
if (session.turn === turn) session.turn = null;
|
|
940
|
+
cancelPermissions(id);
|
|
941
|
+
cancelElicitations(id);
|
|
942
|
+
try { onTurnEnd?.({ sessionId: id, cwd: session.cwd }); } catch { /* informational */ }
|
|
943
|
+
}
|
|
944
|
+
},
|
|
945
|
+
async cancel(msg) {
|
|
946
|
+
const id = sessionId(msg);
|
|
947
|
+
cancelPermissions(id);
|
|
948
|
+
cancelElicitations(id);
|
|
949
|
+
const conn = agents.get(sessions.get(id)?.home || '')?.conn;
|
|
950
|
+
if (conn?.running) await conn.notify('session/cancel', { sessionId: id });
|
|
951
|
+
return { cancelled: true };
|
|
952
|
+
},
|
|
953
|
+
async 'config:set'(msg) {
|
|
954
|
+
const id = sessionId(msg);
|
|
955
|
+
if (typeof msg.configId !== 'string' || !msg.configId) throw Object.assign(new Error('Unknown setting.'), { code: 'CONFIG_INVALID' });
|
|
956
|
+
const value = typeof msg.value === 'boolean' ? msg.value : String(msg.value ?? '');
|
|
957
|
+
const agent = await ensureAgent(sessions.get(id)?.home || null);
|
|
958
|
+
const answer = await agent.request('session/set_config_option', { sessionId: id, configId: msg.configId, value });
|
|
959
|
+
return { configOptions: answer?.configOptions || [] };
|
|
960
|
+
},
|
|
961
|
+
async 'mode:set'(msg) {
|
|
962
|
+
const id = sessionId(msg);
|
|
963
|
+
if (typeof msg.modeId !== 'string' || !msg.modeId) throw Object.assign(new Error('Unknown mode.'), { code: 'MODE_INVALID' });
|
|
964
|
+
const agent = await ensureAgent(sessions.get(id)?.home || null);
|
|
965
|
+
await agent.request('session/set_mode', { sessionId: id, modeId: msg.modeId });
|
|
966
|
+
return { modeId: msg.modeId };
|
|
967
|
+
},
|
|
968
|
+
// For an agent whose model is not a config option: `agy acp` still answers session/set_model.
|
|
969
|
+
async 'model:set'(msg) {
|
|
970
|
+
const id = sessionId(msg);
|
|
971
|
+
if (typeof msg.modelId !== 'string' || !msg.modelId) throw Object.assign(new Error('Unknown model.'), { code: 'MODEL_INVALID' });
|
|
972
|
+
const agent = await ensureAgent(sessions.get(id)?.home || null);
|
|
973
|
+
await agent.request('session/set_model', { sessionId: id, modelId: msg.modelId });
|
|
974
|
+
return { modelId: msg.modelId };
|
|
975
|
+
},
|
|
976
|
+
// Bypass for one tab's session: on, every permission request is answered allow-once (and what is open now is);
|
|
977
|
+
// the hard rails of lib/antigravity/bypass.js still refuse. Only the owner's socket gets here.
|
|
978
|
+
async 'session:bypass'(msg) {
|
|
979
|
+
const id = sessionId(msg);
|
|
980
|
+
const entry = sessions.get(id);
|
|
981
|
+
if (!entry) throw Object.assign(new Error('That conversation is not open.'), { code: 'SESSION_UNKNOWN' });
|
|
982
|
+
if (typeof msg.on !== 'boolean') throw Object.assign(new Error('Bypass is on or off.'), { code: 'BYPASS_INVALID' });
|
|
983
|
+
entry.bypass = msg.on;
|
|
984
|
+
let answered = 0;
|
|
985
|
+
if (msg.on) {
|
|
986
|
+
for (const [requestId, open] of [...permissions]) {
|
|
987
|
+
if (sessionBypass(open.sessionId) && autoAnswer(requestId)) answered++;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return { sessionId: id, bypass: entry.bypass, answered };
|
|
991
|
+
},
|
|
992
|
+
async 'permission:answer'(msg) {
|
|
993
|
+
const requestId = typeof msg.requestId === 'string' ? msg.requestId : '';
|
|
994
|
+
const entry = permissions.get(requestId);
|
|
995
|
+
if (!entry) return { answered: false, stale: true };
|
|
996
|
+
if (msg.optionId === null || msg.optionId === undefined) return { answered: settlePermission(requestId, { outcome: 'cancelled' }) };
|
|
997
|
+
if (!entry.options.some((option) => option.optionId === msg.optionId)) throw Object.assign(new Error('That answer was not one of the choices.'), { code: 'PERMISSION_OPTION_INVALID' });
|
|
998
|
+
return { answered: settlePermission(requestId, { outcome: 'selected', optionId: msg.optionId }) };
|
|
999
|
+
},
|
|
1000
|
+
async 'elicitation:answer'(msg) {
|
|
1001
|
+
const requestId = typeof msg.requestId === 'string' ? msg.requestId : '';
|
|
1002
|
+
if (!elicitations.has(requestId)) return { answered: false, stale: true };
|
|
1003
|
+
if (!ELICITATION_ACTIONS.has(msg.action)) throw Object.assign(new Error('An answer is accept, decline or cancel.'), { code: 'ELICITATION_ACTION_INVALID' });
|
|
1004
|
+
// Content goes with an accept only; a decline or a cancel says nothing more.
|
|
1005
|
+
const content = msg.action === 'accept' ? elicitationContent(msg.content, limits) : undefined;
|
|
1006
|
+
return { answered: settleElicitation(requestId, content === undefined ? { action: msg.action } : { action: msg.action, content }) };
|
|
1007
|
+
},
|
|
1008
|
+
async 'auth:start'(msg) {
|
|
1009
|
+
const slot = await ensureSlot(null);
|
|
1010
|
+
const method = (Array.isArray(slot.init?.authMethods) ? slot.init.authMethods : []).find((m) => m && m.id === msg.methodId);
|
|
1011
|
+
if (!method) throw Object.assign(new Error('Unknown sign-in method.'), { code: 'AUTH_METHOD_INVALID' });
|
|
1012
|
+
// ACP: a terminal sign-in runs `agy` in a terminal and is never sent to authenticate.
|
|
1013
|
+
if (method.type === 'terminal') throw Object.assign(new Error('This sign-in runs in a terminal: run `agy` there to sign in.'), { code: 'AUTH_METHOD_TERMINAL' });
|
|
1014
|
+
await slot.conn.request('authenticate', { methodId: msg.methodId }, { timeoutMs: limits.authTimeoutMs });
|
|
1015
|
+
return { signedIn: true };
|
|
1016
|
+
},
|
|
1017
|
+
async 'auth:logout'() {
|
|
1018
|
+
const slot = await ensureSlot(null);
|
|
1019
|
+
if (!agentView(slot).auth.logout) throw Object.assign(new Error('This version of Antigravity cannot sign out from here.'), { code: 'UNSUPPORTED' });
|
|
1020
|
+
await slot.conn.request('logout', {});
|
|
1021
|
+
return { signedIn: false };
|
|
1022
|
+
},
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
ws.on('message', async (raw) => {
|
|
1026
|
+
let msg;
|
|
1027
|
+
try { msg = JSON.parse(String(raw)); } catch { return; }
|
|
1028
|
+
if (!msg || typeof msg.type !== 'string') return;
|
|
1029
|
+
if (msg.type === 'ping') { send({ type: 'pong' }); return; }
|
|
1030
|
+
const handler = Object.hasOwn(handlers, msg.type) ? handlers[msg.type] : null;
|
|
1031
|
+
const reqId = typeof msg.reqId === 'string' || Number.isInteger(msg.reqId) ? msg.reqId : null;
|
|
1032
|
+
if (!handler) { send({ type: 'result', reqId, ok: false, error: { message: `Unknown request: ${msg.type}`, code: 'UNKNOWN_REQUEST' } }); return; }
|
|
1033
|
+
try {
|
|
1034
|
+
const data = await handler(msg);
|
|
1035
|
+
send({ type: 'result', reqId, ok: true, data });
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
const info = acpErrorInfo(error);
|
|
1038
|
+
if (typeof error?.code === 'string') info.code = error.code;
|
|
1039
|
+
send({ type: 'result', reqId, ok: false, error: info, authRequired: isAuthRequired(error) });
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
ws.on('close', () => { void state.shutdown(); });
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
return {
|
|
1047
|
+
handleSocket,
|
|
1048
|
+
liveCount: () => live.size,
|
|
1049
|
+
/** Folder names of SynaBun run homes open in any panel socket (pruneAntigravityHomes leaves them alone). */
|
|
1050
|
+
activeHomes: () => { const names = new Set(); for (const state of live) for (const name of state.homes()) names.add(name); return names; },
|
|
1051
|
+
/** Prompts that are running in any panel socket (a restart would end them). */
|
|
1052
|
+
turnsRunning: () => { let count = 0; for (const state of live) count += state.turns(); return count; },
|
|
1053
|
+
closeAll: async () => {
|
|
1054
|
+
const closing = [...live].map((state) => state.shutdown());
|
|
1055
|
+
await Promise.allSettled([...closing, ...stopping]);
|
|
1056
|
+
},
|
|
1057
|
+
};
|
|
1058
|
+
}
|