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