@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,406 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SandboxManager:选择 backend、准备 workspace/scratch、执行 agent-code、收集 artifact。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto"
|
|
6
|
+
import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"
|
|
7
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path"
|
|
8
|
+
|
|
9
|
+
import type { SandboxAccess } from "../config/types.ts"
|
|
10
|
+
|
|
11
|
+
import { auditSandbox } from "./audit.ts"
|
|
12
|
+
import { BwrapBackend } from "./backends/bwrap.ts"
|
|
13
|
+
import { DirectBackend } from "./backends/direct.ts"
|
|
14
|
+
import { createSandboxMemoryLimit, type MemoryLimitState } from "./memory-limit.ts"
|
|
15
|
+
import { startSandboxProcess, type SandboxProcessHandle } from "./supervisor.ts"
|
|
16
|
+
import {
|
|
17
|
+
SandboxExecutionError,
|
|
18
|
+
SandboxUnavailableError,
|
|
19
|
+
type AgentCodeRequest,
|
|
20
|
+
type AgentCodeResult,
|
|
21
|
+
type SandboxArtifact,
|
|
22
|
+
type SandboxBackend,
|
|
23
|
+
type SandboxBackendStatus,
|
|
24
|
+
type AgentCodeTaskHandle,
|
|
25
|
+
type SandboxLimits,
|
|
26
|
+
type SandboxManagerOptions,
|
|
27
|
+
type StopMode,
|
|
28
|
+
} from "./types.ts"
|
|
29
|
+
|
|
30
|
+
const MAX_ARTIFACT_FILES = 32
|
|
31
|
+
const MAX_ARTIFACT_BYTES = 64 * 1024 * 1024
|
|
32
|
+
|
|
33
|
+
interface ResolvedLimits extends SandboxLimits {
|
|
34
|
+
readonly timeoutMs: number
|
|
35
|
+
readonly memoryMB: number
|
|
36
|
+
readonly pids: number
|
|
37
|
+
readonly outputLimitBytes: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function clampInteger(value: number, min: number, max: number): number {
|
|
41
|
+
return Math.min(max, Math.max(min, Math.trunc(value)))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function baseEnv(): Record<string, string> {
|
|
45
|
+
return {
|
|
46
|
+
PATH: "/usr/bin:/bin",
|
|
47
|
+
LANG: "C.UTF-8",
|
|
48
|
+
LC_ALL: "C.UTF-8",
|
|
49
|
+
TERM: "dumb",
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function ensureDirectory(path: string): Promise<void> {
|
|
54
|
+
await mkdir(path, { recursive: true })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isInside(root: string, target: string): boolean {
|
|
58
|
+
return target === root || target.startsWith(`${root}${sep}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function collectArtifacts(
|
|
62
|
+
scratchDir: string,
|
|
63
|
+
sandboxDir: string,
|
|
64
|
+
): Promise<{ artifacts: SandboxArtifact[]; artifactDir: string | null }> {
|
|
65
|
+
const source = join(scratchDir, "artifacts")
|
|
66
|
+
const info = await stat(source).catch(() => null)
|
|
67
|
+
if (info === null || !info.isDirectory()) {
|
|
68
|
+
return { artifacts: [], artifactDir: null }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const destination = join(sandboxDir, "artifacts")
|
|
72
|
+
await ensureDirectory(destination)
|
|
73
|
+
const collected: SandboxArtifact[] = []
|
|
74
|
+
let total = 0
|
|
75
|
+
|
|
76
|
+
const entries = await readdir(source, { recursive: true, withFileTypes: true })
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (!entry.isFile() || collected.length >= MAX_ARTIFACT_FILES) {
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
const parent = entry.parentPath ?? source
|
|
82
|
+
const absolute = join(parent, entry.name)
|
|
83
|
+
const rel = relative(source, absolute)
|
|
84
|
+
if (rel.startsWith("..") || resolve(absolute) === resolve(source)) {
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
const fileInfo = await stat(absolute)
|
|
88
|
+
if (fileInfo.size > MAX_ARTIFACT_BYTES || total + fileInfo.size > MAX_ARTIFACT_BYTES) {
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
total += fileInfo.size
|
|
92
|
+
const data = await readFile(absolute)
|
|
93
|
+
const sha256 = createHash("sha256").update(data).digest("hex")
|
|
94
|
+
const target = join(destination, rel)
|
|
95
|
+
await ensureDirectory(dirname(target))
|
|
96
|
+
await writeFile(target, data)
|
|
97
|
+
collected.push({ path: rel.split(sep).join("/"), size: fileInfo.size, sha256 })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { artifacts: collected, artifactDir: destination }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class SandboxManager {
|
|
104
|
+
readonly #options: SandboxManagerOptions
|
|
105
|
+
#backend: SandboxBackend | null
|
|
106
|
+
#resolved: Promise<SandboxBackend> | null = null
|
|
107
|
+
#memoryLimitWarned = false
|
|
108
|
+
#active = 0
|
|
109
|
+
readonly #waiters: Array<() => void> = []
|
|
110
|
+
|
|
111
|
+
constructor(options: SandboxManagerOptions) {
|
|
112
|
+
this.#options = options
|
|
113
|
+
this.#backend = options.backend ?? null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async probe(): Promise<SandboxBackendStatus> {
|
|
117
|
+
if (this.#backend !== null) {
|
|
118
|
+
return await this.#backend.probe()
|
|
119
|
+
}
|
|
120
|
+
if (this.#options.settings.backend === "nsjail") {
|
|
121
|
+
return {
|
|
122
|
+
available: false,
|
|
123
|
+
backend: "nsjail",
|
|
124
|
+
reason: "nsjail 后端尚未实现",
|
|
125
|
+
version: null,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return await new BwrapBackend().probe()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async #resolveBackend(access: SandboxAccess): Promise<SandboxBackend> {
|
|
132
|
+
if (access === "danger-full-access") {
|
|
133
|
+
return new DirectBackend()
|
|
134
|
+
}
|
|
135
|
+
if (this.#backend !== null) {
|
|
136
|
+
return this.#backend
|
|
137
|
+
}
|
|
138
|
+
if (this.#resolved !== null) {
|
|
139
|
+
return await this.#resolved
|
|
140
|
+
}
|
|
141
|
+
this.#resolved = (async (): Promise<SandboxBackend> => {
|
|
142
|
+
const { settings, logger } = this.#options
|
|
143
|
+
if (settings.mode === "off") {
|
|
144
|
+
throw new SandboxUnavailableError("sandbox.mode = off,Agent 代码执行需要 sandbox")
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const preferred = settings.backend
|
|
148
|
+
const candidates: SandboxBackend[] = []
|
|
149
|
+
if (preferred === "auto" || preferred === "bwrap") {
|
|
150
|
+
candidates.push(new BwrapBackend())
|
|
151
|
+
}
|
|
152
|
+
if (preferred === "nsjail") {
|
|
153
|
+
// nsjail 后端尚未实现,显式报告不可用
|
|
154
|
+
throw new SandboxUnavailableError("nsjail 后端尚未实现")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const candidate of candidates) {
|
|
158
|
+
const status = await candidate.probe()
|
|
159
|
+
if (status.available) {
|
|
160
|
+
return candidate
|
|
161
|
+
}
|
|
162
|
+
logger?.warn(
|
|
163
|
+
{ backend: candidate.name, reason: status.reason },
|
|
164
|
+
"sandbox backend 不可用",
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (settings.mode === "best-effort") {
|
|
169
|
+
logger?.warn(
|
|
170
|
+
{ backend: "direct" },
|
|
171
|
+
"sandbox 降级为 direct(无隔离),仅限开发/受信环境",
|
|
172
|
+
)
|
|
173
|
+
return new DirectBackend()
|
|
174
|
+
}
|
|
175
|
+
throw new SandboxUnavailableError("没有可用的 OS 级 sandbox 后端")
|
|
176
|
+
})()
|
|
177
|
+
return await this.#resolved
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async #acquireSlot(): Promise<void> {
|
|
181
|
+
const max = this.#options.settings.maxConcurrent
|
|
182
|
+
if (this.#active < max) {
|
|
183
|
+
this.#active += 1
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
await new Promise<void>((resolve) => {
|
|
187
|
+
this.#waiters.push(resolve)
|
|
188
|
+
})
|
|
189
|
+
this.#active += 1
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#releaseSlot(): void {
|
|
193
|
+
this.#active -= 1
|
|
194
|
+
const next = this.#waiters.shift()
|
|
195
|
+
next?.()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#limits(request: AgentCodeRequest): ResolvedLimits {
|
|
199
|
+
const settings = this.#options.settings
|
|
200
|
+
return {
|
|
201
|
+
timeoutMs: clampInteger(request.timeoutMs ?? settings.defaultTimeoutMs, 100, 600_000),
|
|
202
|
+
memoryMB: clampInteger(request.memoryMB ?? settings.defaultMemoryMB, 32, 8_192),
|
|
203
|
+
pids: clampInteger(64, 1, 512),
|
|
204
|
+
outputLimitBytes: clampInteger(
|
|
205
|
+
request.outputLimitBytes ?? settings.outputLimitBytes,
|
|
206
|
+
1024,
|
|
207
|
+
64 * 1024 * 1024,
|
|
208
|
+
),
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async startAgentCode(request: AgentCodeRequest): Promise<AgentCodeTaskHandle> {
|
|
213
|
+
if (typeof request.command !== "string" || request.command.trim() === "") {
|
|
214
|
+
throw new SandboxExecutionError("命令不能为空")
|
|
215
|
+
}
|
|
216
|
+
if (request.argv.some((item) => typeof item !== "string")) {
|
|
217
|
+
throw new SandboxExecutionError("argv 必须是字符串数组")
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const access = request.access ?? this.#options.settings.access
|
|
221
|
+
if (
|
|
222
|
+
access !== "read-only" &&
|
|
223
|
+
access !== "workspace-write" &&
|
|
224
|
+
access !== "danger-full-access"
|
|
225
|
+
) {
|
|
226
|
+
throw new SandboxExecutionError(`未知 sandbox 权限:${String(access)}`)
|
|
227
|
+
}
|
|
228
|
+
const workspaceDir = resolve(request.workspaceDir ?? process.cwd())
|
|
229
|
+
const cwd = resolve(request.cwd ?? workspaceDir)
|
|
230
|
+
if (access !== "danger-full-access" && !isInside(workspaceDir, cwd)) {
|
|
231
|
+
throw new SandboxExecutionError(`cwd 不在 workspace 内:${cwd}`)
|
|
232
|
+
}
|
|
233
|
+
const workspaceInfo = await stat(workspaceDir).catch(() => null)
|
|
234
|
+
if (workspaceInfo === null || !workspaceInfo.isDirectory()) {
|
|
235
|
+
throw new SandboxExecutionError(`workspace 不存在:${workspaceDir}`)
|
|
236
|
+
}
|
|
237
|
+
const cwdInfo = await stat(cwd).catch(() => null)
|
|
238
|
+
if (cwdInfo === null || !cwdInfo.isDirectory()) {
|
|
239
|
+
throw new SandboxExecutionError(`cwd 不存在:${cwd}`)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const backend = await this.#resolveBackend(access)
|
|
243
|
+
await this.#acquireSlot()
|
|
244
|
+
try {
|
|
245
|
+
return await this.#startAgentCodeLocked(request, backend, workspaceDir, cwd, access)
|
|
246
|
+
} catch (error) {
|
|
247
|
+
this.#releaseSlot()
|
|
248
|
+
throw error
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async runAgentCode(request: AgentCodeRequest): Promise<AgentCodeResult> {
|
|
253
|
+
const task = await this.startAgentCode(request)
|
|
254
|
+
return await task.wait()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async #startAgentCodeLocked(
|
|
258
|
+
request: AgentCodeRequest,
|
|
259
|
+
backend: SandboxBackend,
|
|
260
|
+
workspaceDir: string,
|
|
261
|
+
cwd: string,
|
|
262
|
+
access: SandboxAccess,
|
|
263
|
+
): Promise<AgentCodeTaskHandle> {
|
|
264
|
+
const limits = this.#limits(request)
|
|
265
|
+
const sandboxId = randomUUID()
|
|
266
|
+
const sandboxDir = join(this.#options.paths.dataDir, "sandbox", sandboxId)
|
|
267
|
+
const scratchDir = join(sandboxDir, "scratch")
|
|
268
|
+
await ensureDirectory(join(scratchDir, "tmp"))
|
|
269
|
+
await ensureDirectory(join(scratchDir, "artifacts"))
|
|
270
|
+
|
|
271
|
+
const env = baseEnv()
|
|
272
|
+
if (backend.name === "bwrap") {
|
|
273
|
+
env["HOME"] = "/scratch"
|
|
274
|
+
env["TMPDIR"] = "/scratch/tmp"
|
|
275
|
+
env["SCRATCH"] = "/scratch"
|
|
276
|
+
env["WORKSPACE"] = "/workspace"
|
|
277
|
+
} else {
|
|
278
|
+
env["HOME"] = scratchDir
|
|
279
|
+
env["TMPDIR"] = join(scratchDir, "tmp")
|
|
280
|
+
env["SCRATCH"] = scratchDir
|
|
281
|
+
env["WORKSPACE"] = workspaceDir
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const memoryLimit: MemoryLimitState | null =
|
|
285
|
+
backend.name === "bwrap"
|
|
286
|
+
? await createSandboxMemoryLimit({
|
|
287
|
+
bytes: limits.memoryMB * 1024 * 1024,
|
|
288
|
+
timeoutMs: limits.timeoutMs,
|
|
289
|
+
})
|
|
290
|
+
: null
|
|
291
|
+
if (memoryLimit !== null && !memoryLimit.active && !this.#memoryLimitWarned) {
|
|
292
|
+
this.#memoryLimitWarned = true
|
|
293
|
+
this.#options.logger?.warn(
|
|
294
|
+
{ reason: memoryLimit.reason },
|
|
295
|
+
"沙箱物理内存限制不可用,仅保留 V8 堆上限与超时",
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let processHandle: SandboxProcessHandle
|
|
300
|
+
try {
|
|
301
|
+
const baseSpec = backend.buildAgentLaunch({
|
|
302
|
+
workspaceDir,
|
|
303
|
+
scratchDir,
|
|
304
|
+
cwd,
|
|
305
|
+
access,
|
|
306
|
+
network: request.network,
|
|
307
|
+
command: request.command,
|
|
308
|
+
argv: request.argv,
|
|
309
|
+
env,
|
|
310
|
+
limits,
|
|
311
|
+
})
|
|
312
|
+
const spec =
|
|
313
|
+
memoryLimit !== null && memoryLimit.active ? memoryLimit.wrap(baseSpec) : baseSpec
|
|
314
|
+
processHandle = startSandboxProcess(spec, limits, {
|
|
315
|
+
stdin: request.stdin ?? "",
|
|
316
|
+
})
|
|
317
|
+
} catch (error) {
|
|
318
|
+
await memoryLimit?.finish()
|
|
319
|
+
this.#releaseSlot()
|
|
320
|
+
throw error
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let finalize: Promise<AgentCodeResult> | null = null
|
|
324
|
+
const wait = (): Promise<AgentCodeResult> => {
|
|
325
|
+
finalize ??= (async (): Promise<AgentCodeResult> => {
|
|
326
|
+
try {
|
|
327
|
+
const processResult = await processHandle.done
|
|
328
|
+
const memory = await memoryLimit?.finish()
|
|
329
|
+
const oomKilled = memory?.oomKilled === true
|
|
330
|
+
const { artifacts, artifactDir } = await collectArtifacts(
|
|
331
|
+
scratchDir,
|
|
332
|
+
sandboxDir,
|
|
333
|
+
)
|
|
334
|
+
await rm(scratchDir, { recursive: true, force: true })
|
|
335
|
+
auditSandbox(this.#options.paths, {
|
|
336
|
+
type: "sandbox.agent-code",
|
|
337
|
+
sandboxId,
|
|
338
|
+
backend: backend.name,
|
|
339
|
+
access,
|
|
340
|
+
exitCode: processResult.exitCode,
|
|
341
|
+
timedOut: processResult.timedOut,
|
|
342
|
+
durationMs: processResult.durationMs,
|
|
343
|
+
artifacts: artifacts.length,
|
|
344
|
+
})
|
|
345
|
+
this.#options.logger?.info(
|
|
346
|
+
{
|
|
347
|
+
sandboxId,
|
|
348
|
+
backend: backend.name,
|
|
349
|
+
command: basename(request.command),
|
|
350
|
+
exitCode: processResult.exitCode,
|
|
351
|
+
timedOut: processResult.timedOut,
|
|
352
|
+
durationMs: processResult.durationMs,
|
|
353
|
+
},
|
|
354
|
+
"sandbox agent-code 执行完成",
|
|
355
|
+
)
|
|
356
|
+
return {
|
|
357
|
+
sandboxId,
|
|
358
|
+
backend: backend.name,
|
|
359
|
+
access,
|
|
360
|
+
exitCode: processResult.exitCode,
|
|
361
|
+
signal: processResult.signal,
|
|
362
|
+
stdout: processResult.stdout,
|
|
363
|
+
stderr: processResult.stderr,
|
|
364
|
+
truncated: processResult.truncated,
|
|
365
|
+
timedOut: processResult.timedOut,
|
|
366
|
+
memoryLimit: memoryLimit?.active === true ? "cgroup" : "none",
|
|
367
|
+
memoryLimitReason:
|
|
368
|
+
memoryLimit !== null && !memoryLimit.active ? memoryLimit.reason : null,
|
|
369
|
+
oomKilled,
|
|
370
|
+
durationMs: processResult.durationMs,
|
|
371
|
+
artifacts,
|
|
372
|
+
artifactDir:
|
|
373
|
+
artifactDir === null
|
|
374
|
+
? null
|
|
375
|
+
: backend.name === "bwrap"
|
|
376
|
+
? "/scratch/artifacts"
|
|
377
|
+
: artifactDir,
|
|
378
|
+
sandboxDir: backend.name === "bwrap" ? "/scratch" : sandboxDir,
|
|
379
|
+
hostArtifactDir: artifactDir,
|
|
380
|
+
hostSandboxDir: sandboxDir,
|
|
381
|
+
}
|
|
382
|
+
} finally {
|
|
383
|
+
this.#releaseSlot()
|
|
384
|
+
}
|
|
385
|
+
})()
|
|
386
|
+
void finalize.catch(() => undefined)
|
|
387
|
+
return finalize
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
sandboxId,
|
|
392
|
+
backend: backend.name,
|
|
393
|
+
access,
|
|
394
|
+
get stdout() {
|
|
395
|
+
return processHandle.stdout
|
|
396
|
+
},
|
|
397
|
+
get stderr() {
|
|
398
|
+
return processHandle.stderr
|
|
399
|
+
},
|
|
400
|
+
stop(mode: StopMode) {
|
|
401
|
+
processHandle.stop(mode)
|
|
402
|
+
},
|
|
403
|
+
wait,
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 沙箱物理内存限制。
|
|
3
|
+
*
|
|
4
|
+
* RLIMIT_AS 限制的是虚拟地址空间,会直接打死 V8 / JVM / Go 这类需要预留大块地址空间的
|
|
5
|
+
* 运行时,且并不能约束真实物理内存。这里改用 cgroup v2 的 memory.max:
|
|
6
|
+
*
|
|
7
|
+
* 1. 用 `systemd-run --user --scope -p MemoryMax=<bytes> -p MemorySwapMax=0` 起一个
|
|
8
|
+
* 临时 scope(用户 session 下无需 root);
|
|
9
|
+
* 2. 读 systemctl 的 ControlGroup 拿到 cgroup 路径;
|
|
10
|
+
* 3. 用一层 /bin/sh 包住真正的 sandbox 命令:先把 shell 自己写进 cgroup.procs,
|
|
11
|
+
* 再 exec bwrap,保证 bwrap 及其后代一出生就在受限 cgroup 里;
|
|
12
|
+
* 4. 结束后读 memory.events 判断是否发生过 oom_kill,再停掉 scope。
|
|
13
|
+
*
|
|
14
|
+
* 宿主没有 systemd user manager(例如容器、SSH 非登录环境)时返回 inactive,
|
|
15
|
+
* 调用方退回“只限制 V8 堆 + 超时”的方式,不阻塞命令执行。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { spawn } from "node:child_process"
|
|
19
|
+
import { randomUUID } from "node:crypto"
|
|
20
|
+
import { accessSync, constants, readFileSync } from "node:fs"
|
|
21
|
+
import { join } from "node:path"
|
|
22
|
+
|
|
23
|
+
import type { LaunchSpec } from "./types.ts"
|
|
24
|
+
|
|
25
|
+
export const CGROUP_V2_ROOT = "/sys/fs/cgroup"
|
|
26
|
+
|
|
27
|
+
export interface MemoryLimitState {
|
|
28
|
+
/** 是否真的挂上了 cgroup 限制 */
|
|
29
|
+
readonly active: boolean
|
|
30
|
+
/** active=false 时的原因(给结果与日志用) */
|
|
31
|
+
readonly reason: string | null
|
|
32
|
+
/** 把启动命令包进受限 cgroup;inactive 时原样返回 */
|
|
33
|
+
wrap(spec: LaunchSpec): LaunchSpec
|
|
34
|
+
/** 读 oom 事件并清理 scope;可重复调用 */
|
|
35
|
+
finish(): Promise<{ oomKilled: boolean }>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MemoryLimitCommandResult {
|
|
39
|
+
readonly code: number | null
|
|
40
|
+
readonly stdout: string
|
|
41
|
+
readonly stderr: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 便于单测替换外部命令、文件与时间 */
|
|
45
|
+
export interface MemoryLimitDeps {
|
|
46
|
+
readonly platform: NodeJS.Platform
|
|
47
|
+
readonly env: NodeJS.ProcessEnv
|
|
48
|
+
readonly run: (command: string, args: readonly string[]) => Promise<MemoryLimitCommandResult>
|
|
49
|
+
readonly spawnHolder: (command: string, args: readonly string[]) => { readonly pid?: number }
|
|
50
|
+
readonly readText: (path: string) => string | null
|
|
51
|
+
readonly writable: (path: string) => boolean
|
|
52
|
+
readonly sleep: (ms: number) => Promise<void>
|
|
53
|
+
readonly randomSuffix: () => string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface MemoryLimitOptions {
|
|
57
|
+
/** 物理内存上限,字节 */
|
|
58
|
+
readonly bytes: number
|
|
59
|
+
/** 沙箱超时;用来给 scope 的 RuntimeMaxSec 留余量,避免泄漏 */
|
|
60
|
+
readonly timeoutMs: number
|
|
61
|
+
readonly deps?: Partial<MemoryLimitDeps>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const CGROUP_POLL_TIMES = 10
|
|
65
|
+
const CGROUP_POLL_INTERVAL_MS = 50
|
|
66
|
+
const SCOPE_RUNTIME_GRACE_MS = 120_000
|
|
67
|
+
|
|
68
|
+
function runCommand(command: string, args: readonly string[]): Promise<MemoryLimitCommandResult> {
|
|
69
|
+
return new Promise((resolveCommand) => {
|
|
70
|
+
const child = spawn(command, [...args], {
|
|
71
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
72
|
+
env: process.env,
|
|
73
|
+
})
|
|
74
|
+
const out: Buffer[] = []
|
|
75
|
+
const err: Buffer[] = []
|
|
76
|
+
child.stdout?.on("data", (chunk: Buffer) => out.push(chunk))
|
|
77
|
+
child.stderr?.on("data", (chunk: Buffer) => err.push(chunk))
|
|
78
|
+
child.once("error", (error) => {
|
|
79
|
+
resolveCommand({ code: null, stdout: "", stderr: error.message })
|
|
80
|
+
})
|
|
81
|
+
child.once("close", (code) => {
|
|
82
|
+
resolveCommand({
|
|
83
|
+
code,
|
|
84
|
+
stdout: Buffer.concat(out).toString("utf8"),
|
|
85
|
+
stderr: Buffer.concat(err).toString("utf8"),
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const defaultDeps: MemoryLimitDeps = {
|
|
92
|
+
platform: process.platform,
|
|
93
|
+
env: process.env,
|
|
94
|
+
run: runCommand,
|
|
95
|
+
spawnHolder: (command, args) => {
|
|
96
|
+
const child = spawn(command, [...args], {
|
|
97
|
+
stdio: "ignore",
|
|
98
|
+
env: process.env,
|
|
99
|
+
detached: true,
|
|
100
|
+
})
|
|
101
|
+
child.unref()
|
|
102
|
+
return { pid: child.pid }
|
|
103
|
+
},
|
|
104
|
+
readText: (path) => {
|
|
105
|
+
try {
|
|
106
|
+
return readFileSync(path, "utf8")
|
|
107
|
+
} catch {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
writable: (path) => {
|
|
112
|
+
try {
|
|
113
|
+
accessSync(path, constants.W_OK)
|
|
114
|
+
return true
|
|
115
|
+
} catch {
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
sleep: (ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms)),
|
|
120
|
+
randomSuffix: () => randomUUID().slice(0, 8),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function inactive(reason: string): MemoryLimitState {
|
|
124
|
+
return {
|
|
125
|
+
active: false,
|
|
126
|
+
reason,
|
|
127
|
+
wrap: (spec) => spec,
|
|
128
|
+
finish: async () => ({ oomKilled: false }),
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 从 memory.events 里读 oom_kill 计数 */
|
|
133
|
+
export function parseOomKill(memoryEvents: string | null): boolean {
|
|
134
|
+
if (memoryEvents === null) {
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
const match = /^oom_kill\s+(\d+)\s*$/m.exec(memoryEvents)
|
|
138
|
+
return match !== null && Number(match[1]) > 0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** sandbox 命令外层包一层 sh:先把自己放进 cgroup,再 exec 原命令 */
|
|
142
|
+
export function wrapSpecInCgroup(spec: LaunchSpec, cgroupPath: string): LaunchSpec {
|
|
143
|
+
return {
|
|
144
|
+
...spec,
|
|
145
|
+
command: "/bin/sh",
|
|
146
|
+
args: [
|
|
147
|
+
"-c",
|
|
148
|
+
'echo $$ > "$1/cgroup.procs" || exit 126; shift; exec "$@"',
|
|
149
|
+
"sh",
|
|
150
|
+
cgroupPath,
|
|
151
|
+
spec.command,
|
|
152
|
+
...spec.args,
|
|
153
|
+
],
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function createSandboxMemoryLimit(
|
|
158
|
+
options: MemoryLimitOptions,
|
|
159
|
+
): Promise<MemoryLimitState> {
|
|
160
|
+
const deps: MemoryLimitDeps = { ...defaultDeps, ...options.deps }
|
|
161
|
+
const bytes = Math.trunc(options.bytes)
|
|
162
|
+
if (deps.platform !== "linux") {
|
|
163
|
+
return inactive("非 Linux 平台不使用 cgroup 内存限制")
|
|
164
|
+
}
|
|
165
|
+
if (!Number.isFinite(bytes) || bytes < 16 * 1024 * 1024) {
|
|
166
|
+
return inactive("内存上限过小,跳过 cgroup 限制")
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const unit = `wellspring-sandbox-${process.pid}-${deps.randomSuffix()}`
|
|
170
|
+
const runtimeSec = Math.ceil((Math.max(1, options.timeoutMs) + SCOPE_RUNTIME_GRACE_MS) / 1000)
|
|
171
|
+
const holderArgs = [
|
|
172
|
+
"--user",
|
|
173
|
+
"--scope",
|
|
174
|
+
"--quiet",
|
|
175
|
+
"--collect",
|
|
176
|
+
"--unit",
|
|
177
|
+
unit,
|
|
178
|
+
"--property",
|
|
179
|
+
`MemoryMax=${bytes}`,
|
|
180
|
+
// 不把物理内存挤进 swap,否则 memory.max 会被换页绕过
|
|
181
|
+
"--property",
|
|
182
|
+
"MemorySwapMax=0",
|
|
183
|
+
"--property",
|
|
184
|
+
`RuntimeMaxSec=${runtimeSec}`,
|
|
185
|
+
"--",
|
|
186
|
+
"/bin/sleep",
|
|
187
|
+
"infinity",
|
|
188
|
+
]
|
|
189
|
+
try {
|
|
190
|
+
deps.spawnHolder("systemd-run", holderArgs)
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return inactive(
|
|
193
|
+
`systemd-run 不可用:${error instanceof Error ? error.message : String(error)}`,
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const unitName = `${unit}.scope`
|
|
198
|
+
let cgroup: string | null = null
|
|
199
|
+
let lastError = ""
|
|
200
|
+
for (let attempt = 0; attempt < CGROUP_POLL_TIMES; attempt += 1) {
|
|
201
|
+
const shown = await deps.run("systemctl", [
|
|
202
|
+
"--user",
|
|
203
|
+
"show",
|
|
204
|
+
"-p",
|
|
205
|
+
"ControlGroup",
|
|
206
|
+
"--value",
|
|
207
|
+
unitName,
|
|
208
|
+
])
|
|
209
|
+
const value = shown.stdout.trim()
|
|
210
|
+
if (value.startsWith("/")) {
|
|
211
|
+
cgroup = value
|
|
212
|
+
break
|
|
213
|
+
}
|
|
214
|
+
lastError = shown.stderr.trim()
|
|
215
|
+
await deps.sleep(CGROUP_POLL_INTERVAL_MS)
|
|
216
|
+
}
|
|
217
|
+
const stop = async (): Promise<void> => {
|
|
218
|
+
await deps.run("systemctl", ["--user", "stop", unitName])
|
|
219
|
+
}
|
|
220
|
+
if (cgroup === null) {
|
|
221
|
+
await stop()
|
|
222
|
+
return inactive(`systemd user scope 不可用${lastError === "" ? "" : `:${lastError}`}`)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const cgroupPath = join(CGROUP_V2_ROOT, cgroup)
|
|
226
|
+
if (!deps.writable(join(cgroupPath, "cgroup.procs"))) {
|
|
227
|
+
await stop()
|
|
228
|
+
return inactive(`无法写入 cgroup:${cgroupPath}`)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let finished: Promise<{ oomKilled: boolean }> | null = null
|
|
232
|
+
return {
|
|
233
|
+
active: true,
|
|
234
|
+
reason: null,
|
|
235
|
+
wrap: (spec) => wrapSpecInCgroup(spec, cgroupPath),
|
|
236
|
+
finish: () => {
|
|
237
|
+
finished ??= (async () => {
|
|
238
|
+
const oomKilled = parseOomKill(deps.readText(join(cgroupPath, "memory.events")))
|
|
239
|
+
await stop()
|
|
240
|
+
return { oomKilled }
|
|
241
|
+
})()
|
|
242
|
+
return finished
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
}
|