@tickernelz/paperclip-pro-adapter-utils 2026.925.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/README.md +37 -0
- package/dist/acpx-engine/cli.d.ts +2 -0
- package/dist/acpx-engine/cli.d.ts.map +1 -0
- package/dist/acpx-engine/cli.js +128 -0
- package/dist/acpx-engine/cli.js.map +1 -0
- package/dist/acpx-engine/composed-run-characterization.test.d.ts +2 -0
- package/dist/acpx-engine/composed-run-characterization.test.d.ts.map +1 -0
- package/dist/acpx-engine/composed-run-characterization.test.js +739 -0
- package/dist/acpx-engine/composed-run-characterization.test.js.map +1 -0
- package/dist/acpx-engine/constants.d.ts +20 -0
- package/dist/acpx-engine/constants.d.ts.map +1 -0
- package/dist/acpx-engine/constants.js +37 -0
- package/dist/acpx-engine/constants.js.map +1 -0
- package/dist/acpx-engine/execute-identity.test.d.ts +2 -0
- package/dist/acpx-engine/execute-identity.test.d.ts.map +1 -0
- package/dist/acpx-engine/execute-identity.test.js +218 -0
- package/dist/acpx-engine/execute-identity.test.js.map +1 -0
- package/dist/acpx-engine/execute.d.ts +325 -0
- package/dist/acpx-engine/execute.d.ts.map +1 -0
- package/dist/acpx-engine/execute.js +4448 -0
- package/dist/acpx-engine/execute.js.map +1 -0
- package/dist/acpx-engine/execute.test.d.ts +2 -0
- package/dist/acpx-engine/execute.test.d.ts.map +1 -0
- package/dist/acpx-engine/execute.test.js +6694 -0
- package/dist/acpx-engine/execute.test.js.map +1 -0
- package/dist/acpx-engine/index.d.ts +6 -0
- package/dist/acpx-engine/index.d.ts.map +1 -0
- package/dist/acpx-engine/index.js +6 -0
- package/dist/acpx-engine/index.js.map +1 -0
- package/dist/acpx-engine/local-process-control.d.ts +5 -0
- package/dist/acpx-engine/local-process-control.d.ts.map +1 -0
- package/dist/acpx-engine/local-process-control.js +39 -0
- package/dist/acpx-engine/local-process-control.js.map +1 -0
- package/dist/acpx-engine/local-process-control.test.d.ts +2 -0
- package/dist/acpx-engine/local-process-control.test.d.ts.map +1 -0
- package/dist/acpx-engine/local-process-control.test.js +38 -0
- package/dist/acpx-engine/local-process-control.test.js.map +1 -0
- package/dist/acpx-engine/operator-stop.test.d.ts +2 -0
- package/dist/acpx-engine/operator-stop.test.d.ts.map +1 -0
- package/dist/acpx-engine/operator-stop.test.js +107 -0
- package/dist/acpx-engine/operator-stop.test.js.map +1 -0
- package/dist/acpx-engine/remote-spawn-smoke.test.d.ts +2 -0
- package/dist/acpx-engine/remote-spawn-smoke.test.d.ts.map +1 -0
- package/dist/acpx-engine/remote-spawn-smoke.test.js +151 -0
- package/dist/acpx-engine/remote-spawn-smoke.test.js.map +1 -0
- package/dist/acpx-engine/run-contracts.d.ts +413 -0
- package/dist/acpx-engine/run-contracts.d.ts.map +1 -0
- package/dist/acpx-engine/run-contracts.js +20 -0
- package/dist/acpx-engine/run-contracts.js.map +1 -0
- package/dist/acpx-engine/run-contracts.test.d.ts +2 -0
- package/dist/acpx-engine/run-contracts.test.d.ts.map +1 -0
- package/dist/acpx-engine/run-contracts.test.js +57 -0
- package/dist/acpx-engine/run-contracts.test.js.map +1 -0
- package/dist/acpx-engine/run-coordinator.d.ts +57 -0
- package/dist/acpx-engine/run-coordinator.d.ts.map +1 -0
- package/dist/acpx-engine/run-coordinator.js +63 -0
- package/dist/acpx-engine/run-coordinator.js.map +1 -0
- package/dist/acpx-engine/run-coordinator.test.d.ts +2 -0
- package/dist/acpx-engine/run-coordinator.test.d.ts.map +1 -0
- package/dist/acpx-engine/run-coordinator.test.js +160 -0
- package/dist/acpx-engine/run-coordinator.test.js.map +1 -0
- package/dist/acpx-engine/run-fault-matrix.test.d.ts +2 -0
- package/dist/acpx-engine/run-fault-matrix.test.d.ts.map +1 -0
- package/dist/acpx-engine/run-fault-matrix.test.js +829 -0
- package/dist/acpx-engine/run-fault-matrix.test.js.map +1 -0
- package/dist/acpx-engine/run-resource-ledger.d.ts +30 -0
- package/dist/acpx-engine/run-resource-ledger.d.ts.map +1 -0
- package/dist/acpx-engine/run-resource-ledger.js +148 -0
- package/dist/acpx-engine/run-resource-ledger.js.map +1 -0
- package/dist/acpx-engine/run-resource-ledger.test.d.ts +2 -0
- package/dist/acpx-engine/run-resource-ledger.test.d.ts.map +1 -0
- package/dist/acpx-engine/run-resource-ledger.test.js +154 -0
- package/dist/acpx-engine/run-resource-ledger.test.js.map +1 -0
- package/dist/acpx-engine/run-site-host.d.ts +73 -0
- package/dist/acpx-engine/run-site-host.d.ts.map +1 -0
- package/dist/acpx-engine/run-site-host.js +65 -0
- package/dist/acpx-engine/run-site-host.js.map +1 -0
- package/dist/acpx-engine/run-site-host.test.d.ts +2 -0
- package/dist/acpx-engine/run-site-host.test.d.ts.map +1 -0
- package/dist/acpx-engine/run-site-host.test.js +98 -0
- package/dist/acpx-engine/run-site-host.test.js.map +1 -0
- package/dist/acpx-engine/run-site-sandbox.d.ts +142 -0
- package/dist/acpx-engine/run-site-sandbox.d.ts.map +1 -0
- package/dist/acpx-engine/run-site-sandbox.js +278 -0
- package/dist/acpx-engine/run-site-sandbox.js.map +1 -0
- package/dist/acpx-engine/run-site-sandbox.test.d.ts +2 -0
- package/dist/acpx-engine/run-site-sandbox.test.d.ts.map +1 -0
- package/dist/acpx-engine/run-site-sandbox.test.js +275 -0
- package/dist/acpx-engine/run-site-sandbox.test.js.map +1 -0
- package/dist/acpx-engine/session-codec.d.ts +3 -0
- package/dist/acpx-engine/session-codec.d.ts.map +1 -0
- package/dist/acpx-engine/session-codec.js +49 -0
- package/dist/acpx-engine/session-codec.js.map +1 -0
- package/dist/acpx-engine/session-reuse-store.d.ts +62 -0
- package/dist/acpx-engine/session-reuse-store.d.ts.map +1 -0
- package/dist/acpx-engine/session-reuse-store.js +134 -0
- package/dist/acpx-engine/session-reuse-store.js.map +1 -0
- package/dist/acpx-engine/session-reuse-store.test.d.ts +2 -0
- package/dist/acpx-engine/session-reuse-store.test.d.ts.map +1 -0
- package/dist/acpx-engine/session-reuse-store.test.js +134 -0
- package/dist/acpx-engine/session-reuse-store.test.js.map +1 -0
- package/dist/acpx-engine/settlement-characterization.test.d.ts +2 -0
- package/dist/acpx-engine/settlement-characterization.test.d.ts.map +1 -0
- package/dist/acpx-engine/settlement-characterization.test.js +880 -0
- package/dist/acpx-engine/settlement-characterization.test.js.map +1 -0
- package/dist/acpx-engine/settlement-sequence.d.ts +77 -0
- package/dist/acpx-engine/settlement-sequence.d.ts.map +1 -0
- package/dist/acpx-engine/settlement-sequence.js +119 -0
- package/dist/acpx-engine/settlement-sequence.js.map +1 -0
- package/dist/acpx-engine/settlement-sequence.test.d.ts +2 -0
- package/dist/acpx-engine/settlement-sequence.test.d.ts.map +1 -0
- package/dist/acpx-engine/settlement-sequence.test.js +272 -0
- package/dist/acpx-engine/settlement-sequence.test.js.map +1 -0
- package/dist/acpx-engine/spawn-smoke.test.d.ts +2 -0
- package/dist/acpx-engine/spawn-smoke.test.d.ts.map +1 -0
- package/dist/acpx-engine/spawn-smoke.test.js +184 -0
- package/dist/acpx-engine/spawn-smoke.test.js.map +1 -0
- package/dist/acpx-engine/startup-cancellation.d.ts +8 -0
- package/dist/acpx-engine/startup-cancellation.d.ts.map +1 -0
- package/dist/acpx-engine/startup-cancellation.js +92 -0
- package/dist/acpx-engine/startup-cancellation.js.map +1 -0
- package/dist/acpx-engine/startup-cancellation.test.d.ts +2 -0
- package/dist/acpx-engine/startup-cancellation.test.d.ts.map +1 -0
- package/dist/acpx-engine/startup-cancellation.test.js +115 -0
- package/dist/acpx-engine/startup-cancellation.test.js.map +1 -0
- package/dist/acpx-engine/startup-characterization.test.d.ts +2 -0
- package/dist/acpx-engine/startup-characterization.test.d.ts.map +1 -0
- package/dist/acpx-engine/startup-characterization.test.js +742 -0
- package/dist/acpx-engine/startup-characterization.test.js.map +1 -0
- package/dist/acpx-engine/startup-timing.d.ts +419 -0
- package/dist/acpx-engine/startup-timing.d.ts.map +1 -0
- package/dist/acpx-engine/startup-timing.js +616 -0
- package/dist/acpx-engine/startup-timing.js.map +1 -0
- package/dist/acpx-engine/startup-timing.test.d.ts +2 -0
- package/dist/acpx-engine/startup-timing.test.d.ts.map +1 -0
- package/dist/acpx-engine/startup-timing.test.js +619 -0
- package/dist/acpx-engine/startup-timing.test.js.map +1 -0
- package/dist/acpx-engine/turn-characterization.test.d.ts +2 -0
- package/dist/acpx-engine/turn-characterization.test.d.ts.map +1 -0
- package/dist/acpx-engine/turn-characterization.test.js +566 -0
- package/dist/acpx-engine/turn-characterization.test.js.map +1 -0
- package/dist/acpx-engine/turn-sequence.d.ts +54 -0
- package/dist/acpx-engine/turn-sequence.d.ts.map +1 -0
- package/dist/acpx-engine/turn-sequence.js +75 -0
- package/dist/acpx-engine/turn-sequence.js.map +1 -0
- package/dist/acpx-engine/turn-sequence.test.d.ts +2 -0
- package/dist/acpx-engine/turn-sequence.test.d.ts.map +1 -0
- package/dist/acpx-engine/turn-sequence.test.js +177 -0
- package/dist/acpx-engine/turn-sequence.test.js.map +1 -0
- package/dist/acpx-engine/ui.d.ts +3 -0
- package/dist/acpx-engine/ui.d.ts.map +1 -0
- package/dist/acpx-engine/ui.js +162 -0
- package/dist/acpx-engine/ui.js.map +1 -0
- package/dist/billing.d.ts +2 -0
- package/dist/billing.d.ts.map +1 -0
- package/dist/billing.js +16 -0
- package/dist/billing.js.map +1 -0
- package/dist/billing.test.d.ts +2 -0
- package/dist/billing.test.d.ts.map +1 -0
- package/dist/billing.test.js +14 -0
- package/dist/billing.test.js.map +1 -0
- package/dist/bridge-transport-contract.d.ts +43 -0
- package/dist/bridge-transport-contract.d.ts.map +1 -0
- package/dist/bridge-transport-contract.js +33 -0
- package/dist/bridge-transport-contract.js.map +1 -0
- package/dist/chat-file-delivery.d.ts +10 -0
- package/dist/chat-file-delivery.d.ts.map +1 -0
- package/dist/chat-file-delivery.js +38 -0
- package/dist/chat-file-delivery.js.map +1 -0
- package/dist/chat-file-delivery.test.d.ts +2 -0
- package/dist/chat-file-delivery.test.d.ts.map +1 -0
- package/dist/chat-file-delivery.test.js +92 -0
- package/dist/chat-file-delivery.test.js.map +1 -0
- package/dist/command-managed-runtime.d.ts +162 -0
- package/dist/command-managed-runtime.d.ts.map +1 -0
- package/dist/command-managed-runtime.js +456 -0
- package/dist/command-managed-runtime.js.map +1 -0
- package/dist/command-managed-runtime.test.d.ts +2 -0
- package/dist/command-managed-runtime.test.d.ts.map +1 -0
- package/dist/command-managed-runtime.test.js +908 -0
- package/dist/command-managed-runtime.test.js.map +1 -0
- package/dist/command-redaction.d.ts +15 -0
- package/dist/command-redaction.d.ts.map +1 -0
- package/dist/command-redaction.js +97 -0
- package/dist/command-redaction.js.map +1 -0
- package/dist/command-redaction.test.d.ts +2 -0
- package/dist/command-redaction.test.d.ts.map +1 -0
- package/dist/command-redaction.test.js +80 -0
- package/dist/command-redaction.test.js.map +1 -0
- package/dist/duplex-frame-codec.d.ts +124 -0
- package/dist/duplex-frame-codec.d.ts.map +1 -0
- package/dist/duplex-frame-codec.js +134 -0
- package/dist/duplex-frame-codec.js.map +1 -0
- package/dist/duplex-frame-codec.test.d.ts +2 -0
- package/dist/duplex-frame-codec.test.d.ts.map +1 -0
- package/dist/duplex-frame-codec.test.js +172 -0
- package/dist/duplex-frame-codec.test.js.map +1 -0
- package/dist/duplex-observability.d.ts +220 -0
- package/dist/duplex-observability.d.ts.map +1 -0
- package/dist/duplex-observability.js +248 -0
- package/dist/duplex-observability.js.map +1 -0
- package/dist/duplex-observability.test.d.ts +2 -0
- package/dist/duplex-observability.test.d.ts.map +1 -0
- package/dist/duplex-observability.test.js +55 -0
- package/dist/duplex-observability.test.js.map +1 -0
- package/dist/env-bindings.d.ts +17 -0
- package/dist/env-bindings.d.ts.map +1 -0
- package/dist/env-bindings.js +98 -0
- package/dist/env-bindings.js.map +1 -0
- package/dist/env-bindings.test.d.ts +2 -0
- package/dist/env-bindings.test.d.ts.map +1 -0
- package/dist/env-bindings.test.js +66 -0
- package/dist/env-bindings.test.js.map +1 -0
- package/dist/exclude-patterns.d.ts +4 -0
- package/dist/exclude-patterns.d.ts.map +1 -0
- package/dist/exclude-patterns.js +26 -0
- package/dist/exclude-patterns.js.map +1 -0
- package/dist/execution-target-sandbox.test.d.ts +2 -0
- package/dist/execution-target-sandbox.test.d.ts.map +1 -0
- package/dist/execution-target-sandbox.test.js +6522 -0
- package/dist/execution-target-sandbox.test.js.map +1 -0
- package/dist/execution-target-stdin-race.test.d.ts +2 -0
- package/dist/execution-target-stdin-race.test.d.ts.map +1 -0
- package/dist/execution-target-stdin-race.test.js +1842 -0
- package/dist/execution-target-stdin-race.test.js.map +1 -0
- package/dist/execution-target.d.ts +537 -0
- package/dist/execution-target.d.ts.map +1 -0
- package/dist/execution-target.js +3877 -0
- package/dist/execution-target.js.map +1 -0
- package/dist/execution-target.test.d.ts +2 -0
- package/dist/execution-target.test.d.ts.map +1 -0
- package/dist/execution-target.test.js +346 -0
- package/dist/execution-target.test.js.map +1 -0
- package/dist/git-workspace-sync.d.ts +193 -0
- package/dist/git-workspace-sync.d.ts.map +1 -0
- package/dist/git-workspace-sync.js +786 -0
- package/dist/git-workspace-sync.js.map +1 -0
- package/dist/git-workspace-sync.test.d.ts +2 -0
- package/dist/git-workspace-sync.test.d.ts.map +1 -0
- package/dist/git-workspace-sync.test.js +830 -0
- package/dist/git-workspace-sync.test.js.map +1 -0
- package/dist/github-launcher-environment.test.d.ts +2 -0
- package/dist/github-launcher-environment.test.d.ts.map +1 -0
- package/dist/github-launcher-environment.test.js +285 -0
- package/dist/github-launcher-environment.test.js.map +1 -0
- package/dist/github-launcher.d.ts +8 -0
- package/dist/github-launcher.d.ts.map +1 -0
- package/dist/github-launcher.js +127 -0
- package/dist/github-launcher.js.map +1 -0
- package/dist/github-launcher.test.d.ts +2 -0
- package/dist/github-launcher.test.d.ts.map +1 -0
- package/dist/github-launcher.test.js +166 -0
- package/dist/github-launcher.test.js.map +1 -0
- package/dist/http2-bridge-server.d.ts +465 -0
- package/dist/http2-bridge-server.d.ts.map +1 -0
- package/dist/http2-bridge-server.js +1087 -0
- package/dist/http2-bridge-server.js.map +1 -0
- package/dist/http2-bridge-server.test.d.ts +2 -0
- package/dist/http2-bridge-server.test.d.ts.map +1 -0
- package/dist/http2-bridge-server.test.js +2129 -0
- package/dist/http2-bridge-server.test.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/local-process-sandbox.d.ts +41 -0
- package/dist/local-process-sandbox.d.ts.map +1 -0
- package/dist/local-process-sandbox.js +432 -0
- package/dist/local-process-sandbox.js.map +1 -0
- package/dist/local-process-sandbox.test.d.ts +2 -0
- package/dist/local-process-sandbox.test.d.ts.map +1 -0
- package/dist/local-process-sandbox.test.js +399 -0
- package/dist/local-process-sandbox.test.js.map +1 -0
- package/dist/log-redaction.d.ts +9 -0
- package/dist/log-redaction.d.ts.map +1 -0
- package/dist/log-redaction.js +88 -0
- package/dist/log-redaction.js.map +1 -0
- package/dist/login-capability.d.ts +88 -0
- package/dist/login-capability.d.ts.map +1 -0
- package/dist/login-capability.js +78 -0
- package/dist/login-capability.js.map +1 -0
- package/dist/login-capability.test.d.ts +2 -0
- package/dist/login-capability.test.d.ts.map +1 -0
- package/dist/login-capability.test.js +78 -0
- package/dist/login-capability.test.js.map +1 -0
- package/dist/login-pty-transport.d.ts +80 -0
- package/dist/login-pty-transport.d.ts.map +1 -0
- package/dist/login-pty-transport.js +75 -0
- package/dist/login-pty-transport.js.map +1 -0
- package/dist/login-pty-transport.test.d.ts +2 -0
- package/dist/login-pty-transport.test.d.ts.map +1 -0
- package/dist/login-pty-transport.test.js +185 -0
- package/dist/login-pty-transport.test.js.map +1 -0
- package/dist/login-runner-lifecycle.d.ts +70 -0
- package/dist/login-runner-lifecycle.d.ts.map +1 -0
- package/dist/login-runner-lifecycle.js +52 -0
- package/dist/login-runner-lifecycle.js.map +1 -0
- package/dist/login-runner-lifecycle.test.d.ts +2 -0
- package/dist/login-runner-lifecycle.test.d.ts.map +1 -0
- package/dist/login-runner-lifecycle.test.js +122 -0
- package/dist/login-runner-lifecycle.test.js.map +1 -0
- package/dist/mcp-isolation.integration.test.d.ts +2 -0
- package/dist/mcp-isolation.integration.test.d.ts.map +1 -0
- package/dist/mcp-isolation.integration.test.js +202 -0
- package/dist/mcp-isolation.integration.test.js.map +1 -0
- package/dist/paperclip-runner-permissions.d.ts +110 -0
- package/dist/paperclip-runner-permissions.d.ts.map +1 -0
- package/dist/paperclip-runner-permissions.js +150 -0
- package/dist/paperclip-runner-permissions.js.map +1 -0
- package/dist/paperclip-runner-permissions.test.d.ts +2 -0
- package/dist/paperclip-runner-permissions.test.d.ts.map +1 -0
- package/dist/paperclip-runner-permissions.test.js +47 -0
- package/dist/paperclip-runner-permissions.test.js.map +1 -0
- package/dist/published-exports.test.d.ts +2 -0
- package/dist/published-exports.test.d.ts.map +1 -0
- package/dist/published-exports.test.js +17 -0
- package/dist/published-exports.test.js.map +1 -0
- package/dist/remote-execution-env.d.ts +2 -0
- package/dist/remote-execution-env.d.ts.map +1 -0
- package/dist/remote-execution-env.js +46 -0
- package/dist/remote-execution-env.js.map +1 -0
- package/dist/remote-managed-runtime.d.ts +45 -0
- package/dist/remote-managed-runtime.d.ts.map +1 -0
- package/dist/remote-managed-runtime.js +183 -0
- package/dist/remote-managed-runtime.js.map +1 -0
- package/dist/remote-managed-runtime.test.d.ts +2 -0
- package/dist/remote-managed-runtime.test.d.ts.map +1 -0
- package/dist/remote-managed-runtime.test.js +294 -0
- package/dist/remote-managed-runtime.test.js.map +1 -0
- package/dist/runner-connectivity.d.ts +61 -0
- package/dist/runner-connectivity.d.ts.map +1 -0
- package/dist/runner-connectivity.js +81 -0
- package/dist/runner-connectivity.js.map +1 -0
- package/dist/runner-connectivity.test.d.ts +2 -0
- package/dist/runner-connectivity.test.d.ts.map +1 -0
- package/dist/runner-connectivity.test.js +183 -0
- package/dist/runner-connectivity.test.js.map +1 -0
- package/dist/runtime-progress.d.ts +48 -0
- package/dist/runtime-progress.d.ts.map +1 -0
- package/dist/runtime-progress.js +99 -0
- package/dist/runtime-progress.js.map +1 -0
- package/dist/runtime-progress.test.d.ts +2 -0
- package/dist/runtime-progress.test.d.ts.map +1 -0
- package/dist/runtime-progress.test.js +240 -0
- package/dist/runtime-progress.test.js.map +1 -0
- package/dist/sandbox-callback-bridge-body.d.ts +12 -0
- package/dist/sandbox-callback-bridge-body.d.ts.map +1 -0
- package/dist/sandbox-callback-bridge-body.js +39 -0
- package/dist/sandbox-callback-bridge-body.js.map +1 -0
- package/dist/sandbox-callback-bridge-body.test.d.ts +2 -0
- package/dist/sandbox-callback-bridge-body.test.d.ts.map +1 -0
- package/dist/sandbox-callback-bridge-body.test.js +31 -0
- package/dist/sandbox-callback-bridge-body.test.js.map +1 -0
- package/dist/sandbox-callback-bridge.d.ts +268 -0
- package/dist/sandbox-callback-bridge.d.ts.map +1 -0
- package/dist/sandbox-callback-bridge.js +2357 -0
- package/dist/sandbox-callback-bridge.js.map +1 -0
- package/dist/sandbox-callback-bridge.test.d.ts +2 -0
- package/dist/sandbox-callback-bridge.test.d.ts.map +1 -0
- package/dist/sandbox-callback-bridge.test.js +3253 -0
- package/dist/sandbox-callback-bridge.test.js.map +1 -0
- package/dist/sandbox-file-sync.test.d.ts +2 -0
- package/dist/sandbox-file-sync.test.d.ts.map +1 -0
- package/dist/sandbox-file-sync.test.js +419 -0
- package/dist/sandbox-file-sync.test.js.map +1 -0
- package/dist/sandbox-install-command.d.ts +2 -0
- package/dist/sandbox-install-command.d.ts.map +1 -0
- package/dist/sandbox-install-command.js +47 -0
- package/dist/sandbox-install-command.js.map +1 -0
- package/dist/sandbox-install-command.test.d.ts +2 -0
- package/dist/sandbox-install-command.test.d.ts.map +1 -0
- package/dist/sandbox-install-command.test.js +22 -0
- package/dist/sandbox-install-command.test.js.map +1 -0
- package/dist/sandbox-managed-runtime.d.ts +417 -0
- package/dist/sandbox-managed-runtime.d.ts.map +1 -0
- package/dist/sandbox-managed-runtime.js +1337 -0
- package/dist/sandbox-managed-runtime.js.map +1 -0
- package/dist/sandbox-managed-runtime.test.d.ts +2 -0
- package/dist/sandbox-managed-runtime.test.d.ts.map +1 -0
- package/dist/sandbox-managed-runtime.test.js +3746 -0
- package/dist/sandbox-managed-runtime.test.js.map +1 -0
- package/dist/sandbox-run-log-stream.d.ts +44 -0
- package/dist/sandbox-run-log-stream.d.ts.map +1 -0
- package/dist/sandbox-run-log-stream.js +203 -0
- package/dist/sandbox-run-log-stream.js.map +1 -0
- package/dist/sandbox-shell.d.ts +3 -0
- package/dist/sandbox-shell.d.ts.map +1 -0
- package/dist/sandbox-shell.js +7 -0
- package/dist/sandbox-shell.js.map +1 -0
- package/dist/server-utils-env.test.d.ts +2 -0
- package/dist/server-utils-env.test.d.ts.map +1 -0
- package/dist/server-utils-env.test.js +15 -0
- package/dist/server-utils-env.test.js.map +1 -0
- package/dist/server-utils.d.ts +617 -0
- package/dist/server-utils.d.ts.map +1 -0
- package/dist/server-utils.js +3258 -0
- package/dist/server-utils.js.map +1 -0
- package/dist/server-utils.test.d.ts +2 -0
- package/dist/server-utils.test.d.ts.map +1 -0
- package/dist/server-utils.test.js +3047 -0
- package/dist/server-utils.test.js.map +1 -0
- package/dist/session-compaction.d.ts +25 -0
- package/dist/session-compaction.d.ts.map +1 -0
- package/dist/session-compaction.js +165 -0
- package/dist/session-compaction.js.map +1 -0
- package/dist/skill-library-manifest.d.ts +6 -0
- package/dist/skill-library-manifest.d.ts.map +1 -0
- package/dist/skill-library-manifest.js +53 -0
- package/dist/skill-library-manifest.js.map +1 -0
- package/dist/skill-library-manifest.test.d.ts +2 -0
- package/dist/skill-library-manifest.test.d.ts.map +1 -0
- package/dist/skill-library-manifest.test.js +84 -0
- package/dist/skill-library-manifest.test.js.map +1 -0
- package/dist/skills-staging-follow-symlinks.test.d.ts +2 -0
- package/dist/skills-staging-follow-symlinks.test.d.ts.map +1 -0
- package/dist/skills-staging-follow-symlinks.test.js +126 -0
- package/dist/skills-staging-follow-symlinks.test.js.map +1 -0
- package/dist/ssh-fixture.test.d.ts +2 -0
- package/dist/ssh-fixture.test.d.ts.map +1 -0
- package/dist/ssh-fixture.test.js +832 -0
- package/dist/ssh-fixture.test.js.map +1 -0
- package/dist/ssh.d.ts +124 -0
- package/dist/ssh.d.ts.map +1 -0
- package/dist/ssh.js +1667 -0
- package/dist/ssh.js.map +1 -0
- package/dist/sync-operation-schedule.d.ts +17 -0
- package/dist/sync-operation-schedule.d.ts.map +1 -0
- package/dist/sync-operation-schedule.js +66 -0
- package/dist/sync-operation-schedule.js.map +1 -0
- package/dist/sync-operation-schedule.test.d.ts +2 -0
- package/dist/sync-operation-schedule.test.d.ts.map +1 -0
- package/dist/sync-operation-schedule.test.js +130 -0
- package/dist/sync-operation-schedule.test.js.map +1 -0
- package/dist/test-support/mcp-isolation-harness.d.ts +23 -0
- package/dist/test-support/mcp-isolation-harness.d.ts.map +1 -0
- package/dist/test-support/mcp-isolation-harness.js +67 -0
- package/dist/test-support/mcp-isolation-harness.js.map +1 -0
- package/dist/types.d.ts +780 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace-restore-merge.d.ts +77 -0
- package/dist/workspace-restore-merge.d.ts.map +1 -0
- package/dist/workspace-restore-merge.js +396 -0
- package/dist/workspace-restore-merge.js.map +1 -0
- package/dist/workspace-restore-merge.test.d.ts +2 -0
- package/dist/workspace-restore-merge.test.d.ts.map +1 -0
- package/dist/workspace-restore-merge.test.js +438 -0
- package/dist/workspace-restore-merge.test.js.map +1 -0
- package/dist/workspace-restore-teardown.d.ts +29 -0
- package/dist/workspace-restore-teardown.d.ts.map +1 -0
- package/dist/workspace-restore-teardown.js +34 -0
- package/dist/workspace-restore-teardown.js.map +1 -0
- package/dist/workspace-restore-teardown.test.d.ts +2 -0
- package/dist/workspace-restore-teardown.test.d.ts.map +1 -0
- package/dist/workspace-restore-teardown.test.js +71 -0
- package/dist/workspace-restore-teardown.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/LICENSE +191 -0
- package/node_modules/@agentclientprotocol/sdk/README.md +70 -0
- package/node_modules/@agentclientprotocol/sdk/dist/acp.d.ts +1803 -0
- package/node_modules/@agentclientprotocol/sdk/dist/acp.js +1869 -0
- package/node_modules/@agentclientprotocol/sdk/dist/acp.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/acp.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/acp.test.js +5036 -0
- package/node_modules/@agentclientprotocol/sdk/dist/acp.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/connection.d.ts +89 -0
- package/node_modules/@agentclientprotocol/sdk/dist/connection.js +490 -0
- package/node_modules/@agentclientprotocol/sdk/dist/connection.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/connection.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/connection.test.js +507 -0
- package/node_modules/@agentclientprotocol/sdk/dist/connection.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/cookie-store.d.ts +26 -0
- package/node_modules/@agentclientprotocol/sdk/dist/cookie-store.js +96 -0
- package/node_modules/@agentclientprotocol/sdk/dist/cookie-store.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/cookie-store.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/cookie-store.test.js +77 -0
- package/node_modules/@agentclientprotocol/sdk/dist/cookie-store.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/agent.d.ts +2 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/agent.js +232 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/agent.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/client.d.ts +2 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/client.js +132 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/client.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/dual-version-agent.d.ts +9 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/dual-version-agent.js +221 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/dual-version-agent.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/http-client.d.ts +2 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/http-client.js +86 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/http-client.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/http-server.d.ts +2 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/http-server.js +112 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/http-server.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/ws-client.d.ts +2 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/ws-client.js +88 -0
- package/node_modules/@agentclientprotocol/sdk/dist/examples/ws-client.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/http-stream.d.ts +33 -0
- package/node_modules/@agentclientprotocol/sdk/dist/http-stream.js +435 -0
- package/node_modules/@agentclientprotocol/sdk/dist/http-stream.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/http-stream.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/http-stream.test.js +1380 -0
- package/node_modules/@agentclientprotocol/sdk/dist/http-stream.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.d.ts +603 -0
- package/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.js +1076 -0
- package/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.test.js +917 -0
- package/node_modules/@agentclientprotocol/sdk/dist/jsonrpc.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.d.ts +22 -0
- package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.js +64 -0
- package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.test.js +45 -0
- package/node_modules/@agentclientprotocol/sdk/dist/line-buffer.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.d.ts +17 -0
- package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.js +426 -0
- package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.test.js +499 -0
- package/node_modules/@agentclientprotocol/sdk/dist/node-adapter.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/protocol-router.d.ts +37 -0
- package/node_modules/@agentclientprotocol/sdk/dist/protocol-router.js +474 -0
- package/node_modules/@agentclientprotocol/sdk/dist/protocol-router.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/protocol-router.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/protocol-router.test.js +535 -0
- package/node_modules/@agentclientprotocol/sdk/dist/protocol-router.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/protocol.d.ts +11 -0
- package/node_modules/@agentclientprotocol/sdk/dist/protocol.js +69 -0
- package/node_modules/@agentclientprotocol/sdk/dist/protocol.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/protocol.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/protocol.test.js +82 -0
- package/node_modules/@agentclientprotocol/sdk/dist/protocol.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema/guards.gen.d.ts +183 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema/guards.gen.js +207 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema/guards.gen.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema/index.d.ts +51 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema/index.js +52 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema/index.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.d.ts +6219 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.js +3 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.d.ts +15215 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.js +3084 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema/zod.gen.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema-deserialize.d.ts +30 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema-deserialize.js +98 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema-deserialize.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema-deserialize.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema-deserialize.test.js +93 -0
- package/node_modules/@agentclientprotocol/sdk/dist/schema-deserialize.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server-permission.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server-permission.test.js +261 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server-permission.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server-session-sse.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server-session-sse.test.js +536 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server-session-sse.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server-sse.d.ts +2 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server-sse.js +88 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server-sse.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server-sse.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server-sse.test.js +53 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server-sse.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server-websocket-upgrade.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server-websocket-upgrade.test.js +992 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server-websocket-upgrade.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server.d.ts +81 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server.js +386 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server.test.js +1116 -0
- package/node_modules/@agentclientprotocol/sdk/dist/server.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/sse.d.ts +4 -0
- package/node_modules/@agentclientprotocol/sdk/dist/sse.js +91 -0
- package/node_modules/@agentclientprotocol/sdk/dist/sse.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/sse.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/sse.test.js +126 -0
- package/node_modules/@agentclientprotocol/sdk/dist/sse.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/stream.d.ts +35 -0
- package/node_modules/@agentclientprotocol/sdk/dist/stream.js +115 -0
- package/node_modules/@agentclientprotocol/sdk/dist/stream.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/stream.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/stream.test.js +195 -0
- package/node_modules/@agentclientprotocol/sdk/dist/stream.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/test-support/streams.d.ts +8 -0
- package/node_modules/@agentclientprotocol/sdk/dist/test-support/streams.js +51 -0
- package/node_modules/@agentclientprotocol/sdk/dist/test-support/streams.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/test-support/test-agent.d.ts +24 -0
- package/node_modules/@agentclientprotocol/sdk/dist/test-support/test-agent.js +163 -0
- package/node_modules/@agentclientprotocol/sdk/dist/test-support/test-agent.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/test-support/test-http-server.d.ts +9 -0
- package/node_modules/@agentclientprotocol/sdk/dist/test-support/test-http-server.js +74 -0
- package/node_modules/@agentclientprotocol/sdk/dist/test-support/test-http-server.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.d.ts +957 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.js +1484 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.test.js +1287 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/acp.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/guards.gen.d.ts +946 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/guards.gen.js +1046 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/guards.gen.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/index.d.ts +42 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/index.js +43 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/index.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/types.gen.d.ts +6208 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/types.gen.js +3 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/types.gen.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/zod.gen.d.ts +33267 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/zod.gen.js +3199 -0
- package/node_modules/@agentclientprotocol/sdk/dist/v2/schema/zod.gen.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/ws-server.d.ts +14 -0
- package/node_modules/@agentclientprotocol/sdk/dist/ws-server.js +357 -0
- package/node_modules/@agentclientprotocol/sdk/dist/ws-server.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/ws-stream.d.ts +54 -0
- package/node_modules/@agentclientprotocol/sdk/dist/ws-stream.js +253 -0
- package/node_modules/@agentclientprotocol/sdk/dist/ws-stream.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/ws-stream.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/ws-stream.test.js +836 -0
- package/node_modules/@agentclientprotocol/sdk/dist/ws-stream.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/ws-utils.d.ts +13 -0
- package/node_modules/@agentclientprotocol/sdk/dist/ws-utils.js +78 -0
- package/node_modules/@agentclientprotocol/sdk/dist/ws-utils.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/ws-utils.test.d.ts +1 -0
- package/node_modules/@agentclientprotocol/sdk/dist/ws-utils.test.js +42 -0
- package/node_modules/@agentclientprotocol/sdk/dist/ws-utils.test.js.map +1 -0
- package/node_modules/@agentclientprotocol/sdk/package.json +104 -0
- package/node_modules/@agentclientprotocol/sdk/schema/schema.json +8967 -0
- package/node_modules/@agentclientprotocol/sdk/schema/v2/schema.unstable.json +10242 -0
- package/node_modules/@clack/core/LICENSE +9 -0
- package/node_modules/@clack/core/README.md +22 -0
- package/node_modules/@clack/core/dist/index.d.mts +503 -0
- package/node_modules/@clack/core/dist/index.mjs +1003 -0
- package/node_modules/@clack/core/package.json +63 -0
- package/node_modules/@clack/prompts/LICENSE +9 -0
- package/node_modules/@clack/prompts/README.md +396 -0
- package/node_modules/@clack/prompts/dist/index.d.mts +949 -0
- package/node_modules/@clack/prompts/dist/index.mjs +1423 -0
- package/node_modules/@clack/prompts/package.json +67 -0
- package/node_modules/@esbuild/linux-x64/README.md +3 -0
- package/node_modules/@esbuild/linux-x64/bin/esbuild +0 -0
- package/node_modules/@esbuild/linux-x64/package.json +20 -0
- package/node_modules/acpx/LICENSE +21 -0
- package/node_modules/acpx/README.md +424 -0
- package/node_modules/acpx/dist/cli-CxsS1XQp.js +197 -0
- package/node_modules/acpx/dist/cli-CxsS1XQp.js.map +1 -0
- package/node_modules/acpx/dist/cli.d.ts +14 -0
- package/node_modules/acpx/dist/cli.d.ts.map +1 -0
- package/node_modules/acpx/dist/cli.js +2683 -0
- package/node_modules/acpx/dist/cli.js.map +1 -0
- package/node_modules/acpx/dist/client-DwNyFauq.d.ts +192 -0
- package/node_modules/acpx/dist/client-DwNyFauq.d.ts.map +1 -0
- package/node_modules/acpx/dist/flags-Bjq_XQ4-.js +260 -0
- package/node_modules/acpx/dist/flags-Bjq_XQ4-.js.map +1 -0
- package/node_modules/acpx/dist/flows-Db_LZKu9.js +1920 -0
- package/node_modules/acpx/dist/flows-Db_LZKu9.js.map +1 -0
- package/node_modules/acpx/dist/flows.d.ts +318 -0
- package/node_modules/acpx/dist/flows.d.ts.map +1 -0
- package/node_modules/acpx/dist/flows.js +2 -0
- package/node_modules/acpx/dist/live-checkpoint-ClPCSdrW.js +6210 -0
- package/node_modules/acpx/dist/live-checkpoint-ClPCSdrW.js.map +1 -0
- package/node_modules/acpx/dist/output-CjdF5rHk.js +4593 -0
- package/node_modules/acpx/dist/output-CjdF5rHk.js.map +1 -0
- package/node_modules/acpx/dist/runtime.d.ts +472 -0
- package/node_modules/acpx/dist/runtime.d.ts.map +1 -0
- package/node_modules/acpx/dist/runtime.js +1711 -0
- package/node_modules/acpx/dist/runtime.js.map +1 -0
- package/node_modules/acpx/dist/session-options-jkYbBxGE.d.ts +278 -0
- package/node_modules/acpx/dist/session-options-jkYbBxGE.d.ts.map +1 -0
- package/node_modules/acpx/package.json +129 -0
- package/node_modules/acpx/skills/acpx/SKILL.md +617 -0
- package/node_modules/b4a/LICENSE +201 -0
- package/node_modules/b4a/README.md +153 -0
- package/node_modules/b4a/browser.js +546 -0
- package/node_modules/b4a/index.d.ts +144 -0
- package/node_modules/b4a/index.js +193 -0
- package/node_modules/b4a/lib/ascii.js +31 -0
- package/node_modules/b4a/lib/base64.js +64 -0
- package/node_modules/b4a/lib/hex.js +51 -0
- package/node_modules/b4a/lib/latin1.js +31 -0
- package/node_modules/b4a/lib/utf16le.js +40 -0
- package/node_modules/b4a/lib/utf8.js +144 -0
- package/node_modules/b4a/package.json +54 -0
- package/node_modules/b4a/react-native.js +5 -0
- package/node_modules/b4a/runtime.d.ts +14 -0
- package/node_modules/bare-events/LICENSE +201 -0
- package/node_modules/bare-events/README.md +25 -0
- package/node_modules/bare-events/global.d.ts +15 -0
- package/node_modules/bare-events/global.js +5 -0
- package/node_modules/bare-events/index.d.ts +89 -0
- package/node_modules/bare-events/index.js +397 -0
- package/node_modules/bare-events/lib/errors.js +23 -0
- package/node_modules/bare-events/package.json +63 -0
- package/node_modules/bare-events/web.d.ts +79 -0
- package/node_modules/bare-events/web.js +417 -0
- package/node_modules/bare-fs/CMakeLists.txt +13 -0
- package/node_modules/bare-fs/LICENSE +201 -0
- package/node_modules/bare-fs/README.md +33 -0
- package/node_modules/bare-fs/binding.c +2913 -0
- package/node_modules/bare-fs/binding.js +1 -0
- package/node_modules/bare-fs/index.d.ts +1502 -0
- package/node_modules/bare-fs/index.js +2855 -0
- package/node_modules/bare-fs/lib/constants.d.ts +50 -0
- package/node_modules/bare-fs/lib/constants.js +49 -0
- package/node_modules/bare-fs/lib/errors.d.ts +13 -0
- package/node_modules/bare-fs/lib/errors.js +55 -0
- package/node_modules/bare-fs/package.json +74 -0
- package/node_modules/bare-fs/prebuilds/android-arm/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/android-arm64/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/android-ia32/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/android-x64/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/darwin-arm64/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/darwin-x64/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/ios-arm64/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/ios-arm64-simulator/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/ios-x64-simulator/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/linux-arm64/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/linux-x64/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/win32-arm64/bare-fs.bare +0 -0
- package/node_modules/bare-fs/prebuilds/win32-x64/bare-fs.bare +0 -0
- package/node_modules/bare-fs/promises.d.ts +480 -0
- package/node_modules/bare-fs/promises.js +119 -0
- package/node_modules/bare-path/CMakeLists.txt +13 -0
- package/node_modules/bare-path/LICENSE +201 -0
- package/node_modules/bare-path/NOTICE +28 -0
- package/node_modules/bare-path/README.md +19 -0
- package/node_modules/bare-path/binding.c +106 -0
- package/node_modules/bare-path/binding.js +1 -0
- package/node_modules/bare-path/index.d.ts +27 -0
- package/node_modules/bare-path/index.js +9 -0
- package/node_modules/bare-path/lib/constants.js +11 -0
- package/node_modules/bare-path/lib/posix.js +263 -0
- package/node_modules/bare-path/lib/shared.js +81 -0
- package/node_modules/bare-path/lib/win32.js +564 -0
- package/node_modules/bare-path/package.json +47 -0
- package/node_modules/bare-path/prebuilds/android-arm/bare-path.bare +0 -0
- package/node_modules/bare-path/prebuilds/android-arm64/bare-path.bare +0 -0
- package/node_modules/bare-path/prebuilds/android-ia32/bare-path.bare +0 -0
- package/node_modules/bare-path/prebuilds/android-x64/bare-path.bare +0 -0
- package/node_modules/bare-path/prebuilds/darwin-arm64/bare-path.bare +0 -0
- package/node_modules/bare-path/prebuilds/darwin-x64/bare-path.bare +0 -0
- package/node_modules/bare-path/prebuilds/ios-arm64/bare-path.bare +0 -0
- package/node_modules/bare-path/prebuilds/ios-arm64-simulator/bare-path.bare +0 -0
- package/node_modules/bare-path/prebuilds/ios-x64-simulator/bare-path.bare +0 -0
- package/node_modules/bare-path/prebuilds/linux-arm64/bare-path.bare +0 -0
- package/node_modules/bare-path/prebuilds/linux-x64/bare-path.bare +0 -0
- package/node_modules/bare-path/prebuilds/win32-arm64/bare-path.bare +0 -0
- package/node_modules/bare-path/prebuilds/win32-x64/bare-path.bare +0 -0
- package/node_modules/bare-stream/LICENSE +201 -0
- package/node_modules/bare-stream/README.md +354 -0
- package/node_modules/bare-stream/global.d.ts +31 -0
- package/node_modules/bare-stream/global.js +15 -0
- package/node_modules/bare-stream/index.d.ts +235 -0
- package/node_modules/bare-stream/index.js +493 -0
- package/node_modules/bare-stream/package.json +75 -0
- package/node_modules/bare-stream/promises.js +3 -0
- package/node_modules/bare-stream/web.d.ts +152 -0
- package/node_modules/bare-stream/web.js +607 -0
- package/node_modules/bare-url/CMakeLists.txt +36 -0
- package/node_modules/bare-url/LICENSE +201 -0
- package/node_modules/bare-url/README.md +33 -0
- package/node_modules/bare-url/binding.c +422 -0
- package/node_modules/bare-url/binding.js +1 -0
- package/node_modules/bare-url/global.d.ts +24 -0
- package/node_modules/bare-url/global.js +2 -0
- package/node_modules/bare-url/index.d.ts +96 -0
- package/node_modules/bare-url/index.js +676 -0
- package/node_modules/bare-url/lib/errors.d.ts +10 -0
- package/node_modules/bare-url/lib/errors.js +33 -0
- package/node_modules/bare-url/lib/url-search-params.d.ts +66 -0
- package/node_modules/bare-url/lib/url-search-params.js +276 -0
- package/node_modules/bare-url/package.json +54 -0
- package/node_modules/bare-url/prebuilds/android-arm/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/android-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/android-ia32/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/android-x64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/darwin-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/darwin-x64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/ios-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/ios-arm64-simulator/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/ios-x64-simulator/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/linux-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/linux-x64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/win32-arm64/bare-url.bare +0 -0
- package/node_modules/bare-url/prebuilds/win32-x64/bare-url.bare +0 -0
- package/node_modules/commander/LICENSE +22 -0
- package/node_modules/commander/Readme.md +1172 -0
- package/node_modules/commander/index.js +21 -0
- package/node_modules/commander/lib/argument.js +147 -0
- package/node_modules/commander/lib/command.js +2790 -0
- package/node_modules/commander/lib/error.js +36 -0
- package/node_modules/commander/lib/help.js +731 -0
- package/node_modules/commander/lib/option.js +377 -0
- package/node_modules/commander/lib/suggestSimilar.js +99 -0
- package/node_modules/commander/package-support.json +19 -0
- package/node_modules/commander/package.json +64 -0
- package/node_modules/commander/typings/index.d.ts +1113 -0
- package/node_modules/esbuild/LICENSE.md +21 -0
- package/node_modules/esbuild/README.md +3 -0
- package/node_modules/esbuild/bin/esbuild +223 -0
- package/node_modules/esbuild/install.js +300 -0
- package/node_modules/esbuild/lib/main.d.ts +720 -0
- package/node_modules/esbuild/lib/main.js +2536 -0
- package/node_modules/esbuild/package.json +74 -0
- package/node_modules/events-universal/LICENSE +201 -0
- package/node_modules/events-universal/README.md +17 -0
- package/node_modules/events-universal/bare.js +1 -0
- package/node_modules/events-universal/default.js +1 -0
- package/node_modules/events-universal/index.js +1 -0
- package/node_modules/events-universal/package.json +39 -0
- package/node_modules/events-universal/react-native.js +1 -0
- package/node_modules/fast-fifo/LICENSE +21 -0
- package/node_modules/fast-fifo/README.md +78 -0
- package/node_modules/fast-fifo/fixed-size.js +39 -0
- package/node_modules/fast-fifo/index.js +48 -0
- package/node_modules/fast-fifo/package.json +28 -0
- package/node_modules/fast-string-truncated-width/dist/index.d.ts +4 -0
- package/node_modules/fast-string-truncated-width/dist/index.js +111 -0
- package/node_modules/fast-string-truncated-width/dist/types.d.ts +19 -0
- package/node_modules/fast-string-truncated-width/dist/types.js +2 -0
- package/node_modules/fast-string-truncated-width/dist/utils.d.ts +4 -0
- package/node_modules/fast-string-truncated-width/dist/utils.js +20 -0
- package/node_modules/fast-string-truncated-width/license +21 -0
- package/node_modules/fast-string-truncated-width/package.json +35 -0
- package/node_modules/fast-string-truncated-width/readme.md +59 -0
- package/node_modules/fast-string-width/dist/index.d.ts +4 -0
- package/node_modules/fast-string-width/dist/index.js +14 -0
- package/node_modules/fast-string-width/license +21 -0
- package/node_modules/fast-string-width/package.json +34 -0
- package/node_modules/fast-string-width/readme.md +42 -0
- package/node_modules/fast-wrap-ansi/LICENSE +23 -0
- package/node_modules/fast-wrap-ansi/README.md +26 -0
- package/node_modules/fast-wrap-ansi/lib/main.d.ts +6 -0
- package/node_modules/fast-wrap-ansi/lib/main.js +218 -0
- package/node_modules/fast-wrap-ansi/package.json +51 -0
- package/node_modules/sisteransi/license +21 -0
- package/node_modules/sisteransi/package.json +34 -0
- package/node_modules/sisteransi/readme.md +113 -0
- package/node_modules/sisteransi/src/index.js +58 -0
- package/node_modules/sisteransi/src/sisteransi.d.ts +35 -0
- package/node_modules/skillflag/LICENSE +21 -0
- package/node_modules/skillflag/README.md +44 -0
- package/node_modules/skillflag/dist/bin/skill-install.d.ts +3 -0
- package/node_modules/skillflag/dist/bin/skill-install.d.ts.map +1 -0
- package/node_modules/skillflag/dist/bin/skill-install.js +6 -0
- package/node_modules/skillflag/dist/bin/skill-install.js.map +1 -0
- package/node_modules/skillflag/dist/bin/skillflag.d.ts +3 -0
- package/node_modules/skillflag/dist/bin/skillflag.d.ts.map +1 -0
- package/node_modules/skillflag/dist/bin/skillflag.js +25 -0
- package/node_modules/skillflag/dist/bin/skillflag.js.map +1 -0
- package/node_modules/skillflag/dist/core/digest.d.ts +2 -0
- package/node_modules/skillflag/dist/core/digest.d.ts.map +1 -0
- package/node_modules/skillflag/dist/core/digest.js +15 -0
- package/node_modules/skillflag/dist/core/digest.js.map +1 -0
- package/node_modules/skillflag/dist/core/errors.d.ts +6 -0
- package/node_modules/skillflag/dist/core/errors.d.ts.map +1 -0
- package/node_modules/skillflag/dist/core/errors.js +13 -0
- package/node_modules/skillflag/dist/core/errors.js.map +1 -0
- package/node_modules/skillflag/dist/core/export.d.ts +2 -0
- package/node_modules/skillflag/dist/core/export.d.ts.map +1 -0
- package/node_modules/skillflag/dist/core/export.js +28 -0
- package/node_modules/skillflag/dist/core/export.js.map +1 -0
- package/node_modules/skillflag/dist/core/list.d.ts +21 -0
- package/node_modules/skillflag/dist/core/list.d.ts.map +1 -0
- package/node_modules/skillflag/dist/core/list.js +71 -0
- package/node_modules/skillflag/dist/core/list.js.map +1 -0
- package/node_modules/skillflag/dist/core/paths.d.ts +15 -0
- package/node_modules/skillflag/dist/core/paths.d.ts.map +1 -0
- package/node_modules/skillflag/dist/core/paths.js +149 -0
- package/node_modules/skillflag/dist/core/paths.js.map +1 -0
- package/node_modules/skillflag/dist/core/show.d.ts +2 -0
- package/node_modules/skillflag/dist/core/show.d.ts.map +1 -0
- package/node_modules/skillflag/dist/core/show.js +8 -0
- package/node_modules/skillflag/dist/core/show.js.map +1 -0
- package/node_modules/skillflag/dist/core/skillflag.d.ts +3 -0
- package/node_modules/skillflag/dist/core/skillflag.d.ts.map +1 -0
- package/node_modules/skillflag/dist/core/skillflag.js +2 -0
- package/node_modules/skillflag/dist/core/skillflag.js.map +1 -0
- package/node_modules/skillflag/dist/core/tar.d.ts +16 -0
- package/node_modules/skillflag/dist/core/tar.d.ts.map +1 -0
- package/node_modules/skillflag/dist/core/tar.js +140 -0
- package/node_modules/skillflag/dist/core/tar.js.map +1 -0
- package/node_modules/skillflag/dist/index.d.ts +5 -0
- package/node_modules/skillflag/dist/index.d.ts.map +1 -0
- package/node_modules/skillflag/dist/index.js +3 -0
- package/node_modules/skillflag/dist/index.js.map +1 -0
- package/node_modules/skillflag/dist/install/cli.d.ts +57 -0
- package/node_modules/skillflag/dist/install/cli.d.ts.map +1 -0
- package/node_modules/skillflag/dist/install/cli.js +653 -0
- package/node_modules/skillflag/dist/install/cli.js.map +1 -0
- package/node_modules/skillflag/dist/install/copy.d.ts +2 -0
- package/node_modules/skillflag/dist/install/copy.d.ts.map +1 -0
- package/node_modules/skillflag/dist/install/copy.js +19 -0
- package/node_modules/skillflag/dist/install/copy.js.map +1 -0
- package/node_modules/skillflag/dist/install/errors.d.ts +6 -0
- package/node_modules/skillflag/dist/install/errors.d.ts.map +1 -0
- package/node_modules/skillflag/dist/install/errors.js +13 -0
- package/node_modules/skillflag/dist/install/errors.js.map +1 -0
- package/node_modules/skillflag/dist/install/extract.d.ts +3 -0
- package/node_modules/skillflag/dist/install/extract.d.ts.map +1 -0
- package/node_modules/skillflag/dist/install/extract.js +76 -0
- package/node_modules/skillflag/dist/install/extract.js.map +1 -0
- package/node_modules/skillflag/dist/install/install.d.ts +21 -0
- package/node_modules/skillflag/dist/install/install.d.ts.map +1 -0
- package/node_modules/skillflag/dist/install/install.js +35 -0
- package/node_modules/skillflag/dist/install/install.js.map +1 -0
- package/node_modules/skillflag/dist/install/resolve.d.ts +12 -0
- package/node_modules/skillflag/dist/install/resolve.d.ts.map +1 -0
- package/node_modules/skillflag/dist/install/resolve.js +119 -0
- package/node_modules/skillflag/dist/install/resolve.js.map +1 -0
- package/node_modules/skillflag/dist/install/validate.d.ts +7 -0
- package/node_modules/skillflag/dist/install/validate.d.ts.map +1 -0
- package/node_modules/skillflag/dist/install/validate.js +28 -0
- package/node_modules/skillflag/dist/install/validate.js.map +1 -0
- package/node_modules/skillflag/dist/shared/frontmatter.d.ts +2 -0
- package/node_modules/skillflag/dist/shared/frontmatter.d.ts.map +1 -0
- package/node_modules/skillflag/dist/shared/frontmatter.js +28 -0
- package/node_modules/skillflag/dist/shared/frontmatter.js.map +1 -0
- package/node_modules/skillflag/dist/skillflag.d.ts +29 -0
- package/node_modules/skillflag/dist/skillflag.d.ts.map +1 -0
- package/node_modules/skillflag/dist/skillflag.js +230 -0
- package/node_modules/skillflag/dist/skillflag.js.map +1 -0
- package/node_modules/skillflag/dist/utils/collections.d.ts +3 -0
- package/node_modules/skillflag/dist/utils/collections.d.ts.map +1 -0
- package/node_modules/skillflag/dist/utils/collections.js +18 -0
- package/node_modules/skillflag/dist/utils/collections.js.map +1 -0
- package/node_modules/skillflag/package.json +78 -0
- package/node_modules/skillflag/skills/skillflag/SKILL.md +144 -0
- package/node_modules/streamx/LICENSE +21 -0
- package/node_modules/streamx/README.md +550 -0
- package/node_modules/streamx/index.d.ts +198 -0
- package/node_modules/streamx/index.js +1314 -0
- package/node_modules/streamx/lib/errors.d.ts +13 -0
- package/node_modules/streamx/lib/errors.js +47 -0
- package/node_modules/streamx/package.json +51 -0
- package/node_modules/streamx/runtime.d.ts +55 -0
- package/node_modules/tar-stream/LICENSE +21 -0
- package/node_modules/tar-stream/README.md +183 -0
- package/node_modules/tar-stream/constants.js +14 -0
- package/node_modules/tar-stream/extract.js +418 -0
- package/node_modules/tar-stream/headers.js +322 -0
- package/node_modules/tar-stream/index.d.ts +60 -0
- package/node_modules/tar-stream/index.js +2 -0
- package/node_modules/tar-stream/pack.js +287 -0
- package/node_modules/tar-stream/package.json +50 -0
- package/node_modules/teex/LICENSE +21 -0
- package/node_modules/teex/README.md +31 -0
- package/node_modules/teex/example.js +16 -0
- package/node_modules/teex/index.js +52 -0
- package/node_modules/teex/package.json +26 -0
- package/node_modules/teex/test.js +68 -0
- package/node_modules/text-decoder/LICENSE +201 -0
- package/node_modules/text-decoder/README.md +36 -0
- package/node_modules/text-decoder/index.js +64 -0
- package/node_modules/text-decoder/lib/pass-through-decoder.js +19 -0
- package/node_modules/text-decoder/lib/utf8-decoder.js +185 -0
- package/node_modules/text-decoder/package.json +39 -0
- package/node_modules/tsx/LICENSE +21 -0
- package/node_modules/tsx/README.md +32 -0
- package/node_modules/tsx/dist/cjs/api/index.cjs +2 -0
- package/node_modules/tsx/dist/cjs/api/index.d.cts +35 -0
- package/node_modules/tsx/dist/cjs/api/index.d.mts +35 -0
- package/node_modules/tsx/dist/cjs/api/index.mjs +2 -0
- package/node_modules/tsx/dist/cjs/index.cjs +1 -0
- package/node_modules/tsx/dist/cli.mjs +55 -0
- package/node_modules/tsx/dist/client-XItNFmsq.cjs +1 -0
- package/node_modules/tsx/dist/esm/api/index.cjs +2 -0
- package/node_modules/tsx/dist/esm/api/index.d.cts +37 -0
- package/node_modules/tsx/dist/esm/api/index.d.mts +37 -0
- package/node_modules/tsx/dist/esm/api/index.mjs +2 -0
- package/node_modules/tsx/dist/esm/index.mjs +2 -0
- package/node_modules/tsx/dist/index-Bqjv9TxC.mjs +1 -0
- package/node_modules/tsx/dist/index-DE3OBZuV.mjs +14 -0
- package/node_modules/tsx/dist/lexer-CGfpMDSb.cjs +3 -0
- package/node_modules/tsx/dist/lexer-CanA6ArK.mjs +3 -0
- package/node_modules/tsx/dist/loader.mjs +2 -0
- package/node_modules/tsx/dist/package-Dj0mHsMt.mjs +1 -0
- package/node_modules/tsx/dist/patch-repl.cjs +1 -0
- package/node_modules/tsx/dist/preflight.cjs +1 -0
- package/node_modules/tsx/dist/register-B1c7OH6V.cjs +23 -0
- package/node_modules/tsx/dist/register-nyXW-TH3.mjs +2 -0
- package/node_modules/tsx/dist/repl.mjs +4 -0
- package/node_modules/tsx/dist/require-CBjy4Foe.mjs +10 -0
- package/node_modules/tsx/dist/require-CVaYn9z3.cjs +1 -0
- package/node_modules/tsx/dist/suppress-warnings.cjs +1 -0
- package/node_modules/tsx/dist/temporary-directory-Du7LpLp9.mjs +1 -0
- package/node_modules/tsx/package.json +70 -0
- package/node_modules/zod/LICENSE +21 -0
- package/node_modules/zod/README.md +218 -0
- package/node_modules/zod/compile.cjs +96 -0
- package/node_modules/zod/compile.d.cts +1 -0
- package/node_modules/zod/compile.d.ts +1 -0
- package/node_modules/zod/compile.js +52 -0
- package/node_modules/zod/index.cjs +52 -0
- package/node_modules/zod/index.d.cts +3 -0
- package/node_modules/zod/index.d.ts +3 -0
- package/node_modules/zod/index.js +7 -0
- package/node_modules/zod/locales/index.cjs +36 -0
- package/node_modules/zod/locales/index.d.cts +1 -0
- package/node_modules/zod/locales/index.d.ts +1 -0
- package/node_modules/zod/locales/index.js +1 -0
- package/node_modules/zod/locales/package.json +7 -0
- package/node_modules/zod/mini/index.cjs +51 -0
- package/node_modules/zod/mini/index.d.cts +3 -0
- package/node_modules/zod/mini/index.d.ts +3 -0
- package/node_modules/zod/mini/index.js +3 -0
- package/node_modules/zod/mini/package.json +7 -0
- package/node_modules/zod/package.json +147 -0
- package/node_modules/zod/src/compile.ts +56 -0
- package/node_modules/zod/src/index.ts +7 -0
- package/node_modules/zod/src/locales/index.ts +1 -0
- package/node_modules/zod/src/mini/index.ts +3 -0
- package/node_modules/zod/src/v3/ZodError.ts +340 -0
- package/node_modules/zod/src/v3/benchmarks/datetime.ts +58 -0
- package/node_modules/zod/src/v3/benchmarks/discriminatedUnion.ts +80 -0
- package/node_modules/zod/src/v3/benchmarks/index.ts +59 -0
- package/node_modules/zod/src/v3/benchmarks/ipv4.ts +57 -0
- package/node_modules/zod/src/v3/benchmarks/object.ts +69 -0
- package/node_modules/zod/src/v3/benchmarks/primitives.ts +162 -0
- package/node_modules/zod/src/v3/benchmarks/realworld.ts +63 -0
- package/node_modules/zod/src/v3/benchmarks/string.ts +55 -0
- package/node_modules/zod/src/v3/benchmarks/union.ts +80 -0
- package/node_modules/zod/src/v3/errors.ts +13 -0
- package/node_modules/zod/src/v3/external.ts +6 -0
- package/node_modules/zod/src/v3/helpers/enumUtil.ts +17 -0
- package/node_modules/zod/src/v3/helpers/errorUtil.ts +8 -0
- package/node_modules/zod/src/v3/helpers/parseUtil.ts +176 -0
- package/node_modules/zod/src/v3/helpers/partialUtil.ts +34 -0
- package/node_modules/zod/src/v3/helpers/typeAliases.ts +2 -0
- package/node_modules/zod/src/v3/helpers/util.ts +224 -0
- package/node_modules/zod/src/v3/index.ts +4 -0
- package/node_modules/zod/src/v3/locales/en.ts +124 -0
- package/node_modules/zod/src/v3/standard-schema.ts +113 -0
- package/node_modules/zod/src/v3/tests/Mocker.ts +54 -0
- package/node_modules/zod/src/v3/tests/all-errors.test.ts +157 -0
- package/node_modules/zod/src/v3/tests/anyunknown.test.ts +28 -0
- package/node_modules/zod/src/v3/tests/array.test.ts +71 -0
- package/node_modules/zod/src/v3/tests/async-parsing.test.ts +388 -0
- package/node_modules/zod/src/v3/tests/async-refinements.test.ts +46 -0
- package/node_modules/zod/src/v3/tests/base.test.ts +29 -0
- package/node_modules/zod/src/v3/tests/bigint.test.ts +55 -0
- package/node_modules/zod/src/v3/tests/branded.test.ts +53 -0
- package/node_modules/zod/src/v3/tests/catch.test.ts +220 -0
- package/node_modules/zod/src/v3/tests/coerce.test.ts +133 -0
- package/node_modules/zod/src/v3/tests/complex.test.ts +70 -0
- package/node_modules/zod/src/v3/tests/custom.test.ts +31 -0
- package/node_modules/zod/src/v3/tests/date.test.ts +32 -0
- package/node_modules/zod/src/v3/tests/deepmasking.test.ts +186 -0
- package/node_modules/zod/src/v3/tests/default.test.ts +112 -0
- package/node_modules/zod/src/v3/tests/description.test.ts +33 -0
- package/node_modules/zod/src/v3/tests/discriminated-unions.test.ts +315 -0
- package/node_modules/zod/src/v3/tests/enum.test.ts +80 -0
- package/node_modules/zod/src/v3/tests/error.test.ts +586 -0
- package/node_modules/zod/src/v3/tests/firstparty.test.ts +87 -0
- package/node_modules/zod/src/v3/tests/firstpartyschematypes.test.ts +21 -0
- package/node_modules/zod/src/v3/tests/function.test.ts +261 -0
- package/node_modules/zod/src/v3/tests/generics.test.ts +48 -0
- package/node_modules/zod/src/v3/tests/instanceof.test.ts +37 -0
- package/node_modules/zod/src/v3/tests/intersection.test.ts +128 -0
- package/node_modules/zod/src/v3/tests/language-server.source.ts +76 -0
- package/node_modules/zod/src/v3/tests/language-server.test.ts +206 -0
- package/node_modules/zod/src/v3/tests/literal.test.ts +36 -0
- package/node_modules/zod/src/v3/tests/map.test.ts +110 -0
- package/node_modules/zod/src/v3/tests/masking.test.ts +4 -0
- package/node_modules/zod/src/v3/tests/mocker.test.ts +19 -0
- package/node_modules/zod/src/v3/tests/nan.test.ts +24 -0
- package/node_modules/zod/src/v3/tests/nativeEnum.test.ts +87 -0
- package/node_modules/zod/src/v3/tests/nullable.test.ts +42 -0
- package/node_modules/zod/src/v3/tests/number.test.ts +176 -0
- package/node_modules/zod/src/v3/tests/object-augmentation.test.ts +29 -0
- package/node_modules/zod/src/v3/tests/object-in-es5-env.test.ts +29 -0
- package/node_modules/zod/src/v3/tests/object.test.ts +431 -0
- package/node_modules/zod/src/v3/tests/optional.test.ts +42 -0
- package/node_modules/zod/src/v3/tests/parseUtil.test.ts +23 -0
- package/node_modules/zod/src/v3/tests/parser.test.ts +41 -0
- package/node_modules/zod/src/v3/tests/partials.test.ts +243 -0
- package/node_modules/zod/src/v3/tests/pickomit.test.ts +111 -0
- package/node_modules/zod/src/v3/tests/pipeline.test.ts +29 -0
- package/node_modules/zod/src/v3/tests/preprocess.test.ts +186 -0
- package/node_modules/zod/src/v3/tests/primitive.test.ts +440 -0
- package/node_modules/zod/src/v3/tests/promise.test.ts +90 -0
- package/node_modules/zod/src/v3/tests/readonly.test.ts +114 -0
- package/node_modules/zod/src/v3/tests/record.test.ts +171 -0
- package/node_modules/zod/src/v3/tests/recursive.test.ts +197 -0
- package/node_modules/zod/src/v3/tests/refine.test.ts +313 -0
- package/node_modules/zod/src/v3/tests/safeparse.test.ts +27 -0
- package/node_modules/zod/src/v3/tests/set.test.ts +142 -0
- package/node_modules/zod/src/v3/tests/standard-schema.test.ts +83 -0
- package/node_modules/zod/src/v3/tests/string.test.ts +922 -0
- package/node_modules/zod/src/v3/tests/transformer.test.ts +233 -0
- package/node_modules/zod/src/v3/tests/tuple.test.ts +90 -0
- package/node_modules/zod/src/v3/tests/unions.test.ts +57 -0
- package/node_modules/zod/src/v3/tests/validations.test.ts +133 -0
- package/node_modules/zod/src/v3/tests/void.test.ts +15 -0
- package/node_modules/zod/src/v3/types.ts +5138 -0
- package/node_modules/zod/src/v4/classic/checks.ts +33 -0
- package/node_modules/zod/src/v4/classic/coerce.ts +27 -0
- package/node_modules/zod/src/v4/classic/compat.ts +78 -0
- package/node_modules/zod/src/v4/classic/deep-partial.ts +76 -0
- package/node_modules/zod/src/v4/classic/errors.ts +97 -0
- package/node_modules/zod/src/v4/classic/external.ts +57 -0
- package/node_modules/zod/src/v4/classic/from-json-schema.ts +944 -0
- package/node_modules/zod/src/v4/classic/in-out.ts +45 -0
- package/node_modules/zod/src/v4/classic/index.ts +5 -0
- package/node_modules/zod/src/v4/classic/iso.ts +20 -0
- package/node_modules/zod/src/v4/classic/parse.ts +88 -0
- package/node_modules/zod/src/v4/classic/schemas.ts +2937 -0
- package/node_modules/zod/src/v4/classic/tests/anyunknown.test.ts +26 -0
- package/node_modules/zod/src/v4/classic/tests/apply.test.ts +86 -0
- package/node_modules/zod/src/v4/classic/tests/array.test.ts +264 -0
- package/node_modules/zod/src/v4/classic/tests/assignability.test.ts +402 -0
- package/node_modules/zod/src/v4/classic/tests/async-parsing.test.ts +381 -0
- package/node_modules/zod/src/v4/classic/tests/async-refinements.test.ts +68 -0
- package/node_modules/zod/src/v4/classic/tests/base.test.ts +7 -0
- package/node_modules/zod/src/v4/classic/tests/bigint.test.ts +89 -0
- package/node_modules/zod/src/v4/classic/tests/brand.test.ts +106 -0
- package/node_modules/zod/src/v4/classic/tests/catch.test.ts +604 -0
- package/node_modules/zod/src/v4/classic/tests/coalesce.test.ts +20 -0
- package/node_modules/zod/src/v4/classic/tests/codec-examples.test.ts +572 -0
- package/node_modules/zod/src/v4/classic/tests/codec.test.ts +704 -0
- package/node_modules/zod/src/v4/classic/tests/coerce.test.ts +160 -0
- package/node_modules/zod/src/v4/classic/tests/continuability.test.ts +374 -0
- package/node_modules/zod/src/v4/classic/tests/credit-card.test.ts +76 -0
- package/node_modules/zod/src/v4/classic/tests/custom.test.ts +40 -0
- package/node_modules/zod/src/v4/classic/tests/cyclic-data.test.ts +985 -0
- package/node_modules/zod/src/v4/classic/tests/date.test.ts +92 -0
- package/node_modules/zod/src/v4/classic/tests/datetime.test.ts +316 -0
- package/node_modules/zod/src/v4/classic/tests/deep-partial.test.ts +380 -0
- package/node_modules/zod/src/v4/classic/tests/default-locale.test.ts +32 -0
- package/node_modules/zod/src/v4/classic/tests/default.test.ts +455 -0
- package/node_modules/zod/src/v4/classic/tests/describe-meta-checks.test.ts +27 -0
- package/node_modules/zod/src/v4/classic/tests/description.test.ts +32 -0
- package/node_modules/zod/src/v4/classic/tests/detached-methods.test.ts +221 -0
- package/node_modules/zod/src/v4/classic/tests/discriminated-unions.test.ts +942 -0
- package/node_modules/zod/src/v4/classic/tests/enum.test.ts +305 -0
- package/node_modules/zod/src/v4/classic/tests/error-utils.test.ts +917 -0
- package/node_modules/zod/src/v4/classic/tests/error.test.ts +1108 -0
- package/node_modules/zod/src/v4/classic/tests/file.test.ts +96 -0
- package/node_modules/zod/src/v4/classic/tests/firstparty.test.ts +179 -0
- package/node_modules/zod/src/v4/classic/tests/fix-json-issue.test.ts +26 -0
- package/node_modules/zod/src/v4/classic/tests/from-json-schema.test.ts +1467 -0
- package/node_modules/zod/src/v4/classic/tests/function.test.ts +391 -0
- package/node_modules/zod/src/v4/classic/tests/generics.test.ts +72 -0
- package/node_modules/zod/src/v4/classic/tests/global-config.test.ts +32 -0
- package/node_modules/zod/src/v4/classic/tests/hash.test.ts +67 -0
- package/node_modules/zod/src/v4/classic/tests/iban.test.ts +81 -0
- package/node_modules/zod/src/v4/classic/tests/in-out.test.ts +159 -0
- package/node_modules/zod/src/v4/classic/tests/index.test.ts +958 -0
- package/node_modules/zod/src/v4/classic/tests/instance-footprint.test.ts +319 -0
- package/node_modules/zod/src/v4/classic/tests/instanceof.test.ts +127 -0
- package/node_modules/zod/src/v4/classic/tests/intersection.test.ts +334 -0
- package/node_modules/zod/src/v4/classic/tests/issue-schema.test.ts +215 -0
- package/node_modules/zod/src/v4/classic/tests/jitless-allows-eval.test.ts +39 -0
- package/node_modules/zod/src/v4/classic/tests/json.test.ts +109 -0
- package/node_modules/zod/src/v4/classic/tests/lazy.test.ts +262 -0
- package/node_modules/zod/src/v4/classic/tests/literal.test.ts +128 -0
- package/node_modules/zod/src/v4/classic/tests/locales_ka.test.ts +29 -0
- package/node_modules/zod/src/v4/classic/tests/locales_ro.test.ts +24 -0
- package/node_modules/zod/src/v4/classic/tests/map.test.ts +342 -0
- package/node_modules/zod/src/v4/classic/tests/nan.test.ts +21 -0
- package/node_modules/zod/src/v4/classic/tests/nested-refine.test.ts +168 -0
- package/node_modules/zod/src/v4/classic/tests/nonoptional.test.ts +101 -0
- package/node_modules/zod/src/v4/classic/tests/nullable.test.ts +22 -0
- package/node_modules/zod/src/v4/classic/tests/number.test.ts +401 -0
- package/node_modules/zod/src/v4/classic/tests/object.test.ts +1006 -0
- package/node_modules/zod/src/v4/classic/tests/optin-ladder.test.ts +235 -0
- package/node_modules/zod/src/v4/classic/tests/optional.test.ts +366 -0
- package/node_modules/zod/src/v4/classic/tests/partial.test.ts +497 -0
- package/node_modules/zod/src/v4/classic/tests/pickomit.test.ts +211 -0
- package/node_modules/zod/src/v4/classic/tests/pipe.test.ts +101 -0
- package/node_modules/zod/src/v4/classic/tests/prefault.test.ts +144 -0
- package/node_modules/zod/src/v4/classic/tests/preprocess-types.test.ts +44 -0
- package/node_modules/zod/src/v4/classic/tests/preprocess.test.ts +351 -0
- package/node_modules/zod/src/v4/classic/tests/primitive.test.ts +175 -0
- package/node_modules/zod/src/v4/classic/tests/promise.test.ts +81 -0
- package/node_modules/zod/src/v4/classic/tests/properties.test.ts +141 -0
- package/node_modules/zod/src/v4/classic/tests/prototypes.test.ts +23 -0
- package/node_modules/zod/src/v4/classic/tests/readonly.test.ts +184 -0
- package/node_modules/zod/src/v4/classic/tests/record.test.ts +885 -0
- package/node_modules/zod/src/v4/classic/tests/recursive-types.test.ts +707 -0
- package/node_modules/zod/src/v4/classic/tests/redos.test.ts +68 -0
- package/node_modules/zod/src/v4/classic/tests/refine.test.ts +693 -0
- package/node_modules/zod/src/v4/classic/tests/registries.test.ts +243 -0
- package/node_modules/zod/src/v4/classic/tests/set.test.ts +190 -0
- package/node_modules/zod/src/v4/classic/tests/standard-schema.test.ts +153 -0
- package/node_modules/zod/src/v4/classic/tests/string-formats.test.ts +125 -0
- package/node_modules/zod/src/v4/classic/tests/string.test.ts +1440 -0
- package/node_modules/zod/src/v4/classic/tests/stringbool.test.ts +124 -0
- package/node_modules/zod/src/v4/classic/tests/template-literal.test.ts +812 -0
- package/node_modules/zod/src/v4/classic/tests/to-json-schema-methods.test.ts +438 -0
- package/node_modules/zod/src/v4/classic/tests/to-json-schema.test.ts +4733 -0
- package/node_modules/zod/src/v4/classic/tests/transform.test.ts +378 -0
- package/node_modules/zod/src/v4/classic/tests/tuple.test.ts +511 -0
- package/node_modules/zod/src/v4/classic/tests/union.test.ts +310 -0
- package/node_modules/zod/src/v4/classic/tests/url.test.ts +13 -0
- package/node_modules/zod/src/v4/classic/tests/validate.test.ts +645 -0
- package/node_modules/zod/src/v4/classic/tests/validations.test.ts +283 -0
- package/node_modules/zod/src/v4/classic/tests/void.test.ts +12 -0
- package/node_modules/zod/src/v4/core/api.ts +1875 -0
- package/node_modules/zod/src/v4/core/checks.ts +1266 -0
- package/node_modules/zod/src/v4/core/compile.ts +2313 -0
- package/node_modules/zod/src/v4/core/core.ts +230 -0
- package/node_modules/zod/src/v4/core/doc.ts +52 -0
- package/node_modules/zod/src/v4/core/errors.ts +543 -0
- package/node_modules/zod/src/v4/core/index.ts +19 -0
- package/node_modules/zod/src/v4/core/json-schema-generator.ts +131 -0
- package/node_modules/zod/src/v4/core/json-schema-processors.ts +934 -0
- package/node_modules/zod/src/v4/core/json-schema.ts +148 -0
- package/node_modules/zod/src/v4/core/memoizer.ts +331 -0
- package/node_modules/zod/src/v4/core/parse.ts +309 -0
- package/node_modules/zod/src/v4/core/regexes.ts +217 -0
- package/node_modules/zod/src/v4/core/registries.ts +105 -0
- package/node_modules/zod/src/v4/core/schemas.ts +5192 -0
- package/node_modules/zod/src/v4/core/standard-schema.ts +159 -0
- package/node_modules/zod/src/v4/core/tests/cached.test.ts +54 -0
- package/node_modules/zod/src/v4/core/tests/compile-differential.test.ts +1004 -0
- package/node_modules/zod/src/v4/core/tests/compile-generative.test.ts +445 -0
- package/node_modules/zod/src/v4/core/tests/compile-global.test.ts +89 -0
- package/node_modules/zod/src/v4/core/tests/compile-off-by-default.test.ts +26 -0
- package/node_modules/zod/src/v4/core/tests/compile-stats.test.ts +34 -0
- package/node_modules/zod/src/v4/core/tests/compile.test.ts +2036 -0
- package/node_modules/zod/src/v4/core/tests/email-regex.test.ts +140 -0
- package/node_modules/zod/src/v4/core/tests/extend.test.ts +59 -0
- package/node_modules/zod/src/v4/core/tests/index.test.ts +46 -0
- package/node_modules/zod/src/v4/core/tests/locales/be.test.ts +124 -0
- package/node_modules/zod/src/v4/core/tests/locales/el.test.ts +214 -0
- package/node_modules/zod/src/v4/core/tests/locales/en.test.ts +22 -0
- package/node_modules/zod/src/v4/core/tests/locales/es.test.ts +180 -0
- package/node_modules/zod/src/v4/core/tests/locales/fr.test.ts +72 -0
- package/node_modules/zod/src/v4/core/tests/locales/he.test.ts +379 -0
- package/node_modules/zod/src/v4/core/tests/locales/hr.test.ts +163 -0
- package/node_modules/zod/src/v4/core/tests/locales/nl.test.ts +46 -0
- package/node_modules/zod/src/v4/core/tests/locales/parity.test.ts +42 -0
- package/node_modules/zod/src/v4/core/tests/locales/ru.test.ts +128 -0
- package/node_modules/zod/src/v4/core/tests/locales/tg.test.ts +85 -0
- package/node_modules/zod/src/v4/core/tests/locales/tk.test.ts +69 -0
- package/node_modules/zod/src/v4/core/tests/locales/tr.test.ts +69 -0
- package/node_modules/zod/src/v4/core/tests/locales/uz.test.ts +105 -0
- package/node_modules/zod/src/v4/core/tests/polyfill-collision.test.ts +46 -0
- package/node_modules/zod/src/v4/core/tests/record-constructor.test.ts +125 -0
- package/node_modules/zod/src/v4/core/tests/recursive-tuples.test.ts +45 -0
- package/node_modules/zod/src/v4/core/tests/treeshake.test.ts +51 -0
- package/node_modules/zod/src/v4/core/tests/url-no-canparse.test.ts +133 -0
- package/node_modules/zod/src/v4/core/to-json-schema.ts +857 -0
- package/node_modules/zod/src/v4/core/util.ts +1280 -0
- package/node_modules/zod/src/v4/core/versions.ts +5 -0
- package/node_modules/zod/src/v4/core/visit.ts +193 -0
- package/node_modules/zod/src/v4/core/zsf.ts +323 -0
- package/node_modules/zod/src/v4/index.ts +4 -0
- package/node_modules/zod/src/v4/locales/ar.ts +120 -0
- package/node_modules/zod/src/v4/locales/az.ts +116 -0
- package/node_modules/zod/src/v4/locales/be.ts +188 -0
- package/node_modules/zod/src/v4/locales/bg.ts +133 -0
- package/node_modules/zod/src/v4/locales/bn.ts +123 -0
- package/node_modules/zod/src/v4/locales/ca.ts +121 -0
- package/node_modules/zod/src/v4/locales/ckb.ts +140 -0
- package/node_modules/zod/src/v4/locales/cs.ts +123 -0
- package/node_modules/zod/src/v4/locales/da.ts +128 -0
- package/node_modules/zod/src/v4/locales/de.ts +121 -0
- package/node_modules/zod/src/v4/locales/el.ts +124 -0
- package/node_modules/zod/src/v4/locales/en.ts +136 -0
- package/node_modules/zod/src/v4/locales/eo.ts +123 -0
- package/node_modules/zod/src/v4/locales/es.ts +145 -0
- package/node_modules/zod/src/v4/locales/fa.ts +131 -0
- package/node_modules/zod/src/v4/locales/fi.ts +126 -0
- package/node_modules/zod/src/v4/locales/fr-CA.ts +121 -0
- package/node_modules/zod/src/v4/locales/fr.ts +139 -0
- package/node_modules/zod/src/v4/locales/gu.ts +123 -0
- package/node_modules/zod/src/v4/locales/he.ts +253 -0
- package/node_modules/zod/src/v4/locales/hi.ts +126 -0
- package/node_modules/zod/src/v4/locales/hr.ts +135 -0
- package/node_modules/zod/src/v4/locales/hu.ts +122 -0
- package/node_modules/zod/src/v4/locales/hy.ts +175 -0
- package/node_modules/zod/src/v4/locales/id.ts +120 -0
- package/node_modules/zod/src/v4/locales/index.ts +63 -0
- package/node_modules/zod/src/v4/locales/is.ts +124 -0
- package/node_modules/zod/src/v4/locales/it.ts +121 -0
- package/node_modules/zod/src/v4/locales/ja.ts +119 -0
- package/node_modules/zod/src/v4/locales/ka.ts +128 -0
- package/node_modules/zod/src/v4/locales/kh.ts +7 -0
- package/node_modules/zod/src/v4/locales/km.ts +124 -0
- package/node_modules/zod/src/v4/locales/kn.ts +126 -0
- package/node_modules/zod/src/v4/locales/ko.ts +126 -0
- package/node_modules/zod/src/v4/locales/lt.ts +243 -0
- package/node_modules/zod/src/v4/locales/mk.ts +123 -0
- package/node_modules/zod/src/v4/locales/ms.ts +120 -0
- package/node_modules/zod/src/v4/locales/ne.ts +128 -0
- package/node_modules/zod/src/v4/locales/nl.ts +126 -0
- package/node_modules/zod/src/v4/locales/nn.ts +121 -0
- package/node_modules/zod/src/v4/locales/no.ts +121 -0
- package/node_modules/zod/src/v4/locales/ota.ts +122 -0
- package/node_modules/zod/src/v4/locales/pl.ts +123 -0
- package/node_modules/zod/src/v4/locales/ps.ts +131 -0
- package/node_modules/zod/src/v4/locales/pt-BR.ts +161 -0
- package/node_modules/zod/src/v4/locales/pt.ts +159 -0
- package/node_modules/zod/src/v4/locales/ro.ts +132 -0
- package/node_modules/zod/src/v4/locales/ru.ts +188 -0
- package/node_modules/zod/src/v4/locales/sk.ts +123 -0
- package/node_modules/zod/src/v4/locales/sl.ts +123 -0
- package/node_modules/zod/src/v4/locales/sv.ts +124 -0
- package/node_modules/zod/src/v4/locales/ta.ts +123 -0
- package/node_modules/zod/src/v4/locales/tg.ts +135 -0
- package/node_modules/zod/src/v4/locales/th.ts +124 -0
- package/node_modules/zod/src/v4/locales/tk.ts +113 -0
- package/node_modules/zod/src/v4/locales/tr.ts +116 -0
- package/node_modules/zod/src/v4/locales/ua.ts +7 -0
- package/node_modules/zod/src/v4/locales/uk.ts +122 -0
- package/node_modules/zod/src/v4/locales/ur.ts +124 -0
- package/node_modules/zod/src/v4/locales/uz.ts +120 -0
- package/node_modules/zod/src/v4/locales/vi.ts +122 -0
- package/node_modules/zod/src/v4/locales/yo.ts +129 -0
- package/node_modules/zod/src/v4/locales/zh-CN.ts +121 -0
- package/node_modules/zod/src/v4/locales/zh-TW.ts +120 -0
- package/node_modules/zod/src/v4/mini/checks.ts +33 -0
- package/node_modules/zod/src/v4/mini/coerce.ts +27 -0
- package/node_modules/zod/src/v4/mini/deep-partial.ts +73 -0
- package/node_modules/zod/src/v4/mini/external.ts +52 -0
- package/node_modules/zod/src/v4/mini/in-out.ts +45 -0
- package/node_modules/zod/src/v4/mini/index.ts +3 -0
- package/node_modules/zod/src/v4/mini/iso.ts +66 -0
- package/node_modules/zod/src/v4/mini/parse.ts +16 -0
- package/node_modules/zod/src/v4/mini/schemas.ts +2051 -0
- package/node_modules/zod/src/v4/mini/tests/apply.test.ts +57 -0
- package/node_modules/zod/src/v4/mini/tests/assignability.test.ts +190 -0
- package/node_modules/zod/src/v4/mini/tests/brand.test.ts +94 -0
- package/node_modules/zod/src/v4/mini/tests/checks.test.ts +158 -0
- package/node_modules/zod/src/v4/mini/tests/codec.test.ts +582 -0
- package/node_modules/zod/src/v4/mini/tests/computed.test.ts +30 -0
- package/node_modules/zod/src/v4/mini/tests/cyclic-data.test.ts +84 -0
- package/node_modules/zod/src/v4/mini/tests/deep-partial.test.ts +54 -0
- package/node_modules/zod/src/v4/mini/tests/error.test.ts +41 -0
- package/node_modules/zod/src/v4/mini/tests/functions.test.ts +14 -0
- package/node_modules/zod/src/v4/mini/tests/index.test.ts +1146 -0
- package/node_modules/zod/src/v4/mini/tests/number.test.ts +109 -0
- package/node_modules/zod/src/v4/mini/tests/object.test.ts +292 -0
- package/node_modules/zod/src/v4/mini/tests/prototypes.test.ts +43 -0
- package/node_modules/zod/src/v4/mini/tests/recursive-types.test.ts +341 -0
- package/node_modules/zod/src/v4/mini/tests/standard-schema.test.ts +50 -0
- package/node_modules/zod/src/v4/mini/tests/string.test.ts +396 -0
- package/node_modules/zod/src/v4-mini/index.ts +3 -0
- package/node_modules/zod/v3/ZodError.cjs +167 -0
- package/node_modules/zod/v3/ZodError.d.cts +164 -0
- package/node_modules/zod/v3/ZodError.d.ts +164 -0
- package/node_modules/zod/v3/ZodError.js +143 -0
- package/node_modules/zod/v3/errors.cjs +36 -0
- package/node_modules/zod/v3/errors.d.cts +5 -0
- package/node_modules/zod/v3/errors.d.ts +5 -0
- package/node_modules/zod/v3/errors.js +9 -0
- package/node_modules/zod/v3/external.cjs +41 -0
- package/node_modules/zod/v3/external.d.cts +6 -0
- package/node_modules/zod/v3/external.d.ts +6 -0
- package/node_modules/zod/v3/external.js +6 -0
- package/node_modules/zod/v3/helpers/enumUtil.cjs +21 -0
- package/node_modules/zod/v3/helpers/enumUtil.d.cts +8 -0
- package/node_modules/zod/v3/helpers/enumUtil.d.ts +8 -0
- package/node_modules/zod/v3/helpers/enumUtil.js +1 -0
- package/node_modules/zod/v3/helpers/errorUtil.cjs +28 -0
- package/node_modules/zod/v3/helpers/errorUtil.d.cts +9 -0
- package/node_modules/zod/v3/helpers/errorUtil.d.ts +9 -0
- package/node_modules/zod/v3/helpers/errorUtil.js +6 -0
- package/node_modules/zod/v3/helpers/parseUtil.cjs +143 -0
- package/node_modules/zod/v3/helpers/parseUtil.d.cts +78 -0
- package/node_modules/zod/v3/helpers/parseUtil.d.ts +78 -0
- package/node_modules/zod/v3/helpers/parseUtil.js +109 -0
- package/node_modules/zod/v3/helpers/partialUtil.cjs +21 -0
- package/node_modules/zod/v3/helpers/partialUtil.d.cts +8 -0
- package/node_modules/zod/v3/helpers/partialUtil.d.ts +8 -0
- package/node_modules/zod/v3/helpers/partialUtil.js +1 -0
- package/node_modules/zod/v3/helpers/typeAliases.cjs +21 -0
- package/node_modules/zod/v3/helpers/typeAliases.d.cts +2 -0
- package/node_modules/zod/v3/helpers/typeAliases.d.ts +2 -0
- package/node_modules/zod/v3/helpers/typeAliases.js +1 -0
- package/node_modules/zod/v3/helpers/util.cjs +156 -0
- package/node_modules/zod/v3/helpers/util.d.cts +85 -0
- package/node_modules/zod/v3/helpers/util.d.ts +85 -0
- package/node_modules/zod/v3/helpers/util.js +133 -0
- package/node_modules/zod/v3/index.cjs +52 -0
- package/node_modules/zod/v3/index.d.cts +4 -0
- package/node_modules/zod/v3/index.d.ts +4 -0
- package/node_modules/zod/v3/index.js +4 -0
- package/node_modules/zod/v3/locales/en.cjs +112 -0
- package/node_modules/zod/v3/locales/en.d.cts +3 -0
- package/node_modules/zod/v3/locales/en.d.ts +3 -0
- package/node_modules/zod/v3/locales/en.js +109 -0
- package/node_modules/zod/v3/package.json +7 -0
- package/node_modules/zod/v3/standard-schema.cjs +21 -0
- package/node_modules/zod/v3/standard-schema.d.cts +102 -0
- package/node_modules/zod/v3/standard-schema.d.ts +102 -0
- package/node_modules/zod/v3/standard-schema.js +1 -0
- package/node_modules/zod/v3/types.cjs +3798 -0
- package/node_modules/zod/v3/types.d.cts +1034 -0
- package/node_modules/zod/v3/types.d.ts +1034 -0
- package/node_modules/zod/v3/types.js +3697 -0
- package/node_modules/zod/v4/classic/checks.cjs +53 -0
- package/node_modules/zod/v4/classic/checks.d.cts +1 -0
- package/node_modules/zod/v4/classic/checks.d.ts +1 -0
- package/node_modules/zod/v4/classic/checks.js +1 -0
- package/node_modules/zod/v4/classic/coerce.cjs +66 -0
- package/node_modules/zod/v4/classic/coerce.d.cts +17 -0
- package/node_modules/zod/v4/classic/coerce.d.ts +17 -0
- package/node_modules/zod/v4/classic/coerce.js +17 -0
- package/node_modules/zod/v4/classic/compat.cjs +80 -0
- package/node_modules/zod/v4/classic/compat.d.cts +48 -0
- package/node_modules/zod/v4/classic/compat.d.ts +48 -0
- package/node_modules/zod/v4/classic/compat.js +31 -0
- package/node_modules/zod/v4/classic/deep-partial.cjs +58 -0
- package/node_modules/zod/v4/classic/deep-partial.d.cts +14 -0
- package/node_modules/zod/v4/classic/deep-partial.d.ts +14 -0
- package/node_modules/zod/v4/classic/deep-partial.js +13 -0
- package/node_modules/zod/v4/classic/errors.cjs +102 -0
- package/node_modules/zod/v4/classic/errors.d.cts +29 -0
- package/node_modules/zod/v4/classic/errors.d.ts +29 -0
- package/node_modules/zod/v4/classic/errors.js +57 -0
- package/node_modules/zod/v4/classic/external.cjs +97 -0
- package/node_modules/zod/v4/classic/external.d.cts +19 -0
- package/node_modules/zod/v4/classic/external.d.ts +19 -0
- package/node_modules/zod/v4/classic/external.js +17 -0
- package/node_modules/zod/v4/classic/from-json-schema.cjs +926 -0
- package/node_modules/zod/v4/classic/from-json-schema.d.cts +12 -0
- package/node_modules/zod/v4/classic/from-json-schema.d.ts +12 -0
- package/node_modules/zod/v4/classic/from-json-schema.js +881 -0
- package/node_modules/zod/v4/classic/in-out.cjs +82 -0
- package/node_modules/zod/v4/classic/in-out.d.cts +8 -0
- package/node_modules/zod/v4/classic/in-out.d.ts +8 -0
- package/node_modules/zod/v4/classic/in-out.js +36 -0
- package/node_modules/zod/v4/classic/index.cjs +52 -0
- package/node_modules/zod/v4/classic/index.d.cts +3 -0
- package/node_modules/zod/v4/classic/index.d.ts +3 -0
- package/node_modules/zod/v4/classic/index.js +4 -0
- package/node_modules/zod/v4/classic/iso.cjs +68 -0
- package/node_modules/zod/v4/classic/iso.d.cts +7 -0
- package/node_modules/zod/v4/classic/iso.d.ts +7 -0
- package/node_modules/zod/v4/classic/iso.js +15 -0
- package/node_modules/zod/v4/classic/package.json +7 -0
- package/node_modules/zod/v4/classic/parse.cjs +63 -0
- package/node_modules/zod/v4/classic/parse.d.cts +44 -0
- package/node_modules/zod/v4/classic/parse.d.ts +44 -0
- package/node_modules/zod/v4/classic/parse.js +16 -0
- package/node_modules/zod/v4/classic/schemas.cjs +1676 -0
- package/node_modules/zod/v4/classic/schemas.d.cts +816 -0
- package/node_modules/zod/v4/classic/schemas.d.ts +816 -0
- package/node_modules/zod/v4/classic/schemas.js +1535 -0
- package/node_modules/zod/v4/core/api.cjs +1269 -0
- package/node_modules/zod/v4/core/api.d.cts +335 -0
- package/node_modules/zod/v4/core/api.d.ts +335 -0
- package/node_modules/zod/v4/core/api.js +1107 -0
- package/node_modules/zod/v4/core/checks.cjs +561 -0
- package/node_modules/zod/v4/core/checks.d.cts +294 -0
- package/node_modules/zod/v4/core/checks.d.ts +294 -0
- package/node_modules/zod/v4/core/checks.js +516 -0
- package/node_modules/zod/v4/core/compile.cjs +1908 -0
- package/node_modules/zod/v4/core/compile.d.cts +65 -0
- package/node_modules/zod/v4/core/compile.d.ts +65 -0
- package/node_modules/zod/v4/core/compile.js +1858 -0
- package/node_modules/zod/v4/core/core.cjs +167 -0
- package/node_modules/zod/v4/core/core.d.cts +82 -0
- package/node_modules/zod/v4/core/core.d.ts +82 -0
- package/node_modules/zod/v4/core/core.js +141 -0
- package/node_modules/zod/v4/core/doc.cjs +61 -0
- package/node_modules/zod/v4/core/doc.d.cts +16 -0
- package/node_modules/zod/v4/core/doc.d.ts +16 -0
- package/node_modules/zod/v4/core/doc.js +38 -0
- package/node_modules/zod/v4/core/errors.cjs +317 -0
- package/node_modules/zod/v4/core/errors.d.cts +225 -0
- package/node_modules/zod/v4/core/errors.d.ts +225 -0
- package/node_modules/zod/v4/core/errors.js +267 -0
- package/node_modules/zod/v4/core/index.cjs +70 -0
- package/node_modules/zod/v4/core/index.d.cts +19 -0
- package/node_modules/zod/v4/core/index.d.ts +19 -0
- package/node_modules/zod/v4/core/index.js +19 -0
- package/node_modules/zod/v4/core/json-schema-generator.cjs +122 -0
- package/node_modules/zod/v4/core/json-schema-generator.d.cts +65 -0
- package/node_modules/zod/v4/core/json-schema-generator.d.ts +65 -0
- package/node_modules/zod/v4/core/json-schema-generator.js +99 -0
- package/node_modules/zod/v4/core/json-schema-processors.cjs +929 -0
- package/node_modules/zod/v4/core/json-schema-processors.d.cts +65 -0
- package/node_modules/zod/v4/core/json-schema-processors.d.ts +65 -0
- package/node_modules/zod/v4/core/json-schema-processors.js +843 -0
- package/node_modules/zod/v4/core/json-schema.cjs +21 -0
- package/node_modules/zod/v4/core/json-schema.d.cts +89 -0
- package/node_modules/zod/v4/core/json-schema.d.ts +89 -0
- package/node_modules/zod/v4/core/json-schema.js +1 -0
- package/node_modules/zod/v4/core/memoizer.cjs +360 -0
- package/node_modules/zod/v4/core/memoizer.d.cts +15 -0
- package/node_modules/zod/v4/core/memoizer.d.ts +15 -0
- package/node_modules/zod/v4/core/memoizer.js +311 -0
- package/node_modules/zod/v4/core/package.json +7 -0
- package/node_modules/zod/v4/core/parse.cjs +233 -0
- package/node_modules/zod/v4/core/parse.d.cts +52 -0
- package/node_modules/zod/v4/core/parse.d.ts +52 -0
- package/node_modules/zod/v4/core/parse.js +175 -0
- package/node_modules/zod/v4/core/regexes.cjs +209 -0
- package/node_modules/zod/v4/core/regexes.d.cts +90 -0
- package/node_modules/zod/v4/core/regexes.d.ts +90 -0
- package/node_modules/zod/v4/core/regexes.js +156 -0
- package/node_modules/zod/v4/core/registries.cjs +75 -0
- package/node_modules/zod/v4/core/registries.d.cts +35 -0
- package/node_modules/zod/v4/core/registries.d.ts +35 -0
- package/node_modules/zod/v4/core/registries.js +51 -0
- package/node_modules/zod/v4/core/schemas.cjs +2669 -0
- package/node_modules/zod/v4/core/schemas.d.cts +1273 -0
- package/node_modules/zod/v4/core/schemas.d.ts +1273 -0
- package/node_modules/zod/v4/core/schemas.js +2606 -0
- package/node_modules/zod/v4/core/standard-schema.cjs +21 -0
- package/node_modules/zod/v4/core/standard-schema.d.cts +126 -0
- package/node_modules/zod/v4/core/standard-schema.d.ts +126 -0
- package/node_modules/zod/v4/core/standard-schema.js +1 -0
- package/node_modules/zod/v4/core/to-json-schema.cjs +681 -0
- package/node_modules/zod/v4/core/to-json-schema.d.cts +154 -0
- package/node_modules/zod/v4/core/to-json-schema.d.ts +154 -0
- package/node_modules/zod/v4/core/to-json-schema.js +651 -0
- package/node_modules/zod/v4/core/util.cjs +949 -0
- package/node_modules/zod/v4/core/util.d.cts +278 -0
- package/node_modules/zod/v4/core/util.d.ts +278 -0
- package/node_modules/zod/v4/core/util.js +859 -0
- package/node_modules/zod/v4/core/versions.cjs +27 -0
- package/node_modules/zod/v4/core/versions.d.cts +5 -0
- package/node_modules/zod/v4/core/versions.d.ts +5 -0
- package/node_modules/zod/v4/core/versions.js +5 -0
- package/node_modules/zod/v4/core/visit.cjs +214 -0
- package/node_modules/zod/v4/core/visit.d.cts +30 -0
- package/node_modules/zod/v4/core/visit.d.ts +30 -0
- package/node_modules/zod/v4/core/visit.js +169 -0
- package/node_modules/zod/v4/index.cjs +51 -0
- package/node_modules/zod/v4/index.d.cts +3 -0
- package/node_modules/zod/v4/index.d.ts +3 -0
- package/node_modules/zod/v4/index.js +4 -0
- package/node_modules/zod/v4/locales/ar.cjs +138 -0
- package/node_modules/zod/v4/locales/ar.d.cts +5 -0
- package/node_modules/zod/v4/locales/ar.d.ts +4 -0
- package/node_modules/zod/v4/locales/ar.js +111 -0
- package/node_modules/zod/v4/locales/az.cjs +137 -0
- package/node_modules/zod/v4/locales/az.d.cts +5 -0
- package/node_modules/zod/v4/locales/az.d.ts +4 -0
- package/node_modules/zod/v4/locales/az.js +110 -0
- package/node_modules/zod/v4/locales/be.cjs +195 -0
- package/node_modules/zod/v4/locales/be.d.cts +5 -0
- package/node_modules/zod/v4/locales/be.d.ts +4 -0
- package/node_modules/zod/v4/locales/be.js +168 -0
- package/node_modules/zod/v4/locales/bg.cjs +152 -0
- package/node_modules/zod/v4/locales/bg.d.cts +5 -0
- package/node_modules/zod/v4/locales/bg.d.ts +4 -0
- package/node_modules/zod/v4/locales/bg.js +125 -0
- package/node_modules/zod/v4/locales/bn.cjs +140 -0
- package/node_modules/zod/v4/locales/bn.d.cts +5 -0
- package/node_modules/zod/v4/locales/bn.d.ts +4 -0
- package/node_modules/zod/v4/locales/bn.js +113 -0
- package/node_modules/zod/v4/locales/ca.cjs +139 -0
- package/node_modules/zod/v4/locales/ca.d.cts +5 -0
- package/node_modules/zod/v4/locales/ca.d.ts +4 -0
- package/node_modules/zod/v4/locales/ca.js +112 -0
- package/node_modules/zod/v4/locales/ckb.cjs +160 -0
- package/node_modules/zod/v4/locales/ckb.d.cts +5 -0
- package/node_modules/zod/v4/locales/ckb.d.ts +4 -0
- package/node_modules/zod/v4/locales/ckb.js +133 -0
- package/node_modules/zod/v4/locales/cs.cjs +143 -0
- package/node_modules/zod/v4/locales/cs.d.cts +5 -0
- package/node_modules/zod/v4/locales/cs.d.ts +4 -0
- package/node_modules/zod/v4/locales/cs.js +116 -0
- package/node_modules/zod/v4/locales/da.cjs +147 -0
- package/node_modules/zod/v4/locales/da.d.cts +5 -0
- package/node_modules/zod/v4/locales/da.d.ts +4 -0
- package/node_modules/zod/v4/locales/da.js +120 -0
- package/node_modules/zod/v4/locales/de.cjs +140 -0
- package/node_modules/zod/v4/locales/de.d.cts +5 -0
- package/node_modules/zod/v4/locales/de.d.ts +4 -0
- package/node_modules/zod/v4/locales/de.js +113 -0
- package/node_modules/zod/v4/locales/el.cjs +139 -0
- package/node_modules/zod/v4/locales/el.d.cts +5 -0
- package/node_modules/zod/v4/locales/el.d.ts +4 -0
- package/node_modules/zod/v4/locales/el.js +112 -0
- package/node_modules/zod/v4/locales/en.cjs +152 -0
- package/node_modules/zod/v4/locales/en.d.cts +5 -0
- package/node_modules/zod/v4/locales/en.d.ts +4 -0
- package/node_modules/zod/v4/locales/en.js +125 -0
- package/node_modules/zod/v4/locales/eo.cjs +141 -0
- package/node_modules/zod/v4/locales/eo.d.cts +5 -0
- package/node_modules/zod/v4/locales/eo.d.ts +4 -0
- package/node_modules/zod/v4/locales/eo.js +114 -0
- package/node_modules/zod/v4/locales/es.cjs +163 -0
- package/node_modules/zod/v4/locales/es.d.cts +5 -0
- package/node_modules/zod/v4/locales/es.d.ts +4 -0
- package/node_modules/zod/v4/locales/es.js +136 -0
- package/node_modules/zod/v4/locales/fa.cjs +146 -0
- package/node_modules/zod/v4/locales/fa.d.cts +5 -0
- package/node_modules/zod/v4/locales/fa.d.ts +4 -0
- package/node_modules/zod/v4/locales/fa.js +119 -0
- package/node_modules/zod/v4/locales/fi.cjs +144 -0
- package/node_modules/zod/v4/locales/fi.d.cts +5 -0
- package/node_modules/zod/v4/locales/fi.d.ts +4 -0
- package/node_modules/zod/v4/locales/fi.js +117 -0
- package/node_modules/zod/v4/locales/fr-CA.cjs +139 -0
- package/node_modules/zod/v4/locales/fr-CA.d.cts +5 -0
- package/node_modules/zod/v4/locales/fr-CA.d.ts +4 -0
- package/node_modules/zod/v4/locales/fr-CA.js +112 -0
- package/node_modules/zod/v4/locales/fr.cjs +156 -0
- package/node_modules/zod/v4/locales/fr.d.cts +5 -0
- package/node_modules/zod/v4/locales/fr.d.ts +4 -0
- package/node_modules/zod/v4/locales/fr.js +129 -0
- package/node_modules/zod/v4/locales/gu.cjs +140 -0
- package/node_modules/zod/v4/locales/gu.d.cts +5 -0
- package/node_modules/zod/v4/locales/gu.d.ts +4 -0
- package/node_modules/zod/v4/locales/gu.js +113 -0
- package/node_modules/zod/v4/locales/he.cjs +248 -0
- package/node_modules/zod/v4/locales/he.d.cts +5 -0
- package/node_modules/zod/v4/locales/he.d.ts +4 -0
- package/node_modules/zod/v4/locales/he.js +221 -0
- package/node_modules/zod/v4/locales/hi.cjs +138 -0
- package/node_modules/zod/v4/locales/hi.d.cts +5 -0
- package/node_modules/zod/v4/locales/hi.d.ts +4 -0
- package/node_modules/zod/v4/locales/hi.js +111 -0
- package/node_modules/zod/v4/locales/hr.cjs +153 -0
- package/node_modules/zod/v4/locales/hr.d.cts +5 -0
- package/node_modules/zod/v4/locales/hr.d.ts +4 -0
- package/node_modules/zod/v4/locales/hr.js +126 -0
- package/node_modules/zod/v4/locales/hu.cjs +140 -0
- package/node_modules/zod/v4/locales/hu.d.cts +5 -0
- package/node_modules/zod/v4/locales/hu.d.ts +4 -0
- package/node_modules/zod/v4/locales/hu.js +113 -0
- package/node_modules/zod/v4/locales/hy.cjs +185 -0
- package/node_modules/zod/v4/locales/hy.d.cts +5 -0
- package/node_modules/zod/v4/locales/hy.d.ts +4 -0
- package/node_modules/zod/v4/locales/hy.js +158 -0
- package/node_modules/zod/v4/locales/id.cjs +138 -0
- package/node_modules/zod/v4/locales/id.d.cts +5 -0
- package/node_modules/zod/v4/locales/id.d.ts +4 -0
- package/node_modules/zod/v4/locales/id.js +111 -0
- package/node_modules/zod/v4/locales/index.cjs +152 -0
- package/node_modules/zod/v4/locales/index.d.cts +63 -0
- package/node_modules/zod/v4/locales/index.d.ts +63 -0
- package/node_modules/zod/v4/locales/index.js +63 -0
- package/node_modules/zod/v4/locales/is.cjs +141 -0
- package/node_modules/zod/v4/locales/is.d.cts +5 -0
- package/node_modules/zod/v4/locales/is.d.ts +4 -0
- package/node_modules/zod/v4/locales/is.js +114 -0
- package/node_modules/zod/v4/locales/it.cjs +140 -0
- package/node_modules/zod/v4/locales/it.d.cts +5 -0
- package/node_modules/zod/v4/locales/it.d.ts +4 -0
- package/node_modules/zod/v4/locales/it.js +113 -0
- package/node_modules/zod/v4/locales/ja.cjs +139 -0
- package/node_modules/zod/v4/locales/ja.d.cts +5 -0
- package/node_modules/zod/v4/locales/ja.d.ts +4 -0
- package/node_modules/zod/v4/locales/ja.js +112 -0
- package/node_modules/zod/v4/locales/ka.cjs +144 -0
- package/node_modules/zod/v4/locales/ka.d.cts +5 -0
- package/node_modules/zod/v4/locales/ka.d.ts +4 -0
- package/node_modules/zod/v4/locales/ka.js +117 -0
- package/node_modules/zod/v4/locales/kh.cjs +12 -0
- package/node_modules/zod/v4/locales/kh.d.cts +5 -0
- package/node_modules/zod/v4/locales/kh.d.ts +5 -0
- package/node_modules/zod/v4/locales/kh.js +5 -0
- package/node_modules/zod/v4/locales/km.cjs +142 -0
- package/node_modules/zod/v4/locales/km.d.cts +5 -0
- package/node_modules/zod/v4/locales/km.d.ts +4 -0
- package/node_modules/zod/v4/locales/km.js +115 -0
- package/node_modules/zod/v4/locales/kn.cjs +143 -0
- package/node_modules/zod/v4/locales/kn.d.cts +5 -0
- package/node_modules/zod/v4/locales/kn.d.ts +4 -0
- package/node_modules/zod/v4/locales/kn.js +116 -0
- package/node_modules/zod/v4/locales/ko.cjs +143 -0
- package/node_modules/zod/v4/locales/ko.d.cts +5 -0
- package/node_modules/zod/v4/locales/ko.d.ts +4 -0
- package/node_modules/zod/v4/locales/ko.js +116 -0
- package/node_modules/zod/v4/locales/lt.cjs +234 -0
- package/node_modules/zod/v4/locales/lt.d.cts +5 -0
- package/node_modules/zod/v4/locales/lt.d.ts +4 -0
- package/node_modules/zod/v4/locales/lt.js +207 -0
- package/node_modules/zod/v4/locales/mk.cjs +141 -0
- package/node_modules/zod/v4/locales/mk.d.cts +5 -0
- package/node_modules/zod/v4/locales/mk.d.ts +4 -0
- package/node_modules/zod/v4/locales/mk.js +114 -0
- package/node_modules/zod/v4/locales/ms.cjs +139 -0
- package/node_modules/zod/v4/locales/ms.d.cts +5 -0
- package/node_modules/zod/v4/locales/ms.d.ts +4 -0
- package/node_modules/zod/v4/locales/ms.js +112 -0
- package/node_modules/zod/v4/locales/ne.cjs +138 -0
- package/node_modules/zod/v4/locales/ne.d.cts +5 -0
- package/node_modules/zod/v4/locales/ne.d.ts +4 -0
- package/node_modules/zod/v4/locales/ne.js +111 -0
- package/node_modules/zod/v4/locales/nl.cjs +142 -0
- package/node_modules/zod/v4/locales/nl.d.cts +5 -0
- package/node_modules/zod/v4/locales/nl.d.ts +4 -0
- package/node_modules/zod/v4/locales/nl.js +115 -0
- package/node_modules/zod/v4/locales/nn.cjs +140 -0
- package/node_modules/zod/v4/locales/nn.d.cts +5 -0
- package/node_modules/zod/v4/locales/nn.d.ts +4 -0
- package/node_modules/zod/v4/locales/nn.js +113 -0
- package/node_modules/zod/v4/locales/no.cjs +140 -0
- package/node_modules/zod/v4/locales/no.d.cts +5 -0
- package/node_modules/zod/v4/locales/no.d.ts +4 -0
- package/node_modules/zod/v4/locales/no.js +113 -0
- package/node_modules/zod/v4/locales/ota.cjs +141 -0
- package/node_modules/zod/v4/locales/ota.d.cts +5 -0
- package/node_modules/zod/v4/locales/ota.d.ts +4 -0
- package/node_modules/zod/v4/locales/ota.js +114 -0
- package/node_modules/zod/v4/locales/package.json +7 -0
- package/node_modules/zod/v4/locales/pl.cjs +141 -0
- package/node_modules/zod/v4/locales/pl.d.cts +5 -0
- package/node_modules/zod/v4/locales/pl.d.ts +4 -0
- package/node_modules/zod/v4/locales/pl.js +114 -0
- package/node_modules/zod/v4/locales/ps.cjs +146 -0
- package/node_modules/zod/v4/locales/ps.d.cts +5 -0
- package/node_modules/zod/v4/locales/ps.d.ts +4 -0
- package/node_modules/zod/v4/locales/ps.js +119 -0
- package/node_modules/zod/v4/locales/pt-BR.cjs +170 -0
- package/node_modules/zod/v4/locales/pt-BR.d.cts +5 -0
- package/node_modules/zod/v4/locales/pt-BR.d.ts +4 -0
- package/node_modules/zod/v4/locales/pt-BR.js +143 -0
- package/node_modules/zod/v4/locales/pt.cjs +169 -0
- package/node_modules/zod/v4/locales/pt.d.cts +5 -0
- package/node_modules/zod/v4/locales/pt.d.ts +4 -0
- package/node_modules/zod/v4/locales/pt.js +142 -0
- package/node_modules/zod/v4/locales/ro.cjs +149 -0
- package/node_modules/zod/v4/locales/ro.d.cts +5 -0
- package/node_modules/zod/v4/locales/ro.d.ts +4 -0
- package/node_modules/zod/v4/locales/ro.js +122 -0
- package/node_modules/zod/v4/locales/ru.cjs +195 -0
- package/node_modules/zod/v4/locales/ru.d.cts +5 -0
- package/node_modules/zod/v4/locales/ru.d.ts +4 -0
- package/node_modules/zod/v4/locales/ru.js +168 -0
- package/node_modules/zod/v4/locales/sk.cjs +143 -0
- package/node_modules/zod/v4/locales/sk.d.cts +5 -0
- package/node_modules/zod/v4/locales/sk.d.ts +4 -0
- package/node_modules/zod/v4/locales/sk.js +116 -0
- package/node_modules/zod/v4/locales/sl.cjs +141 -0
- package/node_modules/zod/v4/locales/sl.d.cts +5 -0
- package/node_modules/zod/v4/locales/sl.d.ts +4 -0
- package/node_modules/zod/v4/locales/sl.js +114 -0
- package/node_modules/zod/v4/locales/sv.cjs +142 -0
- package/node_modules/zod/v4/locales/sv.d.cts +5 -0
- package/node_modules/zod/v4/locales/sv.d.ts +4 -0
- package/node_modules/zod/v4/locales/sv.js +115 -0
- package/node_modules/zod/v4/locales/ta.cjs +142 -0
- package/node_modules/zod/v4/locales/ta.d.cts +5 -0
- package/node_modules/zod/v4/locales/ta.d.ts +4 -0
- package/node_modules/zod/v4/locales/ta.js +115 -0
- package/node_modules/zod/v4/locales/tg.cjs +144 -0
- package/node_modules/zod/v4/locales/tg.d.cts +5 -0
- package/node_modules/zod/v4/locales/tg.d.ts +4 -0
- package/node_modules/zod/v4/locales/tg.js +117 -0
- package/node_modules/zod/v4/locales/th.cjs +142 -0
- package/node_modules/zod/v4/locales/th.d.cts +5 -0
- package/node_modules/zod/v4/locales/th.d.ts +4 -0
- package/node_modules/zod/v4/locales/th.js +115 -0
- package/node_modules/zod/v4/locales/tk.cjs +134 -0
- package/node_modules/zod/v4/locales/tk.d.cts +5 -0
- package/node_modules/zod/v4/locales/tk.d.ts +4 -0
- package/node_modules/zod/v4/locales/tk.js +107 -0
- package/node_modules/zod/v4/locales/tr.cjs +137 -0
- package/node_modules/zod/v4/locales/tr.d.cts +5 -0
- package/node_modules/zod/v4/locales/tr.d.ts +4 -0
- package/node_modules/zod/v4/locales/tr.js +110 -0
- package/node_modules/zod/v4/locales/ua.cjs +12 -0
- package/node_modules/zod/v4/locales/ua.d.cts +5 -0
- package/node_modules/zod/v4/locales/ua.d.ts +5 -0
- package/node_modules/zod/v4/locales/ua.js +5 -0
- package/node_modules/zod/v4/locales/uk.cjs +140 -0
- package/node_modules/zod/v4/locales/uk.d.cts +5 -0
- package/node_modules/zod/v4/locales/uk.d.ts +4 -0
- package/node_modules/zod/v4/locales/uk.js +113 -0
- package/node_modules/zod/v4/locales/ur.cjs +142 -0
- package/node_modules/zod/v4/locales/ur.d.cts +5 -0
- package/node_modules/zod/v4/locales/ur.d.ts +4 -0
- package/node_modules/zod/v4/locales/ur.js +115 -0
- package/node_modules/zod/v4/locales/uz.cjs +140 -0
- package/node_modules/zod/v4/locales/uz.d.cts +5 -0
- package/node_modules/zod/v4/locales/uz.d.ts +4 -0
- package/node_modules/zod/v4/locales/uz.js +113 -0
- package/node_modules/zod/v4/locales/vi.cjs +140 -0
- package/node_modules/zod/v4/locales/vi.d.cts +5 -0
- package/node_modules/zod/v4/locales/vi.d.ts +4 -0
- package/node_modules/zod/v4/locales/vi.js +113 -0
- package/node_modules/zod/v4/locales/yo.cjs +139 -0
- package/node_modules/zod/v4/locales/yo.d.cts +5 -0
- package/node_modules/zod/v4/locales/yo.d.ts +4 -0
- package/node_modules/zod/v4/locales/yo.js +112 -0
- package/node_modules/zod/v4/locales/zh-CN.cjs +141 -0
- package/node_modules/zod/v4/locales/zh-CN.d.cts +5 -0
- package/node_modules/zod/v4/locales/zh-CN.d.ts +4 -0
- package/node_modules/zod/v4/locales/zh-CN.js +114 -0
- package/node_modules/zod/v4/locales/zh-TW.cjs +139 -0
- package/node_modules/zod/v4/locales/zh-TW.d.cts +5 -0
- package/node_modules/zod/v4/locales/zh-TW.d.ts +4 -0
- package/node_modules/zod/v4/locales/zh-TW.js +112 -0
- package/node_modules/zod/v4/mini/checks.cjs +54 -0
- package/node_modules/zod/v4/mini/checks.d.cts +1 -0
- package/node_modules/zod/v4/mini/checks.d.ts +1 -0
- package/node_modules/zod/v4/mini/checks.js +1 -0
- package/node_modules/zod/v4/mini/coerce.cjs +71 -0
- package/node_modules/zod/v4/mini/coerce.d.cts +7 -0
- package/node_modules/zod/v4/mini/coerce.d.ts +7 -0
- package/node_modules/zod/v4/mini/coerce.js +22 -0
- package/node_modules/zod/v4/mini/deep-partial.cjs +58 -0
- package/node_modules/zod/v4/mini/deep-partial.d.cts +14 -0
- package/node_modules/zod/v4/mini/deep-partial.d.ts +14 -0
- package/node_modules/zod/v4/mini/deep-partial.js +13 -0
- package/node_modules/zod/v4/mini/external.cjs +95 -0
- package/node_modules/zod/v4/mini/external.d.cts +16 -0
- package/node_modules/zod/v4/mini/external.d.ts +16 -0
- package/node_modules/zod/v4/mini/external.js +16 -0
- package/node_modules/zod/v4/mini/in-out.cjs +82 -0
- package/node_modules/zod/v4/mini/in-out.d.cts +8 -0
- package/node_modules/zod/v4/mini/in-out.d.ts +8 -0
- package/node_modules/zod/v4/mini/in-out.js +36 -0
- package/node_modules/zod/v4/mini/index.cjs +51 -0
- package/node_modules/zod/v4/mini/index.d.cts +3 -0
- package/node_modules/zod/v4/mini/index.d.ts +3 -0
- package/node_modules/zod/v4/mini/index.js +3 -0
- package/node_modules/zod/v4/mini/iso.cjs +83 -0
- package/node_modules/zod/v4/mini/iso.d.cts +22 -0
- package/node_modules/zod/v4/mini/iso.d.ts +22 -0
- package/node_modules/zod/v4/mini/iso.js +34 -0
- package/node_modules/zod/v4/mini/package.json +7 -0
- package/node_modules/zod/v4/mini/parse.cjs +37 -0
- package/node_modules/zod/v4/mini/parse.d.cts +1 -0
- package/node_modules/zod/v4/mini/parse.d.ts +1 -0
- package/node_modules/zod/v4/mini/parse.js +1 -0
- package/node_modules/zod/v4/mini/schemas.cjs +1147 -0
- package/node_modules/zod/v4/mini/schemas.d.cts +480 -0
- package/node_modules/zod/v4/mini/schemas.d.ts +480 -0
- package/node_modules/zod/v4/mini/schemas.js +1002 -0
- package/node_modules/zod/v4/package.json +7 -0
- package/node_modules/zod/v4-mini/index.cjs +51 -0
- package/node_modules/zod/v4-mini/index.d.cts +3 -0
- package/node_modules/zod/v4-mini/index.d.ts +3 -0
- package/node_modules/zod/v4-mini/index.js +3 -0
- package/node_modules/zod/v4-mini/package.json +7 -0
- package/package.json +50 -0
|
@@ -0,0 +1,3877 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
8
|
+
import { githubLauncherSource } from "./github-launcher.js";
|
|
9
|
+
import { prepareCommandManagedRuntime, } from "./command-managed-runtime.js";
|
|
10
|
+
import { buildRemoteExecutionSessionIdentity, prepareRemoteManagedRuntime, remoteExecutionSessionMatches, } from "./remote-managed-runtime.js";
|
|
11
|
+
export { resolveReferencedSourceIgnore } from "./sandbox-managed-runtime.js";
|
|
12
|
+
import { createCommandManagedSandboxCallbackBridgeQueueClient, createSandboxCallbackBridgeAsset, createSandboxCallbackBridgeToken, DEFAULT_SANDBOX_CALLBACK_BRIDGE_MAX_BODY_BYTES, HTTP2_SANDBOX_CALLBACK_BRIDGE_ROUTE_ALLOWLIST, SANDBOX_CALLBACK_BRIDGE_HTTP2_MODE, sandboxCallbackBridgeDirectories, startSandboxCallbackBridgeServer, startSandboxCallbackBridgeWorker, syncRemoteTextFileWithHashSkip, syncSandboxCallbackBridgeEntrypoint, } from "./sandbox-callback-bridge.js";
|
|
13
|
+
import { createHttp2BridgeServer, BridgeProcessCapacityError, } from "./http2-bridge-server.js";
|
|
14
|
+
import { createSandboxRunLogTailFactory, } from "./sandbox-run-log-stream.js";
|
|
15
|
+
import { DEFAULT_DUPLEX_BROKER_BUDGETS, DUPLEX_CHANNEL_LOST_ERROR_CODE, isSafeBridgeMethod, } from "./bridge-transport-contract.js";
|
|
16
|
+
import { decodeDuplexLine, DEFAULT_MAX_DUPLEX_FRAME_BYTES } from "./duplex-frame-codec.js";
|
|
17
|
+
import { createDuplexObservability, mapHttp2EventToDuplexLossReason, } from "./duplex-observability.js";
|
|
18
|
+
import { createSshCommandManagedRuntimeRunner, parseSshRemoteExecutionSpec, runSshCommand, shellQuote } from "./ssh.js";
|
|
19
|
+
import { ensureCommandResolvable, resolveCommandForLogs, runChildProcess, } from "./server-utils.js";
|
|
20
|
+
import { sanitizeRemoteExecutionEnv } from "./remote-execution-env.js";
|
|
21
|
+
import { preferredShellForSandbox, shellCommandArgs } from "./sandbox-shell.js";
|
|
22
|
+
import { runWithRuntimeParent, } from "./acpx-engine/startup-timing.js";
|
|
23
|
+
export function postedIssueCommentLogMarker(method, requestPath, status, body) {
|
|
24
|
+
if (method !== "POST" || !/^\/api\/issues\/[^/]+\/comments$/.test(requestPath) || status < 200 || status >= 300) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const bodyText = typeof body === "string" ? body : body.toString("utf8");
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(bodyText);
|
|
30
|
+
return typeof parsed.id === "string" && parsed.id.length > 0 ? `comment id: ${parsed.id}\n` : null;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export { sanitizeRemoteExecutionEnv } from "./remote-execution-env.js";
|
|
37
|
+
// 4-hour wall-clock backstop for sandbox-backed adapter runs. This is a
|
|
38
|
+
// last-resort kill switch, not the primary hang detector: genuinely hung runs
|
|
39
|
+
// are caught much earlier by the adapters' output-inactivity monitors (e.g.
|
|
40
|
+
// codex-local's 7-minute monitor). The value intentionally matches the
|
|
41
|
+
// recovery watchdog's ACTIVE_RUN_OUTPUT_CRITICAL_THRESHOLD_MS (4h) in
|
|
42
|
+
// server/src/services/recovery/service.ts so healthy long runs are never
|
|
43
|
+
// killed by the adapter before the watchdog would even consider them stuck.
|
|
44
|
+
export const DEFAULT_REMOTE_SANDBOX_ADAPTER_TIMEOUT_SEC = 14_400;
|
|
45
|
+
function parseObject(value) {
|
|
46
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
47
|
+
? value
|
|
48
|
+
: {};
|
|
49
|
+
}
|
|
50
|
+
function readString(value) {
|
|
51
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
52
|
+
}
|
|
53
|
+
// Read a serialized effective-capability snapshot back into a full record. A
|
|
54
|
+
// missing or non-boolean field reads as `false`, so a round-tripped target
|
|
55
|
+
// never grants a capability that the snapshot did not carry. Returns null when
|
|
56
|
+
// there is no object to read.
|
|
57
|
+
function parseEffectiveExecutionCapabilities(value) {
|
|
58
|
+
const parsed = parseObject(value);
|
|
59
|
+
if (Object.keys(parsed).length === 0)
|
|
60
|
+
return null;
|
|
61
|
+
return {
|
|
62
|
+
reusableLeases: parsed.reusableLeases === true,
|
|
63
|
+
nativeSyncIn: parsed.nativeSyncIn === true,
|
|
64
|
+
nativeSyncOut: parsed.nativeSyncOut === true,
|
|
65
|
+
persistentProcessSessions: parsed.persistentProcessSessions === true,
|
|
66
|
+
independentControlCommands: parsed.independentControlCommands === true,
|
|
67
|
+
incrementalSessionOutput: parsed.incrementalSessionOutput === true,
|
|
68
|
+
concurrentSyncOperations: parsed.concurrentSyncOperations === true,
|
|
69
|
+
duplexCommandStream: parsed.duplexCommandStream === true,
|
|
70
|
+
runnerWebSocketIngress: parsed.runnerWebSocketIngress === true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function readStringMeta(parsed, key) {
|
|
74
|
+
return readString(parsed[key]);
|
|
75
|
+
}
|
|
76
|
+
function resolveHostForUrl(rawHost) {
|
|
77
|
+
const host = rawHost.trim();
|
|
78
|
+
// Preserve the wildcard bind's address family: a server bound to 0.0.0.0
|
|
79
|
+
// accepts IPv4, so target the IPv4 loopback (and [::1] for ::) instead of
|
|
80
|
+
// "localhost", which the resolver may map to the other family.
|
|
81
|
+
if (host === "0.0.0.0")
|
|
82
|
+
return "127.0.0.1";
|
|
83
|
+
if (host === "::")
|
|
84
|
+
return "[::1]";
|
|
85
|
+
if (!host)
|
|
86
|
+
return "localhost";
|
|
87
|
+
if (host.includes(":") && !host.startsWith("[") && !host.endsWith("]"))
|
|
88
|
+
return `[${host}]`;
|
|
89
|
+
return host;
|
|
90
|
+
}
|
|
91
|
+
function resolveDefaultPaperclipApiUrl() {
|
|
92
|
+
const runtimeHost = resolveHostForUrl(process.env.PAPERCLIP_LISTEN_HOST ?? process.env.HOST ?? "localhost");
|
|
93
|
+
// 3100 matches the default Paperclip dev server port when the runtime does not provide one.
|
|
94
|
+
const runtimePort = process.env.PAPERCLIP_LISTEN_PORT ?? process.env.PORT ?? "3100";
|
|
95
|
+
return `http://${runtimeHost}:${runtimePort}`;
|
|
96
|
+
}
|
|
97
|
+
function isBridgeDebugEnabled(env) {
|
|
98
|
+
const value = env.PAPERCLIP_BRIDGE_DEBUG?.trim().toLowerCase();
|
|
99
|
+
return value === "1" || value === "true" || value === "yes";
|
|
100
|
+
}
|
|
101
|
+
function isAdapterExecutionTargetInstance(value) {
|
|
102
|
+
const parsed = parseObject(value);
|
|
103
|
+
if (parsed.kind === "local")
|
|
104
|
+
return true;
|
|
105
|
+
if (parsed.kind !== "remote")
|
|
106
|
+
return false;
|
|
107
|
+
if (parsed.transport === "ssh")
|
|
108
|
+
return parseSshRemoteExecutionSpec(parseObject(parsed.spec)) !== null;
|
|
109
|
+
if (parsed.transport !== "sandbox")
|
|
110
|
+
return false;
|
|
111
|
+
return readStringMeta(parsed, "remoteCwd") !== null;
|
|
112
|
+
}
|
|
113
|
+
export function adapterExecutionTargetToRemoteSpec(target) {
|
|
114
|
+
return target?.kind === "remote" && target.transport === "ssh" ? target.spec : null;
|
|
115
|
+
}
|
|
116
|
+
export function adapterExecutionTargetIsRemote(target) {
|
|
117
|
+
return target?.kind === "remote";
|
|
118
|
+
}
|
|
119
|
+
export function adapterExecutionTargetUsesManagedHome(target) {
|
|
120
|
+
return target?.kind === "remote" && target.transport === "sandbox";
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Read the per-run duplex bridge kill switch off a target. Only a sandbox
|
|
124
|
+
* target with `enableSandboxDuplexBridge` set to `true` returns `true`. Every
|
|
125
|
+
* other target and every other value returns `false`, so the caller fails
|
|
126
|
+
* closed to the file bridge.
|
|
127
|
+
*/
|
|
128
|
+
export function adapterExecutionTargetEnablesSandboxDuplexBridge(target) {
|
|
129
|
+
return (target?.kind === "remote" &&
|
|
130
|
+
target.transport === "sandbox" &&
|
|
131
|
+
target.enableSandboxDuplexBridge === true);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Read the injected duplex observability recorder off a target. Only a
|
|
135
|
+
* sandbox target with a recorder attached returns it. Every other target
|
|
136
|
+
* returns null, so the bridge falls back to the safe no-op recorder.
|
|
137
|
+
*/
|
|
138
|
+
export function adapterExecutionTargetDuplexObservabilityRecorder(target) {
|
|
139
|
+
return target?.kind === "remote" && target.transport === "sandbox"
|
|
140
|
+
? target.duplexObservabilityRecorder ?? null
|
|
141
|
+
: null;
|
|
142
|
+
}
|
|
143
|
+
export function adapterExecutionTargetRemoteCwd(target, localCwd) {
|
|
144
|
+
return target?.kind === "remote" ? target.remoteCwd : localCwd;
|
|
145
|
+
}
|
|
146
|
+
export function overrideAdapterExecutionTargetRemoteCwd(target, remoteCwd) {
|
|
147
|
+
const nextRemoteCwd = remoteCwd?.trim();
|
|
148
|
+
if (!target || target.kind !== "remote" || !nextRemoteCwd) {
|
|
149
|
+
return target;
|
|
150
|
+
}
|
|
151
|
+
if (target.remoteCwd === nextRemoteCwd) {
|
|
152
|
+
return target;
|
|
153
|
+
}
|
|
154
|
+
if (target.transport === "ssh") {
|
|
155
|
+
return {
|
|
156
|
+
...target,
|
|
157
|
+
remoteCwd: nextRemoteCwd,
|
|
158
|
+
spec: {
|
|
159
|
+
...target.spec,
|
|
160
|
+
remoteCwd: nextRemoteCwd,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
...target,
|
|
166
|
+
remoteCwd: nextRemoteCwd,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export function resolveAdapterExecutionTargetCwd(target, configuredCwd, localFallbackCwd) {
|
|
170
|
+
if (typeof configuredCwd === "string" && configuredCwd.trim().length > 0) {
|
|
171
|
+
return configuredCwd;
|
|
172
|
+
}
|
|
173
|
+
return adapterExecutionTargetRemoteCwd(target, localFallbackCwd);
|
|
174
|
+
}
|
|
175
|
+
export function adapterExecutionTargetUsesPaperclipBridge(target) {
|
|
176
|
+
return target?.kind === "remote";
|
|
177
|
+
}
|
|
178
|
+
export function describeAdapterExecutionTarget(target) {
|
|
179
|
+
if (!target || target.kind === "local")
|
|
180
|
+
return "local environment";
|
|
181
|
+
if (target.transport === "ssh") {
|
|
182
|
+
return `SSH environment ${target.spec.username}@${target.spec.host}:${target.spec.port}`;
|
|
183
|
+
}
|
|
184
|
+
return `sandbox environment${target.providerKey ? ` (${target.providerKey})` : ""}`;
|
|
185
|
+
}
|
|
186
|
+
export function resolveAdapterExecutionTargetTimeout(target, configuredTimeoutSec) {
|
|
187
|
+
if (typeof configuredTimeoutSec === "number" && Number.isFinite(configuredTimeoutSec)) {
|
|
188
|
+
// Preserve fractional (sub-second) configured values instead of flooring:
|
|
189
|
+
// adapters historically honored e.g. timeoutSec=0.5, and flooring would
|
|
190
|
+
// silently turn it into "no timeout".
|
|
191
|
+
if (configuredTimeoutSec > 0) {
|
|
192
|
+
return { timeoutSec: configuredTimeoutSec, source: "configured" };
|
|
193
|
+
}
|
|
194
|
+
// A negative timeoutSec is the explicit "no adapter wall-clock timeout"
|
|
195
|
+
// opt-out, honored even on sandbox targets. Zero cannot carry that
|
|
196
|
+
// meaning: the adapter config UI persists the schema default of 0 for
|
|
197
|
+
// untouched fields, so timeoutSec=0 in stored config does not signal
|
|
198
|
+
// operator intent and falls through to target defaults below.
|
|
199
|
+
if (configuredTimeoutSec < 0) {
|
|
200
|
+
return { timeoutSec: 0, source: "configured" };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Local and SSH adapters preserve the historical "0 means no adapter
|
|
204
|
+
// timeout" behavior. Sandbox-backed runs execute through provider RPCs
|
|
205
|
+
// that usually apply their own shorter command defaults, so request an
|
|
206
|
+
// explicit longer timeout for full adapter runs when the adapter leaves
|
|
207
|
+
// timeoutSec unset.
|
|
208
|
+
if (target?.kind === "remote" && target.transport === "sandbox") {
|
|
209
|
+
return { timeoutSec: DEFAULT_REMOTE_SANDBOX_ADAPTER_TIMEOUT_SEC, source: "sandbox_default" };
|
|
210
|
+
}
|
|
211
|
+
return { timeoutSec: 0, source: "unlimited" };
|
|
212
|
+
}
|
|
213
|
+
export function resolveAdapterExecutionTargetTimeoutSec(target, configuredTimeoutSec) {
|
|
214
|
+
return resolveAdapterExecutionTargetTimeout(target, configuredTimeoutSec).timeoutSec;
|
|
215
|
+
}
|
|
216
|
+
function describeAdapterExecutionTimeoutSource(source) {
|
|
217
|
+
switch (source) {
|
|
218
|
+
case "configured":
|
|
219
|
+
return "configured via adapterConfig.timeoutSec";
|
|
220
|
+
case "sandbox_default":
|
|
221
|
+
return "sandbox default";
|
|
222
|
+
case "unlimited":
|
|
223
|
+
return "no adapter wall-clock timeout";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Self-describing error message for when the adapter wall-clock execution
|
|
228
|
+
* timeout kills a run. Names the timer that fired and the knob that controls
|
|
229
|
+
* it so run failures never surface as a bare "Timed out".
|
|
230
|
+
*/
|
|
231
|
+
export function formatAdapterExecutionTimeoutErrorMessage(resolution) {
|
|
232
|
+
return (`Run exceeded the adapter execution timeout ` +
|
|
233
|
+
`(timeoutSec=${resolution.timeoutSec}, ${describeAdapterExecutionTimeoutSource(resolution.source)}). ` +
|
|
234
|
+
`Set adapterConfig.timeoutSec to raise it.`);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* One-line start-of-run statement of the effective wall-clock timeout and its
|
|
238
|
+
* source. Callers prefix with `[paperclip] ` and append a newline.
|
|
239
|
+
*/
|
|
240
|
+
export function formatAdapterExecutionTimeoutStartLogLine(resolution) {
|
|
241
|
+
if (resolution.timeoutSec <= 0) {
|
|
242
|
+
if (resolution.source === "configured") {
|
|
243
|
+
return ("Adapter execution timeout: none " +
|
|
244
|
+
"(explicitly disabled via adapterConfig.timeoutSec; set it to a positive value to add one).");
|
|
245
|
+
}
|
|
246
|
+
return ("Adapter execution timeout: none " +
|
|
247
|
+
"(no adapter wall-clock timeout for this target; set adapterConfig.timeoutSec to add one).");
|
|
248
|
+
}
|
|
249
|
+
return (`Adapter execution timeout: timeoutSec=${resolution.timeoutSec} ` +
|
|
250
|
+
`(${describeAdapterExecutionTimeoutSource(resolution.source)}; set adapterConfig.timeoutSec to override).`);
|
|
251
|
+
}
|
|
252
|
+
function requireSandboxRunner(target) {
|
|
253
|
+
if (target.runner)
|
|
254
|
+
return target.runner;
|
|
255
|
+
throw new Error("Sandbox execution target is missing its provider runtime runner. Sandbox commands must execute through the environment runtime.");
|
|
256
|
+
}
|
|
257
|
+
function preferredSandboxShell(target) {
|
|
258
|
+
return preferredShellForSandbox(target.shellCommand);
|
|
259
|
+
}
|
|
260
|
+
// The Secure Shell command runner's own output buffer. This value used to
|
|
261
|
+
// derive from the bridge body limit (`DEFAULT_SANDBOX_CALLBACK_BRIDGE_MAX_BODY_BYTES
|
|
262
|
+
// * 4`), so a bridge limit rise silently grew it too. It now stands on its
|
|
263
|
+
// own local constant, independent of the bridge body limit, so a later
|
|
264
|
+
// bridge limit change never resizes this buffer as a side effect.
|
|
265
|
+
const SSH_COMMAND_MAX_BUFFER_BYTES = 1024 * 1024;
|
|
266
|
+
function adapterExecutionTargetCommandRunner(target) {
|
|
267
|
+
if (target.transport === "ssh") {
|
|
268
|
+
return createSshCommandManagedRuntimeRunner({
|
|
269
|
+
spec: target.spec,
|
|
270
|
+
defaultCwd: target.remoteCwd,
|
|
271
|
+
maxBufferBytes: SSH_COMMAND_MAX_BUFFER_BYTES,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return requireSandboxRunner(target);
|
|
275
|
+
}
|
|
276
|
+
function adapterExecutionTargetShellCommand(target) {
|
|
277
|
+
return target.transport === "ssh" ? "sh" : preferredSandboxShell(target);
|
|
278
|
+
}
|
|
279
|
+
function adapterExecutionTargetTimeoutMs(target) {
|
|
280
|
+
return target.transport === "sandbox" ? target.timeoutMs : undefined;
|
|
281
|
+
}
|
|
282
|
+
export async function ensureAdapterExecutionTargetCommandResolvable(command, target, cwd, env, options = {}) {
|
|
283
|
+
if (target?.kind === "remote" && target.transport === "sandbox") {
|
|
284
|
+
await ensureSandboxCommandResolvable(command, target, sanitizeRemoteExecutionEnv(Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === "string"))), options.installCommand?.trim() || null, options.timeoutSec);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
await ensureCommandResolvable(command, cwd, env, {
|
|
288
|
+
remoteExecution: adapterExecutionTargetToRemoteSpec(target),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
async function probeSandboxCommandResolvable(command, target, env) {
|
|
292
|
+
const runner = requireSandboxRunner(target);
|
|
293
|
+
const probeScript = `command -v ${shellQuote(command)}`;
|
|
294
|
+
const result = await runner.execute({
|
|
295
|
+
command: "sh",
|
|
296
|
+
args: ["-c", probeScript],
|
|
297
|
+
cwd: target.remoteCwd,
|
|
298
|
+
env,
|
|
299
|
+
timeoutMs: target.timeoutMs ?? 15_000,
|
|
300
|
+
});
|
|
301
|
+
return {
|
|
302
|
+
resolved: !result.timedOut && (result.exitCode ?? 1) === 0,
|
|
303
|
+
timedOut: result.timedOut,
|
|
304
|
+
stderr: result.stderr.trim(),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
async function ensureSandboxCommandResolvable(command, target, env, installCommand, timeoutSec) {
|
|
308
|
+
// Probe whether the binary is resolvable inside the sandbox. We previously
|
|
309
|
+
// short-circuited this for sandbox targets, which let the caller report a
|
|
310
|
+
// success message even when the CLI was missing from the image. Now we run
|
|
311
|
+
// a real `command -v` through the same runner the hello probe will use, so
|
|
312
|
+
// the first step honestly reflects whether the binary is on PATH. The
|
|
313
|
+
// sandbox provider is responsible for sourcing login profiles (e2b mirrors
|
|
314
|
+
// SSH's buildSshSpawnTarget) so this and the hello probe agree on PATH.
|
|
315
|
+
let probe = await probeSandboxCommandResolvable(command, target, env);
|
|
316
|
+
if (probe.resolved)
|
|
317
|
+
return;
|
|
318
|
+
if (probe.timedOut) {
|
|
319
|
+
throw new Error(`Timed out checking command "${command}" on sandbox target.`);
|
|
320
|
+
}
|
|
321
|
+
// If the caller supplied an install command, attempt the install once via
|
|
322
|
+
// the sandbox runner (which the sandbox provider wraps in a login shell)
|
|
323
|
+
// and re-probe before reporting failure. This lets fresh sandbox leases
|
|
324
|
+
// bring up the CLI before the resolvability gate, mirroring the test path.
|
|
325
|
+
let installFailureDetail = null;
|
|
326
|
+
if (installCommand) {
|
|
327
|
+
const runner = requireSandboxRunner(target);
|
|
328
|
+
const installTimeoutMs = typeof timeoutSec === "number" && Number.isFinite(timeoutSec) && timeoutSec > 0
|
|
329
|
+
? Math.floor(timeoutSec * 1000)
|
|
330
|
+
: target.timeoutMs ?? 300_000;
|
|
331
|
+
try {
|
|
332
|
+
const installResult = await runner.execute({
|
|
333
|
+
command: "sh",
|
|
334
|
+
args: shellCommandArgs(installCommand),
|
|
335
|
+
cwd: target.remoteCwd,
|
|
336
|
+
env,
|
|
337
|
+
timeoutMs: installTimeoutMs,
|
|
338
|
+
});
|
|
339
|
+
if (installResult.timedOut) {
|
|
340
|
+
installFailureDetail = `install command timed out: ${installCommand}`;
|
|
341
|
+
}
|
|
342
|
+
else if ((installResult.exitCode ?? 0) !== 0) {
|
|
343
|
+
const tail = (text) => text.split(/\r?\n/).filter((line) => line.trim().length > 0).slice(-2).join(" | ").slice(0, 240);
|
|
344
|
+
const reason = tail(installResult.stderr || installResult.stdout) || `exit ${installResult.exitCode ?? "?"}`;
|
|
345
|
+
installFailureDetail = `install command exited ${installResult.exitCode ?? "?"}: ${reason}`;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
installFailureDetail = `install command threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
350
|
+
}
|
|
351
|
+
probe = await probeSandboxCommandResolvable(command, target, env);
|
|
352
|
+
if (probe.resolved)
|
|
353
|
+
return;
|
|
354
|
+
if (probe.timedOut) {
|
|
355
|
+
throw new Error(`Timed out checking command "${command}" on sandbox target.`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const probeStderr = probe.stderr.length > 0 ? ` probe stderr: ${probe.stderr}` : "";
|
|
359
|
+
const installDetail = installFailureDetail ? `; ${installFailureDetail}` : "";
|
|
360
|
+
throw new Error(`Command "${command}" is not installed or not on PATH in the sandbox environment${installDetail}.${probeStderr}`);
|
|
361
|
+
}
|
|
362
|
+
export async function resolveAdapterExecutionTargetCommandForLogs(command, target, cwd, env) {
|
|
363
|
+
if (target?.kind === "remote" && target.transport === "sandbox") {
|
|
364
|
+
return `sandbox://${target.providerKey ?? "provider"}/${target.leaseId ?? "lease"}/${target.remoteCwd} :: ${command}`;
|
|
365
|
+
}
|
|
366
|
+
return await resolveCommandForLogs(command, cwd, env, {
|
|
367
|
+
remoteExecution: adapterExecutionTargetToRemoteSpec(target),
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
// Apply the run-disposition seam to one clean process result. Only a clean
|
|
371
|
+
// completion is success-eligible: a timed-out, signalled, or non-zero-exit
|
|
372
|
+
// result is already a failure, so the seam leaves it unchanged. This is the same
|
|
373
|
+
// success-eligibility rule the ACP lane applies. For a success-eligible result
|
|
374
|
+
// the seam settles the disposition in one atomic broker step: it reads the
|
|
375
|
+
// disposition and marks the host-observed orderly completion together, so a
|
|
376
|
+
// gateway exit after the clean completion cannot latch a false mid-run loss. A
|
|
377
|
+
// duplex control channel that died before the clean completion fails the run
|
|
378
|
+
// closed: the seam sets a non-zero exit code, the typed `duplex_channel_lost`
|
|
379
|
+
// error code, and a stderr note that names only the typed loss reason.
|
|
380
|
+
function applyRunDispositionSeam(result, settleRunDisposition) {
|
|
381
|
+
const successEligible = result.exitCode === 0 && !result.timedOut && result.signal === null;
|
|
382
|
+
if (!successEligible || !settleRunDisposition)
|
|
383
|
+
return result;
|
|
384
|
+
const disposition = settleRunDisposition();
|
|
385
|
+
if (!disposition.failed)
|
|
386
|
+
return result;
|
|
387
|
+
const lossReason = disposition.lossReason ?? "other";
|
|
388
|
+
const note = `[paperclip] The sandbox duplex control channel was lost (${lossReason}) before the run completed.\n`;
|
|
389
|
+
const separator = result.stderr.length > 0 && !result.stderr.endsWith("\n") ? "\n" : "";
|
|
390
|
+
return {
|
|
391
|
+
...result,
|
|
392
|
+
exitCode: 1,
|
|
393
|
+
errorCode: DUPLEX_CHANNEL_LOST_ERROR_CODE,
|
|
394
|
+
stderr: `${result.stderr}${separator}${note}`,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
export async function runAdapterExecutionTargetProcess(runId, target, command, args, options) {
|
|
398
|
+
if (target?.kind === "remote" && target.transport === "sandbox") {
|
|
399
|
+
const runner = requireSandboxRunner(target);
|
|
400
|
+
const env = sanitizeRemoteExecutionEnv(options.env);
|
|
401
|
+
await options.onRuntimeProgress?.({
|
|
402
|
+
phase: "adapter_startup",
|
|
403
|
+
message: "Starting adapter in environment",
|
|
404
|
+
});
|
|
405
|
+
const runLogTail = options.runLogTail?.create() ?? null;
|
|
406
|
+
let execCommand = command;
|
|
407
|
+
let execArgs = args;
|
|
408
|
+
if (runLogTail) {
|
|
409
|
+
({ command: execCommand, args: execArgs } = runLogTail.wrapCommand(command, args));
|
|
410
|
+
runLogTail.start(options.onLog);
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
const result = await runner.execute({
|
|
414
|
+
command: execCommand,
|
|
415
|
+
args: execArgs,
|
|
416
|
+
cwd: target.remoteCwd,
|
|
417
|
+
env,
|
|
418
|
+
stdin: options.stdin,
|
|
419
|
+
timeoutMs: options.timeoutSec > 0 ? options.timeoutSec * 1000 : target.timeoutMs ?? undefined,
|
|
420
|
+
// The tail loop already streams incremental chunks; suppress the
|
|
421
|
+
// runner's end-of-run batched onLog to avoid duplicate log bytes.
|
|
422
|
+
onLog: runLogTail ? undefined : options.onLog,
|
|
423
|
+
onSpawn: options.onSpawn
|
|
424
|
+
? async (meta) => options.onSpawn?.({ ...meta, processGroupId: null })
|
|
425
|
+
: undefined,
|
|
426
|
+
});
|
|
427
|
+
// Settle the duplex run disposition synchronously at the clean-completion
|
|
428
|
+
// boundary, before the run-log tail finishes. The atomic settle marks the
|
|
429
|
+
// host-observed orderly completion in one broker step, so a gateway exit
|
|
430
|
+
// after the clean process completion cannot latch a false mid-run loss. A
|
|
431
|
+
// control channel that died before this clean completion still fails the
|
|
432
|
+
// run closed.
|
|
433
|
+
const settled = applyRunDispositionSeam(result, options.settleRunDisposition);
|
|
434
|
+
if (runLogTail) {
|
|
435
|
+
await runLogTail.finish({ stdout: result.stdout, stderr: result.stderr });
|
|
436
|
+
}
|
|
437
|
+
return settled;
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
if (runLogTail) {
|
|
441
|
+
await runLogTail.abort();
|
|
442
|
+
}
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const env = target?.kind === "remote" && target.transport === "ssh"
|
|
447
|
+
? sanitizeRemoteExecutionEnv(options.env)
|
|
448
|
+
: options.env;
|
|
449
|
+
return await runChildProcess(runId, command, args, {
|
|
450
|
+
cwd: options.cwd,
|
|
451
|
+
env,
|
|
452
|
+
stdin: options.stdin,
|
|
453
|
+
timeoutSec: options.timeoutSec,
|
|
454
|
+
graceSec: options.graceSec,
|
|
455
|
+
onLog: options.onLog,
|
|
456
|
+
onSpawn: options.onSpawn,
|
|
457
|
+
terminalResultCleanup: options.terminalResultCleanup,
|
|
458
|
+
localProcessSandbox: target?.kind === "local" || !target ? options.localProcessSandbox : null,
|
|
459
|
+
remoteExecution: adapterExecutionTargetToRemoteSpec(target),
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
export async function runAdapterExecutionTargetShellCommand(runId, target, command, options) {
|
|
463
|
+
const onLog = options.onLog ?? (async () => { });
|
|
464
|
+
if (target?.kind === "remote") {
|
|
465
|
+
const startedAt = new Date().toISOString();
|
|
466
|
+
const env = sanitizeRemoteExecutionEnv(options.env);
|
|
467
|
+
if (target.transport === "ssh") {
|
|
468
|
+
try {
|
|
469
|
+
// Pass the raw command — `runSshCommand` owns profile sourcing and
|
|
470
|
+
// the outer shell wrapper. Wrapping again here would nest a second
|
|
471
|
+
// shell after the explicit `env KEY=VAL` overrides, re-sourcing
|
|
472
|
+
// login profiles AFTER the override and silently undoing any
|
|
473
|
+
// identity var (NVM_DIR / PATH / etc.) that a profile re-exports.
|
|
474
|
+
const result = await runSshCommand(target.spec, command, {
|
|
475
|
+
env,
|
|
476
|
+
timeoutMs: (options.timeoutSec ?? 15) * 1000,
|
|
477
|
+
});
|
|
478
|
+
if (result.stdout)
|
|
479
|
+
await onLog("stdout", result.stdout);
|
|
480
|
+
if (result.stderr)
|
|
481
|
+
await onLog("stderr", result.stderr);
|
|
482
|
+
return {
|
|
483
|
+
exitCode: 0,
|
|
484
|
+
signal: null,
|
|
485
|
+
timedOut: false,
|
|
486
|
+
stdout: result.stdout,
|
|
487
|
+
stderr: result.stderr,
|
|
488
|
+
pid: null,
|
|
489
|
+
startedAt,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
const timedOutError = error;
|
|
494
|
+
const stdout = timedOutError.stdout ?? "";
|
|
495
|
+
const stderr = timedOutError.stderr ?? "";
|
|
496
|
+
if (typeof timedOutError.code === "number") {
|
|
497
|
+
if (stdout)
|
|
498
|
+
await onLog("stdout", stdout);
|
|
499
|
+
if (stderr)
|
|
500
|
+
await onLog("stderr", stderr);
|
|
501
|
+
return {
|
|
502
|
+
exitCode: timedOutError.code,
|
|
503
|
+
signal: timedOutError.signal ?? null,
|
|
504
|
+
timedOut: false,
|
|
505
|
+
stdout,
|
|
506
|
+
stderr,
|
|
507
|
+
pid: null,
|
|
508
|
+
startedAt,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
if (timedOutError.code !== "ETIMEDOUT") {
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
514
|
+
if (stdout)
|
|
515
|
+
await onLog("stdout", stdout);
|
|
516
|
+
if (stderr)
|
|
517
|
+
await onLog("stderr", stderr);
|
|
518
|
+
return {
|
|
519
|
+
exitCode: null,
|
|
520
|
+
signal: timedOutError.signal ?? null,
|
|
521
|
+
timedOut: true,
|
|
522
|
+
stdout,
|
|
523
|
+
stderr,
|
|
524
|
+
pid: null,
|
|
525
|
+
startedAt,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const shellCommand = preferredSandboxShell(target);
|
|
530
|
+
return await requireSandboxRunner(target).execute({
|
|
531
|
+
command: shellCommand,
|
|
532
|
+
args: shellCommandArgs(command),
|
|
533
|
+
cwd: target.remoteCwd,
|
|
534
|
+
env,
|
|
535
|
+
timeoutMs: (options.timeoutSec ?? 15) * 1000,
|
|
536
|
+
onLog,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
return await runAdapterExecutionTargetProcess(runId, target, "sh", ["-lc", command], {
|
|
540
|
+
cwd: options.cwd,
|
|
541
|
+
env: options.env,
|
|
542
|
+
timeoutSec: options.timeoutSec ?? 15,
|
|
543
|
+
graceSec: options.graceSec ?? 5,
|
|
544
|
+
onLog,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
// Best-effort run of an adapter-supplied install command on a sandbox target
|
|
548
|
+
// before the resolvability + hello probe. Returns null for non-sandbox
|
|
549
|
+
// targets so callers can no-op. Returns a structured check otherwise — never
|
|
550
|
+
// throws — so the rest of the test still runs and reports the post-install
|
|
551
|
+
// state honestly. Caller pushes the check into its result array; the test
|
|
552
|
+
// report shows whether install was attempted and what came back.
|
|
553
|
+
export async function maybeRunSandboxInstallCommand(input) {
|
|
554
|
+
const { target, adapterKey, installCommand } = input;
|
|
555
|
+
if (!target || target.kind !== "remote" || target.transport !== "sandbox") {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
const trimmed = installCommand.trim();
|
|
559
|
+
if (trimmed.length === 0)
|
|
560
|
+
return null;
|
|
561
|
+
const code = `${adapterKey}_install_command_run`;
|
|
562
|
+
// Skip install when the binary is already on PATH. Avoids running
|
|
563
|
+
// network-dependent installers (e.g. `curl ... | bash`) on every test
|
|
564
|
+
// probe when the CLI is preinstalled on the lease/template.
|
|
565
|
+
const detectCommand = input.detectCommand?.trim();
|
|
566
|
+
if (detectCommand) {
|
|
567
|
+
try {
|
|
568
|
+
const probe = await runAdapterExecutionTargetShellCommand(input.runId, target, `command -v ${shellQuote(detectCommand)} >/dev/null 2>&1`, {
|
|
569
|
+
cwd: target.remoteCwd,
|
|
570
|
+
env: input.env ?? {},
|
|
571
|
+
timeoutSec: 30,
|
|
572
|
+
graceSec: 5,
|
|
573
|
+
});
|
|
574
|
+
if (!probe.timedOut && probe.exitCode === 0) {
|
|
575
|
+
return {
|
|
576
|
+
code,
|
|
577
|
+
level: "info",
|
|
578
|
+
message: `${detectCommand} already on PATH; skipped install.`,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
// Fall through to actually running the install — failure to probe
|
|
584
|
+
// is not a reason to skip the install gate.
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
let result;
|
|
588
|
+
try {
|
|
589
|
+
result = await runAdapterExecutionTargetShellCommand(input.runId, target, trimmed, {
|
|
590
|
+
cwd: target.remoteCwd,
|
|
591
|
+
env: input.env ?? {},
|
|
592
|
+
timeoutSec: input.timeoutSec ?? 240,
|
|
593
|
+
graceSec: 10,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
catch (err) {
|
|
597
|
+
return {
|
|
598
|
+
code,
|
|
599
|
+
level: "warn",
|
|
600
|
+
message: "Install command threw before completion.",
|
|
601
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
const tail = (text) => text.split(/\r?\n/).filter((line) => line.trim().length > 0).slice(-3).join(" | ").slice(0, 480);
|
|
605
|
+
if (result.timedOut) {
|
|
606
|
+
return {
|
|
607
|
+
code,
|
|
608
|
+
level: "warn",
|
|
609
|
+
message: `Install command timed out: ${trimmed}`,
|
|
610
|
+
detail: tail(result.stderr || result.stdout),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
if ((result.exitCode ?? 1) === 0) {
|
|
614
|
+
return {
|
|
615
|
+
code,
|
|
616
|
+
level: "info",
|
|
617
|
+
message: `Install command ran: ${trimmed}`,
|
|
618
|
+
...(tail(result.stdout) ? { detail: tail(result.stdout) } : {}),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
code,
|
|
623
|
+
level: "warn",
|
|
624
|
+
message: `Install command exited ${result.exitCode}: ${trimmed}`,
|
|
625
|
+
detail: tail(result.stderr || result.stdout),
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
export async function readAdapterExecutionTargetHomeDir(runId, target, options) {
|
|
629
|
+
const result = await runAdapterExecutionTargetShellCommand(runId, target, 'printf %s "$HOME"', options);
|
|
630
|
+
const homeDir = result.stdout.trim();
|
|
631
|
+
return homeDir.length > 0 ? homeDir : null;
|
|
632
|
+
}
|
|
633
|
+
export async function ensureAdapterExecutionTargetRuntimeCommandInstalled(input) {
|
|
634
|
+
const installCommand = input.installCommand?.trim();
|
|
635
|
+
if (!installCommand || input.target?.kind !== "remote" || input.target.transport !== "sandbox") {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const detectCommand = input.detectCommand?.trim();
|
|
639
|
+
if (detectCommand) {
|
|
640
|
+
const probe = await runAdapterExecutionTargetShellCommand(input.runId, input.target, `command -v ${shellQuote(detectCommand)} >/dev/null 2>&1`, {
|
|
641
|
+
cwd: input.cwd,
|
|
642
|
+
env: input.env,
|
|
643
|
+
timeoutSec: input.timeoutSec,
|
|
644
|
+
graceSec: input.graceSec,
|
|
645
|
+
});
|
|
646
|
+
if (!probe.timedOut && probe.exitCode === 0) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const result = await runAdapterExecutionTargetShellCommand(input.runId, input.target, installCommand, {
|
|
651
|
+
cwd: input.cwd,
|
|
652
|
+
env: input.env,
|
|
653
|
+
timeoutSec: input.timeoutSec,
|
|
654
|
+
graceSec: input.graceSec,
|
|
655
|
+
onLog: input.onLog,
|
|
656
|
+
});
|
|
657
|
+
// A failed or timed-out install is not necessarily fatal: the CLI may already
|
|
658
|
+
// be on PATH from a previous lease's install, the template image, or another
|
|
659
|
+
// path entry. Re-run the detect probe (when one is configured) so a transient
|
|
660
|
+
// install failure does not abort the agent run when the binary is reachable.
|
|
661
|
+
const installFailed = result.timedOut || (result.exitCode ?? 0) !== 0;
|
|
662
|
+
if (!installFailed) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (detectCommand) {
|
|
666
|
+
const recheck = await runAdapterExecutionTargetShellCommand(input.runId, input.target, `command -v ${shellQuote(detectCommand)} >/dev/null 2>&1`, {
|
|
667
|
+
cwd: input.cwd,
|
|
668
|
+
env: input.env,
|
|
669
|
+
timeoutSec: input.timeoutSec,
|
|
670
|
+
graceSec: input.graceSec,
|
|
671
|
+
});
|
|
672
|
+
if (!recheck.timedOut && recheck.exitCode === 0) {
|
|
673
|
+
if (input.onLog) {
|
|
674
|
+
const reason = result.timedOut ? "timed out" : `exited ${result.exitCode ?? "?"}`;
|
|
675
|
+
await input.onLog("stderr", `[paperclip] Install command ${reason} (${installCommand}) but ${detectCommand} is on PATH; continuing.\n`);
|
|
676
|
+
}
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (result.timedOut) {
|
|
681
|
+
throw new Error(`Timed out while installing the adapter runtime command via: ${installCommand}`);
|
|
682
|
+
}
|
|
683
|
+
throw new Error(`Failed to install the adapter runtime command via: ${installCommand}`);
|
|
684
|
+
}
|
|
685
|
+
export async function ensureAdapterExecutionTargetFile(runId, target, filePath, options) {
|
|
686
|
+
await runAdapterExecutionTargetShellCommand(runId, target, `mkdir -p ${shellQuote(path.posix.dirname(filePath))} && : > ${shellQuote(filePath)}`, options);
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Ensure a working directory exists (and is a directory) on the execution target.
|
|
690
|
+
*
|
|
691
|
+
* For local targets this delegates to the local `ensureAbsoluteDirectory` helper
|
|
692
|
+
* (Node fs). For remote (SSH/sandbox) targets it shells out and runs
|
|
693
|
+
* `mkdir -p` (when allowed) followed by a `[ -d ]` check so the result reflects
|
|
694
|
+
* the directory state inside the environment, not on the Paperclip host.
|
|
695
|
+
*
|
|
696
|
+
* Throws an Error with a human-readable message on failure.
|
|
697
|
+
*/
|
|
698
|
+
export async function ensureAdapterExecutionTargetDirectory(runId, target, cwd, options) {
|
|
699
|
+
const createIfMissing = options.createIfMissing ?? false;
|
|
700
|
+
if (!target || target.kind === "local") {
|
|
701
|
+
const { ensureAbsoluteDirectory } = await import("./server-utils.js");
|
|
702
|
+
await ensureAbsoluteDirectory(cwd, { createIfMissing });
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
// Remote (SSH or sandbox): both expect POSIX absolute paths inside the env.
|
|
706
|
+
if (!cwd.startsWith("/")) {
|
|
707
|
+
throw new Error(`Working directory must be an absolute POSIX path on the remote target: "${cwd}"`);
|
|
708
|
+
}
|
|
709
|
+
const quoted = shellQuote(cwd);
|
|
710
|
+
const script = createIfMissing
|
|
711
|
+
? `mkdir -p ${quoted} && [ -d ${quoted} ]`
|
|
712
|
+
: `[ -d ${quoted} ]`;
|
|
713
|
+
const result = await runAdapterExecutionTargetShellCommand(runId, target, script, {
|
|
714
|
+
cwd: target.kind === "remote" ? target.remoteCwd : cwd,
|
|
715
|
+
env: options.env,
|
|
716
|
+
timeoutSec: options.timeoutSec ?? 15,
|
|
717
|
+
graceSec: options.graceSec ?? 5,
|
|
718
|
+
onLog: options.onLog,
|
|
719
|
+
});
|
|
720
|
+
if (result.timedOut) {
|
|
721
|
+
throw new Error(`Timed out checking working directory on remote target: "${cwd}"`);
|
|
722
|
+
}
|
|
723
|
+
if ((result.exitCode ?? 1) !== 0) {
|
|
724
|
+
const detail = (result.stderr || result.stdout || "").trim();
|
|
725
|
+
if (createIfMissing) {
|
|
726
|
+
throw new Error(`Could not create working directory "${cwd}" on remote target${detail ? `: ${detail}` : "."}`);
|
|
727
|
+
}
|
|
728
|
+
throw new Error(`Working directory does not exist on remote target: "${cwd}"${detail ? ` (${detail})` : ""}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
export function adapterExecutionTargetSessionIdentity(target) {
|
|
732
|
+
if (!target || target.kind === "local")
|
|
733
|
+
return null;
|
|
734
|
+
if (target.transport === "ssh")
|
|
735
|
+
return buildRemoteExecutionSessionIdentity(target.spec);
|
|
736
|
+
return {
|
|
737
|
+
transport: "sandbox",
|
|
738
|
+
providerKey: target.providerKey ?? null,
|
|
739
|
+
environmentId: target.environmentId ?? null,
|
|
740
|
+
leaseId: target.leaseId ?? null,
|
|
741
|
+
remoteCwd: target.remoteCwd,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
export function adapterExecutionTargetSessionMatches(saved, target) {
|
|
745
|
+
if (!target || target.kind === "local") {
|
|
746
|
+
return Object.keys(parseObject(saved)).length === 0;
|
|
747
|
+
}
|
|
748
|
+
if (target.transport === "ssh")
|
|
749
|
+
return remoteExecutionSessionMatches(saved, target.spec);
|
|
750
|
+
const current = adapterExecutionTargetSessionIdentity(target);
|
|
751
|
+
const parsedSaved = parseObject(saved);
|
|
752
|
+
return (readStringMeta(parsedSaved, "transport") === current?.transport &&
|
|
753
|
+
readStringMeta(parsedSaved, "providerKey") === current?.providerKey &&
|
|
754
|
+
readStringMeta(parsedSaved, "environmentId") === current?.environmentId &&
|
|
755
|
+
readStringMeta(parsedSaved, "leaseId") === current?.leaseId &&
|
|
756
|
+
readStringMeta(parsedSaved, "remoteCwd") === current?.remoteCwd);
|
|
757
|
+
}
|
|
758
|
+
export function parseAdapterExecutionTarget(value) {
|
|
759
|
+
const parsed = parseObject(value);
|
|
760
|
+
const kind = readStringMeta(parsed, "kind");
|
|
761
|
+
if (kind === "local") {
|
|
762
|
+
return {
|
|
763
|
+
kind: "local",
|
|
764
|
+
environmentId: readStringMeta(parsed, "environmentId"),
|
|
765
|
+
leaseId: readStringMeta(parsed, "leaseId"),
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
if (kind === "remote" && readStringMeta(parsed, "transport") === "ssh") {
|
|
769
|
+
const spec = parseSshRemoteExecutionSpec(parseObject(parsed.spec));
|
|
770
|
+
if (!spec)
|
|
771
|
+
return null;
|
|
772
|
+
return {
|
|
773
|
+
kind: "remote",
|
|
774
|
+
transport: "ssh",
|
|
775
|
+
environmentId: readStringMeta(parsed, "environmentId"),
|
|
776
|
+
leaseId: readStringMeta(parsed, "leaseId"),
|
|
777
|
+
remoteCwd: spec.remoteCwd,
|
|
778
|
+
spec,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
if (kind === "remote" && readStringMeta(parsed, "transport") === "sandbox") {
|
|
782
|
+
const remoteCwd = readStringMeta(parsed, "remoteCwd");
|
|
783
|
+
if (!remoteCwd)
|
|
784
|
+
return null;
|
|
785
|
+
const effectiveCapabilities = parseEffectiveExecutionCapabilities(parsed.effectiveCapabilities);
|
|
786
|
+
return {
|
|
787
|
+
kind: "remote",
|
|
788
|
+
transport: "sandbox",
|
|
789
|
+
providerKey: readStringMeta(parsed, "providerKey"),
|
|
790
|
+
environmentId: readStringMeta(parsed, "environmentId"),
|
|
791
|
+
leaseId: readStringMeta(parsed, "leaseId"),
|
|
792
|
+
remoteCwd,
|
|
793
|
+
timeoutMs: typeof parsed.timeoutMs === "number" ? parsed.timeoutMs : null,
|
|
794
|
+
streamRunLogs: typeof parsed.streamRunLogs === "boolean" ? parsed.streamRunLogs : null,
|
|
795
|
+
// Fail closed: only the literal `true` reads as a grant. An absent field
|
|
796
|
+
// or any other value parses as no grant, so a round-trip never invents one.
|
|
797
|
+
enableSandboxDuplexBridge: parsed.enableSandboxDuplexBridge === true,
|
|
798
|
+
...(effectiveCapabilities ? { effectiveCapabilities } : {}),
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
export function adapterExecutionTargetFromRemoteExecution(remoteExecution, metadata = {}) {
|
|
804
|
+
const parsed = parseObject(remoteExecution);
|
|
805
|
+
const ssh = parseSshRemoteExecutionSpec(parsed);
|
|
806
|
+
if (ssh) {
|
|
807
|
+
return {
|
|
808
|
+
kind: "remote",
|
|
809
|
+
transport: "ssh",
|
|
810
|
+
environmentId: metadata.environmentId ?? null,
|
|
811
|
+
leaseId: metadata.leaseId ?? null,
|
|
812
|
+
remoteCwd: ssh.remoteCwd,
|
|
813
|
+
spec: ssh,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
export function readAdapterExecutionTarget(input) {
|
|
819
|
+
if (isAdapterExecutionTargetInstance(input.executionTarget)) {
|
|
820
|
+
return input.executionTarget;
|
|
821
|
+
}
|
|
822
|
+
return (parseAdapterExecutionTarget(input.executionTarget) ??
|
|
823
|
+
adapterExecutionTargetFromRemoteExecution(input.legacyRemoteExecution));
|
|
824
|
+
}
|
|
825
|
+
export async function prepareAdapterExecutionTargetRuntime(input) {
|
|
826
|
+
const target = input.target ?? { kind: "local" };
|
|
827
|
+
if (target.kind === "local") {
|
|
828
|
+
return {
|
|
829
|
+
target,
|
|
830
|
+
workspaceRemoteDir: null,
|
|
831
|
+
runtimeRootDir: null,
|
|
832
|
+
assetDirs: {},
|
|
833
|
+
additionalSourceDirs: {},
|
|
834
|
+
additionalSourceFailures: [],
|
|
835
|
+
workspaceSyncSnapshot: null,
|
|
836
|
+
restoreWorkspace: async () => { },
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
if (target.transport === "ssh") {
|
|
840
|
+
const prepared = await prepareRemoteManagedRuntime({
|
|
841
|
+
spec: target.spec,
|
|
842
|
+
runId: input.runId,
|
|
843
|
+
adapterKey: input.adapterKey,
|
|
844
|
+
workspaceLocalDir: input.workspaceLocalDir,
|
|
845
|
+
workspaceRemoteDir: input.workspaceRemoteDir,
|
|
846
|
+
syncWorkspace: input.syncWorkspace,
|
|
847
|
+
assets: input.assets,
|
|
848
|
+
additionalSources: input.additionalSources,
|
|
849
|
+
onProgress: input.onProgress,
|
|
850
|
+
});
|
|
851
|
+
return {
|
|
852
|
+
target,
|
|
853
|
+
workspaceRemoteDir: prepared.workspaceRemoteDir,
|
|
854
|
+
runtimeRootDir: prepared.runtimeRootDir,
|
|
855
|
+
assetDirs: prepared.assetDirs,
|
|
856
|
+
additionalSourceDirs: prepared.additionalSourceDirs,
|
|
857
|
+
// The SSH transport does not stage referenced projects (it is out of scope), so it never
|
|
858
|
+
// reports a per-project staging failure.
|
|
859
|
+
additionalSourceFailures: [],
|
|
860
|
+
workspaceSyncSnapshot: null,
|
|
861
|
+
restoreWorkspace: prepared.restoreWorkspace,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
const prepared = await prepareCommandManagedRuntime({
|
|
865
|
+
runner: requireSandboxRunner(target),
|
|
866
|
+
spec: {
|
|
867
|
+
providerKey: target.providerKey,
|
|
868
|
+
shellCommand: target.shellCommand,
|
|
869
|
+
leaseId: target.leaseId,
|
|
870
|
+
remoteCwd: target.remoteCwd,
|
|
871
|
+
timeoutMs: input.timeoutSec && input.timeoutSec > 0
|
|
872
|
+
? input.timeoutSec * 1000
|
|
873
|
+
: target.timeoutMs,
|
|
874
|
+
},
|
|
875
|
+
adapterKey: input.adapterKey,
|
|
876
|
+
workspaceLocalDir: input.workspaceLocalDir,
|
|
877
|
+
workspaceRemoteDir: input.workspaceRemoteDir,
|
|
878
|
+
syncWorkspace: input.syncWorkspace,
|
|
879
|
+
workspaceInboundMode: input.workspaceInboundMode,
|
|
880
|
+
workspaceDurableSeed: input.workspaceDurableSeed,
|
|
881
|
+
workspaceBaseline: input.workspaceBaseline,
|
|
882
|
+
workspaceGitSnapshot: input.workspaceGitSnapshot,
|
|
883
|
+
workspaceExclude: input.workspaceExclude,
|
|
884
|
+
preserveAbsentOnRestore: input.preserveAbsentOnRestore,
|
|
885
|
+
assets: input.assets,
|
|
886
|
+
additionalSources: input.additionalSources,
|
|
887
|
+
installCommand: input.installCommand,
|
|
888
|
+
detectCommand: input.detectCommand,
|
|
889
|
+
onProgress: input.onProgress,
|
|
890
|
+
onRuntimeProgress: input.onRuntimeProgress,
|
|
891
|
+
runtimeSpan: input.runtimeSpan,
|
|
892
|
+
});
|
|
893
|
+
return {
|
|
894
|
+
target,
|
|
895
|
+
workspaceRemoteDir: prepared.workspaceRemoteDir,
|
|
896
|
+
runtimeRootDir: prepared.runtimeRootDir,
|
|
897
|
+
assetDirs: prepared.assetDirs,
|
|
898
|
+
additionalSourceDirs: prepared.additionalSourceDirs,
|
|
899
|
+
additionalSourceFailures: prepared.additionalSourceFailures,
|
|
900
|
+
workspaceSyncSnapshot: prepared.workspaceSyncSnapshot,
|
|
901
|
+
restoreWorkspace: prepared.restoreWorkspace,
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
export function runtimeAssetDir(prepared, key, fallbackRemoteCwd) {
|
|
905
|
+
return prepared.assetDirs[key] ?? path.posix.join(fallbackRemoteCwd, ".paperclip-runtime", key);
|
|
906
|
+
}
|
|
907
|
+
function githubOperationLauncherDirectory(input) {
|
|
908
|
+
// Only controller-generated run IDs may name a removable directory.
|
|
909
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(input.runId))
|
|
910
|
+
throw new Error("Invalid GitHub launcher run ID");
|
|
911
|
+
return input.target?.kind === "remote"
|
|
912
|
+
? path.posix.join(input.target.remoteCwd, ".paperclip-runtime", "github", input.runId)
|
|
913
|
+
: path.join(os.tmpdir(), "paperclip-github-runtime", input.runId);
|
|
914
|
+
}
|
|
915
|
+
/** Call only after execution settles, before releasing its remote environment lease. */
|
|
916
|
+
export async function cleanupGitHubOperationLaunchers(input) {
|
|
917
|
+
const directory = githubOperationLauncherDirectory(input);
|
|
918
|
+
if (input.target?.kind === "remote") {
|
|
919
|
+
const result = await adapterExecutionTargetCommandRunner(input.target).execute({
|
|
920
|
+
command: "sh", args: ["-c", `rm -rf -- ${shellQuote(directory)}`],
|
|
921
|
+
cwd: input.target.remoteCwd, timeoutMs: 5_000,
|
|
922
|
+
});
|
|
923
|
+
if (result.exitCode !== 0)
|
|
924
|
+
throw new Error("Could not clean managed GitHub launchers");
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
async function githubOperationLauncherBasePath(target, env) {
|
|
931
|
+
if (!target)
|
|
932
|
+
return env.PATH || process.env.PATH || "/usr/bin:/bin";
|
|
933
|
+
const configuredPath = sanitizeRemoteExecutionEnv(env).PATH;
|
|
934
|
+
if (configuredPath !== undefined)
|
|
935
|
+
return configuredPath;
|
|
936
|
+
// The provider owns login/profile setup. Query its effective PATH before
|
|
937
|
+
// staging BASH_ENV, rather than substituting the controller's toolchain or
|
|
938
|
+
// a minimal PATH that hides legacy NVM/user-local agent installations.
|
|
939
|
+
const result = await adapterExecutionTargetCommandRunner(target).execute({
|
|
940
|
+
command: "sh",
|
|
941
|
+
args: ["-c", "printf '\\000%s\\000' \"$PATH\""],
|
|
942
|
+
cwd: target.remoteCwd,
|
|
943
|
+
timeoutMs: 15_000,
|
|
944
|
+
});
|
|
945
|
+
// Frame the value so login banners cannot become executable search paths.
|
|
946
|
+
const remotePath = result.stdout.match(/\0([^\0]+)\0/)?.[1];
|
|
947
|
+
if (result.timedOut || result.exitCode !== 0 || !remotePath) {
|
|
948
|
+
throw new Error("Could not resolve remote PATH for managed GitHub launchers");
|
|
949
|
+
}
|
|
950
|
+
return remotePath;
|
|
951
|
+
}
|
|
952
|
+
/** Read only execution-target Git context; never import the controller's credentials into SSH. */
|
|
953
|
+
export async function prepareGitHubExecutionEnvironment(input) {
|
|
954
|
+
const script = String.raw `
|
|
955
|
+
const fs = require('node:fs');
|
|
956
|
+
const path = require('node:path');
|
|
957
|
+
const cp = require('node:child_process');
|
|
958
|
+
const env = {};
|
|
959
|
+
env.PAPERCLIP_RUNNER_NETWORK_ROOTS = JSON.stringify(['/etc/resolv.conf','/etc/hosts','/etc/nsswitch.conf','/etc/ssl/certs','/etc/ssl/cert.pem'].flatMap(p => { try { return [fs.realpathSync(p)]; } catch { return []; } }));
|
|
960
|
+
if (process.argv[1] === 'host') {
|
|
961
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
962
|
+
if (/^(GH_TOKEN|GITHUB_TOKEN|GH_ENTERPRISE_TOKEN|GITHUB_ENTERPRISE_TOKEN|PAPERCLIP_GIT_TOKEN|GH_CONFIG_DIR|GIT_CONFIG_(GLOBAL|SYSTEM|NOSYSTEM|COUNT|KEY_\d+|VALUE_\d+)|GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL)|GIT_ASKPASS|SSH_ASKPASS|SSH_AUTH_SOCK|GIT_SSH_COMMAND|GIT_SSH)$/.test(key)) env[key] = value;
|
|
963
|
+
}
|
|
964
|
+
env.PAPERCLIP_GITHUB_HOST_HOME = process.env.HOME || '';
|
|
965
|
+
env.GH_CONFIG_DIR ||= path.join(process.env.XDG_CONFIG_HOME || path.join(process.env.HOME || '', '.config'), 'gh');
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
const top = cp.execFileSync('git', ['rev-parse', '--show-toplevel'], {encoding:'utf8',stdio:['ignore','pipe','ignore']}).trim();
|
|
969
|
+
if (fs.realpathSync(top) === fs.realpathSync(process.cwd())) {
|
|
970
|
+
env.PAPERCLIP_GIT_METADATA_ROOTS = JSON.stringify(cp.execFileSync('git', ['rev-parse','--path-format=absolute','--git-common-dir','--git-dir'], {encoding:'utf8',stdio:['ignore','pipe','ignore']}).trim().split('\n').map(p => fs.realpathSync(p)));
|
|
971
|
+
}
|
|
972
|
+
} catch {}
|
|
973
|
+
process.stdout.write("\0" + JSON.stringify(env) + "\0");
|
|
974
|
+
`;
|
|
975
|
+
const args = ["-e", script, input.hostCredentials ? "host" : "managed"];
|
|
976
|
+
const remote = input.target?.kind === "remote" ? input.target : null;
|
|
977
|
+
let discovered;
|
|
978
|
+
if (remote) {
|
|
979
|
+
// A legacy SSH host may run a standalone agent binary without Node. Use
|
|
980
|
+
// only the shell and Git, and emit bounded, NUL-framed environment records.
|
|
981
|
+
const probe = String.raw `
|
|
982
|
+
printf '\0PAPERCLIP_GIT_CONTEXT_V1\0'
|
|
983
|
+
if [ "$1" = host ]; then
|
|
984
|
+
for key in GH_TOKEN GITHUB_TOKEN GH_ENTERPRISE_TOKEN GITHUB_ENTERPRISE_TOKEN PAPERCLIP_GIT_TOKEN GH_CONFIG_DIR GIT_CONFIG_GLOBAL GIT_CONFIG_SYSTEM GIT_CONFIG_NOSYSTEM GIT_CONFIG_COUNT GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL GIT_ASKPASS SSH_ASKPASS SSH_AUTH_SOCK GIT_SSH_COMMAND GIT_SSH; do
|
|
985
|
+
eval 'value=${"$"}{'"$key"'-}'
|
|
986
|
+
[ -z "$value" ] || printf '%s\0%s\0' "$key" "$value"
|
|
987
|
+
done
|
|
988
|
+
index=0
|
|
989
|
+
while [ "$index" -lt 32 ]; do
|
|
990
|
+
for prefix in GIT_CONFIG_KEY_ GIT_CONFIG_VALUE_; do
|
|
991
|
+
key="$prefix$index"
|
|
992
|
+
eval 'value=${"$"}{'"$key"'-}'
|
|
993
|
+
[ -z "$value" ] || printf '%s\0%s\0' "$key" "$value"
|
|
994
|
+
done
|
|
995
|
+
index=$((index + 1))
|
|
996
|
+
done
|
|
997
|
+
printf 'PAPERCLIP_GITHUB_HOST_HOME\0%s\0' "$HOME"
|
|
998
|
+
printf 'GH_CONFIG_DIR\0%s\0' "${"$"}{GH_CONFIG_DIR:-${"$"}{XDG_CONFIG_HOME:-$HOME/.config}/gh}"
|
|
999
|
+
fi
|
|
1000
|
+
for file in /etc/resolv.conf /etc/hosts /etc/nsswitch.conf /etc/ssl/certs /etc/ssl/cert.pem; do
|
|
1001
|
+
index=0
|
|
1002
|
+
while [ -L "$file" ] && [ "$index" -lt 40 ]; do
|
|
1003
|
+
target=$(readlink "$file") || break
|
|
1004
|
+
case "$target" in /*) file="$target" ;; *) file="$(dirname "$file")/$target" ;; esac
|
|
1005
|
+
index=$((index + 1))
|
|
1006
|
+
done
|
|
1007
|
+
if [ -e "$file" ]; then
|
|
1008
|
+
parent=$(cd "$(dirname "$file")" && pwd -P) || continue
|
|
1009
|
+
printf 'PAPERCLIP_RUNNER_NETWORK_ROOT\0%s\0' "$parent/$(basename "$file")"
|
|
1010
|
+
fi
|
|
1011
|
+
done
|
|
1012
|
+
cwd=$(pwd -P)
|
|
1013
|
+
top=$(git rev-parse --show-toplevel 2>/dev/null) || top=
|
|
1014
|
+
if [ -n "$top" ] && [ "$(cd "$top" && pwd -P)" = "$cwd" ]; then
|
|
1015
|
+
for kind in --git-common-dir --git-dir; do
|
|
1016
|
+
root=$(git rev-parse --path-format=absolute "$kind" 2>/dev/null) || continue
|
|
1017
|
+
root=$(cd "$root" && pwd -P) || continue
|
|
1018
|
+
printf 'PAPERCLIP_GIT_METADATA_ROOT\0%s\0' "$root"
|
|
1019
|
+
done
|
|
1020
|
+
fi
|
|
1021
|
+
printf '\0PAPERCLIP_GIT_CONTEXT_END\0'
|
|
1022
|
+
`;
|
|
1023
|
+
const result = await adapterExecutionTargetCommandRunner(remote).execute({
|
|
1024
|
+
command: "sh", args: ["-c", probe, "paperclip-git-context", input.hostCredentials ? "host" : "managed"],
|
|
1025
|
+
// The caller's cwd belongs to the controller. Copied sandbox/SSH
|
|
1026
|
+
// workspaces can live at a different path on the execution target.
|
|
1027
|
+
cwd: remote.remoteCwd, timeoutMs: 15_000,
|
|
1028
|
+
});
|
|
1029
|
+
if (result.exitCode !== 0)
|
|
1030
|
+
throw new Error("Could not read execution-target Git context");
|
|
1031
|
+
const payload = result.stdout.split("\0PAPERCLIP_GIT_CONTEXT_V1\0")[1]?.split("\0PAPERCLIP_GIT_CONTEXT_END\0")[0];
|
|
1032
|
+
if (payload === undefined)
|
|
1033
|
+
throw new Error("Could not read execution-target Git context");
|
|
1034
|
+
discovered = {};
|
|
1035
|
+
const records = payload.split("\0");
|
|
1036
|
+
const roots = [];
|
|
1037
|
+
const networkRoots = [];
|
|
1038
|
+
for (let index = 0; index + 1 < records.length; index += 2) {
|
|
1039
|
+
const key = records[index];
|
|
1040
|
+
const value = records[index + 1];
|
|
1041
|
+
if (key === "PAPERCLIP_GIT_METADATA_ROOT")
|
|
1042
|
+
roots.push(value);
|
|
1043
|
+
else if (key === "PAPERCLIP_RUNNER_NETWORK_ROOT")
|
|
1044
|
+
networkRoots.push(value);
|
|
1045
|
+
else
|
|
1046
|
+
discovered[key] = value;
|
|
1047
|
+
}
|
|
1048
|
+
discovered.PAPERCLIP_GIT_METADATA_ROOTS = JSON.stringify([...new Set(roots)]);
|
|
1049
|
+
discovered.PAPERCLIP_RUNNER_NETWORK_ROOTS = JSON.stringify([...new Set(networkRoots)]);
|
|
1050
|
+
}
|
|
1051
|
+
else {
|
|
1052
|
+
const result = await promisify(execFile)(process.execPath, args, { cwd: input.cwd, timeout: 15_000, maxBuffer: 1024 * 1024 });
|
|
1053
|
+
try {
|
|
1054
|
+
discovered = JSON.parse(result.stdout.split("\0")[1] ?? "");
|
|
1055
|
+
}
|
|
1056
|
+
catch {
|
|
1057
|
+
throw new Error("Could not read execution-target Git context");
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
// Controller-derived roots and mode must not be replaced by agent bindings.
|
|
1061
|
+
return { ...discovered, ...input.env,
|
|
1062
|
+
...(input.hostCredentials ? { PAPERCLIP_GITHUB_HOST_HOME: discovered.PAPERCLIP_GITHUB_HOST_HOME } : {}),
|
|
1063
|
+
PAPERCLIP_GIT_METADATA_ROOTS: discovered.PAPERCLIP_GIT_METADATA_ROOTS ?? "[]",
|
|
1064
|
+
PAPERCLIP_RUNNER_NETWORK_ROOTS: discovered.PAPERCLIP_RUNNER_NETWORK_ROOTS ?? "[]",
|
|
1065
|
+
PAPERCLIP_GITHUB_AUTH_MODE: input.hostCredentials ? "host" : "managed",
|
|
1066
|
+
PAPERCLIP_RUNNER_NETWORK_ACCESS: input.networkAccess ? "enabled" : "disabled",
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
/** Stage token-free launchers next to the execution, not in shared global Git config. */
|
|
1070
|
+
export async function prepareGitHubOperationLaunchers(input) {
|
|
1071
|
+
const remote = input.target?.kind === "remote" ? input.target : null;
|
|
1072
|
+
const directory = githubOperationLauncherDirectory(input);
|
|
1073
|
+
const configDirectory = path.posix.join(directory, "gh-config");
|
|
1074
|
+
const basePath = await githubOperationLauncherBasePath(remote, input.env);
|
|
1075
|
+
const managedPath = basePath ? `${directory}:${basePath}` : directory;
|
|
1076
|
+
// Login shells may reorder PATH through /etc/profile or path_helper. Restore
|
|
1077
|
+
// the managed launchers after startup without loading a host user's profile.
|
|
1078
|
+
// Empty merge overrides clear host identity before launch, but Git treats
|
|
1079
|
+
// them as an explicit empty author. Remove them once the shell has inherited
|
|
1080
|
+
// its final environment; preserve nonempty per-operation identity values.
|
|
1081
|
+
const clearEmptyGitIdentity = ["GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL"]
|
|
1082
|
+
.map((key) => `if [ -z "\${${key}-}" ]; then unset ${key}; fi\n`)
|
|
1083
|
+
.join("");
|
|
1084
|
+
const profile = `export PATH=${shellQuote(managedPath)}\n${clearEmptyGitIdentity}`;
|
|
1085
|
+
const files = Object.fromEntries([
|
|
1086
|
+
// Remote launchers live beneath the checkout. Pin their own package scope
|
|
1087
|
+
// so an enclosing project's "type": "module" cannot reinterpret require().
|
|
1088
|
+
["package.json", '{"type":"commonjs"}\n'],
|
|
1089
|
+
...["git", "gh"].map((name) => [name, githubLauncherSource()]),
|
|
1090
|
+
...[".zshenv", ".zprofile", ".zshrc", ".bash_profile", ".bashrc", ".profile"].map((name) => [name, profile]),
|
|
1091
|
+
]);
|
|
1092
|
+
if (remote) {
|
|
1093
|
+
const runner = adapterExecutionTargetCommandRunner(remote);
|
|
1094
|
+
for (const [program, body] of Object.entries(files)) {
|
|
1095
|
+
await syncRemoteTextFileWithHashSkip({
|
|
1096
|
+
runner, remoteCwd: remote.remoteCwd, remoteDir: directory,
|
|
1097
|
+
remotePath: path.posix.join(directory, program), body,
|
|
1098
|
+
label: "GitHub operation launcher", action: "stage GitHub operation launcher",
|
|
1099
|
+
lockDir: path.posix.join(directory, `.${program}.lock`),
|
|
1100
|
+
timeoutMs: 15_000, shellCommand: adapterExecutionTargetShellCommand(remote),
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
const permissions = await runner.execute({ command: "sh", args: ["-c", `chmod 700 ${shellQuote(directory)}/git ${shellQuote(directory)}/gh && mkdir -p ${shellQuote(configDirectory)}`], cwd: remote.remoteCwd, timeoutMs: 15_000 });
|
|
1104
|
+
if (permissions.exitCode !== 0)
|
|
1105
|
+
throw new Error("Could not prepare managed GitHub launchers");
|
|
1106
|
+
}
|
|
1107
|
+
else {
|
|
1108
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
1109
|
+
await fs.mkdir(configDirectory, { recursive: true, mode: 0o700 });
|
|
1110
|
+
for (const [program, body] of Object.entries(files))
|
|
1111
|
+
await fs.writeFile(path.join(directory, program), body, { mode: 0o700 });
|
|
1112
|
+
}
|
|
1113
|
+
return { ...input.env, PATH: managedPath, ZDOTDIR: directory, BASH_ENV: `${directory}/.bashrc`,
|
|
1114
|
+
GH_CONFIG_DIR: configDirectory, PAPERCLIP_GITHUB_LAUNCHER_DIR: directory };
|
|
1115
|
+
}
|
|
1116
|
+
function buildBridgeResponseHeaders(response) {
|
|
1117
|
+
const out = {};
|
|
1118
|
+
// Keep `x-paperclip-bridge-outcome` in this list. The host marks a
|
|
1119
|
+
// possibly-committed mutation with the `indeterminate` outcome. The in-sandbox
|
|
1120
|
+
// server reads that header to map the 504 to a terminal 409. If the forward
|
|
1121
|
+
// drops the header, the server keeps the retryable 504 and a caller that
|
|
1122
|
+
// retries 5xx can repeat a mutation that already committed.
|
|
1123
|
+
for (const key of ["content-type", "etag", "last-modified", "x-paperclip-bridge-outcome"]) {
|
|
1124
|
+
const value = response.headers.get(key);
|
|
1125
|
+
if (value && value.trim().length > 0)
|
|
1126
|
+
out[key] = value.trim();
|
|
1127
|
+
}
|
|
1128
|
+
return out;
|
|
1129
|
+
}
|
|
1130
|
+
function buildBridgeForwardUrl(baseUrl, request) {
|
|
1131
|
+
const url = new URL(request.path, baseUrl);
|
|
1132
|
+
const query = request.query.trim();
|
|
1133
|
+
url.search = query.startsWith("?") ? query.slice(1) : query;
|
|
1134
|
+
return url;
|
|
1135
|
+
}
|
|
1136
|
+
function bridgeResponseBodyLimitError(maxBodyBytes) {
|
|
1137
|
+
return new Error(`Bridge response body exceeded the configured size limit of ${maxBodyBytes} bytes.`);
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Read the forward response body into a `Buffer`, with no text decoding. The
|
|
1141
|
+
* per-request `maxBodyBytes` limit rejects a body larger than the configured
|
|
1142
|
+
* per-request ceiling.
|
|
1143
|
+
*
|
|
1144
|
+
* When the caller passes a `reservation`, this reserves each chunk's bytes
|
|
1145
|
+
* against it immediately after `reader.read()` yields the chunk, and before
|
|
1146
|
+
* `Buffer.from(value)` copies it — the allocation happens inside that
|
|
1147
|
+
* expression, so reserving only before the later `chunks.push` would let the
|
|
1148
|
+
* copy happen first. It also reserves the concatenated buffer's own byte
|
|
1149
|
+
* count before `Buffer.concat` allocates it: the chunk array and the
|
|
1150
|
+
* concatenated buffer are two separate live copies. A denied reservation
|
|
1151
|
+
* cancels the reader and throws {@link BridgeProcessCapacityError}, copying
|
|
1152
|
+
* no further chunk. This function never releases the reservation; the
|
|
1153
|
+
* stream owner that created it does, once the whole forward call settles.
|
|
1154
|
+
*
|
|
1155
|
+
* With no `reservation`, this function enforces only the one request's own
|
|
1156
|
+
* ceiling, exactly as it did before this parameter existed — the queue
|
|
1157
|
+
* transport calls it with no reservation, and its behavior must not change.
|
|
1158
|
+
*/
|
|
1159
|
+
async function readBridgeForwardResponseBody(response, maxBodyBytes, reservation) {
|
|
1160
|
+
const rawContentLength = response.headers.get("content-length");
|
|
1161
|
+
if (rawContentLength) {
|
|
1162
|
+
const contentLength = Number.parseInt(rawContentLength, 10);
|
|
1163
|
+
if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {
|
|
1164
|
+
throw bridgeResponseBodyLimitError(maxBodyBytes);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
if (!response.body) {
|
|
1168
|
+
return Buffer.alloc(0);
|
|
1169
|
+
}
|
|
1170
|
+
const reader = response.body.getReader();
|
|
1171
|
+
const chunks = [];
|
|
1172
|
+
let totalBytes = 0;
|
|
1173
|
+
while (true) {
|
|
1174
|
+
const { done, value } = await reader.read();
|
|
1175
|
+
if (done)
|
|
1176
|
+
break;
|
|
1177
|
+
if (!value)
|
|
1178
|
+
continue;
|
|
1179
|
+
const chunkBytes = value.byteLength;
|
|
1180
|
+
totalBytes += chunkBytes;
|
|
1181
|
+
if (totalBytes > maxBodyBytes) {
|
|
1182
|
+
await reader.cancel().catch(() => undefined);
|
|
1183
|
+
throw bridgeResponseBodyLimitError(maxBodyBytes);
|
|
1184
|
+
}
|
|
1185
|
+
if (reservation && !reservation.reserve(chunkBytes)) {
|
|
1186
|
+
await reader.cancel().catch(() => undefined);
|
|
1187
|
+
throw new BridgeProcessCapacityError();
|
|
1188
|
+
}
|
|
1189
|
+
chunks.push(Buffer.from(value));
|
|
1190
|
+
}
|
|
1191
|
+
if (reservation && !reservation.reserve(totalBytes)) {
|
|
1192
|
+
await reader.cancel().catch(() => undefined);
|
|
1193
|
+
throw new BridgeProcessCapacityError();
|
|
1194
|
+
}
|
|
1195
|
+
return Buffer.concat(chunks, totalBytes);
|
|
1196
|
+
}
|
|
1197
|
+
const PROCESS_SESSION_PROXY_SCRIPT = "paperclip-process-session-proxy.mjs";
|
|
1198
|
+
const PROCESS_SESSION_REMOTE_SCRIPT = "paperclip-process-session-remote.mjs";
|
|
1199
|
+
// The streamed variant writes its output frames to stdout, so it rides a
|
|
1200
|
+
// separate remote path. A sandbox can hold both scripts without the content
|
|
1201
|
+
// hash-skip gate thrashing when a run switches output mode.
|
|
1202
|
+
const PROCESS_SESSION_REMOTE_STREAM_SCRIPT = "paperclip-process-session-remote-stream.mjs";
|
|
1203
|
+
const PROCESS_SESSION_AUTH_TIMEOUT_MS = 5_000;
|
|
1204
|
+
// The bounded budget `stop()` waits for the wrapper's `shutdownAck` event
|
|
1205
|
+
// before it removes `sessionDir` unconditionally. The wrapper writes the
|
|
1206
|
+
// acknowledgement right after it arms its own kill timer, well before its
|
|
1207
|
+
// child actually exits, so this budget only needs to cover message delivery,
|
|
1208
|
+
// not the child's full shutdown.
|
|
1209
|
+
const DEFAULT_PROCESS_SESSION_SHUTDOWN_WAIT_MS = 3_000;
|
|
1210
|
+
function jsonLine(value) {
|
|
1211
|
+
return `${JSON.stringify(value)}\n`;
|
|
1212
|
+
}
|
|
1213
|
+
function splitJsonLines(buffer) {
|
|
1214
|
+
const parts = buffer.split(/\n/);
|
|
1215
|
+
return { lines: parts.slice(0, -1), rest: parts.at(-1) ?? "" };
|
|
1216
|
+
}
|
|
1217
|
+
async function writeProcessSessionProxyScript(dir, port, token) {
|
|
1218
|
+
await fs.mkdir(dir, { recursive: true });
|
|
1219
|
+
const proxyPath = path.join(dir, PROCESS_SESSION_PROXY_SCRIPT);
|
|
1220
|
+
await fs.writeFile(proxyPath, getProcessSessionProxySource({ port, token }), { mode: 0o700 });
|
|
1221
|
+
return proxyPath;
|
|
1222
|
+
}
|
|
1223
|
+
// Content-hash-skip the process-session remote script write, mirroring the
|
|
1224
|
+
// sandbox callback bridge entrypoint sha256 gate. The script is a static
|
|
1225
|
+
// Paperclip-authored `.mjs` that only changes when the build changes, so on a
|
|
1226
|
+
// warm start (same sandbox, script already present) the single sha-gate exec
|
|
1227
|
+
// skips the ~3-exec base64 upload entirely. `syncRemoteTextFileWithHashSkip`
|
|
1228
|
+
// fails loud on a check error rather than silently re-uploading.
|
|
1229
|
+
async function syncProcessSessionRemoteScript(input) {
|
|
1230
|
+
const { uploaded } = await syncRemoteTextFileWithHashSkip({
|
|
1231
|
+
runner: input.runner,
|
|
1232
|
+
remoteCwd: input.remoteCwd,
|
|
1233
|
+
remoteDir: input.remoteScriptDir,
|
|
1234
|
+
remotePath: input.remoteScriptPath,
|
|
1235
|
+
body: getProcessSessionRemoteSource({ outputToStdout: input.outputToStdout === true }),
|
|
1236
|
+
label: "Process session remote script",
|
|
1237
|
+
action: "sync process session remote script",
|
|
1238
|
+
lockDir: path.posix.join(input.remoteScriptDir, ".paperclip-process-session-script.lock"),
|
|
1239
|
+
timeoutMs: input.timeoutMs,
|
|
1240
|
+
shellCommand: input.shellCommand,
|
|
1241
|
+
});
|
|
1242
|
+
return { uploaded };
|
|
1243
|
+
}
|
|
1244
|
+
async function readRemoteJsonFiles(input) {
|
|
1245
|
+
const names = await input.client.listJsonFiles(input.dir);
|
|
1246
|
+
const out = [];
|
|
1247
|
+
for (const name of names) {
|
|
1248
|
+
const filePath = path.posix.join(input.dir, name);
|
|
1249
|
+
const body = await input.client.readTextFile(filePath);
|
|
1250
|
+
await input.client.remove(filePath).catch(() => undefined);
|
|
1251
|
+
out.push({ name, body });
|
|
1252
|
+
}
|
|
1253
|
+
return out;
|
|
1254
|
+
}
|
|
1255
|
+
async function waitForLocalServerListen(server) {
|
|
1256
|
+
await new Promise((resolve, reject) => {
|
|
1257
|
+
server.once("error", reject);
|
|
1258
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1259
|
+
server.off("error", reject);
|
|
1260
|
+
resolve();
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
const address = server.address();
|
|
1264
|
+
if (!address || typeof address === "string") {
|
|
1265
|
+
throw new Error("Process session bridge did not expose a TCP port.");
|
|
1266
|
+
}
|
|
1267
|
+
return address.port;
|
|
1268
|
+
}
|
|
1269
|
+
/** Span name that wraps the socket handler's one `writeTextFile` exec — one
|
|
1270
|
+
* outbound ACP message to the agent. */
|
|
1271
|
+
const AGENT_SESSION_SEND_INPUT_SPAN = "sandbox.agentSession.sendInput";
|
|
1272
|
+
/** Span name that wraps one 100 ms poll tick — `list`, then `read`+`remove` per
|
|
1273
|
+
* file found (`1 + 2n` execs). */
|
|
1274
|
+
const AGENT_SESSION_POLL_OUTPUT_SPAN = "sandbox.agentSession.pollOutput";
|
|
1275
|
+
export async function startAdapterExecutionTargetProcessSessionBridge(input) {
|
|
1276
|
+
if (!input.target || input.target.kind !== "remote" || input.target.transport !== "sandbox") {
|
|
1277
|
+
return null;
|
|
1278
|
+
}
|
|
1279
|
+
const target = input.target;
|
|
1280
|
+
const onLog = input.onLog ?? (async () => { });
|
|
1281
|
+
const runner = requireSandboxRunner(target);
|
|
1282
|
+
// Run one unit of run-time work under its named wrapper span when a span
|
|
1283
|
+
// runner is injected. Without a runner, run the work under the current run
|
|
1284
|
+
// parent, so the inner `sandbox.exec` spans parent to the live run span,
|
|
1285
|
+
// exactly like the earlier behavior.
|
|
1286
|
+
const runRuntimeWork = (name, work) => input.runtimeSpan
|
|
1287
|
+
? input.runtimeSpan(name, work)
|
|
1288
|
+
: runWithRuntimeParent(input.getRuntimeParentContext?.(), work);
|
|
1289
|
+
const shellCommand = preferredSandboxShell(target);
|
|
1290
|
+
const timeoutMs = typeof input.timeoutSec === "number" && Number.isFinite(input.timeoutSec) && input.timeoutSec > 0
|
|
1291
|
+
? Math.trunc(input.timeoutSec * 1000)
|
|
1292
|
+
: target.timeoutMs ?? undefined;
|
|
1293
|
+
const bridgeRuntimeDir = path.posix.join(input.runtimeRootDir?.trim() || path.posix.join(target.remoteCwd, ".paperclip-runtime", input.adapterKey), "process-sessions");
|
|
1294
|
+
const sessionId = randomUUID();
|
|
1295
|
+
const sessionDir = path.posix.join(bridgeRuntimeDir, sessionId);
|
|
1296
|
+
const stdinDir = path.posix.join(sessionDir, "stdin");
|
|
1297
|
+
const eventsDir = path.posix.join(sessionDir, "events");
|
|
1298
|
+
// The streamed wrapper writes its frames to stdout and rides a separate remote
|
|
1299
|
+
// path, so a warm sandbox can hold both wrapper scripts without the content
|
|
1300
|
+
// hash-skip gate thrashing when a run switches output mode.
|
|
1301
|
+
const streamOutput = input.streamOutputViaSession === true;
|
|
1302
|
+
const remoteScriptPath = path.posix.join(bridgeRuntimeDir, streamOutput ? PROCESS_SESSION_REMOTE_STREAM_SCRIPT : PROCESS_SESSION_REMOTE_SCRIPT);
|
|
1303
|
+
const client = createCommandManagedSandboxCallbackBridgeQueueClient({
|
|
1304
|
+
runner,
|
|
1305
|
+
remoteCwd: target.remoteCwd,
|
|
1306
|
+
timeoutMs,
|
|
1307
|
+
shellCommand,
|
|
1308
|
+
});
|
|
1309
|
+
// The launch exec below re-creates stdinDir and eventsDir with one `mkdir -p`,
|
|
1310
|
+
// and the remote script also creates them on start. No reader touches the two
|
|
1311
|
+
// directories before the launch exec runs, so upfront makeDir execs are redundant.
|
|
1312
|
+
await syncProcessSessionRemoteScript({
|
|
1313
|
+
runner,
|
|
1314
|
+
remoteCwd: target.remoteCwd,
|
|
1315
|
+
remoteScriptDir: bridgeRuntimeDir,
|
|
1316
|
+
remoteScriptPath,
|
|
1317
|
+
timeoutMs,
|
|
1318
|
+
shellCommand,
|
|
1319
|
+
outputToStdout: streamOutput,
|
|
1320
|
+
});
|
|
1321
|
+
// Resolve the launch env AFTER the env-independent setup above, so a caller
|
|
1322
|
+
// can defer it until an upstream dependency (e.g. the paperclip bridge's env)
|
|
1323
|
+
// is ready without blocking the dir/script setup.
|
|
1324
|
+
const launchEnv = typeof input.env === "function" ? await input.env() : input.env;
|
|
1325
|
+
const commandPayload = Buffer.from(JSON.stringify({
|
|
1326
|
+
command: input.command,
|
|
1327
|
+
args: input.args,
|
|
1328
|
+
cwd: input.cwd || target.remoteCwd,
|
|
1329
|
+
// The ACP engine has already projected this launch env from explicit
|
|
1330
|
+
// adapter/runtime inputs and registered contributions. Compare against an
|
|
1331
|
+
// empty inherited baseline so an explicit identity value (notably PATH)
|
|
1332
|
+
// is not reclassified as ambient merely because it equals the host value.
|
|
1333
|
+
env: sanitizeRemoteExecutionEnv(launchEnv, {}),
|
|
1334
|
+
}), "utf8").toString("base64");
|
|
1335
|
+
// Base64 plus JSON escaping can push an otherwise valid child environment
|
|
1336
|
+
// past Linux's per-argument/per-variable exec limit. Keep small launches on
|
|
1337
|
+
// the existing path; upload larger envelopes in bounded chunks instead.
|
|
1338
|
+
const commandEnv = {};
|
|
1339
|
+
if (commandPayload.length <= 64 * 1024) {
|
|
1340
|
+
commandEnv.PAPERCLIP_PROCESS_SESSION_COMMAND_B64 = commandPayload;
|
|
1341
|
+
}
|
|
1342
|
+
else {
|
|
1343
|
+
const payloadPath = path.posix.join(sessionDir, "command.b64");
|
|
1344
|
+
const runPayloadSetup = async (script) => {
|
|
1345
|
+
const result = await runner.execute({
|
|
1346
|
+
command: shellCommand,
|
|
1347
|
+
args: shellCommandArgs(script),
|
|
1348
|
+
cwd: target.remoteCwd,
|
|
1349
|
+
timeoutMs,
|
|
1350
|
+
bypassSession: true,
|
|
1351
|
+
});
|
|
1352
|
+
if (result.timedOut || result.exitCode !== 0) {
|
|
1353
|
+
throw new Error("Failed to stage sandbox process session command payload.");
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
try {
|
|
1357
|
+
// The envelope can contain credentials. Its upload intermediates stay
|
|
1358
|
+
// inside a private session directory, and the wrapper deletes it before
|
|
1359
|
+
// spawning the child. Teardown removes it if the wrapper cannot start.
|
|
1360
|
+
await runPayloadSetup(`umask 077 && mkdir -m 700 ${shellQuote(sessionDir)}`);
|
|
1361
|
+
await client.writeTextFile(payloadPath, commandPayload);
|
|
1362
|
+
await runPayloadSetup(`chmod 600 ${shellQuote(payloadPath)}`);
|
|
1363
|
+
}
|
|
1364
|
+
catch (error) {
|
|
1365
|
+
await client.remove(sessionDir).catch(() => undefined);
|
|
1366
|
+
throw error;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
// Legacy poll path: background the wrapper with `nohup` and read its output
|
|
1370
|
+
// event files with the host poll below. The streamed path launches the wrapper
|
|
1371
|
+
// as one foreground session command further down instead, so skip this.
|
|
1372
|
+
if (!streamOutput) {
|
|
1373
|
+
await onLog("stdout", `[paperclip] Starting ACP process session bridge in sandbox (${target.providerKey ?? "provider"}).\n`);
|
|
1374
|
+
const startResult = await runner.execute({
|
|
1375
|
+
command: shellCommand,
|
|
1376
|
+
args: shellCommandArgs([
|
|
1377
|
+
`mkdir -p ${shellQuote(stdinDir)} ${shellQuote(eventsDir)}`,
|
|
1378
|
+
// I3: no numeric process identifier anywhere. Background the
|
|
1379
|
+
// wrapper and let it go; do not capture `$!`.
|
|
1380
|
+
`PAPERCLIP_PROCESS_SESSION_DIR=${shellQuote(sessionDir)} ` +
|
|
1381
|
+
Object.entries(commandEnv).map(([key, value]) => `${key}=${shellQuote(value)} `).join("") +
|
|
1382
|
+
`nohup node ${shellQuote(remoteScriptPath)} >/dev/null 2>&1 < /dev/null &`,
|
|
1383
|
+
].join("\n")),
|
|
1384
|
+
cwd: target.remoteCwd,
|
|
1385
|
+
env: {
|
|
1386
|
+
PAPERCLIP_SANDBOX_EXEC_CHANNEL: "bridge",
|
|
1387
|
+
},
|
|
1388
|
+
timeoutMs,
|
|
1389
|
+
// The wrapper launch is bridge plumbing. Keep it off the persistent
|
|
1390
|
+
// session so it never queues behind an in-run session command.
|
|
1391
|
+
bypassSession: true,
|
|
1392
|
+
}).catch(async (error) => {
|
|
1393
|
+
await client.remove(sessionDir).catch(() => undefined);
|
|
1394
|
+
throw error;
|
|
1395
|
+
});
|
|
1396
|
+
if (startResult.timedOut || (startResult.exitCode ?? 1) !== 0) {
|
|
1397
|
+
await client.remove(sessionDir).catch(() => undefined);
|
|
1398
|
+
throw new Error(`Failed to start sandbox ACP process session bridge: ${startResult.stderr || startResult.stdout}`);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
let socket = null;
|
|
1402
|
+
let stopping = false;
|
|
1403
|
+
// Resolves when `stop()` tears the bridge down. The streamed `sandbox.agentProcess`
|
|
1404
|
+
// span races its work against this, so the span ends at teardown at the latest
|
|
1405
|
+
// even when the remote process lingers, and never outlives the run root span.
|
|
1406
|
+
let signalStopped = () => { };
|
|
1407
|
+
const stopped = new Promise((resolve) => {
|
|
1408
|
+
signalStopped = resolve;
|
|
1409
|
+
});
|
|
1410
|
+
let stdinSeq = 0;
|
|
1411
|
+
// One promise chain per session that serializes the stdin file writes. Each
|
|
1412
|
+
// write is multi-exec on the command-managed client: prepare, append per 32
|
|
1413
|
+
// KiB, then an atomic rename. The chain makes the rename for file N finish
|
|
1414
|
+
// before the write for file N+1 starts, so the files land in send order.
|
|
1415
|
+
// Without it the writes overlap. A small later chunk can then rename ahead of
|
|
1416
|
+
// a big earlier chunk, so the wrapper reads the stdin bytes out of order and
|
|
1417
|
+
// corrupts a large prompt on the stdin path.
|
|
1418
|
+
let stdinWriteChain = Promise.resolve();
|
|
1419
|
+
let pollTimer = null;
|
|
1420
|
+
const pendingRemoteEvents = [];
|
|
1421
|
+
const token = createSandboxCallbackBridgeToken(18);
|
|
1422
|
+
const proxyDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-proxy-"));
|
|
1423
|
+
// `stop()` waits on this promise, bounded, for the wrapper's `shutdownAck`
|
|
1424
|
+
// event. `deliverRemoteEvent` resolves it below and never forwards the
|
|
1425
|
+
// event further: it is a host-internal control ack, not part of the ACP
|
|
1426
|
+
// output stream. An event under `sessionDir` is untrusted telemetry: an
|
|
1427
|
+
// `exit` or `error` event is never treated as proof of shutdown, because
|
|
1428
|
+
// any process running under the sandbox can write one. Only `shutdownAck`
|
|
1429
|
+
// counts, and `stop()` also gives itself a dedicated reader for it below,
|
|
1430
|
+
// so a late `shutdownAck` still lands even after the long-lived poll has
|
|
1431
|
+
// stopped re-arming.
|
|
1432
|
+
let signalShutdownAcknowledged = () => { };
|
|
1433
|
+
const shutdownAcknowledged = new Promise((resolve) => {
|
|
1434
|
+
signalShutdownAcknowledged = resolve;
|
|
1435
|
+
});
|
|
1436
|
+
const writeRemoteEventToSocket = (event) => {
|
|
1437
|
+
if (!socket)
|
|
1438
|
+
return false;
|
|
1439
|
+
socket.write(jsonLine(event));
|
|
1440
|
+
if (event.type === "exit") {
|
|
1441
|
+
stopping = true;
|
|
1442
|
+
socket.end();
|
|
1443
|
+
}
|
|
1444
|
+
else if (event.type === "error") {
|
|
1445
|
+
stopping = true;
|
|
1446
|
+
socket.destroy();
|
|
1447
|
+
}
|
|
1448
|
+
return true;
|
|
1449
|
+
};
|
|
1450
|
+
const deliverRemoteEvent = (event) => {
|
|
1451
|
+
if (event.type === "shutdownAck") {
|
|
1452
|
+
signalShutdownAcknowledged();
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (socket) {
|
|
1456
|
+
writeRemoteEventToSocket(event);
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
pendingRemoteEvents.push(event);
|
|
1460
|
+
if (event.type === "exit" || event.type === "error") {
|
|
1461
|
+
stopping = true;
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
const flushPendingRemoteEvents = () => {
|
|
1465
|
+
if (!socket)
|
|
1466
|
+
return;
|
|
1467
|
+
while (pendingRemoteEvents.length > 0 && socket) {
|
|
1468
|
+
const event = pendingRemoteEvents.shift();
|
|
1469
|
+
if (event)
|
|
1470
|
+
writeRemoteEventToSocket(event);
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
const liveSockets = new Set();
|
|
1474
|
+
// Register the per-connection socket handlers with no run parent context.
|
|
1475
|
+
// A stdin write from a socket handler is a run-time exec, not startup work.
|
|
1476
|
+
// The connection can open under `task.run` and receive stdin later, during an
|
|
1477
|
+
// `agent.turn`. So the handler must read the current-run parent at send time,
|
|
1478
|
+
// not at connect time. A connect-time read captures the parent that was live
|
|
1479
|
+
// when the socket opened, and every later exec span parents to that stale
|
|
1480
|
+
// parent. The `data` handler below reads the getter per message instead.
|
|
1481
|
+
const server = net.createServer((nextSocket) => {
|
|
1482
|
+
liveSockets.add(nextSocket);
|
|
1483
|
+
nextSocket.setEncoding("utf8");
|
|
1484
|
+
nextSocket.on("error", () => undefined);
|
|
1485
|
+
let connectionBuffer = "";
|
|
1486
|
+
let authenticated = false;
|
|
1487
|
+
// Connections own the session (and receive buffered process output) only
|
|
1488
|
+
// after presenting the bridge token; idle unauthenticated peers are dropped.
|
|
1489
|
+
const authTimer = setTimeout(() => {
|
|
1490
|
+
if (!authenticated)
|
|
1491
|
+
nextSocket.destroy();
|
|
1492
|
+
}, PROCESS_SESSION_AUTH_TIMEOUT_MS);
|
|
1493
|
+
authTimer.unref?.();
|
|
1494
|
+
nextSocket.on("close", () => {
|
|
1495
|
+
clearTimeout(authTimer);
|
|
1496
|
+
liveSockets.delete(nextSocket);
|
|
1497
|
+
});
|
|
1498
|
+
nextSocket.on("data", (chunk) => {
|
|
1499
|
+
connectionBuffer += chunk;
|
|
1500
|
+
const split = splitJsonLines(connectionBuffer);
|
|
1501
|
+
connectionBuffer = split.rest;
|
|
1502
|
+
for (const line of split.lines) {
|
|
1503
|
+
if (!line.trim())
|
|
1504
|
+
continue;
|
|
1505
|
+
let message;
|
|
1506
|
+
try {
|
|
1507
|
+
message = JSON.parse(line);
|
|
1508
|
+
}
|
|
1509
|
+
catch {
|
|
1510
|
+
nextSocket.destroy();
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
if (message.token !== token) {
|
|
1514
|
+
nextSocket.destroy();
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
if (!authenticated) {
|
|
1518
|
+
if (socket) {
|
|
1519
|
+
nextSocket.destroy();
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
authenticated = true;
|
|
1523
|
+
clearTimeout(authTimer);
|
|
1524
|
+
socket = nextSocket;
|
|
1525
|
+
flushPendingRemoteEvents();
|
|
1526
|
+
}
|
|
1527
|
+
// Wrap one outbound ACP message to the agent in a
|
|
1528
|
+
// `sandbox.agentSession.sendInput` span, so its one `writeTextFile` exec
|
|
1529
|
+
// groups under one named span. The span runner reads the current-run
|
|
1530
|
+
// parent at send time: the live parent switches to `agent.turn` during
|
|
1531
|
+
// the turn and back to `task.run` after it. A message that is neither
|
|
1532
|
+
// `stdin` nor `stdinEnd` writes nothing, so it opens no span.
|
|
1533
|
+
const stdinPayload = message.type === "stdin" && typeof message.data === "string"
|
|
1534
|
+
? { type: "stdin", data: message.data }
|
|
1535
|
+
: message.type === "stdinEnd"
|
|
1536
|
+
? { type: "stdinEnd" }
|
|
1537
|
+
: null;
|
|
1538
|
+
if (stdinPayload) {
|
|
1539
|
+
stdinSeq += 1;
|
|
1540
|
+
const name = `${String(stdinSeq).padStart(12, "0")}.json`;
|
|
1541
|
+
const filePath = path.posix.join(stdinDir, name);
|
|
1542
|
+
// Chain this write after the previous one, so the atomic rename for
|
|
1543
|
+
// file N finishes before the write for file N+1 starts. Keep the
|
|
1544
|
+
// per-message `sandbox.agentSession.sendInput` span inside the chain.
|
|
1545
|
+
const write = stdinWriteChain.then(() => runRuntimeWork(AGENT_SESSION_SEND_INPUT_SPAN, () => client.writeTextFile(filePath, jsonLine(stdinPayload))));
|
|
1546
|
+
// The next message chains after this write on success or failure, so a
|
|
1547
|
+
// failed write never blocks the chain. This mirrors the wrapper
|
|
1548
|
+
// `writeChain` pattern for its event files.
|
|
1549
|
+
stdinWriteChain = write.then(() => undefined, () => undefined);
|
|
1550
|
+
// Keep the failure behavior: send one error line, then destroy the socket.
|
|
1551
|
+
write.catch((error) => {
|
|
1552
|
+
nextSocket.write(jsonLine({ type: "error", message: error instanceof Error ? error.message : String(error) }));
|
|
1553
|
+
nextSocket.destroy();
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
});
|
|
1559
|
+
const poll = async () => {
|
|
1560
|
+
if (stopping)
|
|
1561
|
+
return;
|
|
1562
|
+
try {
|
|
1563
|
+
// Read every file this tick fetched before this loop decides whether to
|
|
1564
|
+
// keep polling. A `shutdownAck` can land in the same batch right after
|
|
1565
|
+
// an `exit` event; deliver it too, so this tick never drops an
|
|
1566
|
+
// already-fetched (and already-removed-from-disk) event.
|
|
1567
|
+
const events = await readRemoteJsonFiles({ client, dir: eventsDir });
|
|
1568
|
+
for (const event of events) {
|
|
1569
|
+
const parsed = JSON.parse(event.body);
|
|
1570
|
+
deliverRemoteEvent(parsed);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
catch (error) {
|
|
1574
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1575
|
+
await onLog("stderr", `[paperclip] ACP process session bridge poll failed: ${message}\n`);
|
|
1576
|
+
deliverRemoteEvent({ type: "error", message });
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
finally {
|
|
1580
|
+
if (!stopping) {
|
|
1581
|
+
schedulePoll();
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
// Schedule the long-lived poll timer. Wrap each 100 ms poll tick in a
|
|
1586
|
+
// `sandbox.agentSession.pollOutput` span, so the tick's `list` plus per-file
|
|
1587
|
+
// `read`/`remove` execs group under one named span. The poll loop reads remote
|
|
1588
|
+
// event files with run-time execs, not startup work, so the wrapper span and
|
|
1589
|
+
// its child execs parent to the live run span, not to the ended bridge step.
|
|
1590
|
+
// The span runner reads the run parent per tick, because the re-arm timer that
|
|
1591
|
+
// the poll body schedules opens a new tick span: the live parent switches to
|
|
1592
|
+
// `agent.turn` during the turn and back to `task.run` after it.
|
|
1593
|
+
const schedulePoll = () => {
|
|
1594
|
+
pollTimer = setTimeout(() => void runRuntimeWork(AGENT_SESSION_POLL_OUTPUT_SPAN, poll), 100);
|
|
1595
|
+
pollTimer.unref?.();
|
|
1596
|
+
};
|
|
1597
|
+
const port = await waitForLocalServerListen(server);
|
|
1598
|
+
const agentCommand = await writeProcessSessionProxyScript(proxyDir, port, token);
|
|
1599
|
+
if (streamOutput) {
|
|
1600
|
+
// Streamed output path. Run the wrapper as one long-lived session command;
|
|
1601
|
+
// its stdout carries newline-delimited JSON frames that reach the host
|
|
1602
|
+
// through the provider session log stream. Deliver each frame exactly once
|
|
1603
|
+
// by its monotonic `seq`, so a frame that arrives both live and in the final
|
|
1604
|
+
// result is not repeated. There is no host output-file poll here.
|
|
1605
|
+
let streamBuffer = "";
|
|
1606
|
+
let lastSeq = 0;
|
|
1607
|
+
let sawTerminal = false;
|
|
1608
|
+
const deliverFrame = (frame) => {
|
|
1609
|
+
if (typeof frame.seq === "number") {
|
|
1610
|
+
if (frame.seq <= lastSeq)
|
|
1611
|
+
return;
|
|
1612
|
+
lastSeq = frame.seq;
|
|
1613
|
+
}
|
|
1614
|
+
if (frame.type === "exit" || frame.type === "error")
|
|
1615
|
+
sawTerminal = true;
|
|
1616
|
+
deliverRemoteEvent(frame);
|
|
1617
|
+
};
|
|
1618
|
+
const parseFrameLine = (line) => {
|
|
1619
|
+
if (!line.trim())
|
|
1620
|
+
return;
|
|
1621
|
+
let frame;
|
|
1622
|
+
try {
|
|
1623
|
+
frame = JSON.parse(line);
|
|
1624
|
+
}
|
|
1625
|
+
catch {
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
deliverFrame(frame);
|
|
1629
|
+
};
|
|
1630
|
+
// Live delivery: buffer partial lines across stream chunks, deliver each
|
|
1631
|
+
// complete frame line as it arrives.
|
|
1632
|
+
const ingestStreamChunk = (text) => {
|
|
1633
|
+
streamBuffer += text;
|
|
1634
|
+
const split = splitJsonLines(streamBuffer);
|
|
1635
|
+
streamBuffer = split.rest;
|
|
1636
|
+
for (const line of split.lines)
|
|
1637
|
+
parseFrameLine(line);
|
|
1638
|
+
};
|
|
1639
|
+
// Terminal delivery (the defined fallback to the poll): the resolved result
|
|
1640
|
+
// carries the full wrapper stdout even when the live stream degraded to the
|
|
1641
|
+
// provider session-log poll. The text is complete and self-contained, so
|
|
1642
|
+
// re-parse it on its own; the `seq` guard drops every frame the live stream
|
|
1643
|
+
// already delivered. Drop any partial live line — its complete form is in the
|
|
1644
|
+
// full text.
|
|
1645
|
+
const ingestFinalText = (text) => {
|
|
1646
|
+
streamBuffer = "";
|
|
1647
|
+
for (const line of text.split(/\n/))
|
|
1648
|
+
parseFrameLine(line);
|
|
1649
|
+
};
|
|
1650
|
+
await onLog("stdout", `[paperclip] Starting streamed ACP process session bridge in sandbox (${target.providerKey ?? "provider"}).\n`);
|
|
1651
|
+
// Fire the long-lived command; do NOT await it here. `useSession` forces the
|
|
1652
|
+
// persistent session so the provider streams the wrapper stdout back through
|
|
1653
|
+
// `onLog`. On resolve, the terminal re-parse fills any frames the live stream
|
|
1654
|
+
// missed; on reject, deliver one error frame so the local proxy fails loud.
|
|
1655
|
+
//
|
|
1656
|
+
// Wrap the launch in a `sandbox.agentProcess` span. `runRuntimeWork` parents
|
|
1657
|
+
// it to the LIVE RUN root (`task.run` at launch time — no turn has started
|
|
1658
|
+
// yet), not to the ephemeral `bridge.process-session` bring-up step, and it
|
|
1659
|
+
// stays open for the whole process lifetime. The inner `sandbox.exec` nests
|
|
1660
|
+
// under it. Without the wrapper the raw exec's span inherits the ~2.28s
|
|
1661
|
+
// bring-up step as its parent and then dangles ~50s past it, overlapping
|
|
1662
|
+
// `agent.turn` — a child outliving its parent. As a run-scoped span it reads
|
|
1663
|
+
// instead as a resource that OVERLAPS the sibling `agent.turn`, which is the
|
|
1664
|
+
// correct shape (the persistent process hosts the turn; it is not a child of
|
|
1665
|
+
// it, and on multi-turn runs one process spans several turns). `runRuntimeWork`
|
|
1666
|
+
// is voided, not awaited, so bring-up never blocks on the long-lived command,
|
|
1667
|
+
// and it defaults to a no-op parent when no span runner is injected.
|
|
1668
|
+
//
|
|
1669
|
+
// The span is bounded to the bridge lifecycle: it ends when the command
|
|
1670
|
+
// settles OR when `stop()` runs, whichever comes first. `stop()` runs during
|
|
1671
|
+
// run teardown, before the caller ends the `task.run` root span, so the span
|
|
1672
|
+
// never outlives the run root even if the remote process lingers past
|
|
1673
|
+
// teardown (`execute` has no cancel, so a lingering process cannot be forced
|
|
1674
|
+
// to resolve). The command promise keeps running after the span ends so its
|
|
1675
|
+
// frame handlers still deliver; they no-op once `stopping` is set.
|
|
1676
|
+
void runRuntimeWork("sandbox.agentProcess", async () => {
|
|
1677
|
+
const commandSettled = (async () => {
|
|
1678
|
+
try {
|
|
1679
|
+
const result = await runner.execute({
|
|
1680
|
+
command: shellCommand,
|
|
1681
|
+
args: shellCommandArgs(`node ${shellQuote(remoteScriptPath)}`),
|
|
1682
|
+
cwd: target.remoteCwd,
|
|
1683
|
+
env: {
|
|
1684
|
+
PAPERCLIP_PROCESS_SESSION_DIR: sessionDir,
|
|
1685
|
+
...commandEnv,
|
|
1686
|
+
PAPERCLIP_SANDBOX_EXEC_CHANNEL: "bridge",
|
|
1687
|
+
},
|
|
1688
|
+
timeoutMs,
|
|
1689
|
+
useSession: true,
|
|
1690
|
+
onLog: async (stream, chunk) => {
|
|
1691
|
+
if (stream === "stdout")
|
|
1692
|
+
ingestStreamChunk(chunk);
|
|
1693
|
+
},
|
|
1694
|
+
});
|
|
1695
|
+
ingestFinalText(result.stdout);
|
|
1696
|
+
if (!sawTerminal && !stopping) {
|
|
1697
|
+
// A shell can fail before the wrapper emits any frames. Preserve
|
|
1698
|
+
// that launch diagnostic instead of reporting only its exit code.
|
|
1699
|
+
if (result.stderr)
|
|
1700
|
+
await onLog("stderr", result.stderr);
|
|
1701
|
+
deliverRemoteEvent({
|
|
1702
|
+
type: "exit",
|
|
1703
|
+
code: typeof result.exitCode === "number" ? result.exitCode : null,
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
catch (error) {
|
|
1708
|
+
if (!stopping) {
|
|
1709
|
+
deliverRemoteEvent({
|
|
1710
|
+
type: "error",
|
|
1711
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
})();
|
|
1716
|
+
await Promise.race([commandSettled, stopped]);
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
else {
|
|
1720
|
+
schedulePoll();
|
|
1721
|
+
}
|
|
1722
|
+
// `stop()` cannot rely on the long-lived poll above to observe a late
|
|
1723
|
+
// `shutdownAck`: that poll stops re-arming as soon as it forwards a
|
|
1724
|
+
// terminal `exit`/`error` event, and `stop()` itself sets `stopping` on
|
|
1725
|
+
// its own first line. A normal completion's `shutdownAck` file, written a
|
|
1726
|
+
// moment after `exit`, can then land on disk after nobody reads the events
|
|
1727
|
+
// directory any more. Give `stop()` its own bounded reader that looks only
|
|
1728
|
+
// for `shutdownAck` and ignores every other event type, so the wait below
|
|
1729
|
+
// shortens on the wrapper's own proof of shutdown -- never on an `exit` or
|
|
1730
|
+
// `error` event, which any process running under `sessionDir` can forge.
|
|
1731
|
+
let stopReadingForShutdownAck = false;
|
|
1732
|
+
const readShutdownAckUntil = (deadlineEpochMs) => {
|
|
1733
|
+
if (stopReadingForShutdownAck)
|
|
1734
|
+
return;
|
|
1735
|
+
void (async () => {
|
|
1736
|
+
try {
|
|
1737
|
+
const events = await readRemoteJsonFiles({ client, dir: eventsDir });
|
|
1738
|
+
if (stopReadingForShutdownAck)
|
|
1739
|
+
return;
|
|
1740
|
+
for (const event of events) {
|
|
1741
|
+
try {
|
|
1742
|
+
const parsed = JSON.parse(event.body);
|
|
1743
|
+
if (parsed.type === "shutdownAck") {
|
|
1744
|
+
signalShutdownAcknowledged();
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
catch {
|
|
1749
|
+
// Not readable JSON yet. It is not a `shutdownAck`; ignore it.
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
catch {
|
|
1754
|
+
// Best-effort: a read failure here is not proof of anything.
|
|
1755
|
+
}
|
|
1756
|
+
if (!stopReadingForShutdownAck && Date.now() < deadlineEpochMs) {
|
|
1757
|
+
const timer = setTimeout(() => readShutdownAckUntil(deadlineEpochMs), 100);
|
|
1758
|
+
timer.unref?.();
|
|
1759
|
+
}
|
|
1760
|
+
})();
|
|
1761
|
+
};
|
|
1762
|
+
return {
|
|
1763
|
+
agentCommand,
|
|
1764
|
+
stop: async () => {
|
|
1765
|
+
stopping = true;
|
|
1766
|
+
// End the `sandbox.agentProcess` span now, before the caller ends the run
|
|
1767
|
+
// root span, even if the remote command has not resolved yet.
|
|
1768
|
+
signalStopped();
|
|
1769
|
+
if (pollTimer)
|
|
1770
|
+
clearTimeout(pollTimer);
|
|
1771
|
+
for (const liveSocket of liveSockets)
|
|
1772
|
+
liveSocket.destroy();
|
|
1773
|
+
await new Promise((resolve) => server.close(() => resolve())).catch(() => undefined);
|
|
1774
|
+
// Wait for every accepted stdin write before `stdinEnd`. The socket handler
|
|
1775
|
+
// fires each chunk write un-awaited through `stdinWriteChain`, so an earlier
|
|
1776
|
+
// chunk can still be pending here. Chain the `stdinEnd` write onto the same
|
|
1777
|
+
// per-session chain, so its file rename never finishes before an earlier
|
|
1778
|
+
// chunk. `stdinSeq` is stable now, because the sockets are destroyed and the
|
|
1779
|
+
// server is closed, so no new message can increment it.
|
|
1780
|
+
const stdinEndPath = path.posix.join(stdinDir, `${String(stdinSeq + 1).padStart(12, "0")}.json`);
|
|
1781
|
+
const stdinEndWrite = stdinWriteChain.then(() => client.writeTextFile(stdinEndPath, jsonLine({ type: "stdinEnd" })));
|
|
1782
|
+
stdinWriteChain = stdinEndWrite.then(() => undefined, () => undefined);
|
|
1783
|
+
await stdinEndWrite.catch(() => undefined);
|
|
1784
|
+
// The `shutdown` control message tells the wrapper to terminate itself
|
|
1785
|
+
// and its own child (I3: no operating-system signal and no process
|
|
1786
|
+
// identifier cross this boundary — only a file-queue message does).
|
|
1787
|
+
// Chain it onto the same per-session write order as `stdinEnd`, so its
|
|
1788
|
+
// file never lands before the earlier one.
|
|
1789
|
+
const shutdownPath = path.posix.join(stdinDir, `${String(stdinSeq + 2).padStart(12, "0")}.json`);
|
|
1790
|
+
const shutdownWrite = stdinWriteChain.then(() => client.writeTextFile(shutdownPath, jsonLine({ type: "shutdown" })));
|
|
1791
|
+
stdinWriteChain = shutdownWrite.then(() => undefined, () => undefined);
|
|
1792
|
+
await shutdownWrite.catch(() => undefined);
|
|
1793
|
+
// Wait a bounded budget for a hint that the wrapper stopped: only the
|
|
1794
|
+
// `shutdownAck` event counts; an `exit` or `error` event is untrusted
|
|
1795
|
+
// telemetry from inside the sandbox and never shortens this wait or
|
|
1796
|
+
// suppresses the warning below. `shutdownAck` itself is ALSO an
|
|
1797
|
+
// untrusted hint, not proof: any process that shares the sandbox can
|
|
1798
|
+
// write the same event under this session's event directory. It can
|
|
1799
|
+
// only shorten this wait and suppress the warning below; it never
|
|
1800
|
+
// gates, shortens, or replaces the unconditional removal further down.
|
|
1801
|
+
// What actually makes the wrapper's own termination deterministic is
|
|
1802
|
+
// the wrapper-side session-identity latch, not this event.
|
|
1803
|
+
let acknowledgedInTime = false;
|
|
1804
|
+
readShutdownAckUntil(Date.now() + DEFAULT_PROCESS_SESSION_SHUTDOWN_WAIT_MS);
|
|
1805
|
+
await Promise.race([
|
|
1806
|
+
shutdownAcknowledged.then(() => {
|
|
1807
|
+
acknowledgedInTime = true;
|
|
1808
|
+
}),
|
|
1809
|
+
new Promise((resolve) => {
|
|
1810
|
+
const budgetTimer = setTimeout(resolve, DEFAULT_PROCESS_SESSION_SHUTDOWN_WAIT_MS);
|
|
1811
|
+
budgetTimer.unref?.();
|
|
1812
|
+
}),
|
|
1813
|
+
]);
|
|
1814
|
+
stopReadingForShutdownAck = true;
|
|
1815
|
+
if (!acknowledgedInTime) {
|
|
1816
|
+
await onLog("stderr", `[paperclip] ACP process session wrapper did not acknowledge shutdown within ${DEFAULT_PROCESS_SESSION_SHUTDOWN_WAIT_MS}ms; removing the session directory anyway.\n`).catch(() => undefined);
|
|
1817
|
+
}
|
|
1818
|
+
// Unconditional: this removal runs whether or not the wrapper
|
|
1819
|
+
// acknowledged, and whether or not any event (real or forged) arrived
|
|
1820
|
+
// under `sessionDir`. `stop()` runs during run teardown and must stay
|
|
1821
|
+
// non-fatal, so every step above is best-effort and this step never
|
|
1822
|
+
// throws.
|
|
1823
|
+
await client.remove(sessionDir).catch(() => undefined);
|
|
1824
|
+
await fs.rm(proxyDir, { recursive: true, force: true }).catch(() => undefined);
|
|
1825
|
+
},
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
function getProcessSessionProxySource(input) {
|
|
1829
|
+
return `#!/usr/bin/env node
|
|
1830
|
+
import net from "node:net";
|
|
1831
|
+
|
|
1832
|
+
const socket = net.createConnection({ host: "127.0.0.1", port: ${input.port} });
|
|
1833
|
+
const token = ${JSON.stringify(input.token)};
|
|
1834
|
+
let buffer = "";
|
|
1835
|
+
let exiting = false;
|
|
1836
|
+
|
|
1837
|
+
function send(message) {
|
|
1838
|
+
if (exiting) return;
|
|
1839
|
+
socket.write(JSON.stringify({ token, ...message }) + "\\n");
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
function finish(code) {
|
|
1843
|
+
if (exiting) return;
|
|
1844
|
+
exiting = true;
|
|
1845
|
+
process.exitCode = code;
|
|
1846
|
+
// ACP keeps stdin open while waiting for a reply. Release that handle when
|
|
1847
|
+
// the remote process stops, but let pending stdout/stderr writes drain.
|
|
1848
|
+
process.stdin.destroy();
|
|
1849
|
+
socket.end();
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
socket.on("connect", () => send({ type: "hello" }));
|
|
1853
|
+
process.stdin.on("data", (chunk) => send({ type: "stdin", data: Buffer.from(chunk).toString("base64") }));
|
|
1854
|
+
process.stdin.on("end", () => send({ type: "stdinEnd" }));
|
|
1855
|
+
process.stdin.resume();
|
|
1856
|
+
|
|
1857
|
+
socket.setEncoding("utf8");
|
|
1858
|
+
socket.on("data", (chunk) => {
|
|
1859
|
+
buffer += chunk;
|
|
1860
|
+
const parts = buffer.split(/\\n/);
|
|
1861
|
+
buffer = parts.pop() || "";
|
|
1862
|
+
for (const line of parts) {
|
|
1863
|
+
if (!line.trim()) continue;
|
|
1864
|
+
const message = JSON.parse(line);
|
|
1865
|
+
if (message.type === "data") {
|
|
1866
|
+
const out = Buffer.from(message.data || "", "base64");
|
|
1867
|
+
(message.stream === "stderr" ? process.stderr : process.stdout).write(out);
|
|
1868
|
+
} else if (message.type === "error") {
|
|
1869
|
+
process.stderr.write(String(message.message || "Process session bridge failed.") + "\\n");
|
|
1870
|
+
finish(1);
|
|
1871
|
+
} else if (message.type === "exit") {
|
|
1872
|
+
finish(typeof message.code === "number" ? message.code : 1);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
});
|
|
1876
|
+
socket.on("close", () => {
|
|
1877
|
+
if (!exiting) process.exit(1);
|
|
1878
|
+
});
|
|
1879
|
+
`;
|
|
1880
|
+
}
|
|
1881
|
+
export function getProcessSessionRemoteSource(input) {
|
|
1882
|
+
return input?.outputToStdout === true
|
|
1883
|
+
? getProcessSessionRemoteStreamSource()
|
|
1884
|
+
: getProcessSessionRemoteEventFileSource();
|
|
1885
|
+
}
|
|
1886
|
+
// The shared stdin drain. Both wrappers read newline-delimited stdin messages
|
|
1887
|
+
// from the stdin file queue and write them to the child, then end the child
|
|
1888
|
+
// stdin on `stdinEnd`. A write to a closed child stdin only emits an `error`
|
|
1889
|
+
// event, so the wrapper installs a no-op handler at the call site.
|
|
1890
|
+
const PROCESS_SESSION_STDIN_POLL_TAIL = `child.stdin.on("error", () => {});
|
|
1891
|
+
|
|
1892
|
+
// A stdin file can appear before the host finishes the write. An empty read is
|
|
1893
|
+
// the non-atomic-write window; a partial read makes JSON.parse throw. The
|
|
1894
|
+
// poller must not delete a file before it validates the content. So read and
|
|
1895
|
+
// parse each file first, and delete it only after a successful parse. The files
|
|
1896
|
+
// sort in send order. If an earlier file is not readable yet, stop the cycle and
|
|
1897
|
+
// keep the order: a later file (for example stdinEnd) must not run ahead of it.
|
|
1898
|
+
// Retry the earlier file on a later cycle. After the retry limit, drop the file
|
|
1899
|
+
// and write an error event, so a lost message fails loud, and let later files
|
|
1900
|
+
// run.
|
|
1901
|
+
const stdinMaxParseRetries = (() => {
|
|
1902
|
+
const raw = Number.parseInt(process.env.PAPERCLIP_PROCESS_SESSION_STDIN_MAX_RETRIES || "", 10);
|
|
1903
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 100;
|
|
1904
|
+
})();
|
|
1905
|
+
const stdinParseRetries = new Map();
|
|
1906
|
+
// Track the next expected sequence number. The host writes the stdin files in
|
|
1907
|
+
// send order and pads the number to 12 digits, starting at 1. The files sort in
|
|
1908
|
+
// send order. When the smallest present number is greater than expected, an
|
|
1909
|
+
// earlier file has not appeared yet: a missing file, not an unreadable one. Hold
|
|
1910
|
+
// the send order and wait for it, bounded by the same retry budget as the
|
|
1911
|
+
// unreadable-file path. This turns a reordering into a loud error, never silent
|
|
1912
|
+
// corruption.
|
|
1913
|
+
let stdinExpectedSeq = 1;
|
|
1914
|
+
let stdinGapRetries = 0;
|
|
1915
|
+
|
|
1916
|
+
// The bounded grace period between the SIGTERM and the SIGKILL a terminate()
|
|
1917
|
+
// call sends. A test can override it through the environment, so a stubborn
|
|
1918
|
+
// child does not force a slow test.
|
|
1919
|
+
const terminateGraceMs = (() => {
|
|
1920
|
+
const raw = Number.parseInt(process.env.PAPERCLIP_PROCESS_SESSION_TERMINATE_GRACE_MS || "", 10);
|
|
1921
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 3000;
|
|
1922
|
+
})();
|
|
1923
|
+
|
|
1924
|
+
// I2: terminate() is the only function in this wrapper that calls
|
|
1925
|
+
// child.kill(). No child event handler and no sibling callback calls it.
|
|
1926
|
+
// terminate() is idempotent: a second call, or a first call after the child
|
|
1927
|
+
// already exited on its own, does nothing beyond what already ran.
|
|
1928
|
+
async function terminate() {
|
|
1929
|
+
if (terminated) return;
|
|
1930
|
+
terminated = true;
|
|
1931
|
+
shuttingDown = true;
|
|
1932
|
+
stdinClosed = true;
|
|
1933
|
+
child.stdin.end();
|
|
1934
|
+
// A \`false\` return means the child's process handle is already gone (the
|
|
1935
|
+
// child exited before this call ran). ChildProcess#kill() is handle-scoped:
|
|
1936
|
+
// once Node clears the handle at reap, the method call above sends no
|
|
1937
|
+
// signal and never falls back to a stored process identifier (I3). Treat
|
|
1938
|
+
// \`false\` as a no-op and do not retry through a numeric identifier.
|
|
1939
|
+
const sentTerm = child.kill("SIGTERM");
|
|
1940
|
+
if (sentTerm) {
|
|
1941
|
+
killTimer = setTimeout(() => {
|
|
1942
|
+
// Escalate on the same handle only (I2): the grace period expired, so
|
|
1943
|
+
// send SIGKILL through the same child handle, never a numeric
|
|
1944
|
+
// identifier and never a process-group signal.
|
|
1945
|
+
child.kill("SIGKILL");
|
|
1946
|
+
}, terminateGraceMs);
|
|
1947
|
+
killTimer.unref?.();
|
|
1948
|
+
}
|
|
1949
|
+
// This event is an untrusted latency hint, not proof. Any process that can
|
|
1950
|
+
// reach this session's event directory can write the same event type. It
|
|
1951
|
+
// can only shorten the host's shutdown wait and suppress the host's
|
|
1952
|
+
// timeout warning; it is never evidence that this wrapper's lifecycle
|
|
1953
|
+
// completed, and the host's cleanup never depends on it. The identity
|
|
1954
|
+
// latch below is what makes this wrapper's own termination deterministic.
|
|
1955
|
+
await writeEvent({ type: "shutdownAck" });
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// A sandbox peer can delete sessionDir and stdinDir, then recreate a
|
|
1959
|
+
// directory at the same pathname. A pathname does not prove identity: any
|
|
1960
|
+
// process that shares the sandbox can write it. So this wrapper captures the
|
|
1961
|
+
// OS-level identity of both paths once at startup, before the first poll
|
|
1962
|
+
// cycle, and checks it on every later cycle.
|
|
1963
|
+
//
|
|
1964
|
+
// The identity is the device number, the inode number, AND the inode's own
|
|
1965
|
+
// creation time. The device/inode pair alone is not enough: a filesystem can
|
|
1966
|
+
// reissue the exact inode number a just-removed directory held to the very
|
|
1967
|
+
// next directory created at the same path, with no attacker action needed
|
|
1968
|
+
// beyond the recreate the finding already describes. The creation time does
|
|
1969
|
+
// not have this gap: it is set fresh on every inode allocation, even when the
|
|
1970
|
+
// allocator reissues an old inode number, so a recreated directory always
|
|
1971
|
+
// carries a different creation time. The creation time alone is not enough
|
|
1972
|
+
// either, on a filesystem or kernel too old to report it, so this wrapper
|
|
1973
|
+
// keeps the device/inode pair as a second signal rather than relying on
|
|
1974
|
+
// either alone. Ordinary use of stdinDir (the host writing and this wrapper
|
|
1975
|
+
// deleting individual stdin files) changes that directory's OWN change time,
|
|
1976
|
+
// but never its creation time, so the creation time is safe to latch on
|
|
1977
|
+
// without producing a false positive on every stdin message.
|
|
1978
|
+
//
|
|
1979
|
+
// A filesystem or kernel that cannot report a real creation time does not
|
|
1980
|
+
// always report a value of zero. Node fails in one of two ways, and both are
|
|
1981
|
+
// grounded, not assumed: on Linux, when the statx() call finds no creation
|
|
1982
|
+
// time support, the kernel leaves the field unset and Node reports 0. On a
|
|
1983
|
+
// platform whose stat() call has no creation-time field at all, Node copies
|
|
1984
|
+
// the change time into the creation time instead. A 0 value fails open (any
|
|
1985
|
+
// recreated directory then matches on birthtimeMs alone), and a change-time
|
|
1986
|
+
// copy fails closed but far too often (it would move on every stdin file
|
|
1987
|
+
// this wrapper deletes). captureSessionIdentity() below proves the value is
|
|
1988
|
+
// usable before it trusts it, and fails closed on both known fallbacks.
|
|
1989
|
+
let sessionDirIdentity = null;
|
|
1990
|
+
let stdinDirIdentity = null;
|
|
1991
|
+
// The latch. Once set, it never clears. This replaces a counter that a
|
|
1992
|
+
// successful read reset to zero: an attacker who recreated the directory
|
|
1993
|
+
// before the counter reached its threshold kept the wrapper polling forever.
|
|
1994
|
+
// A latch has no threshold to race and no reset path.
|
|
1995
|
+
let identityLost = false;
|
|
1996
|
+
|
|
1997
|
+
async function statPathIdentity(candidatePath) {
|
|
1998
|
+
const stats = await fs.lstat(candidatePath);
|
|
1999
|
+
if (stats.isSymbolicLink()) {
|
|
2000
|
+
const error = new Error("Refusing a symbolic link on a process session control path.");
|
|
2001
|
+
error.code = "EPAPERCLIP_SYMLINK";
|
|
2002
|
+
throw error;
|
|
2003
|
+
}
|
|
2004
|
+
if (!stats.isDirectory()) {
|
|
2005
|
+
const error = new Error("A process session control path is not a directory.");
|
|
2006
|
+
error.code = "ENOTDIR";
|
|
2007
|
+
throw error;
|
|
2008
|
+
}
|
|
2009
|
+
return { dev: stats.dev, ino: stats.ino, birthtimeMs: stats.birthtimeMs };
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
function sameIdentity(left, right) {
|
|
2013
|
+
return left.dev === right.dev && left.ino === right.ino && left.birthtimeMs === right.birthtimeMs;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
async function latchAndTerminate() {
|
|
2017
|
+
if (identityLost) return;
|
|
2018
|
+
identityLost = true;
|
|
2019
|
+
await terminate();
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
let probeSeq = 0;
|
|
2023
|
+
|
|
2024
|
+
// A probe file name that pollStdin() can never read as a stdin message: it
|
|
2025
|
+
// does not end in ".json", so the ".json" filter in pollStdin() skips it if
|
|
2026
|
+
// a poll cycle ever lists the directory during the probe's short window.
|
|
2027
|
+
function nextProbeFileName() {
|
|
2028
|
+
probeSeq += 1;
|
|
2029
|
+
return ".paperclip-birthtime-probe-" + process.pid + "-" + probeSeq;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// Proves a directory's reported birthtimeMs is a real creation time, not a
|
|
2033
|
+
// change-time copy. Creating and removing a file inside a directory changes
|
|
2034
|
+
// that directory's OWN change time but never its true creation time, so a
|
|
2035
|
+
// birthtimeMs that moves across the probe is a change-time copy. Returns
|
|
2036
|
+
// null when the value is proven real. Returns a stderr-ready reason string
|
|
2037
|
+
// on any failure (a detected copy, or a probe that cannot run at all, for
|
|
2038
|
+
// example a permission error or a pre-created probe path): either way the
|
|
2039
|
+
// caller must not trust the value.
|
|
2040
|
+
//
|
|
2041
|
+
// The open uses the "wx" flag: exclusive create, fail if the path exists.
|
|
2042
|
+
// A sandbox peer cannot pre-create the probe path as a symbolic link and
|
|
2043
|
+
// have this call follow it, because "wx" fails closed on an existing path
|
|
2044
|
+
// instead of following a link to it.
|
|
2045
|
+
//
|
|
2046
|
+
// Cleanup checks identity, not only ownership of the initial create. This
|
|
2047
|
+
// wrapper reads the probe file's identity, (dev, ino, ctimeMs), off the open
|
|
2048
|
+
// file descriptor itself (fstat), not off the path, so a peer that swaps the
|
|
2049
|
+
// path in the short gap after create cannot poison the identity this
|
|
2050
|
+
// wrapper trusts as its own. Right before removal, this wrapper reads the
|
|
2051
|
+
// path's identity again and removes it only when that identity still
|
|
2052
|
+
// matches. A same-sandbox peer that deletes the probe file and creates its
|
|
2053
|
+
// own entry at the same path in between leaves a different identity behind,
|
|
2054
|
+
// so this wrapper leaves that entry untouched instead of removing it. This
|
|
2055
|
+
// covers a peer's replacement file, a peer's replacement directory, and a
|
|
2056
|
+
// peer's replacement symbolic link alike, because all three change the
|
|
2057
|
+
// identity this wrapper reads back. The identity check includes ctimeMs,
|
|
2058
|
+
// not only (dev, ino): a filesystem can hand this call's freed inode number
|
|
2059
|
+
// straight back out to a peer's very next create at the same path, so
|
|
2060
|
+
// (dev, ino) alone can match a path this call no longer owns; ctimeMs resets
|
|
2061
|
+
// on every create, so a peer's replacement carries a different one even when
|
|
2062
|
+
// the inode number repeats. Node's filesystem API has no call that removes a
|
|
2063
|
+
// path only when its identity still matches an earlier read as one atomic
|
|
2064
|
+
// step, so a gap remains between this wrapper's final identity read and the
|
|
2065
|
+
// removal call itself. A peer that wins this gap can put any entry at the
|
|
2066
|
+
// probe path before the removal call runs. This can include a pre-existing
|
|
2067
|
+
// file the peer renames into place, not only a file the peer creates fresh.
|
|
2068
|
+
// The removal call then removes whatever entry sits at the probe path at
|
|
2069
|
+
// that moment. Two bounds still hold on that removal. The path always
|
|
2070
|
+
// stays under dirPath. If the entry is a symbolic link, the removal call
|
|
2071
|
+
// removes the link itself instead of following it to a different target.
|
|
2072
|
+
// A non-recursive removal call also fails if the entry is a directory.
|
|
2073
|
+
async function birthtimeSurvivesProbe(dirPath) {
|
|
2074
|
+
let before;
|
|
2075
|
+
try {
|
|
2076
|
+
before = (await fs.lstat(dirPath)).birthtimeMs;
|
|
2077
|
+
} catch {
|
|
2078
|
+
return "its reported creation time could not be read";
|
|
2079
|
+
}
|
|
2080
|
+
const probePath = path.posix.join(dirPath, nextProbeFileName());
|
|
2081
|
+
let handle;
|
|
2082
|
+
try {
|
|
2083
|
+
handle = await fs.open(probePath, "wx");
|
|
2084
|
+
} catch {
|
|
2085
|
+
return "its probe file could not be created exclusively (the path may already exist)";
|
|
2086
|
+
}
|
|
2087
|
+
// fstat on the open handle names the exact inode this call just created.
|
|
2088
|
+
// A path-based lstat here instead would be racy against a peer that swaps
|
|
2089
|
+
// the path in the gap between the create above and the stat: fstat has no
|
|
2090
|
+
// such gap, because a file descriptor keeps naming the inode it opened no
|
|
2091
|
+
// matter what a later swap does to the path.
|
|
2092
|
+
let ownedIdentity = null;
|
|
2093
|
+
try {
|
|
2094
|
+
const createdStats = await handle.stat();
|
|
2095
|
+
// ctimeMs guards against inode reuse; see the function comment above.
|
|
2096
|
+
ownedIdentity = { dev: createdStats.dev, ino: createdStats.ino, ctimeMs: createdStats.ctimeMs };
|
|
2097
|
+
} catch {
|
|
2098
|
+
ownedIdentity = null;
|
|
2099
|
+
} finally {
|
|
2100
|
+
await handle.close().catch(() => undefined);
|
|
2101
|
+
}
|
|
2102
|
+
if (!ownedIdentity) {
|
|
2103
|
+
// fstat on this call's own just-opened descriptor failed. This call then
|
|
2104
|
+
// has no verified identity for the probe file it created, so it must not
|
|
2105
|
+
// check or remove that file by path: a peer could already own the entry
|
|
2106
|
+
// at that path, and a path-based removal here could delete the peer's
|
|
2107
|
+
// entry instead of this call's own file. Fail closed right here instead
|
|
2108
|
+
// of falling through to the birthtime comparison below, so a failed
|
|
2109
|
+
// identity read can never let this probe report success.
|
|
2110
|
+
return "its own probe file's identity could not be read from the open file descriptor";
|
|
2111
|
+
}
|
|
2112
|
+
// The one gap the fs API cannot close: this lstat and the removal below
|
|
2113
|
+
// are two separate calls, not one atomic "remove if identity still
|
|
2114
|
+
// matches" step. A peer that wins this narrow gap can put any entry at
|
|
2115
|
+
// the probe path, including a pre-existing file it renames into place,
|
|
2116
|
+
// and the removal call below removes whatever entry is there when it
|
|
2117
|
+
// runs.
|
|
2118
|
+
const currentStats = await fs.lstat(probePath).catch(() => null);
|
|
2119
|
+
const stillOwned =
|
|
2120
|
+
currentStats !== null &&
|
|
2121
|
+
currentStats.dev === ownedIdentity.dev &&
|
|
2122
|
+
currentStats.ino === ownedIdentity.ino &&
|
|
2123
|
+
currentStats.ctimeMs === ownedIdentity.ctimeMs;
|
|
2124
|
+
if (stillOwned) {
|
|
2125
|
+
await fs.rm(probePath, { force: true }).catch(() => undefined);
|
|
2126
|
+
}
|
|
2127
|
+
let after;
|
|
2128
|
+
try {
|
|
2129
|
+
after = (await fs.lstat(dirPath)).birthtimeMs;
|
|
2130
|
+
} catch {
|
|
2131
|
+
return "its reported creation time could not be read";
|
|
2132
|
+
}
|
|
2133
|
+
return before === after ? null : "its reported creation time changed after a probe write";
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
async function refuseUnusableCreationTime(label, dirPath, reason) {
|
|
2137
|
+
process.stderr.write(
|
|
2138
|
+
"Refusing to trust the process session control path " + label + " (" + dirPath + "): " + reason +
|
|
2139
|
+
". This filesystem or kernel gives no usable creation time. Terminating.\\n",
|
|
2140
|
+
);
|
|
2141
|
+
await latchAndTerminate();
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// Runs once, before the first poll cycle, and before this wrapper captures
|
|
2145
|
+
// the identities it later checks on every cycle. A failed capture fails
|
|
2146
|
+
// closed: the wrapper has no verified identity to check on later cycles, so
|
|
2147
|
+
// it terminates now instead of polling a control path it never verified.
|
|
2148
|
+
//
|
|
2149
|
+
// This wrapper cannot assume stats.birthtimeMs is a real creation time. Node
|
|
2150
|
+
// can report a change-time copy as a creation time. That value fails closed
|
|
2151
|
+
// far too aggressively (it would move on every stdin file this wrapper
|
|
2152
|
+
// deletes), so this wrapper proves the value is not a copy with a probe before
|
|
2153
|
+
// it trusts it, run once here, before either directory's identity is captured.
|
|
2154
|
+
async function captureSessionIdentity() {
|
|
2155
|
+
try {
|
|
2156
|
+
const sessionProbeFailure = await birthtimeSurvivesProbe(sessionDir);
|
|
2157
|
+
if (sessionProbeFailure) {
|
|
2158
|
+
await refuseUnusableCreationTime("sessionDir", sessionDir, sessionProbeFailure);
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
const stdinProbeFailure = await birthtimeSurvivesProbe(stdinDir);
|
|
2162
|
+
if (stdinProbeFailure) {
|
|
2163
|
+
await refuseUnusableCreationTime("stdinDir", stdinDir, stdinProbeFailure);
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
const session = await statPathIdentity(sessionDir);
|
|
2167
|
+
const stdin = await statPathIdentity(stdinDir);
|
|
2168
|
+
sessionDirIdentity = session;
|
|
2169
|
+
stdinDirIdentity = stdin;
|
|
2170
|
+
} catch (error) {
|
|
2171
|
+
process.stderr.write(
|
|
2172
|
+
"Failed to capture the process session identity: " +
|
|
2173
|
+
(error instanceof Error ? error.message : String(error)) + ". Terminating.\\n",
|
|
2174
|
+
);
|
|
2175
|
+
await latchAndTerminate();
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
// Runs on every poll cycle, before the wrapper reads stdinDir. Terminate and
|
|
2180
|
+
// latch on any proof the control path is no longer the one this wrapper
|
|
2181
|
+
// captured at startup (a missing path, a path that is no longer a directory,
|
|
2182
|
+
// a symbolic link, or a directory whose identity changed), AND on every
|
|
2183
|
+
// other lstat failure. A permission error is not transient here: a sandbox
|
|
2184
|
+
// peer can deny search permission on the control directory without removing
|
|
2185
|
+
// it, and treating that as transient would leave the wrapper and its child
|
|
2186
|
+
// alive forever. The error code below only picks the stderr message, so an
|
|
2187
|
+
// operator can still tell a removed directory from a permission error; it
|
|
2188
|
+
// never decides whether to latch.
|
|
2189
|
+
//
|
|
2190
|
+
// Contrast readStdinDirNames() right below, whose catch block stays narrow
|
|
2191
|
+
// on purpose: readdir() opens a directory descriptor, so it can fail with a
|
|
2192
|
+
// genuinely transient error under descriptor exhaustion, and latching there
|
|
2193
|
+
// would kill live sessions under load. lstat() opens no descriptor, and this
|
|
2194
|
+
// function already runs before every call to readStdinDirNames(), so a
|
|
2195
|
+
// permission error latches here before readdir() is ever reached.
|
|
2196
|
+
async function verifySessionIdentity() {
|
|
2197
|
+
if (identityLost) return false;
|
|
2198
|
+
try {
|
|
2199
|
+
const session = await statPathIdentity(sessionDir);
|
|
2200
|
+
const stdin = await statPathIdentity(stdinDir);
|
|
2201
|
+
if (!sameIdentity(session, sessionDirIdentity) || !sameIdentity(stdin, stdinDirIdentity)) {
|
|
2202
|
+
await latchAndTerminate();
|
|
2203
|
+
return false;
|
|
2204
|
+
}
|
|
2205
|
+
return true;
|
|
2206
|
+
} catch (error) {
|
|
2207
|
+
const code = error && typeof error === "object" ? error.code : undefined;
|
|
2208
|
+
const reason =
|
|
2209
|
+
code === "ENOENT"
|
|
2210
|
+
? "the control path no longer exists"
|
|
2211
|
+
: code === "ENOTDIR"
|
|
2212
|
+
? "the control path is no longer a directory"
|
|
2213
|
+
: code === "EPAPERCLIP_SYMLINK"
|
|
2214
|
+
? "the control path is now a symbolic link"
|
|
2215
|
+
: "lstat failed" + (code ? " with " + code : "");
|
|
2216
|
+
process.stderr.write("Latching on a lost process session identity: " + reason + ". Terminating.\\n");
|
|
2217
|
+
await latchAndTerminate();
|
|
2218
|
+
return false;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
// This catch block stays narrow on purpose: see the comment above
|
|
2223
|
+
// verifySessionIdentity() for why a permission error here is treated as
|
|
2224
|
+
// transient while the same error latches there.
|
|
2225
|
+
async function readStdinDirNames() {
|
|
2226
|
+
if (!(await verifySessionIdentity())) return [];
|
|
2227
|
+
try {
|
|
2228
|
+
return await fs.readdir(stdinDir);
|
|
2229
|
+
} catch (error) {
|
|
2230
|
+
const code = error && typeof error === "object" ? error.code : undefined;
|
|
2231
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
2232
|
+
await latchAndTerminate();
|
|
2233
|
+
}
|
|
2234
|
+
return [];
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
async function pollStdin() {
|
|
2239
|
+
while (!shuttingDown) {
|
|
2240
|
+
const entries = (await readStdinDirNames()).filter((name) => name.endsWith(".json")).sort();
|
|
2241
|
+
for (const name of entries) {
|
|
2242
|
+
if (shuttingDown) break;
|
|
2243
|
+
const entrySeq = Number.parseInt(name, 10);
|
|
2244
|
+
// Hold the send order when an earlier file has not appeared. Do not consume
|
|
2245
|
+
// this later file: wait for the missing file on a later cycle, bounded by
|
|
2246
|
+
// the retry budget. After the budget, fail loud and advance past the gap,
|
|
2247
|
+
// so the present file can run.
|
|
2248
|
+
if (Number.isFinite(entrySeq) && entrySeq > stdinExpectedSeq) {
|
|
2249
|
+
stdinGapRetries += 1;
|
|
2250
|
+
if (stdinGapRetries < stdinMaxParseRetries) {
|
|
2251
|
+
break;
|
|
2252
|
+
}
|
|
2253
|
+
await writeEvent({
|
|
2254
|
+
type: "error",
|
|
2255
|
+
message:
|
|
2256
|
+
"Advanced past missing stdin files " + stdinExpectedSeq + " to " + (entrySeq - 1) +
|
|
2257
|
+
" after " + stdinMaxParseRetries + " retries.",
|
|
2258
|
+
});
|
|
2259
|
+
stdinGapRetries = 0;
|
|
2260
|
+
stdinExpectedSeq = entrySeq;
|
|
2261
|
+
}
|
|
2262
|
+
const file = path.posix.join(stdinDir, name);
|
|
2263
|
+
let message;
|
|
2264
|
+
try {
|
|
2265
|
+
// Hardening (I3): open with O_NOFOLLOW where the platform defines it,
|
|
2266
|
+
// so a control-path symbolic link swapped in after the directory
|
|
2267
|
+
// check fails the read instead of following it.
|
|
2268
|
+
const readFlag =
|
|
2269
|
+
typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW : "r";
|
|
2270
|
+
const raw = await fs.readFile(file, { encoding: "utf8", flag: readFlag });
|
|
2271
|
+
// An empty read means the content is not on disk yet. Treat it the same
|
|
2272
|
+
// as a parse failure: keep the file and retry on a later cycle.
|
|
2273
|
+
if (!raw) throw new Error("stdin file is empty");
|
|
2274
|
+
message = JSON.parse(raw);
|
|
2275
|
+
} catch (error) {
|
|
2276
|
+
const retries = (stdinParseRetries.get(name) || 0) + 1;
|
|
2277
|
+
if (retries >= stdinMaxParseRetries) {
|
|
2278
|
+
// The retry limit is reached. Drop the file and write an error event,
|
|
2279
|
+
// so the lost message fails loud. The file is resolved now, so let the
|
|
2280
|
+
// loop go on to the next entry.
|
|
2281
|
+
stdinParseRetries.delete(name);
|
|
2282
|
+
await fs.rm(file, { force: true }).catch(() => undefined);
|
|
2283
|
+
await writeEvent({
|
|
2284
|
+
type: "error",
|
|
2285
|
+
message:
|
|
2286
|
+
"Dropped unreadable stdin file after " + stdinMaxParseRetries + " retries: " + name + ": " +
|
|
2287
|
+
(error instanceof Error ? error.message : String(error)),
|
|
2288
|
+
});
|
|
2289
|
+
// The file is resolved (dropped). Advance the expected number and reset
|
|
2290
|
+
// the gap budget, then let the loop go on to the next entry.
|
|
2291
|
+
if (Number.isFinite(entrySeq)) stdinExpectedSeq = entrySeq + 1;
|
|
2292
|
+
stdinGapRetries = 0;
|
|
2293
|
+
continue;
|
|
2294
|
+
}
|
|
2295
|
+
// The file is not readable yet and is not past the retry limit. Keep it
|
|
2296
|
+
// and stop this cycle to hold the send order. A later file (for example
|
|
2297
|
+
// stdinEnd) must not run before this earlier file. A later cycle reads
|
|
2298
|
+
// from the start again.
|
|
2299
|
+
stdinParseRetries.set(name, retries);
|
|
2300
|
+
break;
|
|
2301
|
+
}
|
|
2302
|
+
// The parse succeeded, so the content is complete. Delete the file first,
|
|
2303
|
+
// then act on the message. A later cycle never re-reads a handled file.
|
|
2304
|
+
stdinParseRetries.delete(name);
|
|
2305
|
+
await fs.rm(file, { force: true }).catch(() => undefined);
|
|
2306
|
+
// The file is handled. Advance the expected number and reset the gap
|
|
2307
|
+
// budget, so the next expected file starts fresh.
|
|
2308
|
+
if (Number.isFinite(entrySeq)) stdinExpectedSeq = entrySeq + 1;
|
|
2309
|
+
stdinGapRetries = 0;
|
|
2310
|
+
if (message.type === "stdin" && typeof message.data === "string") {
|
|
2311
|
+
if (!stdinClosed) child.stdin.write(Buffer.from(message.data, "base64"));
|
|
2312
|
+
} else if (message.type === "stdinEnd") {
|
|
2313
|
+
stdinClosed = true;
|
|
2314
|
+
child.stdin.end();
|
|
2315
|
+
break;
|
|
2316
|
+
} else if (message.type === "shutdown") {
|
|
2317
|
+
await terminate();
|
|
2318
|
+
break;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
if (!shuttingDown) await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
await captureSessionIdentity();
|
|
2326
|
+
|
|
2327
|
+
void pollStdin().catch((error) => void writeEvent({ type: "error", message: error instanceof Error ? error.message : String(error) }));
|
|
2328
|
+
`;
|
|
2329
|
+
// Shared by both wrappers. Refuse a symlink and remove the private envelope
|
|
2330
|
+
// before creating the child; no launch payload file belongs to the agent.
|
|
2331
|
+
const PROCESS_SESSION_READ_COMMAND = `
|
|
2332
|
+
let config;
|
|
2333
|
+
try {
|
|
2334
|
+
let commandPayload = process.env.PAPERCLIP_PROCESS_SESSION_COMMAND_B64;
|
|
2335
|
+
if (!commandPayload) {
|
|
2336
|
+
const payloadPath = path.posix.join(sessionDir, "command.b64");
|
|
2337
|
+
const handle = await fs.open(payloadPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
2338
|
+
try {
|
|
2339
|
+
commandPayload = await handle.readFile("utf8");
|
|
2340
|
+
} finally {
|
|
2341
|
+
await handle.close();
|
|
2342
|
+
await fs.rm(payloadPath, { force: true });
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
config = JSON.parse(Buffer.from(commandPayload, "base64").toString("utf8"));
|
|
2346
|
+
} catch {
|
|
2347
|
+
// No child exists yet. Emit a terminal event on both transports and attest
|
|
2348
|
+
// shutdown, instead of letting a detached polled wrapper fail silently.
|
|
2349
|
+
// Parse errors can quote credential bytes, so use a fixed diagnostic.
|
|
2350
|
+
await writeEvent({ type: "error", message: "Failed to read sandbox process session command payload." });
|
|
2351
|
+
await writeEvent({ type: "shutdownAck" });
|
|
2352
|
+
await new Promise((resolve) => process.stdout.write("", resolve));
|
|
2353
|
+
process.exit(1);
|
|
2354
|
+
}
|
|
2355
|
+
`;
|
|
2356
|
+
// Streamed variant: the wrapper writes each output frame as one newline-
|
|
2357
|
+
// delimited JSON line to its stdout. The host runs this wrapper as one
|
|
2358
|
+
// long-lived session command and reads the frames from the session log stream,
|
|
2359
|
+
// so there is no host output-file poll. Each frame carries a monotonic `seq`,
|
|
2360
|
+
// so the host delivers every frame exactly once whether it arrives live or in
|
|
2361
|
+
// the final result. The wrapper exits when the child closes, so the session
|
|
2362
|
+
// command settles and the session shell (the subshell wrap around it) survives.
|
|
2363
|
+
function getProcessSessionRemoteStreamSource() {
|
|
2364
|
+
return `import { spawn } from "node:child_process";
|
|
2365
|
+
import { promises as fs, constants as fsConstants } from "node:fs";
|
|
2366
|
+
import path from "node:path";
|
|
2367
|
+
|
|
2368
|
+
const sessionDir = process.env.PAPERCLIP_PROCESS_SESSION_DIR;
|
|
2369
|
+
if (!sessionDir) throw new Error("Missing process session bridge env.");
|
|
2370
|
+
|
|
2371
|
+
const stdinDir = path.posix.join(sessionDir, "stdin");
|
|
2372
|
+
let seq = 0;
|
|
2373
|
+
let stdinClosed = false;
|
|
2374
|
+
let shuttingDown = false;
|
|
2375
|
+
let terminated = false;
|
|
2376
|
+
let killTimer = null;
|
|
2377
|
+
|
|
2378
|
+
await fs.mkdir(stdinDir, { recursive: true });
|
|
2379
|
+
|
|
2380
|
+
// One newline-delimited JSON frame per event. Node keeps process.stdout writes
|
|
2381
|
+
// ordered, and the base64 payload holds no newline, so each frame is one line.
|
|
2382
|
+
function writeEvent(event) {
|
|
2383
|
+
seq += 1;
|
|
2384
|
+
process.stdout.write(JSON.stringify({ seq, ...event }) + "\\n");
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
// Hardening (I3): refuse a symbolic link on a control path before this
|
|
2388
|
+
// wrapper reads or writes through it. A symbolic link here could let another
|
|
2389
|
+
// sandbox process redirect the wrapper's file I/O outside the session tree.
|
|
2390
|
+
async function isSymbolicLink(candidatePath) {
|
|
2391
|
+
try {
|
|
2392
|
+
const stats = await fs.lstat(candidatePath);
|
|
2393
|
+
return stats.isSymbolicLink();
|
|
2394
|
+
} catch {
|
|
2395
|
+
return false;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
if ((await isSymbolicLink(sessionDir)) || (await isSymbolicLink(stdinDir))) {
|
|
2400
|
+
await writeEvent({ type: "error", message: "Refusing a symbolic link on a process session control path." });
|
|
2401
|
+
process.exitCode = 1;
|
|
2402
|
+
process.exit(1);
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
${PROCESS_SESSION_READ_COMMAND}
|
|
2406
|
+
|
|
2407
|
+
// Hardening (I3, not containment): the wrapper's own launch env carries the
|
|
2408
|
+
// session dir and the command payload. Scrub both keys before they reach the
|
|
2409
|
+
// spawned child, so the child never inherits a path to its own control files.
|
|
2410
|
+
const childEnv = { ...process.env, ...(config.env || {}) };
|
|
2411
|
+
delete childEnv.PAPERCLIP_PROCESS_SESSION_DIR;
|
|
2412
|
+
delete childEnv.PAPERCLIP_PROCESS_SESSION_COMMAND_B64;
|
|
2413
|
+
|
|
2414
|
+
// I1: exactly one child process per emitted wrapper. Do not add a second
|
|
2415
|
+
// tracked child handle.
|
|
2416
|
+
const child = spawn(config.command, Array.isArray(config.args) ? config.args : [], {
|
|
2417
|
+
cwd: config.cwd || process.cwd(),
|
|
2418
|
+
env: childEnv,
|
|
2419
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2420
|
+
});
|
|
2421
|
+
|
|
2422
|
+
child.stdout.on("data", (chunk) => writeEvent({ type: "data", stream: "stdout", data: Buffer.from(chunk).toString("base64") }));
|
|
2423
|
+
child.stderr.on("data", (chunk) => writeEvent({ type: "data", stream: "stderr", data: Buffer.from(chunk).toString("base64") }));
|
|
2424
|
+
child.on("error", (error) => writeEvent({ type: "error", message: error.message }));
|
|
2425
|
+
// "close" (not "exit") so stdout/stderr fully drain before the exit frame.
|
|
2426
|
+
// Queue the exit frame first, then run terminate(), so the exit frame always
|
|
2427
|
+
// lands even when the child closes on its own, with no stdinEnd and no
|
|
2428
|
+
// shutdown message ever received. writeEvent() only queues an asynchronous
|
|
2429
|
+
// write. terminate()'s own synchronous work (ending the child's stdin and
|
|
2430
|
+
// sending SIGTERM) already runs in this same handler by the time the exit
|
|
2431
|
+
// frame becomes readable on disk. terminate() is idempotent and its
|
|
2432
|
+
// child.kill() call here is always a no-op (I2): the child's process handle
|
|
2433
|
+
// is already gone by the time "close" fires. An error frame carries no such
|
|
2434
|
+
// guarantee: child.on("error", ...) below does not call terminate(), and
|
|
2435
|
+
// neither does the poll loop's own error writes, so those fire while the
|
|
2436
|
+
// wrapper and its child are still fully alive.
|
|
2437
|
+
child.on("close", (code, signal) => {
|
|
2438
|
+
writeEvent({ type: "exit", code, signal });
|
|
2439
|
+
process.exitCode = typeof code === "number" ? code : 1;
|
|
2440
|
+
void terminate();
|
|
2441
|
+
});
|
|
2442
|
+
|
|
2443
|
+
${PROCESS_SESSION_STDIN_POLL_TAIL}`;
|
|
2444
|
+
}
|
|
2445
|
+
function getProcessSessionRemoteEventFileSource() {
|
|
2446
|
+
return `import { spawn } from "node:child_process";
|
|
2447
|
+
import { promises as fs, constants as fsConstants } from "node:fs";
|
|
2448
|
+
import path from "node:path";
|
|
2449
|
+
|
|
2450
|
+
const sessionDir = process.env.PAPERCLIP_PROCESS_SESSION_DIR;
|
|
2451
|
+
if (!sessionDir) throw new Error("Missing process session bridge env.");
|
|
2452
|
+
|
|
2453
|
+
const stdinDir = path.posix.join(sessionDir, "stdin");
|
|
2454
|
+
const eventsDir = path.posix.join(sessionDir, "events");
|
|
2455
|
+
let seq = 0;
|
|
2456
|
+
let stdinClosed = false;
|
|
2457
|
+
let shuttingDown = false;
|
|
2458
|
+
let terminated = false;
|
|
2459
|
+
let killTimer = null;
|
|
2460
|
+
|
|
2461
|
+
await fs.mkdir(stdinDir, { recursive: true });
|
|
2462
|
+
await fs.mkdir(eventsDir, { recursive: true });
|
|
2463
|
+
|
|
2464
|
+
let writeChain = Promise.resolve();
|
|
2465
|
+
|
|
2466
|
+
function writeEvent(event) {
|
|
2467
|
+
seq += 1;
|
|
2468
|
+
const file = path.posix.join(eventsDir, String(seq).padStart(12, "0") + ".json");
|
|
2469
|
+
const write = writeChain.then(async () => {
|
|
2470
|
+
await fs.writeFile(file + ".tmp", JSON.stringify(event) + "\\n", "utf8");
|
|
2471
|
+
await fs.rename(file + ".tmp", file);
|
|
2472
|
+
});
|
|
2473
|
+
writeChain = write.catch(() => undefined);
|
|
2474
|
+
return write;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// Hardening (I3): refuse a symbolic link on a control path before this
|
|
2478
|
+
// wrapper reads or writes through it. A symbolic link here could let another
|
|
2479
|
+
// sandbox process redirect the wrapper's file I/O outside the session tree.
|
|
2480
|
+
async function isSymbolicLink(candidatePath) {
|
|
2481
|
+
try {
|
|
2482
|
+
const stats = await fs.lstat(candidatePath);
|
|
2483
|
+
return stats.isSymbolicLink();
|
|
2484
|
+
} catch {
|
|
2485
|
+
return false;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
if ((await isSymbolicLink(sessionDir)) || (await isSymbolicLink(stdinDir))) {
|
|
2490
|
+
await writeEvent({ type: "error", message: "Refusing a symbolic link on a process session control path." });
|
|
2491
|
+
process.exitCode = 1;
|
|
2492
|
+
process.exit(1);
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
${PROCESS_SESSION_READ_COMMAND}
|
|
2496
|
+
|
|
2497
|
+
// Hardening (I3, not containment): the wrapper's own launch env carries the
|
|
2498
|
+
// session dir and the command payload. Scrub both keys before they reach the
|
|
2499
|
+
// spawned child, so the child never inherits a path to its own control files.
|
|
2500
|
+
const childEnv = { ...process.env, ...(config.env || {}) };
|
|
2501
|
+
delete childEnv.PAPERCLIP_PROCESS_SESSION_DIR;
|
|
2502
|
+
delete childEnv.PAPERCLIP_PROCESS_SESSION_COMMAND_B64;
|
|
2503
|
+
|
|
2504
|
+
// I1: exactly one child process per emitted wrapper. Do not add a second
|
|
2505
|
+
// tracked child handle.
|
|
2506
|
+
const child = spawn(config.command, Array.isArray(config.args) ? config.args : [], {
|
|
2507
|
+
cwd: config.cwd || process.cwd(),
|
|
2508
|
+
env: childEnv,
|
|
2509
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2510
|
+
});
|
|
2511
|
+
|
|
2512
|
+
child.stdout.on("data", (chunk) => void writeEvent({ type: "data", stream: "stdout", data: Buffer.from(chunk).toString("base64") }));
|
|
2513
|
+
child.stderr.on("data", (chunk) => void writeEvent({ type: "data", stream: "stderr", data: Buffer.from(chunk).toString("base64") }));
|
|
2514
|
+
child.on("error", (error) => void writeEvent({ type: "error", message: error.message }));
|
|
2515
|
+
// "close" (not "exit") so stdout/stderr fully drain before the exit event;
|
|
2516
|
+
// the write chain then guarantees the exit file lands after every data file.
|
|
2517
|
+
// Queue the exit event first, then run terminate(), so the poll loop ends
|
|
2518
|
+
// even when the child closes on its own, with no stdinEnd and no shutdown
|
|
2519
|
+
// message ever received. writeEvent() only queues an asynchronous write.
|
|
2520
|
+
// terminate()'s own synchronous work (ending the child's stdin and sending
|
|
2521
|
+
// SIGTERM) already runs in this same handler by the time the exit file
|
|
2522
|
+
// becomes readable on disk. terminate() is idempotent and its child.kill()
|
|
2523
|
+
// call here is always a no-op (I2): the child's process handle is already
|
|
2524
|
+
// gone by the time "close" fires. An error event carries no such guarantee:
|
|
2525
|
+
// child.on("error", ...) below does not call terminate(), and neither does
|
|
2526
|
+
// the poll loop's own error writes, so those fire while the wrapper and its
|
|
2527
|
+
// child are still fully alive.
|
|
2528
|
+
child.on("close", (code, signal) => {
|
|
2529
|
+
void writeEvent({ type: "exit", code, signal });
|
|
2530
|
+
process.exitCode = typeof code === "number" ? code : 1;
|
|
2531
|
+
void terminate();
|
|
2532
|
+
});
|
|
2533
|
+
|
|
2534
|
+
${PROCESS_SESSION_STDIN_POLL_TAIL}`;
|
|
2535
|
+
}
|
|
2536
|
+
/** The default deadline for the duplex readiness handshake, in milliseconds. */
|
|
2537
|
+
const DEFAULT_DUPLEX_READINESS_TIMEOUT_MS = 10_000;
|
|
2538
|
+
/** The default bounded budget to close a partial duplex channel, in milliseconds. */
|
|
2539
|
+
const DEFAULT_DUPLEX_CLEANUP_BUDGET_MS = 2_000;
|
|
2540
|
+
/**
|
|
2541
|
+
* Reserve a loopback port the host assigns to the duplex gateway. The host binds
|
|
2542
|
+
* an ephemeral listener on `127.0.0.1`, reads the port the operating system
|
|
2543
|
+
* chose, then closes the listener. The number is very likely free when the
|
|
2544
|
+
* gateway binds it a moment later. The gateway binds exactly this port or exits
|
|
2545
|
+
* nonzero, so a taken port fails closed to the file bridge and never steers the
|
|
2546
|
+
* endpoint.
|
|
2547
|
+
*/
|
|
2548
|
+
async function reserveHostAssignedLoopbackPort() {
|
|
2549
|
+
return new Promise((resolve, reject) => {
|
|
2550
|
+
const probe = net.createServer();
|
|
2551
|
+
probe.once("error", reject);
|
|
2552
|
+
probe.listen(0, "127.0.0.1", () => {
|
|
2553
|
+
const address = probe.address();
|
|
2554
|
+
if (!address || typeof address === "string") {
|
|
2555
|
+
probe.close(() => reject(new Error("Could not reserve a loopback port for the duplex gateway.")));
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
const reserved = address.port;
|
|
2559
|
+
probe.close(() => resolve(reserved));
|
|
2560
|
+
});
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2563
|
+
/**
|
|
2564
|
+
* Build the argument vector that launches the duplex gateway in the sandbox. The
|
|
2565
|
+
* host passes the assigned port and the per-open nonce only through the launch
|
|
2566
|
+
* environment, so the argument vector sets them as environment assignments in
|
|
2567
|
+
* front of the node command. No addressing data comes from the channel.
|
|
2568
|
+
*
|
|
2569
|
+
* The script uses `exec env NAME=value ... command`. A POSIX shell accepts an
|
|
2570
|
+
* environment-assignment prefix only on a plain command, never on `exec`. The
|
|
2571
|
+
* form `exec NAME=value command` exits with status 127. The `env` utility carries
|
|
2572
|
+
* the assignments, and `exec` still replaces the shell with the gateway process,
|
|
2573
|
+
* so the gateway keeps the process slot and the assigned environment.
|
|
2574
|
+
*/
|
|
2575
|
+
export function buildDuplexGatewayLaunchArgv(input) {
|
|
2576
|
+
const assignments = Object.entries(input.env)
|
|
2577
|
+
.map(([key, value]) => `${key}=${shellQuote(value)}`)
|
|
2578
|
+
.join(" ");
|
|
2579
|
+
const nodeCommand = input.nodeCommand?.trim() || "node";
|
|
2580
|
+
const script = `exec env ${assignments} ${shellQuote(nodeCommand)} ${shellQuote(input.remoteEntrypoint)}`;
|
|
2581
|
+
return [input.shellCommand, ...shellCommandArgs(script)];
|
|
2582
|
+
}
|
|
2583
|
+
/**
|
|
2584
|
+
* Map a readiness failure to the fixed fallback reason. Each readiness failure
|
|
2585
|
+
* maps to exactly one reason from the closed telemetry set, so the fallback
|
|
2586
|
+
* counter and the transport event carry only an approved value.
|
|
2587
|
+
*/
|
|
2588
|
+
function duplexReadinessFallbackReason(reason) {
|
|
2589
|
+
switch (reason) {
|
|
2590
|
+
case "protocol_contamination":
|
|
2591
|
+
return "contaminated";
|
|
2592
|
+
case "nonce_mismatch":
|
|
2593
|
+
return "ready_nonce_mismatch";
|
|
2594
|
+
case "timeout":
|
|
2595
|
+
return "ready_timeout";
|
|
2596
|
+
case "channel_exit":
|
|
2597
|
+
return "ready_invalid";
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
/**
|
|
2601
|
+
* The fixed marker the worker manager puts in a route-busy rejection. The manager
|
|
2602
|
+
* defines the constant; this module matches the fixed string, because the two
|
|
2603
|
+
* layers ship in separate packages and share no import. The marker names the
|
|
2604
|
+
* process-scoped route ceiling, so it carries no route, query, body, or token.
|
|
2605
|
+
*/
|
|
2606
|
+
const DUPLEX_ROUTE_BUSY_ERROR_MARKER = "DUPLEX_CHANNEL_ROUTE_BUSY";
|
|
2607
|
+
/**
|
|
2608
|
+
* Report whether the caught open error is a route-busy rejection. The host maps it
|
|
2609
|
+
* to the `route_busy` fallback stage, so a full process-scoped route ceiling never
|
|
2610
|
+
* folds into a generic open failure.
|
|
2611
|
+
*/
|
|
2612
|
+
function isDuplexRouteBusyError(error) {
|
|
2613
|
+
return error instanceof Error && error.message.includes(DUPLEX_ROUTE_BUSY_ERROR_MARKER);
|
|
2614
|
+
}
|
|
2615
|
+
/**
|
|
2616
|
+
* The cap on the pre-READY readiness buffer, in bytes. The gate reads untrusted
|
|
2617
|
+
* bytes before the READY line arrives, so it bounds the buffer. The cap is the
|
|
2618
|
+
* codec frame-size bound plus one line of margin, so a legitimate maximum-size
|
|
2619
|
+
* READY frame still fits. Past the cap with no newline, the stream cannot be a
|
|
2620
|
+
* valid READY frame, so the gate finishes with protocol contamination.
|
|
2621
|
+
*/
|
|
2622
|
+
const DUPLEX_READINESS_BUFFER_CAP_BYTES = DEFAULT_MAX_DUPLEX_FRAME_BYTES + 4_096;
|
|
2623
|
+
// ---------------------------------------------------------------------------
|
|
2624
|
+
// http2_v1: the client connection preface scan and the run disposition latch.
|
|
2625
|
+
// ---------------------------------------------------------------------------
|
|
2626
|
+
/**
|
|
2627
|
+
* The HTTP/2 client connection preface: 24 octets, `PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n`
|
|
2628
|
+
* (RFC 9113, Section 3.4). A Node `http2.performServerHandshake` call needs a
|
|
2629
|
+
* `Duplex` whose readable side starts at this exact sequence; one extra
|
|
2630
|
+
* leading byte makes the preface invalid, and the server reports a
|
|
2631
|
+
* `PROTOCOL_ERROR`. The server does not skip a leading byte and does not
|
|
2632
|
+
* search for the sequence, so the host finds the offset itself before it
|
|
2633
|
+
* hands the channel to the server.
|
|
2634
|
+
*/
|
|
2635
|
+
const HTTP2_CLIENT_CONNECTION_PREFACE = Buffer.from("505249202a20485454502f322e300d0a0d0a534d0d0a0d0a", "hex");
|
|
2636
|
+
/** The one shared empty buffer. The preface scan starts and resets its two
|
|
2637
|
+
* retained buffers to it. */
|
|
2638
|
+
const HTTP2_PREFACE_EMPTY_BUFFER = Buffer.alloc(0);
|
|
2639
|
+
// The count of bytes the preface scan's substring search examines, in bytes.
|
|
2640
|
+
// A search that always starts from the beginning of the retained buffer
|
|
2641
|
+
// re-examines the whole buffer on every fragment, so this count grows
|
|
2642
|
+
// quadratically in the number of fragments. `findPrefaceFrom` instead
|
|
2643
|
+
// resumes from where the prior search left off, so this count stays linear
|
|
2644
|
+
// in the bytes received. A test reads this count to prove the search work
|
|
2645
|
+
// does not regress to the quadratic shape. Production code never reads this
|
|
2646
|
+
// count.
|
|
2647
|
+
let http2PrefaceScanSearchUnits = 0;
|
|
2648
|
+
// The count of bytes the pre-preface scan buffer copies while it grows its
|
|
2649
|
+
// backing storage. A one-copy-per-fragment append copies the whole retained
|
|
2650
|
+
// buffer on every fragment, so this count grows quadratically in the number
|
|
2651
|
+
// of fragments. The doubling-growth approach copies only on a reallocation,
|
|
2652
|
+
// so this count stays linear in the bytes received. A test reads this count
|
|
2653
|
+
// to prove the growth work does not regress to the quadratic shape.
|
|
2654
|
+
// Production code never reads this count.
|
|
2655
|
+
let http2PrefaceScanBufferGrowthCopyUnits = 0;
|
|
2656
|
+
// The same count as `http2PrefaceScanBufferGrowthCopyUnits`, for the
|
|
2657
|
+
// post-preface replay buffer instead of the pre-preface scan buffer.
|
|
2658
|
+
let http2PrefaceReplayBufferGrowthCopyUnits = 0;
|
|
2659
|
+
/**
|
|
2660
|
+
* A byte buffer that grows its backing storage by doubling its capacity,
|
|
2661
|
+
* instead of copying the whole retained buffer on every appended fragment. A
|
|
2662
|
+
* sender that trickles input in many small fragments would otherwise force
|
|
2663
|
+
* one full copy of the whole retained buffer per fragment: with the buffer
|
|
2664
|
+
* growing toward its cap, that is quadratic work in the number of fragments.
|
|
2665
|
+
* Doubling the backing storage's capacity only when the current capacity
|
|
2666
|
+
* runs out reallocates and copies a logarithmic number of times, so the
|
|
2667
|
+
* total copy work stays linear in the bytes received. `countGrowthCopy`
|
|
2668
|
+
* receives the number of bytes each reallocation copies, so a test can add
|
|
2669
|
+
* these up and prove the growth work stays linear.
|
|
2670
|
+
*/
|
|
2671
|
+
function createGrowableByteBuffer(countGrowthCopy) {
|
|
2672
|
+
let used = HTTP2_PREFACE_EMPTY_BUFFER;
|
|
2673
|
+
let storage = HTTP2_PREFACE_EMPTY_BUFFER;
|
|
2674
|
+
return {
|
|
2675
|
+
view: () => used,
|
|
2676
|
+
length: () => used.length,
|
|
2677
|
+
append: (chunk) => {
|
|
2678
|
+
const usedLength = used.length;
|
|
2679
|
+
const neededLength = usedLength + chunk.byteLength;
|
|
2680
|
+
if (neededLength > storage.length) {
|
|
2681
|
+
let nextCapacity = storage.length === 0 ? chunk.byteLength : storage.length * 2;
|
|
2682
|
+
while (nextCapacity < neededLength) {
|
|
2683
|
+
nextCapacity *= 2;
|
|
2684
|
+
}
|
|
2685
|
+
const grown = Buffer.allocUnsafe(nextCapacity);
|
|
2686
|
+
storage.copy(grown, 0, 0, usedLength);
|
|
2687
|
+
countGrowthCopy(usedLength);
|
|
2688
|
+
storage = grown;
|
|
2689
|
+
}
|
|
2690
|
+
storage.set(chunk, usedLength);
|
|
2691
|
+
used = storage.subarray(0, neededLength);
|
|
2692
|
+
},
|
|
2693
|
+
reset: () => {
|
|
2694
|
+
used = HTTP2_PREFACE_EMPTY_BUFFER;
|
|
2695
|
+
storage = HTTP2_PREFACE_EMPTY_BUFFER;
|
|
2696
|
+
},
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
/**
|
|
2700
|
+
* Find the client connection preface in `buffer` at or after index `from`
|
|
2701
|
+
* and return its offset, or -1. This counts the real scan distance for a
|
|
2702
|
+
* test: from `from` up to the found preface's end, or to the end of the
|
|
2703
|
+
* buffer when it finds none. The count stays linear in the bytes received
|
|
2704
|
+
* when the caller advances `from` to just short of the buffer's end on every
|
|
2705
|
+
* miss, instead of always searching from the start of the buffer.
|
|
2706
|
+
*/
|
|
2707
|
+
function findPrefaceFrom(buffer, from) {
|
|
2708
|
+
const offset = buffer.indexOf(HTTP2_CLIENT_CONNECTION_PREFACE, from);
|
|
2709
|
+
const scannedTo = offset === -1 ? buffer.length : offset + HTTP2_CLIENT_CONNECTION_PREFACE.length;
|
|
2710
|
+
http2PrefaceScanSearchUnits += Math.max(scannedTo - from, 0);
|
|
2711
|
+
return offset;
|
|
2712
|
+
}
|
|
2713
|
+
/**
|
|
2714
|
+
* Wrap the readiness gate's broker channel so its `onData` delivers no byte
|
|
2715
|
+
* until the client connection preface appears, then delivers every byte from
|
|
2716
|
+
* the preface onward.
|
|
2717
|
+
*
|
|
2718
|
+
* The wrapped channel opens its scan window only on the bytes the readiness
|
|
2719
|
+
* gate already retained after it accepted the READY line (constraint: the
|
|
2720
|
+
* scan window opens only after the gate accepts the nonce). Nothing before
|
|
2721
|
+
* that line ever reaches this scan, because the gate itself discards the
|
|
2722
|
+
* whole pre-READY buffer on acceptance. The scan buffers at most
|
|
2723
|
+
* {@link DUPLEX_READINESS_BUFFER_CAP_BYTES}; past that bound with no preface
|
|
2724
|
+
* found, it calls `onMissing` exactly one time and stops buffering, so the
|
|
2725
|
+
* caller can abort the open and fall back to `queue_v1`. The function holds
|
|
2726
|
+
* no prologue byte count: it always scans for the fixed 24-octet sequence,
|
|
2727
|
+
* never a length.
|
|
2728
|
+
*
|
|
2729
|
+
* The bytes that follow the found preface, before the HTTP/2 server binds a
|
|
2730
|
+
* downstream listener, land in `pendingAfterPreface`. This buffer holds
|
|
2731
|
+
* untrusted bytes on the same footing as the scan buffer, so it carries the
|
|
2732
|
+
* same {@link DUPLEX_READINESS_BUFFER_CAP_BYTES} cap. A chunk that would pass
|
|
2733
|
+
* the cap fails closed: the function drops the buffer and stops the channel.
|
|
2734
|
+
* The caller reads {@link replayOverflowed} after the preface settles and, on
|
|
2735
|
+
* `true`, treats the open the same as a missing preface.
|
|
2736
|
+
*/
|
|
2737
|
+
function createHttp2PrefaceScanningChannel(channel, options) {
|
|
2738
|
+
// The pre-preface scan buffer. `scanBuf.append` grows its backing storage
|
|
2739
|
+
// by doubling, instead of copying the whole retained buffer on every
|
|
2740
|
+
// fragment — see {@link createGrowableByteBuffer}. `scanSearchFrom` is the
|
|
2741
|
+
// first index the next search must examine: `findPrefaceFrom` advances it
|
|
2742
|
+
// to just short of the buffer's end on every miss, so a fragmented preface
|
|
2743
|
+
// is found without a full rescan of the retained buffer on every fragment.
|
|
2744
|
+
const scanBuf = createGrowableByteBuffer((copiedBytes) => {
|
|
2745
|
+
http2PrefaceScanBufferGrowthCopyUnits += copiedBytes;
|
|
2746
|
+
});
|
|
2747
|
+
let scanSearchFrom = 0;
|
|
2748
|
+
let sawPreface = false;
|
|
2749
|
+
let failed = false;
|
|
2750
|
+
let downstream = null;
|
|
2751
|
+
// Bytes found after the preface before a downstream listener attaches. The
|
|
2752
|
+
// wrapped channel replays them on attach, the same pattern the readiness
|
|
2753
|
+
// gate itself uses for its own post-READY replay buffer. This also grows
|
|
2754
|
+
// by doubling, for the same reason as `scanBuf`: fragmented post-preface
|
|
2755
|
+
// input must not force a full copy of the retained buffer per fragment.
|
|
2756
|
+
const pendingAfterPreface = createGrowableByteBuffer((copiedBytes) => {
|
|
2757
|
+
http2PrefaceReplayBufferGrowthCopyUnits += copiedBytes;
|
|
2758
|
+
});
|
|
2759
|
+
let replayOverflow = false;
|
|
2760
|
+
// Drop the pending buffer and stop the channel. The caller reads
|
|
2761
|
+
// `replayOverflowed()` after the preface settles and falls back the same
|
|
2762
|
+
// way it does for a missing preface.
|
|
2763
|
+
function overflowAndStop() {
|
|
2764
|
+
replayOverflow = true;
|
|
2765
|
+
pendingAfterPreface.reset();
|
|
2766
|
+
channel.stop();
|
|
2767
|
+
}
|
|
2768
|
+
function deliver(chunk) {
|
|
2769
|
+
if (downstream) {
|
|
2770
|
+
downstream(chunk);
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
if (replayOverflow)
|
|
2774
|
+
return;
|
|
2775
|
+
if (pendingAfterPreface.length() + chunk.byteLength > options.capBytes) {
|
|
2776
|
+
overflowAndStop();
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
pendingAfterPreface.append(chunk);
|
|
2780
|
+
}
|
|
2781
|
+
channel.onData((chunk) => {
|
|
2782
|
+
if (failed || replayOverflow)
|
|
2783
|
+
return;
|
|
2784
|
+
if (sawPreface) {
|
|
2785
|
+
deliver(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
const rawChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
2789
|
+
// Reject the chunk on its prospective length before it grows the scan
|
|
2790
|
+
// buffer, the same way `deliver` bounds `pendingAfterPreface`. A single
|
|
2791
|
+
// oversized chunk — or a chunk that tips an already-large buffer past
|
|
2792
|
+
// the cap — must fail closed here, before `scanBuf.append` performs the
|
|
2793
|
+
// allocation. Checking the cap only after the append still bounds the
|
|
2794
|
+
// retained buffer, but it lets one untrusted chunk force an allocation
|
|
2795
|
+
// as large as the chunk itself, unbounded by `capBytes`.
|
|
2796
|
+
if (scanBuf.length() + rawChunk.byteLength > options.capBytes) {
|
|
2797
|
+
failed = true;
|
|
2798
|
+
scanBuf.reset();
|
|
2799
|
+
scanSearchFrom = 0;
|
|
2800
|
+
options.onMissing();
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
scanBuf.append(rawChunk);
|
|
2804
|
+
const scanBuffer = scanBuf.view();
|
|
2805
|
+
const offset = findPrefaceFrom(scanBuffer, scanSearchFrom);
|
|
2806
|
+
if (offset === -1) {
|
|
2807
|
+
// No match yet. Resume the next search just short of the buffer's
|
|
2808
|
+
// end, keeping back an overlap of one octet less than the preface
|
|
2809
|
+
// length, so a preface split across this fragment and the next one is
|
|
2810
|
+
// still found. Each byte enters that overlap window a bounded number
|
|
2811
|
+
// of times, so the total search work stays linear in the bytes
|
|
2812
|
+
// received, not quadratic in the number of fragments.
|
|
2813
|
+
scanSearchFrom = Math.max(0, scanBuffer.length - (HTTP2_CLIENT_CONNECTION_PREFACE.length - 1));
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
sawPreface = true;
|
|
2817
|
+
options.onFound();
|
|
2818
|
+
const fromPreface = Buffer.from(scanBuffer.subarray(offset));
|
|
2819
|
+
scanBuf.reset();
|
|
2820
|
+
scanSearchFrom = 0;
|
|
2821
|
+
deliver(fromPreface);
|
|
2822
|
+
});
|
|
2823
|
+
return {
|
|
2824
|
+
channel: {
|
|
2825
|
+
write: (data) => channel.write(data),
|
|
2826
|
+
onData: (listener) => {
|
|
2827
|
+
downstream = listener;
|
|
2828
|
+
if (pendingAfterPreface.length() > 0) {
|
|
2829
|
+
// Copy the exact retained bytes instead of handing the listener
|
|
2830
|
+
// the growable buffer's backing view. That backing storage can
|
|
2831
|
+
// run ahead of the bytes in use (the doubling growth in
|
|
2832
|
+
// `pendingAfterPreface.append` over-provisions it), so a raw view
|
|
2833
|
+
// would keep the whole over-provisioned allocation alive for as
|
|
2834
|
+
// long as the listener holds its reference.
|
|
2835
|
+
const replay = Buffer.from(pendingAfterPreface.view());
|
|
2836
|
+
pendingAfterPreface.reset();
|
|
2837
|
+
listener(replay);
|
|
2838
|
+
}
|
|
2839
|
+
},
|
|
2840
|
+
onExit: (listener) => channel.onExit(listener),
|
|
2841
|
+
stop: () => channel.stop(),
|
|
2842
|
+
close: () => channel.close(),
|
|
2843
|
+
},
|
|
2844
|
+
replayOverflowed: () => replayOverflow,
|
|
2845
|
+
/**
|
|
2846
|
+
* Drop the scan buffer, for a caller-side terminal path this function
|
|
2847
|
+
* itself never reaches — the bound readiness timeout elapsing while the
|
|
2848
|
+
* scan is still searching, with no preface found and no cap refusal of
|
|
2849
|
+
* its own. A call after the preface already matched, or after the cap
|
|
2850
|
+
* already failed the scan closed, is a no-op: both paths already reset
|
|
2851
|
+
* the scan buffer themselves.
|
|
2852
|
+
*/
|
|
2853
|
+
disposeScanBuffer: () => {
|
|
2854
|
+
if (sawPreface || failed)
|
|
2855
|
+
return;
|
|
2856
|
+
failed = true;
|
|
2857
|
+
scanBuf.reset();
|
|
2858
|
+
scanSearchFrom = 0;
|
|
2859
|
+
},
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
/**
|
|
2863
|
+
* Wait for {@link createHttp2PrefaceScanningChannel} to settle: either the
|
|
2864
|
+
* preface appears, or the scan passes the bound with no match, or the bound
|
|
2865
|
+
* readiness timeout elapses first. Reapplying the readiness timeout here
|
|
2866
|
+
* keeps one configured value for both the READY-line wait and this
|
|
2867
|
+
* immediately-following preface wait; the task adds no new timeout setting.
|
|
2868
|
+
* Returns the scanning channel alongside the settled result, so the caller
|
|
2869
|
+
* binds the HTTP/2 server to it only on a `found` result. On a `found`
|
|
2870
|
+
* result, the caller must still read `replayOverflowed()`: the post-preface
|
|
2871
|
+
* buffer can overflow its cap after the preface settles as `found` and
|
|
2872
|
+
* before the caller binds a downstream listener.
|
|
2873
|
+
*/
|
|
2874
|
+
function scanForHttp2ClientPreface(channel, options) {
|
|
2875
|
+
let resolveSettled;
|
|
2876
|
+
let settledOnce = false;
|
|
2877
|
+
const settled = new Promise((resolve) => {
|
|
2878
|
+
resolveSettled = resolve;
|
|
2879
|
+
});
|
|
2880
|
+
// Reassigned to the real function once `createHttp2PrefaceScanningChannel`
|
|
2881
|
+
// returns, below. `settle` can only actually run after that point (the
|
|
2882
|
+
// timer fires later, and `onMissing`/`onFound` fire from inside the
|
|
2883
|
+
// channel's own `onData`, which registers after this call), so the
|
|
2884
|
+
// placeholder never runs for real.
|
|
2885
|
+
let disposeScanBuffer = () => { };
|
|
2886
|
+
const settle = (result) => {
|
|
2887
|
+
if (settledOnce)
|
|
2888
|
+
return;
|
|
2889
|
+
settledOnce = true;
|
|
2890
|
+
clearTimeout(timer);
|
|
2891
|
+
// The bound readiness timeout can elapse while the scan still searches,
|
|
2892
|
+
// with no preface found and no cap refusal of its own. That path holds
|
|
2893
|
+
// no other cleanup, so drop the scan buffer here. A `found` result, or a
|
|
2894
|
+
// `missing` result the scan itself already failed closed, is a no-op
|
|
2895
|
+
// inside `disposeScanBuffer`.
|
|
2896
|
+
if (result === "missing")
|
|
2897
|
+
disposeScanBuffer();
|
|
2898
|
+
resolveSettled(result);
|
|
2899
|
+
};
|
|
2900
|
+
const timer = setTimeout(() => settle("missing"), options.timeoutMs);
|
|
2901
|
+
timer.unref?.();
|
|
2902
|
+
const scan = createHttp2PrefaceScanningChannel(channel, {
|
|
2903
|
+
capBytes: options.capBytes,
|
|
2904
|
+
onFound: () => settle("found"),
|
|
2905
|
+
onMissing: () => settle("missing"),
|
|
2906
|
+
});
|
|
2907
|
+
disposeScanBuffer = scan.disposeScanBuffer;
|
|
2908
|
+
return { scanned: scan.channel, settled, replayOverflowed: scan.replayOverflowed };
|
|
2909
|
+
}
|
|
2910
|
+
/**
|
|
2911
|
+
* Test-only surface for {@link scanForHttp2ClientPreface}. A test drives the
|
|
2912
|
+
* post-preface replay cap across every terminal path without the whole
|
|
2913
|
+
* bridge. Production code never reads this export.
|
|
2914
|
+
*/
|
|
2915
|
+
export const __http2PrefaceScanTesting = {
|
|
2916
|
+
scanForHttp2ClientPreface: (channel, options) => scanForHttp2ClientPreface(channel, options),
|
|
2917
|
+
readScanSearchUnits: () => http2PrefaceScanSearchUnits,
|
|
2918
|
+
resetScanSearchUnits: () => {
|
|
2919
|
+
http2PrefaceScanSearchUnits = 0;
|
|
2920
|
+
},
|
|
2921
|
+
readScanBufferGrowthCopyUnits: () => http2PrefaceScanBufferGrowthCopyUnits,
|
|
2922
|
+
resetScanBufferGrowthCopyUnits: () => {
|
|
2923
|
+
http2PrefaceScanBufferGrowthCopyUnits = 0;
|
|
2924
|
+
},
|
|
2925
|
+
readReplayBufferGrowthCopyUnits: () => http2PrefaceReplayBufferGrowthCopyUnits,
|
|
2926
|
+
resetReplayBufferGrowthCopyUnits: () => {
|
|
2927
|
+
http2PrefaceReplayBufferGrowthCopyUnits = 0;
|
|
2928
|
+
},
|
|
2929
|
+
};
|
|
2930
|
+
function createHttp2RunDispositionLatch() {
|
|
2931
|
+
let lossOrdered = false;
|
|
2932
|
+
let lossReason = null;
|
|
2933
|
+
let completionOrdered = false;
|
|
2934
|
+
let lossListener = null;
|
|
2935
|
+
const markOrderlyCompletion = () => {
|
|
2936
|
+
if (completionOrdered || lossOrdered)
|
|
2937
|
+
return;
|
|
2938
|
+
completionOrdered = true;
|
|
2939
|
+
};
|
|
2940
|
+
return {
|
|
2941
|
+
get disposition() {
|
|
2942
|
+
return { failed: lossOrdered, lossReason };
|
|
2943
|
+
},
|
|
2944
|
+
recordLoss(reason) {
|
|
2945
|
+
if (lossOrdered || completionOrdered)
|
|
2946
|
+
return false;
|
|
2947
|
+
lossOrdered = true;
|
|
2948
|
+
lossReason = reason;
|
|
2949
|
+
lossListener?.(reason);
|
|
2950
|
+
return true;
|
|
2951
|
+
},
|
|
2952
|
+
markOrderlyCompletion,
|
|
2953
|
+
settleRunDisposition() {
|
|
2954
|
+
markOrderlyCompletion();
|
|
2955
|
+
return { failed: lossOrdered, lossReason };
|
|
2956
|
+
},
|
|
2957
|
+
onLoss(listener) {
|
|
2958
|
+
lossListener = listener;
|
|
2959
|
+
return () => {
|
|
2960
|
+
if (lossListener === listener)
|
|
2961
|
+
lossListener = null;
|
|
2962
|
+
};
|
|
2963
|
+
},
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
/**
|
|
2967
|
+
* Create the run-log directory on the sandbox before the tail starts. The file
|
|
2968
|
+
* bridge worker creates this directory on the file path. The duplex path starts
|
|
2969
|
+
* no worker, so the host creates the directory here. The tail then reads a real
|
|
2970
|
+
* directory from its first tick.
|
|
2971
|
+
*
|
|
2972
|
+
* This step is best effort. The broker already serves the duplex transport when
|
|
2973
|
+
* the host reaches it, and the tail wrap command runs its own `mkdir -p` as a
|
|
2974
|
+
* backstop. So a create failure must not tear down a working duplex transport;
|
|
2975
|
+
* the host swallows it and still builds the tail. The log line names no raw
|
|
2976
|
+
* error, so no raw error rides a log line here.
|
|
2977
|
+
*/
|
|
2978
|
+
async function ensureSandboxRunLogDirectory(input) {
|
|
2979
|
+
try {
|
|
2980
|
+
await input.runner.execute({
|
|
2981
|
+
command: input.shellCommand,
|
|
2982
|
+
args: shellCommandArgs(`mkdir -p ${shellQuote(input.logsDir)}`),
|
|
2983
|
+
cwd: input.remoteCwd,
|
|
2984
|
+
timeoutMs: input.timeoutMs ?? undefined,
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
catch {
|
|
2988
|
+
// Best effort: the tail wrap command creates the directory before it writes.
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
/**
|
|
2992
|
+
* The duplex readiness gate. The gate owns the single data listener and the
|
|
2993
|
+
* single exit listener of the channel while the host waits for a valid READY
|
|
2994
|
+
* frame. It resolves the handshake, then hands the channel to the broker.
|
|
2995
|
+
*
|
|
2996
|
+
* The gate authenticates readiness with the nonce and the strict READY schema,
|
|
2997
|
+
* not with the line position. A PTY channel echoes the launch wrapper line before
|
|
2998
|
+
* it sets raw mode, so the first line is often not the READY frame. The gate skips
|
|
2999
|
+
* each pre-READY line that does not decode as a READY frame, then accepts the first
|
|
3000
|
+
* line that decodes as a READY frame with the matching nonce. A line that decodes
|
|
3001
|
+
* as a READY frame with a wrong nonce fails the handshake with a nonce mismatch. An
|
|
3002
|
+
* early exit or a timeout also fails the handshake. The gate never dispatches a
|
|
3003
|
+
* request; the broker does that after readiness passes.
|
|
3004
|
+
*
|
|
3005
|
+
* The skipped bytes stay in the capped buffer, so the O(1) cap and the readiness
|
|
3006
|
+
* timeout still bound the wait. The gate enforces the cap on every pre-READY path
|
|
3007
|
+
* and before it decodes a candidate line, so an over-cap prefix never reaches READY
|
|
3008
|
+
* acceptance. The cap check has priority over READY acceptance on every path.
|
|
3009
|
+
*/
|
|
3010
|
+
// The count of the pre-READY newline-scan work, in bytes. Each search adds the
|
|
3011
|
+
// number of bytes it can read. A test reads this count to prove the scan work
|
|
3012
|
+
// stays linear in the bytes received. Production code never reads this count.
|
|
3013
|
+
let duplexReadinessNewlineScanUnits = 0;
|
|
3014
|
+
// The count of bytes `appendReadinessBytes` copies while it grows the pre-READY
|
|
3015
|
+
// buffer's backing storage. A one-copy-per-fragment approach copies the whole
|
|
3016
|
+
// retained buffer on every fragment, so this count grows quadratically in the
|
|
3017
|
+
// number of fragments. The doubling-growth approach copies only on a
|
|
3018
|
+
// reallocation, so this count stays linear in the bytes received. A test reads
|
|
3019
|
+
// this count to prove the growth work does not regress to the quadratic shape.
|
|
3020
|
+
// Production code never reads this count.
|
|
3021
|
+
let duplexReadinessBufferGrowthCopyUnits = 0;
|
|
3022
|
+
/** The newline byte. The readiness buffer is a byte buffer, not a string. */
|
|
3023
|
+
const READINESS_NEWLINE_BYTE = 0x0a;
|
|
3024
|
+
/** The opening-brace byte. The bracketed-paste retry scans for it, not a string. */
|
|
3025
|
+
const READINESS_OPEN_BRACE_BYTE = 0x7b;
|
|
3026
|
+
/** The one shared empty buffer. The gate starts and resets `buffer` to it. */
|
|
3027
|
+
const READINESS_EMPTY_BUFFER = Buffer.alloc(0);
|
|
3028
|
+
/**
|
|
3029
|
+
* Find the first newline in `buffer` at or after index `from` and return its
|
|
3030
|
+
* index, or -1. `Buffer#indexOf` reads the bytes from `from` up to the newline
|
|
3031
|
+
* it finds, or to the end of the buffer when it finds none. This helper counts
|
|
3032
|
+
* that real scan distance for a test. The count stays linear in the bytes
|
|
3033
|
+
* received when the caller advances `from` past each newline it consumes.
|
|
3034
|
+
*/
|
|
3035
|
+
function findNewlineFrom(buffer, from) {
|
|
3036
|
+
const newlineIndex = buffer.indexOf(READINESS_NEWLINE_BYTE, from);
|
|
3037
|
+
const scanned = newlineIndex === -1 ? buffer.length - from : newlineIndex - from + 1;
|
|
3038
|
+
duplexReadinessNewlineScanUnits += scanned;
|
|
3039
|
+
return newlineIndex;
|
|
3040
|
+
}
|
|
3041
|
+
/**
|
|
3042
|
+
* Test-only surface for the pre-READY readiness gate. A test reads the scan
|
|
3043
|
+
* count to prove the newline search work stays linear in the bytes received.
|
|
3044
|
+
* Production code does not use this object.
|
|
3045
|
+
*/
|
|
3046
|
+
export const __duplexReadinessTesting = {
|
|
3047
|
+
readNewlineScanUnits: () => duplexReadinessNewlineScanUnits,
|
|
3048
|
+
resetNewlineScanUnits: () => {
|
|
3049
|
+
duplexReadinessNewlineScanUnits = 0;
|
|
3050
|
+
},
|
|
3051
|
+
readBufferGrowthCopyUnits: () => duplexReadinessBufferGrowthCopyUnits,
|
|
3052
|
+
resetBufferGrowthCopyUnits: () => {
|
|
3053
|
+
duplexReadinessBufferGrowthCopyUnits = 0;
|
|
3054
|
+
},
|
|
3055
|
+
// Build one readiness gate over a supplied channel, so a test can drive the
|
|
3056
|
+
// readiness-replay cap lifecycle across every terminal path without the whole
|
|
3057
|
+
// bridge. Production code never reads this factory.
|
|
3058
|
+
createReadinessGate: (channel, options) => createDuplexReadinessGate(channel, options),
|
|
3059
|
+
};
|
|
3060
|
+
function createDuplexReadinessGate(channel, options) {
|
|
3061
|
+
let settled = false;
|
|
3062
|
+
let readyOk = false;
|
|
3063
|
+
// The gate sets this when a post-READY pre-bind chunk tips the pending replay
|
|
3064
|
+
// buffer past the cap. On that overflow the gate drops the pending buffer and
|
|
3065
|
+
// stops the channel. The caller reads it through `replayOverflowed` and selects
|
|
3066
|
+
// the file bridge.
|
|
3067
|
+
let replayOverflow = false;
|
|
3068
|
+
// The raw bytes the host reads before the READY frame completes. `buffer` is
|
|
3069
|
+
// append-only and always a zero-copy view over the used prefix of `storage`,
|
|
3070
|
+
// so the O(1) cap check on `buffer.length` stays valid.
|
|
3071
|
+
let buffer = READINESS_EMPTY_BUFFER;
|
|
3072
|
+
// The backing storage for `buffer`. `appendReadinessBytes` grows this by
|
|
3073
|
+
// doubling its capacity, instead of copying the whole retained buffer on
|
|
3074
|
+
// every fragment. See `appendReadinessBytes` for why this bounds the total
|
|
3075
|
+
// copy work.
|
|
3076
|
+
let storage = READINESS_EMPTY_BUFFER;
|
|
3077
|
+
// The start index of the current line in `buffer`. A leading blank line
|
|
3078
|
+
// advances this cursor past its newline without a buffer copy.
|
|
3079
|
+
let lineStart = 0;
|
|
3080
|
+
// The next index to search for a newline. The gate scans from here, so each
|
|
3081
|
+
// byte is read at most one time for the newline search.
|
|
3082
|
+
let scanFrom = 0;
|
|
3083
|
+
/**
|
|
3084
|
+
* Append `chunk` to the pre-READY buffer without copying the bytes already
|
|
3085
|
+
* retained. A sender that trickles the handshake in many small fragments
|
|
3086
|
+
* (a slow socket, a byte-at-a-time PTY echo) would otherwise force one full
|
|
3087
|
+
* copy of the whole retained buffer per fragment: with `buffer` growing
|
|
3088
|
+
* toward the {@link DUPLEX_READINESS_BUFFER_CAP_BYTES} cap, that is
|
|
3089
|
+
* quadratic work in the number of fragments. This instead grows `storage`
|
|
3090
|
+
* by doubling its capacity only when the current capacity runs out, so the
|
|
3091
|
+
* backing store reallocates and copies a logarithmic number of times, and
|
|
3092
|
+
* each append copies only the incoming chunk. The used length still reads
|
|
3093
|
+
* in O(1) through `buffer.length`, so every cap check and slice below stays
|
|
3094
|
+
* unchanged.
|
|
3095
|
+
*/
|
|
3096
|
+
function appendReadinessBytes(chunk) {
|
|
3097
|
+
const usedLength = buffer.length;
|
|
3098
|
+
const neededLength = usedLength + chunk.byteLength;
|
|
3099
|
+
if (neededLength > storage.length) {
|
|
3100
|
+
let nextCapacity = storage.length === 0 ? chunk.byteLength : storage.length * 2;
|
|
3101
|
+
while (nextCapacity < neededLength) {
|
|
3102
|
+
nextCapacity *= 2;
|
|
3103
|
+
}
|
|
3104
|
+
const grown = Buffer.allocUnsafe(nextCapacity);
|
|
3105
|
+
storage.copy(grown, 0, 0, usedLength);
|
|
3106
|
+
duplexReadinessBufferGrowthCopyUnits += usedLength;
|
|
3107
|
+
storage = grown;
|
|
3108
|
+
}
|
|
3109
|
+
storage.set(chunk, usedLength);
|
|
3110
|
+
buffer = storage.subarray(0, neededLength);
|
|
3111
|
+
}
|
|
3112
|
+
// The bytes that followed the READY frame, held until the broker binds.
|
|
3113
|
+
let pending = READINESS_EMPTY_BUFFER;
|
|
3114
|
+
// The exit that arrived after READY but before the broker bound, if any.
|
|
3115
|
+
let pendingExit = null;
|
|
3116
|
+
let dataSink = null;
|
|
3117
|
+
let exitSink = null;
|
|
3118
|
+
let resolveReady;
|
|
3119
|
+
const ready = new Promise((resolve) => {
|
|
3120
|
+
resolveReady = resolve;
|
|
3121
|
+
});
|
|
3122
|
+
const timer = setTimeout(() => finish({ ok: false, reason: "timeout" }), options.timeoutMs);
|
|
3123
|
+
timer.unref?.();
|
|
3124
|
+
function finish(result) {
|
|
3125
|
+
if (settled)
|
|
3126
|
+
return;
|
|
3127
|
+
settled = true;
|
|
3128
|
+
clearTimeout(timer);
|
|
3129
|
+
if (result.ok)
|
|
3130
|
+
readyOk = true;
|
|
3131
|
+
resolveReady(result);
|
|
3132
|
+
}
|
|
3133
|
+
channel.onData((chunk) => {
|
|
3134
|
+
if (dataSink) {
|
|
3135
|
+
dataSink(chunk);
|
|
3136
|
+
return;
|
|
3137
|
+
}
|
|
3138
|
+
if (readyOk) {
|
|
3139
|
+
// READY already passed; hold the bytes until the broker binds. Bound this
|
|
3140
|
+
// buffer directly against {@link DUPLEX_READINESS_BUFFER_CAP_BYTES}, the
|
|
3141
|
+
// same way the preface-scan gate's own post-preface replay buffer bounds
|
|
3142
|
+
// itself: a worker that keeps sending bytes after READY, faster than the
|
|
3143
|
+
// broker can bind, cannot grow this buffer past the cap. A chunk that
|
|
3144
|
+
// would pass the cap fails closed: the gate drops the pending buffer,
|
|
3145
|
+
// stops the channel, and sets the overflow flag. The caller reads the
|
|
3146
|
+
// flag and selects the file bridge, because `ready` already resolved
|
|
3147
|
+
// before this synchronous post-READY chunk arrived.
|
|
3148
|
+
if (pending.length + chunk.byteLength > DUPLEX_READINESS_BUFFER_CAP_BYTES) {
|
|
3149
|
+
replayOverflow = true;
|
|
3150
|
+
pending = READINESS_EMPTY_BUFFER;
|
|
3151
|
+
channel.stop();
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
// Copy a first chunk instead of aliasing the caller's `Uint8Array`, so a
|
|
3155
|
+
// channel that reuses its delivered buffer across calls cannot corrupt the
|
|
3156
|
+
// bytes this gate holds for the broker replay.
|
|
3157
|
+
pending = pending.length === 0 ? Buffer.from(chunk) : Buffer.concat([pending, chunk]);
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
if (settled) {
|
|
3161
|
+
// The handshake already failed. The channel is untrusted and can keep
|
|
3162
|
+
// sending bytes until the host closes it. Drop them, so a failed handshake
|
|
3163
|
+
// never grows the buffer after the gate settles.
|
|
3164
|
+
return;
|
|
3165
|
+
}
|
|
3166
|
+
// Append the new bytes and continue the newline search from `scanFrom`, the
|
|
3167
|
+
// first index not yet examined. Each byte is read at most one time for the
|
|
3168
|
+
// search, and `appendReadinessBytes` copies at most the incoming chunk, so
|
|
3169
|
+
// the total work stays linear in the bytes received, not in the number of
|
|
3170
|
+
// fragments they arrive in.
|
|
3171
|
+
appendReadinessBytes(chunk);
|
|
3172
|
+
for (;;) {
|
|
3173
|
+
const newlineIndex = findNewlineFrom(buffer, scanFrom);
|
|
3174
|
+
if (newlineIndex === -1) {
|
|
3175
|
+
// No complete line yet. The whole buffer up to the end is now scanned.
|
|
3176
|
+
scanFrom = buffer.length;
|
|
3177
|
+
// The gate reads untrusted bytes, so bound the pre-READY buffer. Past
|
|
3178
|
+
// the cap with no complete READY line, the stream cannot be a valid
|
|
3179
|
+
// READY frame, so finish with protocol contamination. The retained
|
|
3180
|
+
// skipped lines count against the cap; that is acceptable and fail-closed.
|
|
3181
|
+
//
|
|
3182
|
+
// `buffer` is a byte buffer, so `buffer.length` is the exact byte count,
|
|
3183
|
+
// read in O(1).
|
|
3184
|
+
if (buffer.length > DUPLEX_READINESS_BUFFER_CAP_BYTES) {
|
|
3185
|
+
finish({ ok: false, reason: "protocol_contamination" });
|
|
3186
|
+
}
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
if (newlineIndex === lineStart) {
|
|
3190
|
+
// Skip a blank line, the same as the frame decoder. Advance the line-start
|
|
3191
|
+
// cursor past the newline without a buffer copy, then search the next line
|
|
3192
|
+
// from the position after the newline. Enforce the cap after the advance,
|
|
3193
|
+
// so a blank-line flood past the cap fails closed before READY acceptance.
|
|
3194
|
+
lineStart = newlineIndex + 1;
|
|
3195
|
+
scanFrom = newlineIndex + 1;
|
|
3196
|
+
if (lineStart > DUPLEX_READINESS_BUFFER_CAP_BYTES) {
|
|
3197
|
+
finish({ ok: false, reason: "protocol_contamination" });
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
continue;
|
|
3201
|
+
}
|
|
3202
|
+
// A complete non-blank candidate line spans `[lineStart, newlineIndex)`.
|
|
3203
|
+
// Enforce the cap on the line's end offset before the decode, so an over-cap
|
|
3204
|
+
// prefix never reaches READY acceptance on a completed line.
|
|
3205
|
+
if (newlineIndex + 1 > DUPLEX_READINESS_BUFFER_CAP_BYTES) {
|
|
3206
|
+
finish({ ok: false, reason: "protocol_contamination" });
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3209
|
+
// Slice only the single candidate line for the decode.
|
|
3210
|
+
const line = buffer.slice(lineStart, newlineIndex);
|
|
3211
|
+
let decoded = decodeDuplexLine(line);
|
|
3212
|
+
if (!decoded.ok) {
|
|
3213
|
+
// The whole line did not decode. A terminal can put bytes in front of the
|
|
3214
|
+
// gateway's first frame on the same line, with no newline between them: a
|
|
3215
|
+
// shell with bracketed paste enabled writes its disable sequence and a
|
|
3216
|
+
// bare carriage return (`ESC [ ? 2 0 0 4 l CR`) immediately before the
|
|
3217
|
+
// child's first output. So retry the decode from the first `{` in the
|
|
3218
|
+
// line, which is where a frame can start.
|
|
3219
|
+
//
|
|
3220
|
+
// This does not weaken the handshake. The retry still runs the same
|
|
3221
|
+
// strict decode over the remainder of the line, and readiness still
|
|
3222
|
+
// authenticates on the nonce below, so a prefix cannot forge a frame or
|
|
3223
|
+
// smuggle a second one — it can only be discarded.
|
|
3224
|
+
const braceIndex = line.indexOf(READINESS_OPEN_BRACE_BYTE);
|
|
3225
|
+
if (braceIndex > 0) {
|
|
3226
|
+
decoded = decodeDuplexLine(line.slice(braceIndex));
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
if (decoded.ok && decoded.frame.type === "ready") {
|
|
3230
|
+
// A line that decodes as a READY frame authenticates by the nonce. A wrong
|
|
3231
|
+
// nonce fails the handshake; the matching nonce passes it.
|
|
3232
|
+
if (decoded.frame.nonce !== options.nonce) {
|
|
3233
|
+
finish({ ok: false, reason: "nonce_mismatch" });
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
3236
|
+
// The bytes that follow the READY line become the replay buffer for the
|
|
3237
|
+
// broker.
|
|
3238
|
+
//
|
|
3239
|
+
// Copy the suffix instead of slicing it off `buffer`. `buffer` is a view
|
|
3240
|
+
// over `storage`, and `storage`'s capacity can run ahead of the bytes in
|
|
3241
|
+
// use (the doubling growth in `appendReadinessBytes` over-provisions it).
|
|
3242
|
+
// A slice would keep that whole over-provisioned allocation alive for as
|
|
3243
|
+
// long as the broker replay holds its reference. The copy is exactly
|
|
3244
|
+
// `suffix.length` bytes, one time, not a per-fragment cost.
|
|
3245
|
+
const suffix = Buffer.from(buffer.subarray(newlineIndex + 1));
|
|
3246
|
+
// Drop the original pre-READY buffer and its backing storage now. The
|
|
3247
|
+
// gate keeps only the retained suffix as `pending`. This clear also
|
|
3248
|
+
// covers the broker handoff and the replay disposal. Both run later and
|
|
3249
|
+
// read no buffer bytes.
|
|
3250
|
+
buffer = READINESS_EMPTY_BUFFER;
|
|
3251
|
+
storage = READINESS_EMPTY_BUFFER;
|
|
3252
|
+
pending = suffix;
|
|
3253
|
+
finish({ ok: true });
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
// The line does not decode as a READY frame. A PTY echo line or any other
|
|
3257
|
+
// pre-READY noise reaches here. Skip it and keep scanning; the nonce and the
|
|
3258
|
+
// strict schema, not the line position, authenticate readiness. Enforce the
|
|
3259
|
+
// cap after the advance, so a noise-line flood past the cap fails closed.
|
|
3260
|
+
lineStart = newlineIndex + 1;
|
|
3261
|
+
scanFrom = newlineIndex + 1;
|
|
3262
|
+
if (lineStart > DUPLEX_READINESS_BUFFER_CAP_BYTES) {
|
|
3263
|
+
finish({ ok: false, reason: "protocol_contamination" });
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
});
|
|
3268
|
+
channel.onExit((exit) => {
|
|
3269
|
+
if (exitSink) {
|
|
3270
|
+
exitSink(exit);
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
3273
|
+
if (readyOk) {
|
|
3274
|
+
// The channel exited after READY but before the broker bound. Hold the
|
|
3275
|
+
// exit so the broker still learns of the loss.
|
|
3276
|
+
pendingExit = exit;
|
|
3277
|
+
return;
|
|
3278
|
+
}
|
|
3279
|
+
finish({ ok: false, reason: "channel_exit" });
|
|
3280
|
+
});
|
|
3281
|
+
const brokerChannel = {
|
|
3282
|
+
write: (data) => channel.write(data),
|
|
3283
|
+
onData: (listener) => {
|
|
3284
|
+
dataSink = listener;
|
|
3285
|
+
if (pending.length > 0) {
|
|
3286
|
+
const replay = pending;
|
|
3287
|
+
pending = READINESS_EMPTY_BUFFER;
|
|
3288
|
+
listener(replay);
|
|
3289
|
+
return;
|
|
3290
|
+
}
|
|
3291
|
+
},
|
|
3292
|
+
onExit: (listener) => {
|
|
3293
|
+
exitSink = listener;
|
|
3294
|
+
if (pendingExit) {
|
|
3295
|
+
const exit = pendingExit;
|
|
3296
|
+
pendingExit = null;
|
|
3297
|
+
listener(exit);
|
|
3298
|
+
}
|
|
3299
|
+
},
|
|
3300
|
+
stop: () => channel.stop(),
|
|
3301
|
+
close: () => channel.close(),
|
|
3302
|
+
};
|
|
3303
|
+
return {
|
|
3304
|
+
ready,
|
|
3305
|
+
brokerChannel,
|
|
3306
|
+
replayOverflowed: () => replayOverflow,
|
|
3307
|
+
disposePendingReplay: () => {
|
|
3308
|
+
pending = READINESS_EMPTY_BUFFER;
|
|
3309
|
+
},
|
|
3310
|
+
retainedReadinessBufferLength: () => buffer.length,
|
|
3311
|
+
};
|
|
3312
|
+
}
|
|
3313
|
+
/**
|
|
3314
|
+
* Close a partial duplex channel inside a bounded budget, then stop the child.
|
|
3315
|
+
* The host runs this on any readiness failure, so a failed duplex attempt leaves
|
|
3316
|
+
* no live provider session before the fallback to the file bridge.
|
|
3317
|
+
*/
|
|
3318
|
+
async function closeDuplexChannelWithinBudget(channel, budgetMs) {
|
|
3319
|
+
try {
|
|
3320
|
+
let timer;
|
|
3321
|
+
const budget = new Promise((resolve) => {
|
|
3322
|
+
timer = setTimeout(resolve, budgetMs);
|
|
3323
|
+
timer.unref?.();
|
|
3324
|
+
});
|
|
3325
|
+
await Promise.race([channel.close().catch(() => undefined), budget]);
|
|
3326
|
+
if (timer !== undefined)
|
|
3327
|
+
clearTimeout(timer);
|
|
3328
|
+
}
|
|
3329
|
+
catch {
|
|
3330
|
+
// Best effort: the stop below still removes the child.
|
|
3331
|
+
}
|
|
3332
|
+
finally {
|
|
3333
|
+
try {
|
|
3334
|
+
channel.stop();
|
|
3335
|
+
}
|
|
3336
|
+
catch {
|
|
3337
|
+
// The channel is already gone; nothing more to do.
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
export async function startAdapterExecutionTargetPaperclipBridge(input) {
|
|
3342
|
+
if (!adapterExecutionTargetUsesPaperclipBridge(input.target)) {
|
|
3343
|
+
return null;
|
|
3344
|
+
}
|
|
3345
|
+
if (!input.target || input.target.kind !== "remote") {
|
|
3346
|
+
return null;
|
|
3347
|
+
}
|
|
3348
|
+
const target = input.target;
|
|
3349
|
+
const onLog = input.onLog ?? (async () => { });
|
|
3350
|
+
const hostApiToken = input.hostApiToken?.trim() ?? "";
|
|
3351
|
+
if (hostApiToken.length === 0) {
|
|
3352
|
+
throw new Error("Sandbox bridge mode requires a host-side Paperclip API token.");
|
|
3353
|
+
}
|
|
3354
|
+
// The forward budget for one relayed request. It stays at the broker's default
|
|
3355
|
+
// forward budget (30 s) when the caller sets no option, so current behavior
|
|
3356
|
+
// does not change.
|
|
3357
|
+
const forwardTimeoutMs = input.forwardTimeoutMs ?? DEFAULT_DUPLEX_BROKER_BUDGETS.forwardTimeoutMs;
|
|
3358
|
+
const runtimeRootDir = input.runtimeRootDir?.trim().length
|
|
3359
|
+
? input.runtimeRootDir.trim()
|
|
3360
|
+
: path.posix.join(target.remoteCwd, ".paperclip-runtime", input.adapterKey);
|
|
3361
|
+
const bridgeRuntimeDir = path.posix.join(runtimeRootDir, "paperclip-bridge");
|
|
3362
|
+
const queueDir = path.posix.join(bridgeRuntimeDir, "queue");
|
|
3363
|
+
const assetRemoteDir = path.posix.join(bridgeRuntimeDir, "server");
|
|
3364
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
3365
|
+
const configuredAttachmentBytes = Number(process.env.PAPERCLIP_ATTACHMENT_MAX_BYTES);
|
|
3366
|
+
// A larger upload limit needs multipart headroom. A smaller attachment limit
|
|
3367
|
+
// remains enforced by the API and must not shrink unrelated JSON responses.
|
|
3368
|
+
const defaultBodyBytes = Number.isSafeInteger(configuredAttachmentBytes) && configuredAttachmentBytes > 0
|
|
3369
|
+
? Math.max(DEFAULT_SANDBOX_CALLBACK_BRIDGE_MAX_BODY_BYTES, configuredAttachmentBytes + 64 * 1024)
|
|
3370
|
+
: DEFAULT_SANDBOX_CALLBACK_BRIDGE_MAX_BODY_BYTES;
|
|
3371
|
+
const maxBodyBytes = typeof input.maxBodyBytes === "number" && Number.isFinite(input.maxBodyBytes) && input.maxBodyBytes > 0
|
|
3372
|
+
? Math.trunc(input.maxBodyBytes)
|
|
3373
|
+
: defaultBodyBytes;
|
|
3374
|
+
// The bridge worker runs inside the same process that serves the Paperclip
|
|
3375
|
+
// API, so forwarded sandbox calls must target the LOCAL listen origin. The
|
|
3376
|
+
// PAPERCLIP_RUNTIME_API_URL / PAPERCLIP_API_URL exports now prefer a
|
|
3377
|
+
// configured public base URL, which is the origin browsers and external
|
|
3378
|
+
// agents use; routing this in-process loopback hop through the network edge
|
|
3379
|
+
// breaks deployments whose public origin sits behind a session-gated proxy
|
|
3380
|
+
// (every forwarded agent API call is rejected at the edge). Server boot
|
|
3381
|
+
// exports PAPERCLIP_LISTEN_HOST / PAPERCLIP_LISTEN_PORT before any run
|
|
3382
|
+
// executes, and resolveDefaultPaperclipApiUrl() maps wildcard listen hosts
|
|
3383
|
+
// to the loopback address of the same family (0.0.0.0 -> 127.0.0.1,
|
|
3384
|
+
// :: -> [::1]), so the fallback is always loopback-reachable.
|
|
3385
|
+
// input.hostApiUrl stays available as an explicit override seam.
|
|
3386
|
+
const hostApiUrl = input.hostApiUrl?.trim() || resolveDefaultPaperclipApiUrl();
|
|
3387
|
+
const shellCommand = adapterExecutionTargetShellCommand(target);
|
|
3388
|
+
const runner = adapterExecutionTargetCommandRunner(target);
|
|
3389
|
+
const bridgeTimeoutMs = typeof input.timeoutSec === "number" && Number.isFinite(input.timeoutSec) && input.timeoutSec > 0
|
|
3390
|
+
? Math.trunc(input.timeoutSec * 1000)
|
|
3391
|
+
: adapterExecutionTargetTimeoutMs(target);
|
|
3392
|
+
await onLog("stdout", `[paperclip] Starting sandbox callback bridge for ${input.adapterKey} in ${bridgeRuntimeDir}.\n`);
|
|
3393
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
3394
|
+
// The provider-scoped telemetry facade for the fixed duplex observability
|
|
3395
|
+
// surface. It maps the raw provider key through the allowlist one time, so no
|
|
3396
|
+
// raw plugin key reaches a span, a counter, or the event. The default recorder
|
|
3397
|
+
// is a no-op, so the facade is inert until the host injects a real recorder.
|
|
3398
|
+
const duplexProviderKey = "providerKey" in target ? target.providerKey ?? undefined : undefined;
|
|
3399
|
+
const duplexObservability = createDuplexObservability({
|
|
3400
|
+
recorder: input.duplexObservabilityRecorder ?? undefined,
|
|
3401
|
+
providerKey: duplexProviderKey,
|
|
3402
|
+
// http2_v1 is the one active non-file transport now; every non-file
|
|
3403
|
+
// record this facade produces stamps the `http2` transport value.
|
|
3404
|
+
transport: "http2",
|
|
3405
|
+
});
|
|
3406
|
+
// PAPERCLIP_BRIDGE_DEBUG opts into verbose stdout logs of every bridge proxy
|
|
3407
|
+
// request/response. The query string is logged verbatim, so callers who pass
|
|
3408
|
+
// auth tokens or other sensitive values as query parameters should be aware
|
|
3409
|
+
// those values appear in the host process's stdout when this flag is enabled.
|
|
3410
|
+
// Only intended for active debugging in trusted environments.
|
|
3411
|
+
const bridgeDebugEnabled = isBridgeDebugEnabled(process.env);
|
|
3412
|
+
// One forward of a relayed sandbox request onto the existing Paperclip API
|
|
3413
|
+
// path. The forward applies the real host token and the signed run id, so the
|
|
3414
|
+
// token replacement and the run attribution stay in one place for both the
|
|
3415
|
+
// file bridge and the duplex broker. The sandbox request carries only the
|
|
3416
|
+
// bridge token; the real agent token never leaves the host.
|
|
3417
|
+
const forwardBridgeRequest = async (request, signal, options) => {
|
|
3418
|
+
const method = request.method.trim().toUpperCase() || "GET";
|
|
3419
|
+
// The per-request debug log prints the method, the path, and the query. The
|
|
3420
|
+
// duplex path suppresses it, so no route or query rides a log line there. The
|
|
3421
|
+
// file path keeps the existing behavior.
|
|
3422
|
+
const emitDebugLog = bridgeDebugEnabled && options?.suppressDebugLog !== true;
|
|
3423
|
+
if (emitDebugLog) {
|
|
3424
|
+
await onLog("stdout", `[paperclip] Bridge proxy ${method} ${request.path}${request.query ? `?${request.query}` : ""}\n`);
|
|
3425
|
+
}
|
|
3426
|
+
const headers = new Headers();
|
|
3427
|
+
for (const [key, value] of Object.entries(request.headers)) {
|
|
3428
|
+
if (value.trim().length === 0)
|
|
3429
|
+
continue;
|
|
3430
|
+
headers.set(key, value);
|
|
3431
|
+
}
|
|
3432
|
+
headers.set("authorization", `Bearer ${hostApiToken}`);
|
|
3433
|
+
headers.set("x-paperclip-run-id", input.runId);
|
|
3434
|
+
// Abort the forward when the caller aborts the request (its per-iteration
|
|
3435
|
+
// timeout or watchdog fired, or the broker's forward budget ended), or after
|
|
3436
|
+
// the forward budget here, whichever comes first.
|
|
3437
|
+
const timeoutSignal = AbortSignal.timeout(forwardTimeoutMs);
|
|
3438
|
+
const forwardSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
3439
|
+
// Build the request-body init. A GET or a HEAD carries no body. The file
|
|
3440
|
+
// bridge passes legacy JSON as a string and binary data as a `Buffer`;
|
|
3441
|
+
// HTTP/2 passes raw `Buffer` bodies. Undici accepts a `Buffer` request body directly (a
|
|
3442
|
+
// `Buffer` is an `ArrayBufferView`), so neither shape needs a conversion.
|
|
3443
|
+
// The cast below only bridges a `BodyInit` typing gap: the DOM library
|
|
3444
|
+
// type this project's ambient `RequestInit` resolves to excludes a
|
|
3445
|
+
// `Buffer`, though Undici accepts one at runtime.
|
|
3446
|
+
const forwardInit = {
|
|
3447
|
+
method,
|
|
3448
|
+
headers,
|
|
3449
|
+
signal: forwardSignal,
|
|
3450
|
+
};
|
|
3451
|
+
if (method !== "GET" && method !== "HEAD" && request.body !== undefined) {
|
|
3452
|
+
forwardInit.body = request.body;
|
|
3453
|
+
}
|
|
3454
|
+
const response = await fetch(buildBridgeForwardUrl(hostApiUrl, request), forwardInit);
|
|
3455
|
+
if (emitDebugLog) {
|
|
3456
|
+
await onLog("stdout", `[paperclip] Bridge proxy response ${response.status} for ${method} ${request.path}${request.query ? `?${request.query}` : ""}\n`);
|
|
3457
|
+
}
|
|
3458
|
+
// The host delivered response headers, so the response-body read starts after
|
|
3459
|
+
// the host processed the request. A later response-body read failure (a body
|
|
3460
|
+
// over the size limit, or a stream read error) needs a classification by
|
|
3461
|
+
// method safety. A safe method (GET, HEAD, OPTIONS, TRACE) never changes host
|
|
3462
|
+
// state, so a retry cannot double-apply a mutation and the failure stays
|
|
3463
|
+
// retryable. A mutating or otherwise unsafe method may have committed on the
|
|
3464
|
+
// host, so a retryable status is unsafe: it makes the caller repeat the
|
|
3465
|
+
// request with a new request id outside the broker deduplication set, and the
|
|
3466
|
+
// host applies the mutation twice. For an unsafe method the code returns a
|
|
3467
|
+
// non-retryable 504 and marks the outcome indeterminate, exactly like an
|
|
3468
|
+
// aborted in-flight forward. The in-sandbox server maps the indeterminate 504
|
|
3469
|
+
// to a non-retryable 409 for both the file bridge and the duplex broker.
|
|
3470
|
+
let responseBody;
|
|
3471
|
+
try {
|
|
3472
|
+
responseBody = await readBridgeForwardResponseBody(response, maxBodyBytes, options?.reservation);
|
|
3473
|
+
}
|
|
3474
|
+
catch (error) {
|
|
3475
|
+
// A denied reservation is retryable capacity pressure for a safe
|
|
3476
|
+
// method, not a body-read fault: rethrow it before the method-safety
|
|
3477
|
+
// classification below runs, so it reaches the HTTP/2 bridge's own
|
|
3478
|
+
// capacity-denial catch (which answers the retryable 503 and settles
|
|
3479
|
+
// the stream) instead of this function turning it into a 502. For an
|
|
3480
|
+
// unsafe (mutating) method, the host has already delivered response
|
|
3481
|
+
// headers by this point, so it may already have committed the
|
|
3482
|
+
// mutation. Rethrowing there too would let the retryable 503 reach a
|
|
3483
|
+
// caller that repeats the request, applying the mutation twice. An
|
|
3484
|
+
// unsafe method's capacity denial falls through to the same
|
|
3485
|
+
// non-retryable indeterminate 504 any other response-body read fault
|
|
3486
|
+
// gets below.
|
|
3487
|
+
if (error instanceof BridgeProcessCapacityError && isSafeBridgeMethod(method))
|
|
3488
|
+
throw error;
|
|
3489
|
+
if (isSafeBridgeMethod(method)) {
|
|
3490
|
+
// The method is safe, so a retry cannot double-apply a mutation. Return a
|
|
3491
|
+
// retryable 502 with no indeterminate marker, so the gateway passes it
|
|
3492
|
+
// through as a retryable status.
|
|
3493
|
+
return {
|
|
3494
|
+
status: 502,
|
|
3495
|
+
headers: { "content-type": "application/json" },
|
|
3496
|
+
body: Buffer.from(JSON.stringify({
|
|
3497
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3498
|
+
}), "utf8"),
|
|
3499
|
+
};
|
|
3500
|
+
}
|
|
3501
|
+
return {
|
|
3502
|
+
status: 504,
|
|
3503
|
+
headers: {
|
|
3504
|
+
"content-type": "application/json",
|
|
3505
|
+
"x-paperclip-bridge-outcome": "indeterminate",
|
|
3506
|
+
},
|
|
3507
|
+
body: Buffer.from(JSON.stringify({
|
|
3508
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3509
|
+
outcome: "indeterminate",
|
|
3510
|
+
retryable: false,
|
|
3511
|
+
}), "utf8"),
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3514
|
+
const commentMarker = postedIssueCommentLogMarker(method, request.path, response.status, responseBody);
|
|
3515
|
+
if (commentMarker)
|
|
3516
|
+
await onLog("stdout", commentMarker);
|
|
3517
|
+
return {
|
|
3518
|
+
status: response.status,
|
|
3519
|
+
headers: buildBridgeResponseHeaders(response),
|
|
3520
|
+
body: responseBody,
|
|
3521
|
+
};
|
|
3522
|
+
};
|
|
3523
|
+
// Two strict gates guard the duplex transport. Select duplex only when the
|
|
3524
|
+
// experimental setting is exactly `true`, the resolved capability
|
|
3525
|
+
// `duplexCommandStream` is exactly `true`, and the runner exposes the duplex
|
|
3526
|
+
// channel. Any other value of either gate selects the file bridge below.
|
|
3527
|
+
// HTTP/2 is the preferred transport. `queue_v1` is the soft-deprecated fallback.
|
|
3528
|
+
const duplexRequested = input.enableSandboxDuplexBridge === true;
|
|
3529
|
+
const capabilityGranted = "effectiveCapabilities" in target &&
|
|
3530
|
+
target.effectiveCapabilities?.duplexCommandStream === true;
|
|
3531
|
+
const openDuplexChannel = runner.openDuplexChannel?.bind(runner);
|
|
3532
|
+
// Record the pre-attempt fallback for a file-bridge selection that opens no
|
|
3533
|
+
// channel. `gate_off` marks the kill switch off; `capability_absent` marks the
|
|
3534
|
+
// capability or the runner method absent. A later channel-open failure records
|
|
3535
|
+
// its own fallback through the channel-open attempt below.
|
|
3536
|
+
if (!duplexRequested) {
|
|
3537
|
+
duplexObservability.recordFallback("gate_off");
|
|
3538
|
+
}
|
|
3539
|
+
else if (!capabilityGranted || typeof openDuplexChannel !== "function") {
|
|
3540
|
+
duplexObservability.recordFallback("capability_absent");
|
|
3541
|
+
}
|
|
3542
|
+
if (duplexRequested && capabilityGranted && typeof openDuplexChannel === "function") {
|
|
3543
|
+
// Begin the channel-open attempt. The block reports exactly one terminal:
|
|
3544
|
+
// `ready` on success, or `fallback(reason)` on an open or a readiness failure.
|
|
3545
|
+
const duplexChannelOpen = duplexObservability.startChannelOpen();
|
|
3546
|
+
const readinessTimeoutMs = typeof input.duplexReadinessTimeoutMs === "number" &&
|
|
3547
|
+
Number.isFinite(input.duplexReadinessTimeoutMs) &&
|
|
3548
|
+
input.duplexReadinessTimeoutMs > 0
|
|
3549
|
+
? Math.trunc(input.duplexReadinessTimeoutMs)
|
|
3550
|
+
: DEFAULT_DUPLEX_READINESS_TIMEOUT_MS;
|
|
3551
|
+
// The host assigns the loopback port before it opens the channel. It passes
|
|
3552
|
+
// the port and one random per-open nonce to the gateway only through the
|
|
3553
|
+
// launch environment. It builds the sandbox-facing origin from its own
|
|
3554
|
+
// stored port. No field of any channel frame contributes to the endpoint.
|
|
3555
|
+
//
|
|
3556
|
+
// Provider-boundary rationale (a security-review condition): the Daytona SDK
|
|
3557
|
+
// surface the plugin uses (`sandbox.process` sessions, PTY, and exec;
|
|
3558
|
+
// `sandbox.fs`) exposes no listener identity that the provider control plane
|
|
3559
|
+
// binds to a launched process. No channel-supplied address can be attested,
|
|
3560
|
+
// so this design uses zero channel-supplied addressing data. The only actor
|
|
3561
|
+
// that can pre-bind the host-assigned loopback port before the agent starts
|
|
3562
|
+
// is the provider itself, which already delivers the agent launch
|
|
3563
|
+
// environment. The gateway bind-or-exit contract fails closed. This is an
|
|
3564
|
+
// accepted provider-untrusted residual, not endpoint authentication: the
|
|
3565
|
+
// nonce is a liveness signal, because a compromised channel can read the
|
|
3566
|
+
// launch environment; the endpoint stays safe because it never derives from
|
|
3567
|
+
// the channel.
|
|
3568
|
+
const assignedPort = await reserveHostAssignedLoopbackPort();
|
|
3569
|
+
const nonce = randomBytes(16).toString("hex");
|
|
3570
|
+
const sandboxOrigin = `http://127.0.0.1:${assignedPort}`;
|
|
3571
|
+
let channel = null;
|
|
3572
|
+
// The open runs two stages under one try: the entrypoint sync, then the
|
|
3573
|
+
// channel open. The stage names the exact open-failure outcome in the catch.
|
|
3574
|
+
let openStage = "entrypoint_sync";
|
|
3575
|
+
try {
|
|
3576
|
+
const assetSync = await syncSandboxCallbackBridgeEntrypoint({
|
|
3577
|
+
runner,
|
|
3578
|
+
remoteCwd: target.remoteCwd,
|
|
3579
|
+
assetRemoteDir,
|
|
3580
|
+
bridgeAsset,
|
|
3581
|
+
timeoutMs: bridgeTimeoutMs,
|
|
3582
|
+
shellCommand,
|
|
3583
|
+
});
|
|
3584
|
+
const gatewayEnv = {
|
|
3585
|
+
PAPERCLIP_API_BRIDGE_MODE: SANDBOX_CALLBACK_BRIDGE_HTTP2_MODE,
|
|
3586
|
+
PAPERCLIP_BRIDGE_TOKEN: bridgeToken,
|
|
3587
|
+
PAPERCLIP_BRIDGE_HOST: "127.0.0.1",
|
|
3588
|
+
PAPERCLIP_BRIDGE_PORT: String(assignedPort),
|
|
3589
|
+
PAPERCLIP_BRIDGE_NONCE: nonce,
|
|
3590
|
+
PAPERCLIP_BRIDGE_MAX_BODY_BYTES: String(maxBodyBytes),
|
|
3591
|
+
};
|
|
3592
|
+
const command = buildDuplexGatewayLaunchArgv({
|
|
3593
|
+
shellCommand,
|
|
3594
|
+
remoteEntrypoint: assetSync.remoteEntrypoint,
|
|
3595
|
+
env: gatewayEnv,
|
|
3596
|
+
});
|
|
3597
|
+
openStage = "channel_open";
|
|
3598
|
+
channel = await openDuplexChannel({ command });
|
|
3599
|
+
}
|
|
3600
|
+
catch (error) {
|
|
3601
|
+
// The channel never opened, so no request could carry the bridge token.
|
|
3602
|
+
// The open call is the last statement of the try, so `channel` is still
|
|
3603
|
+
// null here; there is nothing to close. Fall through to the file bridge
|
|
3604
|
+
// below. Bind and keep the caught error, so the fallback names the exact
|
|
3605
|
+
// open-failure stage: a full process-scoped route ceiling is `route_busy`;
|
|
3606
|
+
// otherwise the stage is the entrypoint sync or the channel open. The log
|
|
3607
|
+
// line names only the fixed stage enum, so no raw provider error rides a log
|
|
3608
|
+
// line on the duplex path.
|
|
3609
|
+
const reason = isDuplexRouteBusyError(error)
|
|
3610
|
+
? "route_busy"
|
|
3611
|
+
: openStage === "entrypoint_sync"
|
|
3612
|
+
? "entrypoint_sync_failed"
|
|
3613
|
+
: "channel_open_failed";
|
|
3614
|
+
duplexChannelOpen.fallback(reason);
|
|
3615
|
+
await onLog("stderr", `[paperclip] Could not open the sandbox duplex channel (${reason}). Using the file bridge.\n`);
|
|
3616
|
+
channel = null;
|
|
3617
|
+
}
|
|
3618
|
+
if (channel) {
|
|
3619
|
+
const gate = createDuplexReadinessGate(channel, {
|
|
3620
|
+
nonce,
|
|
3621
|
+
timeoutMs: readinessTimeoutMs,
|
|
3622
|
+
});
|
|
3623
|
+
const readiness = await gate.ready;
|
|
3624
|
+
if (!readiness.ok) {
|
|
3625
|
+
// Fail closed. Close the partial channel inside a bounded budget, then
|
|
3626
|
+
// select the file bridge. The broker never started, so no request that
|
|
3627
|
+
// carries the bridge token reached the channel or any endpoint. The reason
|
|
3628
|
+
// is a fixed enum, so it rides the log line and the fallback telemetry
|
|
3629
|
+
// with no raw value.
|
|
3630
|
+
gate.disposePendingReplay();
|
|
3631
|
+
await closeDuplexChannelWithinBudget(channel, DEFAULT_DUPLEX_CLEANUP_BUDGET_MS);
|
|
3632
|
+
duplexChannelOpen.fallback(duplexReadinessFallbackReason(readiness.reason));
|
|
3633
|
+
await onLog("stderr", `[paperclip] Sandbox duplex readiness failed (${readiness.reason}). Using the file bridge.\n`);
|
|
3634
|
+
}
|
|
3635
|
+
else {
|
|
3636
|
+
// Readiness passed. The gate retained every byte that followed the
|
|
3637
|
+
// accepted READY line. Scan those retained bytes for the HTTP/2
|
|
3638
|
+
// client connection preface — the scan window opens only now, after
|
|
3639
|
+
// the gate accepted the nonce — and start the HTTP/2 session at that
|
|
3640
|
+
// offset, inclusive. A missing preface inside the bounded readiness
|
|
3641
|
+
// buffer aborts the open; the run falls back to `queue_v1` exactly
|
|
3642
|
+
// one time (accepted security fix 6).
|
|
3643
|
+
const openedChannel = channel;
|
|
3644
|
+
const prefaceScan = scanForHttp2ClientPreface(gate.brokerChannel, {
|
|
3645
|
+
capBytes: DUPLEX_READINESS_BUFFER_CAP_BYTES,
|
|
3646
|
+
timeoutMs: readinessTimeoutMs,
|
|
3647
|
+
});
|
|
3648
|
+
const prefaceResult = await prefaceScan.settled;
|
|
3649
|
+
if (prefaceResult === "missing") {
|
|
3650
|
+
// Fail closed, the same shape as a readiness failure: close the
|
|
3651
|
+
// partial channel inside the cleanup budget, then select the file
|
|
3652
|
+
// bridge. No HTTP/2 server ever bound to this channel, so no
|
|
3653
|
+
// request reached it or any endpoint.
|
|
3654
|
+
gate.disposePendingReplay();
|
|
3655
|
+
await closeDuplexChannelWithinBudget(openedChannel, DEFAULT_DUPLEX_CLEANUP_BUDGET_MS);
|
|
3656
|
+
duplexChannelOpen.fallback("preface_missing");
|
|
3657
|
+
await onLog("stderr", "[paperclip] Sandbox HTTP/2 client preface did not appear inside the bounded readiness buffer (preface_missing). Using the file bridge.\n");
|
|
3658
|
+
}
|
|
3659
|
+
else {
|
|
3660
|
+
// The run disposition latch for the http2_v1 path, in the same
|
|
3661
|
+
// shape the retired duplex_v1 broker exposed. A loss ordered before
|
|
3662
|
+
// an orderly completion reports a failure with the typed loss
|
|
3663
|
+
// reason; every other state reports a success.
|
|
3664
|
+
const dispositionLatch = createHttp2RunDispositionLatch();
|
|
3665
|
+
// Set before the first await inside the forward handler, so a loss
|
|
3666
|
+
// that lands mid-request still classifies as `post_dispatch`.
|
|
3667
|
+
let anyStreamDispatched = false;
|
|
3668
|
+
const recordHttp2Loss = (event) => {
|
|
3669
|
+
const reason = mapHttp2EventToDuplexLossReason(event);
|
|
3670
|
+
const disposedNow = dispositionLatch.recordLoss(reason);
|
|
3671
|
+
if (!disposedNow && (reason === "provider_exit" || reason === "transport_closed")) {
|
|
3672
|
+
// A clean channel end that orders after a host-observed orderly
|
|
3673
|
+
// completion is a normal teardown, not a loss: the run already
|
|
3674
|
+
// completed. Emit no loss telemetry and no log line for it, the
|
|
3675
|
+
// same policy the retired duplex_v1 broker applied.
|
|
3676
|
+
return;
|
|
3677
|
+
}
|
|
3678
|
+
const lossClass = anyStreamDispatched ? "post_dispatch" : "pre_dispatch";
|
|
3679
|
+
duplexObservability.recordLoss(lossClass, reason);
|
|
3680
|
+
void onLog("stderr", `[paperclip] Sandbox HTTP/2 channel lost (${reason}). The run fails.\n`);
|
|
3681
|
+
};
|
|
3682
|
+
// The forward handler applies the real host token and the run id
|
|
3683
|
+
// through the existing `forwardBridgeRequest` — the same function
|
|
3684
|
+
// the file bridge uses. The route allowlist, the header allowlist,
|
|
3685
|
+
// and the per-request debug-log suppression already ran inside
|
|
3686
|
+
// `createHttp2BridgeServer`'s own stream handler before this call.
|
|
3687
|
+
// It records the request span with the same latency-and-outcome
|
|
3688
|
+
// shape the retired duplex_v1 broker recorded: `ok` for any
|
|
3689
|
+
// delivered host response (any status), `error` only when the
|
|
3690
|
+
// forward call itself throws.
|
|
3691
|
+
const http2ForwardRequest = async (request) => {
|
|
3692
|
+
anyStreamDispatched = true;
|
|
3693
|
+
const dispatchStartMs = Date.now();
|
|
3694
|
+
try {
|
|
3695
|
+
const result = await forwardBridgeRequest({
|
|
3696
|
+
method: request.method,
|
|
3697
|
+
path: request.pathname,
|
|
3698
|
+
query: request.query,
|
|
3699
|
+
headers: request.headers,
|
|
3700
|
+
body: request.body,
|
|
3701
|
+
}, request.signal, { suppressDebugLog: true, reservation: request.reservation });
|
|
3702
|
+
duplexObservability.recordRequest({ latencyMs: Date.now() - dispatchStartMs, outcome: "ok" });
|
|
3703
|
+
return { status: result.status, headers: result.headers, body: result.body };
|
|
3704
|
+
}
|
|
3705
|
+
catch (error) {
|
|
3706
|
+
duplexObservability.recordRequest({ latencyMs: Date.now() - dispatchStartMs, outcome: "error" });
|
|
3707
|
+
throw error;
|
|
3708
|
+
}
|
|
3709
|
+
};
|
|
3710
|
+
const http2Server = createHttp2BridgeServer({
|
|
3711
|
+
bridgeToken,
|
|
3712
|
+
forwardRequest: http2ForwardRequest,
|
|
3713
|
+
routes: HTTP2_SANDBOX_CALLBACK_BRIDGE_ROUTE_ALLOWLIST,
|
|
3714
|
+
// The same resolved limit the launch environment hands the
|
|
3715
|
+
// sandbox-side gateway (`PAPERCLIP_BRIDGE_MAX_BODY_BYTES`,
|
|
3716
|
+
// below), so the host check and the gateway check enforce one
|
|
3717
|
+
// value instead of the host silently falling back to the
|
|
3718
|
+
// package default.
|
|
3719
|
+
maxBodyBytes,
|
|
3720
|
+
onGoaway: () => recordHttp2Loss("session_goaway"),
|
|
3721
|
+
onSessionError: () => recordHttp2Loss("session_error"),
|
|
3722
|
+
});
|
|
3723
|
+
// Combine the loss hook with the forward-to-Duplex handoff inside
|
|
3724
|
+
// one `onExit` registration: the channel primitive holds exactly
|
|
3725
|
+
// one listener slot, and `bindChannel` below registers the one that
|
|
3726
|
+
// ends the wrapped `Duplex`.
|
|
3727
|
+
//
|
|
3728
|
+
// This object's `stop()` is also the real sandbox-side effect of
|
|
3729
|
+
// the post-bind read backpressure bound `bindChannel` applies
|
|
3730
|
+
// (`wrapDuplexChannelAsNodeDuplex` in `http2-bridge-server.ts`):
|
|
3731
|
+
// this raw provider channel exposes no pause, so once the bounded
|
|
3732
|
+
// read queue there overflows, it calls `stop()` through this exact
|
|
3733
|
+
// chain, down to `prefaceScan.scanned.stop()` and on to the real
|
|
3734
|
+
// channel, instead of letting sandbox-controlled bytes grow host
|
|
3735
|
+
// memory with no bound.
|
|
3736
|
+
const channelForHttp2Server = {
|
|
3737
|
+
write: (data) => prefaceScan.scanned.write(data),
|
|
3738
|
+
onData: (listener) => prefaceScan.scanned.onData(listener),
|
|
3739
|
+
onExit: (listener) => {
|
|
3740
|
+
prefaceScan.scanned.onExit((exit) => {
|
|
3741
|
+
recordHttp2Loss("channel_exit");
|
|
3742
|
+
listener(exit);
|
|
3743
|
+
});
|
|
3744
|
+
},
|
|
3745
|
+
stop: () => prefaceScan.scanned.stop(),
|
|
3746
|
+
close: () => prefaceScan.scanned.close(),
|
|
3747
|
+
};
|
|
3748
|
+
const boundDuplex = http2Server.bindChannel(channelForHttp2Server);
|
|
3749
|
+
// Also catches the `Duplex` this wrapper destroys when the bounded
|
|
3750
|
+
// read backpressure queue overflows post-bind, so that loss still
|
|
3751
|
+
// reaches `recordHttp2Loss` the same way any other write fault does.
|
|
3752
|
+
boundDuplex.on("error", () => recordHttp2Loss("write_error"));
|
|
3753
|
+
duplexChannelOpen.ready();
|
|
3754
|
+
await onLog("stdout", "[paperclip] Sandbox HTTP/2 transport ready; serving the host-assigned origin.\n");
|
|
3755
|
+
// Stream run logs on the http2 path with the same gate and the same
|
|
3756
|
+
// log line as the file path. The http2 path starts no file-bridge
|
|
3757
|
+
// worker, so create the log directory before the tail starts.
|
|
3758
|
+
let duplexRunLogTail = null;
|
|
3759
|
+
if (target.transport === "sandbox" && target.streamRunLogs !== false) {
|
|
3760
|
+
const duplexLogsDir = sandboxCallbackBridgeDirectories(queueDir).logsDir;
|
|
3761
|
+
await ensureSandboxRunLogDirectory({
|
|
3762
|
+
runner,
|
|
3763
|
+
remoteCwd: target.remoteCwd,
|
|
3764
|
+
logsDir: duplexLogsDir,
|
|
3765
|
+
shellCommand,
|
|
3766
|
+
timeoutMs: bridgeTimeoutMs,
|
|
3767
|
+
});
|
|
3768
|
+
duplexRunLogTail = createSandboxRunLogTailFactory({
|
|
3769
|
+
runner,
|
|
3770
|
+
remoteCwd: target.remoteCwd,
|
|
3771
|
+
logsDir: duplexLogsDir,
|
|
3772
|
+
shellCommand,
|
|
3773
|
+
});
|
|
3774
|
+
await onLog("stdout", "[paperclip] Sandbox run log streaming enabled for this run.\n");
|
|
3775
|
+
}
|
|
3776
|
+
return {
|
|
3777
|
+
env: {
|
|
3778
|
+
PAPERCLIP_API_URL: sandboxOrigin,
|
|
3779
|
+
PAPERCLIP_API_KEY: bridgeToken,
|
|
3780
|
+
PAPERCLIP_API_BRIDGE_MODE: SANDBOX_CALLBACK_BRIDGE_HTTP2_MODE,
|
|
3781
|
+
},
|
|
3782
|
+
runLogTail: duplexRunLogTail,
|
|
3783
|
+
readRunDisposition: () => dispositionLatch.disposition,
|
|
3784
|
+
// Atomically read the latch and mark the orderly completion for the
|
|
3785
|
+
// ACP success-eligible terminal, so no await separates the read from
|
|
3786
|
+
// the mark and a teardown loss cannot slip in between.
|
|
3787
|
+
settleRunDisposition: () => dispositionLatch.settleRunDisposition(),
|
|
3788
|
+
markOrderlyCompletion: () => dispositionLatch.markOrderlyCompletion(),
|
|
3789
|
+
onLoss: (listener) => dispositionLatch.onLoss(listener),
|
|
3790
|
+
stop: async () => {
|
|
3791
|
+
// Close the HTTP/2 server's sessions, then the channel, before
|
|
3792
|
+
// lease release, so no live provider session remains when the
|
|
3793
|
+
// caller releases the lease.
|
|
3794
|
+
await http2Server.close();
|
|
3795
|
+
await closeDuplexChannelWithinBudget(openedChannel, DEFAULT_DUPLEX_CLEANUP_BUDGET_MS);
|
|
3796
|
+
await bridgeAsset.cleanup();
|
|
3797
|
+
},
|
|
3798
|
+
};
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
let server = null;
|
|
3804
|
+
let worker = null;
|
|
3805
|
+
try {
|
|
3806
|
+
const client = createCommandManagedSandboxCallbackBridgeQueueClient({
|
|
3807
|
+
runner,
|
|
3808
|
+
remoteCwd: target.remoteCwd,
|
|
3809
|
+
timeoutMs: bridgeTimeoutMs,
|
|
3810
|
+
shellCommand,
|
|
3811
|
+
});
|
|
3812
|
+
// `startSandboxCallbackBridgeWorker` keeps its awaited queue-directory
|
|
3813
|
+
// setup on the active `bridge.paperclip` step, and runs each request under
|
|
3814
|
+
// the run parent context (see `runWithRuntimeParent` inside that function).
|
|
3815
|
+
// So the startup `mkdir` execs stay parented to the step, and every later
|
|
3816
|
+
// request `sandbox.exec` span parents to the live run span.
|
|
3817
|
+
worker = await startSandboxCallbackBridgeWorker({
|
|
3818
|
+
client,
|
|
3819
|
+
queueDir,
|
|
3820
|
+
maxBodyBytes,
|
|
3821
|
+
getRuntimeParentContext: input.getRuntimeParentContext,
|
|
3822
|
+
runtimeSpan: input.runtimeSpan,
|
|
3823
|
+
// The worker encodes binary bodies only at the queue boundary.
|
|
3824
|
+
handleRequest: (request, options) => forwardBridgeRequest(request, options?.signal, {
|
|
3825
|
+
reservation: options?.reservation,
|
|
3826
|
+
}),
|
|
3827
|
+
});
|
|
3828
|
+
server = await startSandboxCallbackBridgeServer({
|
|
3829
|
+
runner,
|
|
3830
|
+
remoteCwd: target.remoteCwd,
|
|
3831
|
+
assetRemoteDir,
|
|
3832
|
+
queueDir,
|
|
3833
|
+
bridgeToken,
|
|
3834
|
+
bridgeAsset,
|
|
3835
|
+
timeoutMs: bridgeTimeoutMs,
|
|
3836
|
+
maxBodyBytes,
|
|
3837
|
+
shellCommand,
|
|
3838
|
+
});
|
|
3839
|
+
}
|
|
3840
|
+
catch (error) {
|
|
3841
|
+
await Promise.allSettled([
|
|
3842
|
+
server?.stop(),
|
|
3843
|
+
worker?.stop(),
|
|
3844
|
+
bridgeAsset.cleanup(),
|
|
3845
|
+
]);
|
|
3846
|
+
throw error;
|
|
3847
|
+
}
|
|
3848
|
+
let runLogTail = null;
|
|
3849
|
+
if (target.transport === "sandbox" && target.streamRunLogs !== false) {
|
|
3850
|
+
runLogTail = createSandboxRunLogTailFactory({
|
|
3851
|
+
runner,
|
|
3852
|
+
remoteCwd: target.remoteCwd,
|
|
3853
|
+
logsDir: sandboxCallbackBridgeDirectories(queueDir).logsDir,
|
|
3854
|
+
shellCommand,
|
|
3855
|
+
});
|
|
3856
|
+
await onLog("stdout", "[paperclip] Sandbox run log streaming enabled for this run.\n");
|
|
3857
|
+
}
|
|
3858
|
+
return {
|
|
3859
|
+
env: {
|
|
3860
|
+
PAPERCLIP_API_URL: server.baseUrl,
|
|
3861
|
+
PAPERCLIP_API_KEY: bridgeToken,
|
|
3862
|
+
PAPERCLIP_API_BRIDGE_MODE: "queue_v1",
|
|
3863
|
+
PAPERCLIP_BRIDGE_QUEUE_DIR: queueDir,
|
|
3864
|
+
},
|
|
3865
|
+
runLogTail,
|
|
3866
|
+
stop: async () => {
|
|
3867
|
+
await Promise.allSettled([
|
|
3868
|
+
server?.stop(),
|
|
3869
|
+
]);
|
|
3870
|
+
await Promise.allSettled([
|
|
3871
|
+
worker?.stop(),
|
|
3872
|
+
bridgeAsset.cleanup(),
|
|
3873
|
+
]);
|
|
3874
|
+
},
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
//# sourceMappingURL=execution-target.js.map
|