@xyit/wellspring 0.0.0-stage → 1.1.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/LICENSE +21 -0
- package/README.md +853 -2
- package/dist/agent/pending-asks.d.ts +20 -0
- package/dist/agent/pending-asks.js +70 -0
- package/dist/agent/pending-asks.js.map +1 -0
- package/dist/agent/task-registry.d.ts +14 -0
- package/dist/agent/task-registry.js +80 -0
- package/dist/agent/task-registry.js.map +1 -0
- package/dist/agent/tool-context.d.ts +30 -0
- package/dist/agent/tool-context.js +10 -0
- package/dist/agent/tool-context.js.map +1 -0
- package/dist/agent/usage.d.ts +40 -0
- package/dist/agent/usage.js +122 -0
- package/dist/agent/usage.js.map +1 -0
- package/dist/api/host/agent.d.ts +165 -0
- package/dist/api/host/agent.js +5 -0
- package/dist/api/host/agent.js.map +1 -0
- package/dist/api/host/events.d.ts +2 -0
- package/dist/api/host/events.js +6 -0
- package/dist/api/host/events.js.map +1 -0
- package/dist/api/host/exec.d.ts +36 -0
- package/dist/api/host/exec.js +10 -0
- package/dist/api/host/exec.js.map +1 -0
- package/dist/api/host/fs.d.ts +24 -0
- package/dist/api/host/fs.js +7 -0
- package/dist/api/host/fs.js.map +1 -0
- package/dist/api/host/hooks.d.ts +210 -0
- package/dist/api/host/hooks.js +401 -0
- package/dist/api/host/hooks.js.map +1 -0
- package/dist/api/host/host.d.ts +36 -0
- package/dist/api/host/host.js +53 -0
- package/dist/api/host/host.js.map +1 -0
- package/dist/api/host/http.d.ts +58 -0
- package/dist/api/host/http.js +6 -0
- package/dist/api/host/http.js.map +1 -0
- package/dist/api/host/index.d.ts +15 -0
- package/dist/api/host/index.js +20 -0
- package/dist/api/host/index.js.map +1 -0
- package/dist/api/host/models.d.ts +65 -0
- package/dist/api/host/models.js +8 -0
- package/dist/api/host/models.js.map +1 -0
- package/dist/api/host/network.d.ts +22 -0
- package/dist/api/host/network.js +7 -0
- package/dist/api/host/network.js.map +1 -0
- package/dist/api/host/plugin.d.ts +138 -0
- package/dist/api/host/plugin.js +6 -0
- package/dist/api/host/plugin.js.map +1 -0
- package/dist/api/host/plugins.d.ts +49 -0
- package/dist/api/host/plugins.js +6 -0
- package/dist/api/host/plugins.js.map +1 -0
- package/dist/api/host/pty.d.ts +55 -0
- package/dist/api/host/pty.js +21 -0
- package/dist/api/host/pty.js.map +1 -0
- package/dist/api/host/session.d.ts +5 -0
- package/dist/api/host/session.js +52 -0
- package/dist/api/host/session.js.map +1 -0
- package/dist/api/host/version.d.ts +2 -0
- package/dist/api/host/version.js +3 -0
- package/dist/api/host/version.js.map +1 -0
- package/dist/api/host/workspace.d.ts +38 -0
- package/dist/api/host/workspace.js +9 -0
- package/dist/api/host/workspace.js.map +1 -0
- package/dist/api/plugin/http-client.d.ts +42 -0
- package/dist/api/plugin/http-client.js +364 -0
- package/dist/api/plugin/http-client.js.map +1 -0
- package/dist/api/plugin/http-protocol.d.ts +152 -0
- package/dist/api/plugin/http-protocol.js +49 -0
- package/dist/api/plugin/http-protocol.js.map +1 -0
- package/dist/api/result/result.d.ts +16 -0
- package/dist/api/result/result.js +43 -0
- package/dist/api/result/result.js.map +1 -0
- package/dist/commands/agent-turn.d.ts +63 -0
- package/dist/commands/agent-turn.js +687 -0
- package/dist/commands/agent-turn.js.map +1 -0
- package/dist/commands/ask-prompt.d.ts +6 -0
- package/dist/commands/ask-prompt.js +49 -0
- package/dist/commands/ask-prompt.js.map +1 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +283 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/conversation.d.ts +51 -0
- package/dist/commands/conversation.js +630 -0
- package/dist/commands/conversation.js.map +1 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +77 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/exec-approval.d.ts +3 -0
- package/dist/commands/exec-approval.js +80 -0
- package/dist/commands/exec-approval.js.map +1 -0
- package/dist/commands/help.d.ts +2 -0
- package/dist/commands/help.js +35 -0
- package/dist/commands/help.js.map +1 -0
- package/dist/commands/key.d.ts +2 -0
- package/dist/commands/key.js +226 -0
- package/dist/commands/key.js.map +1 -0
- package/dist/commands/plugin.d.ts +2 -0
- package/dist/commands/plugin.js +190 -0
- package/dist/commands/plugin.js.map +1 -0
- package/dist/commands/session.d.ts +2 -0
- package/dist/commands/session.js +167 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/shared.d.ts +35 -0
- package/dist/commands/shared.js +66 -0
- package/dist/commands/shared.js.map +1 -0
- package/dist/commands/start.d.ts +2 -0
- package/dist/commands/start.js +75 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/stop.d.ts +2 -0
- package/dist/commands/stop.js +24 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/commands/version.d.ts +2 -0
- package/dist/commands/version.js +33 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/config/codes.d.ts +13 -0
- package/dist/config/codes.js +15 -0
- package/dist/config/codes.js.map +1 -0
- package/dist/config/credentials.d.ts +67 -0
- package/dist/config/credentials.js +214 -0
- package/dist/config/credentials.js.map +1 -0
- package/dist/config/defaults.d.ts +5 -0
- package/dist/config/defaults.js +31 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.js +10 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +10 -0
- package/dist/config/loader.js +68 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/providers.d.ts +30 -0
- package/dist/config/providers.js +97 -0
- package/dist/config/providers.js.map +1 -0
- package/dist/config/types.d.ts +75 -0
- package/dist/config/types.js +4 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config/validate.d.ts +4 -0
- package/dist/config/validate.js +391 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/display/markdown.d.ts +7 -0
- package/dist/display/markdown.js +116 -0
- package/dist/display/markdown.js.map +1 -0
- package/dist/display/tool-label.d.ts +6 -0
- package/dist/display/tool-label.js +81 -0
- package/dist/display/tool-label.js.map +1 -0
- package/dist/event/event.d.ts +27 -0
- package/dist/event/event.js +152 -0
- package/dist/event/event.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +308 -0
- package/dist/index.js.map +1 -0
- package/dist/input/codes.d.ts +15 -0
- package/dist/input/codes.js +20 -0
- package/dist/input/codes.js.map +1 -0
- package/dist/input/commands.d.ts +10 -0
- package/dist/input/commands.js +65 -0
- package/dist/input/commands.js.map +1 -0
- package/dist/input/help.d.ts +8 -0
- package/dist/input/help.js +51 -0
- package/dist/input/help.js.map +1 -0
- package/dist/input/index.d.ts +6 -0
- package/dist/input/index.js +10 -0
- package/dist/input/index.js.map +1 -0
- package/dist/input/parse-options.d.ts +11 -0
- package/dist/input/parse-options.js +51 -0
- package/dist/input/parse-options.js.map +1 -0
- package/dist/input/parse.d.ts +3 -0
- package/dist/input/parse.js +119 -0
- package/dist/input/parse.js.map +1 -0
- package/dist/input/types.d.ts +17 -0
- package/dist/input/types.js +2 -0
- package/dist/input/types.js.map +1 -0
- package/dist/logger/elog4js.d.ts +65 -0
- package/dist/logger/elog4js.js +305 -0
- package/dist/logger/elog4js.js.map +1 -0
- package/dist/logger/logger.d.ts +37 -0
- package/dist/logger/logger.js +196 -0
- package/dist/logger/logger.js.map +1 -0
- package/dist/network/index.d.ts +2 -0
- package/dist/network/index.js +6 -0
- package/dist/network/index.js.map +1 -0
- package/dist/network/openai.d.ts +117 -0
- package/dist/network/openai.js +443 -0
- package/dist/network/openai.js.map +1 -0
- package/dist/network/proxy.d.ts +9 -0
- package/dist/network/proxy.js +72 -0
- package/dist/network/proxy.js.map +1 -0
- package/dist/network/web.d.ts +34 -0
- package/dist/network/web.js +152 -0
- package/dist/network/web.js.map +1 -0
- package/dist/platform/clipboard.d.ts +15 -0
- package/dist/platform/clipboard.js +82 -0
- package/dist/platform/clipboard.js.map +1 -0
- package/dist/platform/exec.d.ts +28 -0
- package/dist/platform/exec.js +95 -0
- package/dist/platform/exec.js.map +1 -0
- package/dist/platform/file.d.ts +38 -0
- package/dist/platform/file.js +197 -0
- package/dist/platform/file.js.map +1 -0
- package/dist/platform/index.d.ts +7 -0
- package/dist/platform/index.js +12 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/notification.d.ts +14 -0
- package/dist/platform/notification.js +82 -0
- package/dist/platform/notification.js.map +1 -0
- package/dist/platform/open-external.d.ts +10 -0
- package/dist/platform/open-external.js +105 -0
- package/dist/platform/open-external.js.map +1 -0
- package/dist/platform/os.d.ts +20 -0
- package/dist/platform/os.js +86 -0
- package/dist/platform/os.js.map +1 -0
- package/dist/platform/path.d.ts +32 -0
- package/dist/platform/path.js +154 -0
- package/dist/platform/path.js.map +1 -0
- package/dist/plugin/access-guard.d.ts +1 -0
- package/dist/plugin/access-guard.js +40 -0
- package/dist/plugin/access-guard.js.map +1 -0
- package/dist/plugin/agent-service.d.ts +25 -0
- package/dist/plugin/agent-service.js +809 -0
- package/dist/plugin/agent-service.js.map +1 -0
- package/dist/plugin/agent-tools.d.ts +12 -0
- package/dist/plugin/agent-tools.js +31 -0
- package/dist/plugin/agent-tools.js.map +1 -0
- package/dist/plugin/capabilities.d.ts +26 -0
- package/dist/plugin/capabilities.js +192 -0
- package/dist/plugin/capabilities.js.map +1 -0
- package/dist/plugin/console-guard.d.ts +2 -0
- package/dist/plugin/console-guard.js +52 -0
- package/dist/plugin/console-guard.js.map +1 -0
- package/dist/plugin/data-access.d.ts +15 -0
- package/dist/plugin/data-access.js +57 -0
- package/dist/plugin/data-access.js.map +1 -0
- package/dist/plugin/fs.d.ts +3 -0
- package/dist/plugin/fs.js +93 -0
- package/dist/plugin/fs.js.map +1 -0
- package/dist/plugin/host-alias.d.ts +2 -0
- package/dist/plugin/host-alias.js +57 -0
- package/dist/plugin/host-alias.js.map +1 -0
- package/dist/plugin/index.d.ts +7 -0
- package/dist/plugin/index.js +11 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/install.d.ts +17 -0
- package/dist/plugin/install.js +163 -0
- package/dist/plugin/install.js.map +1 -0
- package/dist/plugin/loader.d.ts +45 -0
- package/dist/plugin/loader.js +739 -0
- package/dist/plugin/loader.js.map +1 -0
- package/dist/plugin/manage.d.ts +31 -0
- package/dist/plugin/manage.js +271 -0
- package/dist/plugin/manage.js.map +1 -0
- package/dist/plugin/models-service.d.ts +15 -0
- package/dist/plugin/models-service.js +317 -0
- package/dist/plugin/models-service.js.map +1 -0
- package/dist/plugin/network-service.d.ts +10 -0
- package/dist/plugin/network-service.js +87 -0
- package/dist/plugin/network-service.js.map +1 -0
- package/dist/plugin/package-manager.d.ts +46 -0
- package/dist/plugin/package-manager.js +148 -0
- package/dist/plugin/package-manager.js.map +1 -0
- package/dist/plugin/paths.d.ts +6 -0
- package/dist/plugin/paths.js +15 -0
- package/dist/plugin/paths.js.map +1 -0
- package/dist/plugin/registry.d.ts +17 -0
- package/dist/plugin/registry.js +82 -0
- package/dist/plugin/registry.js.map +1 -0
- package/dist/plugin/runtime.d.ts +9 -0
- package/dist/plugin/runtime.js +43 -0
- package/dist/plugin/runtime.js.map +1 -0
- package/dist/plugin/scaffold.d.ts +13 -0
- package/dist/plugin/scaffold.js +132 -0
- package/dist/plugin/scaffold.js.map +1 -0
- package/dist/plugin/state.d.ts +6 -0
- package/dist/plugin/state.js +38 -0
- package/dist/plugin/state.js.map +1 -0
- package/dist/plugin/zip.d.ts +7 -0
- package/dist/plugin/zip.js +138 -0
- package/dist/plugin/zip.js.map +1 -0
- package/dist/sandbox/audit.d.ts +12 -0
- package/dist/sandbox/audit.js +20 -0
- package/dist/sandbox/audit.js.map +1 -0
- package/dist/sandbox/backends/bwrap.d.ts +28 -0
- package/dist/sandbox/backends/bwrap.js +197 -0
- package/dist/sandbox/backends/bwrap.js.map +1 -0
- package/dist/sandbox/backends/direct.d.ts +7 -0
- package/dist/sandbox/backends/direct.js +32 -0
- package/dist/sandbox/backends/direct.js.map +1 -0
- package/dist/sandbox/bridge-state.d.ts +13 -0
- package/dist/sandbox/bridge-state.js +12 -0
- package/dist/sandbox/bridge-state.js.map +1 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +6 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/manager.d.ts +8 -0
- package/dist/sandbox/manager.js +325 -0
- package/dist/sandbox/manager.js.map +1 -0
- package/dist/sandbox/memory-limit.d.ts +44 -0
- package/dist/sandbox/memory-limit.js +193 -0
- package/dist/sandbox/memory-limit.js.map +1 -0
- package/dist/sandbox/plugin-client.d.ts +80 -0
- package/dist/sandbox/plugin-client.js +1601 -0
- package/dist/sandbox/plugin-client.js.map +1 -0
- package/dist/sandbox/plugin-host.d.ts +1 -0
- package/dist/sandbox/plugin-host.js +1001 -0
- package/dist/sandbox/plugin-host.js.map +1 -0
- package/dist/sandbox/protocol.d.ts +43 -0
- package/dist/sandbox/protocol.js +96 -0
- package/dist/sandbox/protocol.js.map +1 -0
- package/dist/sandbox/rpc-timeouts.d.ts +28 -0
- package/dist/sandbox/rpc-timeouts.js +15 -0
- package/dist/sandbox/rpc-timeouts.js.map +1 -0
- package/dist/sandbox/supervisor.d.ts +31 -0
- package/dist/sandbox/supervisor.js +114 -0
- package/dist/sandbox/supervisor.js.map +1 -0
- package/dist/sandbox/types.d.ts +124 -0
- package/dist/sandbox/types.js +17 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/service/control.d.ts +32 -0
- package/dist/service/control.js +117 -0
- package/dist/service/control.js.map +1 -0
- package/dist/session/compress.d.ts +3 -0
- package/dist/session/compress.js +42 -0
- package/dist/session/compress.js.map +1 -0
- package/dist/session/index.d.ts +4 -0
- package/dist/session/index.js +8 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/registry.d.ts +4 -0
- package/dist/session/registry.js +12 -0
- package/dist/session/registry.js.map +1 -0
- package/dist/session/replay.d.ts +10 -0
- package/dist/session/replay.js +109 -0
- package/dist/session/replay.js.map +1 -0
- package/dist/session/session.d.ts +40 -0
- package/dist/session/session.js +108 -0
- package/dist/session/session.js.map +1 -0
- package/dist/session/store.d.ts +58 -0
- package/dist/session/store.js +382 -0
- package/dist/session/store.js.map +1 -0
- package/dist/session/types.d.ts +56 -0
- package/dist/session/types.js +3 -0
- package/dist/session/types.js.map +1 -0
- package/dist/workspace/index.d.ts +7 -0
- package/dist/workspace/index.js +17 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/registry.d.ts +25 -0
- package/dist/workspace/registry.js +293 -0
- package/dist/workspace/registry.js.map +1 -0
- package/package.json +73 -5
- package/scripts/build-node-pty.mjs +55 -0
- package/scripts/rewrite-dts.mjs +36 -0
- package/scripts/run-tests.mjs +41 -0
- package/scripts/verify-sandbox.ts +98 -0
- package/src/agent/pending-asks.ts +88 -0
- package/src/agent/task-registry.ts +108 -0
- package/src/agent/tool-context.ts +47 -0
- package/src/agent/usage.ts +170 -0
- package/src/api/host/agent.ts +162 -0
- package/src/api/host/events.ts +7 -0
- package/src/api/host/exec.ts +47 -0
- package/src/api/host/fs.ts +33 -0
- package/src/api/host/hooks.ts +635 -0
- package/src/api/host/host.ts +99 -0
- package/src/api/host/http.ts +79 -0
- package/src/api/host/index.ts +20 -0
- package/src/api/host/models.ts +81 -0
- package/src/api/host/network.ts +32 -0
- package/src/api/host/plugin.ts +158 -0
- package/src/api/host/plugins.ts +59 -0
- package/src/api/host/pty.ts +73 -0
- package/src/api/host/session.ts +61 -0
- package/src/api/host/version.ts +2 -0
- package/src/api/host/workspace.ts +51 -0
- package/src/api/plugin/http-client.ts +492 -0
- package/src/api/plugin/http-protocol.ts +231 -0
- package/src/api/result/result.ts +52 -0
- package/src/commands/agent-turn.ts +817 -0
- package/src/commands/ask-prompt.ts +52 -0
- package/src/commands/config.ts +306 -0
- package/src/commands/conversation.ts +777 -0
- package/src/commands/doctor.ts +92 -0
- package/src/commands/exec-approval.ts +91 -0
- package/src/commands/help.ts +41 -0
- package/src/commands/key.ts +281 -0
- package/src/commands/plugin.ts +222 -0
- package/src/commands/session.ts +179 -0
- package/src/commands/shared.ts +103 -0
- package/src/commands/start.ts +88 -0
- package/src/commands/stop.ts +24 -0
- package/src/commands/version.ts +40 -0
- package/src/config/codes.ts +17 -0
- package/src/config/credentials.ts +302 -0
- package/src/config/defaults.ts +32 -0
- package/src/config/index.ts +10 -0
- package/src/config/loader.ts +80 -0
- package/src/config/providers.ts +133 -0
- package/src/config/types.ts +88 -0
- package/src/config/validate.ts +458 -0
- package/src/display/markdown.ts +137 -0
- package/src/display/tool-label.ts +87 -0
- package/src/event/event.ts +171 -0
- package/src/index.ts +344 -0
- package/src/input/codes.ts +21 -0
- package/src/input/commands.ts +76 -0
- package/src/input/help.ts +60 -0
- package/src/input/index.ts +10 -0
- package/src/input/parse-options.ts +66 -0
- package/src/input/parse.ts +138 -0
- package/src/input/types.ts +19 -0
- package/src/logger/elog4js.ts +366 -0
- package/src/logger/logger.ts +251 -0
- package/src/network/index.ts +6 -0
- package/src/network/openai.ts +571 -0
- package/src/network/proxy.ts +80 -0
- package/src/network/web.ts +206 -0
- package/src/platform/clipboard.ts +107 -0
- package/src/platform/exec.ts +128 -0
- package/src/platform/file.ts +235 -0
- package/src/platform/index.ts +12 -0
- package/src/platform/notification.ts +108 -0
- package/src/platform/open-external.ts +132 -0
- package/src/platform/os.ts +101 -0
- package/src/platform/path.ts +214 -0
- package/src/plugin/access-guard.ts +46 -0
- package/src/plugin/agent-service.ts +969 -0
- package/src/plugin/agent-tools.ts +43 -0
- package/src/plugin/capabilities.ts +240 -0
- package/src/plugin/console-guard.ts +66 -0
- package/src/plugin/data-access.ts +70 -0
- package/src/plugin/fs.ts +109 -0
- package/src/plugin/host-alias.ts +61 -0
- package/src/plugin/index.ts +11 -0
- package/src/plugin/install.ts +213 -0
- package/src/plugin/loader.ts +952 -0
- package/src/plugin/manage.ts +338 -0
- package/src/plugin/models-service.ts +383 -0
- package/src/plugin/network-service.ts +102 -0
- package/src/plugin/package-manager.ts +214 -0
- package/src/plugin/paths.ts +19 -0
- package/src/plugin/registry.ts +103 -0
- package/src/plugin/runtime.ts +57 -0
- package/src/plugin/scaffold.ts +158 -0
- package/src/plugin/state.ts +52 -0
- package/src/plugin/zip.ts +156 -0
- package/src/sandbox/audit.ts +38 -0
- package/src/sandbox/backends/bwrap.ts +251 -0
- package/src/sandbox/backends/direct.ts +43 -0
- package/src/sandbox/bridge-state.ts +29 -0
- package/src/sandbox/index.ts +5 -0
- package/src/sandbox/manager.ts +406 -0
- package/src/sandbox/memory-limit.ts +245 -0
- package/src/sandbox/plugin-client.ts +1881 -0
- package/src/sandbox/plugin-host.ts +1282 -0
- package/src/sandbox/protocol.ts +142 -0
- package/src/sandbox/pty-bridge.py +154 -0
- package/src/sandbox/rpc-timeouts.ts +43 -0
- package/src/sandbox/supervisor.ts +172 -0
- package/src/sandbox/types.ts +149 -0
- package/src/service/control.ts +155 -0
- package/src/session/compress.ts +47 -0
- package/src/session/index.ts +8 -0
- package/src/session/registry.ts +17 -0
- package/src/session/replay.ts +132 -0
- package/src/session/session.ts +134 -0
- package/src/session/store.ts +468 -0
- package/src/session/types.ts +61 -0
- package/src/workspace/index.ts +21 -0
- package/src/workspace/registry.ts +356 -0
|
@@ -0,0 +1,1881 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 宿主侧 sandbox 插件客户端:启动 child、握手、命令/钩子 RPC、host API 代理。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawn, type ChildProcess } from "node:child_process"
|
|
6
|
+
import { createServer as createHttpServer, type Server as HttpServer } from "node:http"
|
|
7
|
+
import { createServer as createHttpsServer } from "node:https"
|
|
8
|
+
import { WebSocketServer, type WebSocket } from "ws"
|
|
9
|
+
import { randomUUID } from "node:crypto"
|
|
10
|
+
import { cp, mkdir, rm, stat } from "node:fs/promises"
|
|
11
|
+
import { basename, dirname, join, relative, resolve } from "node:path"
|
|
12
|
+
|
|
13
|
+
import type { Logger } from "pino"
|
|
14
|
+
|
|
15
|
+
import type { AppSetting } from "../config/types.ts"
|
|
16
|
+
import { eventBus, EventEntity } from "../event/event.ts"
|
|
17
|
+
import {
|
|
18
|
+
addWorkspace,
|
|
19
|
+
listWorkspaces,
|
|
20
|
+
removeWorkspace,
|
|
21
|
+
renameWorkspace,
|
|
22
|
+
touchWorkspace,
|
|
23
|
+
} from "../workspace/registry.ts"
|
|
24
|
+
import { getElog } from "../logger/elog4js.ts"
|
|
25
|
+
import type { ResolvedPaths } from "../platform/path.ts"
|
|
26
|
+
import { createPluginFileApi } from "../plugin/fs.ts"
|
|
27
|
+
import type { PluginDataService } from "../plugin/data-access.ts"
|
|
28
|
+
import type { HostCapabilities } from "../plugin/capabilities.ts"
|
|
29
|
+
import { getAgentToolContext, type AgentToolContext } from "../agent/tool-context.ts"
|
|
30
|
+
import { ensurePluginDataDir } from "../plugin/runtime.ts"
|
|
31
|
+
import { auditSandbox } from "./audit.ts"
|
|
32
|
+
import { RPC_TIMEOUT_MS } from "./rpc-timeouts.ts"
|
|
33
|
+
import { getHost } from "../api/host/host.ts"
|
|
34
|
+
import { getSession as findSession } from "../session/registry.ts"
|
|
35
|
+
import { registerExternalHook, shutdown as shutdownHost } from "../api/host/hooks.ts"
|
|
36
|
+
import type {
|
|
37
|
+
AgentCallToolResult,
|
|
38
|
+
AgentTool,
|
|
39
|
+
AgentToolIntroduction,
|
|
40
|
+
PluginCommand,
|
|
41
|
+
PluginConfig,
|
|
42
|
+
} from "../api/host/plugin.ts"
|
|
43
|
+
import type { HostFileApi, HostFileEntry, HostFileStat } from "../api/host/fs.ts"
|
|
44
|
+
import {
|
|
45
|
+
HOST_EXEC_DEFAULT_TIMEOUT_MS,
|
|
46
|
+
HOST_EXEC_MAX_OUTPUT_BYTES,
|
|
47
|
+
type HostExecChunk,
|
|
48
|
+
} from "../api/host/exec.ts"
|
|
49
|
+
import {
|
|
50
|
+
HOST_PTY_DEFAULT_COLS,
|
|
51
|
+
HOST_PTY_DEFAULT_ROWS,
|
|
52
|
+
HOST_PTY_MAX_COLS,
|
|
53
|
+
HOST_PTY_MAX_OUTPUT_CHARS,
|
|
54
|
+
HOST_PTY_MAX_POLL_CHARS,
|
|
55
|
+
HOST_PTY_MAX_ROWS,
|
|
56
|
+
HOST_PTY_MAX_SESSIONS,
|
|
57
|
+
HOST_PTY_MIN_COLS,
|
|
58
|
+
HOST_PTY_MIN_ROWS,
|
|
59
|
+
type HostPtyChunk,
|
|
60
|
+
type HostPtySession,
|
|
61
|
+
} from "../api/host/pty.ts"
|
|
62
|
+
import type { HttpRequest, HttpResponse } from "../api/host/http.ts"
|
|
63
|
+
import type { HostAgentChatInput } from "../api/host/agent.ts"
|
|
64
|
+
import type { HostModelsApi, HostModelProviderInput } from "../api/host/models.ts"
|
|
65
|
+
import type { HostNetworkApi, HostNetworkProxyInput } from "../api/host/network.ts"
|
|
66
|
+
import type { HostPluginInstallInput, HostPluginsApi } from "../api/host/plugins.ts"
|
|
67
|
+
import type { HostAgentService } from "../plugin/agent-service.ts"
|
|
68
|
+
import { HTTP_MAX_BODY_BYTES } from "../api/host/http.ts"
|
|
69
|
+
import {
|
|
70
|
+
FrameDecoder,
|
|
71
|
+
encodeMessage,
|
|
72
|
+
nextRequestId,
|
|
73
|
+
type RpcMessage,
|
|
74
|
+
type RpcRequest,
|
|
75
|
+
type RpcResponse,
|
|
76
|
+
} from "./protocol.ts"
|
|
77
|
+
import type { PluginLaunchInput, SandboxBackend, SandboxLimits } from "./types.ts"
|
|
78
|
+
|
|
79
|
+
const HOOK_KIND_BY_API: Readonly<Record<string, string>> = {
|
|
80
|
+
onMessage: "message",
|
|
81
|
+
onSessionStart: "sessionStart",
|
|
82
|
+
beforeTurn: "beforeTurn",
|
|
83
|
+
beforeStep: "beforeStep",
|
|
84
|
+
afterStep: "afterStep",
|
|
85
|
+
afterTurn: "afterTurn",
|
|
86
|
+
onAgentRunSync: "agentRunSync",
|
|
87
|
+
onAgentRunAsync: "agentRunAsync",
|
|
88
|
+
onAgentAsyncTaskComplete: "agentAsyncTaskComplete",
|
|
89
|
+
onAgentCallTool: "agentCallTool",
|
|
90
|
+
onAgentAsk: "agentAsk",
|
|
91
|
+
onAgentRead: "agentRead",
|
|
92
|
+
onAgentQueryTask: "agentQueryTask",
|
|
93
|
+
onAgentSubmit: "agentSubmit",
|
|
94
|
+
onAgentSetTasks: "agentSetTasks",
|
|
95
|
+
onAgentSetGoal: "agentSetGoal",
|
|
96
|
+
onAgentRequestPermission: "agentRequestPermission",
|
|
97
|
+
onUserAnswer: "userAnswer",
|
|
98
|
+
onInit: "init",
|
|
99
|
+
onShutdown: "shutdown",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const activeClients = new Set<PluginSandboxClient>()
|
|
103
|
+
|
|
104
|
+
export async function shutdownAllSandboxPlugins(): Promise<void> {
|
|
105
|
+
const clients = [...activeClients]
|
|
106
|
+
activeClients.clear()
|
|
107
|
+
await Promise.all(clients.map(async (client) => await client.close().catch(() => undefined)))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const PROJECT_ROOT = resolve(import.meta.dirname, "../..")
|
|
111
|
+
|
|
112
|
+
export interface SandboxPluginMetadata {
|
|
113
|
+
readonly id: string
|
|
114
|
+
readonly name: string
|
|
115
|
+
readonly version: string
|
|
116
|
+
readonly status: "ok" | "failed"
|
|
117
|
+
readonly deps: readonly string[]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface SandboxPluginCommandMeta {
|
|
121
|
+
readonly name: string
|
|
122
|
+
readonly summary: string
|
|
123
|
+
readonly usage?: string
|
|
124
|
+
readonly aliases?: readonly string[]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface SandboxAgentToolMeta {
|
|
128
|
+
readonly name: string
|
|
129
|
+
readonly introduction: AgentToolIntroduction
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface SandboxPluginReady {
|
|
133
|
+
readonly plugin: SandboxPluginMetadata
|
|
134
|
+
readonly commands: readonly SandboxPluginCommandMeta[]
|
|
135
|
+
readonly agentTools: readonly SandboxAgentToolMeta[]
|
|
136
|
+
readonly hooks: readonly string[]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** stat 但不存在时返回 null,用于 cwd 校验 */
|
|
140
|
+
async function statSafe(path: string) {
|
|
141
|
+
try {
|
|
142
|
+
return await stat(path)
|
|
143
|
+
} catch {
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 结束整棵进程树:detached 启动时先杀进程组,再兜底 kill */
|
|
149
|
+
function killSession(session: ExecSession): void {
|
|
150
|
+
const child = session.child
|
|
151
|
+
if (child.killed || child.exitCode !== null) return
|
|
152
|
+
if (process.platform !== "win32" && typeof child.pid === "number") {
|
|
153
|
+
try {
|
|
154
|
+
process.kill(-child.pid, "SIGKILL")
|
|
155
|
+
return
|
|
156
|
+
} catch {
|
|
157
|
+
/* 进程组可能已消失,退回单进程 kill */
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
child.kill("SIGKILL")
|
|
162
|
+
} catch {
|
|
163
|
+
/* 已经退出 */
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** node-pty 进程的最小结构,避免把可选依赖变成静态类型依赖 */
|
|
168
|
+
interface NodePtyProcess {
|
|
169
|
+
readonly pid: number
|
|
170
|
+
onData(handler: (data: string) => void): void
|
|
171
|
+
onExit(handler: (event: { exitCode: number; signal?: number }) => void): void
|
|
172
|
+
write(data: string): void
|
|
173
|
+
resize(cols: number, rows: number): void
|
|
174
|
+
kill(signal?: string): void
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface NodePtyModule {
|
|
178
|
+
spawn(file: string, args: string[], options: Record<string, unknown>): NodePtyProcess
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** 后端无关的 PTY 句柄 */
|
|
182
|
+
interface PtyHandle {
|
|
183
|
+
readonly pid: number
|
|
184
|
+
write(data: string): void
|
|
185
|
+
resize(cols: number, rows: number): void
|
|
186
|
+
kill(): void
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
type PtyBackendName = "node-pty" | "python-pty"
|
|
190
|
+
|
|
191
|
+
/** PTY 会话的宿主侧状态 */
|
|
192
|
+
interface PtySession {
|
|
193
|
+
readonly handle: PtyHandle
|
|
194
|
+
readonly backend: PtyBackendName
|
|
195
|
+
readonly file: string
|
|
196
|
+
readonly cwd: string
|
|
197
|
+
cols: number
|
|
198
|
+
rows: number
|
|
199
|
+
output: string
|
|
200
|
+
delivered: number
|
|
201
|
+
done: boolean
|
|
202
|
+
exitCode: number | null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** 无需原生模块的 PTY 后端:python3 + pty-bridge.py */
|
|
206
|
+
const PTY_BRIDGE_PATH = join(PROJECT_ROOT, "src", "sandbox", "pty-bridge.py")
|
|
207
|
+
|
|
208
|
+
let pythonPtyProbe: Promise<string | null> | null = null
|
|
209
|
+
|
|
210
|
+
/** 探测可用的 python 解释器;没有就返回 null */
|
|
211
|
+
async function probePython(): Promise<string | null> {
|
|
212
|
+
if (pythonPtyProbe === null) {
|
|
213
|
+
pythonPtyProbe = (async () => {
|
|
214
|
+
for (const candidate of ["python3", "python"]) {
|
|
215
|
+
const ok = await new Promise<boolean>((resolveOk) => {
|
|
216
|
+
try {
|
|
217
|
+
const probe = spawn(candidate, ["-c", "import pty, termios"], {
|
|
218
|
+
stdio: "ignore",
|
|
219
|
+
})
|
|
220
|
+
probe.on("error", () => resolveOk(false))
|
|
221
|
+
probe.on("close", (code) => resolveOk(code === 0))
|
|
222
|
+
} catch {
|
|
223
|
+
resolveOk(false)
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
if (ok) return candidate
|
|
227
|
+
}
|
|
228
|
+
return null
|
|
229
|
+
})()
|
|
230
|
+
}
|
|
231
|
+
return await pythonPtyProbe
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** PTY 尺寸夹在合理范围,避免异常值把终端撑爆;cols 与 rows 的最小值不同 */
|
|
235
|
+
function clampSize(value: unknown, fallback: number, min: number, max: number): number {
|
|
236
|
+
const size = typeof value === "number" ? Math.round(value) : Number.NaN
|
|
237
|
+
if (!Number.isFinite(size)) return fallback
|
|
238
|
+
return Math.min(max, Math.max(min, size))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function clampCols(value: unknown): number {
|
|
242
|
+
return clampSize(value, HOST_PTY_DEFAULT_COLS, HOST_PTY_MIN_COLS, HOST_PTY_MAX_COLS)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function clampRows(value: unknown): number {
|
|
246
|
+
return clampSize(value, HOST_PTY_DEFAULT_ROWS, HOST_PTY_MIN_ROWS, HOST_PTY_MAX_ROWS)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** 默认登录 shell */
|
|
250
|
+
function defaultShell(): string {
|
|
251
|
+
if (process.platform === "win32") {
|
|
252
|
+
return process.env.ComSpec ?? "powershell.exe"
|
|
253
|
+
}
|
|
254
|
+
return process.env.SHELL ?? "/bin/bash"
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** node-pty 是可选依赖:只尝试加载一次,失败就记住不可用 */
|
|
258
|
+
let nodePtyPromise: Promise<NodePtyModule | null> | null = null
|
|
259
|
+
/** node-pty 加载失败的原因,便于排查是没装还是原生模块装坏了 */
|
|
260
|
+
let nodePtyError: string | null = null
|
|
261
|
+
|
|
262
|
+
async function loadNodePty(): Promise<NodePtyModule | null> {
|
|
263
|
+
if (nodePtyPromise === null) {
|
|
264
|
+
nodePtyPromise = (async () => {
|
|
265
|
+
try {
|
|
266
|
+
// 变量 specifier:node-pty 可能根本没装,不能做成静态 import
|
|
267
|
+
const specifier = "node-pty"
|
|
268
|
+
const module = (await import(specifier)) as unknown as {
|
|
269
|
+
spawn?: NodePtyModule["spawn"]
|
|
270
|
+
default?: { spawn?: NodePtyModule["spawn"] }
|
|
271
|
+
}
|
|
272
|
+
const spawnPty = module.spawn ?? module.default?.spawn
|
|
273
|
+
if (typeof spawnPty !== "function") {
|
|
274
|
+
nodePtyError = "node-pty 已加载但没有导出 spawn"
|
|
275
|
+
return null
|
|
276
|
+
}
|
|
277
|
+
nodePtyError = null
|
|
278
|
+
return module as unknown as NodePtyModule
|
|
279
|
+
} catch (error) {
|
|
280
|
+
// 未安装与原生模块加载失败是两类问题,原因要原样带出去
|
|
281
|
+
nodePtyError = error instanceof Error ? error.message : String(error)
|
|
282
|
+
return null
|
|
283
|
+
}
|
|
284
|
+
})()
|
|
285
|
+
}
|
|
286
|
+
return await nodePtyPromise
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** 终端会话的宿主侧状态 */
|
|
290
|
+
interface ExecSession {
|
|
291
|
+
readonly child: ChildProcess
|
|
292
|
+
readonly cwd: string
|
|
293
|
+
readonly startedAt: number
|
|
294
|
+
output: string
|
|
295
|
+
/** 已交付给插件 poll 的字符数 */
|
|
296
|
+
delivered: number
|
|
297
|
+
done: boolean
|
|
298
|
+
exitCode: number | null
|
|
299
|
+
timedOut: boolean
|
|
300
|
+
truncated: boolean
|
|
301
|
+
timer: NodeJS.Timeout | null
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export interface PluginSandboxPermissions {
|
|
305
|
+
readonly hostApi: readonly string[]
|
|
306
|
+
readonly workspaceRead: boolean
|
|
307
|
+
readonly workspaceWrite: boolean
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export interface PluginSandboxClientOptions {
|
|
311
|
+
readonly pluginId: string
|
|
312
|
+
readonly pluginName: string
|
|
313
|
+
readonly pluginVersion: string
|
|
314
|
+
readonly pluginDir: string
|
|
315
|
+
readonly pluginEntry: string
|
|
316
|
+
readonly paths: ResolvedPaths
|
|
317
|
+
readonly config: AppSetting
|
|
318
|
+
readonly pluginConfig: PluginConfig
|
|
319
|
+
readonly backend: SandboxBackend
|
|
320
|
+
readonly limits: SandboxLimits
|
|
321
|
+
readonly logger: Logger
|
|
322
|
+
readonly permissions: PluginSandboxPermissions
|
|
323
|
+
readonly dataAccess?: PluginDataService
|
|
324
|
+
readonly capabilities?: HostCapabilities
|
|
325
|
+
readonly agent?: HostAgentService
|
|
326
|
+
readonly models?: HostModelsApi
|
|
327
|
+
readonly network?: HostNetworkApi
|
|
328
|
+
readonly plugins?: HostPluginsApi
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export class PluginSandboxError extends Error {
|
|
332
|
+
constructor(message: string) {
|
|
333
|
+
super(message)
|
|
334
|
+
this.name = "PluginSandboxError"
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
interface PendingRequest {
|
|
339
|
+
readonly resolve: (value: unknown) => void
|
|
340
|
+
readonly reject: (error: Error) => void
|
|
341
|
+
readonly timer: NodeJS.Timeout
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function hostSnapshotData(): unknown {
|
|
345
|
+
const host = getHost()
|
|
346
|
+
return {
|
|
347
|
+
status: host.getStatus(),
|
|
348
|
+
os: host.getOS(),
|
|
349
|
+
paths: host.getPaths(),
|
|
350
|
+
plugins: host.getPlugins().map((plugin) => ({
|
|
351
|
+
id: plugin.id,
|
|
352
|
+
name: plugin.name,
|
|
353
|
+
version: plugin.version,
|
|
354
|
+
deps: plugin.deps,
|
|
355
|
+
commands: plugin.commands,
|
|
356
|
+
status: plugin.getStatus(),
|
|
357
|
+
})),
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export class PluginSandboxClient {
|
|
362
|
+
readonly #options: PluginSandboxClientOptions
|
|
363
|
+
readonly #sandboxId = randomUUID()
|
|
364
|
+
readonly #pending = new Map<string, PendingRequest>()
|
|
365
|
+
readonly #decoder = new FrameDecoder()
|
|
366
|
+
readonly #eventUnsubscribes = new Map<string, () => void>()
|
|
367
|
+
readonly #hookUnsubscribes: Array<() => void> = []
|
|
368
|
+
#activeContext: AgentToolContext | null = null
|
|
369
|
+
readonly #httpServers = new Map<
|
|
370
|
+
string,
|
|
371
|
+
{
|
|
372
|
+
readonly server: HttpServer
|
|
373
|
+
readonly host: string
|
|
374
|
+
readonly port: number
|
|
375
|
+
readonly wsServer: WebSocketServer | null
|
|
376
|
+
}
|
|
377
|
+
>()
|
|
378
|
+
readonly #httpResponses = new Map<
|
|
379
|
+
string,
|
|
380
|
+
{
|
|
381
|
+
readonly response: import("node:http").ServerResponse
|
|
382
|
+
/** 响应头尚未应用时先缓存 chunk,避免 set headers 之后的写入报错 */
|
|
383
|
+
readonly chunks: string[]
|
|
384
|
+
ready: boolean
|
|
385
|
+
ended: boolean
|
|
386
|
+
}
|
|
387
|
+
>()
|
|
388
|
+
readonly #wsConnections = new Map<string, WebSocket>()
|
|
389
|
+
/** 终端执行会话:输出缓冲留在宿主侧,插件按需 poll */
|
|
390
|
+
readonly #execSessions = new Map<string, ExecSession>()
|
|
391
|
+
/** PTY 会话:真 tty,支持键盘输入与 ANSI 全屏程序 */
|
|
392
|
+
readonly #ptySessions = new Map<string, PtySession>()
|
|
393
|
+
#child: ChildProcess | null = null
|
|
394
|
+
#ready: SandboxPluginReady | null = null
|
|
395
|
+
#closed = false
|
|
396
|
+
#pluginDataDir: string
|
|
397
|
+
#sandboxDir: string
|
|
398
|
+
#hostSdkDir: string
|
|
399
|
+
|
|
400
|
+
constructor(options: PluginSandboxClientOptions) {
|
|
401
|
+
this.#options = options
|
|
402
|
+
this.#pluginDataDir = ""
|
|
403
|
+
this.#sandboxDir = join(options.paths.dataDir, "sandbox", this.#sandboxId)
|
|
404
|
+
this.#hostSdkDir = join(this.#sandboxDir, "host")
|
|
405
|
+
activeClients.add(this)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
get sandboxId(): string {
|
|
409
|
+
return this.#sandboxId
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
get ready(): SandboxPluginReady | null {
|
|
413
|
+
return this.#ready
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
get pluginDataDir(): string {
|
|
417
|
+
return this.#pluginDataDir
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** sandbox 插件声明的 Agent 工具,call 走 RPC */
|
|
421
|
+
agentToolStubs(): readonly AgentTool[] {
|
|
422
|
+
const tools = this.#ready?.agentTools ?? []
|
|
423
|
+
return tools.map((meta) => ({
|
|
424
|
+
name: meta.name,
|
|
425
|
+
introduction: {
|
|
426
|
+
params: { ...meta.introduction.params },
|
|
427
|
+
content: meta.introduction.content,
|
|
428
|
+
},
|
|
429
|
+
call: async (...args: unknown[]): Promise<AgentCallToolResult> => {
|
|
430
|
+
const input = args.length === 1 ? args[0] : args
|
|
431
|
+
const previous = this.#activeContext
|
|
432
|
+
this.#activeContext = getAgentToolContext()
|
|
433
|
+
try {
|
|
434
|
+
return (await this.#request(
|
|
435
|
+
"plugin.agentTool",
|
|
436
|
+
{ name: meta.name, args: input },
|
|
437
|
+
RPC_TIMEOUT_MS.agentTool,
|
|
438
|
+
)) as AgentCallToolResult
|
|
439
|
+
} finally {
|
|
440
|
+
this.#activeContext = previous
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
}))
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async start(options: { readonly deferInit?: boolean } = {}): Promise<SandboxPluginReady> {
|
|
447
|
+
this.#pluginDataDir = await ensurePluginDataDir(this.#options.pluginId, this.#options.paths)
|
|
448
|
+
await mkdir(this.#sandboxDir, { recursive: true })
|
|
449
|
+
|
|
450
|
+
const isBwrap = this.#options.backend.name === "bwrap"
|
|
451
|
+
/*
|
|
452
|
+
* 总是把宿主 SDK 复制到 dataDir 再运行:
|
|
453
|
+
* - bwrap 后端把 dataDir 里的副本挂到 /host;
|
|
454
|
+
* - direct 后端从 node_modules 里直接跑 .ts 会触发
|
|
455
|
+
* ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING,复制到 dataDir 后
|
|
456
|
+
* Node 才能按源码运行插件 host runner。
|
|
457
|
+
*/
|
|
458
|
+
await this.#prepareHostSdk()
|
|
459
|
+
|
|
460
|
+
const pluginEntryRel = relative(this.#options.pluginDir, this.#options.pluginEntry)
|
|
461
|
+
const env: Record<string, string> = {
|
|
462
|
+
PATH: "/usr/bin:/bin",
|
|
463
|
+
LANG: "C.UTF-8",
|
|
464
|
+
LC_ALL: "C.UTF-8",
|
|
465
|
+
TERM: "dumb",
|
|
466
|
+
HOME: "/tmp",
|
|
467
|
+
TMPDIR: "/tmp",
|
|
468
|
+
}
|
|
469
|
+
const input: PluginLaunchInput = {
|
|
470
|
+
pluginDir: this.#options.pluginDir,
|
|
471
|
+
pluginEntryRel,
|
|
472
|
+
pluginDataDir: this.#pluginDataDir,
|
|
473
|
+
hostSdkDir: this.#hostSdkDir,
|
|
474
|
+
runnerPath: join(this.#hostSdkDir, "src", "sandbox", "plugin-host.ts"),
|
|
475
|
+
env,
|
|
476
|
+
limits: this.#options.limits,
|
|
477
|
+
}
|
|
478
|
+
const spec = this.#options.backend.buildPluginLaunch(input)
|
|
479
|
+
const child = spawn(spec.command, [...spec.args], {
|
|
480
|
+
cwd: spec.cwd,
|
|
481
|
+
env: { ...spec.env },
|
|
482
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
483
|
+
detached: true,
|
|
484
|
+
})
|
|
485
|
+
this.#child = child
|
|
486
|
+
|
|
487
|
+
let stderrBuffer = ""
|
|
488
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
489
|
+
stderrBuffer += chunk.toString("utf8")
|
|
490
|
+
if (stderrBuffer.length > 64 * 1024) {
|
|
491
|
+
stderrBuffer = stderrBuffer.slice(-64 * 1024)
|
|
492
|
+
}
|
|
493
|
+
})
|
|
494
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
495
|
+
let messages: RpcMessage[]
|
|
496
|
+
try {
|
|
497
|
+
messages = this.#decoder.push(chunk)
|
|
498
|
+
} catch (error) {
|
|
499
|
+
this.#failAll(error instanceof Error ? error : new Error(String(error)))
|
|
500
|
+
this.#kill()
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
for (const message of messages) {
|
|
504
|
+
void this.#handleMessage(message)
|
|
505
|
+
}
|
|
506
|
+
})
|
|
507
|
+
child.once("exit", (code, signal) => {
|
|
508
|
+
if (!this.#closed) {
|
|
509
|
+
this.#failAll(
|
|
510
|
+
new PluginSandboxError(
|
|
511
|
+
`插件 sandbox 退出(code=${String(code)}, signal=${String(signal)}):${stderrBuffer.trim()}`,
|
|
512
|
+
),
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
this.#closed = true
|
|
516
|
+
})
|
|
517
|
+
child.once("error", (error) => {
|
|
518
|
+
this.#failAll(
|
|
519
|
+
new PluginSandboxError(
|
|
520
|
+
`插件 sandbox 启动失败:${error instanceof Error ? error.message : String(error)}`,
|
|
521
|
+
),
|
|
522
|
+
)
|
|
523
|
+
this.#closed = true
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
const configureResult = (await this.#request("sandbox.configure", {
|
|
527
|
+
pluginId: this.#options.pluginId,
|
|
528
|
+
pluginName: this.#options.pluginName,
|
|
529
|
+
pluginVersion: this.#options.pluginVersion,
|
|
530
|
+
pluginEntry: isBwrap
|
|
531
|
+
? `/plugin/${pluginEntryRel.split("\\").join("/")}`
|
|
532
|
+
: resolve(this.#options.pluginEntry),
|
|
533
|
+
pluginDataDir: isBwrap ? "/plugin-data" : this.#pluginDataDir,
|
|
534
|
+
pluginConfig: this.#options.pluginConfig,
|
|
535
|
+
paths: this.#options.paths,
|
|
536
|
+
config: this.#options.config,
|
|
537
|
+
host: this.#hasHostApi("getHost") ? hostSnapshotData() : null,
|
|
538
|
+
deferInit: options.deferInit === true,
|
|
539
|
+
})) as SandboxPluginReady
|
|
540
|
+
|
|
541
|
+
this.#ready = configureResult
|
|
542
|
+
if (options.deferInit !== true) {
|
|
543
|
+
this.#registerRemoteHooks(configureResult.hooks)
|
|
544
|
+
}
|
|
545
|
+
auditSandbox(this.#options.paths, {
|
|
546
|
+
type: "sandbox.plugin.start",
|
|
547
|
+
sandboxId: this.#sandboxId,
|
|
548
|
+
pluginId: this.#options.pluginId,
|
|
549
|
+
backend: this.#options.backend.name,
|
|
550
|
+
decision: "allowed",
|
|
551
|
+
permissions: this.#options.permissions.hostApi,
|
|
552
|
+
})
|
|
553
|
+
return configureResult
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async initialize(): Promise<SandboxPluginReady> {
|
|
557
|
+
const result = (await this.#request("plugin.init", {})) as SandboxPluginReady
|
|
558
|
+
this.#ready = result
|
|
559
|
+
this.#registerRemoteHooks(result.hooks)
|
|
560
|
+
return result
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async #request(method: string, params: unknown, timeoutMs = 30_000): Promise<unknown> {
|
|
564
|
+
if (this.#closed || this.#child === null) {
|
|
565
|
+
throw new PluginSandboxError("插件 sandbox 已关闭")
|
|
566
|
+
}
|
|
567
|
+
const id = nextRequestId("host")
|
|
568
|
+
return await new Promise<unknown>((resolveRequest, rejectRequest) => {
|
|
569
|
+
const timer = setTimeout(() => {
|
|
570
|
+
this.#pending.delete(id)
|
|
571
|
+
rejectRequest(new PluginSandboxError(`插件 RPC 超时:${method}`))
|
|
572
|
+
}, timeoutMs)
|
|
573
|
+
timer.unref()
|
|
574
|
+
this.#pending.set(id, {
|
|
575
|
+
resolve: (value) => {
|
|
576
|
+
clearTimeout(timer)
|
|
577
|
+
resolveRequest(value)
|
|
578
|
+
},
|
|
579
|
+
reject: (error) => {
|
|
580
|
+
clearTimeout(timer)
|
|
581
|
+
rejectRequest(error)
|
|
582
|
+
},
|
|
583
|
+
timer,
|
|
584
|
+
})
|
|
585
|
+
this.#send({ v: 1, type: "request", id, method, params, timeoutMs })
|
|
586
|
+
})
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
#notify(method: string, params: unknown): void {
|
|
590
|
+
if (this.#closed || this.#child === null) {
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
this.#send({ v: 1, type: "event", method, params })
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
#send(message: RpcMessage): void {
|
|
597
|
+
if (this.#child?.stdin === null || this.#child?.stdin === undefined) {
|
|
598
|
+
return
|
|
599
|
+
}
|
|
600
|
+
try {
|
|
601
|
+
this.#child.stdin.write(encodeMessage(message))
|
|
602
|
+
} catch {
|
|
603
|
+
// child 已退出;pending 会由 exit 处理
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
#respond(response: RpcResponse): void {
|
|
608
|
+
this.#send(response)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async #handleMessage(message: RpcMessage): Promise<void> {
|
|
612
|
+
if (message.type === "response") {
|
|
613
|
+
const pending = this.#pending.get(message.id)
|
|
614
|
+
if (pending === undefined) {
|
|
615
|
+
return
|
|
616
|
+
}
|
|
617
|
+
this.#pending.delete(message.id)
|
|
618
|
+
if (message.ok) {
|
|
619
|
+
pending.resolve(message.result)
|
|
620
|
+
} else {
|
|
621
|
+
pending.reject(new PluginSandboxError(message.error?.message ?? "插件 RPC 失败"))
|
|
622
|
+
}
|
|
623
|
+
return
|
|
624
|
+
}
|
|
625
|
+
if (message.type === "event") {
|
|
626
|
+
this.#handleEvent(message.method, message.params)
|
|
627
|
+
return
|
|
628
|
+
}
|
|
629
|
+
await this.#handleRequest(message)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
#handleEvent(method: string, params: unknown): void {
|
|
633
|
+
const data = (params ?? {}) as Record<string, unknown>
|
|
634
|
+
if (method === "host.http.stream") {
|
|
635
|
+
const requestId = String(data["requestId"] ?? "")
|
|
636
|
+
const pending = this.#httpResponses.get(requestId)
|
|
637
|
+
if (pending === undefined) {
|
|
638
|
+
return
|
|
639
|
+
}
|
|
640
|
+
if (!pending.ready) {
|
|
641
|
+
if (data["end"] === true) {
|
|
642
|
+
pending.ended = true
|
|
643
|
+
} else {
|
|
644
|
+
pending.chunks.push(String(data["chunk"] ?? ""))
|
|
645
|
+
}
|
|
646
|
+
return
|
|
647
|
+
}
|
|
648
|
+
try {
|
|
649
|
+
if (data["end"] === true) {
|
|
650
|
+
this.#httpResponses.delete(requestId)
|
|
651
|
+
pending.response.end()
|
|
652
|
+
} else {
|
|
653
|
+
pending.response.write(String(data["chunk"] ?? ""))
|
|
654
|
+
}
|
|
655
|
+
} catch {
|
|
656
|
+
this.#httpResponses.delete(requestId)
|
|
657
|
+
}
|
|
658
|
+
return
|
|
659
|
+
}
|
|
660
|
+
const pluginLogger = this.#options.logger.child({ plugin: this.#options.pluginId })
|
|
661
|
+
if (method === "host.log") {
|
|
662
|
+
const level = String(data["level"] ?? "info") as "info" | "warn" | "error"
|
|
663
|
+
const fields = (data["fields"] as Record<string, unknown> | undefined) ?? {}
|
|
664
|
+
const message = String(data["message"] ?? "")
|
|
665
|
+
pluginLogger[level]?.(fields, message)
|
|
666
|
+
return
|
|
667
|
+
}
|
|
668
|
+
if (method === "host.console") {
|
|
669
|
+
const level = String(data["level"] ?? "info") as "info" | "warn" | "error"
|
|
670
|
+
const args = Array.isArray(data["args"]) ? data["args"].map(String) : []
|
|
671
|
+
pluginLogger.error(`插件 [${this.#options.pluginId}] 使用了不推荐的 console logger !`)
|
|
672
|
+
pluginLogger[level]?.(`${this.#options.pluginId} ${args.join(" ")}`)
|
|
673
|
+
return
|
|
674
|
+
}
|
|
675
|
+
if (method === "host.output") {
|
|
676
|
+
const kind = String(data["kind"] ?? "info") as "info" | "warn" | "error" | "print"
|
|
677
|
+
const text = String(data["text"] ?? "")
|
|
678
|
+
getElog().plugin(kind === "print" ? "info" : kind, this.#options.pluginId, text)
|
|
679
|
+
return
|
|
680
|
+
}
|
|
681
|
+
if (method === "host.event.publish") {
|
|
682
|
+
if (!this.#hasHostApi("event")) {
|
|
683
|
+
pluginLogger.warn("sandbox 插件没有 event 权限,已拒绝发布事件")
|
|
684
|
+
return
|
|
685
|
+
}
|
|
686
|
+
const name = String(data["name"] ?? "")
|
|
687
|
+
try {
|
|
688
|
+
eventBus.publish(name, EventEntity.create(data["data"]))
|
|
689
|
+
} catch (error) {
|
|
690
|
+
pluginLogger.warn(
|
|
691
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
692
|
+
"sandbox 插件发布事件失败",
|
|
693
|
+
)
|
|
694
|
+
}
|
|
695
|
+
return
|
|
696
|
+
}
|
|
697
|
+
if (method === "host.event.unsubscribe") {
|
|
698
|
+
const name = String(data["name"] ?? "")
|
|
699
|
+
const unsubscribe = this.#eventUnsubscribes.get(name)
|
|
700
|
+
unsubscribe?.()
|
|
701
|
+
this.#eventUnsubscribes.delete(name)
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** 模型供应商与切换:state / setActive / saveProvider / removeProvider / fetchModels / saveKey / clearKey */
|
|
706
|
+
async #handleModels(method: string, params: Record<string, unknown>): Promise<unknown> {
|
|
707
|
+
const service = this.#options.models
|
|
708
|
+
if (service === undefined) {
|
|
709
|
+
throw new PluginSandboxError("宿主没有模型配置服务")
|
|
710
|
+
}
|
|
711
|
+
if (method === "host.models.state") {
|
|
712
|
+
return await service.state()
|
|
713
|
+
}
|
|
714
|
+
if (method === "host.models.setActive") {
|
|
715
|
+
const model = params["model"]
|
|
716
|
+
const reasoning = params["reasoning"]
|
|
717
|
+
return await service.setActive(
|
|
718
|
+
String(params["providerId"] ?? ""),
|
|
719
|
+
typeof model === "string" && model.trim() !== "" ? model : null,
|
|
720
|
+
typeof reasoning === "string" && reasoning.trim() !== "" ? reasoning : null,
|
|
721
|
+
)
|
|
722
|
+
}
|
|
723
|
+
if (method === "host.models.saveProvider") {
|
|
724
|
+
return await service.saveProvider(params as unknown as HostModelProviderInput)
|
|
725
|
+
}
|
|
726
|
+
if (method === "host.models.removeProvider") {
|
|
727
|
+
return await service.removeProvider(String(params["id"] ?? ""))
|
|
728
|
+
}
|
|
729
|
+
if (method === "host.models.fetchModels") {
|
|
730
|
+
const candidate = params["apiKey"]
|
|
731
|
+
return await service.fetchModels(
|
|
732
|
+
String(params["id"] ?? ""),
|
|
733
|
+
typeof candidate === "string" ? candidate : undefined,
|
|
734
|
+
)
|
|
735
|
+
}
|
|
736
|
+
if (method === "host.models.saveKey") {
|
|
737
|
+
return await service.saveKey(String(params["id"] ?? ""), String(params["apiKey"] ?? ""))
|
|
738
|
+
}
|
|
739
|
+
if (method === "host.models.clearKey") {
|
|
740
|
+
return await service.clearKey(String(params["id"] ?? ""))
|
|
741
|
+
}
|
|
742
|
+
throw new PluginSandboxError(`不支持的 models 方法:${method}`)
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async #handleNetwork(method: string, params: Record<string, unknown>): Promise<unknown> {
|
|
746
|
+
const service = this.#options.network
|
|
747
|
+
if (service === undefined) {
|
|
748
|
+
throw new PluginSandboxError("宿主没有网络配置服务")
|
|
749
|
+
}
|
|
750
|
+
if (method === "host.network.state") {
|
|
751
|
+
return await service.state()
|
|
752
|
+
}
|
|
753
|
+
if (method === "host.network.saveProxy") {
|
|
754
|
+
return await service.saveProxy(params as unknown as HostNetworkProxyInput)
|
|
755
|
+
}
|
|
756
|
+
throw new PluginSandboxError(`不支持的 network 方法:${method}`)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** 插件管理:list / install / remove / enable / disable */
|
|
760
|
+
async #handlePlugins(method: string, params: Record<string, unknown>): Promise<unknown> {
|
|
761
|
+
const service = this.#options.plugins
|
|
762
|
+
if (service === undefined) {
|
|
763
|
+
throw new PluginSandboxError("宿主没有插件管理服务")
|
|
764
|
+
}
|
|
765
|
+
if (method === "host.plugins.list") {
|
|
766
|
+
return await service.list()
|
|
767
|
+
}
|
|
768
|
+
if (method === "host.plugins.install") {
|
|
769
|
+
return await service.install(params as unknown as HostPluginInstallInput)
|
|
770
|
+
}
|
|
771
|
+
if (method === "host.plugins.remove") {
|
|
772
|
+
return await service.remove(String(params["id"] ?? ""))
|
|
773
|
+
}
|
|
774
|
+
if (method === "host.plugins.enable") {
|
|
775
|
+
return await service.enable(String(params["id"] ?? ""))
|
|
776
|
+
}
|
|
777
|
+
if (method === "host.plugins.disable") {
|
|
778
|
+
return await service.disable(String(params["id"] ?? ""))
|
|
779
|
+
}
|
|
780
|
+
throw new PluginSandboxError(`不支持的 plugins 方法:${method}`)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** 终端执行:run/poll/stop 三个动作,输出缓冲留在宿主侧 */
|
|
784
|
+
async #handleExec(method: string, params: Record<string, unknown>): Promise<unknown> {
|
|
785
|
+
if (method === "host.exec.run") {
|
|
786
|
+
return await this.#execRun(params)
|
|
787
|
+
}
|
|
788
|
+
if (method === "host.exec.poll") {
|
|
789
|
+
return this.#execPoll(String(params["sessionId"] ?? ""))
|
|
790
|
+
}
|
|
791
|
+
if (method === "host.exec.stop") {
|
|
792
|
+
this.#execStop(String(params["sessionId"] ?? ""))
|
|
793
|
+
return {}
|
|
794
|
+
}
|
|
795
|
+
throw new PluginSandboxError(`不支持的 exec 方法:${method}`)
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async #execRun(params: Record<string, unknown>): Promise<{
|
|
799
|
+
sessionId: string
|
|
800
|
+
pid: number | null
|
|
801
|
+
cwd: string
|
|
802
|
+
}> {
|
|
803
|
+
const command = String(params["command"] ?? "").trim()
|
|
804
|
+
if (command === "") {
|
|
805
|
+
throw new PluginSandboxError("命令不能为空")
|
|
806
|
+
}
|
|
807
|
+
if (command.length > 8000) {
|
|
808
|
+
throw new PluginSandboxError("命令过长")
|
|
809
|
+
}
|
|
810
|
+
const rawCwd = String(params["cwd"] ?? "")
|
|
811
|
+
const cwd = rawCwd === "" ? process.cwd() : resolve(rawCwd)
|
|
812
|
+
const stat = await statSafe(cwd)
|
|
813
|
+
if (!stat?.isDirectory()) {
|
|
814
|
+
throw new PluginSandboxError(`工作目录不存在或不是目录:${cwd}`)
|
|
815
|
+
}
|
|
816
|
+
const extraEnv: Record<string, string> = {}
|
|
817
|
+
const rawEnv = params["env"]
|
|
818
|
+
if (rawEnv !== null && typeof rawEnv === "object" && !Array.isArray(rawEnv)) {
|
|
819
|
+
for (const [key, value] of Object.entries(rawEnv as Record<string, unknown>)) {
|
|
820
|
+
if (typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
821
|
+
extraEnv[key] = value
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const rawTimeout = params["timeoutMs"]
|
|
826
|
+
const timeoutMs =
|
|
827
|
+
typeof rawTimeout === "number" && Number.isFinite(rawTimeout) && rawTimeout > 0
|
|
828
|
+
? Math.min(rawTimeout, HOST_EXEC_DEFAULT_TIMEOUT_MS)
|
|
829
|
+
: HOST_EXEC_DEFAULT_TIMEOUT_MS
|
|
830
|
+
|
|
831
|
+
const sessionId = randomUUID()
|
|
832
|
+
const child = spawn(command, {
|
|
833
|
+
cwd,
|
|
834
|
+
shell: true,
|
|
835
|
+
env: { ...process.env, ...extraEnv, TERM: "dumb", NO_COLOR: "1" },
|
|
836
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
837
|
+
// 独立进程组,stop 时能连带子进程一起结束
|
|
838
|
+
detached: process.platform !== "win32",
|
|
839
|
+
})
|
|
840
|
+
const session: ExecSession = {
|
|
841
|
+
child,
|
|
842
|
+
cwd,
|
|
843
|
+
startedAt: Date.now(),
|
|
844
|
+
output: "",
|
|
845
|
+
delivered: 0,
|
|
846
|
+
done: false,
|
|
847
|
+
exitCode: null,
|
|
848
|
+
timedOut: false,
|
|
849
|
+
truncated: false,
|
|
850
|
+
timer: null,
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const append = (chunk: Buffer | string): void => {
|
|
854
|
+
if (session.truncated) return
|
|
855
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8")
|
|
856
|
+
if (session.output.length + text.length > HOST_EXEC_MAX_OUTPUT_BYTES) {
|
|
857
|
+
session.output += text.slice(0, HOST_EXEC_MAX_OUTPUT_BYTES - session.output.length)
|
|
858
|
+
session.truncated = true
|
|
859
|
+
killSession(session)
|
|
860
|
+
return
|
|
861
|
+
}
|
|
862
|
+
session.output += text
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
child.stdout?.on("data", append)
|
|
866
|
+
child.stderr?.on("data", append)
|
|
867
|
+
child.on("error", (error) => {
|
|
868
|
+
append(`
|
|
869
|
+
[exec] ${error.message}
|
|
870
|
+
`)
|
|
871
|
+
finish(null)
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
const timer = setTimeout(() => {
|
|
875
|
+
session.timedOut = true
|
|
876
|
+
killSession(session)
|
|
877
|
+
}, timeoutMs)
|
|
878
|
+
Object.assign(session, { timer })
|
|
879
|
+
|
|
880
|
+
let finished = false
|
|
881
|
+
function finish(code: number | null): void {
|
|
882
|
+
if (finished) return
|
|
883
|
+
finished = true
|
|
884
|
+
session.done = true
|
|
885
|
+
session.exitCode = code
|
|
886
|
+
if (session.timer !== null) clearTimeout(session.timer)
|
|
887
|
+
}
|
|
888
|
+
child.on("close", (code) => finish(code))
|
|
889
|
+
|
|
890
|
+
this.#execSessions.set(sessionId, session)
|
|
891
|
+
// 只保留最近 32 个会话,避免长时间使用后内存堆积
|
|
892
|
+
if (this.#execSessions.size > 32) {
|
|
893
|
+
for (const [id, entry] of this.#execSessions) {
|
|
894
|
+
if (this.#execSessions.size <= 32) break
|
|
895
|
+
if (entry.done) this.#execSessions.delete(id)
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return { sessionId, pid: child.pid ?? null, cwd }
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
#execPoll(sessionId: string): HostExecChunk {
|
|
902
|
+
const session = this.#execSessions.get(sessionId)
|
|
903
|
+
if (session === undefined) {
|
|
904
|
+
throw new PluginSandboxError("执行会话不存在或已过期")
|
|
905
|
+
}
|
|
906
|
+
const chunk = session.output.slice(session.delivered)
|
|
907
|
+
session.delivered = session.output.length
|
|
908
|
+
return {
|
|
909
|
+
chunk,
|
|
910
|
+
done: session.done,
|
|
911
|
+
exitCode: session.exitCode,
|
|
912
|
+
timedOut: session.timedOut,
|
|
913
|
+
truncated: session.truncated,
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
#execStop(sessionId: string): void {
|
|
918
|
+
const session = this.#execSessions.get(sessionId)
|
|
919
|
+
if (session === undefined || session.done) return
|
|
920
|
+
killSession(session)
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** PTY:available/spawn/poll/write/resize/kill */
|
|
924
|
+
async #handlePty(method: string, params: Record<string, unknown>): Promise<unknown> {
|
|
925
|
+
if (method === "host.pty.available") {
|
|
926
|
+
const module = await loadNodePty()
|
|
927
|
+
if (module !== null) {
|
|
928
|
+
return { available: true, backend: "node-pty" }
|
|
929
|
+
}
|
|
930
|
+
const python = await probePython()
|
|
931
|
+
if (python !== null) {
|
|
932
|
+
// node-pty 缺失时用 python 自建 pty,仍是真 tty
|
|
933
|
+
return { available: true, backend: "python-pty", reason: nodePtyError ?? null }
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
available: false,
|
|
937
|
+
reason: `node-pty 与 python3 均不可用:${nodePtyError ?? "未安装"}`,
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (method === "host.pty.spawn") {
|
|
941
|
+
return await this.#ptySpawn(params)
|
|
942
|
+
}
|
|
943
|
+
if (method === "host.pty.poll") {
|
|
944
|
+
return this.#ptyPoll(String(params["sessionId"] ?? ""))
|
|
945
|
+
}
|
|
946
|
+
if (method === "host.pty.write") {
|
|
947
|
+
const session = this.#ptySession(String(params["sessionId"] ?? ""))
|
|
948
|
+
const data = String(params["data"] ?? "")
|
|
949
|
+
if (data !== "" && !session.done) session.handle.write(data)
|
|
950
|
+
return {}
|
|
951
|
+
}
|
|
952
|
+
if (method === "host.pty.resize") {
|
|
953
|
+
const session = this.#ptySession(String(params["sessionId"] ?? ""))
|
|
954
|
+
const cols = clampCols(params["cols"])
|
|
955
|
+
const rows = clampRows(params["rows"])
|
|
956
|
+
if (!session.done && (cols !== session.cols || rows !== session.rows)) {
|
|
957
|
+
session.cols = cols
|
|
958
|
+
session.rows = rows
|
|
959
|
+
try {
|
|
960
|
+
session.handle.resize(cols, rows)
|
|
961
|
+
} catch {
|
|
962
|
+
/* 进程可能已退出 */
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return {}
|
|
966
|
+
}
|
|
967
|
+
if (method === "host.pty.kill") {
|
|
968
|
+
const session = this.#ptySession(String(params["sessionId"] ?? ""))
|
|
969
|
+
if (!session.done) {
|
|
970
|
+
try {
|
|
971
|
+
session.handle.kill()
|
|
972
|
+
} catch {
|
|
973
|
+
/* 已经退出 */
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return {}
|
|
977
|
+
}
|
|
978
|
+
throw new PluginSandboxError(`不支持的 pty 方法:${method}`)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
#ptySession(sessionId: string): PtySession {
|
|
982
|
+
const session = this.#ptySessions.get(sessionId)
|
|
983
|
+
if (session === undefined) {
|
|
984
|
+
throw new PluginSandboxError("PTY 会话不存在或已过期")
|
|
985
|
+
}
|
|
986
|
+
return session
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async #ptySpawn(params: Record<string, unknown>): Promise<HostPtySession> {
|
|
990
|
+
const module = await loadNodePty()
|
|
991
|
+
const python = module === null ? await probePython() : null
|
|
992
|
+
if (module === null && python === null) {
|
|
993
|
+
throw new PluginSandboxError(
|
|
994
|
+
`没有可用的 PTY 后端:node-pty 与 python3 均不可用(${nodePtyError ?? "未安装"})`,
|
|
995
|
+
)
|
|
996
|
+
}
|
|
997
|
+
const rawCwd = String(params["cwd"] ?? "")
|
|
998
|
+
const cwd = rawCwd === "" ? process.cwd() : resolve(rawCwd)
|
|
999
|
+
const stat = await statSafe(cwd)
|
|
1000
|
+
if (!stat?.isDirectory()) {
|
|
1001
|
+
throw new PluginSandboxError(`工作目录不存在或不是目录:${cwd}`)
|
|
1002
|
+
}
|
|
1003
|
+
const file = String(params["file"] ?? "").trim() || defaultShell()
|
|
1004
|
+
const rawArgs = params["args"]
|
|
1005
|
+
const args = Array.isArray(rawArgs)
|
|
1006
|
+
? rawArgs.filter((item): item is string => typeof item === "string")
|
|
1007
|
+
: []
|
|
1008
|
+
const extraEnv: Record<string, string> = {}
|
|
1009
|
+
const rawEnv = params["env"]
|
|
1010
|
+
if (rawEnv !== null && typeof rawEnv === "object" && !Array.isArray(rawEnv)) {
|
|
1011
|
+
for (const [key, value] of Object.entries(rawEnv as Record<string, unknown>)) {
|
|
1012
|
+
if (typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
1013
|
+
extraEnv[key] = value
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const cols = clampCols(params["cols"])
|
|
1018
|
+
const rows = clampRows(params["rows"])
|
|
1019
|
+
|
|
1020
|
+
const env = {
|
|
1021
|
+
...process.env,
|
|
1022
|
+
...extraEnv,
|
|
1023
|
+
TERM: "xterm-256color",
|
|
1024
|
+
COLORTERM: "truecolor",
|
|
1025
|
+
}
|
|
1026
|
+
const onData: Array<(data: string) => void> = []
|
|
1027
|
+
const onExit: Array<(code: number | null) => void> = []
|
|
1028
|
+
let handle: PtyHandle
|
|
1029
|
+
let backend: PtyBackendName
|
|
1030
|
+
|
|
1031
|
+
if (module !== null) {
|
|
1032
|
+
// 首选:node-pty 原生模块
|
|
1033
|
+
const pty = module.spawn(file, args, { name: "xterm-256color", cols, rows, cwd, env })
|
|
1034
|
+
pty.onData((data) => {
|
|
1035
|
+
for (const handler of onData) handler(data)
|
|
1036
|
+
})
|
|
1037
|
+
pty.onExit((event) => {
|
|
1038
|
+
const code = typeof event.exitCode === "number" ? event.exitCode : null
|
|
1039
|
+
for (const handler of onExit) handler(code)
|
|
1040
|
+
})
|
|
1041
|
+
handle = {
|
|
1042
|
+
pid: pty.pid,
|
|
1043
|
+
write: (data) => pty.write(data),
|
|
1044
|
+
resize: (nextCols, nextRows) => pty.resize(nextCols, nextRows),
|
|
1045
|
+
kill: () => pty.kill(),
|
|
1046
|
+
}
|
|
1047
|
+
backend = "node-pty"
|
|
1048
|
+
} else {
|
|
1049
|
+
// 回退:python3 自建 pty,控制命令走 stdin 的 JSON 行
|
|
1050
|
+
const config = JSON.stringify({ shell: file, args, cwd, cols, rows, env: extraEnv })
|
|
1051
|
+
const child = spawn(python as string, [PTY_BRIDGE_PATH, config], {
|
|
1052
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1053
|
+
cwd,
|
|
1054
|
+
env,
|
|
1055
|
+
detached: process.platform !== "win32",
|
|
1056
|
+
})
|
|
1057
|
+
const send = (payload: Record<string, unknown>): void => {
|
|
1058
|
+
if (child.stdin === null || child.stdin.destroyed) return
|
|
1059
|
+
try {
|
|
1060
|
+
child.stdin.write(`${JSON.stringify(payload)}
|
|
1061
|
+
`)
|
|
1062
|
+
} catch {
|
|
1063
|
+
/* 桥接已退出 */
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
1067
|
+
const text = chunk.toString("utf8")
|
|
1068
|
+
for (const handler of onData) handler(text)
|
|
1069
|
+
})
|
|
1070
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
1071
|
+
// 桥接的报错也当终端输出,便于在控制台里看到
|
|
1072
|
+
const text = chunk.toString("utf8")
|
|
1073
|
+
for (const handler of onData) handler(text)
|
|
1074
|
+
})
|
|
1075
|
+
child.on("close", (code) => {
|
|
1076
|
+
for (const handler of onExit) handler(typeof code === "number" ? code : null)
|
|
1077
|
+
})
|
|
1078
|
+
handle = {
|
|
1079
|
+
pid: child.pid ?? 0,
|
|
1080
|
+
write: (data) => send({ type: "input", data }),
|
|
1081
|
+
resize: (nextCols, nextRows) =>
|
|
1082
|
+
send({ type: "resize", cols: nextCols, rows: nextRows }),
|
|
1083
|
+
kill: () => {
|
|
1084
|
+
send({ type: "kill" })
|
|
1085
|
+
if (process.platform !== "win32" && typeof child.pid === "number") {
|
|
1086
|
+
try {
|
|
1087
|
+
process.kill(-child.pid, "SIGKILL")
|
|
1088
|
+
return
|
|
1089
|
+
} catch {
|
|
1090
|
+
/* 进程组已消失 */
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
child.kill("SIGKILL")
|
|
1095
|
+
} catch {
|
|
1096
|
+
/* 已退出 */
|
|
1097
|
+
}
|
|
1098
|
+
},
|
|
1099
|
+
}
|
|
1100
|
+
backend = "python-pty"
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const session: PtySession = {
|
|
1104
|
+
handle,
|
|
1105
|
+
backend,
|
|
1106
|
+
file,
|
|
1107
|
+
cwd,
|
|
1108
|
+
cols,
|
|
1109
|
+
rows,
|
|
1110
|
+
output: "",
|
|
1111
|
+
delivered: 0,
|
|
1112
|
+
done: false,
|
|
1113
|
+
exitCode: null,
|
|
1114
|
+
}
|
|
1115
|
+
onData.push((data) => {
|
|
1116
|
+
session.output += data
|
|
1117
|
+
// 超上限时丢最旧的一半,同步回退已读游标
|
|
1118
|
+
if (session.output.length > HOST_PTY_MAX_OUTPUT_CHARS) {
|
|
1119
|
+
const drop = session.output.length - Math.floor(HOST_PTY_MAX_OUTPUT_CHARS / 2)
|
|
1120
|
+
session.output = session.output.slice(drop)
|
|
1121
|
+
session.delivered = Math.max(0, session.delivered - drop)
|
|
1122
|
+
}
|
|
1123
|
+
})
|
|
1124
|
+
onExit.push((code) => {
|
|
1125
|
+
session.done = true
|
|
1126
|
+
session.exitCode = code
|
|
1127
|
+
})
|
|
1128
|
+
|
|
1129
|
+
const sessionId = randomUUID()
|
|
1130
|
+
this.#ptySessions.set(sessionId, session)
|
|
1131
|
+
// 只保留最近的会话,避免长期使用后堆积
|
|
1132
|
+
if (this.#ptySessions.size > HOST_PTY_MAX_SESSIONS) {
|
|
1133
|
+
for (const [id, entry] of this.#ptySessions) {
|
|
1134
|
+
if (this.#ptySessions.size <= HOST_PTY_MAX_SESSIONS) break
|
|
1135
|
+
if (entry.done) this.#ptySessions.delete(id)
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return { sessionId, pid: handle.pid, file, cwd, cols, rows }
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
#ptyPoll(sessionId: string): HostPtyChunk {
|
|
1142
|
+
const session = this.#ptySession(sessionId)
|
|
1143
|
+
const pending = session.output.slice(
|
|
1144
|
+
session.delivered,
|
|
1145
|
+
session.delivered + HOST_PTY_MAX_POLL_CHARS,
|
|
1146
|
+
)
|
|
1147
|
+
session.delivered += pending.length
|
|
1148
|
+
return { chunk: pending, done: session.done, exitCode: session.exitCode }
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
#hasHostApi(name: string): boolean {
|
|
1152
|
+
return this.#options.permissions.hostApi.includes(name)
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
#requireHostApi(name: string): void {
|
|
1156
|
+
if (!this.#hasHostApi(name)) {
|
|
1157
|
+
throw new PluginSandboxError(`插件没有 hostApi.${name} 权限`)
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
async #handleRequest(request: RpcRequest): Promise<void> {
|
|
1162
|
+
try {
|
|
1163
|
+
const params = (request.params ?? {}) as Record<string, unknown>
|
|
1164
|
+
let result: unknown
|
|
1165
|
+
if (request.method === "host.agent.chat") {
|
|
1166
|
+
this.#requireHostApi("agent")
|
|
1167
|
+
if (this.#options.agent === undefined) {
|
|
1168
|
+
throw new PluginSandboxError("宿主没有 Agent 服务")
|
|
1169
|
+
}
|
|
1170
|
+
result = await this.#options.agent.chat(params as unknown as HostAgentChatInput)
|
|
1171
|
+
} else if (request.method === "host.agent.stream") {
|
|
1172
|
+
this.#requireHostApi("agent")
|
|
1173
|
+
if (this.#options.agent === undefined) {
|
|
1174
|
+
throw new PluginSandboxError("宿主没有 Agent 服务")
|
|
1175
|
+
}
|
|
1176
|
+
const streamId = String(params["streamId"] ?? "")
|
|
1177
|
+
const input = params["input"] as unknown as HostAgentChatInput
|
|
1178
|
+
result = await this.#options.agent.stream(input, (event) => {
|
|
1179
|
+
this.#notify("sandbox.agent.stream", { streamId, event })
|
|
1180
|
+
})
|
|
1181
|
+
} else if (request.method === "host.agent.validateWorkspace") {
|
|
1182
|
+
this.#requireHostApi("agent")
|
|
1183
|
+
if (this.#options.agent === undefined) {
|
|
1184
|
+
throw new PluginSandboxError("宿主没有 Agent 服务")
|
|
1185
|
+
}
|
|
1186
|
+
result = await this.#options.agent.validateWorkspace(String(params["path"] ?? ""))
|
|
1187
|
+
} else if (request.method === "host.agent.pickDirectory") {
|
|
1188
|
+
this.#requireHostApi("agent")
|
|
1189
|
+
if (this.#options.agent === undefined) {
|
|
1190
|
+
throw new PluginSandboxError("宿主没有 Agent 服务")
|
|
1191
|
+
}
|
|
1192
|
+
result = await this.#options.agent.pickDirectory()
|
|
1193
|
+
} else if (request.method === "host.agent.searchWorkspaceFiles") {
|
|
1194
|
+
this.#requireHostApi("agent")
|
|
1195
|
+
if (this.#options.agent === undefined) {
|
|
1196
|
+
throw new PluginSandboxError("宿主没有 Agent 服务")
|
|
1197
|
+
}
|
|
1198
|
+
result = await this.#options.agent.searchWorkspaceFiles(
|
|
1199
|
+
String(params["root"] ?? ""),
|
|
1200
|
+
String(params["query"] ?? ""),
|
|
1201
|
+
)
|
|
1202
|
+
} else if (request.method === "host.agent.interject") {
|
|
1203
|
+
this.#requireHostApi("agent")
|
|
1204
|
+
if (this.#options.agent === undefined) {
|
|
1205
|
+
throw new PluginSandboxError("宿主没有 Agent 服务")
|
|
1206
|
+
}
|
|
1207
|
+
result = await this.#options.agent.interject(
|
|
1208
|
+
String(params["sessionId"] ?? ""),
|
|
1209
|
+
String(params["message"] ?? ""),
|
|
1210
|
+
)
|
|
1211
|
+
} else if (request.method === "host.agent.stop") {
|
|
1212
|
+
this.#requireHostApi("agent")
|
|
1213
|
+
if (this.#options.agent === undefined) {
|
|
1214
|
+
throw new PluginSandboxError("宿主没有 Agent 服务")
|
|
1215
|
+
}
|
|
1216
|
+
result = await this.#options.agent.stop(String(params["sessionId"] ?? ""))
|
|
1217
|
+
} else if (request.method === "host.agent.respondApproval") {
|
|
1218
|
+
this.#requireHostApi("agent")
|
|
1219
|
+
if (this.#options.agent === undefined) {
|
|
1220
|
+
throw new PluginSandboxError("宿主没有 Agent 服务")
|
|
1221
|
+
}
|
|
1222
|
+
result = await this.#options.agent.respondApproval(
|
|
1223
|
+
String(params["approvalId"] ?? ""),
|
|
1224
|
+
params["approved"] === true,
|
|
1225
|
+
)
|
|
1226
|
+
} else if (request.method === "host.agent.respondAsk") {
|
|
1227
|
+
this.#requireHostApi("agent")
|
|
1228
|
+
if (this.#options.agent === undefined) {
|
|
1229
|
+
throw new PluginSandboxError("宿主没有 Agent 服务")
|
|
1230
|
+
}
|
|
1231
|
+
result = await this.#options.agent.respondAsk(
|
|
1232
|
+
String(params["askId"] ?? ""),
|
|
1233
|
+
String(params["answer"] ?? ""),
|
|
1234
|
+
)
|
|
1235
|
+
} else if (request.method.startsWith("host.models.")) {
|
|
1236
|
+
this.#requireHostApi("models")
|
|
1237
|
+
if (this.#options.models === undefined) {
|
|
1238
|
+
throw new PluginSandboxError("宿主没有模型配置服务")
|
|
1239
|
+
}
|
|
1240
|
+
result = await this.#handleModels(request.method, params)
|
|
1241
|
+
} else if (request.method.startsWith("host.network.")) {
|
|
1242
|
+
this.#requireHostApi("network")
|
|
1243
|
+
if (this.#options.network === undefined) {
|
|
1244
|
+
throw new PluginSandboxError("宿主没有网络配置服务")
|
|
1245
|
+
}
|
|
1246
|
+
result = await this.#handleNetwork(request.method, params)
|
|
1247
|
+
} else if (request.method.startsWith("host.workspaces.")) {
|
|
1248
|
+
this.#requireHostApi("workspaces")
|
|
1249
|
+
if (request.method === "host.workspaces.list") {
|
|
1250
|
+
result = await listWorkspaces(this.#options.paths)
|
|
1251
|
+
} else if (request.method === "host.workspaces.add") {
|
|
1252
|
+
result = await addWorkspace(
|
|
1253
|
+
{
|
|
1254
|
+
root: String(params["root"] ?? ""),
|
|
1255
|
+
...(typeof params["name"] === "string" ? { name: params["name"] } : {}),
|
|
1256
|
+
},
|
|
1257
|
+
this.#options.paths,
|
|
1258
|
+
)
|
|
1259
|
+
} else if (request.method === "host.workspaces.remove") {
|
|
1260
|
+
result = await removeWorkspace(String(params["id"] ?? ""), this.#options.paths)
|
|
1261
|
+
} else if (request.method === "host.workspaces.rename") {
|
|
1262
|
+
result = await renameWorkspace(
|
|
1263
|
+
String(params["id"] ?? ""),
|
|
1264
|
+
String(params["name"] ?? ""),
|
|
1265
|
+
this.#options.paths,
|
|
1266
|
+
)
|
|
1267
|
+
} else if (request.method === "host.workspaces.touch") {
|
|
1268
|
+
result = await touchWorkspace(String(params["id"] ?? ""), this.#options.paths)
|
|
1269
|
+
} else {
|
|
1270
|
+
throw new Error(`未知 host 方法:${request.method}`)
|
|
1271
|
+
}
|
|
1272
|
+
} else if (request.method.startsWith("host.plugins.")) {
|
|
1273
|
+
this.#requireHostApi("plugins")
|
|
1274
|
+
if (this.#options.plugins === undefined) {
|
|
1275
|
+
throw new PluginSandboxError("宿主没有插件管理服务")
|
|
1276
|
+
}
|
|
1277
|
+
result = await this.#handlePlugins(request.method, params)
|
|
1278
|
+
} else if (request.method === "host.http.createServer") {
|
|
1279
|
+
this.#requireHostApi("http")
|
|
1280
|
+
const serverId = String(params["serverId"] ?? "")
|
|
1281
|
+
const host = String(params["host"] ?? "127.0.0.1")
|
|
1282
|
+
const port = typeof params["port"] === "number" ? params["port"] : 0
|
|
1283
|
+
if (host.trim() === "" || /\s/.test(host)) {
|
|
1284
|
+
throw new PluginSandboxError(`非法监听地址:${host}`)
|
|
1285
|
+
}
|
|
1286
|
+
if (!Number.isInteger(port) || port < 0 || port > 65_535) {
|
|
1287
|
+
throw new PluginSandboxError(`非法端口:${String(port)}`)
|
|
1288
|
+
}
|
|
1289
|
+
const tls = params["tls"] as
|
|
1290
|
+
| { cert?: unknown; key?: unknown; ca?: unknown }
|
|
1291
|
+
| undefined
|
|
1292
|
+
if (
|
|
1293
|
+
tls !== undefined &&
|
|
1294
|
+
(typeof tls.cert !== "string" ||
|
|
1295
|
+
typeof tls.key !== "string" ||
|
|
1296
|
+
tls.cert.trim() === "" ||
|
|
1297
|
+
tls.key.trim() === "")
|
|
1298
|
+
) {
|
|
1299
|
+
throw new PluginSandboxError("TLS 需要 cert 与 key PEM")
|
|
1300
|
+
}
|
|
1301
|
+
const requestListener = async (
|
|
1302
|
+
req: import("node:http").IncomingMessage,
|
|
1303
|
+
res: import("node:http").ServerResponse,
|
|
1304
|
+
): Promise<void> => {
|
|
1305
|
+
try {
|
|
1306
|
+
const chunks: Buffer[] = []
|
|
1307
|
+
let size = 0
|
|
1308
|
+
for await (const chunk of req) {
|
|
1309
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
1310
|
+
size += buffer.length
|
|
1311
|
+
if (size > HTTP_MAX_BODY_BYTES) {
|
|
1312
|
+
throw new Error("HTTP 请求体超过上限")
|
|
1313
|
+
}
|
|
1314
|
+
chunks.push(buffer)
|
|
1315
|
+
}
|
|
1316
|
+
const httpRequest: HttpRequest = {
|
|
1317
|
+
method: req.method ?? "GET",
|
|
1318
|
+
url: req.url ?? "/",
|
|
1319
|
+
headers: Object.fromEntries(
|
|
1320
|
+
Object.entries(req.headers).map(([key, value]) => [
|
|
1321
|
+
key,
|
|
1322
|
+
Array.isArray(value) ? value.join(", ") : (value ?? ""),
|
|
1323
|
+
]),
|
|
1324
|
+
),
|
|
1325
|
+
body: Buffer.concat(chunks).toString("base64"),
|
|
1326
|
+
remoteAddress: req.socket.remoteAddress ?? null,
|
|
1327
|
+
}
|
|
1328
|
+
const requestId = nextRequestId("http-req")
|
|
1329
|
+
this.#httpResponses.set(requestId, {
|
|
1330
|
+
response: res,
|
|
1331
|
+
chunks: [],
|
|
1332
|
+
ready: false,
|
|
1333
|
+
ended: false,
|
|
1334
|
+
})
|
|
1335
|
+
res.on("close", () => this.#httpResponses.delete(requestId))
|
|
1336
|
+
const response = (await this.#request(
|
|
1337
|
+
"plugin.http",
|
|
1338
|
+
{ serverId, requestId, request: httpRequest },
|
|
1339
|
+
180_000,
|
|
1340
|
+
)) as HttpResponse
|
|
1341
|
+
res.statusCode = response.status ?? 200
|
|
1342
|
+
for (const [key, value] of Object.entries(response.headers ?? {})) {
|
|
1343
|
+
res.setHeader(key, value)
|
|
1344
|
+
}
|
|
1345
|
+
if (response.stream === true) {
|
|
1346
|
+
const pending = this.#httpResponses.get(requestId)
|
|
1347
|
+
if (pending !== undefined) {
|
|
1348
|
+
pending.ready = true
|
|
1349
|
+
for (const chunk of pending.chunks) {
|
|
1350
|
+
try {
|
|
1351
|
+
pending.response.write(chunk)
|
|
1352
|
+
} catch {
|
|
1353
|
+
this.#httpResponses.delete(requestId)
|
|
1354
|
+
return
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
pending.chunks.length = 0
|
|
1358
|
+
if (pending.ended) {
|
|
1359
|
+
this.#httpResponses.delete(requestId)
|
|
1360
|
+
pending.response.end()
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
return
|
|
1364
|
+
}
|
|
1365
|
+
this.#httpResponses.delete(requestId)
|
|
1366
|
+
const body =
|
|
1367
|
+
response.body === undefined
|
|
1368
|
+
? Buffer.alloc(0)
|
|
1369
|
+
: Buffer.from(
|
|
1370
|
+
response.body,
|
|
1371
|
+
response.bodyEncoding === "base64" ? "base64" : "utf8",
|
|
1372
|
+
)
|
|
1373
|
+
res.end(body)
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
res.statusCode = 500
|
|
1376
|
+
res.end(error instanceof Error ? error.message : String(error))
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
const server =
|
|
1380
|
+
tls === undefined
|
|
1381
|
+
? createHttpServer(requestListener)
|
|
1382
|
+
: createHttpsServer(
|
|
1383
|
+
{
|
|
1384
|
+
cert: String(tls.cert ?? ""),
|
|
1385
|
+
key: String(tls.key ?? ""),
|
|
1386
|
+
...(typeof tls.ca === "string" ? { ca: tls.ca } : {}),
|
|
1387
|
+
},
|
|
1388
|
+
requestListener,
|
|
1389
|
+
)
|
|
1390
|
+
let wsServer: WebSocketServer | null = null
|
|
1391
|
+
if (params["websocket"] === true) {
|
|
1392
|
+
wsServer = new WebSocketServer({ noServer: true })
|
|
1393
|
+
server.on("upgrade", (req, socket, head) => {
|
|
1394
|
+
wsServer?.handleUpgrade(req, socket, head, (ws) => {
|
|
1395
|
+
const connectionId = nextRequestId("ws")
|
|
1396
|
+
this.#wsConnections.set(connectionId, ws)
|
|
1397
|
+
const headers = Object.fromEntries(
|
|
1398
|
+
Object.entries(req.headers).map(([key, value]) => [
|
|
1399
|
+
key,
|
|
1400
|
+
Array.isArray(value) ? value.join(", ") : (value ?? ""),
|
|
1401
|
+
]),
|
|
1402
|
+
)
|
|
1403
|
+
const httpRequest: HttpRequest = {
|
|
1404
|
+
method: req.method ?? "GET",
|
|
1405
|
+
url: req.url ?? "/",
|
|
1406
|
+
headers,
|
|
1407
|
+
body: "",
|
|
1408
|
+
remoteAddress: req.socket.remoteAddress ?? null,
|
|
1409
|
+
}
|
|
1410
|
+
void this.#request("plugin.websocket.connect", {
|
|
1411
|
+
serverId,
|
|
1412
|
+
connectionId,
|
|
1413
|
+
request: httpRequest,
|
|
1414
|
+
}).catch(() => ws.close())
|
|
1415
|
+
ws.on("message", (data, isBinary) => {
|
|
1416
|
+
void this.#request("plugin.websocket.message", {
|
|
1417
|
+
connectionId,
|
|
1418
|
+
data: Buffer.from(data as Buffer).toString("base64"),
|
|
1419
|
+
binary: isBinary,
|
|
1420
|
+
}).catch(() => undefined)
|
|
1421
|
+
})
|
|
1422
|
+
ws.on("close", (code, reason) => {
|
|
1423
|
+
this.#wsConnections.delete(connectionId)
|
|
1424
|
+
void this.#request("plugin.websocket.close", {
|
|
1425
|
+
connectionId,
|
|
1426
|
+
code,
|
|
1427
|
+
reason: reason.toString(),
|
|
1428
|
+
}).catch(() => undefined)
|
|
1429
|
+
})
|
|
1430
|
+
})
|
|
1431
|
+
})
|
|
1432
|
+
}
|
|
1433
|
+
await new Promise<void>((resolveListen, rejectListen) => {
|
|
1434
|
+
server.once("error", rejectListen)
|
|
1435
|
+
server.listen(port, host, () => {
|
|
1436
|
+
server.off("error", rejectListen)
|
|
1437
|
+
resolveListen()
|
|
1438
|
+
})
|
|
1439
|
+
})
|
|
1440
|
+
const address = server.address()
|
|
1441
|
+
const actualPort =
|
|
1442
|
+
typeof address === "object" && address !== null ? address.port : port
|
|
1443
|
+
this.#httpServers.set(serverId, { server, host, port: actualPort, wsServer })
|
|
1444
|
+
result = { serverId, host, port: actualPort }
|
|
1445
|
+
} else if (request.method === "host.http.closeServer") {
|
|
1446
|
+
this.#requireHostApi("http")
|
|
1447
|
+
const serverId = String(params["serverId"] ?? "")
|
|
1448
|
+
const record = this.#httpServers.get(serverId)
|
|
1449
|
+
if (record !== undefined) {
|
|
1450
|
+
this.#httpServers.delete(serverId)
|
|
1451
|
+
for (const ws of this.#wsConnections.values()) {
|
|
1452
|
+
ws.close()
|
|
1453
|
+
}
|
|
1454
|
+
record.wsServer?.close()
|
|
1455
|
+
// SSE / 长轮询会保持连接,server.close() 要等它们全部断开;
|
|
1456
|
+
// 关闭时给一点收尾时间,然后强制断开剩余连接,避免 Ctrl+C 卡住。
|
|
1457
|
+
const server = record.server
|
|
1458
|
+
await new Promise<void>((resolveClose) => {
|
|
1459
|
+
let settled = false
|
|
1460
|
+
const finish = (): void => {
|
|
1461
|
+
if (settled) {
|
|
1462
|
+
return
|
|
1463
|
+
}
|
|
1464
|
+
settled = true
|
|
1465
|
+
clearTimeout(forceTimer)
|
|
1466
|
+
resolveClose()
|
|
1467
|
+
}
|
|
1468
|
+
server.close(() => finish())
|
|
1469
|
+
server.closeIdleConnections()
|
|
1470
|
+
const forceTimer = setTimeout(() => {
|
|
1471
|
+
server.closeAllConnections()
|
|
1472
|
+
finish()
|
|
1473
|
+
}, 800)
|
|
1474
|
+
forceTimer.unref()
|
|
1475
|
+
})
|
|
1476
|
+
}
|
|
1477
|
+
result = {}
|
|
1478
|
+
} else if (request.method === "host.websocket.send") {
|
|
1479
|
+
this.#requireHostApi("http")
|
|
1480
|
+
const connectionId = String(params["connectionId"] ?? "")
|
|
1481
|
+
const ws = this.#wsConnections.get(connectionId)
|
|
1482
|
+
if (ws === undefined) {
|
|
1483
|
+
throw new PluginSandboxError(`WebSocket 连接不存在:${connectionId}`)
|
|
1484
|
+
}
|
|
1485
|
+
ws.send(String(params["data"] ?? ""))
|
|
1486
|
+
result = {}
|
|
1487
|
+
} else if (request.method === "host.websocket.close") {
|
|
1488
|
+
this.#requireHostApi("http")
|
|
1489
|
+
const connectionId = String(params["connectionId"] ?? "")
|
|
1490
|
+
const ws = this.#wsConnections.get(connectionId)
|
|
1491
|
+
if (ws !== undefined) {
|
|
1492
|
+
this.#wsConnections.delete(connectionId)
|
|
1493
|
+
ws.close(
|
|
1494
|
+
typeof params["code"] === "number" ? params["code"] : 1000,
|
|
1495
|
+
String(params["reason"] ?? ""),
|
|
1496
|
+
)
|
|
1497
|
+
}
|
|
1498
|
+
result = {}
|
|
1499
|
+
} else if (request.method.startsWith("host.pty.")) {
|
|
1500
|
+
this.#requireHostApi("pty")
|
|
1501
|
+
result = await this.#handlePty(request.method, params)
|
|
1502
|
+
} else if (request.method.startsWith("host.exec.")) {
|
|
1503
|
+
this.#requireHostApi("exec")
|
|
1504
|
+
result = await this.#handleExec(request.method, params)
|
|
1505
|
+
} else if (request.method === "host.pluginData") {
|
|
1506
|
+
this.#requireHostApi("fs")
|
|
1507
|
+
const targetId = String(params["targetId"] ?? "")
|
|
1508
|
+
const method = String(params["method"] ?? "")
|
|
1509
|
+
const args = Array.isArray(params["args"]) ? params["args"] : []
|
|
1510
|
+
if (this.#options.dataAccess === undefined) {
|
|
1511
|
+
throw new PluginSandboxError("宿主没有 plugin data 服务")
|
|
1512
|
+
}
|
|
1513
|
+
const api = await this.#options.dataAccess.open(this.#options.pluginId, targetId)
|
|
1514
|
+
if (api === null) {
|
|
1515
|
+
throw new PluginSandboxError(`没有插件 data 访问权限:${targetId}`)
|
|
1516
|
+
}
|
|
1517
|
+
const target = (api as unknown as Record<string, unknown>)[method]
|
|
1518
|
+
if (typeof target !== "function") {
|
|
1519
|
+
throw new PluginSandboxError(`不支持的 data 方法:${method}`)
|
|
1520
|
+
}
|
|
1521
|
+
try {
|
|
1522
|
+
const value = await (
|
|
1523
|
+
target as (...values: unknown[]) => Promise<unknown>
|
|
1524
|
+
).apply(api, args)
|
|
1525
|
+
result = value instanceof Uint8Array ? [...value] : value
|
|
1526
|
+
} catch (error) {
|
|
1527
|
+
throw new PluginSandboxError(
|
|
1528
|
+
error instanceof Error ? error.message : String(error),
|
|
1529
|
+
)
|
|
1530
|
+
}
|
|
1531
|
+
} else if (request.method.startsWith("host.fs.")) {
|
|
1532
|
+
this.#requireHostApi("fs")
|
|
1533
|
+
result = await this.#handleFs(request.method, params, false)
|
|
1534
|
+
} else if (request.method.startsWith("host.workspaceFs.")) {
|
|
1535
|
+
this.#requireHostApi("fs")
|
|
1536
|
+
if (!this.#options.permissions.workspaceRead) {
|
|
1537
|
+
throw new PluginSandboxError("插件没有 workspace 读权限")
|
|
1538
|
+
}
|
|
1539
|
+
result = await this.#handleFs(request.method, params, true)
|
|
1540
|
+
} else if (request.method === "host.event.subscribe") {
|
|
1541
|
+
this.#requireHostApi("event")
|
|
1542
|
+
const name = String(params["name"] ?? "")
|
|
1543
|
+
if (!this.#eventUnsubscribes.has(name)) {
|
|
1544
|
+
const unsubscribe = eventBus.subscribe(name, (event) => {
|
|
1545
|
+
this.#notify("sandbox.event", { name, data: event.getData() })
|
|
1546
|
+
})
|
|
1547
|
+
this.#eventUnsubscribes.set(name, unsubscribe)
|
|
1548
|
+
}
|
|
1549
|
+
result = {}
|
|
1550
|
+
} else if (request.method === "host.shutdown") {
|
|
1551
|
+
this.#requireHostApi("shutdown")
|
|
1552
|
+
shutdownHost()
|
|
1553
|
+
result = {}
|
|
1554
|
+
} else if (request.method.startsWith("host.capabilities.")) {
|
|
1555
|
+
this.#requireHostApi("capabilities")
|
|
1556
|
+
if (this.#options.capabilities === undefined) {
|
|
1557
|
+
throw new PluginSandboxError("宿主没有 capabilities 服务")
|
|
1558
|
+
}
|
|
1559
|
+
const name = request.method.slice("host.capabilities.".length)
|
|
1560
|
+
if (name === "executeCode") {
|
|
1561
|
+
result = await this.#options.capabilities.executeCode(
|
|
1562
|
+
params as never,
|
|
1563
|
+
this.#activeContext ?? undefined,
|
|
1564
|
+
)
|
|
1565
|
+
} else if (name === "startBackgroundCode") {
|
|
1566
|
+
result = await this.#options.capabilities.startBackgroundCode(
|
|
1567
|
+
params as never,
|
|
1568
|
+
this.#activeContext ?? undefined,
|
|
1569
|
+
)
|
|
1570
|
+
} else if (name === "queryTask") {
|
|
1571
|
+
result = this.#options.capabilities.queryTask(
|
|
1572
|
+
typeof params["taskId"] === "string" ? params["taskId"] : undefined,
|
|
1573
|
+
)
|
|
1574
|
+
} else if (name === "stopTask") {
|
|
1575
|
+
result = this.#options.capabilities.stopTask(
|
|
1576
|
+
typeof params["taskId"] === "string" ? params["taskId"] : undefined,
|
|
1577
|
+
params["mode"] === "ctrl_c" ? "ctrl_c" : "kill",
|
|
1578
|
+
)
|
|
1579
|
+
} else if (name === "webSearch") {
|
|
1580
|
+
result = await this.#options.capabilities.webSearch(
|
|
1581
|
+
String(params["query"] ?? ""),
|
|
1582
|
+
typeof params["count"] === "number" ? params["count"] : undefined,
|
|
1583
|
+
)
|
|
1584
|
+
} else if (name === "webFetch") {
|
|
1585
|
+
result = await this.#options.capabilities.webFetch(String(params["url"] ?? ""))
|
|
1586
|
+
} else {
|
|
1587
|
+
throw new PluginSandboxError(`未知 capability:${name}`)
|
|
1588
|
+
}
|
|
1589
|
+
} else if (request.method === "host.getHost") {
|
|
1590
|
+
this.#requireHostApi("getHost")
|
|
1591
|
+
result = hostSnapshotData()
|
|
1592
|
+
} else if (request.method === "host.session.exists") {
|
|
1593
|
+
result = findSession(String(params["id"] ?? "")) !== null
|
|
1594
|
+
} else if (request.method === "host.session.message") {
|
|
1595
|
+
const target = findSession(String(params["id"] ?? ""))
|
|
1596
|
+
if (target === null) {
|
|
1597
|
+
throw new PluginSandboxError("会话不存在")
|
|
1598
|
+
}
|
|
1599
|
+
const content = Array.isArray(params["content"])
|
|
1600
|
+
? params["content"].map(String)
|
|
1601
|
+
: [String(params["content"] ?? "")]
|
|
1602
|
+
await target.message(content, String(params["type"] ?? "message"))
|
|
1603
|
+
result = {}
|
|
1604
|
+
} else {
|
|
1605
|
+
throw new Error(`未知 host 方法:${request.method}`)
|
|
1606
|
+
}
|
|
1607
|
+
this.#respond({ v: 1, type: "response", id: request.id, ok: true, result })
|
|
1608
|
+
} catch (error) {
|
|
1609
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1610
|
+
if (message.includes("权限")) {
|
|
1611
|
+
auditSandbox(this.#options.paths, {
|
|
1612
|
+
type: "sandbox.permission",
|
|
1613
|
+
sandboxId: this.#sandboxId,
|
|
1614
|
+
pluginId: this.#options.pluginId,
|
|
1615
|
+
backend: this.#options.backend.name,
|
|
1616
|
+
decision: "denied",
|
|
1617
|
+
method: request.method,
|
|
1618
|
+
reason: message,
|
|
1619
|
+
})
|
|
1620
|
+
}
|
|
1621
|
+
this.#respond({
|
|
1622
|
+
v: 1,
|
|
1623
|
+
type: "response",
|
|
1624
|
+
id: request.id,
|
|
1625
|
+
ok: false,
|
|
1626
|
+
error: {
|
|
1627
|
+
code: "host.rpc-failed",
|
|
1628
|
+
message,
|
|
1629
|
+
},
|
|
1630
|
+
})
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
async #handleFs(
|
|
1635
|
+
method: string,
|
|
1636
|
+
params: Record<string, unknown>,
|
|
1637
|
+
workspace: boolean,
|
|
1638
|
+
): Promise<unknown> {
|
|
1639
|
+
const api: HostFileApi = workspace
|
|
1640
|
+
? createPluginFileApi(process.cwd())
|
|
1641
|
+
: createPluginFileApi(this.#pluginDataDir)
|
|
1642
|
+
const path = String(params["path"] ?? "")
|
|
1643
|
+
const action = method.replace(/^host\.(fs|workspaceFs)\./, "")
|
|
1644
|
+
if (
|
|
1645
|
+
workspace &&
|
|
1646
|
+
["writeText", "writeBytes", "writeJson", "mkdir", "remove"].includes(action)
|
|
1647
|
+
) {
|
|
1648
|
+
if (!this.#options.permissions.workspaceWrite) {
|
|
1649
|
+
throw new PluginSandboxError("插件没有 workspace 写权限")
|
|
1650
|
+
}
|
|
1651
|
+
const context = this.#activeContext
|
|
1652
|
+
if (context !== null && context.permission === "read-only") {
|
|
1653
|
+
throw new PluginSandboxError("当前 Agent 权限是 read-only,不能修改工作区文件")
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
switch (action) {
|
|
1657
|
+
case "readText":
|
|
1658
|
+
return { text: await api.readText(path) }
|
|
1659
|
+
case "readBytes": {
|
|
1660
|
+
const data = await api.readBytes(path)
|
|
1661
|
+
return { data: data === null ? null : Buffer.from(data).toString("base64") }
|
|
1662
|
+
}
|
|
1663
|
+
case "readJson":
|
|
1664
|
+
return { value: await api.readJson(path) }
|
|
1665
|
+
case "writeText":
|
|
1666
|
+
await api.writeText(path, String(params["text"] ?? ""))
|
|
1667
|
+
return {}
|
|
1668
|
+
case "writeBytes":
|
|
1669
|
+
await api.writeBytes(
|
|
1670
|
+
path,
|
|
1671
|
+
new Uint8Array(Buffer.from(String(params["data"] ?? ""), "base64")),
|
|
1672
|
+
)
|
|
1673
|
+
return {}
|
|
1674
|
+
case "writeJson":
|
|
1675
|
+
await api.writeJson(path, params["value"])
|
|
1676
|
+
return {}
|
|
1677
|
+
case "list":
|
|
1678
|
+
return { entries: await api.list(path) }
|
|
1679
|
+
case "stat":
|
|
1680
|
+
return { stat: await api.stat(path) }
|
|
1681
|
+
case "mkdir":
|
|
1682
|
+
await api.mkdir(path)
|
|
1683
|
+
return {}
|
|
1684
|
+
case "remove":
|
|
1685
|
+
await api.remove(path)
|
|
1686
|
+
return {}
|
|
1687
|
+
case "exists":
|
|
1688
|
+
return { exists: await api.exists(path) }
|
|
1689
|
+
default:
|
|
1690
|
+
throw new Error(`未知 fs 方法:${method}`)
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
#registerRemoteHooks(hooks: readonly string[]): void {
|
|
1695
|
+
for (const hook of hooks) {
|
|
1696
|
+
const kind = HOOK_KIND_BY_API[hook]
|
|
1697
|
+
if (kind === undefined) {
|
|
1698
|
+
continue
|
|
1699
|
+
}
|
|
1700
|
+
const unregister = registerExternalHook(
|
|
1701
|
+
kind as never,
|
|
1702
|
+
this.#options.pluginId,
|
|
1703
|
+
async (payload) => {
|
|
1704
|
+
const result = (await this.runHook(
|
|
1705
|
+
hook,
|
|
1706
|
+
payload as Record<string, unknown>,
|
|
1707
|
+
)) as {
|
|
1708
|
+
prompt?: string[]
|
|
1709
|
+
}
|
|
1710
|
+
if (hook === "onMessage" || hook === "onSessionStart") {
|
|
1711
|
+
const target = payload as { prompt?: string[] }
|
|
1712
|
+
if (Array.isArray(result.prompt) && Array.isArray(target.prompt)) {
|
|
1713
|
+
target.prompt.splice(0, target.prompt.length, ...result.prompt)
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
},
|
|
1717
|
+
)
|
|
1718
|
+
this.#hookUnsubscribes.push(unregister)
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
async runCommand(
|
|
1723
|
+
name: string,
|
|
1724
|
+
argv: readonly string[],
|
|
1725
|
+
cwd: string,
|
|
1726
|
+
json: boolean,
|
|
1727
|
+
): Promise<number> {
|
|
1728
|
+
const result = (await this.#request("plugin.command", {
|
|
1729
|
+
name,
|
|
1730
|
+
argv: [...argv],
|
|
1731
|
+
cwd,
|
|
1732
|
+
json,
|
|
1733
|
+
})) as { exitCode?: number }
|
|
1734
|
+
return result.exitCode ?? 0
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
async runHook(hook: string, payload: Record<string, unknown>): Promise<unknown> {
|
|
1738
|
+
return await this.#request("plugin.hook", { hook, payload }, 30_000)
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
commandStubs(): PluginCommand[] {
|
|
1742
|
+
if (this.#ready === null) {
|
|
1743
|
+
return []
|
|
1744
|
+
}
|
|
1745
|
+
return this.#ready.commands.map((command) => ({
|
|
1746
|
+
name: command.name,
|
|
1747
|
+
summary: command.summary,
|
|
1748
|
+
usage: command.usage,
|
|
1749
|
+
aliases: command.aliases,
|
|
1750
|
+
run: async (ctx) => this.runCommand(command.name, ctx.argv, ctx.cwd, ctx.json),
|
|
1751
|
+
}))
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
async close(): Promise<void> {
|
|
1755
|
+
activeClients.delete(this)
|
|
1756
|
+
for (const [serverId, record] of this.#httpServers) {
|
|
1757
|
+
this.#httpServers.delete(serverId)
|
|
1758
|
+
await new Promise<void>((resolveClose) => record.server.close(() => resolveClose()))
|
|
1759
|
+
}
|
|
1760
|
+
if (this.#child === null) {
|
|
1761
|
+
return
|
|
1762
|
+
}
|
|
1763
|
+
auditSandbox(this.#options.paths, {
|
|
1764
|
+
type: "sandbox.plugin.stop",
|
|
1765
|
+
sandboxId: this.#sandboxId,
|
|
1766
|
+
pluginId: this.#options.pluginId,
|
|
1767
|
+
backend: this.#options.backend.name,
|
|
1768
|
+
})
|
|
1769
|
+
try {
|
|
1770
|
+
await this.#request("sandbox.shutdown", {}, 5_000)
|
|
1771
|
+
} catch {
|
|
1772
|
+
// 超时或已退出
|
|
1773
|
+
}
|
|
1774
|
+
this.#closed = true
|
|
1775
|
+
this.#kill()
|
|
1776
|
+
// 先收掉仍在跑的进程,再清监听,避免成了孤儿
|
|
1777
|
+
for (const session of this.#execSessions.values()) {
|
|
1778
|
+
if (!session.done) killSession(session)
|
|
1779
|
+
}
|
|
1780
|
+
this.#execSessions.clear()
|
|
1781
|
+
for (const session of this.#ptySessions.values()) {
|
|
1782
|
+
if (session.done) continue
|
|
1783
|
+
try {
|
|
1784
|
+
session.handle.kill()
|
|
1785
|
+
} catch {
|
|
1786
|
+
/* 已经退出 */
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
this.#ptySessions.clear()
|
|
1790
|
+
for (const unsubscribe of this.#eventUnsubscribes.values()) {
|
|
1791
|
+
unsubscribe()
|
|
1792
|
+
}
|
|
1793
|
+
for (const unsubscribe of this.#hookUnsubscribes) {
|
|
1794
|
+
unsubscribe()
|
|
1795
|
+
}
|
|
1796
|
+
await rm(this.#sandboxDir, { recursive: true, force: true }).catch(() => undefined)
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
async #prepareHostSdk(): Promise<void> {
|
|
1800
|
+
const copies: Array<[string, string]> = [
|
|
1801
|
+
[
|
|
1802
|
+
join(PROJECT_ROOT, "src", "api", "host"),
|
|
1803
|
+
join(this.#hostSdkDir, "src", "api", "host"),
|
|
1804
|
+
],
|
|
1805
|
+
[
|
|
1806
|
+
join(PROJECT_ROOT, "src", "api", "plugin"),
|
|
1807
|
+
join(this.#hostSdkDir, "src", "api", "plugin"),
|
|
1808
|
+
],
|
|
1809
|
+
[
|
|
1810
|
+
join(PROJECT_ROOT, "src", "event", "event.ts"),
|
|
1811
|
+
join(this.#hostSdkDir, "src", "event", "event.ts"),
|
|
1812
|
+
],
|
|
1813
|
+
[
|
|
1814
|
+
join(PROJECT_ROOT, "src", "session", "session.ts"),
|
|
1815
|
+
join(this.#hostSdkDir, "src", "session", "session.ts"),
|
|
1816
|
+
],
|
|
1817
|
+
[
|
|
1818
|
+
join(PROJECT_ROOT, "src", "session", "registry.ts"),
|
|
1819
|
+
join(this.#hostSdkDir, "src", "session", "registry.ts"),
|
|
1820
|
+
],
|
|
1821
|
+
[
|
|
1822
|
+
join(PROJECT_ROOT, "src", "session", "replay.ts"),
|
|
1823
|
+
join(this.#hostSdkDir, "src", "session", "replay.ts"),
|
|
1824
|
+
],
|
|
1825
|
+
[
|
|
1826
|
+
join(PROJECT_ROOT, "src", "session", "types.ts"),
|
|
1827
|
+
join(this.#hostSdkDir, "src", "session", "types.ts"),
|
|
1828
|
+
],
|
|
1829
|
+
[
|
|
1830
|
+
join(PROJECT_ROOT, "src", "workspace", "index.ts"),
|
|
1831
|
+
join(this.#hostSdkDir, "src", "workspace", "index.ts"),
|
|
1832
|
+
],
|
|
1833
|
+
[
|
|
1834
|
+
join(PROJECT_ROOT, "src", "sandbox", "bridge-state.ts"),
|
|
1835
|
+
join(this.#hostSdkDir, "src", "sandbox", "bridge-state.ts"),
|
|
1836
|
+
],
|
|
1837
|
+
[
|
|
1838
|
+
join(PROJECT_ROOT, "src", "sandbox", "protocol.ts"),
|
|
1839
|
+
join(this.#hostSdkDir, "src", "sandbox", "protocol.ts"),
|
|
1840
|
+
],
|
|
1841
|
+
[
|
|
1842
|
+
join(PROJECT_ROOT, "src", "sandbox", "rpc-timeouts.ts"),
|
|
1843
|
+
join(this.#hostSdkDir, "src", "sandbox", "rpc-timeouts.ts"),
|
|
1844
|
+
],
|
|
1845
|
+
[
|
|
1846
|
+
join(PROJECT_ROOT, "src", "sandbox", "plugin-host.ts"),
|
|
1847
|
+
join(this.#hostSdkDir, "src", "sandbox", "plugin-host.ts"),
|
|
1848
|
+
],
|
|
1849
|
+
[
|
|
1850
|
+
join(PROJECT_ROOT, "src", "plugin", "host-alias.ts"),
|
|
1851
|
+
join(this.#hostSdkDir, "src", "plugin", "host-alias.ts"),
|
|
1852
|
+
],
|
|
1853
|
+
[
|
|
1854
|
+
join(PROJECT_ROOT, "src", "plugin", "access-guard.ts"),
|
|
1855
|
+
join(this.#hostSdkDir, "src", "plugin", "access-guard.ts"),
|
|
1856
|
+
],
|
|
1857
|
+
]
|
|
1858
|
+
for (const [source, target] of copies) {
|
|
1859
|
+
await mkdir(dirname(target), { recursive: true })
|
|
1860
|
+
await cp(source, target, { recursive: true })
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
#failAll(error: Error): void {
|
|
1865
|
+
for (const pending of this.#pending.values()) {
|
|
1866
|
+
pending.reject(error)
|
|
1867
|
+
}
|
|
1868
|
+
this.#pending.clear()
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
#kill(): void {
|
|
1872
|
+
if (this.#child?.pid !== undefined) {
|
|
1873
|
+
try {
|
|
1874
|
+
process.kill(-this.#child.pid, "SIGKILL")
|
|
1875
|
+
} catch {
|
|
1876
|
+
this.#child.kill("SIGKILL")
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
this.#closed = true
|
|
1880
|
+
}
|
|
1881
|
+
}
|