@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,3253 @@
|
|
|
1
|
+
import { execFile as execFileCallback, spawn } from "node:child_process";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, readdir, rm, utimes, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
import { getActiveStepContext, measureStartupStep } from "./acpx-engine/startup-timing.js";
|
|
9
|
+
import { prepareCommandManagedRuntime } from "./command-managed-runtime.js";
|
|
10
|
+
import { authorizeSandboxCallbackBridgeRequestWithRoutes, createCommandManagedSandboxCallbackBridgeQueueClient, createFileSystemSandboxCallbackBridgeQueueClient, createSandboxCallbackBridgeAsset, createSandboxCallbackBridgeToken, getSandboxBridgeProcessBodyLedgerSource, getSandboxCallbackBridgeServerSource, HTTP2_SANDBOX_CALLBACK_BRIDGE_ROUTE_ALLOWLIST, sandboxCallbackBridgeDirectories, syncRemoteTextFileWithHashSkip, syncSandboxCallbackBridgeEntrypoint, startSandboxCallbackBridgeServer, startSandboxCallbackBridgeWorker, } from "./sandbox-callback-bridge.js";
|
|
11
|
+
import { createHttp2BridgeServer } from "./http2-bridge-server.js";
|
|
12
|
+
const execFile = promisify(execFileCallback);
|
|
13
|
+
describe("sandbox callback bridge", () => {
|
|
14
|
+
const cleanupDirs = [];
|
|
15
|
+
const cleanupFns = [];
|
|
16
|
+
function createExecRunner() {
|
|
17
|
+
return {
|
|
18
|
+
execute: async (input) => {
|
|
19
|
+
const startedAt = new Date().toISOString();
|
|
20
|
+
const env = {
|
|
21
|
+
...process.env,
|
|
22
|
+
...input.env,
|
|
23
|
+
};
|
|
24
|
+
const command = input.command === "sh" ? "/bin/sh" : input.command === "bash" ? "/bin/bash" : input.command;
|
|
25
|
+
const args = [...(input.args ?? [])];
|
|
26
|
+
if (input.stdin != null &&
|
|
27
|
+
(input.command === "sh" || input.command === "bash") &&
|
|
28
|
+
(args[0] === "-c" || args[0] === "-lc") &&
|
|
29
|
+
typeof args[1] === "string") {
|
|
30
|
+
env.PAPERCLIP_TEST_STDIN = input.stdin;
|
|
31
|
+
args[1] = `printf '%s' \"$PAPERCLIP_TEST_STDIN\" | (${args[1]})`;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const result = await execFile(command, args, {
|
|
35
|
+
cwd: input.cwd,
|
|
36
|
+
env,
|
|
37
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
38
|
+
timeout: input.timeoutMs,
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
exitCode: 0,
|
|
42
|
+
signal: null,
|
|
43
|
+
timedOut: false,
|
|
44
|
+
stdout: result.stdout,
|
|
45
|
+
stderr: result.stderr,
|
|
46
|
+
pid: null,
|
|
47
|
+
startedAt,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const err = error;
|
|
52
|
+
return {
|
|
53
|
+
exitCode: typeof err.code === "number" ? err.code : null,
|
|
54
|
+
signal: err.signal ?? null,
|
|
55
|
+
timedOut: Boolean(err.killed && input.timeoutMs),
|
|
56
|
+
stdout: err.stdout ?? "",
|
|
57
|
+
stderr: err.stderr ?? "",
|
|
58
|
+
pid: null,
|
|
59
|
+
startedAt,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async function waitForJsonFile(directory, timeoutMs = 2_000) {
|
|
66
|
+
const deadline = Date.now() + timeoutMs;
|
|
67
|
+
while (Date.now() < deadline) {
|
|
68
|
+
const entries = await readdir(directory).catch(() => []);
|
|
69
|
+
const match = entries.find((entry) => entry.endsWith(".json"));
|
|
70
|
+
if (match)
|
|
71
|
+
return match;
|
|
72
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`Timed out waiting for a JSON file in ${directory}.`);
|
|
75
|
+
}
|
|
76
|
+
afterEach(async () => {
|
|
77
|
+
while (cleanupFns.length > 0) {
|
|
78
|
+
const cleanup = cleanupFns.pop();
|
|
79
|
+
if (!cleanup)
|
|
80
|
+
continue;
|
|
81
|
+
await cleanup().catch(() => undefined);
|
|
82
|
+
}
|
|
83
|
+
while (cleanupDirs.length > 0) {
|
|
84
|
+
const dir = cleanupDirs.pop();
|
|
85
|
+
if (!dir)
|
|
86
|
+
continue;
|
|
87
|
+
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
it("round-trips localhost bridge requests over the sandbox queue without forwarding the bridge token", async () => {
|
|
91
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-runtime-"));
|
|
92
|
+
cleanupDirs.push(rootDir);
|
|
93
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
94
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
95
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
96
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
97
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "bridge test\n", "utf8");
|
|
98
|
+
const runner = createExecRunner();
|
|
99
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
100
|
+
cleanupFns.push(bridgeAsset.cleanup);
|
|
101
|
+
const prepared = await prepareCommandManagedRuntime({
|
|
102
|
+
runner,
|
|
103
|
+
spec: {
|
|
104
|
+
remoteCwd: remoteWorkspaceDir,
|
|
105
|
+
timeoutMs: 30_000,
|
|
106
|
+
},
|
|
107
|
+
adapterKey: "codex",
|
|
108
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
109
|
+
assets: [
|
|
110
|
+
{
|
|
111
|
+
key: "bridge",
|
|
112
|
+
localDir: bridgeAsset.localDir,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
const queueDir = path.posix.join(prepared.runtimeRootDir, "paperclip-bridge");
|
|
117
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
118
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
119
|
+
const seenRequests = [];
|
|
120
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
121
|
+
client: createFileSystemSandboxCallbackBridgeQueueClient(),
|
|
122
|
+
queueDir,
|
|
123
|
+
authorizeRequest: async (request) => ["/api/agents/me", "/runtime-tools/github/credentials"].includes(request.path) ? null : `Route not allowed: ${request.method} ${request.path}`,
|
|
124
|
+
handleRequest: async (request) => {
|
|
125
|
+
seenRequests.push({
|
|
126
|
+
method: request.method,
|
|
127
|
+
path: request.path,
|
|
128
|
+
query: request.query,
|
|
129
|
+
headers: request.headers,
|
|
130
|
+
body: request.body,
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
status: 200,
|
|
134
|
+
headers: {
|
|
135
|
+
"content-type": "application/json",
|
|
136
|
+
etag: '"bridge-rev-1"',
|
|
137
|
+
"last-modified": "Tue, 01 Apr 2025 00:00:00 GMT",
|
|
138
|
+
},
|
|
139
|
+
body: JSON.stringify({
|
|
140
|
+
ok: true,
|
|
141
|
+
method: request.method,
|
|
142
|
+
path: request.path,
|
|
143
|
+
}),
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
cleanupFns.push(async () => {
|
|
148
|
+
await worker.stop();
|
|
149
|
+
});
|
|
150
|
+
const bridge = await startSandboxCallbackBridgeServer({
|
|
151
|
+
runner,
|
|
152
|
+
remoteCwd: remoteWorkspaceDir,
|
|
153
|
+
assetRemoteDir: prepared.assetDirs.bridge,
|
|
154
|
+
queueDir,
|
|
155
|
+
bridgeToken,
|
|
156
|
+
timeoutMs: 30_000,
|
|
157
|
+
});
|
|
158
|
+
cleanupFns.push(async () => {
|
|
159
|
+
await bridge.stop();
|
|
160
|
+
});
|
|
161
|
+
const okResponse = await fetch(`${bridge.baseUrl}/api/agents/me?view=compact`, {
|
|
162
|
+
headers: {
|
|
163
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
164
|
+
accept: "application/json",
|
|
165
|
+
"if-none-match": '"client-cache-key"',
|
|
166
|
+
"x-paperclip-run-id": "run-bridge-1",
|
|
167
|
+
"x-bridge-debug": "drop-me",
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
expect(okResponse.status).toBe(200);
|
|
171
|
+
expect(okResponse.headers.get("content-type")).toContain("application/json");
|
|
172
|
+
expect(okResponse.headers.get("etag")).toBe('"bridge-rev-1"');
|
|
173
|
+
expect(okResponse.headers.get("last-modified")).toBe("Tue, 01 Apr 2025 00:00:00 GMT");
|
|
174
|
+
await expect(okResponse.json()).resolves.toMatchObject({
|
|
175
|
+
ok: true,
|
|
176
|
+
method: "GET",
|
|
177
|
+
path: "/api/agents/me",
|
|
178
|
+
});
|
|
179
|
+
const deniedResponse = await fetch(`${bridge.baseUrl}/api/issues/issue-1`, {
|
|
180
|
+
method: "PATCH",
|
|
181
|
+
headers: {
|
|
182
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
183
|
+
"content-type": "application/json",
|
|
184
|
+
},
|
|
185
|
+
body: JSON.stringify({ status: "in_progress" }),
|
|
186
|
+
});
|
|
187
|
+
expect(deniedResponse.status).toBe(403);
|
|
188
|
+
await expect(deniedResponse.json()).resolves.toMatchObject({
|
|
189
|
+
error: "Route not allowed: PATCH /api/issues/issue-1",
|
|
190
|
+
});
|
|
191
|
+
const unauthorizedResponse = await fetch(`${bridge.baseUrl}/api/agents/me`, {
|
|
192
|
+
headers: {
|
|
193
|
+
authorization: "Bearer wrong-token",
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
expect(unauthorizedResponse.status).toBe(401);
|
|
197
|
+
await expect(unauthorizedResponse.json()).resolves.toMatchObject({
|
|
198
|
+
error: "Invalid bridge token.",
|
|
199
|
+
});
|
|
200
|
+
expect(seenRequests).toHaveLength(1);
|
|
201
|
+
expect(seenRequests[0]).toMatchObject({
|
|
202
|
+
method: "GET",
|
|
203
|
+
path: "/api/agents/me",
|
|
204
|
+
query: "?view=compact",
|
|
205
|
+
body: "",
|
|
206
|
+
headers: {
|
|
207
|
+
accept: "application/json",
|
|
208
|
+
"if-none-match": '"client-cache-key"',
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
expect(seenRequests[0]?.headers.authorization).toBeUndefined();
|
|
212
|
+
expect(seenRequests[0]?.headers["x-paperclip-run-id"]).toBeUndefined();
|
|
213
|
+
const githubResponse = await fetch(`${bridge.baseUrl}/runtime-tools/github/credentials`, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: {
|
|
216
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
217
|
+
"content-type": "application/json",
|
|
218
|
+
"x-paperclip-github-capability": "test-run-scoped-capability",
|
|
219
|
+
},
|
|
220
|
+
body: "{}",
|
|
221
|
+
});
|
|
222
|
+
expect(githubResponse.status).toBe(200);
|
|
223
|
+
await githubResponse.arrayBuffer();
|
|
224
|
+
expect(seenRequests[1]).toMatchObject({
|
|
225
|
+
method: "POST", path: "/runtime-tools/github/credentials", body: "{}",
|
|
226
|
+
headers: { "x-paperclip-github-capability": "test-run-scoped-capability" },
|
|
227
|
+
});
|
|
228
|
+
expect(seenRequests[1]?.headers.authorization).toBeUndefined();
|
|
229
|
+
});
|
|
230
|
+
it("serves schema discovery over the queue and denies schema mutations and lookalikes", async () => {
|
|
231
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-schema-"));
|
|
232
|
+
cleanupDirs.push(rootDir);
|
|
233
|
+
const queueDir = path.join(rootDir, "queue");
|
|
234
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
235
|
+
const schema = { openapi: "3.1.0", paths: {} };
|
|
236
|
+
const forwarded = [];
|
|
237
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
238
|
+
client: createFileSystemSandboxCallbackBridgeQueueClient(), queueDir,
|
|
239
|
+
handleRequest: async (request) => {
|
|
240
|
+
forwarded.push(`${request.method} ${request.path}`);
|
|
241
|
+
return { status: 200, body: JSON.stringify(schema) };
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
cleanupFns.push(() => worker.stop());
|
|
245
|
+
const requests = [
|
|
246
|
+
{ method: "GET", path: "/api/openapi.json" },
|
|
247
|
+
{ method: "POST", path: "/api/openapi.json" },
|
|
248
|
+
{ method: "PATCH", path: "/api/openapi.json" },
|
|
249
|
+
{ method: "DELETE", path: "/api/openapi.json" },
|
|
250
|
+
{ method: "GET", path: "/api/openapi.json/extra" },
|
|
251
|
+
{ method: "GET", path: "/api/openapiXjson" },
|
|
252
|
+
{ method: "GET", path: "/api/secrets" },
|
|
253
|
+
];
|
|
254
|
+
for (const [index, request] of requests.entries()) {
|
|
255
|
+
await writeFile(path.join(directories.requestsDir, `schema-${index}.json`), JSON.stringify({
|
|
256
|
+
id: `schema-${index}`, ...request, query: "", headers: {}, body: "", createdAt: new Date().toISOString(),
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
await worker.stop({ drainTimeoutMs: 5_000 });
|
|
260
|
+
for (const [index] of requests.entries()) {
|
|
261
|
+
const response = JSON.parse(await readFile(path.join(directories.responsesDir, `schema-${index}.json`), "utf8"));
|
|
262
|
+
expect(response.status).toBe(index === 0 ? 200 : 403);
|
|
263
|
+
if (index === 0)
|
|
264
|
+
expect(JSON.parse(response.body)).toEqual(schema);
|
|
265
|
+
}
|
|
266
|
+
expect(forwarded).toEqual(["GET /api/openapi.json"]);
|
|
267
|
+
});
|
|
268
|
+
it("denies non-allowlisted requests by default", async () => {
|
|
269
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-default-policy-"));
|
|
270
|
+
cleanupDirs.push(rootDir);
|
|
271
|
+
const queueDir = path.posix.join(rootDir, "queue");
|
|
272
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
273
|
+
let handled = 0;
|
|
274
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
275
|
+
client: createFileSystemSandboxCallbackBridgeQueueClient(),
|
|
276
|
+
queueDir,
|
|
277
|
+
handleRequest: async () => {
|
|
278
|
+
handled += 1;
|
|
279
|
+
return {
|
|
280
|
+
status: 200,
|
|
281
|
+
body: "should not happen",
|
|
282
|
+
};
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
await writeFile(path.posix.join(directories.requestsDir, "req-1.json"), `${JSON.stringify({
|
|
286
|
+
id: "req-1",
|
|
287
|
+
method: "DELETE",
|
|
288
|
+
path: "/api/secrets",
|
|
289
|
+
query: "",
|
|
290
|
+
headers: {},
|
|
291
|
+
body: "",
|
|
292
|
+
createdAt: new Date().toISOString(),
|
|
293
|
+
})}\n`, "utf8");
|
|
294
|
+
await worker.stop({ drainTimeoutMs: 1_000 });
|
|
295
|
+
const response = JSON.parse(await readFile(path.posix.join(directories.responsesDir, "req-1.json"), "utf8"));
|
|
296
|
+
expect(handled).toBe(0);
|
|
297
|
+
expect(response.status).toBe(403);
|
|
298
|
+
expect(JSON.parse(response.body)).toEqual({
|
|
299
|
+
error: "Route not allowed: DELETE /api/secrets",
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
it("drains already-queued requests on stop", async () => {
|
|
303
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-drain-"));
|
|
304
|
+
cleanupDirs.push(rootDir);
|
|
305
|
+
const queueDir = path.posix.join(rootDir, "queue");
|
|
306
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
307
|
+
const processed = [];
|
|
308
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
309
|
+
client: createFileSystemSandboxCallbackBridgeQueueClient(),
|
|
310
|
+
queueDir,
|
|
311
|
+
authorizeRequest: async () => null,
|
|
312
|
+
handleRequest: async (request) => {
|
|
313
|
+
processed.push(request.id);
|
|
314
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
315
|
+
return {
|
|
316
|
+
status: 200,
|
|
317
|
+
body: request.id,
|
|
318
|
+
};
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
await writeFile(path.posix.join(directories.requestsDir, "req-a.json"), `${JSON.stringify({
|
|
322
|
+
id: "req-a",
|
|
323
|
+
method: "GET",
|
|
324
|
+
path: "/api/agents/me",
|
|
325
|
+
query: "",
|
|
326
|
+
headers: {},
|
|
327
|
+
body: "",
|
|
328
|
+
createdAt: new Date().toISOString(),
|
|
329
|
+
})}\n`, "utf8");
|
|
330
|
+
await writeFile(path.posix.join(directories.requestsDir, "req-b.json"), `${JSON.stringify({
|
|
331
|
+
id: "req-b",
|
|
332
|
+
method: "GET",
|
|
333
|
+
path: "/api/agents/me",
|
|
334
|
+
query: "",
|
|
335
|
+
headers: {},
|
|
336
|
+
body: "",
|
|
337
|
+
createdAt: new Date().toISOString(),
|
|
338
|
+
})}\n`, "utf8");
|
|
339
|
+
await worker.stop({ drainTimeoutMs: 1_000 });
|
|
340
|
+
expect(processed).toEqual(["req-a", "req-b"]);
|
|
341
|
+
await expect(readFile(path.posix.join(directories.responsesDir, "req-a.json"), "utf8")).resolves.toContain("\"req-a\"");
|
|
342
|
+
await expect(readFile(path.posix.join(directories.responsesDir, "req-b.json"), "utf8")).resolves.toContain("\"req-b\"");
|
|
343
|
+
});
|
|
344
|
+
it("writes fast 503 responses for queued requests that miss the drain deadline", async () => {
|
|
345
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-drain-timeout-"));
|
|
346
|
+
cleanupDirs.push(rootDir);
|
|
347
|
+
const queueDir = path.posix.join(rootDir, "queue");
|
|
348
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
349
|
+
const processed = [];
|
|
350
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
351
|
+
client: createFileSystemSandboxCallbackBridgeQueueClient(),
|
|
352
|
+
queueDir,
|
|
353
|
+
authorizeRequest: async () => null,
|
|
354
|
+
handleRequest: async (request) => {
|
|
355
|
+
processed.push(request.id);
|
|
356
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
357
|
+
return {
|
|
358
|
+
status: 200,
|
|
359
|
+
body: request.id,
|
|
360
|
+
};
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
await writeFile(path.posix.join(directories.requestsDir, "req-a.json"), `${JSON.stringify({
|
|
364
|
+
id: "req-a",
|
|
365
|
+
method: "GET",
|
|
366
|
+
path: "/api/agents/me",
|
|
367
|
+
query: "",
|
|
368
|
+
headers: {},
|
|
369
|
+
body: "",
|
|
370
|
+
createdAt: new Date().toISOString(),
|
|
371
|
+
})}\n`, "utf8");
|
|
372
|
+
await writeFile(path.posix.join(directories.requestsDir, "req-b.json"), `${JSON.stringify({
|
|
373
|
+
id: "req-b",
|
|
374
|
+
method: "GET",
|
|
375
|
+
path: "/api/agents/me",
|
|
376
|
+
query: "",
|
|
377
|
+
headers: {},
|
|
378
|
+
body: "",
|
|
379
|
+
createdAt: new Date().toISOString(),
|
|
380
|
+
})}\n`, "utf8");
|
|
381
|
+
for (let attempt = 0; attempt < 50 && processed.length === 0; attempt += 1) {
|
|
382
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
383
|
+
}
|
|
384
|
+
await worker.stop({ drainTimeoutMs: 10 });
|
|
385
|
+
expect(processed).toEqual(["req-a"]);
|
|
386
|
+
await expect(readFile(path.posix.join(directories.responsesDir, "req-a.json"), "utf8")).resolves.toContain("\"req-a\"");
|
|
387
|
+
await expect(readFile(path.posix.join(directories.responsesDir, "req-b.json"), "utf8")).resolves.toContain("Bridge worker stopped before request could be handled.");
|
|
388
|
+
});
|
|
389
|
+
it("handles SSH queue polling failures without emitting an unhandled rejection", async () => {
|
|
390
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-ssh-failure-"));
|
|
391
|
+
cleanupDirs.push(rootDir);
|
|
392
|
+
const queueDir = path.posix.join(rootDir, "queue");
|
|
393
|
+
const unhandled = [];
|
|
394
|
+
const onUnhandledRejection = (reason) => {
|
|
395
|
+
unhandled.push(reason);
|
|
396
|
+
};
|
|
397
|
+
process.on("unhandledRejection", onUnhandledRejection);
|
|
398
|
+
try {
|
|
399
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
400
|
+
client: {
|
|
401
|
+
makeDir: async () => { },
|
|
402
|
+
makeDirs: async () => { },
|
|
403
|
+
listJsonFiles: async () => {
|
|
404
|
+
throw new Error("list /remote/.paperclip-runtime/gemini/paperclip-bridge/queue/requests failed with exit code 255: kex_exchange_identification: read: Connection reset by peer");
|
|
405
|
+
},
|
|
406
|
+
readTextFile: async () => {
|
|
407
|
+
throw new Error("unexpected readTextFile");
|
|
408
|
+
},
|
|
409
|
+
writeTextFile: async () => {
|
|
410
|
+
throw new Error("unexpected writeTextFile");
|
|
411
|
+
},
|
|
412
|
+
rename: async () => {
|
|
413
|
+
throw new Error("unexpected rename");
|
|
414
|
+
},
|
|
415
|
+
remove: async () => { },
|
|
416
|
+
},
|
|
417
|
+
queueDir,
|
|
418
|
+
authorizeRequest: async () => null,
|
|
419
|
+
handleRequest: async () => ({
|
|
420
|
+
status: 200,
|
|
421
|
+
body: "ok",
|
|
422
|
+
}),
|
|
423
|
+
});
|
|
424
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
425
|
+
await worker.stop();
|
|
426
|
+
expect(unhandled).toEqual([]);
|
|
427
|
+
}
|
|
428
|
+
finally {
|
|
429
|
+
process.off("unhandledRejection", onUnhandledRejection);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
it("recovers from transient queue polling failures and keeps relaying requests", async () => {
|
|
433
|
+
// A single reset or slow sandbox exec used to unwind the poll loop into its
|
|
434
|
+
// terminal catch, killing the relay for the rest of the run. The loop must
|
|
435
|
+
// instead back off, retry, and still deliver requests queued after the
|
|
436
|
+
// failure window.
|
|
437
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-transient-"));
|
|
438
|
+
cleanupDirs.push(rootDir);
|
|
439
|
+
const queueDir = path.posix.join(rootDir, "queue");
|
|
440
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
441
|
+
const baseClient = createFileSystemSandboxCallbackBridgeQueueClient();
|
|
442
|
+
let listCalls = 0;
|
|
443
|
+
const client = {
|
|
444
|
+
...baseClient,
|
|
445
|
+
listJsonFiles: async (dirPath) => {
|
|
446
|
+
listCalls += 1;
|
|
447
|
+
if (listCalls <= 3) {
|
|
448
|
+
throw new Error("list requests failed: kex_exchange_identification: read: Connection reset by peer");
|
|
449
|
+
}
|
|
450
|
+
return baseClient.listJsonFiles(dirPath);
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
const seenPaths = [];
|
|
454
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
455
|
+
client,
|
|
456
|
+
queueDir,
|
|
457
|
+
authorizeRequest: async () => null,
|
|
458
|
+
handleRequest: async (request) => {
|
|
459
|
+
seenPaths.push(request.path);
|
|
460
|
+
return {
|
|
461
|
+
status: 200,
|
|
462
|
+
headers: { "content-type": "application/json" },
|
|
463
|
+
body: JSON.stringify({ ok: true }),
|
|
464
|
+
};
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
cleanupFns.push(async () => {
|
|
468
|
+
await worker.stop();
|
|
469
|
+
});
|
|
470
|
+
const requestId = "transient-recovery-1";
|
|
471
|
+
await writeFile(path.join(directories.requestsDir, `${requestId}.json`), JSON.stringify({
|
|
472
|
+
id: requestId,
|
|
473
|
+
method: "GET",
|
|
474
|
+
path: "/api/agents/me",
|
|
475
|
+
query: "",
|
|
476
|
+
headers: {},
|
|
477
|
+
body: "",
|
|
478
|
+
}), "utf8");
|
|
479
|
+
const responseFile = await waitForJsonFile(directories.responsesDir, 10_000);
|
|
480
|
+
const raw = await readFile(path.join(directories.responsesDir, responseFile), "utf8");
|
|
481
|
+
expect(JSON.parse(raw)).toMatchObject({ id: requestId, status: 200 });
|
|
482
|
+
expect(seenPaths).toEqual(["/api/agents/me"]);
|
|
483
|
+
expect(listCalls).toBeGreaterThan(3);
|
|
484
|
+
});
|
|
485
|
+
it("keeps the queue-directory setup on the startup step but resets the poll loop store", async () => {
|
|
486
|
+
// The worker starts inside the measured `bridge.paperclip` step. Its awaited
|
|
487
|
+
// queue-directory setup is startup work, so a `makeDir` `sandbox.exec` span
|
|
488
|
+
// must keep the active step and its `criticalPath` flag. The long-lived poll
|
|
489
|
+
// loop runs run-time execs for the whole run, so a loop `sandbox.exec` span
|
|
490
|
+
// must open unparented with no stale flag. This test reads the active step in
|
|
491
|
+
// both places and proves the boundary sits at the loop, not the whole worker.
|
|
492
|
+
let setupStep = "unset";
|
|
493
|
+
let loopStep = "unset";
|
|
494
|
+
let resolveFirstPoll = () => { };
|
|
495
|
+
const firstPoll = new Promise((resolve) => {
|
|
496
|
+
resolveFirstPoll = resolve;
|
|
497
|
+
});
|
|
498
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-step-store-"));
|
|
499
|
+
cleanupDirs.push(rootDir);
|
|
500
|
+
const queueDir = path.posix.join(rootDir, "queue");
|
|
501
|
+
const worker = await measureStartupStep({}, () => 0, "bridge.paperclip", () => startSandboxCallbackBridgeWorker({
|
|
502
|
+
client: {
|
|
503
|
+
makeDir: async () => {
|
|
504
|
+
setupStep = getActiveStepContext();
|
|
505
|
+
},
|
|
506
|
+
makeDirs: async () => {
|
|
507
|
+
setupStep = getActiveStepContext();
|
|
508
|
+
},
|
|
509
|
+
listJsonFiles: async () => {
|
|
510
|
+
loopStep = getActiveStepContext();
|
|
511
|
+
resolveFirstPoll();
|
|
512
|
+
return [];
|
|
513
|
+
},
|
|
514
|
+
readTextFile: async () => {
|
|
515
|
+
throw new Error("unexpected readTextFile");
|
|
516
|
+
},
|
|
517
|
+
writeTextFile: async () => {
|
|
518
|
+
throw new Error("unexpected writeTextFile");
|
|
519
|
+
},
|
|
520
|
+
rename: async () => {
|
|
521
|
+
throw new Error("unexpected rename");
|
|
522
|
+
},
|
|
523
|
+
remove: async () => { },
|
|
524
|
+
},
|
|
525
|
+
queueDir,
|
|
526
|
+
authorizeRequest: async () => null,
|
|
527
|
+
handleRequest: async () => ({ status: 200, body: "ok" }),
|
|
528
|
+
}), { criticalPath: false });
|
|
529
|
+
await firstPoll;
|
|
530
|
+
await worker.stop();
|
|
531
|
+
// The setup ran on the active step, so its exec span parents to the step.
|
|
532
|
+
expect(setupStep).not.toBe("unset");
|
|
533
|
+
expect(setupStep).not.toBeNull();
|
|
534
|
+
expect(setupStep.criticalPath).toBe(false);
|
|
535
|
+
// The loop ran outside that store, so its exec span opens unparented with no
|
|
536
|
+
// stale `criticalPath` flag.
|
|
537
|
+
expect(loopStep).toBeNull();
|
|
538
|
+
});
|
|
539
|
+
it("test_paperclip_loop_exec_parents_to_run_context", async () => {
|
|
540
|
+
// The worker starts inside the measured `bridge.paperclip` step. Its awaited
|
|
541
|
+
// queue-directory setup is startup work and keeps the active step. The poll
|
|
542
|
+
// loop shell stays outside that store. But a per-request unit of work is
|
|
543
|
+
// run-time work, so the worker runs each request under the current-run
|
|
544
|
+
// parent context. A request `sandbox.exec` span then parents to the live run
|
|
545
|
+
// span, not to the ended startup step. This test drives the worker with a
|
|
546
|
+
// `getRuntimeParentContext` that returns a known token, queues one request,
|
|
547
|
+
// and proves the request work reads that token from the active step store.
|
|
548
|
+
const runParentToken = { marker: "run-parent-token" };
|
|
549
|
+
let setupStep = "unset";
|
|
550
|
+
let requestStep = "unset";
|
|
551
|
+
let served = false;
|
|
552
|
+
let resolveServed = () => { };
|
|
553
|
+
const requestServed = new Promise((resolve) => {
|
|
554
|
+
resolveServed = resolve;
|
|
555
|
+
});
|
|
556
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-run-parent-"));
|
|
557
|
+
cleanupDirs.push(rootDir);
|
|
558
|
+
const queueDir = path.posix.join(rootDir, "queue");
|
|
559
|
+
const worker = await measureStartupStep({}, () => 0, "bridge.paperclip", () => startSandboxCallbackBridgeWorker({
|
|
560
|
+
client: {
|
|
561
|
+
makeDir: async () => {
|
|
562
|
+
setupStep = getActiveStepContext();
|
|
563
|
+
},
|
|
564
|
+
makeDirs: async () => {
|
|
565
|
+
setupStep = getActiveStepContext();
|
|
566
|
+
},
|
|
567
|
+
// Return one request on the first poll, then nothing.
|
|
568
|
+
listJsonFiles: async () => (served ? [] : ["000000000001.json"]),
|
|
569
|
+
readTextFile: async () => JSON.stringify({ id: "req-1", method: "GET", path: "/", query: "", headers: {}, body: "" }),
|
|
570
|
+
writeTextFile: async () => { },
|
|
571
|
+
rename: async () => { },
|
|
572
|
+
remove: async () => { },
|
|
573
|
+
},
|
|
574
|
+
queueDir,
|
|
575
|
+
authorizeRequest: async () => null,
|
|
576
|
+
handleRequest: async () => {
|
|
577
|
+
requestStep = getActiveStepContext();
|
|
578
|
+
served = true;
|
|
579
|
+
resolveServed();
|
|
580
|
+
return { status: 200, body: "ok" };
|
|
581
|
+
},
|
|
582
|
+
getRuntimeParentContext: () => runParentToken,
|
|
583
|
+
}), { criticalPath: false });
|
|
584
|
+
await requestServed;
|
|
585
|
+
await worker.stop();
|
|
586
|
+
// The setup ran on the active step, so its exec span parents to the step.
|
|
587
|
+
expect(setupStep).not.toBe("unset");
|
|
588
|
+
expect(setupStep).not.toBeNull();
|
|
589
|
+
// The request work ran under the run parent context. Its exec span parents
|
|
590
|
+
// to the run token, not to the ended startup step, and it carries no
|
|
591
|
+
// startup `criticalPath` flag.
|
|
592
|
+
expect(requestStep).not.toBe("unset");
|
|
593
|
+
expect(requestStep).not.toBeNull();
|
|
594
|
+
expect(requestStep.parentContext).toBe(runParentToken);
|
|
595
|
+
expect(requestStep.criticalPath).toBe(false);
|
|
596
|
+
});
|
|
597
|
+
it("wraps each request in a sandbox.callbackBridge.relayRequest span", async () => {
|
|
598
|
+
// With a span runner injected, the worker wraps each request in one
|
|
599
|
+
// `sandbox.callbackBridge.relayRequest` span, so the request's read, write,
|
|
600
|
+
// and remove execs group under one named span. This test drives the worker
|
|
601
|
+
// with a recording runner and proves it opens the wrapper span around the
|
|
602
|
+
// request work.
|
|
603
|
+
const wrapped = [];
|
|
604
|
+
let served = false;
|
|
605
|
+
let resolveServed = () => { };
|
|
606
|
+
const requestServed = new Promise((resolve) => {
|
|
607
|
+
resolveServed = resolve;
|
|
608
|
+
});
|
|
609
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-relay-span-"));
|
|
610
|
+
cleanupDirs.push(rootDir);
|
|
611
|
+
const queueDir = path.posix.join(rootDir, "queue");
|
|
612
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
613
|
+
client: {
|
|
614
|
+
makeDir: async () => { },
|
|
615
|
+
makeDirs: async () => { },
|
|
616
|
+
listJsonFiles: async () => (served ? [] : ["000000000001.json"]),
|
|
617
|
+
readTextFile: async () => JSON.stringify({ id: "req-1", method: "GET", path: "/", query: "", headers: {}, body: "" }),
|
|
618
|
+
writeTextFile: async () => { },
|
|
619
|
+
rename: async () => { },
|
|
620
|
+
remove: async () => { },
|
|
621
|
+
},
|
|
622
|
+
queueDir,
|
|
623
|
+
authorizeRequest: async () => null,
|
|
624
|
+
handleRequest: async () => {
|
|
625
|
+
served = true;
|
|
626
|
+
resolveServed();
|
|
627
|
+
return { status: 200, body: "ok" };
|
|
628
|
+
},
|
|
629
|
+
// Record each wrapper span name, then run the wrapped work.
|
|
630
|
+
runtimeSpan: async (name, work) => {
|
|
631
|
+
wrapped.push(name);
|
|
632
|
+
return work();
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
await requestServed;
|
|
636
|
+
await worker.stop();
|
|
637
|
+
expect(wrapped).toContain("sandbox.callbackBridge.relayRequest");
|
|
638
|
+
});
|
|
639
|
+
it("test_paperclip_loop_exec_stays_unparented_without_getter", async () => {
|
|
640
|
+
// With no `getRuntimeParentContext`, a request runs with an empty active
|
|
641
|
+
// step store, exactly like the earlier `runWithoutActiveStep` behavior. So a
|
|
642
|
+
// request `sandbox.exec` span opens unparented with no stale startup flag.
|
|
643
|
+
let requestStep = "unset";
|
|
644
|
+
let served = false;
|
|
645
|
+
let resolveServed = () => { };
|
|
646
|
+
const requestServed = new Promise((resolve) => {
|
|
647
|
+
resolveServed = resolve;
|
|
648
|
+
});
|
|
649
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-no-getter-"));
|
|
650
|
+
cleanupDirs.push(rootDir);
|
|
651
|
+
const queueDir = path.posix.join(rootDir, "queue");
|
|
652
|
+
const worker = await measureStartupStep({}, () => 0, "bridge.paperclip", () => startSandboxCallbackBridgeWorker({
|
|
653
|
+
client: {
|
|
654
|
+
makeDir: async () => { },
|
|
655
|
+
makeDirs: async () => { },
|
|
656
|
+
listJsonFiles: async () => (served ? [] : ["000000000001.json"]),
|
|
657
|
+
readTextFile: async () => JSON.stringify({ id: "req-1", method: "GET", path: "/", query: "", headers: {}, body: "" }),
|
|
658
|
+
writeTextFile: async () => { },
|
|
659
|
+
rename: async () => { },
|
|
660
|
+
remove: async () => { },
|
|
661
|
+
},
|
|
662
|
+
queueDir,
|
|
663
|
+
authorizeRequest: async () => null,
|
|
664
|
+
handleRequest: async () => {
|
|
665
|
+
requestStep = getActiveStepContext();
|
|
666
|
+
served = true;
|
|
667
|
+
resolveServed();
|
|
668
|
+
return { status: 200, body: "ok" };
|
|
669
|
+
},
|
|
670
|
+
}), { criticalPath: false });
|
|
671
|
+
await requestServed;
|
|
672
|
+
await worker.stop();
|
|
673
|
+
expect(requestStep).toBeNull();
|
|
674
|
+
});
|
|
675
|
+
it("serializes remote response writes so stop does not recreate a late orphaned response", async () => {
|
|
676
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-response-lock-"));
|
|
677
|
+
cleanupDirs.push(rootDir);
|
|
678
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
679
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
680
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
681
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
682
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "bridge response lock test\n", "utf8");
|
|
683
|
+
const runner = createExecRunner();
|
|
684
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
685
|
+
cleanupFns.push(bridgeAsset.cleanup);
|
|
686
|
+
const prepared = await prepareCommandManagedRuntime({
|
|
687
|
+
runner,
|
|
688
|
+
spec: {
|
|
689
|
+
remoteCwd: remoteWorkspaceDir,
|
|
690
|
+
timeoutMs: 30_000,
|
|
691
|
+
},
|
|
692
|
+
adapterKey: "codex",
|
|
693
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
694
|
+
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
|
|
695
|
+
});
|
|
696
|
+
const queueDir = path.posix.join(prepared.runtimeRootDir, "paperclip-bridge");
|
|
697
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
698
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
699
|
+
const seenRequestIds = [];
|
|
700
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
701
|
+
client: createCommandManagedSandboxCallbackBridgeQueueClient({
|
|
702
|
+
runner,
|
|
703
|
+
remoteCwd: remoteWorkspaceDir,
|
|
704
|
+
timeoutMs: 30_000,
|
|
705
|
+
}),
|
|
706
|
+
queueDir,
|
|
707
|
+
authorizeRequest: async () => null,
|
|
708
|
+
handleRequest: async (request) => {
|
|
709
|
+
seenRequestIds.push(request.id);
|
|
710
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
711
|
+
return {
|
|
712
|
+
status: 200,
|
|
713
|
+
headers: { "content-type": "application/json" },
|
|
714
|
+
body: JSON.stringify({ ok: true, id: request.id }),
|
|
715
|
+
};
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
cleanupFns.push(async () => {
|
|
719
|
+
await worker.stop();
|
|
720
|
+
});
|
|
721
|
+
const bridge = await startSandboxCallbackBridgeServer({
|
|
722
|
+
runner,
|
|
723
|
+
remoteCwd: remoteWorkspaceDir,
|
|
724
|
+
assetRemoteDir: prepared.assetDirs.bridge,
|
|
725
|
+
queueDir,
|
|
726
|
+
bridgeToken,
|
|
727
|
+
timeoutMs: 30_000,
|
|
728
|
+
});
|
|
729
|
+
cleanupFns.push(async () => {
|
|
730
|
+
await bridge.stop();
|
|
731
|
+
});
|
|
732
|
+
const responsePromise = fetch(`${bridge.baseUrl}/api/agents/me`, {
|
|
733
|
+
headers: {
|
|
734
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
735
|
+
},
|
|
736
|
+
});
|
|
737
|
+
for (let attempt = 0; attempt < 50 && seenRequestIds.length === 0; attempt += 1) {
|
|
738
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
739
|
+
}
|
|
740
|
+
expect(seenRequestIds).toHaveLength(1);
|
|
741
|
+
await worker.stop({ drainTimeoutMs: 10 });
|
|
742
|
+
const response = await responsePromise;
|
|
743
|
+
expect(response.status).toBe(503);
|
|
744
|
+
await expect(response.json()).resolves.toEqual({
|
|
745
|
+
error: "Bridge worker stopped before request could be handled.",
|
|
746
|
+
});
|
|
747
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
748
|
+
await expect(readdir(directories.responsesDir)).resolves.toEqual([]);
|
|
749
|
+
await expect(readdir(directories.responsesDir).then((entries) => entries.filter((entry) => entry.endsWith(".tmp") || entry.includes(".paperclip-write.lock")))).resolves.toEqual([]);
|
|
750
|
+
});
|
|
751
|
+
it("rejects non-JSON request bodies and full queues at the bridge server", async () => {
|
|
752
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-server-guards-"));
|
|
753
|
+
cleanupDirs.push(rootDir);
|
|
754
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
755
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
756
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
757
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
758
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "bridge guard test\n", "utf8");
|
|
759
|
+
const runner = createExecRunner();
|
|
760
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
761
|
+
cleanupFns.push(bridgeAsset.cleanup);
|
|
762
|
+
const prepared = await prepareCommandManagedRuntime({
|
|
763
|
+
runner,
|
|
764
|
+
spec: {
|
|
765
|
+
remoteCwd: remoteWorkspaceDir,
|
|
766
|
+
timeoutMs: 30_000,
|
|
767
|
+
},
|
|
768
|
+
adapterKey: "codex",
|
|
769
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
770
|
+
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
|
|
771
|
+
});
|
|
772
|
+
const queueDir = path.posix.join(prepared.runtimeRootDir, "paperclip-bridge");
|
|
773
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
774
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
775
|
+
const bridge = await startSandboxCallbackBridgeServer({
|
|
776
|
+
runner,
|
|
777
|
+
remoteCwd: remoteWorkspaceDir,
|
|
778
|
+
assetRemoteDir: prepared.assetDirs.bridge,
|
|
779
|
+
queueDir,
|
|
780
|
+
bridgeToken,
|
|
781
|
+
timeoutMs: 30_000,
|
|
782
|
+
maxQueueDepth: 1,
|
|
783
|
+
});
|
|
784
|
+
cleanupFns.push(async () => {
|
|
785
|
+
await bridge.stop();
|
|
786
|
+
});
|
|
787
|
+
await writeFile(path.posix.join(directories.requestsDir, "existing.json"), `${JSON.stringify({
|
|
788
|
+
id: "existing",
|
|
789
|
+
method: "GET",
|
|
790
|
+
path: "/api/agents/me",
|
|
791
|
+
query: "",
|
|
792
|
+
headers: {},
|
|
793
|
+
body: "",
|
|
794
|
+
createdAt: new Date().toISOString(),
|
|
795
|
+
})}\n`, "utf8");
|
|
796
|
+
const queueFullResponse = await fetch(`${bridge.baseUrl}/api/agents/me`, {
|
|
797
|
+
headers: {
|
|
798
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
799
|
+
},
|
|
800
|
+
});
|
|
801
|
+
expect(queueFullResponse.status).toBe(503);
|
|
802
|
+
await expect(queueFullResponse.json()).resolves.toEqual({
|
|
803
|
+
error: "Bridge request queue is full.",
|
|
804
|
+
});
|
|
805
|
+
await rm(path.posix.join(directories.requestsDir, "existing.json"), { force: true });
|
|
806
|
+
const nonJsonResponse = await fetch(`${bridge.baseUrl}/api/issues/issue-1/comments`, {
|
|
807
|
+
method: "POST",
|
|
808
|
+
headers: {
|
|
809
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
810
|
+
"content-type": "text/plain",
|
|
811
|
+
},
|
|
812
|
+
body: "not json",
|
|
813
|
+
});
|
|
814
|
+
expect(nonJsonResponse.status).toBe(415);
|
|
815
|
+
await expect(nonJsonResponse.json()).resolves.toEqual({
|
|
816
|
+
error: "Bridge only accepts JSON request bodies.",
|
|
817
|
+
});
|
|
818
|
+
// The queue transport keeps its 415 gate for the attachment upload path
|
|
819
|
+
// too: it carries a string envelope only, so it never admits a binary body.
|
|
820
|
+
const attachmentOctetStreamResponse = await fetch(`${bridge.baseUrl}/api/companies/co-1/issues/issue-1/attachments`, {
|
|
821
|
+
method: "POST",
|
|
822
|
+
headers: {
|
|
823
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
824
|
+
"content-type": "application/octet-stream",
|
|
825
|
+
},
|
|
826
|
+
body: Buffer.from([0x50, 0x4b, 0x03, 0x04]),
|
|
827
|
+
});
|
|
828
|
+
expect(attachmentOctetStreamResponse.status).toBe(415);
|
|
829
|
+
await expect(attachmentOctetStreamResponse.json()).resolves.toEqual({
|
|
830
|
+
error: "Bridge only accepts JSON request bodies.",
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
it("returns a 502 when the host response times out", async () => {
|
|
834
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-timeout-"));
|
|
835
|
+
cleanupDirs.push(rootDir);
|
|
836
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
837
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
838
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
839
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
840
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "bridge timeout test\n", "utf8");
|
|
841
|
+
const runner = createExecRunner();
|
|
842
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
843
|
+
cleanupFns.push(bridgeAsset.cleanup);
|
|
844
|
+
const prepared = await prepareCommandManagedRuntime({
|
|
845
|
+
runner,
|
|
846
|
+
spec: {
|
|
847
|
+
remoteCwd: remoteWorkspaceDir,
|
|
848
|
+
timeoutMs: 30_000,
|
|
849
|
+
},
|
|
850
|
+
adapterKey: "codex",
|
|
851
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
852
|
+
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
|
|
853
|
+
});
|
|
854
|
+
const queueDir = path.posix.join(prepared.runtimeRootDir, "paperclip-bridge");
|
|
855
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
856
|
+
const bridge = await startSandboxCallbackBridgeServer({
|
|
857
|
+
runner,
|
|
858
|
+
remoteCwd: remoteWorkspaceDir,
|
|
859
|
+
assetRemoteDir: prepared.assetDirs.bridge,
|
|
860
|
+
queueDir,
|
|
861
|
+
bridgeToken,
|
|
862
|
+
timeoutMs: 30_000,
|
|
863
|
+
pollIntervalMs: 10,
|
|
864
|
+
responseTimeoutMs: 75,
|
|
865
|
+
});
|
|
866
|
+
cleanupFns.push(async () => {
|
|
867
|
+
await bridge.stop();
|
|
868
|
+
});
|
|
869
|
+
const response = await fetch(`${bridge.baseUrl}/api/agents/me`, {
|
|
870
|
+
headers: {
|
|
871
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
872
|
+
},
|
|
873
|
+
});
|
|
874
|
+
expect(response.status).toBe(502);
|
|
875
|
+
await expect(response.json()).resolves.toEqual({
|
|
876
|
+
error: "Timed out waiting for host bridge response.",
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
it("returns a 502 for malformed host response files", async () => {
|
|
880
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-malformed-response-"));
|
|
881
|
+
cleanupDirs.push(rootDir);
|
|
882
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
883
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
884
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
885
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
886
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "bridge malformed response test\n", "utf8");
|
|
887
|
+
const runner = createExecRunner();
|
|
888
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
889
|
+
cleanupFns.push(bridgeAsset.cleanup);
|
|
890
|
+
const prepared = await prepareCommandManagedRuntime({
|
|
891
|
+
runner,
|
|
892
|
+
spec: {
|
|
893
|
+
remoteCwd: remoteWorkspaceDir,
|
|
894
|
+
timeoutMs: 30_000,
|
|
895
|
+
},
|
|
896
|
+
adapterKey: "codex",
|
|
897
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
898
|
+
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
|
|
899
|
+
});
|
|
900
|
+
const queueDir = path.posix.join(prepared.runtimeRootDir, "paperclip-bridge");
|
|
901
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
902
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
903
|
+
const bridge = await startSandboxCallbackBridgeServer({
|
|
904
|
+
runner,
|
|
905
|
+
remoteCwd: remoteWorkspaceDir,
|
|
906
|
+
assetRemoteDir: prepared.assetDirs.bridge,
|
|
907
|
+
queueDir,
|
|
908
|
+
bridgeToken,
|
|
909
|
+
timeoutMs: 30_000,
|
|
910
|
+
pollIntervalMs: 10,
|
|
911
|
+
responseTimeoutMs: 1_000,
|
|
912
|
+
});
|
|
913
|
+
cleanupFns.push(async () => {
|
|
914
|
+
await bridge.stop();
|
|
915
|
+
});
|
|
916
|
+
const responsePromise = fetch(`${bridge.baseUrl}/api/agents/me`, {
|
|
917
|
+
headers: {
|
|
918
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
919
|
+
},
|
|
920
|
+
});
|
|
921
|
+
const requestFile = await waitForJsonFile(directories.requestsDir);
|
|
922
|
+
await writeFile(path.posix.join(directories.responsesDir, requestFile), '{"status":200,"headers":{"content-type":"application/json"},"body"', "utf8");
|
|
923
|
+
const response = await responsePromise;
|
|
924
|
+
expect(response.status).toBe(502);
|
|
925
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
926
|
+
error: expect.stringMatching(/JSON|Unexpected|Unterminated/i),
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
it("reuses an already-uploaded bridge entrypoint when the remote file hash matches", async () => {
|
|
930
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-sync-"));
|
|
931
|
+
cleanupDirs.push(rootDir);
|
|
932
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
933
|
+
const remoteAssetDir = path.posix.join(remoteWorkspaceDir, ".paperclip-runtime", "codex", "paperclip-bridge", "server");
|
|
934
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
935
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
936
|
+
cleanupFns.push(bridgeAsset.cleanup);
|
|
937
|
+
const originalSource = await readFile(bridgeAsset.entrypoint, "utf8");
|
|
938
|
+
const expandedSource = `${originalSource}\n// bridge payload padding\n`;
|
|
939
|
+
await writeFile(bridgeAsset.entrypoint, expandedSource, "utf8");
|
|
940
|
+
const runner = createExecRunner();
|
|
941
|
+
const first = await syncSandboxCallbackBridgeEntrypoint({
|
|
942
|
+
runner,
|
|
943
|
+
remoteCwd: remoteWorkspaceDir,
|
|
944
|
+
assetRemoteDir: remoteAssetDir,
|
|
945
|
+
bridgeAsset,
|
|
946
|
+
timeoutMs: 30_000,
|
|
947
|
+
});
|
|
948
|
+
const second = await syncSandboxCallbackBridgeEntrypoint({
|
|
949
|
+
runner,
|
|
950
|
+
remoteCwd: remoteWorkspaceDir,
|
|
951
|
+
assetRemoteDir: remoteAssetDir,
|
|
952
|
+
bridgeAsset,
|
|
953
|
+
timeoutMs: 30_000,
|
|
954
|
+
});
|
|
955
|
+
expect(first.uploaded).toBe(true);
|
|
956
|
+
expect(second.uploaded).toBe(false);
|
|
957
|
+
await expect(readFile(path.posix.join(remoteAssetDir, "paperclip-bridge-server.mjs"), "utf8")).resolves.toBe(expandedSource);
|
|
958
|
+
await expect(readdir(remoteAssetDir).then((entries) => entries.filter((entry) => entry.endsWith(".paperclip-upload.b64") ||
|
|
959
|
+
entry.endsWith(".partial") ||
|
|
960
|
+
entry === ".paperclip-bridge-upload.lock"))).resolves.toEqual([]);
|
|
961
|
+
});
|
|
962
|
+
it("rejects a corrupted bridge entrypoint upload without committing a torn remote file", async () => {
|
|
963
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-sync-corrupt-"));
|
|
964
|
+
cleanupDirs.push(rootDir);
|
|
965
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
966
|
+
const remoteAssetDir = path.posix.join(remoteWorkspaceDir, ".paperclip-runtime", "codex", "paperclip-bridge", "server");
|
|
967
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
968
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
969
|
+
cleanupFns.push(bridgeAsset.cleanup);
|
|
970
|
+
const runner = {
|
|
971
|
+
execute: async (input) => await createExecRunner().execute({
|
|
972
|
+
...input,
|
|
973
|
+
stdin: input.stdin != null ? "" : input.stdin,
|
|
974
|
+
}),
|
|
975
|
+
};
|
|
976
|
+
await expect(syncSandboxCallbackBridgeEntrypoint({
|
|
977
|
+
runner,
|
|
978
|
+
remoteCwd: remoteWorkspaceDir,
|
|
979
|
+
assetRemoteDir: remoteAssetDir,
|
|
980
|
+
bridgeAsset,
|
|
981
|
+
timeoutMs: 30_000,
|
|
982
|
+
})).rejects.toThrow(/sha mismatch/i);
|
|
983
|
+
await expect(readFile(path.posix.join(remoteAssetDir, "paperclip-bridge-server.mjs"), "utf8")).rejects.toThrow();
|
|
984
|
+
await expect(readdir(remoteAssetDir).then((entries) => entries.filter((entry) => entry.endsWith(".paperclip-upload.b64") ||
|
|
985
|
+
entry.endsWith(".partial") ||
|
|
986
|
+
entry === ".paperclip-bridge-upload.lock"))).resolves.toEqual([]);
|
|
987
|
+
});
|
|
988
|
+
// The process-session remote script is a static, Paperclip-authored `.mjs`
|
|
989
|
+
// written into the sandbox on every bridge start. `syncRemoteTextFileWithHashSkip`
|
|
990
|
+
// (which now backs that write, mirroring the bridge-entrypoint sha256 gate)
|
|
991
|
+
// content-hash-skips it so a warm start where the remote script already matches
|
|
992
|
+
// costs ZERO write execs instead of the prior ~3 (prepare/append/finalize base64
|
|
993
|
+
// upload).
|
|
994
|
+
it("test_process_session_script_skipped_when_remote_hash_matches: warm start with a matching remote hash writes 0 execs", async () => {
|
|
995
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-hashskip-warm-"));
|
|
996
|
+
cleanupDirs.push(rootDir);
|
|
997
|
+
const remoteDir = path.join(rootDir, "runtime", "codex", "process-sessions");
|
|
998
|
+
const remotePath = path.posix.join(remoteDir, "paperclip-process-session-remote.mjs");
|
|
999
|
+
const lockDir = path.posix.join(remoteDir, ".paperclip-process-session-script.lock");
|
|
1000
|
+
const body = "console.log('process session remote script v1');\n";
|
|
1001
|
+
let execCount = 0;
|
|
1002
|
+
const inner = createExecRunner();
|
|
1003
|
+
const runner = {
|
|
1004
|
+
execute: async (input) => {
|
|
1005
|
+
execCount += 1;
|
|
1006
|
+
return inner.execute(input);
|
|
1007
|
+
},
|
|
1008
|
+
};
|
|
1009
|
+
const args = {
|
|
1010
|
+
runner,
|
|
1011
|
+
remoteCwd: rootDir,
|
|
1012
|
+
remoteDir,
|
|
1013
|
+
remotePath,
|
|
1014
|
+
body,
|
|
1015
|
+
label: "Process session remote script",
|
|
1016
|
+
action: "sync process session remote script",
|
|
1017
|
+
lockDir,
|
|
1018
|
+
timeoutMs: 30_000,
|
|
1019
|
+
};
|
|
1020
|
+
// Cold start: the script is uploaded (single sha-gate exec that writes).
|
|
1021
|
+
const first = await syncRemoteTextFileWithHashSkip(args);
|
|
1022
|
+
expect(first.uploaded).toBe(true);
|
|
1023
|
+
await expect(readFile(remotePath, "utf8")).resolves.toBe(body);
|
|
1024
|
+
// Warm start: the remote hash matches, so the write is skipped entirely.
|
|
1025
|
+
execCount = 0;
|
|
1026
|
+
const second = await syncRemoteTextFileWithHashSkip(args);
|
|
1027
|
+
expect(second.uploaded).toBe(false);
|
|
1028
|
+
// A single hash-gate round-trip that performed 0 writes (down from ~3 execs).
|
|
1029
|
+
expect(execCount).toBe(1);
|
|
1030
|
+
// sha is still returned on the skip path so callers get a well-formed result.
|
|
1031
|
+
expect(second.sha256).toBe(first.sha256);
|
|
1032
|
+
// The remote file is unchanged and no upload/partial/lock leftovers remain.
|
|
1033
|
+
await expect(readFile(remotePath, "utf8")).resolves.toBe(body);
|
|
1034
|
+
await expect(readdir(remoteDir).then((entries) => entries.filter((entry) => entry.endsWith(".paperclip-upload.b64") ||
|
|
1035
|
+
entry.endsWith(".partial") ||
|
|
1036
|
+
entry === ".paperclip-process-session-script.lock"))).resolves.toEqual([]);
|
|
1037
|
+
});
|
|
1038
|
+
it("test_process_session_script_rewritten_on_hash_mismatch: a mismatched remote hash still rewrites the script", async () => {
|
|
1039
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-hashskip-cold-"));
|
|
1040
|
+
cleanupDirs.push(rootDir);
|
|
1041
|
+
const remoteDir = path.join(rootDir, "runtime", "codex", "process-sessions");
|
|
1042
|
+
const remotePath = path.posix.join(remoteDir, "paperclip-process-session-remote.mjs");
|
|
1043
|
+
const lockDir = path.posix.join(remoteDir, ".paperclip-process-session-script.lock");
|
|
1044
|
+
const body = "console.log('process session remote script v2');\n";
|
|
1045
|
+
// Pre-seed the remote with a DIFFERENT script (a prior/stale build).
|
|
1046
|
+
await mkdir(remoteDir, { recursive: true });
|
|
1047
|
+
await writeFile(remotePath, "console.log('stale remote script');\n", "utf8");
|
|
1048
|
+
const result = await syncRemoteTextFileWithHashSkip({
|
|
1049
|
+
runner: createExecRunner(),
|
|
1050
|
+
remoteCwd: rootDir,
|
|
1051
|
+
remoteDir,
|
|
1052
|
+
remotePath,
|
|
1053
|
+
body,
|
|
1054
|
+
label: "Process session remote script",
|
|
1055
|
+
action: "sync process session remote script",
|
|
1056
|
+
lockDir,
|
|
1057
|
+
timeoutMs: 30_000,
|
|
1058
|
+
});
|
|
1059
|
+
expect(result.uploaded).toBe(true);
|
|
1060
|
+
await expect(readFile(remotePath, "utf8")).resolves.toBe(body);
|
|
1061
|
+
});
|
|
1062
|
+
it("fails loud when the hash-skip sync exec errors instead of silently re-uploading", async () => {
|
|
1063
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-hashskip-fail-"));
|
|
1064
|
+
cleanupDirs.push(rootDir);
|
|
1065
|
+
const remoteDir = path.join(rootDir, "runtime", "codex", "process-sessions");
|
|
1066
|
+
const remotePath = path.posix.join(remoteDir, "paperclip-process-session-remote.mjs");
|
|
1067
|
+
const lockDir = path.posix.join(remoteDir, ".paperclip-process-session-script.lock");
|
|
1068
|
+
// A runner whose exec fails: the hash-gate cannot be evaluated. The write
|
|
1069
|
+
// must surface the failure, never swallow it and re-upload behind a green
|
|
1070
|
+
// return value.
|
|
1071
|
+
const runner = {
|
|
1072
|
+
execute: async () => ({
|
|
1073
|
+
exitCode: 1,
|
|
1074
|
+
signal: null,
|
|
1075
|
+
timedOut: false,
|
|
1076
|
+
stdout: "",
|
|
1077
|
+
stderr: "hash gate boom",
|
|
1078
|
+
pid: null,
|
|
1079
|
+
startedAt: new Date().toISOString(),
|
|
1080
|
+
}),
|
|
1081
|
+
};
|
|
1082
|
+
await expect(syncRemoteTextFileWithHashSkip({
|
|
1083
|
+
runner,
|
|
1084
|
+
remoteCwd: rootDir,
|
|
1085
|
+
remoteDir,
|
|
1086
|
+
remotePath,
|
|
1087
|
+
body: "console.log('never written');\n",
|
|
1088
|
+
label: "Process session remote script",
|
|
1089
|
+
action: "sync process session remote script",
|
|
1090
|
+
lockDir,
|
|
1091
|
+
timeoutMs: 30_000,
|
|
1092
|
+
})).rejects.toThrow(/sync process session remote script/i);
|
|
1093
|
+
// Nothing was written to the remote path on the failure path.
|
|
1094
|
+
await expect(readFile(remotePath, "utf8")).rejects.toThrow();
|
|
1095
|
+
});
|
|
1096
|
+
it("permits the documented heartbeat surface and denies unrelated routes", () => {
|
|
1097
|
+
const allowed = [
|
|
1098
|
+
{ method: "POST", path: "/runtime-tools/github/credentials" },
|
|
1099
|
+
{ method: "GET", path: "/api/agents/me" },
|
|
1100
|
+
{ method: "GET", path: "/api/agents/me/inbox-lite" },
|
|
1101
|
+
{ method: "GET", path: "/api/agents/me/inbox/mine" },
|
|
1102
|
+
{ method: "GET", path: "/api/agents/agent-1" },
|
|
1103
|
+
{ method: "GET", path: "/api/agents/agent-1/skills" },
|
|
1104
|
+
{ method: "POST", path: "/api/agents/agent-1/skills/sync" },
|
|
1105
|
+
{ method: "PATCH", path: "/api/agents/agent-1/instructions-path" },
|
|
1106
|
+
{ method: "GET", path: "/api/companies/co-1" },
|
|
1107
|
+
{ method: "GET", path: "/api/companies/co-1/dashboard" },
|
|
1108
|
+
{ method: "GET", path: "/api/companies/co-1/agents" },
|
|
1109
|
+
{ method: "GET", path: "/api/companies/co-1/issues" },
|
|
1110
|
+
{ method: "GET", path: "/api/companies/co-1/projects" },
|
|
1111
|
+
{ method: "GET", path: "/api/companies/co-1/goals" },
|
|
1112
|
+
{ method: "GET", path: "/api/companies/co-1/org" },
|
|
1113
|
+
{ method: "GET", path: "/api/companies/co-1/approvals" },
|
|
1114
|
+
{ method: "GET", path: "/api/companies/co-1/routines" },
|
|
1115
|
+
{ method: "GET", path: "/api/companies/co-1/skills" },
|
|
1116
|
+
{ method: "GET", path: "/api/companies/co-1/email/inboxes" },
|
|
1117
|
+
{ method: "GET", path: "/api/companies/co-1/email/tasks/issue-1" },
|
|
1118
|
+
{ method: "GET", path: "/api/companies/co-1/email/deliveries/send-1" },
|
|
1119
|
+
{ method: "POST", path: "/api/companies/co-1/email/send" },
|
|
1120
|
+
// Hire skill (paperclip-create-agent): discovery + submit + issue linking
|
|
1121
|
+
{ method: "GET", path: "/llms/agent-configuration.txt" },
|
|
1122
|
+
{ method: "GET", path: "/llms/agent-configuration/claude_local.txt" },
|
|
1123
|
+
{ method: "GET", path: "/llms/agent-icons.txt" },
|
|
1124
|
+
{ method: "GET", path: "/api/companies/co-1/agent-configurations" },
|
|
1125
|
+
{ method: "POST", path: "/api/companies/co-1/agent-hires" },
|
|
1126
|
+
{ method: "POST", path: "/api/issues/issue-1/approvals" },
|
|
1127
|
+
{ method: "GET", path: "/api/projects/proj-1" },
|
|
1128
|
+
{ method: "GET", path: "/api/goals/goal-1" },
|
|
1129
|
+
{ method: "GET", path: "/api/issues/issue-1" },
|
|
1130
|
+
{ method: "GET", path: "/api/issues/issue-1/heartbeat-context" },
|
|
1131
|
+
{ method: "GET", path: "/api/issues/issue-1/comments" },
|
|
1132
|
+
{ method: "GET", path: "/api/issues/issue-1/comments/c-1" },
|
|
1133
|
+
{ method: "POST", path: "/api/issues/issue-1/comments" },
|
|
1134
|
+
{ method: "GET", path: "/api/issues/issue-1/documents" },
|
|
1135
|
+
{ method: "GET", path: "/api/issues/issue-1/documents/plan" },
|
|
1136
|
+
{ method: "GET", path: "/api/issues/issue-1/documents/plan/revisions" },
|
|
1137
|
+
{ method: "PUT", path: "/api/issues/issue-1/documents/plan" },
|
|
1138
|
+
{ method: "POST", path: "/api/issues/issue-1/checkout" },
|
|
1139
|
+
{ method: "POST", path: "/api/issues/issue-1/release" },
|
|
1140
|
+
{ method: "PATCH", path: "/api/issues/issue-1" },
|
|
1141
|
+
{ method: "GET", path: "/api/issues/issue-1/approvals" },
|
|
1142
|
+
{ method: "GET", path: "/api/issues/issue-1/work-products" },
|
|
1143
|
+
{ method: "POST", path: "/api/issues/issue-1/work-products" },
|
|
1144
|
+
{ method: "PATCH", path: "/api/work-products/wp-1" },
|
|
1145
|
+
{ method: "GET", path: "/api/issues/issue-1/interactions" },
|
|
1146
|
+
{ method: "GET", path: "/api/issues/issue-1/interactions/inter-1" },
|
|
1147
|
+
{ method: "POST", path: "/api/issues/issue-1/interactions" },
|
|
1148
|
+
{ method: "POST", path: "/api/issues/issue-1/interactions/inter-1/accept" },
|
|
1149
|
+
{ method: "POST", path: "/api/issues/issue-1/interactions/inter-1/reject" },
|
|
1150
|
+
{ method: "POST", path: "/api/issues/issue-1/interactions/inter-1/respond" },
|
|
1151
|
+
{ method: "POST", path: "/api/issues/issue-1/interactions/inter-1/verdicts" },
|
|
1152
|
+
{ method: "POST", path: "/api/issues/issue-1/interactions/inter-1/withdraw" },
|
|
1153
|
+
{ method: "POST", path: "/api/companies/co-1/issues" },
|
|
1154
|
+
{ method: "GET", path: "/api/approvals/ap-1" },
|
|
1155
|
+
{ method: "GET", path: "/api/approvals/ap-1/issues" },
|
|
1156
|
+
{ method: "GET", path: "/api/approvals/ap-1/comments" },
|
|
1157
|
+
{ method: "POST", path: "/api/approvals/ap-1/comments" },
|
|
1158
|
+
{ method: "POST", path: "/api/companies/co-1/approvals" },
|
|
1159
|
+
{ method: "GET", path: "/api/execution-workspaces/ws-1" },
|
|
1160
|
+
{ method: "POST", path: "/api/execution-workspaces/ws-1/runtime-services/start" },
|
|
1161
|
+
{ method: "POST", path: "/api/execution-workspaces/ws-1/runtime-services/stop" },
|
|
1162
|
+
{ method: "POST", path: "/api/execution-workspaces/ws-1/runtime-services/restart" },
|
|
1163
|
+
{ method: "GET", path: "/api/routines/r-1" },
|
|
1164
|
+
{ method: "GET", path: "/api/routines/r-1/runs" },
|
|
1165
|
+
{ method: "POST", path: "/api/companies/co-1/routines" },
|
|
1166
|
+
{ method: "PATCH", path: "/api/routines/r-1" },
|
|
1167
|
+
{ method: "POST", path: "/api/routines/r-1/run" },
|
|
1168
|
+
{ method: "POST", path: "/api/routines/r-1/triggers" },
|
|
1169
|
+
{ method: "PATCH", path: "/api/routine-triggers/t-1" },
|
|
1170
|
+
{ method: "DELETE", path: "/api/routine-triggers/t-1" },
|
|
1171
|
+
];
|
|
1172
|
+
for (const request of allowed) {
|
|
1173
|
+
expect(authorizeSandboxCallbackBridgeRequestWithRoutes(request)).toBeNull();
|
|
1174
|
+
}
|
|
1175
|
+
const denied = [
|
|
1176
|
+
{ method: "POST", path: "/api/companies/co-1/email/inboxes" },
|
|
1177
|
+
{ method: "POST", path: "/api/companies/co-1/email/connections" },
|
|
1178
|
+
{ method: "POST", path: "/api/companies/co-1/email/inspect" },
|
|
1179
|
+
{ method: "POST", path: "/api/companies/co-1/email/deliveries/send-1/resolve" },
|
|
1180
|
+
{ method: "POST", path: "/api/email/inboxes/inbox-1/reconnect" },
|
|
1181
|
+
{ method: "POST", path: "/api/email/inboxes/inbox-1/control" },
|
|
1182
|
+
{ method: "DELETE", path: "/api/companies/co-1/email/tasks/issue-1" },
|
|
1183
|
+
{ method: "DELETE", path: "/api/secrets" },
|
|
1184
|
+
// Pin the runtime-services regex to start/stop/restart only — anything
|
|
1185
|
+
// else (delete, reset, wipe, etc.) must stay denied even if the API
|
|
1186
|
+
// grows new actions later.
|
|
1187
|
+
{ method: "POST", path: "/api/execution-workspaces/ws-1/runtime-services/delete" },
|
|
1188
|
+
{ method: "POST", path: "/api/companies/co-1/agents" },
|
|
1189
|
+
// The hire allowlist must not over-match: only the exact .txt discovery
|
|
1190
|
+
// files, only agent-hires (not /agents), and no sub-resources beyond it.
|
|
1191
|
+
{ method: "GET", path: "/llms/agent-configuration" },
|
|
1192
|
+
{ method: "GET", path: "/llms/secrets.txt" },
|
|
1193
|
+
{ method: "POST", path: "/api/companies/co-1/agent-hires/ap-1" },
|
|
1194
|
+
{ method: "POST", path: "/api/issues/issue-1/approvals/ap-1" },
|
|
1195
|
+
{ method: "POST", path: "/api/agents/agent-1/pause" },
|
|
1196
|
+
{ method: "POST", path: "/api/agents/agent-1/terminate" },
|
|
1197
|
+
{ method: "POST", path: "/api/agents/agent-1/keys" },
|
|
1198
|
+
{ method: "POST", path: "/api/companies/co-1/exports" },
|
|
1199
|
+
{ method: "POST", path: "/api/companies/co-1/imports/apply" },
|
|
1200
|
+
{ method: "POST", path: "/api/companies/co-1/archive" },
|
|
1201
|
+
{ method: "DELETE", path: "/api/issues/issue-1/documents/plan" },
|
|
1202
|
+
{ method: "DELETE", path: "/api/issues/issue-1/approvals/ap-1" },
|
|
1203
|
+
{ method: "DELETE", path: "/api/work-products/wp-1" },
|
|
1204
|
+
{ method: "POST", path: "/api/approvals/ap-1/approve" },
|
|
1205
|
+
{ method: "POST", path: "/api/approvals/ap-1/reject" },
|
|
1206
|
+
{ method: "POST", path: "/api/companies/co-1/logo" },
|
|
1207
|
+
{ method: "GET", path: "/api/companies/co-1/secrets" },
|
|
1208
|
+
{ method: "PATCH", path: "/api/secrets/secret-1" },
|
|
1209
|
+
];
|
|
1210
|
+
for (const request of denied) {
|
|
1211
|
+
expect(authorizeSandboxCallbackBridgeRequestWithRoutes(request)).toBe(`Route not allowed: ${request.method} ${request.path}`);
|
|
1212
|
+
}
|
|
1213
|
+
// Both transports admit the full attachment workflow.
|
|
1214
|
+
const http2Allowed = [
|
|
1215
|
+
{ method: "GET", path: "/api/issues/issue-1/attachments" },
|
|
1216
|
+
{ method: "POST", path: "/api/companies/co-1/issues/issue-1/attachments" },
|
|
1217
|
+
{ method: "GET", path: "/api/attachments/att-1/content" },
|
|
1218
|
+
];
|
|
1219
|
+
for (const request of http2Allowed) {
|
|
1220
|
+
expect(authorizeSandboxCallbackBridgeRequestWithRoutes(request, HTTP2_SANDBOX_CALLBACK_BRIDGE_ROUTE_ALLOWLIST)).toBeNull();
|
|
1221
|
+
}
|
|
1222
|
+
const http2Denied = [
|
|
1223
|
+
// Wrong method for each attachment rule.
|
|
1224
|
+
{ method: "GET", path: "/api/companies/co-1/issues/issue-1/attachments" },
|
|
1225
|
+
{ method: "POST", path: "/api/attachments/att-1/content" },
|
|
1226
|
+
// Extra path segment for each attachment rule.
|
|
1227
|
+
{ method: "POST", path: "/api/companies/co-1/issues/issue-1/attachments/att-1" },
|
|
1228
|
+
{ method: "GET", path: "/api/attachments/att-1/content/extra" },
|
|
1229
|
+
];
|
|
1230
|
+
for (const request of http2Denied) {
|
|
1231
|
+
expect(authorizeSandboxCallbackBridgeRequestWithRoutes(request, HTTP2_SANDBOX_CALLBACK_BRIDGE_ROUTE_ALLOWLIST)).toBe(`Route not allowed: ${request.method} ${request.path}`);
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
it("admits listing, uploads and downloads on the default queue route list", () => {
|
|
1235
|
+
const attachmentRequests = [
|
|
1236
|
+
{ method: "GET", path: "/api/issues/issue-1/attachments" },
|
|
1237
|
+
{ method: "POST", path: "/api/companies/co-1/issues/issue-1/attachments" },
|
|
1238
|
+
{ method: "GET", path: "/api/attachments/att-1/content" },
|
|
1239
|
+
];
|
|
1240
|
+
for (const request of attachmentRequests) {
|
|
1241
|
+
expect(authorizeSandboxCallbackBridgeRequestWithRoutes(request)).toBeNull();
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
it("marks command-managed bridge operations with the bridge execution channel", async () => {
|
|
1245
|
+
const runner = {
|
|
1246
|
+
execute: vi.fn(async () => ({
|
|
1247
|
+
exitCode: 0,
|
|
1248
|
+
signal: null,
|
|
1249
|
+
timedOut: false,
|
|
1250
|
+
stdout: "",
|
|
1251
|
+
stderr: "",
|
|
1252
|
+
pid: null,
|
|
1253
|
+
startedAt: new Date().toISOString(),
|
|
1254
|
+
})),
|
|
1255
|
+
};
|
|
1256
|
+
const client = createCommandManagedSandboxCallbackBridgeQueueClient({
|
|
1257
|
+
runner,
|
|
1258
|
+
remoteCwd: "/workspace",
|
|
1259
|
+
timeoutMs: 30_000,
|
|
1260
|
+
});
|
|
1261
|
+
await client.makeDir("/workspace/.paperclip-runtime/codex/paperclip-bridge/queue");
|
|
1262
|
+
expect(runner.execute).toHaveBeenCalledWith(expect.objectContaining({
|
|
1263
|
+
env: {
|
|
1264
|
+
PAPERCLIP_SANDBOX_EXEC_CHANNEL: "bridge",
|
|
1265
|
+
},
|
|
1266
|
+
}));
|
|
1267
|
+
});
|
|
1268
|
+
it("creates the bridge queue directories in one directory-creation exec", async () => {
|
|
1269
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-makedirs-"));
|
|
1270
|
+
cleanupDirs.push(rootDir);
|
|
1271
|
+
const queueDir = path.posix.join(rootDir, "queue");
|
|
1272
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
1273
|
+
const makeDir = vi.fn(async () => { });
|
|
1274
|
+
const makeDirs = vi.fn(async () => { });
|
|
1275
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
1276
|
+
client: {
|
|
1277
|
+
makeDir,
|
|
1278
|
+
makeDirs,
|
|
1279
|
+
listJsonFiles: async () => [],
|
|
1280
|
+
readTextFile: async () => {
|
|
1281
|
+
throw new Error("unexpected readTextFile");
|
|
1282
|
+
},
|
|
1283
|
+
writeTextFile: async () => { },
|
|
1284
|
+
rename: async () => { },
|
|
1285
|
+
remove: async () => { },
|
|
1286
|
+
},
|
|
1287
|
+
queueDir,
|
|
1288
|
+
authorizeRequest: async () => null,
|
|
1289
|
+
handleRequest: async () => ({ status: 200, body: "ok" }),
|
|
1290
|
+
});
|
|
1291
|
+
await worker.stop();
|
|
1292
|
+
expect(makeDir).not.toHaveBeenCalled();
|
|
1293
|
+
expect(makeDirs).toHaveBeenCalledTimes(1);
|
|
1294
|
+
expect(makeDirs).toHaveBeenCalledWith([
|
|
1295
|
+
directories.rootDir,
|
|
1296
|
+
directories.requestsDir,
|
|
1297
|
+
directories.responsesDir,
|
|
1298
|
+
directories.logsDir,
|
|
1299
|
+
]);
|
|
1300
|
+
});
|
|
1301
|
+
it("falls back to sequential makeDir when the queue client omits makeDirs", async () => {
|
|
1302
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-makedir-fallback-"));
|
|
1303
|
+
cleanupDirs.push(rootDir);
|
|
1304
|
+
const queueDir = path.posix.join(rootDir, "queue");
|
|
1305
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
1306
|
+
const makeDir = vi.fn(async (_remotePath) => { });
|
|
1307
|
+
// A queue client that predates the batched makeDirs method. The worker
|
|
1308
|
+
// must still create every queue directory through sequential makeDir.
|
|
1309
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
1310
|
+
client: {
|
|
1311
|
+
makeDir,
|
|
1312
|
+
listJsonFiles: async () => [],
|
|
1313
|
+
readTextFile: async () => {
|
|
1314
|
+
throw new Error("unexpected readTextFile");
|
|
1315
|
+
},
|
|
1316
|
+
writeTextFile: async () => { },
|
|
1317
|
+
rename: async () => { },
|
|
1318
|
+
remove: async () => { },
|
|
1319
|
+
},
|
|
1320
|
+
queueDir,
|
|
1321
|
+
authorizeRequest: async () => null,
|
|
1322
|
+
handleRequest: async () => ({ status: 200, body: "ok" }),
|
|
1323
|
+
});
|
|
1324
|
+
await worker.stop();
|
|
1325
|
+
expect(makeDir.mock.calls.map((call) => call[0])).toEqual([
|
|
1326
|
+
directories.rootDir,
|
|
1327
|
+
directories.requestsDir,
|
|
1328
|
+
directories.responsesDir,
|
|
1329
|
+
directories.logsDir,
|
|
1330
|
+
]);
|
|
1331
|
+
});
|
|
1332
|
+
it("runs one mkdir -p exec for makeDirs on the command-managed queue client", async () => {
|
|
1333
|
+
const runner = {
|
|
1334
|
+
execute: vi.fn(async (_input) => ({
|
|
1335
|
+
exitCode: 0,
|
|
1336
|
+
signal: null,
|
|
1337
|
+
timedOut: false,
|
|
1338
|
+
stdout: "",
|
|
1339
|
+
stderr: "",
|
|
1340
|
+
pid: null,
|
|
1341
|
+
startedAt: new Date().toISOString(),
|
|
1342
|
+
})),
|
|
1343
|
+
};
|
|
1344
|
+
const client = createCommandManagedSandboxCallbackBridgeQueueClient({
|
|
1345
|
+
runner,
|
|
1346
|
+
remoteCwd: "/workspace",
|
|
1347
|
+
timeoutMs: 30_000,
|
|
1348
|
+
});
|
|
1349
|
+
// The command-managed client always provides the batched makeDirs method.
|
|
1350
|
+
expect(client.makeDirs).toBeDefined();
|
|
1351
|
+
await client.makeDirs?.(["/workspace/a", "/workspace/b", "/workspace/c"]);
|
|
1352
|
+
expect(runner.execute).toHaveBeenCalledTimes(1);
|
|
1353
|
+
const call = runner.execute.mock.calls[0][0];
|
|
1354
|
+
const script = call.args?.[call.args.length - 1] ?? "";
|
|
1355
|
+
expect(script).toContain("mkdir -p");
|
|
1356
|
+
expect(script).toContain("/workspace/a");
|
|
1357
|
+
expect(script).toContain("/workspace/b");
|
|
1358
|
+
expect(script).toContain("/workspace/c");
|
|
1359
|
+
});
|
|
1360
|
+
// Capture the run-level error the worker surfaces through `runtimeSpan`. The
|
|
1361
|
+
// worker runs a throwing function under the `sandbox.callbackBridge.workerFailed`
|
|
1362
|
+
// span; this double records that error and re-throws, like the real runner.
|
|
1363
|
+
function createWorkerErrorCapture() {
|
|
1364
|
+
const workerErrors = [];
|
|
1365
|
+
const runtimeSpan = async (name, work) => {
|
|
1366
|
+
try {
|
|
1367
|
+
return await work();
|
|
1368
|
+
}
|
|
1369
|
+
catch (error) {
|
|
1370
|
+
if (name === "sandbox.callbackBridge.workerFailed") {
|
|
1371
|
+
workerErrors.push(error instanceof Error ? error.message : String(error));
|
|
1372
|
+
}
|
|
1373
|
+
throw error;
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
return { runtimeSpan, workerErrors };
|
|
1377
|
+
}
|
|
1378
|
+
function bridgeRequestJson(id) {
|
|
1379
|
+
return `${JSON.stringify({
|
|
1380
|
+
id,
|
|
1381
|
+
method: "GET",
|
|
1382
|
+
path: "/api/agents/me",
|
|
1383
|
+
query: "",
|
|
1384
|
+
headers: {},
|
|
1385
|
+
body: "",
|
|
1386
|
+
createdAt: new Date().toISOString(),
|
|
1387
|
+
})}\n`;
|
|
1388
|
+
}
|
|
1389
|
+
it("times out a stalled poll, surfaces a run-level error, and recovers to deliver the request", async () => {
|
|
1390
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-hang-"));
|
|
1391
|
+
cleanupDirs.push(rootDir);
|
|
1392
|
+
const queueDir = path.posix.join(rootDir, "queue");
|
|
1393
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
1394
|
+
await mkdir(directories.requestsDir, { recursive: true });
|
|
1395
|
+
await writeFile(path.posix.join(directories.requestsDir, "req-a.json"), bridgeRequestJson("req-a"), "utf8");
|
|
1396
|
+
const { runtimeSpan, workerErrors } = createWorkerErrorCapture();
|
|
1397
|
+
const base = createFileSystemSandboxCallbackBridgeQueueClient();
|
|
1398
|
+
let listCalls = 0;
|
|
1399
|
+
const client = {
|
|
1400
|
+
...base,
|
|
1401
|
+
// The first poll never resolves — a silently unresponsive sandbox channel.
|
|
1402
|
+
// The per-iteration timeout must convert the hang into a caught error, and
|
|
1403
|
+
// the loop must then back off and retry rather than die: the request never
|
|
1404
|
+
// reached the handler, so the retry delivers the real response. A
|
|
1405
|
+
// sustained outage is the watchdog's job (proven separately below), not a
|
|
1406
|
+
// reason to fail a request one transient hang could still serve.
|
|
1407
|
+
listJsonFiles: async (dir) => {
|
|
1408
|
+
listCalls += 1;
|
|
1409
|
+
if (listCalls === 1) {
|
|
1410
|
+
return await new Promise(() => { });
|
|
1411
|
+
}
|
|
1412
|
+
return await base.listJsonFiles(dir);
|
|
1413
|
+
},
|
|
1414
|
+
};
|
|
1415
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
1416
|
+
client,
|
|
1417
|
+
queueDir,
|
|
1418
|
+
iterationTimeoutMs: 50,
|
|
1419
|
+
watchdogTimeoutMs: 10_000,
|
|
1420
|
+
runtimeSpan,
|
|
1421
|
+
authorizeRequest: async () => null,
|
|
1422
|
+
handleRequest: async () => ({ status: 200, body: "ok" }),
|
|
1423
|
+
});
|
|
1424
|
+
const responseFile = await waitForJsonFile(directories.responsesDir, 3_000);
|
|
1425
|
+
const responseBody = await readFile(path.posix.join(directories.responsesDir, responseFile), "utf8");
|
|
1426
|
+
expect(JSON.parse(responseBody).status).toBe(200);
|
|
1427
|
+
expect(workerErrors.length).toBeGreaterThan(0);
|
|
1428
|
+
expect(workerErrors[0]).toContain("timed out");
|
|
1429
|
+
await worker.stop({ drainTimeoutMs: 10 });
|
|
1430
|
+
});
|
|
1431
|
+
it("does not abandon an in-flight handler, so a started mutation is not applied twice", async () => {
|
|
1432
|
+
// Prove the completion fence for a request whose handler already started. The
|
|
1433
|
+
// per-iteration timeout rejects the wrapper but does not stop the handler, so
|
|
1434
|
+
// the host operation (a mutation) is still in flight. The recovery path must
|
|
1435
|
+
// not write a 503 there; a 503 makes the caller retry while the original
|
|
1436
|
+
// mutation still completes, applying it twice. The test proves no 503 lands
|
|
1437
|
+
// and the handler's real response is delivered exactly once.
|
|
1438
|
+
const waitFor = async (predicate, timeoutMs) => {
|
|
1439
|
+
const deadline = Date.now() + timeoutMs;
|
|
1440
|
+
while (Date.now() < deadline) {
|
|
1441
|
+
if (predicate()) {
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
1445
|
+
}
|
|
1446
|
+
throw new Error("waitFor timed out");
|
|
1447
|
+
};
|
|
1448
|
+
const queueDir = "/virtual-bridge/queue";
|
|
1449
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
1450
|
+
const requestFile = "req-late.json";
|
|
1451
|
+
const requestPath = path.posix.join(directories.requestsDir, requestFile);
|
|
1452
|
+
const responsePath = path.posix.join(directories.responsesDir, requestFile);
|
|
1453
|
+
// An in-memory queue client with no file-existence guards. A response write
|
|
1454
|
+
// always lands here, so only the completion fence controls the outcome. The
|
|
1455
|
+
// real filesystem and command clients add their own existence guards; this
|
|
1456
|
+
// client removes them, so the test isolates the fence.
|
|
1457
|
+
const requestBodies = new Map();
|
|
1458
|
+
requestBodies.set(requestPath, bridgeRequestJson("req-late"));
|
|
1459
|
+
const responseWrites = [];
|
|
1460
|
+
const requestRemovals = [];
|
|
1461
|
+
const handlerControl = { release: null };
|
|
1462
|
+
let handlerCompleted = false;
|
|
1463
|
+
const client = {
|
|
1464
|
+
makeDir: async () => { },
|
|
1465
|
+
makeDirs: async () => { },
|
|
1466
|
+
listJsonFiles: async (dir) => dir === directories.requestsDir
|
|
1467
|
+
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
|
|
1468
|
+
: [],
|
|
1469
|
+
readTextFile: async (remotePath) => {
|
|
1470
|
+
const body = requestBodies.get(remotePath);
|
|
1471
|
+
if (body === undefined) {
|
|
1472
|
+
throw new Error(`missing request ${remotePath}`);
|
|
1473
|
+
}
|
|
1474
|
+
return body;
|
|
1475
|
+
},
|
|
1476
|
+
writeTextFile: async () => { },
|
|
1477
|
+
writeResponseFile: async (remotePath, body) => {
|
|
1478
|
+
responseWrites.push({ path: remotePath, status: JSON.parse(body.trim()).status });
|
|
1479
|
+
return { wrote: true };
|
|
1480
|
+
},
|
|
1481
|
+
rename: async () => { },
|
|
1482
|
+
remove: async (remotePath) => {
|
|
1483
|
+
requestRemovals.push(remotePath);
|
|
1484
|
+
requestBodies.delete(remotePath);
|
|
1485
|
+
},
|
|
1486
|
+
};
|
|
1487
|
+
const { runtimeSpan, workerErrors } = createWorkerErrorCapture();
|
|
1488
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
1489
|
+
client,
|
|
1490
|
+
queueDir,
|
|
1491
|
+
iterationTimeoutMs: 50,
|
|
1492
|
+
watchdogTimeoutMs: 10_000,
|
|
1493
|
+
runtimeSpan,
|
|
1494
|
+
authorizeRequest: async () => null,
|
|
1495
|
+
// The handler stays pending until the test releases it, well after the
|
|
1496
|
+
// per-iteration timeout fires. It records that it finished, so the test
|
|
1497
|
+
// proves the in-flight handler truly ran.
|
|
1498
|
+
handleRequest: () => new Promise((resolve) => {
|
|
1499
|
+
handlerControl.release = () => {
|
|
1500
|
+
handlerCompleted = true;
|
|
1501
|
+
resolve({ status: 200, body: "req-late" });
|
|
1502
|
+
};
|
|
1503
|
+
}),
|
|
1504
|
+
});
|
|
1505
|
+
// Wait until the per-iteration timeout surfaced a run error and the handler
|
|
1506
|
+
// is pending. The recovery path ran, so it already skipped the in-flight
|
|
1507
|
+
// request.
|
|
1508
|
+
await waitFor(() => workerErrors.some((message) => message.includes("timed out")) && handlerControl.release !== null, 3_000);
|
|
1509
|
+
// The recovery path wrote no 503 for the in-flight request.
|
|
1510
|
+
expect(responseWrites.some((write) => write.status === 503)).toBe(false);
|
|
1511
|
+
// Release the handler after the timeout. It delivers the real response.
|
|
1512
|
+
handlerControl.release?.();
|
|
1513
|
+
await waitFor(() => handlerCompleted, 3_000);
|
|
1514
|
+
// Give the finalize write and remove time to land.
|
|
1515
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1516
|
+
expect(handlerCompleted).toBe(true);
|
|
1517
|
+
// The handler committed exactly one response for the request: the real 200.
|
|
1518
|
+
expect(responseWrites.filter((write) => write.path === responsePath)).toEqual([
|
|
1519
|
+
{ path: responsePath, status: 200 },
|
|
1520
|
+
]);
|
|
1521
|
+
expect(responseWrites.some((write) => write.status === 503)).toBe(false);
|
|
1522
|
+
// The handler removed the request file exactly once, after it finalized.
|
|
1523
|
+
expect(requestRemovals).toEqual([requestPath]);
|
|
1524
|
+
await worker.stop({ drainTimeoutMs: 10 });
|
|
1525
|
+
});
|
|
1526
|
+
it("aborts an in-flight handler on timeout, so a cooperating handler finalizes instead of stranding the request", async () => {
|
|
1527
|
+
// A handler that threads the worker signal into its work must stop when the
|
|
1528
|
+
// per-iteration timeout fires. It then finalizes with its own error
|
|
1529
|
+
// response, so the request does not strand with no response. The recovery
|
|
1530
|
+
// path still writes no 503 for the handler-owned request, so a started
|
|
1531
|
+
// mutation is not applied twice. The abort reaches the handler after the host
|
|
1532
|
+
// operation started, so the handler finalizes a non-retryable 504, not a
|
|
1533
|
+
// retryable 502; the caller then does not retry a mutation that may have
|
|
1534
|
+
// committed.
|
|
1535
|
+
const waitFor = async (predicate, timeoutMs) => {
|
|
1536
|
+
const deadline = Date.now() + timeoutMs;
|
|
1537
|
+
while (Date.now() < deadline) {
|
|
1538
|
+
if (predicate()) {
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
1542
|
+
}
|
|
1543
|
+
throw new Error("waitFor timed out");
|
|
1544
|
+
};
|
|
1545
|
+
const queueDir = "/virtual-bridge/queue";
|
|
1546
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
1547
|
+
const requestFile = "req-abort.json";
|
|
1548
|
+
const requestPath = path.posix.join(directories.requestsDir, requestFile);
|
|
1549
|
+
const responsePath = path.posix.join(directories.responsesDir, requestFile);
|
|
1550
|
+
const requestBodies = new Map();
|
|
1551
|
+
requestBodies.set(requestPath, bridgeRequestJson("req-abort"));
|
|
1552
|
+
const responseWrites = [];
|
|
1553
|
+
const requestRemovals = [];
|
|
1554
|
+
const client = {
|
|
1555
|
+
makeDir: async () => { },
|
|
1556
|
+
makeDirs: async () => { },
|
|
1557
|
+
listJsonFiles: async (dir) => dir === directories.requestsDir
|
|
1558
|
+
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
|
|
1559
|
+
: [],
|
|
1560
|
+
readTextFile: async (remotePath) => {
|
|
1561
|
+
const body = requestBodies.get(remotePath);
|
|
1562
|
+
if (body === undefined) {
|
|
1563
|
+
throw new Error(`missing request ${remotePath}`);
|
|
1564
|
+
}
|
|
1565
|
+
return body;
|
|
1566
|
+
},
|
|
1567
|
+
writeTextFile: async () => { },
|
|
1568
|
+
writeResponseFile: async (remotePath, body) => {
|
|
1569
|
+
responseWrites.push({ path: remotePath, status: JSON.parse(body.trim()).status });
|
|
1570
|
+
return { wrote: true };
|
|
1571
|
+
},
|
|
1572
|
+
rename: async () => { },
|
|
1573
|
+
remove: async (remotePath) => {
|
|
1574
|
+
requestRemovals.push(remotePath);
|
|
1575
|
+
requestBodies.delete(remotePath);
|
|
1576
|
+
},
|
|
1577
|
+
};
|
|
1578
|
+
const { runtimeSpan, workerErrors } = createWorkerErrorCapture();
|
|
1579
|
+
let handlerAborted = false;
|
|
1580
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
1581
|
+
client,
|
|
1582
|
+
queueDir,
|
|
1583
|
+
iterationTimeoutMs: 50,
|
|
1584
|
+
watchdogTimeoutMs: 10_000,
|
|
1585
|
+
runtimeSpan,
|
|
1586
|
+
authorizeRequest: async () => null,
|
|
1587
|
+
// The handler stays pending until the worker aborts the signal. It then
|
|
1588
|
+
// rejects, so the request finalizes with the handler's own error response.
|
|
1589
|
+
handleRequest: (_request, options) => new Promise((_resolve, reject) => {
|
|
1590
|
+
options?.signal.addEventListener("abort", () => {
|
|
1591
|
+
handlerAborted = true;
|
|
1592
|
+
reject(new Error("aborted by worker"));
|
|
1593
|
+
});
|
|
1594
|
+
}),
|
|
1595
|
+
});
|
|
1596
|
+
// The handler finalizes only after the worker aborts it. The request gets a
|
|
1597
|
+
// terminal response (a 502 from the handler failure), never stranded.
|
|
1598
|
+
await waitFor(() => responseWrites.some((write) => write.path === responsePath), 3_000);
|
|
1599
|
+
expect(handlerAborted).toBe(true);
|
|
1600
|
+
expect(workerErrors.some((message) => message.includes("timed out"))).toBe(true);
|
|
1601
|
+
// Exactly one response landed: the handler's non-retryable 504. The recovery
|
|
1602
|
+
// path wrote no competing 503, and the aborted handler wrote no retryable 502.
|
|
1603
|
+
expect(responseWrites.filter((write) => write.path === responsePath)).toEqual([
|
|
1604
|
+
{ path: responsePath, status: 504 },
|
|
1605
|
+
]);
|
|
1606
|
+
expect(responseWrites.some((write) => write.status === 503)).toBe(false);
|
|
1607
|
+
expect(responseWrites.some((write) => write.status === 502)).toBe(false);
|
|
1608
|
+
// The handler removed the request file after it finalized. The recovery path
|
|
1609
|
+
// may issue a redundant idempotent remove for the same path when the handler
|
|
1610
|
+
// finalized first, so assert the removal happened rather than a fixed count.
|
|
1611
|
+
expect(requestRemovals).toContain(requestPath);
|
|
1612
|
+
await worker.stop({ drainTimeoutMs: 10 });
|
|
1613
|
+
});
|
|
1614
|
+
it("finalizes an aborted handler that ignores the signal and never settles with a non-retryable 504 backstop", async () => {
|
|
1615
|
+
// A handler that does not thread the worker signal into its work and never
|
|
1616
|
+
// settles must not strand the request. The recovery path aborts the handler,
|
|
1617
|
+
// waits the grace, then writes a non-retryable 504 backstop, so the request
|
|
1618
|
+
// gets a terminal response even when the handler ignores the abort. The 504
|
|
1619
|
+
// is non-retryable, so the caller does not retry a mutation that may have
|
|
1620
|
+
// committed.
|
|
1621
|
+
const waitFor = async (predicate, timeoutMs) => {
|
|
1622
|
+
const deadline = Date.now() + timeoutMs;
|
|
1623
|
+
while (Date.now() < deadline) {
|
|
1624
|
+
if (predicate()) {
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
1628
|
+
}
|
|
1629
|
+
throw new Error("waitFor timed out");
|
|
1630
|
+
};
|
|
1631
|
+
const queueDir = "/virtual-bridge/queue";
|
|
1632
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
1633
|
+
const requestFile = "req-stuck.json";
|
|
1634
|
+
const requestPath = path.posix.join(directories.requestsDir, requestFile);
|
|
1635
|
+
const responsePath = path.posix.join(directories.responsesDir, requestFile);
|
|
1636
|
+
const requestBodies = new Map();
|
|
1637
|
+
requestBodies.set(requestPath, bridgeRequestJson("req-stuck"));
|
|
1638
|
+
const responseWrites = [];
|
|
1639
|
+
const requestRemovals = [];
|
|
1640
|
+
const client = {
|
|
1641
|
+
makeDir: async () => { },
|
|
1642
|
+
makeDirs: async () => { },
|
|
1643
|
+
listJsonFiles: async (dir) => dir === directories.requestsDir
|
|
1644
|
+
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
|
|
1645
|
+
: [],
|
|
1646
|
+
readTextFile: async (remotePath) => {
|
|
1647
|
+
const body = requestBodies.get(remotePath);
|
|
1648
|
+
if (body === undefined) {
|
|
1649
|
+
throw new Error(`missing request ${remotePath}`);
|
|
1650
|
+
}
|
|
1651
|
+
return body;
|
|
1652
|
+
},
|
|
1653
|
+
writeTextFile: async () => { },
|
|
1654
|
+
writeResponseFile: async (remotePath, body) => {
|
|
1655
|
+
responseWrites.push({ path: remotePath, status: JSON.parse(body.trim()).status, body });
|
|
1656
|
+
return { wrote: true };
|
|
1657
|
+
},
|
|
1658
|
+
rename: async () => { },
|
|
1659
|
+
remove: async (remotePath) => {
|
|
1660
|
+
requestRemovals.push(remotePath);
|
|
1661
|
+
requestBodies.delete(remotePath);
|
|
1662
|
+
},
|
|
1663
|
+
};
|
|
1664
|
+
const { runtimeSpan, workerErrors } = createWorkerErrorCapture();
|
|
1665
|
+
let handlerStarted = false;
|
|
1666
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
1667
|
+
client,
|
|
1668
|
+
queueDir,
|
|
1669
|
+
iterationTimeoutMs: 50,
|
|
1670
|
+
watchdogTimeoutMs: 10_000,
|
|
1671
|
+
// A short grace, so the backstop fires quickly after the abort.
|
|
1672
|
+
abortedHandlerGraceMs: 30,
|
|
1673
|
+
runtimeSpan,
|
|
1674
|
+
authorizeRequest: async () => null,
|
|
1675
|
+
// The handler ignores the worker signal and never settles. Without the
|
|
1676
|
+
// backstop, the request would stay without a response forever.
|
|
1677
|
+
handleRequest: () => {
|
|
1678
|
+
handlerStarted = true;
|
|
1679
|
+
return new Promise(() => { });
|
|
1680
|
+
},
|
|
1681
|
+
});
|
|
1682
|
+
// The backstop writes the terminal response after the grace.
|
|
1683
|
+
await waitFor(() => responseWrites.some((write) => write.path === responsePath), 3_000);
|
|
1684
|
+
expect(handlerStarted).toBe(true);
|
|
1685
|
+
expect(workerErrors.some((message) => message.includes("timed out"))).toBe(true);
|
|
1686
|
+
// Exactly one response landed: the non-retryable 504 backstop. The recovery
|
|
1687
|
+
// path wrote no retryable 503 or 502 for the handler-owned request.
|
|
1688
|
+
const requestResponses = responseWrites.filter((write) => write.path === responsePath);
|
|
1689
|
+
expect(requestResponses).toHaveLength(1);
|
|
1690
|
+
expect(requestResponses[0]?.status).toBe(504);
|
|
1691
|
+
const parsed = JSON.parse(requestResponses[0].body.trim());
|
|
1692
|
+
const responseBody = JSON.parse(parsed.body);
|
|
1693
|
+
expect(responseBody.outcome).toBe("indeterminate");
|
|
1694
|
+
expect(responseBody.retryable).toBe(false);
|
|
1695
|
+
expect(parsed.headers?.["x-paperclip-bridge-outcome"]).toBe("indeterminate");
|
|
1696
|
+
expect(responseWrites.some((write) => write.status === 503)).toBe(false);
|
|
1697
|
+
expect(responseWrites.some((write) => write.status === 502)).toBe(false);
|
|
1698
|
+
// The backstop removed the request file, so it does not strand in the queue.
|
|
1699
|
+
expect(requestRemovals).toContain(requestPath);
|
|
1700
|
+
await worker.stop({ drainTimeoutMs: 10 });
|
|
1701
|
+
});
|
|
1702
|
+
it("finalizes a late worker-aborted handler with a non-retryable 504, so the caller does not retry a committed mutation", async () => {
|
|
1703
|
+
// Prove the completion status for a mutating request that the worker aborts.
|
|
1704
|
+
// The per-iteration timeout aborts a handler that already started its host
|
|
1705
|
+
// operation, so the mutation may have committed. The bridge cannot cancel a
|
|
1706
|
+
// host operation that is in flight. The handler completes late, after the
|
|
1707
|
+
// abort. Its response must be a non-retryable 504, never a retryable 502 or
|
|
1708
|
+
// 503; a retry of either would apply the mutation twice.
|
|
1709
|
+
const waitFor = async (predicate, timeoutMs) => {
|
|
1710
|
+
const deadline = Date.now() + timeoutMs;
|
|
1711
|
+
while (Date.now() < deadline) {
|
|
1712
|
+
if (predicate()) {
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
1716
|
+
}
|
|
1717
|
+
throw new Error("waitFor timed out");
|
|
1718
|
+
};
|
|
1719
|
+
const queueDir = "/virtual-bridge/queue";
|
|
1720
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
1721
|
+
const requestFile = "req-late-abort.json";
|
|
1722
|
+
const requestPath = path.posix.join(directories.requestsDir, requestFile);
|
|
1723
|
+
const responsePath = path.posix.join(directories.responsesDir, requestFile);
|
|
1724
|
+
const requestBodies = new Map();
|
|
1725
|
+
requestBodies.set(requestPath, bridgeRequestJson("req-late-abort"));
|
|
1726
|
+
const responseWrites = [];
|
|
1727
|
+
const requestRemovals = [];
|
|
1728
|
+
const client = {
|
|
1729
|
+
makeDir: async () => { },
|
|
1730
|
+
makeDirs: async () => { },
|
|
1731
|
+
listJsonFiles: async (dir) => dir === directories.requestsDir
|
|
1732
|
+
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
|
|
1733
|
+
: [],
|
|
1734
|
+
readTextFile: async (remotePath) => {
|
|
1735
|
+
const body = requestBodies.get(remotePath);
|
|
1736
|
+
if (body === undefined) {
|
|
1737
|
+
throw new Error(`missing request ${remotePath}`);
|
|
1738
|
+
}
|
|
1739
|
+
return body;
|
|
1740
|
+
},
|
|
1741
|
+
writeTextFile: async () => { },
|
|
1742
|
+
writeResponseFile: async (remotePath, body) => {
|
|
1743
|
+
responseWrites.push({ path: remotePath, status: JSON.parse(body.trim()).status, body });
|
|
1744
|
+
return { wrote: true };
|
|
1745
|
+
},
|
|
1746
|
+
rename: async () => { },
|
|
1747
|
+
remove: async (remotePath) => {
|
|
1748
|
+
requestRemovals.push(remotePath);
|
|
1749
|
+
requestBodies.delete(remotePath);
|
|
1750
|
+
},
|
|
1751
|
+
};
|
|
1752
|
+
const { runtimeSpan, workerErrors } = createWorkerErrorCapture();
|
|
1753
|
+
let handlerCompletedLate = false;
|
|
1754
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
1755
|
+
client,
|
|
1756
|
+
queueDir,
|
|
1757
|
+
iterationTimeoutMs: 50,
|
|
1758
|
+
watchdogTimeoutMs: 10_000,
|
|
1759
|
+
runtimeSpan,
|
|
1760
|
+
authorizeRequest: async () => null,
|
|
1761
|
+
// The handler mirrors a mutating forward that already committed on the
|
|
1762
|
+
// host. It rejects only after the worker aborts it, so its completion is
|
|
1763
|
+
// late. The mutation stays committed; the abort cannot undo it.
|
|
1764
|
+
handleRequest: (_request, options) => new Promise((_resolve, reject) => {
|
|
1765
|
+
options?.signal.addEventListener("abort", () => {
|
|
1766
|
+
handlerCompletedLate = true;
|
|
1767
|
+
reject(new Error("aborted by worker after the mutation committed"));
|
|
1768
|
+
});
|
|
1769
|
+
}),
|
|
1770
|
+
});
|
|
1771
|
+
await waitFor(() => responseWrites.some((write) => write.path === responsePath), 3_000);
|
|
1772
|
+
expect(handlerCompletedLate).toBe(true);
|
|
1773
|
+
expect(workerErrors.some((message) => message.includes("timed out"))).toBe(true);
|
|
1774
|
+
// Exactly one response landed for the request: the non-retryable 504.
|
|
1775
|
+
const requestResponses = responseWrites.filter((write) => write.path === responsePath);
|
|
1776
|
+
expect(requestResponses).toHaveLength(1);
|
|
1777
|
+
expect(requestResponses[0]?.status).toBe(504);
|
|
1778
|
+
// The bridge wrote no retryable status for the possibly-committed mutation.
|
|
1779
|
+
expect(responseWrites.some((write) => write.status === 502)).toBe(false);
|
|
1780
|
+
expect(responseWrites.some((write) => write.status === 503)).toBe(false);
|
|
1781
|
+
// The response marks the outcome indeterminate and non-retryable, so the
|
|
1782
|
+
// caller does not retry.
|
|
1783
|
+
const parsed = JSON.parse(requestResponses[0]?.body ?? "{}");
|
|
1784
|
+
expect(parsed.status).toBe(504);
|
|
1785
|
+
const responseBody = JSON.parse(parsed.body);
|
|
1786
|
+
expect(responseBody.outcome).toBe("indeterminate");
|
|
1787
|
+
expect(responseBody.retryable).toBe(false);
|
|
1788
|
+
expect(parsed.headers?.["x-paperclip-bridge-outcome"]).toBe("indeterminate");
|
|
1789
|
+
expect(requestRemovals).toContain(requestPath);
|
|
1790
|
+
await worker.stop({ drainTimeoutMs: 10 });
|
|
1791
|
+
});
|
|
1792
|
+
it("retries a failed 504 backstop write and keeps the request until it lands, so the caller is not stranded", async () => {
|
|
1793
|
+
// A backstop write can fail transiently, or it can exceed the iteration
|
|
1794
|
+
// timeout. The recovery path must not drop the request file on that failure. A
|
|
1795
|
+
// dropped file leaves no terminal 504 for the caller and fences out a late
|
|
1796
|
+
// handler, so the caller waits to its own deadline and gets a generic 502. The
|
|
1797
|
+
// recovery path retries the write and removes the request file only after the
|
|
1798
|
+
// write lands.
|
|
1799
|
+
const waitFor = async (predicate, timeoutMs) => {
|
|
1800
|
+
const deadline = Date.now() + timeoutMs;
|
|
1801
|
+
while (Date.now() < deadline) {
|
|
1802
|
+
if (predicate()) {
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
1806
|
+
}
|
|
1807
|
+
throw new Error("waitFor timed out");
|
|
1808
|
+
};
|
|
1809
|
+
const queueDir = "/virtual-bridge/queue";
|
|
1810
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
1811
|
+
const requestFile = "req-retry.json";
|
|
1812
|
+
const requestPath = path.posix.join(directories.requestsDir, requestFile);
|
|
1813
|
+
const responsePath = path.posix.join(directories.responsesDir, requestFile);
|
|
1814
|
+
const requestBodies = new Map();
|
|
1815
|
+
requestBodies.set(requestPath, bridgeRequestJson("req-retry"));
|
|
1816
|
+
const responseWrites = [];
|
|
1817
|
+
const requestRemovals = [];
|
|
1818
|
+
let writeAttempts = 0;
|
|
1819
|
+
const client = {
|
|
1820
|
+
makeDir: async () => { },
|
|
1821
|
+
makeDirs: async () => { },
|
|
1822
|
+
listJsonFiles: async (dir) => dir === directories.requestsDir
|
|
1823
|
+
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
|
|
1824
|
+
: [],
|
|
1825
|
+
readTextFile: async (remotePath) => {
|
|
1826
|
+
const body = requestBodies.get(remotePath);
|
|
1827
|
+
if (body === undefined) {
|
|
1828
|
+
throw new Error(`missing request ${remotePath}`);
|
|
1829
|
+
}
|
|
1830
|
+
return body;
|
|
1831
|
+
},
|
|
1832
|
+
writeTextFile: async () => { },
|
|
1833
|
+
writeResponseFile: async (remotePath, body) => {
|
|
1834
|
+
writeAttempts += 1;
|
|
1835
|
+
// The first backstop write fails; the retry then succeeds.
|
|
1836
|
+
if (writeAttempts === 1) {
|
|
1837
|
+
throw new Error("simulated transient write failure");
|
|
1838
|
+
}
|
|
1839
|
+
responseWrites.push({ path: remotePath, status: JSON.parse(body.trim()).status, body });
|
|
1840
|
+
return { wrote: true };
|
|
1841
|
+
},
|
|
1842
|
+
rename: async () => { },
|
|
1843
|
+
remove: async (remotePath) => {
|
|
1844
|
+
requestRemovals.push({ path: remotePath, afterWrites: writeAttempts });
|
|
1845
|
+
requestBodies.delete(remotePath);
|
|
1846
|
+
},
|
|
1847
|
+
};
|
|
1848
|
+
const { runtimeSpan } = createWorkerErrorCapture();
|
|
1849
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
1850
|
+
client,
|
|
1851
|
+
queueDir,
|
|
1852
|
+
iterationTimeoutMs: 50,
|
|
1853
|
+
watchdogTimeoutMs: 10_000,
|
|
1854
|
+
// A short grace, so the backstop fires quickly after the abort.
|
|
1855
|
+
abortedHandlerGraceMs: 30,
|
|
1856
|
+
runtimeSpan,
|
|
1857
|
+
authorizeRequest: async () => null,
|
|
1858
|
+
// The handler ignores the worker signal and never settles, so only the
|
|
1859
|
+
// backstop can finalize the request.
|
|
1860
|
+
handleRequest: () => new Promise(() => { }),
|
|
1861
|
+
});
|
|
1862
|
+
// The backstop retries the write, so the terminal 504 lands after the failure.
|
|
1863
|
+
await waitFor(() => responseWrites.some((write) => write.path === responsePath), 3_000);
|
|
1864
|
+
const requestResponses = responseWrites.filter((write) => write.path === responsePath);
|
|
1865
|
+
expect(requestResponses).toHaveLength(1);
|
|
1866
|
+
expect(requestResponses[0]?.status).toBe(504);
|
|
1867
|
+
// The write failed once, so the backstop wrote on a later attempt.
|
|
1868
|
+
expect(writeAttempts).toBeGreaterThanOrEqual(2);
|
|
1869
|
+
// The recovery path removed the request file only after the write landed. The
|
|
1870
|
+
// failed first attempt kept the file, so the request never dropped with no
|
|
1871
|
+
// response.
|
|
1872
|
+
const requestPathRemovals = requestRemovals.filter((entry) => entry.path === requestPath);
|
|
1873
|
+
expect(requestPathRemovals.length).toBeGreaterThanOrEqual(1);
|
|
1874
|
+
for (const entry of requestPathRemovals) {
|
|
1875
|
+
expect(entry.afterWrites).toBeGreaterThanOrEqual(2);
|
|
1876
|
+
}
|
|
1877
|
+
// The bridge wrote no retryable status for the possibly-committed mutation.
|
|
1878
|
+
expect(responseWrites.some((write) => write.status === 503)).toBe(false);
|
|
1879
|
+
expect(responseWrites.some((write) => write.status === 502)).toBe(false);
|
|
1880
|
+
await worker.stop({ drainTimeoutMs: 10 });
|
|
1881
|
+
});
|
|
1882
|
+
it("retries a handler response write that fails transiently and delivers the real response, not a retryable 503", async () => {
|
|
1883
|
+
// A handler settles with its own response, but the terminal write fails
|
|
1884
|
+
// once. `finalize` must retry the write and deliver the real response. It
|
|
1885
|
+
// must not fence out recovery and leave the request for a retryable 503,
|
|
1886
|
+
// which would let the caller repeat a possibly-committed mutation.
|
|
1887
|
+
const waitFor = async (predicate, timeoutMs) => {
|
|
1888
|
+
const deadline = Date.now() + timeoutMs;
|
|
1889
|
+
while (Date.now() < deadline) {
|
|
1890
|
+
if (predicate()) {
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
1894
|
+
}
|
|
1895
|
+
throw new Error("waitFor timed out");
|
|
1896
|
+
};
|
|
1897
|
+
const queueDir = "/virtual-bridge/queue";
|
|
1898
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
1899
|
+
const requestFile = "req-write-retry.json";
|
|
1900
|
+
const requestPath = path.posix.join(directories.requestsDir, requestFile);
|
|
1901
|
+
const responsePath = path.posix.join(directories.responsesDir, requestFile);
|
|
1902
|
+
const requestBodies = new Map();
|
|
1903
|
+
requestBodies.set(requestPath, bridgeRequestJson("req-write-retry"));
|
|
1904
|
+
const responseWrites = [];
|
|
1905
|
+
const requestRemovals = [];
|
|
1906
|
+
let writeAttempts = 0;
|
|
1907
|
+
const client = {
|
|
1908
|
+
makeDir: async () => { },
|
|
1909
|
+
makeDirs: async () => { },
|
|
1910
|
+
listJsonFiles: async (dir) => dir === directories.requestsDir
|
|
1911
|
+
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
|
|
1912
|
+
: [],
|
|
1913
|
+
readTextFile: async (remotePath) => {
|
|
1914
|
+
const body = requestBodies.get(remotePath);
|
|
1915
|
+
if (body === undefined) {
|
|
1916
|
+
throw new Error(`missing request ${remotePath}`);
|
|
1917
|
+
}
|
|
1918
|
+
return body;
|
|
1919
|
+
},
|
|
1920
|
+
writeTextFile: async () => { },
|
|
1921
|
+
writeResponseFile: async (remotePath, body) => {
|
|
1922
|
+
writeAttempts += 1;
|
|
1923
|
+
// The first handler-response write fails; the retry then succeeds.
|
|
1924
|
+
if (writeAttempts === 1) {
|
|
1925
|
+
throw new Error("simulated transient response write failure");
|
|
1926
|
+
}
|
|
1927
|
+
responseWrites.push({ path: remotePath, status: JSON.parse(body.trim()).status, body });
|
|
1928
|
+
return { wrote: true };
|
|
1929
|
+
},
|
|
1930
|
+
rename: async () => { },
|
|
1931
|
+
remove: async (remotePath) => {
|
|
1932
|
+
requestRemovals.push(remotePath);
|
|
1933
|
+
requestBodies.delete(remotePath);
|
|
1934
|
+
},
|
|
1935
|
+
};
|
|
1936
|
+
const { runtimeSpan } = createWorkerErrorCapture();
|
|
1937
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
1938
|
+
client,
|
|
1939
|
+
queueDir,
|
|
1940
|
+
iterationTimeoutMs: 500,
|
|
1941
|
+
watchdogTimeoutMs: 10_000,
|
|
1942
|
+
runtimeSpan,
|
|
1943
|
+
authorizeRequest: async () => null,
|
|
1944
|
+
// The handler settles with its own 200 response.
|
|
1945
|
+
handleRequest: async () => ({ status: 200, body: JSON.stringify({ ok: true }) }),
|
|
1946
|
+
});
|
|
1947
|
+
await waitFor(() => responseWrites.some((write) => write.path === responsePath), 3_000);
|
|
1948
|
+
// Exactly one response landed: the handler's real 200. The retry delivered it
|
|
1949
|
+
// after the transient failure.
|
|
1950
|
+
const requestResponses = responseWrites.filter((write) => write.path === responsePath);
|
|
1951
|
+
expect(requestResponses).toHaveLength(1);
|
|
1952
|
+
expect(requestResponses[0]?.status).toBe(200);
|
|
1953
|
+
expect(writeAttempts).toBeGreaterThanOrEqual(2);
|
|
1954
|
+
// The bridge wrote no retryable status, so the caller does not repeat the
|
|
1955
|
+
// request.
|
|
1956
|
+
expect(responseWrites.some((write) => write.status === 503)).toBe(false);
|
|
1957
|
+
expect(responseWrites.some((write) => write.status === 502)).toBe(false);
|
|
1958
|
+
// The handler removed the request file after the write landed.
|
|
1959
|
+
expect(requestRemovals).toContain(requestPath);
|
|
1960
|
+
await worker.stop({ drainTimeoutMs: 10 });
|
|
1961
|
+
});
|
|
1962
|
+
it("re-arms the 504 backstop when every handler response write fails, so the caller gets a terminal 504 instead of stranding", async () => {
|
|
1963
|
+
// A handler settles with its own response, but every terminal write fails.
|
|
1964
|
+
// `finalize` must roll back the fence and re-arm the 504 backstop, so the
|
|
1965
|
+
// recovery still delivers a non-retryable terminal response. Without the
|
|
1966
|
+
// re-arm, a rejected write leaves the request for a retryable 503 and a hung
|
|
1967
|
+
// write strands the caller until its own deadline.
|
|
1968
|
+
const waitFor = async (predicate, timeoutMs) => {
|
|
1969
|
+
const deadline = Date.now() + timeoutMs;
|
|
1970
|
+
while (Date.now() < deadline) {
|
|
1971
|
+
if (predicate()) {
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
1975
|
+
}
|
|
1976
|
+
throw new Error("waitFor timed out");
|
|
1977
|
+
};
|
|
1978
|
+
const queueDir = "/virtual-bridge/queue";
|
|
1979
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
1980
|
+
const requestFile = "req-write-backstop.json";
|
|
1981
|
+
const requestPath = path.posix.join(directories.requestsDir, requestFile);
|
|
1982
|
+
const responsePath = path.posix.join(directories.responsesDir, requestFile);
|
|
1983
|
+
const requestBodies = new Map();
|
|
1984
|
+
requestBodies.set(requestPath, bridgeRequestJson("req-write-backstop"));
|
|
1985
|
+
const responseWrites = [];
|
|
1986
|
+
const requestRemovals = [];
|
|
1987
|
+
let writeAttempts = 0;
|
|
1988
|
+
const client = {
|
|
1989
|
+
makeDir: async () => { },
|
|
1990
|
+
makeDirs: async () => { },
|
|
1991
|
+
listJsonFiles: async (dir) => dir === directories.requestsDir
|
|
1992
|
+
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
|
|
1993
|
+
: [],
|
|
1994
|
+
readTextFile: async (remotePath) => {
|
|
1995
|
+
const body = requestBodies.get(remotePath);
|
|
1996
|
+
if (body === undefined) {
|
|
1997
|
+
throw new Error(`missing request ${remotePath}`);
|
|
1998
|
+
}
|
|
1999
|
+
return body;
|
|
2000
|
+
},
|
|
2001
|
+
writeTextFile: async () => { },
|
|
2002
|
+
writeResponseFile: async (remotePath, body) => {
|
|
2003
|
+
writeAttempts += 1;
|
|
2004
|
+
// Fail every `finalize` write attempt. The re-armed 504 backstop then
|
|
2005
|
+
// writes on a later attempt.
|
|
2006
|
+
if (writeAttempts <= 3) {
|
|
2007
|
+
throw new Error("simulated persistent response write failure");
|
|
2008
|
+
}
|
|
2009
|
+
responseWrites.push({ path: remotePath, status: JSON.parse(body.trim()).status, body });
|
|
2010
|
+
return { wrote: true };
|
|
2011
|
+
},
|
|
2012
|
+
rename: async () => { },
|
|
2013
|
+
remove: async (remotePath) => {
|
|
2014
|
+
requestRemovals.push(remotePath);
|
|
2015
|
+
requestBodies.delete(remotePath);
|
|
2016
|
+
},
|
|
2017
|
+
};
|
|
2018
|
+
const { runtimeSpan } = createWorkerErrorCapture();
|
|
2019
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
2020
|
+
client,
|
|
2021
|
+
queueDir,
|
|
2022
|
+
iterationTimeoutMs: 500,
|
|
2023
|
+
watchdogTimeoutMs: 10_000,
|
|
2024
|
+
// A short grace, so the re-armed backstop fires quickly.
|
|
2025
|
+
abortedHandlerGraceMs: 30,
|
|
2026
|
+
runtimeSpan,
|
|
2027
|
+
authorizeRequest: async () => null,
|
|
2028
|
+
// The handler settles with its own 200 response, a committed mutation.
|
|
2029
|
+
handleRequest: async () => ({ status: 200, body: JSON.stringify({ ok: true }) }),
|
|
2030
|
+
});
|
|
2031
|
+
// The re-armed backstop writes the terminal 504 after the finalize writes fail.
|
|
2032
|
+
await waitFor(() => responseWrites.some((write) => write.path === responsePath), 3_000);
|
|
2033
|
+
const requestResponses = responseWrites.filter((write) => write.path === responsePath);
|
|
2034
|
+
expect(requestResponses).toHaveLength(1);
|
|
2035
|
+
// The backstop delivered a non-retryable 504 with an indeterminate outcome.
|
|
2036
|
+
expect(requestResponses[0]?.status).toBe(504);
|
|
2037
|
+
const parsed = JSON.parse(requestResponses[0]?.body ?? "{}");
|
|
2038
|
+
const responseBody = JSON.parse(parsed.body);
|
|
2039
|
+
expect(responseBody.outcome).toBe("indeterminate");
|
|
2040
|
+
expect(responseBody.retryable).toBe(false);
|
|
2041
|
+
expect(parsed.headers?.["x-paperclip-bridge-outcome"]).toBe("indeterminate");
|
|
2042
|
+
// The finalize writes failed before the backstop wrote, so it took a later
|
|
2043
|
+
// attempt.
|
|
2044
|
+
expect(writeAttempts).toBeGreaterThanOrEqual(4);
|
|
2045
|
+
// The bridge wrote no retryable status for the possibly-committed mutation.
|
|
2046
|
+
expect(responseWrites.some((write) => write.status === 503)).toBe(false);
|
|
2047
|
+
expect(responseWrites.some((write) => write.status === 502)).toBe(false);
|
|
2048
|
+
// The backstop removed the request file, so it does not strand in the queue.
|
|
2049
|
+
expect(requestRemovals).toContain(requestPath);
|
|
2050
|
+
await worker.stop({ drainTimeoutMs: 10 });
|
|
2051
|
+
});
|
|
2052
|
+
it("retries a recovery 503 write that fails transiently, delivers the 503, and removes the request", async () => {
|
|
2053
|
+
// A request attempt times out (its first read hangs), so the loop's request
|
|
2054
|
+
// catch runs the recovery pass, which aborts the queued request with a 503.
|
|
2055
|
+
// The first 503 write fails, so the recovery must retry it inside the same
|
|
2056
|
+
// pass. It then delivers the 503 and removes the request. The request is
|
|
2057
|
+
// unclaimed, so its host mutation never ran; the 503 stays retry-safe. (A
|
|
2058
|
+
// hung poll no longer triggers this pass — the loop backs off and retries
|
|
2059
|
+
// the poll instead, and a sustained hang is the watchdog's job.)
|
|
2060
|
+
const waitFor = async (predicate, timeoutMs) => {
|
|
2061
|
+
const deadline = Date.now() + timeoutMs;
|
|
2062
|
+
while (Date.now() < deadline) {
|
|
2063
|
+
if (predicate()) {
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
2067
|
+
}
|
|
2068
|
+
throw new Error("waitFor timed out");
|
|
2069
|
+
};
|
|
2070
|
+
const queueDir = "/virtual-bridge/queue";
|
|
2071
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
2072
|
+
const requestFile = "req-503-retry.json";
|
|
2073
|
+
const requestPath = path.posix.join(directories.requestsDir, requestFile);
|
|
2074
|
+
const responsePath = path.posix.join(directories.responsesDir, requestFile);
|
|
2075
|
+
const requestBodies = new Map();
|
|
2076
|
+
requestBodies.set(requestPath, bridgeRequestJson("req-503-retry"));
|
|
2077
|
+
const responseWrites = [];
|
|
2078
|
+
const requestRemovals = [];
|
|
2079
|
+
let readCalls = 0;
|
|
2080
|
+
let writeAttempts = 0;
|
|
2081
|
+
const client = {
|
|
2082
|
+
makeDir: async () => { },
|
|
2083
|
+
makeDirs: async () => { },
|
|
2084
|
+
listJsonFiles: async (dir) => dir === directories.requestsDir
|
|
2085
|
+
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
|
|
2086
|
+
: [],
|
|
2087
|
+
// The first read never resolves — a silently unresponsive sandbox channel
|
|
2088
|
+
// hit mid-request, before the handler claim. The per-iteration timeout
|
|
2089
|
+
// converts the hang into a caught error, so the loop's request catch runs
|
|
2090
|
+
// the recovery pass. The recovery's own read resolves, so it can build and
|
|
2091
|
+
// deliver the 503.
|
|
2092
|
+
readTextFile: async (remotePath) => {
|
|
2093
|
+
readCalls += 1;
|
|
2094
|
+
if (readCalls === 1) {
|
|
2095
|
+
return await new Promise(() => { });
|
|
2096
|
+
}
|
|
2097
|
+
const body = requestBodies.get(remotePath);
|
|
2098
|
+
if (body === undefined) {
|
|
2099
|
+
throw new Error(`missing request ${remotePath}`);
|
|
2100
|
+
}
|
|
2101
|
+
return body;
|
|
2102
|
+
},
|
|
2103
|
+
writeTextFile: async () => { },
|
|
2104
|
+
writeResponseFile: async (remotePath, body) => {
|
|
2105
|
+
writeAttempts += 1;
|
|
2106
|
+
// The first recovery 503 write fails; the retry then succeeds.
|
|
2107
|
+
if (writeAttempts === 1) {
|
|
2108
|
+
throw new Error("simulated transient recovery 503 write failure");
|
|
2109
|
+
}
|
|
2110
|
+
responseWrites.push({ path: remotePath, status: JSON.parse(body.trim()).status, body });
|
|
2111
|
+
return { wrote: true };
|
|
2112
|
+
},
|
|
2113
|
+
rename: async () => { },
|
|
2114
|
+
remove: async (remotePath) => {
|
|
2115
|
+
requestRemovals.push(remotePath);
|
|
2116
|
+
requestBodies.delete(remotePath);
|
|
2117
|
+
},
|
|
2118
|
+
};
|
|
2119
|
+
const { runtimeSpan } = createWorkerErrorCapture();
|
|
2120
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
2121
|
+
client,
|
|
2122
|
+
queueDir,
|
|
2123
|
+
iterationTimeoutMs: 200,
|
|
2124
|
+
watchdogTimeoutMs: 10_000,
|
|
2125
|
+
runtimeSpan,
|
|
2126
|
+
authorizeRequest: async () => null,
|
|
2127
|
+
handleRequest: async () => ({ status: 200, body: "ok" }),
|
|
2128
|
+
});
|
|
2129
|
+
await waitFor(() => responseWrites.some((write) => write.path === responsePath), 3_000);
|
|
2130
|
+
// Exactly one response landed: the retry-safe 503. The retry delivered it
|
|
2131
|
+
// after the transient failure.
|
|
2132
|
+
const requestResponses = responseWrites.filter((write) => write.path === responsePath);
|
|
2133
|
+
expect(requestResponses).toHaveLength(1);
|
|
2134
|
+
expect(requestResponses[0]?.status).toBe(503);
|
|
2135
|
+
expect(writeAttempts).toBeGreaterThanOrEqual(2);
|
|
2136
|
+
// The recovery removed the request file only after the 503 write landed.
|
|
2137
|
+
expect(requestRemovals).toContain(requestPath);
|
|
2138
|
+
await worker.stop({ drainTimeoutMs: 10 });
|
|
2139
|
+
});
|
|
2140
|
+
it("keeps the queued request when every recovery 503 write fails, so a later pass can still deliver a terminal 503", async () => {
|
|
2141
|
+
// The poll times out, so the recovery path aborts the queued request with a
|
|
2142
|
+
// 503. Every 503 write fails. The recovery must keep the request file instead
|
|
2143
|
+
// of dropping it. Without the fix, the recovery removed the request before its
|
|
2144
|
+
// single write attempt, so a failed write left no queue state; a later
|
|
2145
|
+
// recovery pass found nothing and the caller waited until its own deadline.
|
|
2146
|
+
const waitFor = async (predicate, timeoutMs) => {
|
|
2147
|
+
const deadline = Date.now() + timeoutMs;
|
|
2148
|
+
while (Date.now() < deadline) {
|
|
2149
|
+
if (predicate()) {
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
2153
|
+
}
|
|
2154
|
+
throw new Error("waitFor timed out");
|
|
2155
|
+
};
|
|
2156
|
+
const queueDir = "/virtual-bridge/queue";
|
|
2157
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
2158
|
+
const requestFile = "req-503-keep.json";
|
|
2159
|
+
const requestPath = path.posix.join(directories.requestsDir, requestFile);
|
|
2160
|
+
const responsePath = path.posix.join(directories.responsesDir, requestFile);
|
|
2161
|
+
const requestBodies = new Map();
|
|
2162
|
+
requestBodies.set(requestPath, bridgeRequestJson("req-503-keep"));
|
|
2163
|
+
const responseWrites = [];
|
|
2164
|
+
const requestRemovals = [];
|
|
2165
|
+
let listCalls = 0;
|
|
2166
|
+
let writeAttempts = 0;
|
|
2167
|
+
const client = {
|
|
2168
|
+
makeDir: async () => { },
|
|
2169
|
+
makeDirs: async () => { },
|
|
2170
|
+
listJsonFiles: async (dir) => {
|
|
2171
|
+
if (dir !== directories.requestsDir) {
|
|
2172
|
+
return [];
|
|
2173
|
+
}
|
|
2174
|
+
listCalls += 1;
|
|
2175
|
+
if (listCalls === 1) {
|
|
2176
|
+
return await new Promise(() => { });
|
|
2177
|
+
}
|
|
2178
|
+
return [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort();
|
|
2179
|
+
},
|
|
2180
|
+
readTextFile: async (remotePath) => {
|
|
2181
|
+
const body = requestBodies.get(remotePath);
|
|
2182
|
+
if (body === undefined) {
|
|
2183
|
+
throw new Error(`missing request ${remotePath}`);
|
|
2184
|
+
}
|
|
2185
|
+
return body;
|
|
2186
|
+
},
|
|
2187
|
+
writeTextFile: async () => { },
|
|
2188
|
+
writeResponseFile: async () => {
|
|
2189
|
+
writeAttempts += 1;
|
|
2190
|
+
// Fail every recovery 503 write attempt.
|
|
2191
|
+
throw new Error("simulated persistent recovery 503 write failure");
|
|
2192
|
+
},
|
|
2193
|
+
rename: async () => { },
|
|
2194
|
+
remove: async (remotePath) => {
|
|
2195
|
+
requestRemovals.push(remotePath);
|
|
2196
|
+
requestBodies.delete(remotePath);
|
|
2197
|
+
},
|
|
2198
|
+
};
|
|
2199
|
+
const { runtimeSpan } = createWorkerErrorCapture();
|
|
2200
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
2201
|
+
client,
|
|
2202
|
+
queueDir,
|
|
2203
|
+
iterationTimeoutMs: 200,
|
|
2204
|
+
watchdogTimeoutMs: 10_000,
|
|
2205
|
+
runtimeSpan,
|
|
2206
|
+
authorizeRequest: async () => null,
|
|
2207
|
+
handleRequest: async () => ({ status: 200, body: "ok" }),
|
|
2208
|
+
});
|
|
2209
|
+
// Wait until the recovery pass has exhausted its bounded 503 write attempts
|
|
2210
|
+
// (three, the same bound as the finalize and 504 backstop writes).
|
|
2211
|
+
await waitFor(() => writeAttempts >= 3, 3_000);
|
|
2212
|
+
// The recovery could not write the 503, so it kept the request file. The
|
|
2213
|
+
// request still sits in the queue for a later recovery pass to deliver a
|
|
2214
|
+
// terminal 503. A drop here would strand the caller until its own deadline.
|
|
2215
|
+
expect(requestRemovals).not.toContain(requestPath);
|
|
2216
|
+
expect(requestBodies.has(requestPath)).toBe(true);
|
|
2217
|
+
// No response landed, because every write failed. The recovery wrote no
|
|
2218
|
+
// partial or retryable status for the possibly-committed mutation.
|
|
2219
|
+
expect(responseWrites).toHaveLength(0);
|
|
2220
|
+
await worker.stop({ drainTimeoutMs: 10 });
|
|
2221
|
+
});
|
|
2222
|
+
it("maps an indeterminate host outcome to a non-retryable 409 in the in-sandbox bridge server", () => {
|
|
2223
|
+
// The in-sandbox server returns the host response to the sandbox caller. A 5xx
|
|
2224
|
+
// status is retryable by convention, so the server must not forward the
|
|
2225
|
+
// indeterminate 504 as a retry-safe status. It maps the indeterminate outcome
|
|
2226
|
+
// to a non-retryable 409, so a caller that retries 5xx does not repeat a
|
|
2227
|
+
// possibly-committed mutation. The outcome header and body still forward, so a
|
|
2228
|
+
// caller that reads them still sees the indeterminate result.
|
|
2229
|
+
const source = getSandboxCallbackBridgeServerSource();
|
|
2230
|
+
expect(source).toContain("x-paperclip-bridge-outcome");
|
|
2231
|
+
expect(source).toContain('=== "indeterminate"');
|
|
2232
|
+
expect(source).toContain("res.statusCode = 409");
|
|
2233
|
+
});
|
|
2234
|
+
it("abandons a request before its handler starts, so the mutation never runs", async () => {
|
|
2235
|
+
// Prove the reverse race. When the recovery path claims a request before the
|
|
2236
|
+
// handler starts its host operation, the handler must bail without running
|
|
2237
|
+
// the mutation. A retry after the 503 then applies the mutation once.
|
|
2238
|
+
const waitFor = async (predicate, timeoutMs) => {
|
|
2239
|
+
const deadline = Date.now() + timeoutMs;
|
|
2240
|
+
while (Date.now() < deadline) {
|
|
2241
|
+
if (predicate()) {
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
2245
|
+
}
|
|
2246
|
+
throw new Error("waitFor timed out");
|
|
2247
|
+
};
|
|
2248
|
+
const queueDir = "/virtual-bridge/queue";
|
|
2249
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
2250
|
+
const requestFile = "req-early.json";
|
|
2251
|
+
const requestPath = path.posix.join(directories.requestsDir, requestFile);
|
|
2252
|
+
const responsePath = path.posix.join(directories.responsesDir, requestFile);
|
|
2253
|
+
const requestBodies = new Map();
|
|
2254
|
+
requestBodies.set(requestPath, bridgeRequestJson("req-early"));
|
|
2255
|
+
const responseWrites = [];
|
|
2256
|
+
const client = {
|
|
2257
|
+
makeDir: async () => { },
|
|
2258
|
+
makeDirs: async () => { },
|
|
2259
|
+
listJsonFiles: async (dir) => dir === directories.requestsDir
|
|
2260
|
+
? [...requestBodies.keys()].map((entry) => path.posix.basename(entry)).sort()
|
|
2261
|
+
: [],
|
|
2262
|
+
readTextFile: async (remotePath) => {
|
|
2263
|
+
const body = requestBodies.get(remotePath);
|
|
2264
|
+
if (body === undefined) {
|
|
2265
|
+
throw new Error(`missing request ${remotePath}`);
|
|
2266
|
+
}
|
|
2267
|
+
return body;
|
|
2268
|
+
},
|
|
2269
|
+
writeTextFile: async () => { },
|
|
2270
|
+
writeResponseFile: async (remotePath, body) => {
|
|
2271
|
+
responseWrites.push({ path: remotePath, status: JSON.parse(body.trim()).status });
|
|
2272
|
+
return { wrote: true };
|
|
2273
|
+
},
|
|
2274
|
+
rename: async () => { },
|
|
2275
|
+
remove: async (remotePath) => {
|
|
2276
|
+
requestBodies.delete(remotePath);
|
|
2277
|
+
},
|
|
2278
|
+
};
|
|
2279
|
+
const { runtimeSpan, workerErrors } = createWorkerErrorCapture();
|
|
2280
|
+
// The authorize step stays pending until the test releases it, so the handler
|
|
2281
|
+
// does not start before the per-iteration timeout fires and the recovery path
|
|
2282
|
+
// claims the request.
|
|
2283
|
+
const authorizeControl = { release: null };
|
|
2284
|
+
let handlerCalls = 0;
|
|
2285
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
2286
|
+
client,
|
|
2287
|
+
queueDir,
|
|
2288
|
+
iterationTimeoutMs: 50,
|
|
2289
|
+
watchdogTimeoutMs: 10_000,
|
|
2290
|
+
runtimeSpan,
|
|
2291
|
+
authorizeRequest: () => new Promise((resolve) => {
|
|
2292
|
+
authorizeControl.release = () => resolve(null);
|
|
2293
|
+
}),
|
|
2294
|
+
handleRequest: async () => {
|
|
2295
|
+
handlerCalls += 1;
|
|
2296
|
+
return { status: 200, body: "req-early" };
|
|
2297
|
+
},
|
|
2298
|
+
});
|
|
2299
|
+
// Wait until the recovery path wrote the 503 and the authorize step is
|
|
2300
|
+
// pending.
|
|
2301
|
+
await waitFor(() => responseWrites.some((write) => write.status === 503) && authorizeControl.release !== null, 3_000);
|
|
2302
|
+
expect(responseWrites.some((write) => write.path === responsePath && write.status === 503)).toBe(true);
|
|
2303
|
+
// Release authorize after the 503. The handler must bail at its claim.
|
|
2304
|
+
authorizeControl.release?.();
|
|
2305
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2306
|
+
// The handler never ran, so the mutation never applied.
|
|
2307
|
+
expect(handlerCalls).toBe(0);
|
|
2308
|
+
// No competing 200 landed over the 503.
|
|
2309
|
+
expect(responseWrites.some((write) => write.path === responsePath && write.status === 200)).toBe(false);
|
|
2310
|
+
expect(workerErrors.some((message) => message.includes("timed out"))).toBe(true);
|
|
2311
|
+
await worker.stop({ drainTimeoutMs: 10 });
|
|
2312
|
+
});
|
|
2313
|
+
it("trips the watchdog on a stalled poll, writes a 503, and surfaces a run-level error", async () => {
|
|
2314
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-watchdog-"));
|
|
2315
|
+
cleanupDirs.push(rootDir);
|
|
2316
|
+
const queueDir = path.posix.join(rootDir, "queue");
|
|
2317
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
2318
|
+
await mkdir(directories.requestsDir, { recursive: true });
|
|
2319
|
+
await writeFile(path.posix.join(directories.requestsDir, "req-w.json"), bridgeRequestJson("req-w"), "utf8");
|
|
2320
|
+
const base = createFileSystemSandboxCallbackBridgeQueueClient();
|
|
2321
|
+
let listCalls = 0;
|
|
2322
|
+
const client = {
|
|
2323
|
+
...base,
|
|
2324
|
+
listJsonFiles: async (dir) => {
|
|
2325
|
+
listCalls += 1;
|
|
2326
|
+
// The first poll (the loop) never resolves — a stalled channel that the
|
|
2327
|
+
// per-iteration timeout does not catch soon enough. Later calls (the
|
|
2328
|
+
// watchdog's failPendingRequests) resolve, so it enumerates and 503s.
|
|
2329
|
+
if (listCalls === 1) {
|
|
2330
|
+
return await new Promise(() => { });
|
|
2331
|
+
}
|
|
2332
|
+
return await base.listJsonFiles(dir);
|
|
2333
|
+
},
|
|
2334
|
+
};
|
|
2335
|
+
const { runtimeSpan, workerErrors } = createWorkerErrorCapture();
|
|
2336
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
2337
|
+
client,
|
|
2338
|
+
queueDir,
|
|
2339
|
+
// The per-iteration timeout is far larger than the watchdog threshold, so
|
|
2340
|
+
// the watchdog — not the per-iteration timeout — is the mechanism proven.
|
|
2341
|
+
iterationTimeoutMs: 400,
|
|
2342
|
+
watchdogTimeoutMs: 50,
|
|
2343
|
+
runtimeSpan,
|
|
2344
|
+
authorizeRequest: async () => null,
|
|
2345
|
+
handleRequest: async () => ({ status: 200, body: "ok" }),
|
|
2346
|
+
});
|
|
2347
|
+
const responseFile = await waitForJsonFile(directories.responsesDir, 3_000);
|
|
2348
|
+
const responseBody = await readFile(path.posix.join(directories.responsesDir, responseFile), "utf8");
|
|
2349
|
+
expect(JSON.parse(responseBody).status).toBe(503);
|
|
2350
|
+
expect(workerErrors.some((message) => message.includes("no successful poll iteration"))).toBe(true);
|
|
2351
|
+
await worker.stop({ drainTimeoutMs: 400 });
|
|
2352
|
+
});
|
|
2353
|
+
it("processes a fast request with no false-positive timeout and no run-level error", async () => {
|
|
2354
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-fast-"));
|
|
2355
|
+
cleanupDirs.push(rootDir);
|
|
2356
|
+
const queueDir = path.posix.join(rootDir, "queue");
|
|
2357
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
2358
|
+
await mkdir(directories.requestsDir, { recursive: true });
|
|
2359
|
+
await writeFile(path.posix.join(directories.requestsDir, "req-ok.json"), bridgeRequestJson("req-ok"), "utf8");
|
|
2360
|
+
const { runtimeSpan, workerErrors } = createWorkerErrorCapture();
|
|
2361
|
+
const processed = [];
|
|
2362
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
2363
|
+
client: createFileSystemSandboxCallbackBridgeQueueClient(),
|
|
2364
|
+
queueDir,
|
|
2365
|
+
iterationTimeoutMs: 200,
|
|
2366
|
+
watchdogTimeoutMs: 1_000,
|
|
2367
|
+
runtimeSpan,
|
|
2368
|
+
authorizeRequest: async () => null,
|
|
2369
|
+
handleRequest: async (request) => {
|
|
2370
|
+
processed.push(request.id);
|
|
2371
|
+
return { status: 200, body: request.id };
|
|
2372
|
+
},
|
|
2373
|
+
});
|
|
2374
|
+
const responseFile = await waitForJsonFile(directories.responsesDir, 3_000);
|
|
2375
|
+
const responseBody = await readFile(path.posix.join(directories.responsesDir, responseFile), "utf8");
|
|
2376
|
+
expect(JSON.parse(responseBody).status).toBe(200);
|
|
2377
|
+
expect(JSON.parse(responseBody).body).toBe("req-ok");
|
|
2378
|
+
// Let several idle poll iterations and watchdog checks pass. A healthy idle
|
|
2379
|
+
// loop must never trip the watchdog and never surface a run-level error.
|
|
2380
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
2381
|
+
expect(processed).toEqual(["req-ok"]);
|
|
2382
|
+
expect(workerErrors).toEqual([]);
|
|
2383
|
+
await worker.stop({ drainTimeoutMs: 50 });
|
|
2384
|
+
});
|
|
2385
|
+
async function prepareGatewayFixture(prefix) {
|
|
2386
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), prefix));
|
|
2387
|
+
cleanupDirs.push(rootDir);
|
|
2388
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
2389
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
2390
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
2391
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
2392
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "bridge test\n", "utf8");
|
|
2393
|
+
const runner = createExecRunner();
|
|
2394
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
2395
|
+
cleanupFns.push(bridgeAsset.cleanup);
|
|
2396
|
+
const prepared = await prepareCommandManagedRuntime({
|
|
2397
|
+
runner,
|
|
2398
|
+
spec: { remoteCwd: remoteWorkspaceDir, timeoutMs: 30_000 },
|
|
2399
|
+
adapterKey: "codex",
|
|
2400
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
2401
|
+
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
|
|
2402
|
+
});
|
|
2403
|
+
const queueDir = path.posix.join(prepared.runtimeRootDir, "paperclip-bridge");
|
|
2404
|
+
return {
|
|
2405
|
+
runner,
|
|
2406
|
+
remoteWorkspaceDir,
|
|
2407
|
+
assetRemoteDir: prepared.assetDirs.bridge,
|
|
2408
|
+
queueDir,
|
|
2409
|
+
directories: sandboxCallbackBridgeDirectories(queueDir),
|
|
2410
|
+
bridgeToken: createSandboxCallbackBridgeToken(),
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
it("cleans up a timed-out request file and keeps serving after the host recovers", async () => {
|
|
2414
|
+
const fixture = await prepareGatewayFixture("paperclip-bridge-timeout-clean-");
|
|
2415
|
+
const bridge = await startSandboxCallbackBridgeServer({
|
|
2416
|
+
runner: fixture.runner,
|
|
2417
|
+
remoteCwd: fixture.remoteWorkspaceDir,
|
|
2418
|
+
assetRemoteDir: fixture.assetRemoteDir,
|
|
2419
|
+
queueDir: fixture.queueDir,
|
|
2420
|
+
bridgeToken: fixture.bridgeToken,
|
|
2421
|
+
timeoutMs: 30_000,
|
|
2422
|
+
responseTimeoutMs: 600,
|
|
2423
|
+
pollIntervalMs: 50,
|
|
2424
|
+
});
|
|
2425
|
+
cleanupFns.push(async () => {
|
|
2426
|
+
await bridge.stop();
|
|
2427
|
+
});
|
|
2428
|
+
// No worker runs, so the request times out at the gateway.
|
|
2429
|
+
const timedOut = await fetch(`${bridge.baseUrl}/api/agents/me`, {
|
|
2430
|
+
headers: { authorization: `Bearer ${fixture.bridgeToken}` },
|
|
2431
|
+
});
|
|
2432
|
+
expect(timedOut.status).toBe(502);
|
|
2433
|
+
await expect(timedOut.json()).resolves.toMatchObject({
|
|
2434
|
+
error: expect.stringContaining("Timed out"),
|
|
2435
|
+
});
|
|
2436
|
+
// The gateway cleaned its own request file, so nothing counts toward the
|
|
2437
|
+
// queue-depth cap after the caller gave up.
|
|
2438
|
+
const leftover = (await readdir(fixture.directories.requestsDir).catch(() => [])).filter((name) => name.endsWith(".json"));
|
|
2439
|
+
expect(leftover).toEqual([]);
|
|
2440
|
+
// A worker that comes up afterwards serves the next request normally —
|
|
2441
|
+
// the timeout neither wedged nor killed the gateway.
|
|
2442
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
2443
|
+
client: createFileSystemSandboxCallbackBridgeQueueClient(),
|
|
2444
|
+
queueDir: fixture.queueDir,
|
|
2445
|
+
authorizeRequest: async () => null,
|
|
2446
|
+
handleRequest: async () => ({
|
|
2447
|
+
status: 200,
|
|
2448
|
+
headers: { "content-type": "application/json" },
|
|
2449
|
+
body: JSON.stringify({ ok: true }),
|
|
2450
|
+
}),
|
|
2451
|
+
});
|
|
2452
|
+
cleanupFns.push(async () => {
|
|
2453
|
+
await worker.stop();
|
|
2454
|
+
});
|
|
2455
|
+
const recovered = await fetch(`${bridge.baseUrl}/api/agents/me`, {
|
|
2456
|
+
headers: { authorization: `Bearer ${fixture.bridgeToken}` },
|
|
2457
|
+
});
|
|
2458
|
+
expect(recovered.status).toBe(200);
|
|
2459
|
+
await expect(recovered.json()).resolves.toMatchObject({ ok: true });
|
|
2460
|
+
}, 30_000);
|
|
2461
|
+
it("sweeps stale request files before rejecting at the queue-depth cap", async () => {
|
|
2462
|
+
const fixture = await prepareGatewayFixture("paperclip-bridge-stale-sweep-");
|
|
2463
|
+
const bridge = await startSandboxCallbackBridgeServer({
|
|
2464
|
+
runner: fixture.runner,
|
|
2465
|
+
remoteCwd: fixture.remoteWorkspaceDir,
|
|
2466
|
+
assetRemoteDir: fixture.assetRemoteDir,
|
|
2467
|
+
queueDir: fixture.queueDir,
|
|
2468
|
+
bridgeToken: fixture.bridgeToken,
|
|
2469
|
+
timeoutMs: 30_000,
|
|
2470
|
+
responseTimeoutMs: 500,
|
|
2471
|
+
pollIntervalMs: 50,
|
|
2472
|
+
maxQueueDepth: 1,
|
|
2473
|
+
});
|
|
2474
|
+
cleanupFns.push(async () => {
|
|
2475
|
+
await bridge.stop();
|
|
2476
|
+
});
|
|
2477
|
+
// Plant an orphaned request file (a killed caller, or a previous gateway
|
|
2478
|
+
// process's leftover) and backdate it beyond the response deadline.
|
|
2479
|
+
const orphanPath = path.join(fixture.directories.requestsDir, "orphan.json");
|
|
2480
|
+
await writeFile(orphanPath, bridgeRequestJson("orphan"), "utf8");
|
|
2481
|
+
const staleTime = new Date(Date.now() - 60_000);
|
|
2482
|
+
await utimes(orphanPath, staleTime, staleTime);
|
|
2483
|
+
// The queue sits at the cap, but the only entry is stale: the gateway must
|
|
2484
|
+
// sweep it and admit the request instead of answering 503. With no worker
|
|
2485
|
+
// the admitted request then times out (502) — proof it entered the queue.
|
|
2486
|
+
const response = await fetch(`${bridge.baseUrl}/api/agents/me`, {
|
|
2487
|
+
headers: { authorization: `Bearer ${fixture.bridgeToken}` },
|
|
2488
|
+
});
|
|
2489
|
+
expect(response.status).toBe(502);
|
|
2490
|
+
const body = (await response.json());
|
|
2491
|
+
expect(body.error ?? "").not.toContain("queue is full");
|
|
2492
|
+
const leftover = (await readdir(fixture.directories.requestsDir).catch(() => [])).filter((name) => name.endsWith(".json"));
|
|
2493
|
+
expect(leftover).toEqual([]);
|
|
2494
|
+
}, 30_000);
|
|
2495
|
+
it("skips a request file that vanished before the read instead of escalating", async () => {
|
|
2496
|
+
// The gateway deletes a request file when its caller stops waiting. The
|
|
2497
|
+
// worker's read then races the deletion; a vanished file must be a quiet
|
|
2498
|
+
// skip, not a worker failure with a recovery pass.
|
|
2499
|
+
const queueDir = "/virtual-bridge/vanished";
|
|
2500
|
+
let listCalls = 0;
|
|
2501
|
+
const handled = [];
|
|
2502
|
+
const responseStatuses = [];
|
|
2503
|
+
const { runtimeSpan, workerErrors } = createWorkerErrorCapture();
|
|
2504
|
+
const client = {
|
|
2505
|
+
makeDir: async () => { },
|
|
2506
|
+
makeDirs: async () => { },
|
|
2507
|
+
listJsonFiles: async () => {
|
|
2508
|
+
listCalls += 1;
|
|
2509
|
+
return listCalls === 1 ? ["ghost.json"] : [];
|
|
2510
|
+
},
|
|
2511
|
+
readTextFile: async () => {
|
|
2512
|
+
throw new Error("cat: ghost.json: No such file or directory");
|
|
2513
|
+
},
|
|
2514
|
+
writeTextFile: async () => { },
|
|
2515
|
+
writeResponseFile: async (_remotePath, body) => {
|
|
2516
|
+
responseStatuses.push(JSON.parse(body.trim()).status);
|
|
2517
|
+
return { wrote: true };
|
|
2518
|
+
},
|
|
2519
|
+
rename: async () => { },
|
|
2520
|
+
remove: async () => { },
|
|
2521
|
+
};
|
|
2522
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
2523
|
+
client,
|
|
2524
|
+
queueDir,
|
|
2525
|
+
runtimeSpan,
|
|
2526
|
+
authorizeRequest: async () => null,
|
|
2527
|
+
handleRequest: async (request) => {
|
|
2528
|
+
handled.push(request.id);
|
|
2529
|
+
return { status: 200, body: "ok" };
|
|
2530
|
+
},
|
|
2531
|
+
});
|
|
2532
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
2533
|
+
await worker.stop({ drainTimeoutMs: 50 });
|
|
2534
|
+
expect(handled).toEqual([]);
|
|
2535
|
+
expect(responseStatuses).toEqual([]);
|
|
2536
|
+
expect(workerErrors).toEqual([]);
|
|
2537
|
+
});
|
|
2538
|
+
it("embeds crash handlers and queue hygiene in the generated gateway source", () => {
|
|
2539
|
+
// A crashed gateway is a dead loopback port for the rest of the run, so
|
|
2540
|
+
// the generated source must keep its crash handlers and its stale-queue
|
|
2541
|
+
// sweep. The readiness gate matters too: survival applies only after the
|
|
2542
|
+
// gateway is adoptable, so a startup fault still fails fast. This pins
|
|
2543
|
+
// their presence; the behavior is proven above and below.
|
|
2544
|
+
const source = getSandboxCallbackBridgeServerSource();
|
|
2545
|
+
expect(source).toContain('process.on("uncaughtException"');
|
|
2546
|
+
expect(source).toContain('process.on("unhandledRejection"');
|
|
2547
|
+
expect(source).toContain("gatewayReady");
|
|
2548
|
+
expect(source).toContain("sweepStaleRequests");
|
|
2549
|
+
});
|
|
2550
|
+
it("exits fast when the gateway cannot bind its port instead of lingering un-ready", async () => {
|
|
2551
|
+
// Before readiness, the crash handlers must not keep the process alive: a
|
|
2552
|
+
// failed bind means the gateway can never serve, and surviving would only
|
|
2553
|
+
// leave an un-ready zombie while the host waits out its readiness poll.
|
|
2554
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-bind-fail-"));
|
|
2555
|
+
cleanupDirs.push(rootDir);
|
|
2556
|
+
const entrypoint = path.join(rootDir, "paperclip-bridge-server.mjs");
|
|
2557
|
+
await writeFile(entrypoint, getSandboxCallbackBridgeServerSource(), "utf8");
|
|
2558
|
+
const queueDir = path.join(rootDir, "queue");
|
|
2559
|
+
await mkdir(queueDir, { recursive: true });
|
|
2560
|
+
const blocker = createServer();
|
|
2561
|
+
await new Promise((resolve) => {
|
|
2562
|
+
blocker.listen(0, "127.0.0.1", resolve);
|
|
2563
|
+
});
|
|
2564
|
+
cleanupFns.push(() => new Promise((resolve) => {
|
|
2565
|
+
blocker.close(() => resolve());
|
|
2566
|
+
}));
|
|
2567
|
+
const blockedPort = blocker.address().port;
|
|
2568
|
+
const child = spawn(process.execPath, [entrypoint], {
|
|
2569
|
+
env: {
|
|
2570
|
+
...process.env,
|
|
2571
|
+
PAPERCLIP_BRIDGE_QUEUE_DIR: queueDir,
|
|
2572
|
+
PAPERCLIP_BRIDGE_TOKEN: "test-token",
|
|
2573
|
+
PAPERCLIP_BRIDGE_PORT: String(blockedPort),
|
|
2574
|
+
},
|
|
2575
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
2576
|
+
});
|
|
2577
|
+
let stderr = "";
|
|
2578
|
+
child.stderr.on("data", (chunk) => {
|
|
2579
|
+
stderr += chunk;
|
|
2580
|
+
});
|
|
2581
|
+
const exitCode = await new Promise((resolve) => {
|
|
2582
|
+
child.on("close", resolve);
|
|
2583
|
+
});
|
|
2584
|
+
expect(exitCode).toBe(1);
|
|
2585
|
+
expect(stderr).toContain("[paperclip-bridge] server error");
|
|
2586
|
+
expect(stderr).toContain("EADDRINUSE");
|
|
2587
|
+
}, 15_000);
|
|
2588
|
+
it("exits nonzero for the retired duplex_v1 mode instead of starting the queue gateway", async () => {
|
|
2589
|
+
// The closed mode allowlist rejects `duplex_v1` before the queue-directory
|
|
2590
|
+
// check, so a stale `duplex_v1` launch environment fails startup instead
|
|
2591
|
+
// of silently falling through to the queue gateway.
|
|
2592
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-mode-duplex-"));
|
|
2593
|
+
cleanupDirs.push(rootDir);
|
|
2594
|
+
const entrypoint = path.join(rootDir, "paperclip-bridge-server.mjs");
|
|
2595
|
+
await writeFile(entrypoint, getSandboxCallbackBridgeServerSource(), "utf8");
|
|
2596
|
+
const queueDir = path.join(rootDir, "queue");
|
|
2597
|
+
await mkdir(queueDir, { recursive: true });
|
|
2598
|
+
const child = spawn(process.execPath, [entrypoint], {
|
|
2599
|
+
env: {
|
|
2600
|
+
...process.env,
|
|
2601
|
+
PAPERCLIP_API_BRIDGE_MODE: "duplex_v1",
|
|
2602
|
+
PAPERCLIP_BRIDGE_QUEUE_DIR: queueDir,
|
|
2603
|
+
PAPERCLIP_BRIDGE_TOKEN: "test-token",
|
|
2604
|
+
PAPERCLIP_BRIDGE_PORT: "0",
|
|
2605
|
+
},
|
|
2606
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
2607
|
+
});
|
|
2608
|
+
let stderr = "";
|
|
2609
|
+
child.stderr.on("data", (chunk) => {
|
|
2610
|
+
stderr += chunk;
|
|
2611
|
+
});
|
|
2612
|
+
const exitCode = await new Promise((resolve) => {
|
|
2613
|
+
child.on("close", resolve);
|
|
2614
|
+
});
|
|
2615
|
+
expect(exitCode).not.toBe(0);
|
|
2616
|
+
expect(stderr).toContain("Unsupported PAPERCLIP_API_BRIDGE_MODE: duplex_v1");
|
|
2617
|
+
}, 15_000);
|
|
2618
|
+
it("exits nonzero for an unknown bridge mode instead of starting the queue gateway", async () => {
|
|
2619
|
+
// The closed mode allowlist rejects every value it does not name, not
|
|
2620
|
+
// only the retired duplex transport.
|
|
2621
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-mode-unknown-"));
|
|
2622
|
+
cleanupDirs.push(rootDir);
|
|
2623
|
+
const entrypoint = path.join(rootDir, "paperclip-bridge-server.mjs");
|
|
2624
|
+
await writeFile(entrypoint, getSandboxCallbackBridgeServerSource(), "utf8");
|
|
2625
|
+
const queueDir = path.join(rootDir, "queue");
|
|
2626
|
+
await mkdir(queueDir, { recursive: true });
|
|
2627
|
+
const child = spawn(process.execPath, [entrypoint], {
|
|
2628
|
+
env: {
|
|
2629
|
+
...process.env,
|
|
2630
|
+
PAPERCLIP_API_BRIDGE_MODE: "totally_unknown_mode",
|
|
2631
|
+
PAPERCLIP_BRIDGE_QUEUE_DIR: queueDir,
|
|
2632
|
+
PAPERCLIP_BRIDGE_TOKEN: "test-token",
|
|
2633
|
+
PAPERCLIP_BRIDGE_PORT: "0",
|
|
2634
|
+
},
|
|
2635
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
2636
|
+
});
|
|
2637
|
+
let stderr = "";
|
|
2638
|
+
child.stderr.on("data", (chunk) => {
|
|
2639
|
+
stderr += chunk;
|
|
2640
|
+
});
|
|
2641
|
+
const exitCode = await new Promise((resolve) => {
|
|
2642
|
+
child.on("close", resolve);
|
|
2643
|
+
});
|
|
2644
|
+
expect(exitCode).not.toBe(0);
|
|
2645
|
+
expect(stderr).toContain("Unsupported PAPERCLIP_API_BRIDGE_MODE: totally_unknown_mode");
|
|
2646
|
+
}, 15_000);
|
|
2647
|
+
it("test_http2_gateway_writes_no_frame_between_ready_and_the_preface", async () => {
|
|
2648
|
+
// Spawn the real generated gateway in http2_v1 mode and read its raw
|
|
2649
|
+
// stdout bytes. The only frame-codec write on this path is the READY
|
|
2650
|
+
// line; the very next bytes must be the HTTP/2 client connection preface
|
|
2651
|
+
// with nothing in between, because the gateway hands stdout to the
|
|
2652
|
+
// HTTP/2 client immediately after it writes READY and starts no
|
|
2653
|
+
// heartbeat timer and writes no envelope frame on this path.
|
|
2654
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-http2-gateway-"));
|
|
2655
|
+
cleanupDirs.push(rootDir);
|
|
2656
|
+
const entrypoint = path.join(rootDir, "paperclip-bridge-server.mjs");
|
|
2657
|
+
await writeFile(entrypoint, getSandboxCallbackBridgeServerSource(), "utf8");
|
|
2658
|
+
const probe = createServer();
|
|
2659
|
+
const assignedPort = await new Promise((resolve, reject) => {
|
|
2660
|
+
probe.once("error", reject);
|
|
2661
|
+
probe.listen(0, "127.0.0.1", () => {
|
|
2662
|
+
const address = probe.address();
|
|
2663
|
+
if (!address || typeof address === "string") {
|
|
2664
|
+
reject(new Error("Could not reserve a loopback port for the test."));
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
probe.close(() => resolve(address.port));
|
|
2668
|
+
});
|
|
2669
|
+
});
|
|
2670
|
+
const nonce = "test-nonce-http2";
|
|
2671
|
+
const child = spawn(process.execPath, [entrypoint], {
|
|
2672
|
+
env: {
|
|
2673
|
+
...process.env,
|
|
2674
|
+
PAPERCLIP_API_BRIDGE_MODE: "http2_v1",
|
|
2675
|
+
PAPERCLIP_BRIDGE_TOKEN: "test-token",
|
|
2676
|
+
PAPERCLIP_BRIDGE_PORT: String(assignedPort),
|
|
2677
|
+
PAPERCLIP_BRIDGE_NONCE: nonce,
|
|
2678
|
+
},
|
|
2679
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2680
|
+
});
|
|
2681
|
+
cleanupFns.push(async () => {
|
|
2682
|
+
child.kill();
|
|
2683
|
+
});
|
|
2684
|
+
let stderr = "";
|
|
2685
|
+
child.stderr.on("data", (chunk) => {
|
|
2686
|
+
stderr += chunk.toString("utf8");
|
|
2687
|
+
});
|
|
2688
|
+
const chunks = [];
|
|
2689
|
+
const preface = Buffer.from("505249202a20485454502f322e300d0a0d0a534d0d0a0d0a", "hex");
|
|
2690
|
+
const firstBytes = await new Promise((resolve, reject) => {
|
|
2691
|
+
const timer = setTimeout(() => reject(new Error("Timed out waiting for the http2 gateway stdout. stderr: " + stderr)), 5000);
|
|
2692
|
+
child.stdout.on("data", (chunk) => {
|
|
2693
|
+
chunks.push(chunk);
|
|
2694
|
+
const total = Buffer.concat(chunks);
|
|
2695
|
+
const newlineIndex = total.indexOf(0x0a);
|
|
2696
|
+
if (newlineIndex !== -1 && total.length >= newlineIndex + 1 + preface.length) {
|
|
2697
|
+
clearTimeout(timer);
|
|
2698
|
+
resolve(total);
|
|
2699
|
+
}
|
|
2700
|
+
});
|
|
2701
|
+
child.once("error", reject);
|
|
2702
|
+
child.once("exit", (code) => {
|
|
2703
|
+
clearTimeout(timer);
|
|
2704
|
+
reject(new Error("The http2 gateway exited early with code " + String(code) + ". stderr: " + stderr));
|
|
2705
|
+
});
|
|
2706
|
+
});
|
|
2707
|
+
const newlineIndex = firstBytes.indexOf(0x0a);
|
|
2708
|
+
expect(newlineIndex).toBeGreaterThan(0);
|
|
2709
|
+
// Assert the exact UTF-8 bytes, not a parsed-and-matched object.
|
|
2710
|
+
// `JSON.stringify` writes keys in the object-literal insertion order, so a
|
|
2711
|
+
// reordered or reformatted call site at the gateway's one `writeFrame` call
|
|
2712
|
+
// would change the bytes on the wire without failing a looser assertion.
|
|
2713
|
+
const readyLine = firstBytes.subarray(0, newlineIndex).toString("utf8");
|
|
2714
|
+
expect(readyLine).toBe(`{"version":2,"type":"ready","nonce":"${nonce}"}`);
|
|
2715
|
+
const afterReady = firstBytes.subarray(newlineIndex + 1, newlineIndex + 1 + preface.length);
|
|
2716
|
+
expect(afterReady).toEqual(preface);
|
|
2717
|
+
}, 15_000);
|
|
2718
|
+
/**
|
|
2719
|
+
* Spawn the real generated gateway in `http2_v1` mode, then bind the real
|
|
2720
|
+
* host-side `createHttp2BridgeServer` to its stdio. The gateway writes one
|
|
2721
|
+
* READY line before it hands stdout to its HTTP/2 client, so this helper
|
|
2722
|
+
* strips that line first and feeds the host only the raw HTTP/2 bytes that
|
|
2723
|
+
* follow. A test then drives the gateway with ordinary HTTP/1.1 requests
|
|
2724
|
+
* against its loopback port and inspects the exact bytes `forwardRequest`
|
|
2725
|
+
* receives, proving the send path carries no intermediate string.
|
|
2726
|
+
*/
|
|
2727
|
+
async function startHttp2GatewayForTest(options) {
|
|
2728
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-http2-test-"));
|
|
2729
|
+
cleanupDirs.push(rootDir);
|
|
2730
|
+
const entrypoint = path.join(rootDir, "paperclip-bridge-server.mjs");
|
|
2731
|
+
await writeFile(entrypoint, getSandboxCallbackBridgeServerSource(), "utf8");
|
|
2732
|
+
const probe = createServer();
|
|
2733
|
+
const assignedPort = await new Promise((resolve, reject) => {
|
|
2734
|
+
probe.once("error", reject);
|
|
2735
|
+
probe.listen(0, "127.0.0.1", () => {
|
|
2736
|
+
const address = probe.address();
|
|
2737
|
+
if (!address || typeof address === "string") {
|
|
2738
|
+
reject(new Error("Could not reserve a loopback port for the test."));
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
probe.close(() => resolve(address.port));
|
|
2742
|
+
});
|
|
2743
|
+
});
|
|
2744
|
+
const child = spawn(process.execPath, [entrypoint], {
|
|
2745
|
+
env: {
|
|
2746
|
+
...process.env,
|
|
2747
|
+
PAPERCLIP_API_BRIDGE_MODE: "http2_v1",
|
|
2748
|
+
PAPERCLIP_BRIDGE_TOKEN: options.bridgeToken,
|
|
2749
|
+
PAPERCLIP_BRIDGE_PORT: String(assignedPort),
|
|
2750
|
+
PAPERCLIP_BRIDGE_NONCE: "test-nonce",
|
|
2751
|
+
...(options.maxBodyBytes != null
|
|
2752
|
+
? { PAPERCLIP_BRIDGE_MAX_BODY_BYTES: String(options.maxBodyBytes) }
|
|
2753
|
+
: {}),
|
|
2754
|
+
},
|
|
2755
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2756
|
+
});
|
|
2757
|
+
let stderr = "";
|
|
2758
|
+
child.stderr.on("data", (chunk) => {
|
|
2759
|
+
stderr += chunk.toString("utf8");
|
|
2760
|
+
});
|
|
2761
|
+
const dataListeners = [];
|
|
2762
|
+
const pending = [];
|
|
2763
|
+
let dispatchStarted = false;
|
|
2764
|
+
let sawReadyLine = false;
|
|
2765
|
+
let readyBuffer = Buffer.alloc(0);
|
|
2766
|
+
let resolveReady;
|
|
2767
|
+
let rejectReady;
|
|
2768
|
+
const readyPromise = new Promise((resolve, reject) => {
|
|
2769
|
+
resolveReady = resolve;
|
|
2770
|
+
rejectReady = reject;
|
|
2771
|
+
});
|
|
2772
|
+
const readyTimer = setTimeout(() => rejectReady(new Error("Timed out waiting for the http2 gateway READY line. stderr: " + stderr)), 5000);
|
|
2773
|
+
const deliver = (chunk) => {
|
|
2774
|
+
if (dispatchStarted) {
|
|
2775
|
+
for (const listener of dataListeners)
|
|
2776
|
+
listener(chunk);
|
|
2777
|
+
}
|
|
2778
|
+
else {
|
|
2779
|
+
pending.push(chunk);
|
|
2780
|
+
}
|
|
2781
|
+
};
|
|
2782
|
+
child.stdout.on("data", (chunk) => {
|
|
2783
|
+
if (!sawReadyLine) {
|
|
2784
|
+
readyBuffer = Buffer.concat([readyBuffer, chunk]);
|
|
2785
|
+
const newlineIndex = readyBuffer.indexOf(0x0a);
|
|
2786
|
+
if (newlineIndex === -1)
|
|
2787
|
+
return;
|
|
2788
|
+
sawReadyLine = true;
|
|
2789
|
+
clearTimeout(readyTimer);
|
|
2790
|
+
const rest = readyBuffer.subarray(newlineIndex + 1);
|
|
2791
|
+
readyBuffer = Buffer.alloc(0);
|
|
2792
|
+
resolveReady();
|
|
2793
|
+
if (rest.length > 0)
|
|
2794
|
+
deliver(rest);
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
deliver(chunk);
|
|
2798
|
+
});
|
|
2799
|
+
child.once("exit", (code) => {
|
|
2800
|
+
clearTimeout(readyTimer);
|
|
2801
|
+
if (!sawReadyLine) {
|
|
2802
|
+
rejectReady(new Error("The http2 gateway exited early with code " + String(code) + ". stderr: " + stderr));
|
|
2803
|
+
}
|
|
2804
|
+
});
|
|
2805
|
+
const channel = {
|
|
2806
|
+
write: (data) => {
|
|
2807
|
+
child.stdin.write(Buffer.from(data));
|
|
2808
|
+
},
|
|
2809
|
+
onData: (listener) => {
|
|
2810
|
+
dataListeners.push(listener);
|
|
2811
|
+
},
|
|
2812
|
+
onExit: (listener) => {
|
|
2813
|
+
child.once("exit", (code) => listener({ exitCode: code }));
|
|
2814
|
+
},
|
|
2815
|
+
stop: () => {
|
|
2816
|
+
child.kill();
|
|
2817
|
+
},
|
|
2818
|
+
close: async () => {
|
|
2819
|
+
child.stdin.end();
|
|
2820
|
+
},
|
|
2821
|
+
};
|
|
2822
|
+
await readyPromise;
|
|
2823
|
+
const handle = createHttp2BridgeServer({
|
|
2824
|
+
bridgeToken: options.bridgeToken,
|
|
2825
|
+
forwardRequest: options.forwardRequest,
|
|
2826
|
+
});
|
|
2827
|
+
const boundDuplex = handle.bindChannel(channel);
|
|
2828
|
+
dispatchStarted = true;
|
|
2829
|
+
const buffered = pending.splice(0);
|
|
2830
|
+
for (const chunk of buffered) {
|
|
2831
|
+
for (const listener of dataListeners)
|
|
2832
|
+
listener(chunk);
|
|
2833
|
+
}
|
|
2834
|
+
// Destroy the bound duplex directly instead of `handle.close()`. The
|
|
2835
|
+
// duplex here wraps a spawned process's raw stdio, not a real socket,
|
|
2836
|
+
// and `close()` waits on a graceful HTTP/2 GOAWAY exchange that never
|
|
2837
|
+
// settles over this transport. A direct destroy ends the session at
|
|
2838
|
+
// once, which is correct for test teardown.
|
|
2839
|
+
const stop = async () => {
|
|
2840
|
+
boundDuplex.destroy();
|
|
2841
|
+
child.kill();
|
|
2842
|
+
};
|
|
2843
|
+
cleanupFns.push(stop);
|
|
2844
|
+
return { baseUrl: `http://127.0.0.1:${assignedPort}`, stop };
|
|
2845
|
+
}
|
|
2846
|
+
it("forwards the exact request body bytes to the HTTP/2 host handler, including a non-ASCII character", async () => {
|
|
2847
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
2848
|
+
const seenBodies = [];
|
|
2849
|
+
const gateway = await startHttp2GatewayForTest({
|
|
2850
|
+
bridgeToken,
|
|
2851
|
+
forwardRequest: async (request) => {
|
|
2852
|
+
seenBodies.push(request.body);
|
|
2853
|
+
return {
|
|
2854
|
+
status: 200,
|
|
2855
|
+
headers: { "content-type": "application/json" },
|
|
2856
|
+
body: Buffer.from(JSON.stringify({ ok: true }), "utf8"),
|
|
2857
|
+
};
|
|
2858
|
+
},
|
|
2859
|
+
});
|
|
2860
|
+
// "café" holds one multi-byte UTF-8 character. A body-to-string-to-body
|
|
2861
|
+
// round trip still reproduces this text correctly, so the byte-for-byte
|
|
2862
|
+
// comparison below is the real proof: it fails if any re-encoding step
|
|
2863
|
+
// runs, even one that happens to preserve valid UTF-8 text.
|
|
2864
|
+
const bodyText = JSON.stringify({ note: "café" });
|
|
2865
|
+
const bodyBytes = Buffer.from(bodyText, "utf8");
|
|
2866
|
+
const response = await fetch(`${gateway.baseUrl}/api/issues/issue-1/comments`, {
|
|
2867
|
+
method: "POST",
|
|
2868
|
+
headers: {
|
|
2869
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
2870
|
+
"content-type": "application/json",
|
|
2871
|
+
},
|
|
2872
|
+
body: bodyBytes,
|
|
2873
|
+
});
|
|
2874
|
+
expect(response.status).toBe(200);
|
|
2875
|
+
expect(seenBodies).toHaveLength(1);
|
|
2876
|
+
expect(seenBodies[0]?.equals(bodyBytes)).toBe(true);
|
|
2877
|
+
}, 15_000);
|
|
2878
|
+
it("forwards malformed UTF-8 bytes to the HTTP/2 host handler unchanged", async () => {
|
|
2879
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
2880
|
+
const seenBodies = [];
|
|
2881
|
+
// Byte 0xC3 opens a two-byte UTF-8 sequence; 0x28 is not a valid
|
|
2882
|
+
// continuation byte, so this body is not valid UTF-8. The gateway does
|
|
2883
|
+
// not decode or validate the body, so these exact bytes must still
|
|
2884
|
+
// arrive at the host handler unchanged, and the same bytes must return
|
|
2885
|
+
// to the caller unchanged. The host answers with a non-JSON content
|
|
2886
|
+
// type, so the round trip proves the gateway applies no format-specific
|
|
2887
|
+
// handling on the response leg either.
|
|
2888
|
+
const malformedBytes = Buffer.from([0x7b, 0x22, 0x61, 0x22, 0x3a, 0xc3, 0x28, 0x7d]);
|
|
2889
|
+
const gateway = await startHttp2GatewayForTest({
|
|
2890
|
+
bridgeToken,
|
|
2891
|
+
forwardRequest: async (request) => {
|
|
2892
|
+
seenBodies.push(request.body);
|
|
2893
|
+
return { status: 200, headers: { "content-type": "application/octet-stream" }, body: malformedBytes };
|
|
2894
|
+
},
|
|
2895
|
+
});
|
|
2896
|
+
const response = await fetch(`${gateway.baseUrl}/api/issues/issue-1/comments`, {
|
|
2897
|
+
method: "POST",
|
|
2898
|
+
headers: {
|
|
2899
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
2900
|
+
"content-type": "application/json",
|
|
2901
|
+
},
|
|
2902
|
+
body: malformedBytes,
|
|
2903
|
+
});
|
|
2904
|
+
expect(response.status).toBe(200);
|
|
2905
|
+
expect(seenBodies).toHaveLength(1);
|
|
2906
|
+
expect(seenBodies[0]?.equals(malformedBytes)).toBe(true);
|
|
2907
|
+
const responseBytes = Buffer.from(await response.arrayBuffer());
|
|
2908
|
+
expect(responseBytes.equals(malformedBytes)).toBe(true);
|
|
2909
|
+
}, 15_000);
|
|
2910
|
+
it("rejects a request body over maxBodyBytes on the HTTP/2 path before it forwards a byte", async () => {
|
|
2911
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
2912
|
+
const maxBodyBytes = 32;
|
|
2913
|
+
let forwardCalls = 0;
|
|
2914
|
+
const gateway = await startHttp2GatewayForTest({
|
|
2915
|
+
bridgeToken,
|
|
2916
|
+
maxBodyBytes,
|
|
2917
|
+
forwardRequest: async () => {
|
|
2918
|
+
forwardCalls += 1;
|
|
2919
|
+
return { status: 200, headers: {}, body: Buffer.alloc(0) };
|
|
2920
|
+
},
|
|
2921
|
+
});
|
|
2922
|
+
const oversizeBody = Buffer.alloc(maxBodyBytes + 1, 0x41);
|
|
2923
|
+
const response = await fetch(`${gateway.baseUrl}/api/issues/issue-1/comments`, {
|
|
2924
|
+
method: "POST",
|
|
2925
|
+
headers: {
|
|
2926
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
2927
|
+
"content-type": "application/json",
|
|
2928
|
+
},
|
|
2929
|
+
body: oversizeBody,
|
|
2930
|
+
});
|
|
2931
|
+
expect(response.status).toBe(502);
|
|
2932
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
2933
|
+
error: "Bridge request body exceeded the configured size limit.",
|
|
2934
|
+
});
|
|
2935
|
+
expect(forwardCalls).toBe(0);
|
|
2936
|
+
}, 15_000);
|
|
2937
|
+
/** A resolvable gate a test can await, then release on its own schedule. */
|
|
2938
|
+
function createGate() {
|
|
2939
|
+
let reach;
|
|
2940
|
+
const reached = new Promise((resolve) => {
|
|
2941
|
+
reach = resolve;
|
|
2942
|
+
});
|
|
2943
|
+
return { reached, reach };
|
|
2944
|
+
}
|
|
2945
|
+
it("denies a body once concurrent reservations reach the gateway's own process ledger ceiling, then admits again once they release", async () => {
|
|
2946
|
+
// The per-body maxBodyBytes cap alone does not bound how many bodies
|
|
2947
|
+
// this gateway process holds in memory at once. The generated gateway's
|
|
2948
|
+
// own process ledger gives it a second, independent ceiling: 4
|
|
2949
|
+
// concurrent max-size bodies, each counted twice (the retained chunk
|
|
2950
|
+
// array and the concatenated copy), exactly fills maxBodyBytes * 8. This
|
|
2951
|
+
// drives 4 concurrent requests to that exact ceiling, held open by a
|
|
2952
|
+
// forward call that does not return, then proves a 5th is denied and a
|
|
2953
|
+
// later one succeeds once the 4 held requests release their bytes.
|
|
2954
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
2955
|
+
const maxBodyBytes = 400;
|
|
2956
|
+
const holdBody = Buffer.alloc(maxBodyBytes, 0x42);
|
|
2957
|
+
const HOLD_COUNT = 4;
|
|
2958
|
+
const reachedGates = Array.from({ length: HOLD_COUNT }, () => createGate());
|
|
2959
|
+
const releaseGates = Array.from({ length: HOLD_COUNT }, () => createGate());
|
|
2960
|
+
let forwardCalls = 0;
|
|
2961
|
+
const gateway = await startHttp2GatewayForTest({
|
|
2962
|
+
bridgeToken,
|
|
2963
|
+
maxBodyBytes,
|
|
2964
|
+
forwardRequest: async () => {
|
|
2965
|
+
const callIndex = forwardCalls;
|
|
2966
|
+
forwardCalls += 1;
|
|
2967
|
+
if (callIndex < HOLD_COUNT) {
|
|
2968
|
+
reachedGates[callIndex].reach();
|
|
2969
|
+
await releaseGates[callIndex].reached;
|
|
2970
|
+
}
|
|
2971
|
+
return { status: 200, headers: {}, body: Buffer.alloc(0) };
|
|
2972
|
+
},
|
|
2973
|
+
});
|
|
2974
|
+
const postHoldBody = () => fetch(`${gateway.baseUrl}/api/issues/issue-1/comments`, {
|
|
2975
|
+
method: "POST",
|
|
2976
|
+
headers: { authorization: `Bearer ${bridgeToken}`, "content-type": "application/json" },
|
|
2977
|
+
body: holdBody,
|
|
2978
|
+
});
|
|
2979
|
+
const holdResponses = Promise.all(Array.from({ length: HOLD_COUNT }, () => postHoldBody()));
|
|
2980
|
+
// Each held request's own forward call started, which only happens
|
|
2981
|
+
// after readBodyBytes already reserved that request's bytes, so all 4
|
|
2982
|
+
// reservations are live once every gate below resolves.
|
|
2983
|
+
await Promise.all(reachedGates.map((gate) => gate.reached));
|
|
2984
|
+
const deniedResponse = await postHoldBody();
|
|
2985
|
+
expect(deniedResponse.status).toBe(503);
|
|
2986
|
+
await expect(deniedResponse.json()).resolves.toMatchObject({
|
|
2987
|
+
error: "The bridge gateway process reached its reserved body byte ceiling. Retry later.",
|
|
2988
|
+
});
|
|
2989
|
+
// The denial happened during the body read, before this stream's own
|
|
2990
|
+
// forward call ever ran.
|
|
2991
|
+
expect(forwardCalls).toBe(HOLD_COUNT);
|
|
2992
|
+
releaseGates.forEach((gate) => gate.reach());
|
|
2993
|
+
const responses = await holdResponses;
|
|
2994
|
+
for (const response of responses) {
|
|
2995
|
+
expect(response.status).toBe(200);
|
|
2996
|
+
}
|
|
2997
|
+
// Every held reservation released once its own forward call settled: a
|
|
2998
|
+
// fresh request at the same size now succeeds again.
|
|
2999
|
+
const recoveredResponse = await postHoldBody();
|
|
3000
|
+
expect(recoveredResponse.status).toBe(200);
|
|
3001
|
+
}, 15_000);
|
|
3002
|
+
it("maps an indeterminate host outcome to a non-retryable 409 on the HTTP/2 gateway for an unsafe method", async () => {
|
|
3003
|
+
// The host answers a mutating request's response-body capacity denial with a
|
|
3004
|
+
// 504 and the indeterminate outcome header, exactly like an aborted in-flight
|
|
3005
|
+
// forward: the host may have already committed the mutation, so the status
|
|
3006
|
+
// must not be retryable. The HTTP/2 gateway must map that 504 to a 409, the
|
|
3007
|
+
// same map the file gateway already applies, so a standard retry policy does
|
|
3008
|
+
// not repeat the mutation. The outcome header and body must survive the map.
|
|
3009
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
3010
|
+
const indeterminateBody = JSON.stringify({ error: "response body over limit", outcome: "indeterminate" });
|
|
3011
|
+
const gateway = await startHttp2GatewayForTest({
|
|
3012
|
+
bridgeToken,
|
|
3013
|
+
forwardRequest: async () => ({
|
|
3014
|
+
status: 504,
|
|
3015
|
+
headers: { "content-type": "application/json", "x-paperclip-bridge-outcome": "indeterminate" },
|
|
3016
|
+
body: Buffer.from(indeterminateBody, "utf8"),
|
|
3017
|
+
}),
|
|
3018
|
+
});
|
|
3019
|
+
const response = await fetch(`${gateway.baseUrl}/api/issues/issue-1/comments`, {
|
|
3020
|
+
method: "POST",
|
|
3021
|
+
headers: { authorization: `Bearer ${bridgeToken}`, "content-type": "application/json" },
|
|
3022
|
+
body: JSON.stringify({ note: "test" }),
|
|
3023
|
+
});
|
|
3024
|
+
expect(response.status).toBe(409);
|
|
3025
|
+
expect(response.status).toBeLessThan(500);
|
|
3026
|
+
expect(response.headers.get("x-paperclip-bridge-outcome")).toBe("indeterminate");
|
|
3027
|
+
const responseBytes = Buffer.from(await response.arrayBuffer());
|
|
3028
|
+
expect(responseBytes.toString("utf8")).toBe(indeterminateBody);
|
|
3029
|
+
}, 15_000);
|
|
3030
|
+
it("passes through a safe method's retryable 503 on the HTTP/2 gateway unchanged", async () => {
|
|
3031
|
+
// A safe method's response-body capacity denial carries no indeterminate
|
|
3032
|
+
// outcome header, so the gateway must keep it retryable: the map above must
|
|
3033
|
+
// not fire on a plain 503.
|
|
3034
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
3035
|
+
const gateway = await startHttp2GatewayForTest({
|
|
3036
|
+
bridgeToken,
|
|
3037
|
+
forwardRequest: async () => ({
|
|
3038
|
+
status: 503,
|
|
3039
|
+
headers: { "content-type": "application/json" },
|
|
3040
|
+
body: Buffer.from(JSON.stringify({ error: "response body over limit" }), "utf8"),
|
|
3041
|
+
}),
|
|
3042
|
+
});
|
|
3043
|
+
const response = await fetch(`${gateway.baseUrl}/api/issues/issue-1/comments`, {
|
|
3044
|
+
method: "GET",
|
|
3045
|
+
headers: { authorization: `Bearer ${bridgeToken}` },
|
|
3046
|
+
});
|
|
3047
|
+
expect(response.status).toBe(503);
|
|
3048
|
+
expect(response.headers.get("x-paperclip-bridge-outcome")).toBeNull();
|
|
3049
|
+
}, 15_000);
|
|
3050
|
+
async function startQueueGatewayForFileTest(options) {
|
|
3051
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "paperclip-file-codec-"));
|
|
3052
|
+
cleanupDirs.push(root);
|
|
3053
|
+
const asset = await createSandboxCallbackBridgeAsset();
|
|
3054
|
+
cleanupFns.push(asset.cleanup);
|
|
3055
|
+
const queueDir = path.join(root, "queue");
|
|
3056
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
3057
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
3058
|
+
client: options.client ?? createFileSystemSandboxCallbackBridgeQueueClient(), queueDir,
|
|
3059
|
+
maxBodyBytes: options.maxBodyBytes, handleRequest: options.handleRequest, pollIntervalMs: 10,
|
|
3060
|
+
});
|
|
3061
|
+
cleanupFns.push(() => worker.stop());
|
|
3062
|
+
const gateway = await startSandboxCallbackBridgeServer({
|
|
3063
|
+
runner: createExecRunner(), remoteCwd: root, assetRemoteDir: asset.localDir,
|
|
3064
|
+
queueDir, bridgeToken, maxBodyBytes: options.maxBodyBytes, pollIntervalMs: 10,
|
|
3065
|
+
});
|
|
3066
|
+
cleanupFns.push(() => gateway.stop());
|
|
3067
|
+
return { ...gateway, bridgeToken, queueDir };
|
|
3068
|
+
}
|
|
3069
|
+
it.each(["queue", "http2"])("preserves multipart bytes at the configured limit on %s and rejects overflow before forwarding", async (mode) => {
|
|
3070
|
+
const maxBodyBytes = 1024;
|
|
3071
|
+
const bytes = Buffer.alloc(maxBodyBytes, 0xff);
|
|
3072
|
+
const handled = vi.fn(async (request) => ({
|
|
3073
|
+
status: 200, headers: { "content-type": "application/octet-stream" }, body: Buffer.from(request.body ?? ""),
|
|
3074
|
+
}));
|
|
3075
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
3076
|
+
const gateway = mode === "queue"
|
|
3077
|
+
? await startQueueGatewayForFileTest({ maxBodyBytes, handleRequest: handled })
|
|
3078
|
+
: { ...await startHttp2GatewayForTest({ bridgeToken, maxBodyBytes, forwardRequest: handled }), bridgeToken };
|
|
3079
|
+
const post = (body) => fetch(`${gateway.baseUrl}/api/companies/c/issues/i/attachments`, {
|
|
3080
|
+
method: "POST", headers: { authorization: `Bearer ${gateway.bridgeToken}`, "content-type": "multipart/form-data; boundary=exact-boundary" }, body: new Uint8Array(body),
|
|
3081
|
+
});
|
|
3082
|
+
const response = await post(bytes);
|
|
3083
|
+
expect(response.status).toBe(200);
|
|
3084
|
+
expect(Buffer.from(await response.arrayBuffer())).toEqual(bytes);
|
|
3085
|
+
expect(handled).toHaveBeenCalledTimes(1);
|
|
3086
|
+
expect(handled.mock.calls[0][0]).toMatchObject({ headers: { "content-type": "multipart/form-data; boundary=exact-boundary" } });
|
|
3087
|
+
const overflow = await post(Buffer.alloc(maxBodyBytes + 1));
|
|
3088
|
+
expect(overflow.status).toBeGreaterThanOrEqual(400);
|
|
3089
|
+
expect(handled).toHaveBeenCalledTimes(1);
|
|
3090
|
+
});
|
|
3091
|
+
it("rejects malformed queue encodings without forwarding a mutation", async () => {
|
|
3092
|
+
const handled = vi.fn(async () => ({ status: 200, body: "ok" }));
|
|
3093
|
+
const gateway = await startQueueGatewayForFileTest({ maxBodyBytes: 1024, handleRequest: handled });
|
|
3094
|
+
const directories = sandboxCallbackBridgeDirectories(gateway.queueDir);
|
|
3095
|
+
for (const [id, bodyEncoding, body] of [["bad-base64", "base64", "YR=="], ["bad-encoding", "hex", "00"], ["too-big", "base64", Buffer.alloc(1025).toString("base64")]]) {
|
|
3096
|
+
await createFileSystemSandboxCallbackBridgeQueueClient().writeTextFile(path.join(directories.requestsDir, `${id}.json`), JSON.stringify({
|
|
3097
|
+
id, method: "POST", path: "/api/companies/c/issues/i/attachments", query: "", headers: {}, bodyEncoding, body,
|
|
3098
|
+
}));
|
|
3099
|
+
const responsePath = path.join(directories.responsesDir, `${id}.json`);
|
|
3100
|
+
await vi.waitFor(async () => expect(JSON.parse(await readFile(responsePath, "utf8")).status).toBe(400));
|
|
3101
|
+
}
|
|
3102
|
+
expect(handled).not.toHaveBeenCalled();
|
|
3103
|
+
});
|
|
3104
|
+
it("reports a corrupted upload response as indeterminate and never forwards twice", async () => {
|
|
3105
|
+
const client = createFileSystemSandboxCallbackBridgeQueueClient();
|
|
3106
|
+
const writeResponse = client.writeResponseFile.bind(client);
|
|
3107
|
+
client.writeResponseFile = (destination, body, options) => writeResponse(destination, JSON.stringify({ ...JSON.parse(body), bodyEncoding: "base64", body: "invalid" }), options);
|
|
3108
|
+
const handled = vi.fn(async () => ({ status: 201, body: Buffer.from("saved") }));
|
|
3109
|
+
const gateway = await startQueueGatewayForFileTest({ client, maxBodyBytes: 1024, handleRequest: handled });
|
|
3110
|
+
const response = await fetch(`${gateway.baseUrl}/api/companies/c/issues/i/attachments`, {
|
|
3111
|
+
method: "POST", headers: { authorization: `Bearer ${gateway.bridgeToken}`, "content-type": "multipart/form-data; boundary=boundary" }, body: Buffer.from([0xff]),
|
|
3112
|
+
});
|
|
3113
|
+
expect(response.status).toBe(409);
|
|
3114
|
+
expect(response.headers.get("x-paperclip-bridge-outcome")).toBe("indeterminate");
|
|
3115
|
+
await expect(response.json()).resolves.toMatchObject({ retryable: false, outcome: "indeterminate" });
|
|
3116
|
+
expect(handled).toHaveBeenCalledTimes(1);
|
|
3117
|
+
});
|
|
3118
|
+
it("rejects a request body over maxBodyBytes on the queue path before it writes the queue file", async () => {
|
|
3119
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-queue-maxbody-"));
|
|
3120
|
+
cleanupDirs.push(rootDir);
|
|
3121
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
3122
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
3123
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
3124
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
3125
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "bridge maxBodyBytes test\n", "utf8");
|
|
3126
|
+
const runner = createExecRunner();
|
|
3127
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
3128
|
+
cleanupFns.push(bridgeAsset.cleanup);
|
|
3129
|
+
const prepared = await prepareCommandManagedRuntime({
|
|
3130
|
+
runner,
|
|
3131
|
+
spec: { remoteCwd: remoteWorkspaceDir, timeoutMs: 30_000 },
|
|
3132
|
+
adapterKey: "codex",
|
|
3133
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
3134
|
+
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
|
|
3135
|
+
});
|
|
3136
|
+
const queueDir = path.posix.join(prepared.runtimeRootDir, "paperclip-bridge");
|
|
3137
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
3138
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
3139
|
+
const maxBodyBytes = 32;
|
|
3140
|
+
const bridge = await startSandboxCallbackBridgeServer({
|
|
3141
|
+
runner,
|
|
3142
|
+
remoteCwd: remoteWorkspaceDir,
|
|
3143
|
+
assetRemoteDir: prepared.assetDirs.bridge,
|
|
3144
|
+
queueDir,
|
|
3145
|
+
bridgeToken,
|
|
3146
|
+
timeoutMs: 30_000,
|
|
3147
|
+
maxBodyBytes,
|
|
3148
|
+
});
|
|
3149
|
+
cleanupFns.push(async () => {
|
|
3150
|
+
await bridge.stop();
|
|
3151
|
+
});
|
|
3152
|
+
const oversizeBody = Buffer.alloc(maxBodyBytes + 1, 0x41);
|
|
3153
|
+
const response = await fetch(`${bridge.baseUrl}/api/issues/issue-1/comments`, {
|
|
3154
|
+
method: "POST",
|
|
3155
|
+
headers: {
|
|
3156
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
3157
|
+
"content-type": "application/json",
|
|
3158
|
+
},
|
|
3159
|
+
body: oversizeBody,
|
|
3160
|
+
});
|
|
3161
|
+
expect(response.status).toBe(502);
|
|
3162
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
3163
|
+
error: "Bridge request body exceeded the configured size limit.",
|
|
3164
|
+
});
|
|
3165
|
+
const requestFiles = await readdir(directories.requestsDir);
|
|
3166
|
+
expect(requestFiles.filter((name) => name.endsWith(".json"))).toHaveLength(0);
|
|
3167
|
+
});
|
|
3168
|
+
it("keeps the queue request payload's body field as a plain JSON string", async () => {
|
|
3169
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bridge-queue-body-shape-"));
|
|
3170
|
+
cleanupDirs.push(rootDir);
|
|
3171
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
3172
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
3173
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
3174
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
3175
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "bridge body shape test\n", "utf8");
|
|
3176
|
+
const runner = createExecRunner();
|
|
3177
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
3178
|
+
cleanupFns.push(bridgeAsset.cleanup);
|
|
3179
|
+
const prepared = await prepareCommandManagedRuntime({
|
|
3180
|
+
runner,
|
|
3181
|
+
spec: { remoteCwd: remoteWorkspaceDir, timeoutMs: 30_000 },
|
|
3182
|
+
adapterKey: "codex",
|
|
3183
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
3184
|
+
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
|
|
3185
|
+
});
|
|
3186
|
+
const queueDir = path.posix.join(prepared.runtimeRootDir, "paperclip-bridge");
|
|
3187
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
3188
|
+
const requestBodyText = JSON.stringify({ note: "café" });
|
|
3189
|
+
const seenRequests = [];
|
|
3190
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
3191
|
+
client: createFileSystemSandboxCallbackBridgeQueueClient(),
|
|
3192
|
+
queueDir,
|
|
3193
|
+
authorizeRequest: async () => null,
|
|
3194
|
+
handleRequest: async (request) => {
|
|
3195
|
+
seenRequests.push({ body: request.body });
|
|
3196
|
+
return { status: 200, headers: {}, body: JSON.stringify({ ok: true }) };
|
|
3197
|
+
},
|
|
3198
|
+
});
|
|
3199
|
+
cleanupFns.push(async () => {
|
|
3200
|
+
await worker.stop();
|
|
3201
|
+
});
|
|
3202
|
+
const bridge = await startSandboxCallbackBridgeServer({
|
|
3203
|
+
runner,
|
|
3204
|
+
remoteCwd: remoteWorkspaceDir,
|
|
3205
|
+
assetRemoteDir: prepared.assetDirs.bridge,
|
|
3206
|
+
queueDir,
|
|
3207
|
+
bridgeToken,
|
|
3208
|
+
timeoutMs: 30_000,
|
|
3209
|
+
});
|
|
3210
|
+
cleanupFns.push(async () => {
|
|
3211
|
+
await bridge.stop();
|
|
3212
|
+
});
|
|
3213
|
+
const response = await fetch(`${bridge.baseUrl}/api/issues/issue-1/comments`, {
|
|
3214
|
+
method: "POST",
|
|
3215
|
+
headers: {
|
|
3216
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
3217
|
+
"content-type": "application/json",
|
|
3218
|
+
},
|
|
3219
|
+
body: requestBodyText,
|
|
3220
|
+
});
|
|
3221
|
+
expect(response.status).toBe(200);
|
|
3222
|
+
expect(seenRequests).toHaveLength(1);
|
|
3223
|
+
expect(typeof seenRequests[0]?.body).toBe("string");
|
|
3224
|
+
expect(seenRequests[0]?.body).toBe(requestBodyText);
|
|
3225
|
+
});
|
|
3226
|
+
});
|
|
3227
|
+
// This describe block covers the zero-dependency process body-byte ledger
|
|
3228
|
+
// every generated gateway embeds (`BRIDGE_PROCESS_BODY_LEDGER_SOURCE`),
|
|
3229
|
+
// exercised directly with no spawned process involved, the same way the
|
|
3230
|
+
// codec source in `execution-target-sandbox.test.ts` gets its own direct
|
|
3231
|
+
// coverage.
|
|
3232
|
+
describe("embedded sandbox gateway process body ledger", () => {
|
|
3233
|
+
it("reserves and releases bytes against its own ceiling, denying only once it is exceeded", () => {
|
|
3234
|
+
const ledgerFactory = new Function(`${getSandboxBridgeProcessBodyLedgerSource()}\nreturn createBridgeProcessBodyLedger;`);
|
|
3235
|
+
const createBridgeProcessBodyLedger = ledgerFactory();
|
|
3236
|
+
const ledger = createBridgeProcessBodyLedger(100);
|
|
3237
|
+
expect(ledger.reserve(60)).toBe(true);
|
|
3238
|
+
expect(ledger.reservedBytes).toBe(60);
|
|
3239
|
+
// A denied reservation reserves nothing: the total stays exactly what
|
|
3240
|
+
// the first call reserved.
|
|
3241
|
+
expect(ledger.reserve(41)).toBe(false);
|
|
3242
|
+
expect(ledger.reservedBytes).toBe(60);
|
|
3243
|
+
expect(ledger.reserve(40)).toBe(true);
|
|
3244
|
+
expect(ledger.reservedBytes).toBe(100);
|
|
3245
|
+
ledger.release(60);
|
|
3246
|
+
expect(ledger.reservedBytes).toBe(40);
|
|
3247
|
+
// Room freed by the release admits a request the full ceiling would
|
|
3248
|
+
// have denied.
|
|
3249
|
+
expect(ledger.reserve(60)).toBe(true);
|
|
3250
|
+
expect(ledger.reservedBytes).toBe(100);
|
|
3251
|
+
});
|
|
3252
|
+
});
|
|
3253
|
+
//# sourceMappingURL=sandbox-callback-bridge.test.js.map
|