@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,3746 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { promises as fsPromises } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, mkdtemp, readFile, readdir, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { execFile as execFileCallback, spawn } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
import { resetLocalGitIndexToHead, runLocalGit, setExpensiveWorkspaceGitExecutor, WORKSPACE_GIT_SCAN_SATURATED_CODE, } from "./git-workspace-sync.js";
|
|
10
|
+
import { assertSyncOperationsConfined, escapeTarExcludeLiteral, mirrorDirectory, prepareSandboxManagedRuntime, REFERENCED_SOURCE_IGNORE_FAILURE_REASONS, resolveReferencedSourceIgnore, } from "./sandbox-managed-runtime.js";
|
|
11
|
+
import { prepareCommandManagedRuntime, } from "./command-managed-runtime.js";
|
|
12
|
+
import { SYNC_OPERATION_CONCURRENCY_LIMIT } from "./sync-operation-schedule.js";
|
|
13
|
+
import { createRuntimeSpanRunner, getActiveStepContext, measureStartupStep, } from "./acpx-engine/startup-timing.js";
|
|
14
|
+
function toArrayBuffer(bytes) {
|
|
15
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
16
|
+
}
|
|
17
|
+
// Sum the file sizes under a directory, recursively. Test-only: it stands in
|
|
18
|
+
// for a real provider's own byte count on a `kind: "directory"` mapping, so a
|
|
19
|
+
// fake `syncIn`/`syncOut` can report a real, non-zero `bytesTransferred`.
|
|
20
|
+
async function directoryByteSize(dir) {
|
|
21
|
+
let total = 0;
|
|
22
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const entryPath = path.join(dir, entry.name);
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
total += await directoryByteSize(entryPath);
|
|
27
|
+
}
|
|
28
|
+
else if (entry.isFile()) {
|
|
29
|
+
total += (await stat(entryPath)).size;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return total;
|
|
33
|
+
}
|
|
34
|
+
// Give a bare fake client a `syncIn` that reproduces the non-native base64-tar
|
|
35
|
+
// FALLBACK (place each file mapping via `writeFile`, then run the operation's
|
|
36
|
+
// ordered `postUploadCommands` fail-fast via `run`) — byte-for-byte the prior
|
|
37
|
+
// inline `writeFile`+`run` sequence, exercised through the unified seam. Inbound
|
|
38
|
+
// staging uses only `kind: "file"` mappings.
|
|
39
|
+
function attachFallbackSyncIn(client, timeoutMs = 30_000) {
|
|
40
|
+
client.syncIn = async (operations) => {
|
|
41
|
+
const resultOperations = [];
|
|
42
|
+
for (const operation of operations) {
|
|
43
|
+
let filesTransferred = 0;
|
|
44
|
+
let bytesTransferred = 0;
|
|
45
|
+
for (const mapping of operation.files) {
|
|
46
|
+
const bytes = await readFile(mapping.sourcePath);
|
|
47
|
+
await client.makeDir(path.posix.dirname(mapping.targetPath));
|
|
48
|
+
if (mapping.mode != null) {
|
|
49
|
+
const staged = `${mapping.targetPath}.pcstage`;
|
|
50
|
+
await client.writeFile(staged, toArrayBuffer(bytes));
|
|
51
|
+
await client.run(`chmod ${(mapping.mode & 0o7777).toString(8)} '${staged}' && mv -f '${staged}' '${mapping.targetPath}'`, { timeoutMs });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
await client.writeFile(mapping.targetPath, toArrayBuffer(bytes));
|
|
55
|
+
}
|
|
56
|
+
filesTransferred += 1;
|
|
57
|
+
bytesTransferred += bytes.byteLength;
|
|
58
|
+
}
|
|
59
|
+
for (const command of operation.postUploadCommands ?? []) {
|
|
60
|
+
await client.run(command.command, { timeoutMs: command.timeoutMs ?? timeoutMs });
|
|
61
|
+
}
|
|
62
|
+
resultOperations.push({ operationId: operation.operationId, filesTransferred, bytesTransferred });
|
|
63
|
+
}
|
|
64
|
+
return { operations: resultOperations };
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// A NATIVE-simulating `syncIn`: it records the operations for assertion and
|
|
68
|
+
// materializes files directly (never through the client's `writeFile`/`run`), so
|
|
69
|
+
// a test can prove the orchestrator delegates entirely to `syncIn` (0 direct
|
|
70
|
+
// `writeFile`/`run` execs). Post-upload commands still run in-sandbox (via `sh`),
|
|
71
|
+
// modeling a provider that honors `postUploadCommands` after `uploadFiles`.
|
|
72
|
+
function attachNativeRecordingSyncIn(client, captured) {
|
|
73
|
+
client.syncIn = async (operations) => {
|
|
74
|
+
const resultOperations = [];
|
|
75
|
+
for (const operation of operations) {
|
|
76
|
+
captured.push(operation);
|
|
77
|
+
let filesTransferred = 0;
|
|
78
|
+
let bytesTransferred = 0;
|
|
79
|
+
for (const mapping of operation.files) {
|
|
80
|
+
const bytes = await readFile(mapping.sourcePath);
|
|
81
|
+
await mkdir(path.posix.dirname(mapping.targetPath), { recursive: true });
|
|
82
|
+
await writeFile(mapping.targetPath, bytes);
|
|
83
|
+
if (mapping.mode != null)
|
|
84
|
+
await fsPromises.chmod(mapping.targetPath, mapping.mode);
|
|
85
|
+
filesTransferred += 1;
|
|
86
|
+
bytesTransferred += bytes.byteLength;
|
|
87
|
+
}
|
|
88
|
+
for (const command of operation.postUploadCommands ?? []) {
|
|
89
|
+
await execFile("sh", ["-c", command.command], { maxBuffer: 32 * 1024 * 1024 });
|
|
90
|
+
}
|
|
91
|
+
resultOperations.push({ operationId: operation.operationId, filesTransferred, bytesTransferred });
|
|
92
|
+
}
|
|
93
|
+
return { operations: resultOperations };
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// A capturing `syncIn` that records every operation for assertion and
|
|
97
|
+
// materializes file AND directory mappings. A directory mapping (a referenced
|
|
98
|
+
// project) uses `mirrorDirectory`, so a test can assert the advisory `access`
|
|
99
|
+
// intent on directory mappings as well as file mappings. It reports a real
|
|
100
|
+
// `bytesTransferred` for a directory mapping too, from a post-mirror byte
|
|
101
|
+
// count, so a test can assert on the emitted progress line.
|
|
102
|
+
function attachCapturingSyncIn(client, captured) {
|
|
103
|
+
client.syncIn = async (operations) => {
|
|
104
|
+
const resultOperations = [];
|
|
105
|
+
for (const operation of operations) {
|
|
106
|
+
captured.push(operation);
|
|
107
|
+
let filesTransferred = 0;
|
|
108
|
+
let bytesTransferred = 0;
|
|
109
|
+
for (const mapping of operation.files) {
|
|
110
|
+
await mkdir(path.posix.dirname(mapping.targetPath), { recursive: true });
|
|
111
|
+
if (mapping.kind === "directory") {
|
|
112
|
+
await mirrorDirectory(mapping.sourcePath, mapping.targetPath);
|
|
113
|
+
bytesTransferred += await directoryByteSize(mapping.targetPath);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const bytes = await readFile(mapping.sourcePath);
|
|
117
|
+
await writeFile(mapping.targetPath, bytes);
|
|
118
|
+
if (mapping.mode != null)
|
|
119
|
+
await fsPromises.chmod(mapping.targetPath, mapping.mode);
|
|
120
|
+
bytesTransferred += bytes.byteLength;
|
|
121
|
+
}
|
|
122
|
+
filesTransferred += 1;
|
|
123
|
+
}
|
|
124
|
+
for (const command of operation.postUploadCommands ?? []) {
|
|
125
|
+
await execFile("sh", ["-c", command.command], { maxBuffer: 32 * 1024 * 1024 });
|
|
126
|
+
}
|
|
127
|
+
resultOperations.push({ operationId: operation.operationId, filesTransferred, bytesTransferred });
|
|
128
|
+
}
|
|
129
|
+
return { operations: resultOperations };
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
vi.mock("node:fs", async (importOriginal) => {
|
|
133
|
+
const actual = await importOriginal();
|
|
134
|
+
return {
|
|
135
|
+
...actual,
|
|
136
|
+
promises: {
|
|
137
|
+
...actual.promises,
|
|
138
|
+
chmod: vi.fn(actual.promises.chmod),
|
|
139
|
+
rename: vi.fn(actual.promises.rename),
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
const execFile = promisify(execFileCallback);
|
|
144
|
+
async function git(cwd, args) {
|
|
145
|
+
const { stdout } = await execFile("git", ["-C", cwd, ...args], {
|
|
146
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
147
|
+
});
|
|
148
|
+
return stdout.trim();
|
|
149
|
+
}
|
|
150
|
+
// A minimal committed Git repository, for the referenced-project ignore tests.
|
|
151
|
+
async function initGitRepo(repoDir) {
|
|
152
|
+
await mkdir(repoDir, { recursive: true });
|
|
153
|
+
await git(repoDir, ["init", "-q"]);
|
|
154
|
+
await git(repoDir, ["config", "user.name", "Paperclip Test"]);
|
|
155
|
+
await git(repoDir, ["config", "user.email", "test@paperclip.dev"]);
|
|
156
|
+
await writeFile(path.join(repoDir, "README.md"), "root\n", "utf8");
|
|
157
|
+
await git(repoDir, ["add", "README.md"]);
|
|
158
|
+
await git(repoDir, ["commit", "-qm", "base"]);
|
|
159
|
+
}
|
|
160
|
+
// A `CommandManagedRuntimeRunner` that runs real shell commands on the host
|
|
161
|
+
// filesystem (host FS stands in for the sandbox FS), exposing no native
|
|
162
|
+
// `syncIn` — staging rides the real base64/tar fallback, the same transport a
|
|
163
|
+
// provider without native sync uses. Unlike the in-file fake clients, this
|
|
164
|
+
// fallback DOES build a real tarball via `createTarballFromDirectory` for a
|
|
165
|
+
// `directory`-kind mapping, so it is the one that genuinely applies `exclude`.
|
|
166
|
+
function makeInlineSpawnRunner() {
|
|
167
|
+
return {
|
|
168
|
+
execute: (input) => new Promise((resolve) => {
|
|
169
|
+
const startedAt = new Date().toISOString();
|
|
170
|
+
const command = input.command === "sh" ? "/bin/sh" : input.command === "bash" ? "/bin/bash" : input.command;
|
|
171
|
+
const child = spawn(command, input.args ?? [], { cwd: input.cwd, env: { ...process.env, ...input.env } });
|
|
172
|
+
let stdout = "";
|
|
173
|
+
let stderr = "";
|
|
174
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
175
|
+
child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
|
|
176
|
+
child.on("error", () => resolve({ exitCode: 127, signal: null, timedOut: false, stdout, stderr, pid: null, startedAt }));
|
|
177
|
+
child.on("close", (code) => resolve({ exitCode: code ?? 0, signal: null, timedOut: false, stdout, stderr, pid: child.pid ?? null, startedAt }));
|
|
178
|
+
if (input.stdin != null)
|
|
179
|
+
child.stdin.write(input.stdin);
|
|
180
|
+
child.stdin.end();
|
|
181
|
+
}),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
// Stage ONE referenced project end-to-end (real tar fallback, real `exclude`
|
|
185
|
+
// filtering) through the full `prepareCommandManagedRuntime` seam, for the
|
|
186
|
+
// referenced-project ignore regression tests.
|
|
187
|
+
async function stageOneReferencedProject(referencedDir, ignoreResolution) {
|
|
188
|
+
const rootDir = path.dirname(referencedDir);
|
|
189
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
190
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
191
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
192
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "anchor\n", "utf8");
|
|
193
|
+
return await prepareCommandManagedRuntime({
|
|
194
|
+
runner: makeInlineSpawnRunner(),
|
|
195
|
+
spec: { remoteCwd: remoteWorkspaceDir, timeoutMs: 30_000 },
|
|
196
|
+
adapterKey: "test-adapter",
|
|
197
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
198
|
+
additionalSources: [{ localPath: referencedDir, projectId: "proj", ignoreResolution }],
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
async function listTarMembers(rootDir, name, bytes) {
|
|
202
|
+
const tarPath = path.join(rootDir, name);
|
|
203
|
+
await writeFile(tarPath, bytes);
|
|
204
|
+
const { stdout } = await execFile("tar", ["-tf", tarPath], { maxBuffer: 32 * 1024 * 1024 });
|
|
205
|
+
return stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
206
|
+
}
|
|
207
|
+
// Build a filesystem-backed managed-runtime client. The host tarball path runs
|
|
208
|
+
// unchanged; the client just materializes the mappings on the local disk, so a
|
|
209
|
+
// pack-span test needs no provider.
|
|
210
|
+
function makeFilesystemClient() {
|
|
211
|
+
const client = {
|
|
212
|
+
makeDir: async (remotePath) => {
|
|
213
|
+
await mkdir(remotePath, { recursive: true });
|
|
214
|
+
},
|
|
215
|
+
writeFile: async (remotePath, bytes) => {
|
|
216
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
217
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
218
|
+
},
|
|
219
|
+
readFile: async (remotePath) => await readFile(remotePath),
|
|
220
|
+
listFiles: async (remotePath) => {
|
|
221
|
+
const entries = await readdir(remotePath, { withFileTypes: true }).catch(() => []);
|
|
222
|
+
return entries
|
|
223
|
+
.filter((entry) => entry.isFile())
|
|
224
|
+
.map((entry) => entry.name)
|
|
225
|
+
.sort((left, right) => left.localeCompare(right));
|
|
226
|
+
},
|
|
227
|
+
remove: async (remotePath) => {
|
|
228
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
229
|
+
},
|
|
230
|
+
run: async (command) => {
|
|
231
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
attachFallbackSyncIn(client);
|
|
235
|
+
return client;
|
|
236
|
+
}
|
|
237
|
+
// A fake trace context that records every span and its parent by name. It
|
|
238
|
+
// satisfies the structural `StartupTraceContext` contract, so the real
|
|
239
|
+
// `createRuntimeSpanRunner` and `measureStartupStep` drive it unchanged. The
|
|
240
|
+
// opaque parent token is the parent's `RecordedSpan`, so a child span reads its
|
|
241
|
+
// parent name from the start context.
|
|
242
|
+
function createRecordingTraceContext() {
|
|
243
|
+
const spans = [];
|
|
244
|
+
const byHandle = new WeakMap();
|
|
245
|
+
const tracer = {
|
|
246
|
+
startSpan(name, options, context) {
|
|
247
|
+
const parent = context;
|
|
248
|
+
const record = {
|
|
249
|
+
name,
|
|
250
|
+
parentName: parent?.name ?? null,
|
|
251
|
+
ended: false,
|
|
252
|
+
attributes: { ...(options?.attributes ?? {}) },
|
|
253
|
+
};
|
|
254
|
+
spans.push(record);
|
|
255
|
+
const handle = {
|
|
256
|
+
setAttribute(key, value) {
|
|
257
|
+
record.attributes[key] = value;
|
|
258
|
+
},
|
|
259
|
+
setStatus() { },
|
|
260
|
+
end() {
|
|
261
|
+
record.ended = true;
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
byHandle.set(handle, record);
|
|
265
|
+
return handle;
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
const traceContext = {
|
|
269
|
+
tracer,
|
|
270
|
+
contextWithSpan: (span) => byHandle.get(span),
|
|
271
|
+
};
|
|
272
|
+
return { traceContext, spans };
|
|
273
|
+
}
|
|
274
|
+
describe("sandbox managed runtime", () => {
|
|
275
|
+
const cleanupDirs = [];
|
|
276
|
+
it.each(["host_current", "adopt_remote", "durable_seed"])("stages and restores both project repositories with independent Git histories (%s)", async (mode) => {
|
|
277
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "paperclip-multi-repo-"));
|
|
278
|
+
cleanupDirs.push(root);
|
|
279
|
+
const local = path.join(root, "local");
|
|
280
|
+
const remote = path.join(root, "remote");
|
|
281
|
+
const secondPath = ".paperclip-repositories/backend";
|
|
282
|
+
for (const [relative, contents] of [["", "frontend"], [secondPath, "backend"]]) {
|
|
283
|
+
const cwd = path.join(local, relative);
|
|
284
|
+
await mkdir(cwd, { recursive: true });
|
|
285
|
+
await git(cwd, ["init", "-b", "main"]);
|
|
286
|
+
await git(cwd, ["config", "user.name", "Test"]);
|
|
287
|
+
await git(cwd, ["config", "user.email", "test@example.com"]);
|
|
288
|
+
await writeFile(path.join(cwd, "README.md"), contents);
|
|
289
|
+
await writeFile(path.join(cwd, ".gitignore"), "secret.txt\n");
|
|
290
|
+
await git(cwd, ["add", "."]);
|
|
291
|
+
await git(cwd, ["commit", "-m", contents]);
|
|
292
|
+
await writeFile(path.join(cwd, "secret.txt"), "must stay local");
|
|
293
|
+
}
|
|
294
|
+
await writeFile(path.join(local, ".git/info/exclude"), ".paperclip-repositories/\n");
|
|
295
|
+
await writeFile(path.join(local, secondPath, "dirty.txt"), "local edit");
|
|
296
|
+
await writeFile(path.join(local, secondPath, "host-config.txt"), "excluded by operator");
|
|
297
|
+
const seed = { workspaceArchivePath: path.join(root, "workspace.tar"), gitArchivePath: path.join(root, "git.tar") };
|
|
298
|
+
const input = {
|
|
299
|
+
spec: { transport: "sandbox", provider: "test", sandboxId: "two-repos", remoteCwd: remote, timeoutMs: 30_000, apiKey: null },
|
|
300
|
+
client: makeFilesystemClient(), adapterKey: "test", workspaceLocalDir: local,
|
|
301
|
+
workspaceDurableSeed: seed,
|
|
302
|
+
workspaceExclude: [`${secondPath}/host-config.txt`],
|
|
303
|
+
};
|
|
304
|
+
let prepared = await prepareSandboxManagedRuntime(input);
|
|
305
|
+
if (mode !== "host_current") {
|
|
306
|
+
if (mode === "durable_seed")
|
|
307
|
+
await rm(remote, { recursive: true, force: true });
|
|
308
|
+
await writeFile(path.join(local, secondPath, "host-only.txt"), "concurrent host work");
|
|
309
|
+
prepared = await prepareSandboxManagedRuntime({
|
|
310
|
+
...input, workspaceInboundMode: mode,
|
|
311
|
+
workspaceBaseline: prepared.workspaceSyncSnapshot.baseline,
|
|
312
|
+
workspaceGitSnapshot: prepared.workspaceSyncSnapshot.gitSnapshot,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
for (const relative of ["", secondPath]) {
|
|
316
|
+
const cwd = path.join(remote, relative);
|
|
317
|
+
expect((await lstat(path.join(cwd, ".git"))).isDirectory()).toBe(true);
|
|
318
|
+
await expect(stat(path.join(cwd, "secret.txt"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
319
|
+
await writeFile(path.join(cwd, "README.md"), `updated ${relative}`);
|
|
320
|
+
await git(cwd, ["-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-am", "remote change"]);
|
|
321
|
+
}
|
|
322
|
+
expect(await readFile(path.join(remote, secondPath, "dirty.txt"), "utf8")).toBe("local edit");
|
|
323
|
+
await expect(stat(path.join(remote, secondPath, "host-config.txt"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
324
|
+
await writeFile(path.join(remote, secondPath, "host-config.txt"), "remote must not replace host config");
|
|
325
|
+
await prepared.restoreWorkspace();
|
|
326
|
+
expect(await readFile(path.join(local, secondPath, "host-config.txt"), "utf8")).toBe("excluded by operator");
|
|
327
|
+
if (mode !== "host_current")
|
|
328
|
+
expect(await readFile(path.join(local, secondPath, "host-only.txt"), "utf8")).toBe("concurrent host work");
|
|
329
|
+
for (const relative of ["", secondPath]) {
|
|
330
|
+
expect(await readFile(path.join(local, relative, "README.md"), "utf8")).toBe(`updated ${relative}`);
|
|
331
|
+
expect(await git(path.join(local, relative), ["log", "-1", "--format=%s"])).toBe("remote change");
|
|
332
|
+
expect(await readFile(path.join(local, relative, "secret.txt"), "utf8")).toBe("must stay local");
|
|
333
|
+
}
|
|
334
|
+
}, 30_000);
|
|
335
|
+
afterEach(async () => {
|
|
336
|
+
while (cleanupDirs.length > 0) {
|
|
337
|
+
const dir = cleanupDirs.pop();
|
|
338
|
+
if (!dir)
|
|
339
|
+
continue;
|
|
340
|
+
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
it("adopts a warm remote workspace without inbound overwrite and still merges outbound changes", async () => {
|
|
344
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-adopt-"));
|
|
345
|
+
cleanupDirs.push(rootDir);
|
|
346
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
347
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
348
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
349
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
350
|
+
await writeFile(path.join(localWorkspaceDir, "continuity.txt"), "host baseline\n", "utf8");
|
|
351
|
+
await writeFile(path.join(remoteWorkspaceDir, "continuity.txt"), "remote retained\n", "utf8");
|
|
352
|
+
const client = makeFilesystemClient();
|
|
353
|
+
const syncIn = vi.spyOn(client, "syncIn");
|
|
354
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
355
|
+
spec: {
|
|
356
|
+
transport: "sandbox",
|
|
357
|
+
provider: "test",
|
|
358
|
+
sandboxId: "sandbox-warm",
|
|
359
|
+
remoteCwd: remoteWorkspaceDir,
|
|
360
|
+
timeoutMs: 30_000,
|
|
361
|
+
apiKey: null,
|
|
362
|
+
},
|
|
363
|
+
adapterKey: "test-adapter",
|
|
364
|
+
client,
|
|
365
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
366
|
+
workspaceInboundMode: "adopt_remote",
|
|
367
|
+
});
|
|
368
|
+
expect(syncIn).not.toHaveBeenCalled();
|
|
369
|
+
await expect(readFile(path.join(remoteWorkspaceDir, "continuity.txt"), "utf8")).resolves.toBe("remote retained\n");
|
|
370
|
+
expect(prepared.workspaceSyncSnapshot).not.toBeNull();
|
|
371
|
+
await writeFile(path.join(remoteWorkspaceDir, "continuity.txt"), "remote finalized\n", "utf8");
|
|
372
|
+
await prepared.restoreWorkspace();
|
|
373
|
+
await expect(readFile(path.join(localWorkspaceDir, "continuity.txt"), "utf8")).resolves.toBe("remote finalized\n");
|
|
374
|
+
});
|
|
375
|
+
it("reconstructs a replacement workspace from the exact durable pre-turn seed", async () => {
|
|
376
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-durable-seed-"));
|
|
377
|
+
cleanupDirs.push(rootDir);
|
|
378
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
379
|
+
const firstRemoteDir = path.join(rootDir, "first-remote");
|
|
380
|
+
const replacementRemoteDir = path.join(rootDir, "replacement-remote");
|
|
381
|
+
const durableSeed = {
|
|
382
|
+
workspaceArchivePath: path.join(rootDir, "state", "workspace.tar"),
|
|
383
|
+
gitArchivePath: path.join(rootDir, "state", "git.tar"),
|
|
384
|
+
};
|
|
385
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
386
|
+
await writeFile(path.join(localWorkspaceDir, "continuity.txt"), "durable pre-turn bytes\n", "utf8");
|
|
387
|
+
const first = await prepareSandboxManagedRuntime({
|
|
388
|
+
spec: {
|
|
389
|
+
transport: "sandbox",
|
|
390
|
+
provider: "test",
|
|
391
|
+
sandboxId: "sandbox-first",
|
|
392
|
+
remoteCwd: firstRemoteDir,
|
|
393
|
+
timeoutMs: 30_000,
|
|
394
|
+
apiKey: null,
|
|
395
|
+
},
|
|
396
|
+
adapterKey: "test-adapter",
|
|
397
|
+
client: makeFilesystemClient(),
|
|
398
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
399
|
+
workspaceInboundMode: "host_current",
|
|
400
|
+
workspaceDurableSeed: durableSeed,
|
|
401
|
+
});
|
|
402
|
+
expect(first.workspaceSyncSnapshot).not.toBeNull();
|
|
403
|
+
await expect(stat(durableSeed.workspaceArchivePath)).resolves.toMatchObject({
|
|
404
|
+
mode: expect.any(Number),
|
|
405
|
+
});
|
|
406
|
+
await writeFile(path.join(localWorkspaceDir, "continuity.txt"), "concurrent host edit must not enter replacement\n", "utf8");
|
|
407
|
+
await prepareSandboxManagedRuntime({
|
|
408
|
+
spec: {
|
|
409
|
+
transport: "sandbox",
|
|
410
|
+
provider: "test",
|
|
411
|
+
sandboxId: "sandbox-replacement",
|
|
412
|
+
remoteCwd: replacementRemoteDir,
|
|
413
|
+
timeoutMs: 30_000,
|
|
414
|
+
apiKey: null,
|
|
415
|
+
},
|
|
416
|
+
adapterKey: "test-adapter",
|
|
417
|
+
client: makeFilesystemClient(),
|
|
418
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
419
|
+
workspaceInboundMode: "durable_seed",
|
|
420
|
+
workspaceDurableSeed: durableSeed,
|
|
421
|
+
workspaceBaseline: first.workspaceSyncSnapshot.baseline,
|
|
422
|
+
workspaceGitSnapshot: first.workspaceSyncSnapshot.gitSnapshot,
|
|
423
|
+
});
|
|
424
|
+
await expect(readFile(path.join(replacementRemoteDir, "continuity.txt"), "utf8")).resolves.toBe("durable pre-turn bytes\n");
|
|
425
|
+
});
|
|
426
|
+
it("preserves excluded local workspace artifacts during restore mirroring", async () => {
|
|
427
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-restore-"));
|
|
428
|
+
cleanupDirs.push(rootDir);
|
|
429
|
+
const sourceDir = path.join(rootDir, "source");
|
|
430
|
+
const targetDir = path.join(rootDir, "target");
|
|
431
|
+
await mkdir(path.join(sourceDir, "src"), { recursive: true });
|
|
432
|
+
await mkdir(path.join(targetDir, ".claude"), { recursive: true });
|
|
433
|
+
await mkdir(path.join(targetDir, ".paperclip-runtime"), { recursive: true });
|
|
434
|
+
await writeFile(path.join(sourceDir, "src", "app.ts"), "export const value = 2;\n", "utf8");
|
|
435
|
+
await writeFile(path.join(targetDir, "stale.txt"), "remove me\n", "utf8");
|
|
436
|
+
await writeFile(path.join(targetDir, ".claude", "settings.json"), "{\"keep\":true}\n", "utf8");
|
|
437
|
+
await writeFile(path.join(targetDir, ".claude.json"), "{\"keep\":true}\n", "utf8");
|
|
438
|
+
await writeFile(path.join(targetDir, ".paperclip-runtime", "state.json"), "{}\n", "utf8");
|
|
439
|
+
await mirrorDirectory(sourceDir, targetDir, {
|
|
440
|
+
preserveAbsent: [".paperclip-runtime", ".claude", ".claude.json"],
|
|
441
|
+
});
|
|
442
|
+
await expect(readFile(path.join(targetDir, "src", "app.ts"), "utf8")).resolves.toBe("export const value = 2;\n");
|
|
443
|
+
await expect(readFile(path.join(targetDir, ".claude", "settings.json"), "utf8")).resolves.toBe("{\"keep\":true}\n");
|
|
444
|
+
await expect(readFile(path.join(targetDir, ".claude.json"), "utf8")).resolves.toBe("{\"keep\":true}\n");
|
|
445
|
+
await expect(readFile(path.join(targetDir, ".paperclip-runtime", "state.json"), "utf8")).resolves.toBe("{}\n");
|
|
446
|
+
await expect(readFile(path.join(targetDir, "stale.txt"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
|
|
447
|
+
});
|
|
448
|
+
it("applies file mode on a staged sibling before renaming into place", async () => {
|
|
449
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-copy-mode-"));
|
|
450
|
+
cleanupDirs.push(rootDir);
|
|
451
|
+
const sourceDir = path.join(rootDir, "source");
|
|
452
|
+
const targetDir = path.join(rootDir, "target");
|
|
453
|
+
const relativePath = path.join("nested", "script.sh");
|
|
454
|
+
const sourcePath = path.join(sourceDir, relativePath);
|
|
455
|
+
const targetPath = path.join(targetDir, relativePath);
|
|
456
|
+
await mkdir(path.dirname(sourcePath), { recursive: true });
|
|
457
|
+
await writeFile(sourcePath, "#!/bin/sh\necho hello\n", { mode: 0o600 });
|
|
458
|
+
await mkdir(targetDir, { recursive: true });
|
|
459
|
+
const chmodMock = vi.mocked(fsPromises.chmod);
|
|
460
|
+
const renameMock = vi.mocked(fsPromises.rename);
|
|
461
|
+
chmodMock.mockClear();
|
|
462
|
+
renameMock.mockClear();
|
|
463
|
+
await mirrorDirectory(sourceDir, targetDir);
|
|
464
|
+
await expect(readFile(targetPath, "utf8")).resolves.toBe("#!/bin/sh\necho hello\n");
|
|
465
|
+
expect(chmodMock).toHaveBeenCalledTimes(1);
|
|
466
|
+
expect(renameMock).toHaveBeenCalledTimes(1);
|
|
467
|
+
expect(chmodMock.mock.calls[0]?.[0]).toContain(".paperclip-copy.");
|
|
468
|
+
expect(chmodMock.mock.calls[0]?.[0]).not.toBe(targetPath);
|
|
469
|
+
expect(chmodMock.mock.invocationCallOrder[0]).toBeLessThan(renameMock.mock.invocationCallOrder[0]);
|
|
470
|
+
});
|
|
471
|
+
it("cleans up a staged sibling when chmod fails before rename", async () => {
|
|
472
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-copy-cleanup-"));
|
|
473
|
+
cleanupDirs.push(rootDir);
|
|
474
|
+
const sourceDir = path.join(rootDir, "source");
|
|
475
|
+
const targetDir = path.join(rootDir, "target");
|
|
476
|
+
const relativePath = path.join("nested", "script.sh");
|
|
477
|
+
const sourcePath = path.join(sourceDir, relativePath);
|
|
478
|
+
const targetPath = path.join(targetDir, relativePath);
|
|
479
|
+
await mkdir(path.dirname(sourcePath), { recursive: true });
|
|
480
|
+
await writeFile(sourcePath, "#!/bin/sh\necho hello\n", { mode: 0o600 });
|
|
481
|
+
await mkdir(targetDir, { recursive: true });
|
|
482
|
+
const chmodMock = vi.mocked(fsPromises.chmod);
|
|
483
|
+
const renameMock = vi.mocked(fsPromises.rename);
|
|
484
|
+
chmodMock.mockClear();
|
|
485
|
+
renameMock.mockClear();
|
|
486
|
+
chmodMock.mockImplementationOnce(async () => {
|
|
487
|
+
throw new Error("chmod failed");
|
|
488
|
+
});
|
|
489
|
+
await expect(mirrorDirectory(sourceDir, targetDir)).rejects.toThrow(/chmod failed/);
|
|
490
|
+
expect(chmodMock).toHaveBeenCalledTimes(1);
|
|
491
|
+
expect(renameMock).not.toHaveBeenCalled();
|
|
492
|
+
const stagedPath = chmodMock.mock.calls[0]?.[0];
|
|
493
|
+
expect(stagedPath).toContain(".paperclip-copy.");
|
|
494
|
+
await expect(readFile(stagedPath, "utf8")).rejects.toMatchObject({ code: "ENOENT" });
|
|
495
|
+
await expect(readFile(targetPath, "utf8")).rejects.toMatchObject({ code: "ENOENT" });
|
|
496
|
+
});
|
|
497
|
+
it("syncs workspace and assets through a provider-neutral sandbox client", async () => {
|
|
498
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-managed-"));
|
|
499
|
+
cleanupDirs.push(rootDir);
|
|
500
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
501
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
502
|
+
const localAssetsDir = path.join(rootDir, "local-assets");
|
|
503
|
+
const linkedAssetPath = path.join(rootDir, "linked-skill.md");
|
|
504
|
+
await mkdir(path.join(localWorkspaceDir, ".claude"), { recursive: true });
|
|
505
|
+
await mkdir(localAssetsDir, { recursive: true });
|
|
506
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "local workspace\n", "utf8");
|
|
507
|
+
await writeFile(path.join(localWorkspaceDir, "._README.md"), "appledouble\n", "utf8");
|
|
508
|
+
await writeFile(path.join(localWorkspaceDir, ".claude", "settings.json"), "{\"local\":true}\n", "utf8");
|
|
509
|
+
await writeFile(linkedAssetPath, "skill body\n", "utf8");
|
|
510
|
+
await symlink(linkedAssetPath, path.join(localAssetsDir, "skill.md"));
|
|
511
|
+
const client = {
|
|
512
|
+
makeDir: async (remotePath) => {
|
|
513
|
+
await mkdir(remotePath, { recursive: true });
|
|
514
|
+
},
|
|
515
|
+
writeFile: async (remotePath, bytes) => {
|
|
516
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
517
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
518
|
+
},
|
|
519
|
+
readFile: async (remotePath) => await readFile(remotePath),
|
|
520
|
+
listFiles: async (remotePath) => {
|
|
521
|
+
const entries = await readdir(remotePath, { withFileTypes: true }).catch(() => []);
|
|
522
|
+
return entries
|
|
523
|
+
.filter((entry) => entry.isFile())
|
|
524
|
+
.map((entry) => entry.name)
|
|
525
|
+
.sort((left, right) => left.localeCompare(right));
|
|
526
|
+
},
|
|
527
|
+
remove: async (remotePath) => {
|
|
528
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
529
|
+
},
|
|
530
|
+
run: async (command) => {
|
|
531
|
+
await execFile("sh", ["-c", command], {
|
|
532
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
533
|
+
});
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
const runtimeStatuses = [];
|
|
537
|
+
attachFallbackSyncIn(client);
|
|
538
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
539
|
+
spec: {
|
|
540
|
+
transport: "sandbox",
|
|
541
|
+
provider: "test",
|
|
542
|
+
sandboxId: "sandbox-1",
|
|
543
|
+
remoteCwd: remoteWorkspaceDir,
|
|
544
|
+
timeoutMs: 30_000,
|
|
545
|
+
apiKey: null,
|
|
546
|
+
},
|
|
547
|
+
adapterKey: "test-adapter",
|
|
548
|
+
client,
|
|
549
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
550
|
+
workspaceExclude: [".claude"],
|
|
551
|
+
preserveAbsentOnRestore: [".claude"],
|
|
552
|
+
onRuntimeProgress: async (status) => {
|
|
553
|
+
runtimeStatuses.push(`${status.phase}:${status.message}`);
|
|
554
|
+
},
|
|
555
|
+
assets: [{
|
|
556
|
+
key: "skills",
|
|
557
|
+
localDir: localAssetsDir,
|
|
558
|
+
followSymlinks: true,
|
|
559
|
+
}],
|
|
560
|
+
});
|
|
561
|
+
await expect(readFile(path.join(remoteWorkspaceDir, "README.md"), "utf8")).resolves.toBe("local workspace\n");
|
|
562
|
+
await expect(readFile(path.join(remoteWorkspaceDir, "._README.md"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
|
|
563
|
+
await expect(readFile(path.join(remoteWorkspaceDir, ".claude", "settings.json"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
|
|
564
|
+
await expect(readFile(path.join(prepared.assetDirs.skills, "skill.md"), "utf8")).resolves.toBe("skill body\n");
|
|
565
|
+
expect((await lstat(path.join(prepared.assetDirs.skills, "skill.md"))).isFile()).toBe(true);
|
|
566
|
+
await writeFile(path.join(remoteWorkspaceDir, "README.md"), "remote workspace\n", "utf8");
|
|
567
|
+
await writeFile(path.join(remoteWorkspaceDir, "remote-only.txt"), "sync back\n", "utf8");
|
|
568
|
+
await mkdir(path.join(localWorkspaceDir, ".paperclip-runtime"), { recursive: true });
|
|
569
|
+
await writeFile(path.join(localWorkspaceDir, ".paperclip-runtime", "state.json"), "{}\n", "utf8");
|
|
570
|
+
await writeFile(path.join(localWorkspaceDir, "local-stale.txt"), "remove\n", "utf8");
|
|
571
|
+
await prepared.restoreWorkspace();
|
|
572
|
+
await expect(readFile(path.join(localWorkspaceDir, "README.md"), "utf8")).resolves.toBe("remote workspace\n");
|
|
573
|
+
await expect(readFile(path.join(localWorkspaceDir, "remote-only.txt"), "utf8")).resolves.toBe("sync back\n");
|
|
574
|
+
await expect(readFile(path.join(localWorkspaceDir, "local-stale.txt"), "utf8")).resolves.toBe("remove\n");
|
|
575
|
+
await expect(readFile(path.join(localWorkspaceDir, ".claude", "settings.json"), "utf8")).resolves.toBe("{\"local\":true}\n");
|
|
576
|
+
await expect(readFile(path.join(localWorkspaceDir, ".paperclip-runtime", "state.json"), "utf8")).resolves.toBe("{}\n");
|
|
577
|
+
expect(runtimeStatuses).toEqual(expect.arrayContaining([
|
|
578
|
+
"config_sync:Syncing workspace to environment",
|
|
579
|
+
"config_sync:Syncing runtime assets to environment",
|
|
580
|
+
"restore:Restoring workspace from environment",
|
|
581
|
+
"finalize:Finalizing workspace",
|
|
582
|
+
]));
|
|
583
|
+
expect(runtimeStatuses).toEqual(expect.arrayContaining([
|
|
584
|
+
expect.stringMatching(/^config_sync:Syncing workspace to environment: 100% \(\d+\.\d\/\d+\.\d MB\)$/),
|
|
585
|
+
expect.stringMatching(/^config_sync:Syncing skills to environment: 100% \(\d+\.\d\/\d+\.\d MB\)$/),
|
|
586
|
+
expect.stringMatching(/^restore:Restoring workspace from environment: 100% \(\d+\.\d\/\d+\.\d MB\)$/),
|
|
587
|
+
]));
|
|
588
|
+
expect(runtimeStatuses.at(-1)).toBe("finalize:Finalizing workspace");
|
|
589
|
+
});
|
|
590
|
+
it("falls back to the host-known byte count when the provider reports 0 for inbound workspace sync", async () => {
|
|
591
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-zero-report-"));
|
|
592
|
+
cleanupDirs.push(rootDir);
|
|
593
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
594
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
595
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
596
|
+
// Sizeable content, well above the 0.1 MB rounding step, so a fallback bug
|
|
597
|
+
// that stays at 0 is distinguishable from a small transfer that would
|
|
598
|
+
// still round down to "0.0 MB".
|
|
599
|
+
await writeFile(path.join(localWorkspaceDir, "large.bin"), Buffer.alloc(300 * 1024, "a"));
|
|
600
|
+
const client = makeFilesystemClient();
|
|
601
|
+
const realSyncIn = client.syncIn;
|
|
602
|
+
// Simulate a provider that under-reports: it moves the real bytes but
|
|
603
|
+
// returns 0 in `bytesTransferred`, exactly like a buggy or minimal plugin.
|
|
604
|
+
client.syncIn = async (operations) => {
|
|
605
|
+
const result = await realSyncIn(operations);
|
|
606
|
+
return {
|
|
607
|
+
operations: result.operations.map((operation) => ({ ...operation, bytesTransferred: 0 })),
|
|
608
|
+
};
|
|
609
|
+
};
|
|
610
|
+
const lines = [];
|
|
611
|
+
await prepareSandboxManagedRuntime({
|
|
612
|
+
spec: {
|
|
613
|
+
transport: "sandbox",
|
|
614
|
+
provider: "test",
|
|
615
|
+
sandboxId: "sandbox-1",
|
|
616
|
+
remoteCwd: remoteWorkspaceDir,
|
|
617
|
+
timeoutMs: 30_000,
|
|
618
|
+
apiKey: null,
|
|
619
|
+
},
|
|
620
|
+
adapterKey: "test-adapter",
|
|
621
|
+
client,
|
|
622
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
623
|
+
onProgress: (line) => { lines.push(line); },
|
|
624
|
+
});
|
|
625
|
+
await expect(readFile(path.join(remoteWorkspaceDir, "large.bin"))).resolves.toHaveLength(300 * 1024);
|
|
626
|
+
const workspaceLines = lines.filter((line) => line.includes("Syncing workspace to environment"));
|
|
627
|
+
expect(workspaceLines.length).toBeGreaterThan(0);
|
|
628
|
+
// Even though the provider reported 0, the line still shows the real,
|
|
629
|
+
// host-known workspace size, from the caller-supplied fallback.
|
|
630
|
+
expect(workspaceLines.some((line) => /\(\d+\.\d\/\d+\.\d MB\)/.test(line))).toBe(true);
|
|
631
|
+
expect(workspaceLines.some((line) => line.includes("(0.0/0.0 MB)"))).toBe(false);
|
|
632
|
+
});
|
|
633
|
+
it.each(["workspace", "git-workspace"])("rejects an asset key that collides with the reserved %s archive name", async (reservedKey) => {
|
|
634
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-asset-key-"));
|
|
635
|
+
cleanupDirs.push(rootDir);
|
|
636
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
637
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
638
|
+
const localAssetsDir = path.join(rootDir, "local-assets");
|
|
639
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
640
|
+
await mkdir(localAssetsDir, { recursive: true });
|
|
641
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace\n", "utf8");
|
|
642
|
+
const client = makeFilesystemClient();
|
|
643
|
+
await expect(prepareSandboxManagedRuntime({
|
|
644
|
+
spec: {
|
|
645
|
+
transport: "sandbox",
|
|
646
|
+
provider: "test",
|
|
647
|
+
sandboxId: "sandbox-1",
|
|
648
|
+
remoteCwd: remoteWorkspaceDir,
|
|
649
|
+
timeoutMs: 30_000,
|
|
650
|
+
apiKey: null,
|
|
651
|
+
},
|
|
652
|
+
adapterKey: "test-adapter",
|
|
653
|
+
client,
|
|
654
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
655
|
+
assets: [{ key: reservedKey, localDir: localAssetsDir }],
|
|
656
|
+
})).rejects.toThrow(/collides with a reserved runtime archive name/);
|
|
657
|
+
// The reserved-key guard fails before any workspace or asset archive is
|
|
658
|
+
// built, so nothing lands in the remote workspace directory.
|
|
659
|
+
await expect(readdir(remoteWorkspaceDir)).rejects.toMatchObject({ code: "ENOENT" });
|
|
660
|
+
});
|
|
661
|
+
it.each(["skills/nested", "skills\\nested", "..", "../escape"])("rejects an asset key that is not a simple path segment: %s", async (unsafeKey) => {
|
|
662
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-asset-key-"));
|
|
663
|
+
cleanupDirs.push(rootDir);
|
|
664
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
665
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
666
|
+
const localAssetsDir = path.join(rootDir, "local-assets");
|
|
667
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
668
|
+
await mkdir(localAssetsDir, { recursive: true });
|
|
669
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace\n", "utf8");
|
|
670
|
+
const client = makeFilesystemClient();
|
|
671
|
+
await expect(prepareSandboxManagedRuntime({
|
|
672
|
+
spec: {
|
|
673
|
+
transport: "sandbox",
|
|
674
|
+
provider: "test",
|
|
675
|
+
sandboxId: "sandbox-1",
|
|
676
|
+
remoteCwd: remoteWorkspaceDir,
|
|
677
|
+
timeoutMs: 30_000,
|
|
678
|
+
apiKey: null,
|
|
679
|
+
},
|
|
680
|
+
adapterKey: "test-adapter",
|
|
681
|
+
client,
|
|
682
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
683
|
+
assets: [{ key: unsafeKey, localDir: localAssetsDir }],
|
|
684
|
+
})).rejects.toThrow(/is not a simple path segment/);
|
|
685
|
+
});
|
|
686
|
+
it("syncs git-backed workspaces through a shallow standalone clone and keeps .git out of archives", async () => {
|
|
687
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-git-"));
|
|
688
|
+
cleanupDirs.push(rootDir);
|
|
689
|
+
const sourceRepoDir = path.join(rootDir, "source-repo");
|
|
690
|
+
const localWorkspaceDir = path.join(rootDir, "local-worktree");
|
|
691
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
692
|
+
await mkdir(sourceRepoDir, { recursive: true });
|
|
693
|
+
await git(sourceRepoDir, ["init"]);
|
|
694
|
+
await git(sourceRepoDir, ["checkout", "-b", "main"]);
|
|
695
|
+
await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
|
|
696
|
+
await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
|
|
697
|
+
await writeFile(path.join(sourceRepoDir, ".gitignore"), "node_modules/\n", "utf8");
|
|
698
|
+
await writeFile(path.join(sourceRepoDir, "tracked.txt"), "base\n", "utf8");
|
|
699
|
+
await writeFile(path.join(sourceRepoDir, "clean.txt"), "from git\n", "utf8");
|
|
700
|
+
await writeFile(path.join(sourceRepoDir, "deleted.txt"), "delete me\n", "utf8");
|
|
701
|
+
await git(sourceRepoDir, ["add", ".gitignore", "tracked.txt", "clean.txt", "deleted.txt"]);
|
|
702
|
+
await git(sourceRepoDir, ["commit", "-m", "base"]);
|
|
703
|
+
await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
|
|
704
|
+
expect((await lstat(path.join(localWorkspaceDir, ".git"))).isFile()).toBe(true);
|
|
705
|
+
await mkdir(path.join(localWorkspaceDir, "node_modules"), { recursive: true });
|
|
706
|
+
await writeFile(path.join(localWorkspaceDir, "tracked.txt"), "dirty local\n", "utf8");
|
|
707
|
+
await writeFile(path.join(localWorkspaceDir, "untracked.txt"), "from local\n", "utf8");
|
|
708
|
+
await writeFile(path.join(localWorkspaceDir, "node_modules", "cache.bin"), "do not upload\n", "utf8");
|
|
709
|
+
await rm(path.join(localWorkspaceDir, "deleted.txt"));
|
|
710
|
+
const uploadedTars = [];
|
|
711
|
+
const downloadedTars = [];
|
|
712
|
+
const driveProgress = async (total, onProgress) => {
|
|
713
|
+
if (!onProgress)
|
|
714
|
+
return;
|
|
715
|
+
await onProgress(Math.max(1, Math.floor(total / 2)), total);
|
|
716
|
+
await onProgress(total, total);
|
|
717
|
+
};
|
|
718
|
+
const client = {
|
|
719
|
+
makeDir: async (remotePath) => {
|
|
720
|
+
await mkdir(remotePath, { recursive: true });
|
|
721
|
+
},
|
|
722
|
+
writeFile: async (remotePath, bytes, options) => {
|
|
723
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
724
|
+
const buffer = Buffer.from(bytes);
|
|
725
|
+
if (remotePath.endsWith("-upload.tar"))
|
|
726
|
+
uploadedTars.push({ remotePath, bytes: buffer });
|
|
727
|
+
await writeFile(remotePath, buffer);
|
|
728
|
+
await driveProgress(buffer.byteLength, options?.onProgress);
|
|
729
|
+
},
|
|
730
|
+
readFile: async (remotePath, options) => {
|
|
731
|
+
const buffer = await readFile(remotePath);
|
|
732
|
+
if (remotePath.endsWith("workspace-download.tar"))
|
|
733
|
+
downloadedTars.push({ remotePath, bytes: buffer });
|
|
734
|
+
await driveProgress(buffer.byteLength, options?.onProgress);
|
|
735
|
+
return buffer;
|
|
736
|
+
},
|
|
737
|
+
listFiles: async () => [],
|
|
738
|
+
remove: async (remotePath) => {
|
|
739
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
740
|
+
},
|
|
741
|
+
run: async (command) => {
|
|
742
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
743
|
+
},
|
|
744
|
+
};
|
|
745
|
+
const runtimeStatuses = [];
|
|
746
|
+
attachFallbackSyncIn(client);
|
|
747
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
748
|
+
spec: {
|
|
749
|
+
transport: "sandbox",
|
|
750
|
+
provider: "test",
|
|
751
|
+
sandboxId: "sandbox-1",
|
|
752
|
+
remoteCwd: remoteWorkspaceDir,
|
|
753
|
+
timeoutMs: 30_000,
|
|
754
|
+
apiKey: null,
|
|
755
|
+
},
|
|
756
|
+
adapterKey: "test-adapter",
|
|
757
|
+
client,
|
|
758
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
759
|
+
onRuntimeProgress: async (status) => {
|
|
760
|
+
runtimeStatuses.push({ phase: status.phase, message: status.message });
|
|
761
|
+
},
|
|
762
|
+
});
|
|
763
|
+
expect((await lstat(path.join(remoteWorkspaceDir, ".git"))).isDirectory()).toBe(true);
|
|
764
|
+
await expect(readFile(path.join(remoteWorkspaceDir, ".git", "shallow"), "utf8")).resolves.toContain(await git(localWorkspaceDir, ["rev-parse", "HEAD"]));
|
|
765
|
+
expect(await git(remoteWorkspaceDir, ["rev-list", "--count", "HEAD"])).toBe("1");
|
|
766
|
+
expect(await git(remoteWorkspaceDir, ["status", "--short"])).toContain("M tracked.txt");
|
|
767
|
+
expect(await git(remoteWorkspaceDir, ["status", "--short"])).toContain("?? untracked.txt");
|
|
768
|
+
await expect(readFile(path.join(remoteWorkspaceDir, "deleted.txt"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
|
|
769
|
+
const gitUpload = uploadedTars.find((entry) => path.posix.basename(entry.remotePath) === "git-workspace-upload.tar");
|
|
770
|
+
const workspaceUpload = uploadedTars.find((entry) => path.posix.basename(entry.remotePath) === "workspace-upload.tar");
|
|
771
|
+
expect(gitUpload).toBeDefined();
|
|
772
|
+
expect(workspaceUpload).toBeDefined();
|
|
773
|
+
const gitMembers = await listTarMembers(rootDir, "git-upload-list.tar", gitUpload.bytes);
|
|
774
|
+
const workspaceMembers = await listTarMembers(rootDir, "workspace-upload-list.tar", workspaceUpload.bytes);
|
|
775
|
+
expect(gitMembers.some((entry) => entry === ".git" || entry.startsWith(".git/"))).toBe(true);
|
|
776
|
+
expect(workspaceMembers.some((entry) => entry === ".git" || entry.startsWith(".git/"))).toBe(false);
|
|
777
|
+
expect(workspaceMembers).toContain("tracked.txt");
|
|
778
|
+
expect(workspaceMembers).toContain("untracked.txt");
|
|
779
|
+
expect(workspaceMembers).not.toContain("clean.txt");
|
|
780
|
+
expect(workspaceMembers.some((entry) => entry === "node_modules" || entry.startsWith("node_modules/"))).toBe(false);
|
|
781
|
+
await git(remoteWorkspaceDir, ["config", "user.name", "Paperclip Sandbox"]);
|
|
782
|
+
await git(remoteWorkspaceDir, ["config", "user.email", "sandbox@paperclip.dev"]);
|
|
783
|
+
await git(remoteWorkspaceDir, ["add", "-A"]);
|
|
784
|
+
await git(remoteWorkspaceDir, ["commit", "-m", "sandbox update"]);
|
|
785
|
+
await writeFile(path.join(remoteWorkspaceDir, "tracked.txt"), "remote dirty\n", "utf8");
|
|
786
|
+
await writeFile(path.join(remoteWorkspaceDir, "remote-only.txt"), "from sandbox\n", "utf8");
|
|
787
|
+
await prepared.restoreWorkspace();
|
|
788
|
+
expect((await lstat(path.join(localWorkspaceDir, ".git"))).isFile()).toBe(true);
|
|
789
|
+
expect(await git(localWorkspaceDir, ["log", "-1", "--pretty=%s"])).toBe("sandbox update");
|
|
790
|
+
await expect(readFile(path.join(localWorkspaceDir, "tracked.txt"), "utf8")).resolves.toBe("remote dirty\n");
|
|
791
|
+
await expect(readFile(path.join(localWorkspaceDir, "remote-only.txt"), "utf8")).resolves.toBe("from sandbox\n");
|
|
792
|
+
await expect(readFile(path.join(localWorkspaceDir, "deleted.txt"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
|
|
793
|
+
await expect(readFile(path.join(localWorkspaceDir, "node_modules", "cache.bin"), "utf8")).resolves.toBe("do not upload\n");
|
|
794
|
+
expect(downloadedTars).toHaveLength(1);
|
|
795
|
+
const downloadMembers = await listTarMembers(rootDir, "workspace-download-list.tar", downloadedTars[0].bytes);
|
|
796
|
+
expect(downloadMembers.some((entry) => entry === ".git" || entry.startsWith(".git/"))).toBe(false);
|
|
797
|
+
expect(downloadMembers.some((entry) => entry === "node_modules" || entry.startsWith("node_modules/"))).toBe(false);
|
|
798
|
+
expect(runtimeStatuses.map((status) => status.phase)).toEqual(expect.arrayContaining([
|
|
799
|
+
"git_sync",
|
|
800
|
+
"config_sync",
|
|
801
|
+
"export",
|
|
802
|
+
"restore",
|
|
803
|
+
"finalize",
|
|
804
|
+
]));
|
|
805
|
+
// Git history and workspace overlay sync as ONE merged operation, so a single
|
|
806
|
+
// transfer-progress event rides the config_sync (workspace) phase. The git_sync
|
|
807
|
+
// phase still emits its plain status message (asserted by the arrayContaining
|
|
808
|
+
// check above).
|
|
809
|
+
expect(runtimeStatuses.some((status) => (status.phase === "config_sync" &&
|
|
810
|
+
/^Syncing workspace to environment: 100% \(\d+\.\d\/\d+\.\d MB\)$/.test(status.message)))).toBe(true);
|
|
811
|
+
expect(runtimeStatuses.some((status) => (status.phase === "export" &&
|
|
812
|
+
/^Exporting git history from environment: 100% \(\d+\.\d\/\d+\.\d MB\)$/.test(status.message)))).toBe(true);
|
|
813
|
+
});
|
|
814
|
+
it("repairs stale host index deletions when the sandbox restores a clean git worktree", async () => {
|
|
815
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-clean-restore-"));
|
|
816
|
+
cleanupDirs.push(rootDir);
|
|
817
|
+
const sourceRepoDir = path.join(rootDir, "source-repo");
|
|
818
|
+
const localWorkspaceDir = path.join(rootDir, "local-worktree");
|
|
819
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
820
|
+
await mkdir(sourceRepoDir, { recursive: true });
|
|
821
|
+
await git(sourceRepoDir, ["init"]);
|
|
822
|
+
await git(sourceRepoDir, ["checkout", "-b", "main"]);
|
|
823
|
+
await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
|
|
824
|
+
await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
|
|
825
|
+
await writeFile(path.join(sourceRepoDir, "kept.txt"), "kept\n", "utf8");
|
|
826
|
+
await writeFile(path.join(sourceRepoDir, "restored.txt"), "restored\n", "utf8");
|
|
827
|
+
await git(sourceRepoDir, ["add", "kept.txt", "restored.txt"]);
|
|
828
|
+
await git(sourceRepoDir, ["commit", "-m", "base"]);
|
|
829
|
+
await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
|
|
830
|
+
await git(localWorkspaceDir, ["rm", "restored.txt"]);
|
|
831
|
+
expect(await git(localWorkspaceDir, ["status", "--short"])).toContain("D restored.txt");
|
|
832
|
+
const missingStatusReads = [];
|
|
833
|
+
const client = {
|
|
834
|
+
makeDir: async (remotePath) => {
|
|
835
|
+
await mkdir(remotePath, { recursive: true });
|
|
836
|
+
},
|
|
837
|
+
writeFile: async (remotePath, bytes) => {
|
|
838
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
839
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
840
|
+
},
|
|
841
|
+
readFile: async (remotePath) => {
|
|
842
|
+
if (remotePath.endsWith("workspace-status.txt")) {
|
|
843
|
+
missingStatusReads.push(remotePath);
|
|
844
|
+
throw new Error("status file unavailable");
|
|
845
|
+
}
|
|
846
|
+
return await readFile(remotePath);
|
|
847
|
+
},
|
|
848
|
+
listFiles: async () => [],
|
|
849
|
+
remove: async (remotePath) => {
|
|
850
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
851
|
+
},
|
|
852
|
+
run: async (command) => {
|
|
853
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
854
|
+
},
|
|
855
|
+
};
|
|
856
|
+
attachFallbackSyncIn(client);
|
|
857
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
858
|
+
spec: {
|
|
859
|
+
transport: "sandbox",
|
|
860
|
+
provider: "test",
|
|
861
|
+
sandboxId: "sandbox-1",
|
|
862
|
+
remoteCwd: remoteWorkspaceDir,
|
|
863
|
+
timeoutMs: 30_000,
|
|
864
|
+
apiKey: null,
|
|
865
|
+
},
|
|
866
|
+
adapterKey: "test-adapter",
|
|
867
|
+
client,
|
|
868
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
869
|
+
});
|
|
870
|
+
expect(await git(remoteWorkspaceDir, ["status", "--short"])).toContain("D restored.txt");
|
|
871
|
+
await git(remoteWorkspaceDir, ["reset", "--hard", "HEAD"]);
|
|
872
|
+
expect(await git(remoteWorkspaceDir, ["status", "--short"])).toBe("");
|
|
873
|
+
await prepared.restoreWorkspace();
|
|
874
|
+
await expect(readFile(path.join(localWorkspaceDir, "restored.txt"), "utf8")).resolves.toBe("restored\n");
|
|
875
|
+
expect(await git(localWorkspaceDir, ["ls-files", "restored.txt"])).toBe("restored.txt");
|
|
876
|
+
expect(await git(localWorkspaceDir, ["status", "--short"])).toBe("");
|
|
877
|
+
expect(await git(localWorkspaceDir, ["diff", "--name-status", "HEAD", "--"])).toBe("");
|
|
878
|
+
expect(await git(localWorkspaceDir, ["diff", "--cached", "--name-status", "HEAD", "--"])).toBe("");
|
|
879
|
+
expect(missingStatusReads).toHaveLength(1);
|
|
880
|
+
});
|
|
881
|
+
it("does not fail clean restore checks when local working tree changes survive", async () => {
|
|
882
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-preserved-local-"));
|
|
883
|
+
cleanupDirs.push(rootDir);
|
|
884
|
+
const sourceRepoDir = path.join(rootDir, "source-repo");
|
|
885
|
+
const localWorkspaceDir = path.join(rootDir, "local-worktree");
|
|
886
|
+
await mkdir(sourceRepoDir, { recursive: true });
|
|
887
|
+
await git(sourceRepoDir, ["init"]);
|
|
888
|
+
await git(sourceRepoDir, ["checkout", "-b", "main"]);
|
|
889
|
+
await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
|
|
890
|
+
await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
|
|
891
|
+
await writeFile(path.join(sourceRepoDir, "kept.txt"), "base\n", "utf8");
|
|
892
|
+
await git(sourceRepoDir, ["add", "kept.txt"]);
|
|
893
|
+
await git(sourceRepoDir, ["commit", "-m", "base"]);
|
|
894
|
+
await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
|
|
895
|
+
await writeFile(path.join(localWorkspaceDir, "kept.txt"), "local user change\n", "utf8");
|
|
896
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
897
|
+
try {
|
|
898
|
+
await resetLocalGitIndexToHead({
|
|
899
|
+
localDir: localWorkspaceDir,
|
|
900
|
+
checkWorkingTreeClean: true,
|
|
901
|
+
});
|
|
902
|
+
expect(warnSpy).toHaveBeenCalledWith("[paperclip] Workspace restore preserved local working tree changes after clean sandbox restore.");
|
|
903
|
+
}
|
|
904
|
+
finally {
|
|
905
|
+
warnSpy.mockRestore();
|
|
906
|
+
}
|
|
907
|
+
await expect(readFile(path.join(localWorkspaceDir, "kept.txt"), "utf8")).resolves.toBe("local user change\n");
|
|
908
|
+
expect(await git(localWorkspaceDir, ["diff", "--cached", "--name-status", "HEAD", "--"])).toBe("");
|
|
909
|
+
expect(await git(localWorkspaceDir, ["status", "--short"])).toContain("M kept.txt");
|
|
910
|
+
});
|
|
911
|
+
it("excludes unignored dependency trees from git-backed workspace overlay archives", async () => {
|
|
912
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-unignored-deps-"));
|
|
913
|
+
cleanupDirs.push(rootDir);
|
|
914
|
+
const sourceRepoDir = path.join(rootDir, "source-repo");
|
|
915
|
+
const localWorkspaceDir = path.join(rootDir, "local-worktree");
|
|
916
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
917
|
+
await mkdir(sourceRepoDir, { recursive: true });
|
|
918
|
+
await git(sourceRepoDir, ["init"]);
|
|
919
|
+
await git(sourceRepoDir, ["checkout", "-b", "main"]);
|
|
920
|
+
await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
|
|
921
|
+
await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
|
|
922
|
+
await mkdir(path.join(sourceRepoDir, "src"), { recursive: true });
|
|
923
|
+
await writeFile(path.join(sourceRepoDir, "src", "tracked.ts"), "export const tracked = true;\n", "utf8");
|
|
924
|
+
await git(sourceRepoDir, ["add", "src/tracked.ts"]);
|
|
925
|
+
await git(sourceRepoDir, ["commit", "-m", "base"]);
|
|
926
|
+
await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
|
|
927
|
+
await mkdir(path.join(localWorkspaceDir, "node_modules", "root-package"), { recursive: true });
|
|
928
|
+
await mkdir(path.join(localWorkspaceDir, "packages", "ui", "node_modules", "nested-package"), { recursive: true });
|
|
929
|
+
await writeFile(path.join(localWorkspaceDir, "node_modules", "root-package", "cache.bin"), "root dependency\n", "utf8");
|
|
930
|
+
await writeFile(path.join(localWorkspaceDir, "packages", "ui", "node_modules", "nested-package", "cache.bin"), "nested dependency\n", "utf8");
|
|
931
|
+
await writeFile(path.join(localWorkspaceDir, "src", "local-only.ts"), "export const local = true;\n", "utf8");
|
|
932
|
+
const uploadedTars = [];
|
|
933
|
+
const downloadedTars = [];
|
|
934
|
+
const client = {
|
|
935
|
+
makeDir: async (remotePath) => {
|
|
936
|
+
await mkdir(remotePath, { recursive: true });
|
|
937
|
+
},
|
|
938
|
+
writeFile: async (remotePath, bytes) => {
|
|
939
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
940
|
+
const buffer = Buffer.from(bytes);
|
|
941
|
+
if (remotePath.endsWith("-upload.tar"))
|
|
942
|
+
uploadedTars.push({ remotePath, bytes: buffer });
|
|
943
|
+
await writeFile(remotePath, buffer);
|
|
944
|
+
},
|
|
945
|
+
readFile: async (remotePath) => {
|
|
946
|
+
const buffer = await readFile(remotePath);
|
|
947
|
+
if (remotePath.endsWith("workspace-download.tar"))
|
|
948
|
+
downloadedTars.push({ remotePath, bytes: buffer });
|
|
949
|
+
return buffer;
|
|
950
|
+
},
|
|
951
|
+
listFiles: async () => [],
|
|
952
|
+
remove: async (remotePath) => {
|
|
953
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
954
|
+
},
|
|
955
|
+
run: async (command) => {
|
|
956
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
957
|
+
},
|
|
958
|
+
};
|
|
959
|
+
attachFallbackSyncIn(client);
|
|
960
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
961
|
+
spec: {
|
|
962
|
+
transport: "sandbox",
|
|
963
|
+
provider: "test",
|
|
964
|
+
sandboxId: "sandbox-1",
|
|
965
|
+
remoteCwd: remoteWorkspaceDir,
|
|
966
|
+
timeoutMs: 30_000,
|
|
967
|
+
apiKey: null,
|
|
968
|
+
},
|
|
969
|
+
adapterKey: "test-adapter",
|
|
970
|
+
client,
|
|
971
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
972
|
+
});
|
|
973
|
+
const workspaceUpload = uploadedTars.find((entry) => path.posix.basename(entry.remotePath) === "workspace-upload.tar");
|
|
974
|
+
expect(workspaceUpload).toBeDefined();
|
|
975
|
+
const workspaceMembers = await listTarMembers(rootDir, "unignored-deps-workspace-upload.tar", workspaceUpload.bytes);
|
|
976
|
+
expect(workspaceMembers).toContain("src/local-only.ts");
|
|
977
|
+
expect(workspaceMembers.some((entry) => entry === "node_modules" || entry.startsWith("node_modules/"))).toBe(false);
|
|
978
|
+
expect(workspaceMembers.some((entry) => entry.includes("/node_modules/") || entry.endsWith("/node_modules"))).toBe(false);
|
|
979
|
+
await expect(readFile(path.join(remoteWorkspaceDir, "src", "local-only.ts"), "utf8")).resolves.toBe("export const local = true;\n");
|
|
980
|
+
await expect(readFile(path.join(remoteWorkspaceDir, "node_modules", "root-package", "cache.bin"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
|
|
981
|
+
await expect(readFile(path.join(remoteWorkspaceDir, "packages", "ui", "node_modules", "nested-package", "cache.bin"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
|
|
982
|
+
await mkdir(path.join(remoteWorkspaceDir, "node_modules", "sandbox-package"), { recursive: true });
|
|
983
|
+
await mkdir(path.join(remoteWorkspaceDir, "packages", "ui", "node_modules", "sandbox-package"), { recursive: true });
|
|
984
|
+
await writeFile(path.join(remoteWorkspaceDir, "node_modules", "sandbox-package", "cache.bin"), "sandbox root dependency\n", "utf8");
|
|
985
|
+
await writeFile(path.join(remoteWorkspaceDir, "packages", "ui", "node_modules", "sandbox-package", "cache.bin"), "sandbox nested dependency\n", "utf8");
|
|
986
|
+
await writeFile(path.join(remoteWorkspaceDir, "src", "remote-only.ts"), "export const remote = true;\n", "utf8");
|
|
987
|
+
await prepared.restoreWorkspace();
|
|
988
|
+
await expect(readFile(path.join(localWorkspaceDir, "node_modules", "root-package", "cache.bin"), "utf8")).resolves.toBe("root dependency\n");
|
|
989
|
+
await expect(readFile(path.join(localWorkspaceDir, "packages", "ui", "node_modules", "nested-package", "cache.bin"), "utf8")).resolves.toBe("nested dependency\n");
|
|
990
|
+
await expect(readFile(path.join(localWorkspaceDir, "src", "remote-only.ts"), "utf8")).resolves.toBe("export const remote = true;\n");
|
|
991
|
+
await expect(readFile(path.join(localWorkspaceDir, "node_modules", "sandbox-package", "cache.bin"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
|
|
992
|
+
expect(downloadedTars).toHaveLength(1);
|
|
993
|
+
const downloadMembers = await listTarMembers(rootDir, "unignored-deps-workspace-download.tar", downloadedTars[0].bytes);
|
|
994
|
+
expect(downloadMembers.some((entry) => entry === ".git" || entry.startsWith(".git/"))).toBe(false);
|
|
995
|
+
expect(downloadMembers.some((entry) => entry === "node_modules" || entry.startsWith("node_modules/"))).toBe(false);
|
|
996
|
+
expect(downloadMembers.some((entry) => entry.includes("/node_modules/") || entry.endsWith("/node_modules"))).toBe(false);
|
|
997
|
+
});
|
|
998
|
+
it("excludes an anchor-workspace ignored file whose name has leading and trailing whitespace from the staged tree", async () => {
|
|
999
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-ignored-whitespace-"));
|
|
1000
|
+
cleanupDirs.push(rootDir);
|
|
1001
|
+
const workspaceLocalDir = path.join(rootDir, "workspace");
|
|
1002
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
1003
|
+
await initGitRepo(workspaceLocalDir);
|
|
1004
|
+
// A double-wildcard pattern avoids the separate rule that Git trims an
|
|
1005
|
+
// unescaped trailing space in a .gitignore PATTERN itself; the padding
|
|
1006
|
+
// under test lives in the matched FILE name, proving the anchor `splitNul`
|
|
1007
|
+
// parser keeps it instead of trimming it away and missing the exclude.
|
|
1008
|
+
const ignoredName = " ignored padded ";
|
|
1009
|
+
await writeFile(path.join(workspaceLocalDir, ".gitignore"), "*ignored*padded*\n", "utf8");
|
|
1010
|
+
await writeFile(path.join(workspaceLocalDir, ignoredName), "TOKEN=abc\n", "utf8");
|
|
1011
|
+
await writeFile(path.join(workspaceLocalDir, "kept.txt"), "kept\n", "utf8");
|
|
1012
|
+
const uploadedTars = [];
|
|
1013
|
+
const client = {
|
|
1014
|
+
makeDir: async (remotePath) => {
|
|
1015
|
+
await mkdir(remotePath, { recursive: true });
|
|
1016
|
+
},
|
|
1017
|
+
writeFile: async (remotePath, bytes) => {
|
|
1018
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
1019
|
+
const buffer = Buffer.from(bytes);
|
|
1020
|
+
if (remotePath.endsWith("-upload.tar"))
|
|
1021
|
+
uploadedTars.push({ remotePath, bytes: buffer });
|
|
1022
|
+
await writeFile(remotePath, buffer);
|
|
1023
|
+
},
|
|
1024
|
+
readFile: async (remotePath) => await readFile(remotePath),
|
|
1025
|
+
listFiles: async () => [],
|
|
1026
|
+
remove: async (remotePath) => {
|
|
1027
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
1028
|
+
},
|
|
1029
|
+
run: async (command) => {
|
|
1030
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
1031
|
+
},
|
|
1032
|
+
};
|
|
1033
|
+
attachFallbackSyncIn(client);
|
|
1034
|
+
await prepareSandboxManagedRuntime({
|
|
1035
|
+
spec: {
|
|
1036
|
+
transport: "sandbox",
|
|
1037
|
+
provider: "test",
|
|
1038
|
+
sandboxId: "sandbox-1",
|
|
1039
|
+
remoteCwd: remoteWorkspaceDir,
|
|
1040
|
+
timeoutMs: 30_000,
|
|
1041
|
+
apiKey: null,
|
|
1042
|
+
},
|
|
1043
|
+
adapterKey: "test-adapter",
|
|
1044
|
+
client,
|
|
1045
|
+
workspaceLocalDir,
|
|
1046
|
+
});
|
|
1047
|
+
const workspaceUpload = uploadedTars.find((entry) => path.posix.basename(entry.remotePath) === "workspace-upload.tar");
|
|
1048
|
+
expect(workspaceUpload).toBeDefined();
|
|
1049
|
+
const members = await listTarMembers(rootDir, "ignored-whitespace-workspace-upload.tar", workspaceUpload.bytes);
|
|
1050
|
+
expect(members).not.toContain(ignoredName);
|
|
1051
|
+
expect(members).toContain("kept.txt");
|
|
1052
|
+
});
|
|
1053
|
+
it("builds workspace/asset tarballs without a './' self-entry (so untar does not chmod/utime an unowned target dir)", async () => {
|
|
1054
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-tarself-"));
|
|
1055
|
+
cleanupDirs.push(rootDir);
|
|
1056
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
1057
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
1058
|
+
const localAssetsDir = path.join(rootDir, "local-assets");
|
|
1059
|
+
await mkdir(path.join(localWorkspaceDir, "src"), { recursive: true });
|
|
1060
|
+
await mkdir(localAssetsDir, { recursive: true });
|
|
1061
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "ws\n", "utf8");
|
|
1062
|
+
await writeFile(path.join(localWorkspaceDir, "src", "main.ts"), "x\n", "utf8");
|
|
1063
|
+
await writeFile(path.join(localAssetsDir, "asset.txt"), "a\n", "utf8");
|
|
1064
|
+
// Capture every tar uploaded/downloaded through the sandbox so we can inspect its members.
|
|
1065
|
+
const uploadedTars = [];
|
|
1066
|
+
const downloadedTars = [];
|
|
1067
|
+
const client = {
|
|
1068
|
+
makeDir: async (remotePath) => {
|
|
1069
|
+
await mkdir(remotePath, { recursive: true });
|
|
1070
|
+
},
|
|
1071
|
+
writeFile: async (remotePath, bytes) => {
|
|
1072
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
1073
|
+
const buffer = Buffer.from(bytes);
|
|
1074
|
+
if (remotePath.endsWith("-upload.tar"))
|
|
1075
|
+
uploadedTars.push({ remotePath, bytes: buffer });
|
|
1076
|
+
await writeFile(remotePath, buffer);
|
|
1077
|
+
},
|
|
1078
|
+
readFile: async (remotePath) => {
|
|
1079
|
+
const buffer = await readFile(remotePath);
|
|
1080
|
+
if (remotePath.endsWith("workspace-download.tar"))
|
|
1081
|
+
downloadedTars.push({ remotePath, bytes: buffer });
|
|
1082
|
+
return buffer;
|
|
1083
|
+
},
|
|
1084
|
+
listFiles: async () => [],
|
|
1085
|
+
remove: async (remotePath) => {
|
|
1086
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
1087
|
+
},
|
|
1088
|
+
run: async (command) => {
|
|
1089
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
1090
|
+
},
|
|
1091
|
+
};
|
|
1092
|
+
attachFallbackSyncIn(client);
|
|
1093
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
1094
|
+
spec: {
|
|
1095
|
+
transport: "sandbox",
|
|
1096
|
+
provider: "test",
|
|
1097
|
+
sandboxId: "sandbox-1",
|
|
1098
|
+
remoteCwd: remoteWorkspaceDir,
|
|
1099
|
+
timeoutMs: 30_000,
|
|
1100
|
+
apiKey: null,
|
|
1101
|
+
},
|
|
1102
|
+
adapterKey: "test-adapter",
|
|
1103
|
+
client,
|
|
1104
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
1105
|
+
assets: [{ key: "skills", localDir: localAssetsDir }],
|
|
1106
|
+
});
|
|
1107
|
+
expect(uploadedTars.length).toBeGreaterThanOrEqual(2);
|
|
1108
|
+
for (const { remotePath, bytes } of uploadedTars) {
|
|
1109
|
+
const listPath = path.join(rootDir, `list-${path.basename(remotePath)}`);
|
|
1110
|
+
await writeFile(listPath, bytes);
|
|
1111
|
+
const { stdout } = await execFile("tar", ["-tf", listPath], { maxBuffer: 32 * 1024 * 1024 });
|
|
1112
|
+
const members = stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1113
|
+
// The archive must NOT contain a self-entry for the root directory; that is
|
|
1114
|
+
// what makes tar try to mutate the (possibly unowned) extraction target.
|
|
1115
|
+
expect(members).not.toContain(".");
|
|
1116
|
+
expect(members).not.toContain("./");
|
|
1117
|
+
}
|
|
1118
|
+
// And the workspace still extracts correctly into an existing target dir.
|
|
1119
|
+
await expect(readFile(path.join(remoteWorkspaceDir, "README.md"), "utf8")).resolves.toBe("ws\n");
|
|
1120
|
+
await expect(readFile(path.join(remoteWorkspaceDir, "src", "main.ts"), "utf8")).resolves.toBe("x\n");
|
|
1121
|
+
await prepared.restoreWorkspace();
|
|
1122
|
+
expect(downloadedTars).toHaveLength(1);
|
|
1123
|
+
const downloadMembers = await listTarMembers(rootDir, "workspace-download-list.tar", downloadedTars[0].bytes);
|
|
1124
|
+
expect(downloadMembers).not.toContain(".");
|
|
1125
|
+
expect(downloadMembers).not.toContain("./");
|
|
1126
|
+
});
|
|
1127
|
+
it("excludes transient symlinked home dirs from the asset tar while keeping required content", async () => {
|
|
1128
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-home-tmp-"));
|
|
1129
|
+
cleanupDirs.push(rootDir);
|
|
1130
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
1131
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
1132
|
+
const homeDir = path.join(rootDir, "codex-home");
|
|
1133
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
1134
|
+
// Simulate a host Codex binary that a stale `tmp/arg0` symlink points at.
|
|
1135
|
+
// With followSymlinks the archive would otherwise inline this whole file.
|
|
1136
|
+
const hostBinary = path.join(rootDir, "codex-host-binary");
|
|
1137
|
+
const binaryMarker = "HOST_CODEX_BINARY_BYTES";
|
|
1138
|
+
await writeFile(hostBinary, `${binaryMarker}\n`.repeat(4096), "utf8");
|
|
1139
|
+
// Required managed-home content that MUST still reach the sandbox.
|
|
1140
|
+
await mkdir(path.join(homeDir, "skills"), { recursive: true });
|
|
1141
|
+
await writeFile(path.join(homeDir, "auth.json"), "{\"OPENAI_API_KEY\":\"sk-test\"}\n", "utf8");
|
|
1142
|
+
await writeFile(path.join(homeDir, "config.toml"), "model = \"gpt\"\n", "utf8");
|
|
1143
|
+
await writeFile(path.join(homeDir, "skills", "demo.md"), "skill body\n", "utf8");
|
|
1144
|
+
// Transient dirs holding symlinks to the host binary (the bloat source).
|
|
1145
|
+
await mkdir(path.join(homeDir, "tmp", "arg0"), { recursive: true });
|
|
1146
|
+
await mkdir(path.join(homeDir, ".tmp"), { recursive: true });
|
|
1147
|
+
await symlink(hostBinary, path.join(homeDir, "tmp", "arg0", "codex"));
|
|
1148
|
+
await symlink(hostBinary, path.join(homeDir, ".tmp", "codex"));
|
|
1149
|
+
const uploadedTars = [];
|
|
1150
|
+
const client = {
|
|
1151
|
+
makeDir: async (remotePath) => {
|
|
1152
|
+
await mkdir(remotePath, { recursive: true });
|
|
1153
|
+
},
|
|
1154
|
+
writeFile: async (remotePath, bytes) => {
|
|
1155
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
1156
|
+
const buffer = Buffer.from(bytes);
|
|
1157
|
+
if (remotePath.endsWith("-upload.tar"))
|
|
1158
|
+
uploadedTars.push({ remotePath, bytes: buffer });
|
|
1159
|
+
await writeFile(remotePath, buffer);
|
|
1160
|
+
},
|
|
1161
|
+
readFile: async (remotePath) => await readFile(remotePath),
|
|
1162
|
+
listFiles: async () => [],
|
|
1163
|
+
remove: async (remotePath) => {
|
|
1164
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
1165
|
+
},
|
|
1166
|
+
run: async (command) => {
|
|
1167
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
1168
|
+
},
|
|
1169
|
+
};
|
|
1170
|
+
attachFallbackSyncIn(client);
|
|
1171
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
1172
|
+
spec: {
|
|
1173
|
+
transport: "sandbox",
|
|
1174
|
+
provider: "test",
|
|
1175
|
+
sandboxId: "sandbox-1",
|
|
1176
|
+
remoteCwd: remoteWorkspaceDir,
|
|
1177
|
+
timeoutMs: 30_000,
|
|
1178
|
+
apiKey: null,
|
|
1179
|
+
},
|
|
1180
|
+
adapterKey: "codex",
|
|
1181
|
+
client,
|
|
1182
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
1183
|
+
assets: [{
|
|
1184
|
+
key: "home",
|
|
1185
|
+
localDir: homeDir,
|
|
1186
|
+
followSymlinks: true,
|
|
1187
|
+
exclude: ["tmp", ".tmp"],
|
|
1188
|
+
}],
|
|
1189
|
+
});
|
|
1190
|
+
const homeTar = uploadedTars.find(({ remotePath }) => path.basename(remotePath) === "home-upload.tar");
|
|
1191
|
+
expect(homeTar).toBeDefined();
|
|
1192
|
+
const members = await listTarMembers(rootDir, "home-members.tar", homeTar.bytes);
|
|
1193
|
+
// Transient symlink trees must be filtered out entirely.
|
|
1194
|
+
expect(members.some((entry) => entry === "tmp" || entry.startsWith("tmp/"))).toBe(false);
|
|
1195
|
+
expect(members.some((entry) => entry === ".tmp" || entry.startsWith(".tmp/"))).toBe(false);
|
|
1196
|
+
// Required managed-home content must survive.
|
|
1197
|
+
expect(members).toContain("auth.json");
|
|
1198
|
+
expect(members).toContain("config.toml");
|
|
1199
|
+
expect(members.some((entry) => entry === "skills/demo.md")).toBe(true);
|
|
1200
|
+
// The host binary bytes must not have been inlined into the upload.
|
|
1201
|
+
expect(homeTar.bytes.includes(Buffer.from(binaryMarker))).toBe(false);
|
|
1202
|
+
// The extracted sandbox home keeps required content and omits the transient dirs.
|
|
1203
|
+
await expect(readFile(path.join(prepared.assetDirs.home, "auth.json"), "utf8"))
|
|
1204
|
+
.resolves.toBe("{\"OPENAI_API_KEY\":\"sk-test\"}\n");
|
|
1205
|
+
await expect(readFile(path.join(prepared.assetDirs.home, "skills", "demo.md"), "utf8"))
|
|
1206
|
+
.resolves.toBe("skill body\n");
|
|
1207
|
+
await expect(lstat(path.join(prepared.assetDirs.home, "tmp"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
1208
|
+
await expect(lstat(path.join(prepared.assetDirs.home, ".tmp"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
1209
|
+
});
|
|
1210
|
+
it("emits throttled, labeled upload and restore progress with direction and percentages", async () => {
|
|
1211
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-progress-"));
|
|
1212
|
+
cleanupDirs.push(rootDir);
|
|
1213
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
1214
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
1215
|
+
const localAssetsDir = path.join(rootDir, "local-assets");
|
|
1216
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
1217
|
+
await mkdir(localAssetsDir, { recursive: true });
|
|
1218
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace\n", "utf8");
|
|
1219
|
+
await writeFile(path.join(localAssetsDir, "skill.md"), "skill\n", "utf8");
|
|
1220
|
+
// Drive byte progress in 100 fine (1%) increments so the throttle has many
|
|
1221
|
+
// chances to emit; the reporter must collapse them to ~one line per 10% step.
|
|
1222
|
+
const driveProgress = async (total, onProgress) => {
|
|
1223
|
+
if (!onProgress)
|
|
1224
|
+
return;
|
|
1225
|
+
for (let i = 1; i <= 100; i++) {
|
|
1226
|
+
await onProgress(Math.floor((total * i) / 100), total);
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
const client = {
|
|
1230
|
+
makeDir: async (remotePath) => {
|
|
1231
|
+
await mkdir(remotePath, { recursive: true });
|
|
1232
|
+
},
|
|
1233
|
+
writeFile: async (remotePath, bytes, options) => {
|
|
1234
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
1235
|
+
const buffer = Buffer.from(bytes);
|
|
1236
|
+
await writeFile(remotePath, buffer);
|
|
1237
|
+
await driveProgress(buffer.byteLength, options?.onProgress);
|
|
1238
|
+
},
|
|
1239
|
+
readFile: async (remotePath, options) => {
|
|
1240
|
+
const buffer = await readFile(remotePath);
|
|
1241
|
+
await driveProgress(buffer.byteLength, options?.onProgress);
|
|
1242
|
+
return buffer;
|
|
1243
|
+
},
|
|
1244
|
+
listFiles: async () => [],
|
|
1245
|
+
remove: async (remotePath) => {
|
|
1246
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
1247
|
+
},
|
|
1248
|
+
run: async (command) => {
|
|
1249
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
1250
|
+
},
|
|
1251
|
+
};
|
|
1252
|
+
const lines = [];
|
|
1253
|
+
attachFallbackSyncIn(client);
|
|
1254
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
1255
|
+
spec: {
|
|
1256
|
+
transport: "sandbox",
|
|
1257
|
+
provider: "test",
|
|
1258
|
+
sandboxId: "sandbox-1",
|
|
1259
|
+
remoteCwd: remoteWorkspaceDir,
|
|
1260
|
+
timeoutMs: 30_000,
|
|
1261
|
+
apiKey: null,
|
|
1262
|
+
},
|
|
1263
|
+
adapterKey: "test-adapter",
|
|
1264
|
+
client,
|
|
1265
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
1266
|
+
assets: [{ key: "skills", localDir: localAssetsDir }],
|
|
1267
|
+
onProgress: (line) => {
|
|
1268
|
+
lines.push(line);
|
|
1269
|
+
},
|
|
1270
|
+
});
|
|
1271
|
+
const uploadWorkspaceLines = lines.filter((line) => line.includes("Syncing workspace to environment"));
|
|
1272
|
+
const uploadAssetLines = lines.filter((line) => line.includes("Syncing skills to environment"));
|
|
1273
|
+
expect(uploadWorkspaceLines.length).toBeGreaterThan(0);
|
|
1274
|
+
expect(uploadAssetLines.length).toBeGreaterThan(0);
|
|
1275
|
+
// 100 reported increments must be throttled to at most ~one line per 10% step.
|
|
1276
|
+
expect(uploadWorkspaceLines.length).toBeLessThanOrEqual(11);
|
|
1277
|
+
// Reaches 100% and shows the MB breakdown.
|
|
1278
|
+
expect(uploadWorkspaceLines.some((line) => line.includes("100%"))).toBe(true);
|
|
1279
|
+
expect(uploadWorkspaceLines.every((line) => /\(\d+\.\d\/\d+\.\d MB\)/.test(line))).toBe(true);
|
|
1280
|
+
await prepared.restoreWorkspace();
|
|
1281
|
+
const restoreLines = lines.filter((line) => line.includes("Restoring workspace from environment"));
|
|
1282
|
+
expect(restoreLines.length).toBeGreaterThan(0);
|
|
1283
|
+
expect(restoreLines.length).toBeLessThanOrEqual(11);
|
|
1284
|
+
expect(restoreLines.some((line) => line.includes("100%"))).toBe(true);
|
|
1285
|
+
});
|
|
1286
|
+
it("creates valid empty workspace tarballs when the workspace is empty", async () => {
|
|
1287
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-empty-"));
|
|
1288
|
+
cleanupDirs.push(rootDir);
|
|
1289
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
1290
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
1291
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
1292
|
+
const downloadedTars = [];
|
|
1293
|
+
const runCommands = [];
|
|
1294
|
+
const client = {
|
|
1295
|
+
makeDir: async (remotePath) => {
|
|
1296
|
+
await mkdir(remotePath, { recursive: true });
|
|
1297
|
+
},
|
|
1298
|
+
writeFile: async (remotePath, bytes) => {
|
|
1299
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
1300
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
1301
|
+
},
|
|
1302
|
+
readFile: async (remotePath) => {
|
|
1303
|
+
const buffer = await readFile(remotePath);
|
|
1304
|
+
if (remotePath.endsWith("workspace-download.tar"))
|
|
1305
|
+
downloadedTars.push({ remotePath, bytes: buffer });
|
|
1306
|
+
return buffer;
|
|
1307
|
+
},
|
|
1308
|
+
listFiles: async () => [],
|
|
1309
|
+
remove: async (remotePath) => {
|
|
1310
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
1311
|
+
},
|
|
1312
|
+
run: async (command) => {
|
|
1313
|
+
runCommands.push(command);
|
|
1314
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
1315
|
+
},
|
|
1316
|
+
};
|
|
1317
|
+
attachFallbackSyncIn(client);
|
|
1318
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
1319
|
+
spec: {
|
|
1320
|
+
transport: "sandbox",
|
|
1321
|
+
provider: "test",
|
|
1322
|
+
sandboxId: "sandbox-1",
|
|
1323
|
+
remoteCwd: remoteWorkspaceDir,
|
|
1324
|
+
timeoutMs: 30_000,
|
|
1325
|
+
apiKey: null,
|
|
1326
|
+
},
|
|
1327
|
+
adapterKey: "test-adapter",
|
|
1328
|
+
client,
|
|
1329
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
1330
|
+
});
|
|
1331
|
+
await prepared.restoreWorkspace();
|
|
1332
|
+
expect(downloadedTars).toHaveLength(1);
|
|
1333
|
+
const members = await listTarMembers(rootDir, "empty-workspace-download.tar", downloadedTars[0].bytes);
|
|
1334
|
+
expect(members).toEqual([]);
|
|
1335
|
+
const emptyArchiveCommand = runCommands.find((command) => command.includes("dd if=/dev/zero"));
|
|
1336
|
+
expect(emptyArchiveCommand).toBeDefined();
|
|
1337
|
+
expect(emptyArchiveCommand).not.toContain("/dev/null");
|
|
1338
|
+
});
|
|
1339
|
+
it("provisions a contribution-less asset via a plain tar extract and restores it as a no-op", async () => {
|
|
1340
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-default-asset-"));
|
|
1341
|
+
cleanupDirs.push(rootDir);
|
|
1342
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
1343
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
1344
|
+
const localAssetsDir = path.join(rootDir, "local-assets");
|
|
1345
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
1346
|
+
await mkdir(localAssetsDir, { recursive: true });
|
|
1347
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace\n", "utf8");
|
|
1348
|
+
await writeFile(path.join(localAssetsDir, "plain.txt"), "plain asset\n", "utf8");
|
|
1349
|
+
const stagedWrites = [];
|
|
1350
|
+
const runCommands = [];
|
|
1351
|
+
const client = {
|
|
1352
|
+
makeDir: async (remotePath) => {
|
|
1353
|
+
await mkdir(remotePath, { recursive: true });
|
|
1354
|
+
},
|
|
1355
|
+
writeFile: async (remotePath, bytes) => {
|
|
1356
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
1357
|
+
if (!remotePath.endsWith("-upload.tar"))
|
|
1358
|
+
stagedWrites.push(path.basename(remotePath));
|
|
1359
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
1360
|
+
},
|
|
1361
|
+
readFile: async (remotePath) => await readFile(remotePath),
|
|
1362
|
+
listFiles: async () => [],
|
|
1363
|
+
remove: async (remotePath) => {
|
|
1364
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
1365
|
+
},
|
|
1366
|
+
run: async (command) => {
|
|
1367
|
+
runCommands.push(command);
|
|
1368
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
1369
|
+
},
|
|
1370
|
+
};
|
|
1371
|
+
attachFallbackSyncIn(client);
|
|
1372
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
1373
|
+
spec: {
|
|
1374
|
+
transport: "sandbox",
|
|
1375
|
+
provider: "test",
|
|
1376
|
+
sandboxId: "sandbox-1",
|
|
1377
|
+
remoteCwd: remoteWorkspaceDir,
|
|
1378
|
+
timeoutMs: 30_000,
|
|
1379
|
+
apiKey: null,
|
|
1380
|
+
},
|
|
1381
|
+
adapterKey: "test-adapter",
|
|
1382
|
+
client,
|
|
1383
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
1384
|
+
// No `provision` / `restore` on the asset: it must ride the default path.
|
|
1385
|
+
assets: [{ key: "plain", localDir: localAssetsDir }],
|
|
1386
|
+
});
|
|
1387
|
+
// Extracted through the default `tar -xf` path.
|
|
1388
|
+
await expect(readFile(path.join(prepared.assetDirs.plain, "plain.txt"), "utf8")).resolves.toBe("plain asset\n");
|
|
1389
|
+
// A contribution-less asset stages no extra files beyond its own tar.
|
|
1390
|
+
expect(stagedWrites.filter((name) => name.includes("plain"))).toEqual([]);
|
|
1391
|
+
// The extract command is the generic tar path, not an adapter-specific script.
|
|
1392
|
+
const assetExtract = runCommands.find((command) => command.includes(`${path.posix.basename(prepared.assetDirs.plain)}-upload.tar`));
|
|
1393
|
+
expect(assetExtract).toBeDefined();
|
|
1394
|
+
expect(assetExtract).toContain("tar -xf");
|
|
1395
|
+
expect(assetExtract).not.toMatch(/\.sh|\.cjs/);
|
|
1396
|
+
// Restore is a clean no-op for a contribution-less asset (no throw, asset dir untouched).
|
|
1397
|
+
await expect(prepared.restoreWorkspace()).resolves.toBeUndefined();
|
|
1398
|
+
await expect(readFile(path.join(prepared.assetDirs.plain, "plain.txt"), "utf8")).resolves.toBe("plain asset\n");
|
|
1399
|
+
});
|
|
1400
|
+
it("round-trips a non-codex asset through generic provision + restore contributions", async () => {
|
|
1401
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-seam-"));
|
|
1402
|
+
cleanupDirs.push(rootDir);
|
|
1403
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
1404
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
1405
|
+
const localAssetsDir = path.join(rootDir, "local-assets");
|
|
1406
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
1407
|
+
await mkdir(localAssetsDir, { recursive: true });
|
|
1408
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace\n", "utf8");
|
|
1409
|
+
await writeFile(path.join(localAssetsDir, "seed.txt"), "seed\n", "utf8");
|
|
1410
|
+
const client = {
|
|
1411
|
+
makeDir: async (remotePath) => {
|
|
1412
|
+
await mkdir(remotePath, { recursive: true });
|
|
1413
|
+
},
|
|
1414
|
+
writeFile: async (remotePath, bytes) => {
|
|
1415
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
1416
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
1417
|
+
},
|
|
1418
|
+
readFile: async (remotePath) => await readFile(remotePath),
|
|
1419
|
+
listFiles: async () => [],
|
|
1420
|
+
remove: async (remotePath) => {
|
|
1421
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
1422
|
+
},
|
|
1423
|
+
run: async (command) => {
|
|
1424
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
1425
|
+
},
|
|
1426
|
+
};
|
|
1427
|
+
// A minimal shell quoter local to the test's custom extract command; the seam
|
|
1428
|
+
// itself carries no adapter knowledge — the fake asset supplies everything.
|
|
1429
|
+
const q = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
1430
|
+
const restored = [];
|
|
1431
|
+
const stagedContentSeen = [];
|
|
1432
|
+
attachFallbackSyncIn(client);
|
|
1433
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
1434
|
+
spec: {
|
|
1435
|
+
transport: "sandbox",
|
|
1436
|
+
provider: "test",
|
|
1437
|
+
sandboxId: "sandbox-1",
|
|
1438
|
+
remoteCwd: remoteWorkspaceDir,
|
|
1439
|
+
timeoutMs: 30_000,
|
|
1440
|
+
apiKey: null,
|
|
1441
|
+
},
|
|
1442
|
+
adapterKey: "generic-adapter",
|
|
1443
|
+
client,
|
|
1444
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
1445
|
+
assets: [{
|
|
1446
|
+
key: "widget",
|
|
1447
|
+
localDir: localAssetsDir,
|
|
1448
|
+
provision: {
|
|
1449
|
+
stageFiles: [{ name: "widget-helper.txt", contents: "helper-bytes\n" }],
|
|
1450
|
+
// Extract the asset AND consume the staged helper file, proving both
|
|
1451
|
+
// stageFiles and postUploadCommand flow through the core generically.
|
|
1452
|
+
postUploadCommand: ({ assetTarPath, assetDir, runtimeRootDir }) => `rm -rf ${q(assetDir)} && mkdir -p ${q(assetDir)} && ` +
|
|
1453
|
+
`tar -xf ${q(assetTarPath)} -C ${q(assetDir)} && rm -f ${q(assetTarPath)} && ` +
|
|
1454
|
+
`cp ${q(path.posix.join(runtimeRootDir, "widget-helper.txt"))} ${q(path.posix.join(assetDir, "helper.copied.txt"))}`,
|
|
1455
|
+
},
|
|
1456
|
+
restore: async ({ assetDir, readFile: readRemote }) => {
|
|
1457
|
+
const bytes = await readRemote(path.posix.join(assetDir, "refreshed.txt"));
|
|
1458
|
+
restored.push(bytes.toString("utf8"));
|
|
1459
|
+
},
|
|
1460
|
+
}],
|
|
1461
|
+
});
|
|
1462
|
+
// provision: the asset's own content extracted...
|
|
1463
|
+
await expect(readFile(path.join(prepared.assetDirs.widget, "seed.txt"), "utf8")).resolves.toBe("seed\n");
|
|
1464
|
+
// ...the staged helper file was written to the runtime root and consumed by the custom extract command.
|
|
1465
|
+
await expect(readFile(path.join(prepared.assetDirs.widget, "helper.copied.txt"), "utf8")).resolves.toBe("helper-bytes\n");
|
|
1466
|
+
stagedContentSeen.push("provisioned");
|
|
1467
|
+
// Simulate the sandbox refreshing a file inside the asset dir, then restore.
|
|
1468
|
+
await writeFile(path.join(prepared.assetDirs.widget, "refreshed.txt"), "refreshed-by-sandbox\n", "utf8");
|
|
1469
|
+
await prepared.restoreWorkspace();
|
|
1470
|
+
// restore contribution was invoked with a working remote readFile against assetDir.
|
|
1471
|
+
expect(restored).toEqual(["refreshed-by-sandbox\n"]);
|
|
1472
|
+
expect(stagedContentSeen).toEqual(["provisioned"]);
|
|
1473
|
+
});
|
|
1474
|
+
it("rejects a provision stageFile.name that is not a simple basename", async () => {
|
|
1475
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-traversal-"));
|
|
1476
|
+
cleanupDirs.push(rootDir);
|
|
1477
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
1478
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
1479
|
+
const localAssetsDir = path.join(rootDir, "local-assets");
|
|
1480
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
1481
|
+
await mkdir(localAssetsDir, { recursive: true });
|
|
1482
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace\n", "utf8");
|
|
1483
|
+
await writeFile(path.join(localAssetsDir, "seed.txt"), "seed\n", "utf8");
|
|
1484
|
+
const writtenPaths = [];
|
|
1485
|
+
const client = {
|
|
1486
|
+
makeDir: async (remotePath) => {
|
|
1487
|
+
await mkdir(remotePath, { recursive: true });
|
|
1488
|
+
},
|
|
1489
|
+
writeFile: async (remotePath, bytes) => {
|
|
1490
|
+
writtenPaths.push(remotePath);
|
|
1491
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
1492
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
1493
|
+
},
|
|
1494
|
+
readFile: async (remotePath) => await readFile(remotePath),
|
|
1495
|
+
listFiles: async () => [],
|
|
1496
|
+
remove: async (remotePath) => {
|
|
1497
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
1498
|
+
},
|
|
1499
|
+
run: async (command) => {
|
|
1500
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
1501
|
+
},
|
|
1502
|
+
};
|
|
1503
|
+
attachFallbackSyncIn(client);
|
|
1504
|
+
// A compromised adapter supplying a traversal name must be rejected before
|
|
1505
|
+
// the core ever writes outside the runtime root.
|
|
1506
|
+
for (const maliciousName of ["../evil.txt", "..", "nested/child.txt", "back\\slash.txt", "../../etc/passwd"]) {
|
|
1507
|
+
writtenPaths.length = 0;
|
|
1508
|
+
await expect(prepareSandboxManagedRuntime({
|
|
1509
|
+
spec: {
|
|
1510
|
+
transport: "sandbox",
|
|
1511
|
+
provider: "test",
|
|
1512
|
+
sandboxId: "sandbox-1",
|
|
1513
|
+
remoteCwd: remoteWorkspaceDir,
|
|
1514
|
+
timeoutMs: 30_000,
|
|
1515
|
+
apiKey: null,
|
|
1516
|
+
},
|
|
1517
|
+
adapterKey: "generic-adapter",
|
|
1518
|
+
client,
|
|
1519
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
1520
|
+
assets: [{
|
|
1521
|
+
key: "widget",
|
|
1522
|
+
localDir: localAssetsDir,
|
|
1523
|
+
provision: {
|
|
1524
|
+
stageFiles: [{ name: maliciousName, contents: "payload\n" }],
|
|
1525
|
+
},
|
|
1526
|
+
}],
|
|
1527
|
+
})).rejects.toThrow(/must be a simple basename/);
|
|
1528
|
+
// The guard fires before the offending write, so nothing landed under the runtime root.
|
|
1529
|
+
expect(writtenPaths.some((p) => p.endsWith("evil.txt") || p.endsWith("passwd") || p.endsWith("child.txt"))).toBe(false);
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
it("routes a custom-provisioned asset through a single syncIn operation with its post-upload command (native runner → 0 direct writeFile/run)", async () => {
|
|
1533
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-native-asset-"));
|
|
1534
|
+
cleanupDirs.push(rootDir);
|
|
1535
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
1536
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
1537
|
+
const localAssetsDir = path.join(rootDir, "local-assets");
|
|
1538
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
1539
|
+
await mkdir(localAssetsDir, { recursive: true });
|
|
1540
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace\n", "utf8");
|
|
1541
|
+
await writeFile(path.join(localAssetsDir, "seed.txt"), "seed\n", "utf8");
|
|
1542
|
+
// A native runner delegates every staging step to `syncIn`; the orchestrator
|
|
1543
|
+
// must make NO direct `writeFile`/`run` exec. These record any leak.
|
|
1544
|
+
const directWrites = [];
|
|
1545
|
+
const directRuns = [];
|
|
1546
|
+
const client = {
|
|
1547
|
+
makeDir: async (remotePath) => {
|
|
1548
|
+
await mkdir(remotePath, { recursive: true });
|
|
1549
|
+
},
|
|
1550
|
+
writeFile: async (remotePath, bytes) => {
|
|
1551
|
+
directWrites.push(remotePath);
|
|
1552
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
1553
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
1554
|
+
},
|
|
1555
|
+
readFile: async (remotePath) => await readFile(remotePath),
|
|
1556
|
+
listFiles: async () => [],
|
|
1557
|
+
remove: async (remotePath) => {
|
|
1558
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
1559
|
+
},
|
|
1560
|
+
run: async (command) => {
|
|
1561
|
+
directRuns.push(command);
|
|
1562
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
1563
|
+
},
|
|
1564
|
+
};
|
|
1565
|
+
const captured = [];
|
|
1566
|
+
attachNativeRecordingSyncIn(client, captured);
|
|
1567
|
+
const q = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
1568
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
1569
|
+
spec: {
|
|
1570
|
+
transport: "sandbox",
|
|
1571
|
+
provider: "test",
|
|
1572
|
+
sandboxId: "sandbox-1",
|
|
1573
|
+
remoteCwd: remoteWorkspaceDir,
|
|
1574
|
+
timeoutMs: 30_000,
|
|
1575
|
+
apiKey: null,
|
|
1576
|
+
},
|
|
1577
|
+
adapterKey: "generic-adapter",
|
|
1578
|
+
client,
|
|
1579
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
1580
|
+
assets: [{
|
|
1581
|
+
key: "widget",
|
|
1582
|
+
localDir: localAssetsDir,
|
|
1583
|
+
provision: {
|
|
1584
|
+
stageFiles: [{ name: "widget-helper.sh", contents: "#!/bin/sh\ntar -xf \"$2\" -C \"$1\"\n" }],
|
|
1585
|
+
// A bespoke post-upload command that consumes the staged helper — proves
|
|
1586
|
+
// the custom command (not a plain default `tar -xf`) rides syncIn.
|
|
1587
|
+
postUploadCommand: ({ assetTarPath, assetDir, runtimeRootDir }) => `rm -rf ${q(assetDir)} && mkdir -p ${q(assetDir)} && ` +
|
|
1588
|
+
`sh ${q(path.posix.join(runtimeRootDir, "widget-helper.sh"))} ${q(assetDir)} ${q(assetTarPath)} && ` +
|
|
1589
|
+
`rm -f ${q(assetTarPath)}`,
|
|
1590
|
+
},
|
|
1591
|
+
}],
|
|
1592
|
+
});
|
|
1593
|
+
// The orchestrator delegated everything to syncIn: no direct exec/writeFile.
|
|
1594
|
+
expect(directWrites).toEqual([]);
|
|
1595
|
+
expect(directRuns).toEqual([]);
|
|
1596
|
+
// Exactly one operation carries the asset: the asset tar + the staged helper
|
|
1597
|
+
// as `files`, and the bespoke command as the ordered post-upload command.
|
|
1598
|
+
const assetOp = captured.find((op) => op.files.some((mapping) => mapping.targetPath.endsWith("widget-upload.tar")));
|
|
1599
|
+
expect(assetOp).toBeDefined();
|
|
1600
|
+
const targets = assetOp.files.map((mapping) => path.posix.basename(mapping.targetPath)).sort();
|
|
1601
|
+
expect(targets).toEqual(["widget-helper.sh", "widget-upload.tar"]);
|
|
1602
|
+
expect(assetOp.files.every((mapping) => mapping.kind === "file")).toBe(true);
|
|
1603
|
+
expect(assetOp.postUploadCommands).toHaveLength(1);
|
|
1604
|
+
expect(assetOp.postUploadCommands[0].command).toContain("widget-helper.sh");
|
|
1605
|
+
expect(assetOp.postUploadCommands[0].command).not.toBe(`rm -rf ${q(path.posix.join(prepared.runtimeRootDir, "widget"))} && mkdir -p ${q(path.posix.join(prepared.runtimeRootDir, "widget"))}`);
|
|
1606
|
+
// The asset tar carries the asset directory as its read-write destination,
|
|
1607
|
+
// because the extract command fills that directory, not the staging archive.
|
|
1608
|
+
const assetTarMapping = assetOp.files.find((mapping) => mapping.targetPath.endsWith("widget-upload.tar"));
|
|
1609
|
+
expect(assetTarMapping?.access).toBe("rw");
|
|
1610
|
+
expect(assetTarMapping?.writablePath).toBe(prepared.assetDirs.widget);
|
|
1611
|
+
// The staged helper file is a read-only input that the command consumes, so
|
|
1612
|
+
// it is `access: "ro"` and never joins the writable set.
|
|
1613
|
+
const stageMapping = assetOp.files.find((mapping) => mapping.targetPath.endsWith("widget-helper.sh"));
|
|
1614
|
+
expect(stageMapping?.access).toBe("ro");
|
|
1615
|
+
expect(stageMapping?.writablePath).toBeUndefined();
|
|
1616
|
+
// The asset actually materialized through the native seam.
|
|
1617
|
+
await expect(readFile(path.join(prepared.assetDirs.widget, "seed.txt"), "utf8")).resolves.toBe("seed\n");
|
|
1618
|
+
});
|
|
1619
|
+
it("stages git and workspace via syncIn preserving .paperclip-runtime (native runner → 0 direct writeFile/run)", async () => {
|
|
1620
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-native-git-"));
|
|
1621
|
+
cleanupDirs.push(rootDir);
|
|
1622
|
+
const sourceRepoDir = path.join(rootDir, "source-repo");
|
|
1623
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
1624
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
1625
|
+
await mkdir(sourceRepoDir, { recursive: true });
|
|
1626
|
+
await git(sourceRepoDir, ["init"]);
|
|
1627
|
+
await git(sourceRepoDir, ["checkout", "-b", "main"]);
|
|
1628
|
+
await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
|
|
1629
|
+
await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
|
|
1630
|
+
await writeFile(path.join(sourceRepoDir, "tracked.txt"), "tracked\n", "utf8");
|
|
1631
|
+
await git(sourceRepoDir, ["add", "tracked.txt"]);
|
|
1632
|
+
await git(sourceRepoDir, ["commit", "-m", "base"]);
|
|
1633
|
+
await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
|
|
1634
|
+
// Pre-seed the sandbox with a `.paperclip-runtime` dir that MUST survive.
|
|
1635
|
+
await mkdir(path.join(remoteWorkspaceDir, ".paperclip-runtime"), { recursive: true });
|
|
1636
|
+
await writeFile(path.join(remoteWorkspaceDir, ".paperclip-runtime", "keep.txt"), "keep\n", "utf8");
|
|
1637
|
+
const directWrites = [];
|
|
1638
|
+
const directRuns = [];
|
|
1639
|
+
const client = {
|
|
1640
|
+
makeDir: async (remotePath) => {
|
|
1641
|
+
await mkdir(remotePath, { recursive: true });
|
|
1642
|
+
},
|
|
1643
|
+
writeFile: async (remotePath, bytes) => {
|
|
1644
|
+
directWrites.push(remotePath);
|
|
1645
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
1646
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
1647
|
+
},
|
|
1648
|
+
readFile: async (remotePath) => await readFile(remotePath),
|
|
1649
|
+
listFiles: async () => [],
|
|
1650
|
+
remove: async (remotePath) => {
|
|
1651
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
1652
|
+
},
|
|
1653
|
+
run: async (command) => {
|
|
1654
|
+
directRuns.push(command);
|
|
1655
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
1656
|
+
},
|
|
1657
|
+
};
|
|
1658
|
+
const captured = [];
|
|
1659
|
+
attachNativeRecordingSyncIn(client, captured);
|
|
1660
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
1661
|
+
spec: {
|
|
1662
|
+
transport: "sandbox",
|
|
1663
|
+
provider: "test",
|
|
1664
|
+
sandboxId: "sandbox-1",
|
|
1665
|
+
remoteCwd: remoteWorkspaceDir,
|
|
1666
|
+
timeoutMs: 30_000,
|
|
1667
|
+
apiKey: null,
|
|
1668
|
+
},
|
|
1669
|
+
adapterKey: "test-adapter",
|
|
1670
|
+
client,
|
|
1671
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
1672
|
+
});
|
|
1673
|
+
// Delegated entirely to syncIn.
|
|
1674
|
+
expect(directWrites).toEqual([]);
|
|
1675
|
+
expect(directRuns).toEqual([]);
|
|
1676
|
+
// One merged operation carries BOTH the git-history and workspace-overlay tars
|
|
1677
|
+
// as two `file` mappings, each with its extract as an ordered post-command.
|
|
1678
|
+
expect(captured).toHaveLength(1);
|
|
1679
|
+
const op = captured[0];
|
|
1680
|
+
const byBase = (base) => op.files.find((mapping) => path.posix.basename(mapping.targetPath) === base);
|
|
1681
|
+
expect(byBase("git-workspace-upload.tar")).toBeDefined();
|
|
1682
|
+
expect(byBase("workspace-upload.tar")).toBeDefined();
|
|
1683
|
+
expect(op.files.every((mapping) => mapping.kind === "file")).toBe(true);
|
|
1684
|
+
// The first post-upload command extracts the git history and preserves
|
|
1685
|
+
// `.paperclip-runtime` while replacing the rest of the tree (wipe-except-preserved).
|
|
1686
|
+
const gitCommand = op.postUploadCommands[0].command;
|
|
1687
|
+
expect(gitCommand).toContain(".paperclip-runtime");
|
|
1688
|
+
expect(gitCommand).toContain("tar -xf");
|
|
1689
|
+
// The pre-seeded runtime dir survived the git+workspace staging.
|
|
1690
|
+
await expect(readFile(path.join(remoteWorkspaceDir, ".paperclip-runtime", "keep.txt"), "utf8")).resolves.toBe("keep\n");
|
|
1691
|
+
await expect(readFile(path.join(remoteWorkspaceDir, "tracked.txt"), "utf8")).resolves.toBe("tracked\n");
|
|
1692
|
+
expect(prepared.workspaceRemoteDir).toBe(remoteWorkspaceDir);
|
|
1693
|
+
});
|
|
1694
|
+
it("the workspace wipe command preserves in-flight sync scratch tarballs (.paperclip-upload-*)", async () => {
|
|
1695
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-scratch-shape-"));
|
|
1696
|
+
cleanupDirs.push(rootDir);
|
|
1697
|
+
const sourceRepoDir = path.join(rootDir, "source-repo");
|
|
1698
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
1699
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
1700
|
+
await mkdir(sourceRepoDir, { recursive: true });
|
|
1701
|
+
await git(sourceRepoDir, ["init"]);
|
|
1702
|
+
await git(sourceRepoDir, ["checkout", "-b", "main"]);
|
|
1703
|
+
await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
|
|
1704
|
+
await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
|
|
1705
|
+
await writeFile(path.join(sourceRepoDir, "tracked.txt"), "tracked\n", "utf8");
|
|
1706
|
+
await git(sourceRepoDir, ["add", "tracked.txt"]);
|
|
1707
|
+
await git(sourceRepoDir, ["commit", "-m", "base"]);
|
|
1708
|
+
await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
|
|
1709
|
+
const client = {
|
|
1710
|
+
makeDir: async (remotePath) => {
|
|
1711
|
+
await mkdir(remotePath, { recursive: true });
|
|
1712
|
+
},
|
|
1713
|
+
writeFile: async (remotePath, bytes) => {
|
|
1714
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
1715
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
1716
|
+
},
|
|
1717
|
+
readFile: async (remotePath) => await readFile(remotePath),
|
|
1718
|
+
listFiles: async () => [],
|
|
1719
|
+
remove: async (remotePath) => {
|
|
1720
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
1721
|
+
},
|
|
1722
|
+
run: async (command) => {
|
|
1723
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
1724
|
+
},
|
|
1725
|
+
};
|
|
1726
|
+
const captured = [];
|
|
1727
|
+
attachNativeRecordingSyncIn(client, captured);
|
|
1728
|
+
await prepareSandboxManagedRuntime({
|
|
1729
|
+
spec: {
|
|
1730
|
+
transport: "sandbox",
|
|
1731
|
+
provider: "test",
|
|
1732
|
+
sandboxId: "sandbox-1",
|
|
1733
|
+
remoteCwd: remoteWorkspaceDir,
|
|
1734
|
+
timeoutMs: 30_000,
|
|
1735
|
+
apiKey: null,
|
|
1736
|
+
},
|
|
1737
|
+
adapterKey: "test-adapter",
|
|
1738
|
+
client,
|
|
1739
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
1740
|
+
});
|
|
1741
|
+
// The wipe `find` runs before the extract. It must preserve the daytona
|
|
1742
|
+
// scratch prefix so a concurrent referenced-project upload survives the wipe.
|
|
1743
|
+
const wipeCommand = captured[0].postUploadCommands[0].command;
|
|
1744
|
+
expect(wipeCommand).toContain("find ");
|
|
1745
|
+
expect(wipeCommand).toContain("! -name '.paperclip-upload-*'");
|
|
1746
|
+
});
|
|
1747
|
+
it("the workspace wipe keeps an in-flight scratch tarball at the root but removes a stale sibling", async () => {
|
|
1748
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-scratch-race-"));
|
|
1749
|
+
cleanupDirs.push(rootDir);
|
|
1750
|
+
const sourceRepoDir = path.join(rootDir, "source-repo");
|
|
1751
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
1752
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
1753
|
+
await mkdir(sourceRepoDir, { recursive: true });
|
|
1754
|
+
await git(sourceRepoDir, ["init"]);
|
|
1755
|
+
await git(sourceRepoDir, ["checkout", "-b", "main"]);
|
|
1756
|
+
await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
|
|
1757
|
+
await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
|
|
1758
|
+
await writeFile(path.join(sourceRepoDir, "tracked.txt"), "tracked\n", "utf8");
|
|
1759
|
+
await git(sourceRepoDir, ["add", "tracked.txt"]);
|
|
1760
|
+
await git(sourceRepoDir, ["commit", "-m", "base"]);
|
|
1761
|
+
await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
|
|
1762
|
+
// Pre-seed the sandbox root. `.paperclip-upload-test.tar` simulates a
|
|
1763
|
+
// concurrent referenced-project scratch tarball in flight; `stale-junk.txt`
|
|
1764
|
+
// is an unrelated child that the wipe must remove.
|
|
1765
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
1766
|
+
await writeFile(path.join(remoteWorkspaceDir, ".paperclip-upload-test.tar"), "scratch\n", "utf8");
|
|
1767
|
+
await writeFile(path.join(remoteWorkspaceDir, "stale-junk.txt"), "junk\n", "utf8");
|
|
1768
|
+
const client = {
|
|
1769
|
+
makeDir: async (remotePath) => {
|
|
1770
|
+
await mkdir(remotePath, { recursive: true });
|
|
1771
|
+
},
|
|
1772
|
+
writeFile: async (remotePath, bytes) => {
|
|
1773
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
1774
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
1775
|
+
},
|
|
1776
|
+
readFile: async (remotePath) => await readFile(remotePath),
|
|
1777
|
+
listFiles: async () => [],
|
|
1778
|
+
remove: async (remotePath) => {
|
|
1779
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
1780
|
+
},
|
|
1781
|
+
run: async (command) => {
|
|
1782
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
1783
|
+
},
|
|
1784
|
+
};
|
|
1785
|
+
const captured = [];
|
|
1786
|
+
attachNativeRecordingSyncIn(client, captured);
|
|
1787
|
+
await prepareSandboxManagedRuntime({
|
|
1788
|
+
spec: {
|
|
1789
|
+
transport: "sandbox",
|
|
1790
|
+
provider: "test",
|
|
1791
|
+
sandboxId: "sandbox-1",
|
|
1792
|
+
remoteCwd: remoteWorkspaceDir,
|
|
1793
|
+
timeoutMs: 30_000,
|
|
1794
|
+
apiKey: null,
|
|
1795
|
+
},
|
|
1796
|
+
adapterKey: "test-adapter",
|
|
1797
|
+
client,
|
|
1798
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
1799
|
+
});
|
|
1800
|
+
// The real `find` wipe ran through `sh -c`. The scratch tarball survived and
|
|
1801
|
+
// the unrelated sibling did not.
|
|
1802
|
+
await expect(readFile(path.join(remoteWorkspaceDir, ".paperclip-upload-test.tar"), "utf8")).resolves.toBe("scratch\n");
|
|
1803
|
+
await expect(readFile(path.join(remoteWorkspaceDir, "stale-junk.txt"), "utf8")).rejects.toThrow();
|
|
1804
|
+
});
|
|
1805
|
+
it("issues one merged syncIn operation for a git-backed workspace stage-sync with two ordered extract commands", async () => {
|
|
1806
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-merged-git-"));
|
|
1807
|
+
cleanupDirs.push(rootDir);
|
|
1808
|
+
const sourceRepoDir = path.join(rootDir, "source-repo");
|
|
1809
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
1810
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
1811
|
+
await mkdir(sourceRepoDir, { recursive: true });
|
|
1812
|
+
await git(sourceRepoDir, ["init"]);
|
|
1813
|
+
await git(sourceRepoDir, ["checkout", "-b", "main"]);
|
|
1814
|
+
await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
|
|
1815
|
+
await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
|
|
1816
|
+
await writeFile(path.join(sourceRepoDir, "tracked.txt"), "tracked\n", "utf8");
|
|
1817
|
+
await git(sourceRepoDir, ["add", "tracked.txt"]);
|
|
1818
|
+
await git(sourceRepoDir, ["commit", "-m", "base"]);
|
|
1819
|
+
await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
|
|
1820
|
+
// Pre-seed the sandbox with a `.paperclip-runtime` dir that MUST survive.
|
|
1821
|
+
await mkdir(path.join(remoteWorkspaceDir, ".paperclip-runtime"), { recursive: true });
|
|
1822
|
+
await writeFile(path.join(remoteWorkspaceDir, ".paperclip-runtime", "keep.txt"), "keep\n", "utf8");
|
|
1823
|
+
const client = {
|
|
1824
|
+
makeDir: async (remotePath) => {
|
|
1825
|
+
await mkdir(remotePath, { recursive: true });
|
|
1826
|
+
},
|
|
1827
|
+
writeFile: async (remotePath, bytes) => {
|
|
1828
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
1829
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
1830
|
+
},
|
|
1831
|
+
readFile: async (remotePath) => await readFile(remotePath),
|
|
1832
|
+
listFiles: async () => [],
|
|
1833
|
+
remove: async (remotePath) => {
|
|
1834
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
1835
|
+
},
|
|
1836
|
+
run: async (command) => {
|
|
1837
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
1838
|
+
},
|
|
1839
|
+
};
|
|
1840
|
+
const captured = [];
|
|
1841
|
+
attachNativeRecordingSyncIn(client, captured);
|
|
1842
|
+
// Count how many times `syncIn` is invoked so the merge collapses the two
|
|
1843
|
+
// workspace staging steps into a single native round trip.
|
|
1844
|
+
let syncInCallCount = 0;
|
|
1845
|
+
const recordingSyncIn = client.syncIn;
|
|
1846
|
+
client.syncIn = async (operations) => {
|
|
1847
|
+
syncInCallCount += 1;
|
|
1848
|
+
return recordingSyncIn(operations);
|
|
1849
|
+
};
|
|
1850
|
+
await prepareSandboxManagedRuntime({
|
|
1851
|
+
spec: {
|
|
1852
|
+
transport: "sandbox",
|
|
1853
|
+
provider: "test",
|
|
1854
|
+
sandboxId: "sandbox-1",
|
|
1855
|
+
remoteCwd: remoteWorkspaceDir,
|
|
1856
|
+
timeoutMs: 30_000,
|
|
1857
|
+
apiKey: null,
|
|
1858
|
+
},
|
|
1859
|
+
adapterKey: "test-adapter",
|
|
1860
|
+
client,
|
|
1861
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
1862
|
+
});
|
|
1863
|
+
// One `syncIn` call carrying exactly one operation for the whole workspace.
|
|
1864
|
+
expect(syncInCallCount).toBe(1);
|
|
1865
|
+
expect(captured).toHaveLength(1);
|
|
1866
|
+
const op = captured[0];
|
|
1867
|
+
// Both host tars ride the one operation as two `file` mappings.
|
|
1868
|
+
const byBase = (base) => op.files.find((mapping) => path.posix.basename(mapping.targetPath) === base);
|
|
1869
|
+
const gitMapping = byBase("git-workspace-upload.tar");
|
|
1870
|
+
const overlayMapping = byBase("workspace-upload.tar");
|
|
1871
|
+
expect(gitMapping).toBeDefined();
|
|
1872
|
+
expect(overlayMapping).toBeDefined();
|
|
1873
|
+
expect(op.files).toHaveLength(2);
|
|
1874
|
+
expect(op.files.every((mapping) => mapping.kind === "file")).toBe(true);
|
|
1875
|
+
// Both tar targets live under `.paperclip-runtime`, so the git extract's wipe
|
|
1876
|
+
// (which preserves `.paperclip-runtime`) cannot delete the overlay tar before
|
|
1877
|
+
// the overlay extract runs.
|
|
1878
|
+
for (const mapping of op.files) {
|
|
1879
|
+
expect(mapping.targetPath).toContain("/.paperclip-runtime/");
|
|
1880
|
+
}
|
|
1881
|
+
// Two ordered extract commands: git history first (wipe-except-preserved),
|
|
1882
|
+
// overlay second (merge, no wipe). No deleted paths in this clean worktree.
|
|
1883
|
+
const commands = op.postUploadCommands ?? [];
|
|
1884
|
+
expect(commands).toHaveLength(2);
|
|
1885
|
+
expect(commands[0].command).toContain("git-workspace-upload.tar");
|
|
1886
|
+
expect(commands[0].command).toContain(".paperclip-runtime");
|
|
1887
|
+
expect(commands[0].command).toContain("find ");
|
|
1888
|
+
expect(commands[1].command).toContain("workspace-upload.tar");
|
|
1889
|
+
expect(commands[1].command).not.toContain("git-workspace-upload.tar");
|
|
1890
|
+
expect(commands[1].command).not.toContain("find ");
|
|
1891
|
+
// The pre-seeded runtime dir survived and the workspace overlay applied.
|
|
1892
|
+
await expect(readFile(path.join(remoteWorkspaceDir, ".paperclip-runtime", "keep.txt"), "utf8")).resolves.toBe("keep\n");
|
|
1893
|
+
await expect(readFile(path.join(remoteWorkspaceDir, "tracked.txt"), "utf8")).resolves.toBe("tracked\n");
|
|
1894
|
+
});
|
|
1895
|
+
it("the merged workspace confine guard covers both tar mappings (escape in either trips it)", () => {
|
|
1896
|
+
const runtimeRoot = "/home/daytona/paperclip-workspace/.paperclip-runtime/test-adapter";
|
|
1897
|
+
const tempRoot = "/tmp/paperclip-sandbox-sync-abc";
|
|
1898
|
+
const gitMapping = {
|
|
1899
|
+
sourcePath: `${tempRoot}/git-workspace.tar`,
|
|
1900
|
+
targetPath: `${runtimeRoot}/git-workspace-upload.tar`,
|
|
1901
|
+
kind: "file",
|
|
1902
|
+
};
|
|
1903
|
+
const overlayMapping = {
|
|
1904
|
+
sourcePath: `${tempRoot}/workspace.tar`,
|
|
1905
|
+
targetPath: `${runtimeRoot}/workspace-upload.tar`,
|
|
1906
|
+
kind: "file",
|
|
1907
|
+
};
|
|
1908
|
+
const roots = { sourceRoots: [tempRoot], targetRoots: [runtimeRoot] };
|
|
1909
|
+
// A confined merged operation with both tar mappings passes the guard.
|
|
1910
|
+
expect(() => assertSyncOperationsConfined([{ operationId: "merged", files: [gitMapping, overlayMapping] }], roots)).not.toThrow();
|
|
1911
|
+
// A `..` target escape in the overlay mapping trips the guard, so the whole
|
|
1912
|
+
// merged operation is rejected before any transfer.
|
|
1913
|
+
expect(() => assertSyncOperationsConfined([{
|
|
1914
|
+
operationId: "merged",
|
|
1915
|
+
files: [
|
|
1916
|
+
gitMapping,
|
|
1917
|
+
{ ...overlayMapping, targetPath: `${runtimeRoot}/../../etc/workspace-upload.tar` },
|
|
1918
|
+
],
|
|
1919
|
+
}], roots)).toThrow(/escapes its confinement root|not a confined absolute path/);
|
|
1920
|
+
// An absolute-path source escape in the git mapping trips the guard too.
|
|
1921
|
+
expect(() => assertSyncOperationsConfined([{
|
|
1922
|
+
operationId: "merged",
|
|
1923
|
+
files: [{ ...gitMapping, sourcePath: "/etc/passwd" }, overlayMapping],
|
|
1924
|
+
}], roots)).toThrow(/escapes its confinement root|not a confined absolute path/);
|
|
1925
|
+
});
|
|
1926
|
+
// Regression lock: a representative `codex_local` start stages its inbound bytes
|
|
1927
|
+
// as TWO `syncIn` operations. The git-history and workspace-overlay tars share
|
|
1928
|
+
// ONE merged operation (one native `uploadFiles` round-trip that carries both
|
|
1929
|
+
// tars, with the two extract commands as ordered `postUploadCommands`); the
|
|
1930
|
+
// managed Codex `home` asset (auth.json merge) is the second operation. Every
|
|
1931
|
+
// inbound step is routed through `client.syncIn`, with no separate
|
|
1932
|
+
// custom-provision diversion. Assert the collapsed round-trip count so a future
|
|
1933
|
+
// change that re-inlines a `writeFile`+`run` sequence — or splits the merged
|
|
1934
|
+
// workspace operation back into two — fails loudly here instead of silently
|
|
1935
|
+
// regressing the start path.
|
|
1936
|
+
it("collapses a representative codex_local start to two syncIn round-trips: one merged workspace op plus the asset op", async () => {
|
|
1937
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-codex-roundtrip-"));
|
|
1938
|
+
cleanupDirs.push(rootDir);
|
|
1939
|
+
const sourceRepoDir = path.join(rootDir, "source-repo");
|
|
1940
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
1941
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
1942
|
+
const homeDir = path.join(rootDir, "codex-home");
|
|
1943
|
+
// Git-backed workspace → git history + workspace overlay share one merged op.
|
|
1944
|
+
await mkdir(sourceRepoDir, { recursive: true });
|
|
1945
|
+
await git(sourceRepoDir, ["init"]);
|
|
1946
|
+
await git(sourceRepoDir, ["checkout", "-b", "main"]);
|
|
1947
|
+
await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
|
|
1948
|
+
await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
|
|
1949
|
+
await writeFile(path.join(sourceRepoDir, "tracked.txt"), "tracked\n", "utf8");
|
|
1950
|
+
await git(sourceRepoDir, ["add", "tracked.txt"]);
|
|
1951
|
+
await git(sourceRepoDir, ["commit", "-m", "base"]);
|
|
1952
|
+
await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
|
|
1953
|
+
// Managed Codex home with an auth.json that a custom post-upload command
|
|
1954
|
+
// merges in-sandbox — the credential path is routed onto native uploadFiles.
|
|
1955
|
+
await mkdir(homeDir, { recursive: true });
|
|
1956
|
+
await writeFile(path.join(homeDir, "auth.json"), "{\"OPENAI_API_KEY\":\"sk-test\"}\n", "utf8");
|
|
1957
|
+
await writeFile(path.join(homeDir, "config.toml"), "model = \"gpt\"\n", "utf8");
|
|
1958
|
+
// A native runner delegates every staging step to `syncIn`; ANY direct
|
|
1959
|
+
// writeFile/run exec is a collapse regression.
|
|
1960
|
+
const directWrites = [];
|
|
1961
|
+
const directRuns = [];
|
|
1962
|
+
const client = {
|
|
1963
|
+
makeDir: async (remotePath) => {
|
|
1964
|
+
await mkdir(remotePath, { recursive: true });
|
|
1965
|
+
},
|
|
1966
|
+
writeFile: async (remotePath, bytes) => {
|
|
1967
|
+
directWrites.push(remotePath);
|
|
1968
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
1969
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
1970
|
+
},
|
|
1971
|
+
readFile: async (remotePath) => await readFile(remotePath),
|
|
1972
|
+
listFiles: async () => [],
|
|
1973
|
+
remove: async (remotePath) => {
|
|
1974
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
1975
|
+
},
|
|
1976
|
+
run: async (command) => {
|
|
1977
|
+
directRuns.push(command);
|
|
1978
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
1979
|
+
},
|
|
1980
|
+
};
|
|
1981
|
+
const captured = [];
|
|
1982
|
+
attachNativeRecordingSyncIn(client, captured);
|
|
1983
|
+
const q = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
1984
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
1985
|
+
spec: {
|
|
1986
|
+
transport: "sandbox",
|
|
1987
|
+
provider: "test",
|
|
1988
|
+
sandboxId: "sandbox-1",
|
|
1989
|
+
remoteCwd: remoteWorkspaceDir,
|
|
1990
|
+
timeoutMs: 30_000,
|
|
1991
|
+
apiKey: null,
|
|
1992
|
+
},
|
|
1993
|
+
adapterKey: "codex",
|
|
1994
|
+
client,
|
|
1995
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
1996
|
+
assets: [{
|
|
1997
|
+
key: "home",
|
|
1998
|
+
localDir: homeDir,
|
|
1999
|
+
provision: {
|
|
2000
|
+
stageFiles: [{ name: "home-merge.sh", contents: "#!/bin/sh\ntar -xf \"$2\" -C \"$1\"\n" }],
|
|
2001
|
+
postUploadCommand: ({ assetTarPath, assetDir, runtimeRootDir }) => `mkdir -p ${q(assetDir)} && ` +
|
|
2002
|
+
`sh ${q(path.posix.join(runtimeRootDir, "home-merge.sh"))} ${q(assetDir)} ${q(assetTarPath)} && ` +
|
|
2003
|
+
`rm -f ${q(assetTarPath)}`,
|
|
2004
|
+
},
|
|
2005
|
+
}],
|
|
2006
|
+
});
|
|
2007
|
+
// The orchestrator delegated everything to syncIn: no re-inlined writeFile/run.
|
|
2008
|
+
expect(directWrites).toEqual([]);
|
|
2009
|
+
expect(directRuns).toEqual([]);
|
|
2010
|
+
// The collapsed count: exactly two inbound round-trips — the merged workspace
|
|
2011
|
+
// op (git history + overlay) and the home asset op.
|
|
2012
|
+
expect(captured).toHaveLength(2);
|
|
2013
|
+
const hasBase = (op, base) => op.files.some((mapping) => path.posix.basename(mapping.targetPath) === base);
|
|
2014
|
+
const workspaceOp = captured.find((op) => hasBase(op, "workspace-upload.tar"));
|
|
2015
|
+
const homeOp = captured.find((op) => hasBase(op, "home-upload.tar"));
|
|
2016
|
+
expect(workspaceOp).toBeDefined();
|
|
2017
|
+
expect(homeOp).toBeDefined();
|
|
2018
|
+
// The merged workspace op carries BOTH the git-history and overlay tars, with
|
|
2019
|
+
// both extract commands as ordered post-upload commands (git first, overlay
|
|
2020
|
+
// second). The two tars ride one native uploadFiles round-trip.
|
|
2021
|
+
expect(hasBase(workspaceOp, "git-workspace-upload.tar")).toBe(true);
|
|
2022
|
+
expect(workspaceOp.files).toHaveLength(2);
|
|
2023
|
+
expect((workspaceOp.postUploadCommands ?? []).length).toBeGreaterThanOrEqual(2);
|
|
2024
|
+
// Every operation is a native uploadFiles (all `file` mappings) whose
|
|
2025
|
+
// extract/merge rides as an ordered provider-executed post-upload command.
|
|
2026
|
+
for (const op of captured) {
|
|
2027
|
+
expect(op.files.length).toBeGreaterThanOrEqual(1);
|
|
2028
|
+
expect(op.files.every((mapping) => mapping.kind === "file")).toBe(true);
|
|
2029
|
+
expect(op.postUploadCommands ?? []).not.toHaveLength(0);
|
|
2030
|
+
}
|
|
2031
|
+
// Operation ids are distinct, so "2 operations" is 2 real round-trips.
|
|
2032
|
+
expect(new Set(captured.map((op) => op.operationId)).size).toBe(2);
|
|
2033
|
+
// The credential asset actually materialized through the native seam.
|
|
2034
|
+
await expect(readFile(path.join(prepared.assetDirs.home, "auth.json"), "utf8"))
|
|
2035
|
+
.resolves.toBe("{\"OPENAI_API_KEY\":\"sk-test\"}\n");
|
|
2036
|
+
});
|
|
2037
|
+
it("authors the advisory access intent rw on workspace, git, and asset inbound mappings", async () => {
|
|
2038
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-access-rw-"));
|
|
2039
|
+
cleanupDirs.push(rootDir);
|
|
2040
|
+
const sourceRepoDir = path.join(rootDir, "source-repo");
|
|
2041
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
2042
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
2043
|
+
const assetDir = path.join(rootDir, "asset-home");
|
|
2044
|
+
// A git-backed workspace produces both a git-history tar and an overlay tar.
|
|
2045
|
+
await mkdir(sourceRepoDir, { recursive: true });
|
|
2046
|
+
await git(sourceRepoDir, ["init"]);
|
|
2047
|
+
await git(sourceRepoDir, ["checkout", "-b", "main"]);
|
|
2048
|
+
await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
|
|
2049
|
+
await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
|
|
2050
|
+
await writeFile(path.join(sourceRepoDir, "tracked.txt"), "tracked\n", "utf8");
|
|
2051
|
+
await git(sourceRepoDir, ["add", "tracked.txt"]);
|
|
2052
|
+
await git(sourceRepoDir, ["commit", "-m", "base"]);
|
|
2053
|
+
await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
|
|
2054
|
+
await mkdir(assetDir, { recursive: true });
|
|
2055
|
+
await writeFile(path.join(assetDir, "config.toml"), "model = \"gpt\"\n", "utf8");
|
|
2056
|
+
const client = {
|
|
2057
|
+
makeDir: async (remotePath) => { await mkdir(remotePath, { recursive: true }); },
|
|
2058
|
+
writeFile: async (remotePath, bytes) => {
|
|
2059
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
2060
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
2061
|
+
},
|
|
2062
|
+
readFile: async (remotePath) => await readFile(remotePath),
|
|
2063
|
+
listFiles: async () => [],
|
|
2064
|
+
remove: async (remotePath) => { await rm(remotePath, { recursive: true, force: true }); },
|
|
2065
|
+
run: async (command) => { await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 }); },
|
|
2066
|
+
};
|
|
2067
|
+
const captured = [];
|
|
2068
|
+
attachCapturingSyncIn(client, captured);
|
|
2069
|
+
await prepareSandboxManagedRuntime({
|
|
2070
|
+
spec: {
|
|
2071
|
+
transport: "sandbox",
|
|
2072
|
+
provider: "test",
|
|
2073
|
+
sandboxId: "sandbox-1",
|
|
2074
|
+
remoteCwd: remoteWorkspaceDir,
|
|
2075
|
+
timeoutMs: 30_000,
|
|
2076
|
+
apiKey: null,
|
|
2077
|
+
},
|
|
2078
|
+
adapterKey: "test-adapter",
|
|
2079
|
+
client,
|
|
2080
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
2081
|
+
assets: [{ key: "home", localDir: assetDir }],
|
|
2082
|
+
});
|
|
2083
|
+
const findMapping = (base) => captured
|
|
2084
|
+
.flatMap((op) => op.files)
|
|
2085
|
+
.find((mapping) => path.posix.basename(mapping.targetPath) === base);
|
|
2086
|
+
// The workspace, git-history, and asset destinations receive read-write bytes,
|
|
2087
|
+
// so the author marks each mapping `access: "rw"`.
|
|
2088
|
+
expect(findMapping("workspace-upload.tar")?.access).toBe("rw");
|
|
2089
|
+
expect(findMapping("git-workspace-upload.tar")?.access).toBe("rw");
|
|
2090
|
+
expect(findMapping("home-upload.tar")?.access).toBe("rw");
|
|
2091
|
+
// Each tar mapping uploads a staging archive under the runtime root, so its
|
|
2092
|
+
// `targetPath` is not the read-write destination. `writablePath` names the
|
|
2093
|
+
// directory that the post-upload extract command fills: the workspace
|
|
2094
|
+
// directory for the workspace and git tars, and the asset directory for the
|
|
2095
|
+
// asset tar.
|
|
2096
|
+
const remoteAssetDir = path.posix.join(remoteWorkspaceDir, ".paperclip-runtime", "test-adapter", "home");
|
|
2097
|
+
expect(findMapping("workspace-upload.tar")?.writablePath).toBe(remoteWorkspaceDir);
|
|
2098
|
+
expect(findMapping("git-workspace-upload.tar")?.writablePath).toBe(remoteWorkspaceDir);
|
|
2099
|
+
expect(findMapping("home-upload.tar")?.writablePath).toBe(remoteAssetDir);
|
|
2100
|
+
});
|
|
2101
|
+
it("authors the advisory access intent ro on referenced-project inbound mappings", async () => {
|
|
2102
|
+
const flagKey = "PAPERCLIP_MULTI_PROJECT_WORKSPACE_SYNC";
|
|
2103
|
+
const priorFlag = process.env[flagKey];
|
|
2104
|
+
process.env[flagKey] = "1";
|
|
2105
|
+
try {
|
|
2106
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-access-ro-"));
|
|
2107
|
+
cleanupDirs.push(rootDir);
|
|
2108
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
2109
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
2110
|
+
const referencedDir = path.join(rootDir, "referenced-project");
|
|
2111
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
2112
|
+
await mkdir(referencedDir, { recursive: true });
|
|
2113
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "anchor\n", "utf8");
|
|
2114
|
+
await writeFile(path.join(referencedDir, "notes.md"), "reference\n", "utf8");
|
|
2115
|
+
const client = {
|
|
2116
|
+
makeDir: async (remotePath) => { await mkdir(remotePath, { recursive: true }); },
|
|
2117
|
+
writeFile: async (remotePath, bytes) => {
|
|
2118
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
2119
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
2120
|
+
},
|
|
2121
|
+
readFile: async (remotePath) => await readFile(remotePath),
|
|
2122
|
+
listFiles: async () => [],
|
|
2123
|
+
remove: async (remotePath) => { await rm(remotePath, { recursive: true, force: true }); },
|
|
2124
|
+
run: async (command) => { await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 }); },
|
|
2125
|
+
};
|
|
2126
|
+
const captured = [];
|
|
2127
|
+
attachCapturingSyncIn(client, captured);
|
|
2128
|
+
await prepareSandboxManagedRuntime({
|
|
2129
|
+
spec: {
|
|
2130
|
+
transport: "sandbox",
|
|
2131
|
+
provider: "test",
|
|
2132
|
+
sandboxId: "sandbox-1",
|
|
2133
|
+
remoteCwd: remoteWorkspaceDir,
|
|
2134
|
+
timeoutMs: 30_000,
|
|
2135
|
+
apiKey: null,
|
|
2136
|
+
},
|
|
2137
|
+
adapterKey: "test-adapter",
|
|
2138
|
+
client,
|
|
2139
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
2140
|
+
additionalSources: [{ localPath: referencedDir, projectId: "proj-first", ignoreResolution: { kind: "other" } }],
|
|
2141
|
+
});
|
|
2142
|
+
const referencedMapping = captured
|
|
2143
|
+
.flatMap((op) => op.files)
|
|
2144
|
+
.find((mapping) => path.posix.basename(mapping.targetPath) === "project-proj-first");
|
|
2145
|
+
// A referenced project is a read-only tree, so the author marks it `access: "ro"`.
|
|
2146
|
+
expect(referencedMapping).toBeDefined();
|
|
2147
|
+
expect(referencedMapping?.kind).toBe("directory");
|
|
2148
|
+
expect(referencedMapping?.access).toBe("ro");
|
|
2149
|
+
}
|
|
2150
|
+
finally {
|
|
2151
|
+
if (priorFlag === undefined)
|
|
2152
|
+
delete process.env[flagKey];
|
|
2153
|
+
else
|
|
2154
|
+
process.env[flagKey] = priorFlag;
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
it("reports the real transferred bytes for a referenced project's inbound staging", async () => {
|
|
2158
|
+
const flagKey = "PAPERCLIP_MULTI_PROJECT_WORKSPACE_SYNC";
|
|
2159
|
+
const priorFlag = process.env[flagKey];
|
|
2160
|
+
process.env[flagKey] = "1";
|
|
2161
|
+
try {
|
|
2162
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-project-bytes-"));
|
|
2163
|
+
cleanupDirs.push(rootDir);
|
|
2164
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
2165
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
2166
|
+
const referencedDir = path.join(rootDir, "referenced-project");
|
|
2167
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
2168
|
+
await mkdir(referencedDir, { recursive: true });
|
|
2169
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "anchor\n", "utf8");
|
|
2170
|
+
// A referenced project has no host tarball to `fs.stat`, so its progress
|
|
2171
|
+
// line depends entirely on the transport's own `bytesTransferred`. Give it
|
|
2172
|
+
// real, sizeable content (well above the 0.1 MB rounding step), so a bug
|
|
2173
|
+
// that keeps the line at 0 stays distinguishable from a correctly-reported
|
|
2174
|
+
// small transfer that would still round down to "0.0 MB".
|
|
2175
|
+
await writeFile(path.join(referencedDir, "notes.md"), Buffer.alloc(300 * 1024, "a"));
|
|
2176
|
+
const client = {
|
|
2177
|
+
makeDir: async (remotePath) => { await mkdir(remotePath, { recursive: true }); },
|
|
2178
|
+
writeFile: async (remotePath, bytes) => {
|
|
2179
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
2180
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
2181
|
+
},
|
|
2182
|
+
readFile: async (remotePath) => await readFile(remotePath),
|
|
2183
|
+
listFiles: async () => [],
|
|
2184
|
+
remove: async (remotePath) => { await rm(remotePath, { recursive: true, force: true }); },
|
|
2185
|
+
run: async (command) => { await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 }); },
|
|
2186
|
+
};
|
|
2187
|
+
attachCapturingSyncIn(client, []);
|
|
2188
|
+
const lines = [];
|
|
2189
|
+
await prepareSandboxManagedRuntime({
|
|
2190
|
+
spec: {
|
|
2191
|
+
transport: "sandbox",
|
|
2192
|
+
provider: "test",
|
|
2193
|
+
sandboxId: "sandbox-1",
|
|
2194
|
+
remoteCwd: remoteWorkspaceDir,
|
|
2195
|
+
timeoutMs: 30_000,
|
|
2196
|
+
apiKey: null,
|
|
2197
|
+
},
|
|
2198
|
+
adapterKey: "test-adapter",
|
|
2199
|
+
client,
|
|
2200
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
2201
|
+
additionalSources: [{ localPath: referencedDir, projectId: "proj-first", ignoreResolution: { kind: "other" } }],
|
|
2202
|
+
onProgress: (line) => { lines.push(line); },
|
|
2203
|
+
});
|
|
2204
|
+
const projectLines = lines.filter((line) => line.includes("Syncing project-proj-first to environment"));
|
|
2205
|
+
expect(projectLines.length).toBeGreaterThan(0);
|
|
2206
|
+
// The transfer landed real bytes, so the terminal line carries the actual
|
|
2207
|
+
// byte total and a 100% completion, not the "0.0 MB" that a discarded
|
|
2208
|
+
// sync result would show.
|
|
2209
|
+
expect(projectLines.some((line) => line.includes("100%"))).toBe(true);
|
|
2210
|
+
expect(projectLines.every((line) => /\(\d+\.\d\/\d+\.\d MB\)/.test(line))).toBe(true);
|
|
2211
|
+
expect(projectLines.some((line) => line.includes("(0.0/0.0 MB)"))).toBe(false);
|
|
2212
|
+
}
|
|
2213
|
+
finally {
|
|
2214
|
+
if (priorFlag === undefined)
|
|
2215
|
+
delete process.env[flagKey];
|
|
2216
|
+
else
|
|
2217
|
+
process.env[flagKey] = priorFlag;
|
|
2218
|
+
}
|
|
2219
|
+
});
|
|
2220
|
+
it("keeps the sandbox runtime core free of Codex-specific string literals", async () => {
|
|
2221
|
+
const coreSource = await readFile(new URL("./sandbox-managed-runtime.ts", import.meta.url), "utf8");
|
|
2222
|
+
// The seam must be generic: no adapter (Codex) knowledge may live in the core.
|
|
2223
|
+
expect(coreSource).not.toMatch(/codex/i);
|
|
2224
|
+
expect(coreSource).not.toMatch(/auth\.json/i);
|
|
2225
|
+
expect(coreSource).not.toMatch(/merge-extract|merge-decision/i);
|
|
2226
|
+
});
|
|
2227
|
+
// End-to-end: drive the WHOLE prepare path — `prepareCommandManagedRuntime`
|
|
2228
|
+
// building the client, syncing the anchor workspace, then staging the
|
|
2229
|
+
// referenced projects — through a runner that runs real shell commands on the
|
|
2230
|
+
// host filesystem (host FS stands in for the sandbox FS). The runner exposes no
|
|
2231
|
+
// native syncIn, so staging rides the base64/tar fallback, the same transport a
|
|
2232
|
+
// provider without native sync uses. In production the kill-switch
|
|
2233
|
+
// `PAPERCLIP_MULTI_PROJECT_WORKSPACE_SYNC` gates whether run prep resolves any
|
|
2234
|
+
// referenced projects (OFF ⇒ none reach this layer). Enable it in-test only to
|
|
2235
|
+
// model the ON scenario, and prove multi-project isolation plus one-failure
|
|
2236
|
+
// isolation end-to-end.
|
|
2237
|
+
it("stages multiple referenced projects into isolated sandbox dirs end-to-end, skipping a failing source", async () => {
|
|
2238
|
+
const flagKey = "PAPERCLIP_MULTI_PROJECT_WORKSPACE_SYNC";
|
|
2239
|
+
const priorFlag = process.env[flagKey];
|
|
2240
|
+
process.env[flagKey] = "1";
|
|
2241
|
+
try {
|
|
2242
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-e2e-additional-"));
|
|
2243
|
+
cleanupDirs.push(rootDir);
|
|
2244
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
2245
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
2246
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
2247
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
2248
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "anchor content\n", "utf8");
|
|
2249
|
+
// Two real referenced-project checkouts (one with a nested file) plus a
|
|
2250
|
+
// deliberately-missing source between them.
|
|
2251
|
+
const first = path.join(rootDir, "referenced-first");
|
|
2252
|
+
const second = path.join(rootDir, "referenced-second");
|
|
2253
|
+
await mkdir(path.join(first, "docs"), { recursive: true });
|
|
2254
|
+
await mkdir(second, { recursive: true });
|
|
2255
|
+
await writeFile(path.join(first, "docs", "guide.md"), "first guide\n", "utf8");
|
|
2256
|
+
await writeFile(path.join(second, "notes.md"), "second notes\n", "utf8");
|
|
2257
|
+
const runner = {
|
|
2258
|
+
execute: (input) => new Promise((resolve) => {
|
|
2259
|
+
const startedAt = new Date().toISOString();
|
|
2260
|
+
const command = input.command === "sh" ? "/bin/sh" : input.command === "bash" ? "/bin/bash" : input.command;
|
|
2261
|
+
const child = spawn(command, input.args ?? [], { cwd: input.cwd, env: { ...process.env, ...input.env } });
|
|
2262
|
+
let stdout = "";
|
|
2263
|
+
let stderr = "";
|
|
2264
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
2265
|
+
child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
|
|
2266
|
+
child.on("error", () => resolve({ exitCode: 127, signal: null, timedOut: false, stdout, stderr, pid: null, startedAt }));
|
|
2267
|
+
child.on("close", (code) => resolve({ exitCode: code ?? 0, signal: null, timedOut: false, stdout, stderr, pid: child.pid ?? null, startedAt }));
|
|
2268
|
+
if (input.stdin != null)
|
|
2269
|
+
child.stdin.write(input.stdin);
|
|
2270
|
+
child.stdin.end();
|
|
2271
|
+
}),
|
|
2272
|
+
};
|
|
2273
|
+
const prepared = await prepareCommandManagedRuntime({
|
|
2274
|
+
runner,
|
|
2275
|
+
spec: { remoteCwd: remoteWorkspaceDir, timeoutMs: 30_000 },
|
|
2276
|
+
adapterKey: "test-adapter",
|
|
2277
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
2278
|
+
additionalSources: [
|
|
2279
|
+
{ localPath: first, projectId: "proj-first", ignoreResolution: { kind: "other" } },
|
|
2280
|
+
{ localPath: path.join(rootDir, "referenced-missing"), projectId: "proj-missing", ignoreResolution: { kind: "other" } },
|
|
2281
|
+
{ localPath: second, projectId: "proj-second", ignoreResolution: { kind: "other" } },
|
|
2282
|
+
],
|
|
2283
|
+
});
|
|
2284
|
+
const runtimeRootDir = path.posix.join(remoteWorkspaceDir, ".paperclip-runtime", "test-adapter");
|
|
2285
|
+
// The anchor workspace synced normally and stays byte-identical.
|
|
2286
|
+
await expect(readFile(path.join(remoteWorkspaceDir, "README.md"), "utf8")).resolves.toBe("anchor content\n");
|
|
2287
|
+
// Each healthy referenced project landed in its OWN isolated dir; the
|
|
2288
|
+
// missing one is skipped, not fatal.
|
|
2289
|
+
expect(Object.keys(prepared.additionalSourceDirs).sort()).toEqual(["proj-first", "proj-second"]);
|
|
2290
|
+
expect(prepared.additionalSourceDirs["proj-first"]).toBe(path.posix.join(runtimeRootDir, "project-proj-first"));
|
|
2291
|
+
expect(prepared.additionalSourceDirs["proj-second"]).toBe(path.posix.join(runtimeRootDir, "project-proj-second"));
|
|
2292
|
+
expect(prepared.additionalSourceDirs["proj-missing"]).toBeUndefined();
|
|
2293
|
+
// The skipped project is a first-class per-project failure outcome, not only a warning, so the
|
|
2294
|
+
// run can count it in the requested-vs-synced accounting. The two healthy projects do not
|
|
2295
|
+
// appear as failures.
|
|
2296
|
+
expect(prepared.additionalSourceFailures.map((failure) => failure.projectId)).toEqual([
|
|
2297
|
+
"proj-missing",
|
|
2298
|
+
]);
|
|
2299
|
+
expect(prepared.additionalSourceFailures[0].error.length).toBeGreaterThan(0);
|
|
2300
|
+
await expect(readFile(path.join(prepared.additionalSourceDirs["proj-first"], "docs", "guide.md"), "utf8"))
|
|
2301
|
+
.resolves.toBe("first guide\n");
|
|
2302
|
+
await expect(readFile(path.join(prepared.additionalSourceDirs["proj-second"], "notes.md"), "utf8"))
|
|
2303
|
+
.resolves.toBe("second notes\n");
|
|
2304
|
+
// Neither project's tree leaked into the anchor workspace or into the other
|
|
2305
|
+
// project's dir.
|
|
2306
|
+
await expect(readFile(path.join(remoteWorkspaceDir, "notes.md"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
|
|
2307
|
+
await expect(readFile(path.join(prepared.additionalSourceDirs["proj-first"], "notes.md"), "utf8")).rejects
|
|
2308
|
+
.toMatchObject({ code: "ENOENT" });
|
|
2309
|
+
await expect(readFile(path.join(runtimeRootDir, "project-proj-missing"), "utf8")).rejects
|
|
2310
|
+
.toMatchObject({ code: "ENOENT" });
|
|
2311
|
+
}
|
|
2312
|
+
finally {
|
|
2313
|
+
if (priorFlag === undefined)
|
|
2314
|
+
delete process.env[flagKey];
|
|
2315
|
+
else
|
|
2316
|
+
process.env[flagKey] = priorFlag;
|
|
2317
|
+
}
|
|
2318
|
+
});
|
|
2319
|
+
describe("resolveReferencedSourceIgnore", () => {
|
|
2320
|
+
it("re-relativizes root-relative ignored paths to a nested localPath", async () => {
|
|
2321
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-nested-"));
|
|
2322
|
+
cleanupDirs.push(rootDir);
|
|
2323
|
+
const repo = path.join(rootDir, "repo");
|
|
2324
|
+
await initGitRepo(repo);
|
|
2325
|
+
// An ignored entry OUTSIDE the referenced project's localPath, and two
|
|
2326
|
+
// ignored entries inside it (one top-level, one nested).
|
|
2327
|
+
await writeFile(path.join(repo, ".gitignore"), "outside-secret.env\npackages/app/secret.env\npackages/app/build/\n", "utf8");
|
|
2328
|
+
await writeFile(path.join(repo, "outside-secret.env"), "outside\n", "utf8");
|
|
2329
|
+
const localPath = path.join(repo, "packages", "app");
|
|
2330
|
+
// Commit a tracked file under `localPath` first. Otherwise the whole
|
|
2331
|
+
// `packages/` directory is untracked, and `git status` collapses it to
|
|
2332
|
+
// one `packages/` line instead of reporting entries inside it
|
|
2333
|
+
// individually — the fixture needs the individual entries.
|
|
2334
|
+
await mkdir(localPath, { recursive: true });
|
|
2335
|
+
await writeFile(path.join(localPath, "index.ts"), "export {};\n", "utf8");
|
|
2336
|
+
await git(repo, ["add", "packages/app/index.ts"]);
|
|
2337
|
+
await git(repo, ["commit", "-qm", "add app"]);
|
|
2338
|
+
await mkdir(path.join(localPath, "build"), { recursive: true });
|
|
2339
|
+
await writeFile(path.join(localPath, "secret.env"), "TOKEN=abc\n", "utf8");
|
|
2340
|
+
await writeFile(path.join(localPath, "build", "out.js"), "artifact\n", "utf8");
|
|
2341
|
+
const resolution = await resolveReferencedSourceIgnore(localPath);
|
|
2342
|
+
// Only the entries under `localPath` apply, re-relativized to it — the
|
|
2343
|
+
// sibling `outside-secret.env` never appears, and the prefix
|
|
2344
|
+
// `packages/app/` is stripped.
|
|
2345
|
+
expect(resolution).toEqual({ kind: "git", ignoredPaths: ["build", "secret.env"] });
|
|
2346
|
+
});
|
|
2347
|
+
it("keeps today's fixed excludes for a non-Git source", async () => {
|
|
2348
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-nongit-"));
|
|
2349
|
+
cleanupDirs.push(rootDir);
|
|
2350
|
+
const plainDir = path.join(rootDir, "plain-project");
|
|
2351
|
+
await mkdir(plainDir, { recursive: true });
|
|
2352
|
+
await writeFile(path.join(plainDir, "file.txt"), "body\n", "utf8");
|
|
2353
|
+
await expect(resolveReferencedSourceIgnore(plainDir)).resolves.toEqual({ kind: "other" });
|
|
2354
|
+
});
|
|
2355
|
+
it("fails closed on a real Git error instead of returning an unfiltered result", async () => {
|
|
2356
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-fail-"));
|
|
2357
|
+
cleanupDirs.push(rootDir);
|
|
2358
|
+
const repo = path.join(rootDir, "repo");
|
|
2359
|
+
await initGitRepo(repo);
|
|
2360
|
+
// Corrupt the index so `git rev-parse --show-toplevel` still succeeds but
|
|
2361
|
+
// `git status --ignored` fails with a real error (not "not a git repository").
|
|
2362
|
+
await writeFile(path.join(repo, ".git", "index"), "not a valid index\n", "utf8");
|
|
2363
|
+
const resolution = await resolveReferencedSourceIgnore(repo);
|
|
2364
|
+
// The reason is the fixed category, never the caught error's own message
|
|
2365
|
+
// (which would embed `repo`, an absolute host path).
|
|
2366
|
+
expect(resolution).toEqual({ kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.scanFailed });
|
|
2367
|
+
});
|
|
2368
|
+
// Nadia's required probes: none of these three example absolute paths —
|
|
2369
|
+
// an ordinary POSIX path, a home directory, and a Windows path — may ever
|
|
2370
|
+
// reach `reason`, however they arrive (a caught Git error, or a raw
|
|
2371
|
+
// toplevel string that makes `localPath` a non-descendant).
|
|
2372
|
+
const SENSITIVE_PATH_PROBES = ["/srv/alice/project", "/home/alice/project", "C:\\Users\\alice\\project"];
|
|
2373
|
+
for (const sensitivePath of SENSITIVE_PATH_PROBES) {
|
|
2374
|
+
it(`redacts a caught Git error embedding ${sensitivePath} to the fixed category`, async () => {
|
|
2375
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-redact-caught-"));
|
|
2376
|
+
cleanupDirs.push(rootDir);
|
|
2377
|
+
const repo = path.join(rootDir, "repo");
|
|
2378
|
+
await initGitRepo(repo);
|
|
2379
|
+
try {
|
|
2380
|
+
setExpensiveWorkspaceGitExecutor(async (input) => {
|
|
2381
|
+
if (input.operation === "referenced_source.ignored_files") {
|
|
2382
|
+
throw Object.assign(new Error(`fatal: unable to read tree object for ${sensitivePath}, pid 4242`), { stderr: `fatal: unable to read tree object for ${sensitivePath}, pid 4242` });
|
|
2383
|
+
}
|
|
2384
|
+
return await runLocalGit(input.localDir, [...input.args], {
|
|
2385
|
+
timeout: input.timeout,
|
|
2386
|
+
maxBuffer: input.maxBuffer,
|
|
2387
|
+
env: input.env,
|
|
2388
|
+
});
|
|
2389
|
+
});
|
|
2390
|
+
const resolution = await resolveReferencedSourceIgnore(repo);
|
|
2391
|
+
expect(resolution).toEqual({ kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.scanFailed });
|
|
2392
|
+
}
|
|
2393
|
+
finally {
|
|
2394
|
+
setExpensiveWorkspaceGitExecutor(null);
|
|
2395
|
+
}
|
|
2396
|
+
});
|
|
2397
|
+
it(`redacts a non-descendant toplevel embedding ${sensitivePath} to the fixed category`, async () => {
|
|
2398
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-redact-nondescendant-"));
|
|
2399
|
+
cleanupDirs.push(rootDir);
|
|
2400
|
+
const localPath = path.join(rootDir, "referenced");
|
|
2401
|
+
await mkdir(localPath, { recursive: true });
|
|
2402
|
+
try {
|
|
2403
|
+
// A toplevel string with no relation to `localPath` — the resolver
|
|
2404
|
+
// must treat it as a non-descendant and fail closed with the fixed
|
|
2405
|
+
// category, never a message built from `sensitivePath` or `localPath`.
|
|
2406
|
+
setExpensiveWorkspaceGitExecutor(async (input) => {
|
|
2407
|
+
if (input.operation === "referenced_source.toplevel") {
|
|
2408
|
+
return { stdout: `${sensitivePath}\n`, stderr: "" };
|
|
2409
|
+
}
|
|
2410
|
+
return { stdout: "", stderr: "" };
|
|
2411
|
+
});
|
|
2412
|
+
const resolution = await resolveReferencedSourceIgnore(localPath);
|
|
2413
|
+
expect(resolution).toEqual({ kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.toplevelNotDescendant });
|
|
2414
|
+
}
|
|
2415
|
+
finally {
|
|
2416
|
+
setExpensiveWorkspaceGitExecutor(null);
|
|
2417
|
+
}
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
it("fails closed with a fixed category when the parsed ignored-entry count exceeds the bound", async () => {
|
|
2421
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-bound-count-"));
|
|
2422
|
+
cleanupDirs.push(rootDir);
|
|
2423
|
+
const repo = path.join(rootDir, "repo");
|
|
2424
|
+
await initGitRepo(repo);
|
|
2425
|
+
const overLimitCount = 10_001;
|
|
2426
|
+
const syntheticIgnored = `${Array.from({ length: overLimitCount }, (_, index) => `!! entry-${index}`).join("\0")}\0`;
|
|
2427
|
+
try {
|
|
2428
|
+
setExpensiveWorkspaceGitExecutor(async (input) => {
|
|
2429
|
+
if (input.operation === "referenced_source.ignored_files") {
|
|
2430
|
+
return { stdout: syntheticIgnored, stderr: "" };
|
|
2431
|
+
}
|
|
2432
|
+
return await runLocalGit(input.localDir, [...input.args], {
|
|
2433
|
+
timeout: input.timeout,
|
|
2434
|
+
maxBuffer: input.maxBuffer,
|
|
2435
|
+
env: input.env,
|
|
2436
|
+
});
|
|
2437
|
+
});
|
|
2438
|
+
const resolution = await resolveReferencedSourceIgnore(repo);
|
|
2439
|
+
expect(resolution).toEqual({ kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.limitExceeded });
|
|
2440
|
+
}
|
|
2441
|
+
finally {
|
|
2442
|
+
setExpensiveWorkspaceGitExecutor(null);
|
|
2443
|
+
}
|
|
2444
|
+
});
|
|
2445
|
+
it("fails closed with a fixed category when the total UTF-8 byte size of ignored paths exceeds the bound", async () => {
|
|
2446
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-bound-bytes-"));
|
|
2447
|
+
cleanupDirs.push(rootDir);
|
|
2448
|
+
const repo = path.join(rootDir, "repo");
|
|
2449
|
+
await initGitRepo(repo);
|
|
2450
|
+
// One entry alone exceeds the 2 MiB bound, well under the entry-count bound.
|
|
2451
|
+
const hugeEntry = "a".repeat(3 * 1024 * 1024);
|
|
2452
|
+
const syntheticIgnored = `!! ${hugeEntry}\0`;
|
|
2453
|
+
try {
|
|
2454
|
+
setExpensiveWorkspaceGitExecutor(async (input) => {
|
|
2455
|
+
if (input.operation === "referenced_source.ignored_files") {
|
|
2456
|
+
return { stdout: syntheticIgnored, stderr: "" };
|
|
2457
|
+
}
|
|
2458
|
+
return await runLocalGit(input.localDir, [...input.args], {
|
|
2459
|
+
timeout: input.timeout,
|
|
2460
|
+
maxBuffer: input.maxBuffer,
|
|
2461
|
+
env: input.env,
|
|
2462
|
+
});
|
|
2463
|
+
});
|
|
2464
|
+
const resolution = await resolveReferencedSourceIgnore(repo);
|
|
2465
|
+
expect(resolution).toEqual({ kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.limitExceeded });
|
|
2466
|
+
}
|
|
2467
|
+
finally {
|
|
2468
|
+
setExpensiveWorkspaceGitExecutor(null);
|
|
2469
|
+
}
|
|
2470
|
+
});
|
|
2471
|
+
it("stages no bytes for either a count-breach or a byte-breach project, and stages a healthy sibling", async () => {
|
|
2472
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-bound-staging-"));
|
|
2473
|
+
cleanupDirs.push(rootDir);
|
|
2474
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
2475
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
2476
|
+
const healthyDir = path.join(rootDir, "referenced-healthy");
|
|
2477
|
+
const countBreachDir = path.join(rootDir, "referenced-count-breach");
|
|
2478
|
+
const byteBreachDir = path.join(rootDir, "referenced-byte-breach");
|
|
2479
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
2480
|
+
await mkdir(healthyDir, { recursive: true });
|
|
2481
|
+
await mkdir(countBreachDir, { recursive: true });
|
|
2482
|
+
await mkdir(byteBreachDir, { recursive: true });
|
|
2483
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "anchor\n", "utf8");
|
|
2484
|
+
await writeFile(path.join(healthyDir, "notes.md"), "healthy\n", "utf8");
|
|
2485
|
+
await writeFile(path.join(countBreachDir, "should-never-ship.txt"), "must not stage\n", "utf8");
|
|
2486
|
+
await writeFile(path.join(byteBreachDir, "should-never-ship.txt"), "must not stage\n", "utf8");
|
|
2487
|
+
const countBreachReason = { kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.limitExceeded };
|
|
2488
|
+
const byteBreachReason = { kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.limitExceeded };
|
|
2489
|
+
const prepared = await prepareCommandManagedRuntime({
|
|
2490
|
+
runner: makeInlineSpawnRunner(),
|
|
2491
|
+
spec: { remoteCwd: remoteWorkspaceDir, timeoutMs: 30_000 },
|
|
2492
|
+
adapterKey: "test-adapter",
|
|
2493
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
2494
|
+
additionalSources: [
|
|
2495
|
+
{ localPath: healthyDir, projectId: "healthy", ignoreResolution: { kind: "other" } },
|
|
2496
|
+
{ localPath: countBreachDir, projectId: "count-breach", ignoreResolution: countBreachReason },
|
|
2497
|
+
{ localPath: byteBreachDir, projectId: "byte-breach", ignoreResolution: byteBreachReason },
|
|
2498
|
+
],
|
|
2499
|
+
});
|
|
2500
|
+
expect(Object.keys(prepared.additionalSourceDirs).sort()).toEqual(["healthy"]);
|
|
2501
|
+
expect(prepared.additionalSourceFailures.map((failure) => failure.projectId).sort()).toEqual([
|
|
2502
|
+
"byte-breach",
|
|
2503
|
+
"count-breach",
|
|
2504
|
+
]);
|
|
2505
|
+
const runtimeRootDir = path.posix.join(remoteWorkspaceDir, ".paperclip-runtime", "test-adapter");
|
|
2506
|
+
await expect(readFile(path.join(runtimeRootDir, "project-count-breach", "should-never-ship.txt"), "utf8")).rejects
|
|
2507
|
+
.toMatchObject({ code: "ENOENT" });
|
|
2508
|
+
await expect(readFile(path.join(runtimeRootDir, "project-byte-breach", "should-never-ship.txt"), "utf8")).rejects
|
|
2509
|
+
.toMatchObject({ code: "ENOENT" });
|
|
2510
|
+
});
|
|
2511
|
+
describe("saturation retry", () => {
|
|
2512
|
+
afterEach(() => {
|
|
2513
|
+
vi.useRealTimers();
|
|
2514
|
+
setExpensiveWorkspaceGitExecutor(null);
|
|
2515
|
+
});
|
|
2516
|
+
function throwSaturated() {
|
|
2517
|
+
throw Object.assign(new Error("Changed files are temporarily unavailable because the Git scan queue is full"), { code: WORKSPACE_GIT_SCAN_SATURATED_CODE });
|
|
2518
|
+
}
|
|
2519
|
+
// Every case here synthesizes BOTH scan operations at the executor seam
|
|
2520
|
+
// instead of spawning real `git` — the property under test is the
|
|
2521
|
+
// retry's own timing and attempt count, and a fake-timer-driven test
|
|
2522
|
+
// must not also depend on a real child process's independent, real-time
|
|
2523
|
+
// completion. `toplevel` need not exist on disk: `resolveReferencedSourceIgnore`
|
|
2524
|
+
// falls back to a plain string compare when `fs.realpath` fails, and the
|
|
2525
|
+
// fixture path is used unchanged on both sides of that compare.
|
|
2526
|
+
const repo = "/fixture/referenced-project";
|
|
2527
|
+
it("retries a saturated scan up to two times and succeeds on the third attempt", async () => {
|
|
2528
|
+
let ignoredCallCount = 0;
|
|
2529
|
+
setExpensiveWorkspaceGitExecutor(async (input) => {
|
|
2530
|
+
if (input.operation === "referenced_source.toplevel") {
|
|
2531
|
+
return { stdout: `${repo}\n`, stderr: "" };
|
|
2532
|
+
}
|
|
2533
|
+
ignoredCallCount += 1;
|
|
2534
|
+
if (ignoredCallCount <= 2)
|
|
2535
|
+
throwSaturated();
|
|
2536
|
+
return { stdout: "", stderr: "" };
|
|
2537
|
+
});
|
|
2538
|
+
vi.useFakeTimers();
|
|
2539
|
+
const resolutionPromise = resolveReferencedSourceIgnore(repo);
|
|
2540
|
+
// Bounded backoff: none before attempt 1, 1 s before attempt 2, 2 s
|
|
2541
|
+
// before attempt 3.
|
|
2542
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
2543
|
+
await vi.advanceTimersByTimeAsync(2_000);
|
|
2544
|
+
const resolution = await resolutionPromise;
|
|
2545
|
+
expect(resolution).toEqual({ kind: "git", ignoredPaths: [] });
|
|
2546
|
+
expect(ignoredCallCount).toBe(3);
|
|
2547
|
+
});
|
|
2548
|
+
it("fails closed after three saturated attempts, with no further Git invocation", async () => {
|
|
2549
|
+
let ignoredCallCount = 0;
|
|
2550
|
+
setExpensiveWorkspaceGitExecutor(async (input) => {
|
|
2551
|
+
if (input.operation === "referenced_source.toplevel") {
|
|
2552
|
+
return { stdout: `${repo}\n`, stderr: "" };
|
|
2553
|
+
}
|
|
2554
|
+
ignoredCallCount += 1;
|
|
2555
|
+
throwSaturated();
|
|
2556
|
+
});
|
|
2557
|
+
vi.useFakeTimers();
|
|
2558
|
+
const resolutionPromise = resolveReferencedSourceIgnore(repo);
|
|
2559
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
2560
|
+
await vi.advanceTimersByTimeAsync(2_000);
|
|
2561
|
+
const resolution = await resolutionPromise;
|
|
2562
|
+
expect(resolution).toEqual({ kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.scanFailed });
|
|
2563
|
+
// Three total attempts (the first plus two retries) — no fourth,
|
|
2564
|
+
// unscheduled invocation past the retry budget.
|
|
2565
|
+
expect(ignoredCallCount).toBe(3);
|
|
2566
|
+
});
|
|
2567
|
+
it("makes exactly one attempt and fails closed on a timeout, never retrying it", async () => {
|
|
2568
|
+
let ignoredCallCount = 0;
|
|
2569
|
+
setExpensiveWorkspaceGitExecutor(async (input) => {
|
|
2570
|
+
if (input.operation === "referenced_source.toplevel") {
|
|
2571
|
+
return { stdout: `${repo}\n`, stderr: "" };
|
|
2572
|
+
}
|
|
2573
|
+
ignoredCallCount += 1;
|
|
2574
|
+
throw Object.assign(new Error("Workspace Git scan timed out after 8000ms"), {
|
|
2575
|
+
code: "workspace_git_scan_timeout",
|
|
2576
|
+
});
|
|
2577
|
+
});
|
|
2578
|
+
const resolution = await resolveReferencedSourceIgnore(repo);
|
|
2579
|
+
expect(resolution).toEqual({ kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.scanFailed });
|
|
2580
|
+
expect(ignoredCallCount).toBe(1);
|
|
2581
|
+
});
|
|
2582
|
+
it("makes exactly one attempt and fails closed on an output-limit breach, never retrying it", async () => {
|
|
2583
|
+
let ignoredCallCount = 0;
|
|
2584
|
+
setExpensiveWorkspaceGitExecutor(async (input) => {
|
|
2585
|
+
if (input.operation === "referenced_source.toplevel") {
|
|
2586
|
+
return { stdout: `${repo}\n`, stderr: "" };
|
|
2587
|
+
}
|
|
2588
|
+
ignoredCallCount += 1;
|
|
2589
|
+
throw Object.assign(new Error("Workspace Git scan exceeded its output limit"), {
|
|
2590
|
+
code: "workspace_git_scan_output_limit",
|
|
2591
|
+
});
|
|
2592
|
+
});
|
|
2593
|
+
const resolution = await resolveReferencedSourceIgnore(repo);
|
|
2594
|
+
expect(resolution).toEqual({ kind: "failed", reason: REFERENCED_SOURCE_IGNORE_FAILURE_REASONS.scanFailed });
|
|
2595
|
+
expect(ignoredCallCount).toBe(1);
|
|
2596
|
+
});
|
|
2597
|
+
});
|
|
2598
|
+
});
|
|
2599
|
+
it("escapes glob metacharacters so a literal ignored path never over-excludes a sibling", async () => {
|
|
2600
|
+
// A bare tar `--exclude` pattern treats `*`/`?`/`[` as globs. Without
|
|
2601
|
+
// escaping, an ignored file named `secret[1].txt` would exclude the
|
|
2602
|
+
// unrelated sibling `secret1.txt` too (both match the glob `secret[1].txt`,
|
|
2603
|
+
// whose `[1]` is a one-character class matching the literal digit `1`).
|
|
2604
|
+
expect(escapeTarExcludeLiteral("secret[1].txt")).toBe("secret\\[1].txt");
|
|
2605
|
+
expect(escapeTarExcludeLiteral("wildcard*name")).toBe("wildcard\\*name");
|
|
2606
|
+
expect(escapeTarExcludeLiteral("question?mark")).toBe("question\\?mark");
|
|
2607
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-glob-"));
|
|
2608
|
+
cleanupDirs.push(rootDir);
|
|
2609
|
+
const referencedDir = path.join(rootDir, "referenced-project");
|
|
2610
|
+
await initGitRepo(referencedDir);
|
|
2611
|
+
// The gitignore pattern itself escapes `[` and `]` (gitignore patterns are
|
|
2612
|
+
// globs too), so it ignores ONLY the literal file `secret[1].txt`.
|
|
2613
|
+
await writeFile(path.join(referencedDir, ".gitignore"), "secret\\[1\\].txt\n", "utf8");
|
|
2614
|
+
await writeFile(path.join(referencedDir, "secret[1].txt"), "ignored\n", "utf8");
|
|
2615
|
+
// A sibling that would ALSO match the UNESCAPED tar exclude glob
|
|
2616
|
+
// `secret[1].txt` (its `[1]` is a one-character class matching `1`), if the
|
|
2617
|
+
// staging path failed to escape the ignored entry before passing it to tar.
|
|
2618
|
+
await writeFile(path.join(referencedDir, "secret1.txt"), "must stay\n", "utf8");
|
|
2619
|
+
const ignoreResolution = await resolveReferencedSourceIgnore(referencedDir);
|
|
2620
|
+
expect(ignoreResolution).toEqual({ kind: "git", ignoredPaths: ["secret[1].txt"] });
|
|
2621
|
+
const prepared = await stageOneReferencedProject(referencedDir, ignoreResolution);
|
|
2622
|
+
const stagedDir = prepared.additionalSourceDirs["proj"];
|
|
2623
|
+
await expect(readFile(path.join(stagedDir, "secret[1].txt"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
|
|
2624
|
+
await expect(readFile(path.join(stagedDir, "secret1.txt"), "utf8")).resolves.toBe("must stay\n");
|
|
2625
|
+
});
|
|
2626
|
+
it("never ships a Git-ignored secret in a referenced project's staged tree", async () => {
|
|
2627
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-secret-"));
|
|
2628
|
+
cleanupDirs.push(rootDir);
|
|
2629
|
+
const referencedDir = path.join(rootDir, "referenced-project");
|
|
2630
|
+
await initGitRepo(referencedDir);
|
|
2631
|
+
await writeFile(path.join(referencedDir, ".gitignore"), "secret.env\n", "utf8");
|
|
2632
|
+
await writeFile(path.join(referencedDir, "secret.env"), "TOKEN=abc\n", "utf8");
|
|
2633
|
+
await writeFile(path.join(referencedDir, "tracked.md"), "kept\n", "utf8");
|
|
2634
|
+
const ignoreResolution = await resolveReferencedSourceIgnore(referencedDir);
|
|
2635
|
+
expect(ignoreResolution).toEqual({ kind: "git", ignoredPaths: ["secret.env"] });
|
|
2636
|
+
const prepared = await stageOneReferencedProject(referencedDir, ignoreResolution);
|
|
2637
|
+
const stagedDir = prepared.additionalSourceDirs["proj"];
|
|
2638
|
+
await expect(readFile(path.join(stagedDir, "secret.env"), "utf8")).rejects.toMatchObject({ code: "ENOENT" });
|
|
2639
|
+
await expect(readFile(path.join(stagedDir, "tracked.md"), "utf8")).resolves.toBe("kept\n");
|
|
2640
|
+
});
|
|
2641
|
+
it("stages no bytes for a project whose ignore resolution failed, and stages the rest", async () => {
|
|
2642
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-ignore-failed-skip-"));
|
|
2643
|
+
cleanupDirs.push(rootDir);
|
|
2644
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
2645
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
2646
|
+
const healthyDir = path.join(rootDir, "referenced-healthy");
|
|
2647
|
+
const failedDir = path.join(rootDir, "referenced-failed");
|
|
2648
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
2649
|
+
await mkdir(healthyDir, { recursive: true });
|
|
2650
|
+
await mkdir(failedDir, { recursive: true });
|
|
2651
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "anchor\n", "utf8");
|
|
2652
|
+
await writeFile(path.join(healthyDir, "notes.md"), "healthy\n", "utf8");
|
|
2653
|
+
await writeFile(path.join(failedDir, "should-never-ship.txt"), "must not stage\n", "utf8");
|
|
2654
|
+
const prepared = await prepareCommandManagedRuntime({
|
|
2655
|
+
runner: makeInlineSpawnRunner(),
|
|
2656
|
+
spec: { remoteCwd: remoteWorkspaceDir, timeoutMs: 30_000 },
|
|
2657
|
+
adapterKey: "test-adapter",
|
|
2658
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
2659
|
+
additionalSources: [
|
|
2660
|
+
{ localPath: healthyDir, projectId: "healthy", ignoreResolution: { kind: "other" } },
|
|
2661
|
+
{ localPath: failedDir, projectId: "failed", ignoreResolution: { kind: "failed", reason: "boom: git status timed out" } },
|
|
2662
|
+
],
|
|
2663
|
+
});
|
|
2664
|
+
// The failed project is not staged at all (fail closed) — no bytes reach the
|
|
2665
|
+
// sandbox for it — and is recorded as a first-class failure. The healthy
|
|
2666
|
+
// project stages normally.
|
|
2667
|
+
expect(Object.keys(prepared.additionalSourceDirs)).toEqual(["healthy"]);
|
|
2668
|
+
expect(prepared.additionalSourceFailures.map((failure) => failure.projectId)).toEqual(["failed"]);
|
|
2669
|
+
expect(prepared.additionalSourceFailures[0].error).toContain("boom: git status timed out");
|
|
2670
|
+
const runtimeRootDir = path.posix.join(remoteWorkspaceDir, ".paperclip-runtime", "test-adapter");
|
|
2671
|
+
await expect(readFile(path.join(runtimeRootDir, "project-failed", "should-never-ship.txt"), "utf8")).rejects
|
|
2672
|
+
.toMatchObject({ code: "ENOENT" });
|
|
2673
|
+
});
|
|
2674
|
+
it("builds the workspace tarball inside one host pack span for a usual workspace sync", async () => {
|
|
2675
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-pack-span-"));
|
|
2676
|
+
cleanupDirs.push(rootDir);
|
|
2677
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
2678
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
2679
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
2680
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace body\n", "utf8");
|
|
2681
|
+
// Record every span name the runner opens and run the wrapped work, so the
|
|
2682
|
+
// test proves the host opens a span around each host-side staging sub-step
|
|
2683
|
+
// for the usual (plain) workspace sync: the git enumeration, the baseline
|
|
2684
|
+
// content-hash walk, and the tarball build, in that order.
|
|
2685
|
+
const openedSpans = [];
|
|
2686
|
+
const runtimeSpan = async (name, work) => {
|
|
2687
|
+
openedSpans.push(name);
|
|
2688
|
+
return await work();
|
|
2689
|
+
};
|
|
2690
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
2691
|
+
spec: {
|
|
2692
|
+
transport: "sandbox",
|
|
2693
|
+
provider: "test",
|
|
2694
|
+
sandboxId: "sandbox-pack",
|
|
2695
|
+
remoteCwd: remoteWorkspaceDir,
|
|
2696
|
+
timeoutMs: 30_000,
|
|
2697
|
+
apiKey: null,
|
|
2698
|
+
},
|
|
2699
|
+
adapterKey: "test-adapter",
|
|
2700
|
+
client: makeFilesystemClient(),
|
|
2701
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
2702
|
+
runtimeSpan,
|
|
2703
|
+
});
|
|
2704
|
+
// The workspace stage task opens its own `stage.workspace` span, and the
|
|
2705
|
+
// host tarball build opens the `pack` span inside it. The two pre-task
|
|
2706
|
+
// sub-steps stay ahead of the task.
|
|
2707
|
+
expect(openedSpans).toEqual(["snapshot.git", "snapshot.baseline", "stage.workspace", "pack"]);
|
|
2708
|
+
// The tarball build still lands the workspace inside the span, so the wrap
|
|
2709
|
+
// changes no staging behavior.
|
|
2710
|
+
await expect(readFile(path.join(remoteWorkspaceDir, "README.md"), "utf8")).resolves.toBe("workspace body\n");
|
|
2711
|
+
expect(prepared.workspaceRemoteDir).toBe(remoteWorkspaceDir);
|
|
2712
|
+
});
|
|
2713
|
+
it("nests the host pack span under the stage.workspace task span", async () => {
|
|
2714
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-pack-nest-"));
|
|
2715
|
+
cleanupDirs.push(rootDir);
|
|
2716
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
2717
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
2718
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
2719
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace body\n", "utf8");
|
|
2720
|
+
const { traceContext, spans } = createRecordingTraceContext();
|
|
2721
|
+
// The root span stands in for `sandbox.startup`. Its child context is the
|
|
2722
|
+
// step span's parent, exactly as the executor wires it.
|
|
2723
|
+
const rootHandle = traceContext.tracer.startSpan("sandbox.startup", undefined, undefined);
|
|
2724
|
+
const rootContext = traceContext.contextWithSpan(rootHandle);
|
|
2725
|
+
// The stage runner parents each span to the ACTIVE startup step, so the
|
|
2726
|
+
// `pack` span nests under `stage.sync`. This is the exact runner the
|
|
2727
|
+
// executor threads into the staging seam.
|
|
2728
|
+
const stageRuntimeSpan = createRuntimeSpanRunner(traceContext, () => getActiveStepContext()?.parentContext);
|
|
2729
|
+
// A deterministic monotonic clock, so the step timing stays test-stable.
|
|
2730
|
+
let clock = 0;
|
|
2731
|
+
const now = () => (clock += 1000);
|
|
2732
|
+
await measureStartupStep({}, now, "stage.sync", async () => {
|
|
2733
|
+
await prepareSandboxManagedRuntime({
|
|
2734
|
+
spec: {
|
|
2735
|
+
transport: "sandbox",
|
|
2736
|
+
provider: "test",
|
|
2737
|
+
sandboxId: "sandbox-nest",
|
|
2738
|
+
remoteCwd: remoteWorkspaceDir,
|
|
2739
|
+
timeoutMs: 30_000,
|
|
2740
|
+
apiKey: null,
|
|
2741
|
+
},
|
|
2742
|
+
adapterKey: "test-adapter",
|
|
2743
|
+
client: makeFilesystemClient(),
|
|
2744
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
2745
|
+
runtimeSpan: stageRuntimeSpan,
|
|
2746
|
+
});
|
|
2747
|
+
}, {
|
|
2748
|
+
tracer: traceContext.tracer,
|
|
2749
|
+
parentContext: rootContext,
|
|
2750
|
+
contextWithSpan: (span) => traceContext.contextWithSpan(span),
|
|
2751
|
+
});
|
|
2752
|
+
const stageSpan = spans.find((span) => span.name === "stage.sync");
|
|
2753
|
+
const workspaceSpan = spans.find((span) => span.name === "stage.workspace");
|
|
2754
|
+
const packSpan = spans.find((span) => span.name === "pack");
|
|
2755
|
+
expect(stageSpan).toBeDefined();
|
|
2756
|
+
expect(workspaceSpan).toBeDefined();
|
|
2757
|
+
expect(packSpan).toBeDefined();
|
|
2758
|
+
expect(packSpan.ended).toBe(true);
|
|
2759
|
+
// The workspace stage task opens its own `stage.workspace` span under
|
|
2760
|
+
// `stage.sync`, and the `pack` span nests under `stage.workspace`.
|
|
2761
|
+
expect(workspaceSpan.parentName).toBe("stage.sync");
|
|
2762
|
+
expect(packSpan.parentName).toBe("stage.workspace");
|
|
2763
|
+
// The two pre-`pack` staging sub-steps nest under `stage.sync` the same way,
|
|
2764
|
+
// so the previously hidden gap at the head of the step is now attributed.
|
|
2765
|
+
for (const name of ["snapshot.git", "snapshot.baseline"]) {
|
|
2766
|
+
const span = spans.find((candidate) => candidate.name === name);
|
|
2767
|
+
expect(span, name).toBeDefined();
|
|
2768
|
+
expect(span.ended).toBe(true);
|
|
2769
|
+
expect(span.parentName).toBe("stage.sync");
|
|
2770
|
+
}
|
|
2771
|
+
});
|
|
2772
|
+
});
|
|
2773
|
+
function defer() {
|
|
2774
|
+
let resolve;
|
|
2775
|
+
let reject;
|
|
2776
|
+
const promise = new Promise((res, rej) => {
|
|
2777
|
+
resolve = res;
|
|
2778
|
+
reject = rej;
|
|
2779
|
+
});
|
|
2780
|
+
return { promise, resolve, reject };
|
|
2781
|
+
}
|
|
2782
|
+
function settleTick(ms) {
|
|
2783
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2784
|
+
}
|
|
2785
|
+
// A span recorder that captures each opened span name and the set of spans that
|
|
2786
|
+
// are open right now. `opened` records every span in open order. `openNow` holds
|
|
2787
|
+
// the names of the spans that started but did not end yet, so a test proves two
|
|
2788
|
+
// concurrent tasks keep their spans open at the same time.
|
|
2789
|
+
function makeSpanRecorder() {
|
|
2790
|
+
const opened = [];
|
|
2791
|
+
const openNow = new Set();
|
|
2792
|
+
const runtimeSpan = async (name, work) => {
|
|
2793
|
+
opened.push(name);
|
|
2794
|
+
openNow.add(name);
|
|
2795
|
+
try {
|
|
2796
|
+
return await work();
|
|
2797
|
+
}
|
|
2798
|
+
finally {
|
|
2799
|
+
openNow.delete(name);
|
|
2800
|
+
}
|
|
2801
|
+
};
|
|
2802
|
+
return { runtimeSpan, opened, openNow };
|
|
2803
|
+
}
|
|
2804
|
+
function labelOfOperation(operation) {
|
|
2805
|
+
const bases = operation.files.map((mapping) => path.posix.basename(mapping.targetPath));
|
|
2806
|
+
if (bases.some((base) => base === "workspace-upload.tar" || base === "git-workspace-upload.tar")) {
|
|
2807
|
+
return "workspace";
|
|
2808
|
+
}
|
|
2809
|
+
const assetBase = bases.find((base) => base.endsWith("-upload.tar"));
|
|
2810
|
+
if (assetBase) {
|
|
2811
|
+
return assetBase.slice(0, -"-upload.tar".length);
|
|
2812
|
+
}
|
|
2813
|
+
const projectBase = bases.find((base) => base.startsWith("project-"));
|
|
2814
|
+
if (projectBase) {
|
|
2815
|
+
return projectBase;
|
|
2816
|
+
}
|
|
2817
|
+
return bases[0] ?? operation.operationId;
|
|
2818
|
+
}
|
|
2819
|
+
function makeControlledSyncClient(options) {
|
|
2820
|
+
const started = [];
|
|
2821
|
+
const settled = [];
|
|
2822
|
+
const gates = new Map();
|
|
2823
|
+
const failures = new Map();
|
|
2824
|
+
const startWaiters = new Map();
|
|
2825
|
+
const control = {
|
|
2826
|
+
started,
|
|
2827
|
+
settled,
|
|
2828
|
+
waitForStart(label) {
|
|
2829
|
+
if (started.includes(label)) {
|
|
2830
|
+
return Promise.resolve();
|
|
2831
|
+
}
|
|
2832
|
+
return new Promise((resolve) => {
|
|
2833
|
+
const waiters = startWaiters.get(label) ?? [];
|
|
2834
|
+
waiters.push(resolve);
|
|
2835
|
+
startWaiters.set(label, waiters);
|
|
2836
|
+
});
|
|
2837
|
+
},
|
|
2838
|
+
hold(label) {
|
|
2839
|
+
if (!gates.has(label)) {
|
|
2840
|
+
gates.set(label, defer());
|
|
2841
|
+
}
|
|
2842
|
+
},
|
|
2843
|
+
release(label) {
|
|
2844
|
+
gates.get(label)?.resolve();
|
|
2845
|
+
},
|
|
2846
|
+
failWith(label, error) {
|
|
2847
|
+
failures.set(label, error);
|
|
2848
|
+
gates.get(label)?.resolve();
|
|
2849
|
+
},
|
|
2850
|
+
};
|
|
2851
|
+
const noop = async () => { };
|
|
2852
|
+
const client = {
|
|
2853
|
+
makeDir: noop,
|
|
2854
|
+
writeFile: noop,
|
|
2855
|
+
readFile: async () => Buffer.alloc(0),
|
|
2856
|
+
listFiles: async () => [],
|
|
2857
|
+
remove: noop,
|
|
2858
|
+
run: noop,
|
|
2859
|
+
allowConcurrentSyncOperations: options.concurrent,
|
|
2860
|
+
syncIn: async (operations) => {
|
|
2861
|
+
const operation = operations[0];
|
|
2862
|
+
const label = labelOfOperation(operation);
|
|
2863
|
+
started.push(label);
|
|
2864
|
+
const waiters = startWaiters.get(label) ?? [];
|
|
2865
|
+
startWaiters.delete(label);
|
|
2866
|
+
for (const waiter of waiters) {
|
|
2867
|
+
waiter();
|
|
2868
|
+
}
|
|
2869
|
+
try {
|
|
2870
|
+
const gate = gates.get(label);
|
|
2871
|
+
if (gate) {
|
|
2872
|
+
await gate.promise;
|
|
2873
|
+
}
|
|
2874
|
+
const failure = failures.get(label);
|
|
2875
|
+
if (failure) {
|
|
2876
|
+
throw failure;
|
|
2877
|
+
}
|
|
2878
|
+
return {
|
|
2879
|
+
operations: [{
|
|
2880
|
+
operationId: operation.operationId,
|
|
2881
|
+
filesTransferred: operation.files.length,
|
|
2882
|
+
bytesTransferred: 0,
|
|
2883
|
+
}],
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
finally {
|
|
2887
|
+
settled.push(label);
|
|
2888
|
+
}
|
|
2889
|
+
},
|
|
2890
|
+
};
|
|
2891
|
+
return { client, control };
|
|
2892
|
+
}
|
|
2893
|
+
describe("sandbox managed runtime inbound coordinator", () => {
|
|
2894
|
+
const cleanupDirs = [];
|
|
2895
|
+
afterEach(async () => {
|
|
2896
|
+
while (cleanupDirs.length > 0) {
|
|
2897
|
+
const dir = cleanupDirs.pop();
|
|
2898
|
+
if (!dir)
|
|
2899
|
+
continue;
|
|
2900
|
+
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
2901
|
+
}
|
|
2902
|
+
});
|
|
2903
|
+
function makeSpec(remoteCwd) {
|
|
2904
|
+
return {
|
|
2905
|
+
transport: "sandbox",
|
|
2906
|
+
provider: "test",
|
|
2907
|
+
sandboxId: "sandbox-1",
|
|
2908
|
+
remoteCwd,
|
|
2909
|
+
timeoutMs: 30_000,
|
|
2910
|
+
apiKey: null,
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
// Create a temp root with one workspace directory and any named asset/project
|
|
2914
|
+
// directories. Each directory carries one file, so the host tar step has bytes.
|
|
2915
|
+
async function makeInboundDirs(names) {
|
|
2916
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-inbound-coordinator-"));
|
|
2917
|
+
cleanupDirs.push(rootDir);
|
|
2918
|
+
const workspaceDir = path.join(rootDir, "workspace");
|
|
2919
|
+
await mkdir(workspaceDir, { recursive: true });
|
|
2920
|
+
await writeFile(path.join(workspaceDir, "file.txt"), "workspace\n", "utf8");
|
|
2921
|
+
const dirs = new Map();
|
|
2922
|
+
for (const name of names) {
|
|
2923
|
+
const dir = path.join(rootDir, name);
|
|
2924
|
+
await mkdir(dir, { recursive: true });
|
|
2925
|
+
await writeFile(path.join(dir, "file.txt"), `${name}\n`, "utf8");
|
|
2926
|
+
dirs.set(name, dir);
|
|
2927
|
+
}
|
|
2928
|
+
return { rootDir, workspaceDir, dirOf: (name) => dirs.get(name) };
|
|
2929
|
+
}
|
|
2930
|
+
// Resolve true when the label started within the window, false on timeout.
|
|
2931
|
+
async function startedWithin(control, label, ms) {
|
|
2932
|
+
return Promise.race([
|
|
2933
|
+
control.waitForStart(label).then(() => true),
|
|
2934
|
+
settleTick(ms).then(() => false),
|
|
2935
|
+
]);
|
|
2936
|
+
}
|
|
2937
|
+
it("with concurrency permitted, the home asset upload starts while the workspace upload is held open", async () => {
|
|
2938
|
+
const { workspaceDir, dirOf } = await makeInboundDirs(["home"]);
|
|
2939
|
+
const { client, control } = makeControlledSyncClient({ concurrent: true });
|
|
2940
|
+
control.hold("workspace");
|
|
2941
|
+
const prepared = prepareSandboxManagedRuntime({
|
|
2942
|
+
spec: makeSpec("/remote/cwd"),
|
|
2943
|
+
adapterKey: "codex",
|
|
2944
|
+
client,
|
|
2945
|
+
workspaceLocalDir: workspaceDir,
|
|
2946
|
+
assets: [{ key: "home", localDir: dirOf("home") }],
|
|
2947
|
+
});
|
|
2948
|
+
prepared.catch(() => undefined);
|
|
2949
|
+
// The workspace upload is open and held. The home asset upload still starts,
|
|
2950
|
+
// so the coordinator runs the two operations concurrently.
|
|
2951
|
+
await control.waitForStart("workspace");
|
|
2952
|
+
expect(await startedWithin(control, "home", 4000)).toBe(true);
|
|
2953
|
+
expect(control.settled).not.toContain("workspace");
|
|
2954
|
+
control.release("workspace");
|
|
2955
|
+
await prepared;
|
|
2956
|
+
expect(control.settled).toContain("home");
|
|
2957
|
+
});
|
|
2958
|
+
it("with five operations and the bound of 4, the fifth operation does not start while four stay open", async () => {
|
|
2959
|
+
expect(SYNC_OPERATION_CONCURRENCY_LIMIT).toBe(4);
|
|
2960
|
+
const assetKeys = Array.from({ length: SYNC_OPERATION_CONCURRENCY_LIMIT + 1 }, (_unused, index) => `asset-${index}`);
|
|
2961
|
+
const { workspaceDir, dirOf } = await makeInboundDirs(assetKeys);
|
|
2962
|
+
const { client, control } = makeControlledSyncClient({ concurrent: true });
|
|
2963
|
+
for (const key of assetKeys) {
|
|
2964
|
+
control.hold(key);
|
|
2965
|
+
}
|
|
2966
|
+
const prepared = prepareSandboxManagedRuntime({
|
|
2967
|
+
spec: makeSpec("/remote/cwd"),
|
|
2968
|
+
adapterKey: "codex",
|
|
2969
|
+
client,
|
|
2970
|
+
syncWorkspace: false,
|
|
2971
|
+
workspaceLocalDir: workspaceDir,
|
|
2972
|
+
assets: assetKeys.map((key) => ({ key, localDir: dirOf(key) })),
|
|
2973
|
+
});
|
|
2974
|
+
prepared.catch(() => undefined);
|
|
2975
|
+
const firstFour = assetKeys.slice(0, SYNC_OPERATION_CONCURRENCY_LIMIT);
|
|
2976
|
+
const fifth = assetKeys[SYNC_OPERATION_CONCURRENCY_LIMIT];
|
|
2977
|
+
for (const key of firstFour) {
|
|
2978
|
+
await control.waitForStart(key);
|
|
2979
|
+
}
|
|
2980
|
+
// The bound holds the fifth operation while four stay open.
|
|
2981
|
+
expect(await startedWithin(control, fifth, 300)).toBe(false);
|
|
2982
|
+
expect(control.started.slice().sort()).toEqual(firstFour.slice().sort());
|
|
2983
|
+
// One release frees one slot, so the fifth operation starts.
|
|
2984
|
+
control.release(firstFour[0]);
|
|
2985
|
+
expect(await startedWithin(control, fifth, 4000)).toBe(true);
|
|
2986
|
+
for (const key of assetKeys) {
|
|
2987
|
+
control.release(key);
|
|
2988
|
+
}
|
|
2989
|
+
await prepared;
|
|
2990
|
+
expect(control.settled.slice().sort()).toEqual(assetKeys.slice().sort());
|
|
2991
|
+
});
|
|
2992
|
+
it("holds one upload open after another upload fails, and returns only after both settle", async () => {
|
|
2993
|
+
const { workspaceDir, dirOf } = await makeInboundDirs(["asset-a", "asset-b"]);
|
|
2994
|
+
const { client, control } = makeControlledSyncClient({ concurrent: true });
|
|
2995
|
+
control.hold("asset-b");
|
|
2996
|
+
let coordinatorSettled = false;
|
|
2997
|
+
const prepared = prepareSandboxManagedRuntime({
|
|
2998
|
+
spec: makeSpec("/remote/cwd"),
|
|
2999
|
+
adapterKey: "codex",
|
|
3000
|
+
client,
|
|
3001
|
+
syncWorkspace: false,
|
|
3002
|
+
workspaceLocalDir: workspaceDir,
|
|
3003
|
+
assets: [
|
|
3004
|
+
{ key: "asset-a", localDir: dirOf("asset-a") },
|
|
3005
|
+
{ key: "asset-b", localDir: dirOf("asset-b") },
|
|
3006
|
+
],
|
|
3007
|
+
});
|
|
3008
|
+
const done = prepared.then(() => { coordinatorSettled = true; }, () => { coordinatorSettled = true; });
|
|
3009
|
+
control.failWith("asset-a", new Error("asset-a-fail"));
|
|
3010
|
+
await control.waitForStart("asset-b");
|
|
3011
|
+
await settleTick(100);
|
|
3012
|
+
// The first upload already failed. The second upload is still open, so the
|
|
3013
|
+
// coordinator must not return yet.
|
|
3014
|
+
expect(coordinatorSettled).toBe(false);
|
|
3015
|
+
expect(control.settled).toContain("asset-a");
|
|
3016
|
+
expect(control.settled).not.toContain("asset-b");
|
|
3017
|
+
control.release("asset-b");
|
|
3018
|
+
await done;
|
|
3019
|
+
expect(coordinatorSettled).toBe(true);
|
|
3020
|
+
expect(control.settled).toEqual(expect.arrayContaining(["asset-a", "asset-b"]));
|
|
3021
|
+
});
|
|
3022
|
+
it("records a referenced-project failure as nonfatal and finishes the other referenced-project uploads", async () => {
|
|
3023
|
+
const { workspaceDir, dirOf } = await makeInboundDirs(["good", "bad"]);
|
|
3024
|
+
const { client, control } = makeControlledSyncClient({ concurrent: true });
|
|
3025
|
+
control.failWith("project-bad", new Error("bad-upload"));
|
|
3026
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
3027
|
+
spec: makeSpec("/remote/cwd"),
|
|
3028
|
+
adapterKey: "codex",
|
|
3029
|
+
client,
|
|
3030
|
+
syncWorkspace: false,
|
|
3031
|
+
workspaceLocalDir: workspaceDir,
|
|
3032
|
+
additionalSources: [
|
|
3033
|
+
{ localPath: dirOf("good"), projectId: "good", ignoreResolution: { kind: "other" } },
|
|
3034
|
+
{ localPath: dirOf("bad"), projectId: "bad", ignoreResolution: { kind: "other" } },
|
|
3035
|
+
],
|
|
3036
|
+
});
|
|
3037
|
+
// The healthy project synced; the failed project is a recorded, nonfatal
|
|
3038
|
+
// outcome, so the coordinator resolved.
|
|
3039
|
+
expect(Object.keys(prepared.additionalSourceDirs)).toEqual(["good"]);
|
|
3040
|
+
expect(prepared.additionalSourceFailures.map((failure) => failure.projectId)).toEqual(["bad"]);
|
|
3041
|
+
expect(prepared.additionalSourceFailures[0].error).toContain("bad-upload");
|
|
3042
|
+
expect(control.settled).toContain("project-good");
|
|
3043
|
+
});
|
|
3044
|
+
it("raises an asset failure as fatal after the barrier", async () => {
|
|
3045
|
+
const { workspaceDir, dirOf } = await makeInboundDirs(["asset-good", "asset-bad"]);
|
|
3046
|
+
const { client, control } = makeControlledSyncClient({ concurrent: true });
|
|
3047
|
+
control.failWith("asset-bad", new Error("asset-bad-fail"));
|
|
3048
|
+
await expect(prepareSandboxManagedRuntime({
|
|
3049
|
+
spec: makeSpec("/remote/cwd"),
|
|
3050
|
+
adapterKey: "codex",
|
|
3051
|
+
client,
|
|
3052
|
+
syncWorkspace: false,
|
|
3053
|
+
workspaceLocalDir: workspaceDir,
|
|
3054
|
+
assets: [
|
|
3055
|
+
{ key: "asset-good", localDir: dirOf("asset-good") },
|
|
3056
|
+
{ key: "asset-bad", localDir: dirOf("asset-bad") },
|
|
3057
|
+
],
|
|
3058
|
+
})).rejects.toThrow("asset-bad-fail");
|
|
3059
|
+
// The barrier still settles the healthy asset before the coordinator raises.
|
|
3060
|
+
expect(control.settled).toContain("asset-good");
|
|
3061
|
+
});
|
|
3062
|
+
it("raises the earlier required failure when the workspace and an asset both fail", async () => {
|
|
3063
|
+
const { workspaceDir, dirOf } = await makeInboundDirs(["asset-a"]);
|
|
3064
|
+
const { client, control } = makeControlledSyncClient({ concurrent: true });
|
|
3065
|
+
control.failWith("workspace", new Error("workspace-fail"));
|
|
3066
|
+
control.failWith("asset-a", new Error("asset-a-fail"));
|
|
3067
|
+
// The workspace comes before the asset in stable operation order, so the
|
|
3068
|
+
// coordinator raises the workspace failure.
|
|
3069
|
+
await expect(prepareSandboxManagedRuntime({
|
|
3070
|
+
spec: makeSpec("/remote/cwd"),
|
|
3071
|
+
adapterKey: "codex",
|
|
3072
|
+
client,
|
|
3073
|
+
workspaceLocalDir: workspaceDir,
|
|
3074
|
+
assets: [{ key: "asset-a", localDir: dirOf("asset-a") }],
|
|
3075
|
+
})).rejects.toThrow("workspace-fail");
|
|
3076
|
+
});
|
|
3077
|
+
it("with concurrency forbidden, keeps the serial schedule in the current order", async () => {
|
|
3078
|
+
const { workspaceDir, dirOf } = await makeInboundDirs(["asset-a"]);
|
|
3079
|
+
const { client, control } = makeControlledSyncClient({ concurrent: false });
|
|
3080
|
+
control.hold("workspace");
|
|
3081
|
+
const prepared = prepareSandboxManagedRuntime({
|
|
3082
|
+
spec: makeSpec("/remote/cwd"),
|
|
3083
|
+
adapterKey: "codex",
|
|
3084
|
+
client,
|
|
3085
|
+
workspaceLocalDir: workspaceDir,
|
|
3086
|
+
assets: [{ key: "asset-a", localDir: dirOf("asset-a") }],
|
|
3087
|
+
});
|
|
3088
|
+
prepared.catch(() => undefined);
|
|
3089
|
+
// The workspace runs first and is held open. Serial mode runs one operation
|
|
3090
|
+
// at a time, so the asset upload does not start until the workspace settles.
|
|
3091
|
+
await control.waitForStart("workspace");
|
|
3092
|
+
expect(await startedWithin(control, "asset-a", 300)).toBe(false);
|
|
3093
|
+
expect(control.started).toEqual(["workspace"]);
|
|
3094
|
+
control.release("workspace");
|
|
3095
|
+
await control.waitForStart("asset-a");
|
|
3096
|
+
await prepared;
|
|
3097
|
+
// The serial order stays workspace first, then the asset.
|
|
3098
|
+
expect(control.started).toEqual(["workspace", "asset-a"]);
|
|
3099
|
+
});
|
|
3100
|
+
it("opens one named span per inbound task: stage.workspace, stage.asset.<key>, stage.project.<id>", async () => {
|
|
3101
|
+
const { workspaceDir, dirOf } = await makeInboundDirs(["home", "proj"]);
|
|
3102
|
+
const { client } = makeControlledSyncClient({ concurrent: false });
|
|
3103
|
+
const { runtimeSpan, opened } = makeSpanRecorder();
|
|
3104
|
+
await prepareSandboxManagedRuntime({
|
|
3105
|
+
spec: makeSpec("/remote/cwd"),
|
|
3106
|
+
adapterKey: "codex",
|
|
3107
|
+
client,
|
|
3108
|
+
workspaceLocalDir: workspaceDir,
|
|
3109
|
+
assets: [{ key: "home", localDir: dirOf("home") }],
|
|
3110
|
+
additionalSources: [{ localPath: dirOf("proj"), projectId: "proj-1", ignoreResolution: { kind: "other" } }],
|
|
3111
|
+
runtimeSpan,
|
|
3112
|
+
});
|
|
3113
|
+
// Each inbound task carries its own named span. The workspace task, the home
|
|
3114
|
+
// asset task, and the referenced-project task each open one.
|
|
3115
|
+
expect(opened).toContain("stage.workspace");
|
|
3116
|
+
expect(opened).toContain("stage.asset.home");
|
|
3117
|
+
expect(opened).toContain("stage.project.proj-1");
|
|
3118
|
+
});
|
|
3119
|
+
it("with concurrency permitted, the workspace and asset inbound task spans overlap in time", async () => {
|
|
3120
|
+
const { workspaceDir, dirOf } = await makeInboundDirs(["home"]);
|
|
3121
|
+
const { client, control } = makeControlledSyncClient({ concurrent: true });
|
|
3122
|
+
const { runtimeSpan, openNow } = makeSpanRecorder();
|
|
3123
|
+
control.hold("workspace");
|
|
3124
|
+
control.hold("home");
|
|
3125
|
+
const prepared = prepareSandboxManagedRuntime({
|
|
3126
|
+
spec: makeSpec("/remote/cwd"),
|
|
3127
|
+
adapterKey: "codex",
|
|
3128
|
+
client,
|
|
3129
|
+
workspaceLocalDir: workspaceDir,
|
|
3130
|
+
assets: [{ key: "home", localDir: dirOf("home") }],
|
|
3131
|
+
runtimeSpan,
|
|
3132
|
+
});
|
|
3133
|
+
prepared.catch(() => undefined);
|
|
3134
|
+
// Both uploads are held open at their transfer. Each task span opens before
|
|
3135
|
+
// its transfer and stays open while the transfer is held, so the two spans
|
|
3136
|
+
// are open at the same time.
|
|
3137
|
+
await control.waitForStart("workspace");
|
|
3138
|
+
await control.waitForStart("home");
|
|
3139
|
+
expect(openNow.has("stage.workspace")).toBe(true);
|
|
3140
|
+
expect(openNow.has("stage.asset.home")).toBe(true);
|
|
3141
|
+
control.release("workspace");
|
|
3142
|
+
control.release("home");
|
|
3143
|
+
await prepared;
|
|
3144
|
+
});
|
|
3145
|
+
});
|
|
3146
|
+
function makeOutboundControl() {
|
|
3147
|
+
const started = [];
|
|
3148
|
+
const settled = [];
|
|
3149
|
+
const restoreTempDirs = new Map();
|
|
3150
|
+
const gates = new Map();
|
|
3151
|
+
const failures = new Map();
|
|
3152
|
+
const startWaiters = new Map();
|
|
3153
|
+
const control = {
|
|
3154
|
+
started,
|
|
3155
|
+
settled,
|
|
3156
|
+
restoreTempDirs,
|
|
3157
|
+
waitForStart(label) {
|
|
3158
|
+
if (started.includes(label)) {
|
|
3159
|
+
return Promise.resolve();
|
|
3160
|
+
}
|
|
3161
|
+
return new Promise((resolve) => {
|
|
3162
|
+
const waiters = startWaiters.get(label) ?? [];
|
|
3163
|
+
waiters.push(resolve);
|
|
3164
|
+
startWaiters.set(label, waiters);
|
|
3165
|
+
});
|
|
3166
|
+
},
|
|
3167
|
+
hold(label) {
|
|
3168
|
+
if (!gates.has(label)) {
|
|
3169
|
+
gates.set(label, defer());
|
|
3170
|
+
}
|
|
3171
|
+
},
|
|
3172
|
+
release(label) {
|
|
3173
|
+
gates.get(label)?.resolve();
|
|
3174
|
+
},
|
|
3175
|
+
failWith(label, error) {
|
|
3176
|
+
failures.set(label, error);
|
|
3177
|
+
gates.get(label)?.resolve();
|
|
3178
|
+
},
|
|
3179
|
+
};
|
|
3180
|
+
// Record the task start, notify start waiters, wait for the gate, raise a set
|
|
3181
|
+
// failure, then run the task body and record the settle. A gate that a test
|
|
3182
|
+
// never holds resolves at once, so an unheld task runs straight through.
|
|
3183
|
+
async function gate(label, run) {
|
|
3184
|
+
started.push(label);
|
|
3185
|
+
const waiters = startWaiters.get(label) ?? [];
|
|
3186
|
+
startWaiters.delete(label);
|
|
3187
|
+
for (const waiter of waiters) {
|
|
3188
|
+
waiter();
|
|
3189
|
+
}
|
|
3190
|
+
try {
|
|
3191
|
+
const held = gates.get(label);
|
|
3192
|
+
if (held) {
|
|
3193
|
+
await held.promise;
|
|
3194
|
+
}
|
|
3195
|
+
const failure = failures.get(label);
|
|
3196
|
+
if (failure) {
|
|
3197
|
+
throw failure;
|
|
3198
|
+
}
|
|
3199
|
+
return await run();
|
|
3200
|
+
}
|
|
3201
|
+
finally {
|
|
3202
|
+
settled.push(label);
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
return { control, gate };
|
|
3206
|
+
}
|
|
3207
|
+
// A native filesystem client whose `syncOut` copies the sandbox workspace back
|
|
3208
|
+
// through the gate. The inbound prepare step uses the base64-tar fallback
|
|
3209
|
+
// `syncIn`. The client opts into concurrency by the flag.
|
|
3210
|
+
function makeGatedOutboundClient(concurrent, gate) {
|
|
3211
|
+
const client = {
|
|
3212
|
+
makeDir: async (remotePath) => {
|
|
3213
|
+
await mkdir(remotePath, { recursive: true });
|
|
3214
|
+
},
|
|
3215
|
+
writeFile: async (remotePath, bytes) => {
|
|
3216
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
3217
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
3218
|
+
},
|
|
3219
|
+
readFile: async (remotePath) => await readFile(remotePath),
|
|
3220
|
+
listFiles: async (remotePath) => {
|
|
3221
|
+
const entries = await readdir(remotePath, { withFileTypes: true }).catch(() => []);
|
|
3222
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
|
|
3223
|
+
},
|
|
3224
|
+
remove: async (remotePath) => {
|
|
3225
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
3226
|
+
},
|
|
3227
|
+
run: async (command) => {
|
|
3228
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
3229
|
+
},
|
|
3230
|
+
allowConcurrentSyncOperations: concurrent,
|
|
3231
|
+
syncOut: async (operations) => gate("workspace", async () => {
|
|
3232
|
+
for (const operation of operations) {
|
|
3233
|
+
for (const mapping of operation.files) {
|
|
3234
|
+
if (mapping.kind === "directory") {
|
|
3235
|
+
await mirrorDirectory(mapping.sourcePath, mapping.targetPath);
|
|
3236
|
+
}
|
|
3237
|
+
else {
|
|
3238
|
+
await mkdir(path.dirname(mapping.targetPath), { recursive: true });
|
|
3239
|
+
await writeFile(mapping.targetPath, await readFile(mapping.sourcePath));
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
return {
|
|
3244
|
+
operations: operations.map((operation) => ({
|
|
3245
|
+
operationId: operation.operationId,
|
|
3246
|
+
filesTransferred: operation.files.length,
|
|
3247
|
+
bytesTransferred: 0,
|
|
3248
|
+
})),
|
|
3249
|
+
};
|
|
3250
|
+
}),
|
|
3251
|
+
};
|
|
3252
|
+
attachFallbackSyncIn(client);
|
|
3253
|
+
return client;
|
|
3254
|
+
}
|
|
3255
|
+
// Build one asset with a controlled `restore`. The restore records its own
|
|
3256
|
+
// temp directory, then runs through the gate. The temp directory proves that
|
|
3257
|
+
// two concurrent restore tasks keep separate scratch state.
|
|
3258
|
+
function makeControlledAsset(key, localDir, control, gate) {
|
|
3259
|
+
return {
|
|
3260
|
+
key,
|
|
3261
|
+
localDir,
|
|
3262
|
+
restore: async (ctx) => {
|
|
3263
|
+
control.restoreTempDirs.set(key, ctx.tempDir);
|
|
3264
|
+
await gate(key, async () => {
|
|
3265
|
+
if (ctx.tempDir) {
|
|
3266
|
+
await writeFile(path.join(ctx.tempDir, `scratch-${key}.txt`), key, "utf8");
|
|
3267
|
+
}
|
|
3268
|
+
});
|
|
3269
|
+
},
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
describe("sandbox managed runtime outbound coordinator", () => {
|
|
3273
|
+
const cleanupDirs = [];
|
|
3274
|
+
afterEach(async () => {
|
|
3275
|
+
while (cleanupDirs.length > 0) {
|
|
3276
|
+
const dir = cleanupDirs.pop();
|
|
3277
|
+
if (!dir)
|
|
3278
|
+
continue;
|
|
3279
|
+
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
3280
|
+
}
|
|
3281
|
+
});
|
|
3282
|
+
function makeSpec(remoteCwd) {
|
|
3283
|
+
return {
|
|
3284
|
+
transport: "sandbox",
|
|
3285
|
+
provider: "test",
|
|
3286
|
+
sandboxId: "sandbox-1",
|
|
3287
|
+
remoteCwd,
|
|
3288
|
+
timeoutMs: 30_000,
|
|
3289
|
+
apiKey: null,
|
|
3290
|
+
};
|
|
3291
|
+
}
|
|
3292
|
+
// Create a temp root with one workspace directory and any named asset
|
|
3293
|
+
// directories. Each directory carries one file, so the host tar step has
|
|
3294
|
+
// bytes and the merge has content.
|
|
3295
|
+
async function makeOutboundDirs(names) {
|
|
3296
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-outbound-coordinator-"));
|
|
3297
|
+
cleanupDirs.push(rootDir);
|
|
3298
|
+
const workspaceDir = path.join(rootDir, "workspace");
|
|
3299
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
3300
|
+
await mkdir(workspaceDir, { recursive: true });
|
|
3301
|
+
await writeFile(path.join(workspaceDir, "file.txt"), "workspace\n", "utf8");
|
|
3302
|
+
const dirs = new Map();
|
|
3303
|
+
for (const name of names) {
|
|
3304
|
+
const dir = path.join(rootDir, name);
|
|
3305
|
+
await mkdir(dir, { recursive: true });
|
|
3306
|
+
await writeFile(path.join(dir, "file.txt"), `${name}\n`, "utf8");
|
|
3307
|
+
dirs.set(name, dir);
|
|
3308
|
+
}
|
|
3309
|
+
return { rootDir, workspaceDir, remoteWorkspaceDir, dirOf: (name) => dirs.get(name) };
|
|
3310
|
+
}
|
|
3311
|
+
// Resolve true when the label started within the window, false on timeout.
|
|
3312
|
+
async function startedWithin(control, label, ms) {
|
|
3313
|
+
return Promise.race([
|
|
3314
|
+
control.waitForStart(label).then(() => true),
|
|
3315
|
+
settleTick(ms).then(() => false),
|
|
3316
|
+
]);
|
|
3317
|
+
}
|
|
3318
|
+
it("with concurrency permitted, an asset restore starts while the workspace restore is held open", async () => {
|
|
3319
|
+
const { workspaceDir, remoteWorkspaceDir, dirOf } = await makeOutboundDirs(["home"]);
|
|
3320
|
+
const { control, gate } = makeOutboundControl();
|
|
3321
|
+
const client = makeGatedOutboundClient(true, gate);
|
|
3322
|
+
control.hold("workspace");
|
|
3323
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
3324
|
+
spec: makeSpec(remoteWorkspaceDir),
|
|
3325
|
+
adapterKey: "codex",
|
|
3326
|
+
client,
|
|
3327
|
+
workspaceLocalDir: workspaceDir,
|
|
3328
|
+
assets: [makeControlledAsset("home", dirOf("home"), control, gate)],
|
|
3329
|
+
});
|
|
3330
|
+
const restore = prepared.restoreWorkspace();
|
|
3331
|
+
restore.catch(() => undefined);
|
|
3332
|
+
// The workspace restore is open and held. The home asset restore still
|
|
3333
|
+
// starts, so the coordinator runs the two tasks concurrently.
|
|
3334
|
+
await control.waitForStart("workspace");
|
|
3335
|
+
expect(await startedWithin(control, "home", 4000)).toBe(true);
|
|
3336
|
+
expect(control.settled).not.toContain("workspace");
|
|
3337
|
+
control.release("workspace");
|
|
3338
|
+
await restore;
|
|
3339
|
+
expect(control.settled).toContain("home");
|
|
3340
|
+
expect(control.settled).toContain("workspace");
|
|
3341
|
+
});
|
|
3342
|
+
it("holds one restore open after another restore fails, and returns only after both settle", async () => {
|
|
3343
|
+
const { workspaceDir, remoteWorkspaceDir, dirOf } = await makeOutboundDirs(["asset-a", "asset-b"]);
|
|
3344
|
+
const { control, gate } = makeOutboundControl();
|
|
3345
|
+
const client = makeGatedOutboundClient(true, gate);
|
|
3346
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
3347
|
+
spec: makeSpec(remoteWorkspaceDir),
|
|
3348
|
+
adapterKey: "codex",
|
|
3349
|
+
client,
|
|
3350
|
+
syncWorkspace: false,
|
|
3351
|
+
workspaceLocalDir: workspaceDir,
|
|
3352
|
+
assets: [
|
|
3353
|
+
makeControlledAsset("asset-a", dirOf("asset-a"), control, gate),
|
|
3354
|
+
makeControlledAsset("asset-b", dirOf("asset-b"), control, gate),
|
|
3355
|
+
],
|
|
3356
|
+
});
|
|
3357
|
+
control.hold("asset-b");
|
|
3358
|
+
control.failWith("asset-a", new Error("asset-a-fail"));
|
|
3359
|
+
let coordinatorSettled = false;
|
|
3360
|
+
const done = prepared.restoreWorkspace().then(() => { coordinatorSettled = true; }, () => { coordinatorSettled = true; });
|
|
3361
|
+
await control.waitForStart("asset-b");
|
|
3362
|
+
await settleTick(100);
|
|
3363
|
+
// The first restore already failed. The second restore is still open, so
|
|
3364
|
+
// the coordinator must not return yet.
|
|
3365
|
+
expect(coordinatorSettled).toBe(false);
|
|
3366
|
+
expect(control.settled).toContain("asset-a");
|
|
3367
|
+
expect(control.settled).not.toContain("asset-b");
|
|
3368
|
+
control.release("asset-b");
|
|
3369
|
+
await done;
|
|
3370
|
+
expect(coordinatorSettled).toBe(true);
|
|
3371
|
+
expect(control.settled).toEqual(expect.arrayContaining(["asset-a", "asset-b"]));
|
|
3372
|
+
});
|
|
3373
|
+
it("with concurrency forbidden, keeps the serial restore schedule in the current order", async () => {
|
|
3374
|
+
const { workspaceDir, remoteWorkspaceDir, dirOf } = await makeOutboundDirs(["asset-a"]);
|
|
3375
|
+
const { control, gate } = makeOutboundControl();
|
|
3376
|
+
const client = makeGatedOutboundClient(false, gate);
|
|
3377
|
+
control.hold("workspace");
|
|
3378
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
3379
|
+
spec: makeSpec(remoteWorkspaceDir),
|
|
3380
|
+
adapterKey: "codex",
|
|
3381
|
+
client,
|
|
3382
|
+
workspaceLocalDir: workspaceDir,
|
|
3383
|
+
assets: [makeControlledAsset("asset-a", dirOf("asset-a"), control, gate)],
|
|
3384
|
+
});
|
|
3385
|
+
const restore = prepared.restoreWorkspace();
|
|
3386
|
+
restore.catch(() => undefined);
|
|
3387
|
+
// The workspace restore runs first and is held open. Serial mode runs one
|
|
3388
|
+
// task at a time, so the asset restore does not start until the workspace
|
|
3389
|
+
// restore settles.
|
|
3390
|
+
await control.waitForStart("workspace");
|
|
3391
|
+
expect(await startedWithin(control, "asset-a", 300)).toBe(false);
|
|
3392
|
+
expect(control.started).toEqual(["workspace"]);
|
|
3393
|
+
control.release("workspace");
|
|
3394
|
+
await control.waitForStart("asset-a");
|
|
3395
|
+
await restore;
|
|
3396
|
+
// The serial order stays the workspace restore first, then the asset.
|
|
3397
|
+
expect(control.started).toEqual(["workspace", "asset-a"]);
|
|
3398
|
+
});
|
|
3399
|
+
it("with concurrency permitted, two concurrent restore tasks use separate temporary state", async () => {
|
|
3400
|
+
const { workspaceDir, remoteWorkspaceDir, dirOf } = await makeOutboundDirs(["asset-a", "asset-b"]);
|
|
3401
|
+
const { control, gate } = makeOutboundControl();
|
|
3402
|
+
const client = makeGatedOutboundClient(true, gate);
|
|
3403
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
3404
|
+
spec: makeSpec(remoteWorkspaceDir),
|
|
3405
|
+
adapterKey: "codex",
|
|
3406
|
+
client,
|
|
3407
|
+
syncWorkspace: false,
|
|
3408
|
+
workspaceLocalDir: workspaceDir,
|
|
3409
|
+
assets: [
|
|
3410
|
+
makeControlledAsset("asset-a", dirOf("asset-a"), control, gate),
|
|
3411
|
+
makeControlledAsset("asset-b", dirOf("asset-b"), control, gate),
|
|
3412
|
+
],
|
|
3413
|
+
});
|
|
3414
|
+
control.hold("asset-a");
|
|
3415
|
+
control.hold("asset-b");
|
|
3416
|
+
const restore = prepared.restoreWorkspace();
|
|
3417
|
+
// Both restore tasks start together under the bound. Each task carries its
|
|
3418
|
+
// own restore temp directory, so the two directories differ.
|
|
3419
|
+
await control.waitForStart("asset-a");
|
|
3420
|
+
await control.waitForStart("asset-b");
|
|
3421
|
+
const tempA = control.restoreTempDirs.get("asset-a");
|
|
3422
|
+
const tempB = control.restoreTempDirs.get("asset-b");
|
|
3423
|
+
expect(tempA).toBeTruthy();
|
|
3424
|
+
expect(tempB).toBeTruthy();
|
|
3425
|
+
expect(tempA).not.toBe(tempB);
|
|
3426
|
+
expect(tempA).toContain("paperclip-sandbox-restore-");
|
|
3427
|
+
expect(tempB).toContain("paperclip-sandbox-restore-");
|
|
3428
|
+
control.release("asset-a");
|
|
3429
|
+
control.release("asset-b");
|
|
3430
|
+
await restore;
|
|
3431
|
+
expect(control.settled).toEqual(expect.arrayContaining(["asset-a", "asset-b"]));
|
|
3432
|
+
});
|
|
3433
|
+
it("opens one named span per outbound restore task: restore.workspace, restore.asset.<key>", async () => {
|
|
3434
|
+
const { workspaceDir, remoteWorkspaceDir, dirOf } = await makeOutboundDirs(["home"]);
|
|
3435
|
+
const { control, gate } = makeOutboundControl();
|
|
3436
|
+
const client = makeGatedOutboundClient(true, gate);
|
|
3437
|
+
const { runtimeSpan, opened } = makeSpanRecorder();
|
|
3438
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
3439
|
+
spec: makeSpec(remoteWorkspaceDir),
|
|
3440
|
+
adapterKey: "codex",
|
|
3441
|
+
client,
|
|
3442
|
+
workspaceLocalDir: workspaceDir,
|
|
3443
|
+
assets: [makeControlledAsset("home", dirOf("home"), control, gate)],
|
|
3444
|
+
runtimeSpan,
|
|
3445
|
+
});
|
|
3446
|
+
await prepared.restoreWorkspace();
|
|
3447
|
+
// Each outbound restore task carries its own named span. The workspace
|
|
3448
|
+
// restore task and the home asset restore task each open one.
|
|
3449
|
+
expect(opened).toContain("restore.workspace");
|
|
3450
|
+
expect(opened).toContain("restore.asset.home");
|
|
3451
|
+
});
|
|
3452
|
+
it("with concurrency permitted, the workspace and asset restore task spans overlap in time", async () => {
|
|
3453
|
+
const { workspaceDir, remoteWorkspaceDir, dirOf } = await makeOutboundDirs(["home"]);
|
|
3454
|
+
const { control, gate } = makeOutboundControl();
|
|
3455
|
+
const client = makeGatedOutboundClient(true, gate);
|
|
3456
|
+
const { runtimeSpan, openNow } = makeSpanRecorder();
|
|
3457
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
3458
|
+
spec: makeSpec(remoteWorkspaceDir),
|
|
3459
|
+
adapterKey: "codex",
|
|
3460
|
+
client,
|
|
3461
|
+
workspaceLocalDir: workspaceDir,
|
|
3462
|
+
assets: [makeControlledAsset("home", dirOf("home"), control, gate)],
|
|
3463
|
+
runtimeSpan,
|
|
3464
|
+
});
|
|
3465
|
+
control.hold("workspace");
|
|
3466
|
+
control.hold("home");
|
|
3467
|
+
const restore = prepared.restoreWorkspace();
|
|
3468
|
+
restore.catch(() => undefined);
|
|
3469
|
+
// Both restore tasks are held open. Each restore task span opens before its
|
|
3470
|
+
// work and stays open while the work is held, so the two spans are open at
|
|
3471
|
+
// the same time.
|
|
3472
|
+
await control.waitForStart("workspace");
|
|
3473
|
+
await control.waitForStart("home");
|
|
3474
|
+
expect(openNow.has("restore.workspace")).toBe(true);
|
|
3475
|
+
expect(openNow.has("restore.asset.home")).toBe(true);
|
|
3476
|
+
control.release("workspace");
|
|
3477
|
+
control.release("home");
|
|
3478
|
+
await restore;
|
|
3479
|
+
});
|
|
3480
|
+
});
|
|
3481
|
+
// The bundle export inside the workspace restore task selects its outbound
|
|
3482
|
+
// transport by the client. A client with native `syncOut` copies the bundle
|
|
3483
|
+
// straight into the host restore temp directory through one `kind: "file"`
|
|
3484
|
+
// mapping. A client without native `syncOut` reads the bundle back through
|
|
3485
|
+
// `readFile`. These tests build a git-backed workspace and assert the transport
|
|
3486
|
+
// branch, the confinement guard, and the full-bundle retry.
|
|
3487
|
+
describe("sandbox git-bundle export transport", () => {
|
|
3488
|
+
const cleanupDirs = [];
|
|
3489
|
+
afterEach(async () => {
|
|
3490
|
+
while (cleanupDirs.length > 0) {
|
|
3491
|
+
const dir = cleanupDirs.pop();
|
|
3492
|
+
if (!dir)
|
|
3493
|
+
continue;
|
|
3494
|
+
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
3495
|
+
}
|
|
3496
|
+
});
|
|
3497
|
+
// Copy a directory tree and drop each entry whose name matches an exclude
|
|
3498
|
+
// term. This models a native provider `syncOut` for a directory mapping: it
|
|
3499
|
+
// honors `exclude`, so `.git`, `node_modules`, and the runtime root never
|
|
3500
|
+
// reach the host restore temp directory. The tar fallback drops the same set.
|
|
3501
|
+
async function copyDirectoryWithExclude(sourceDir, targetDir, exclude) {
|
|
3502
|
+
const excludeNames = new Set((exclude ?? []).map((entry) => entry.replace(/\/$/, "")));
|
|
3503
|
+
await mkdir(targetDir, { recursive: true });
|
|
3504
|
+
const entries = await readdir(sourceDir, { withFileTypes: true });
|
|
3505
|
+
for (const entry of entries) {
|
|
3506
|
+
if (excludeNames.has(entry.name))
|
|
3507
|
+
continue;
|
|
3508
|
+
const source = path.join(sourceDir, entry.name);
|
|
3509
|
+
const target = path.join(targetDir, entry.name);
|
|
3510
|
+
if (entry.isDirectory()) {
|
|
3511
|
+
await copyDirectoryWithExclude(source, target, exclude);
|
|
3512
|
+
}
|
|
3513
|
+
else if (entry.isSymbolicLink()) {
|
|
3514
|
+
await symlink(await fsPromises.readlink(source), target);
|
|
3515
|
+
}
|
|
3516
|
+
else {
|
|
3517
|
+
await writeFile(target, await readFile(source));
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
// Build a git-backed managed-runtime client. `native` toggles the outbound
|
|
3522
|
+
// `syncOut`. The client records every `syncOut` operation and every `readFile`
|
|
3523
|
+
// remote path, so a test can prove which transport moved the bundle.
|
|
3524
|
+
function makeTransportClient(native, capture) {
|
|
3525
|
+
const client = {
|
|
3526
|
+
makeDir: async (remotePath) => {
|
|
3527
|
+
await mkdir(remotePath, { recursive: true });
|
|
3528
|
+
},
|
|
3529
|
+
writeFile: async (remotePath, bytes) => {
|
|
3530
|
+
await mkdir(path.dirname(remotePath), { recursive: true });
|
|
3531
|
+
await writeFile(remotePath, Buffer.from(bytes));
|
|
3532
|
+
},
|
|
3533
|
+
readFile: async (remotePath) => {
|
|
3534
|
+
capture.readFilePaths.push(remotePath);
|
|
3535
|
+
return await readFile(remotePath);
|
|
3536
|
+
},
|
|
3537
|
+
listFiles: async () => [],
|
|
3538
|
+
remove: async (remotePath) => {
|
|
3539
|
+
await rm(remotePath, { recursive: true, force: true });
|
|
3540
|
+
},
|
|
3541
|
+
run: async (command) => {
|
|
3542
|
+
await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
|
|
3543
|
+
},
|
|
3544
|
+
};
|
|
3545
|
+
attachFallbackSyncIn(client);
|
|
3546
|
+
if (native) {
|
|
3547
|
+
client.syncOut = async (operations) => {
|
|
3548
|
+
const resultOperations = [];
|
|
3549
|
+
for (const operation of operations) {
|
|
3550
|
+
capture.syncOutOperations.push(operation);
|
|
3551
|
+
// Report a real `bytesTransferred`, from a post-copy byte count, so a
|
|
3552
|
+
// test can assert on the emitted progress line — the same way a real
|
|
3553
|
+
// provider reports the bytes it actually moved.
|
|
3554
|
+
let bytesTransferred = 0;
|
|
3555
|
+
for (const mapping of operation.files) {
|
|
3556
|
+
if (mapping.kind === "directory") {
|
|
3557
|
+
await copyDirectoryWithExclude(mapping.sourcePath, mapping.targetPath, mapping.exclude);
|
|
3558
|
+
bytesTransferred += await directoryByteSize(mapping.targetPath);
|
|
3559
|
+
}
|
|
3560
|
+
else {
|
|
3561
|
+
await mkdir(path.dirname(mapping.targetPath), { recursive: true });
|
|
3562
|
+
const bytes = await readFile(mapping.sourcePath);
|
|
3563
|
+
await writeFile(mapping.targetPath, bytes);
|
|
3564
|
+
bytesTransferred += bytes.byteLength;
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
resultOperations.push({
|
|
3568
|
+
operationId: operation.operationId,
|
|
3569
|
+
filesTransferred: operation.files.length,
|
|
3570
|
+
bytesTransferred,
|
|
3571
|
+
});
|
|
3572
|
+
}
|
|
3573
|
+
return { operations: resultOperations };
|
|
3574
|
+
};
|
|
3575
|
+
}
|
|
3576
|
+
return client;
|
|
3577
|
+
}
|
|
3578
|
+
const gitSpec = (remoteWorkspaceDir) => ({
|
|
3579
|
+
transport: "sandbox",
|
|
3580
|
+
provider: "test",
|
|
3581
|
+
sandboxId: "sandbox-1",
|
|
3582
|
+
remoteCwd: remoteWorkspaceDir,
|
|
3583
|
+
timeoutMs: 30_000,
|
|
3584
|
+
apiKey: null,
|
|
3585
|
+
});
|
|
3586
|
+
// Create a git repository with one commit and a linked worktree. Return the
|
|
3587
|
+
// host worktree directory and the sandbox workspace directory the prepare step
|
|
3588
|
+
// fills through a shallow standalone clone.
|
|
3589
|
+
async function setupGitBackedWorkspace(prefix) {
|
|
3590
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), prefix));
|
|
3591
|
+
cleanupDirs.push(rootDir);
|
|
3592
|
+
const sourceRepoDir = path.join(rootDir, "source-repo");
|
|
3593
|
+
const localWorkspaceDir = path.join(rootDir, "local-worktree");
|
|
3594
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
3595
|
+
await mkdir(sourceRepoDir, { recursive: true });
|
|
3596
|
+
await git(sourceRepoDir, ["init"]);
|
|
3597
|
+
await git(sourceRepoDir, ["checkout", "-b", "main"]);
|
|
3598
|
+
await git(sourceRepoDir, ["config", "user.name", "Paperclip Test"]);
|
|
3599
|
+
await git(sourceRepoDir, ["config", "user.email", "test@paperclip.dev"]);
|
|
3600
|
+
await writeFile(path.join(sourceRepoDir, ".gitignore"), "node_modules/\n", "utf8");
|
|
3601
|
+
await writeFile(path.join(sourceRepoDir, "tracked.txt"), "base\n", "utf8");
|
|
3602
|
+
await git(sourceRepoDir, ["add", "-A"]);
|
|
3603
|
+
await git(sourceRepoDir, ["commit", "-m", "base"]);
|
|
3604
|
+
await git(sourceRepoDir, ["worktree", "add", "-b", "work", localWorkspaceDir, "HEAD"]);
|
|
3605
|
+
return { localWorkspaceDir, remoteWorkspaceDir };
|
|
3606
|
+
}
|
|
3607
|
+
// Advance the sandbox history by one commit that also adds a new file. The
|
|
3608
|
+
// caller runs the restore after this, so the export moves this new history.
|
|
3609
|
+
async function commitInSandbox(remoteWorkspaceDir) {
|
|
3610
|
+
await git(remoteWorkspaceDir, ["config", "user.name", "Paperclip Sandbox"]);
|
|
3611
|
+
await git(remoteWorkspaceDir, ["config", "user.email", "sandbox@paperclip.dev"]);
|
|
3612
|
+
await writeFile(path.join(remoteWorkspaceDir, "remote-only.txt"), "from sandbox\n", "utf8");
|
|
3613
|
+
await git(remoteWorkspaceDir, ["add", "-A"]);
|
|
3614
|
+
await git(remoteWorkspaceDir, ["commit", "-m", "sandbox update"]);
|
|
3615
|
+
}
|
|
3616
|
+
function bundleFileMappings(capture) {
|
|
3617
|
+
return capture.syncOutOperations
|
|
3618
|
+
.flatMap((operation) => operation.files)
|
|
3619
|
+
.filter((mapping) => path.posix.basename(mapping.sourcePath) === "git-delta.bundle");
|
|
3620
|
+
}
|
|
3621
|
+
it("moves the bundle through one native syncOut file mapping, never through readFile", async () => {
|
|
3622
|
+
const capture = { syncOutOperations: [], readFilePaths: [] };
|
|
3623
|
+
const { localWorkspaceDir, remoteWorkspaceDir } = await setupGitBackedWorkspace("paperclip-bundle-native-");
|
|
3624
|
+
const client = makeTransportClient(true, capture);
|
|
3625
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
3626
|
+
spec: gitSpec(remoteWorkspaceDir),
|
|
3627
|
+
adapterKey: "test-adapter",
|
|
3628
|
+
client,
|
|
3629
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
3630
|
+
});
|
|
3631
|
+
await commitInSandbox(remoteWorkspaceDir);
|
|
3632
|
+
await prepared.restoreWorkspace();
|
|
3633
|
+
// The restore imported the sandbox commit and its new file.
|
|
3634
|
+
expect(await git(localWorkspaceDir, ["log", "-1", "--pretty=%s"])).toBe("sandbox update");
|
|
3635
|
+
await expect(readFile(path.join(localWorkspaceDir, "remote-only.txt"), "utf8")).resolves.toBe("from sandbox\n");
|
|
3636
|
+
// The bundle rode exactly one native `kind: "file"` mapping into the host
|
|
3637
|
+
// restore temp directory; `readFile` never touched the bundle.
|
|
3638
|
+
const mappings = bundleFileMappings(capture);
|
|
3639
|
+
expect(mappings).toHaveLength(1);
|
|
3640
|
+
expect(mappings[0].kind).toBe("file");
|
|
3641
|
+
expect(path.posix.basename(mappings[0].targetPath)).toBe("git-delta.bundle");
|
|
3642
|
+
expect(capture.readFilePaths.some((remotePath) => remotePath.endsWith("git-delta.bundle"))).toBe(false);
|
|
3643
|
+
// The small status file stays on `readFile`; the change does not migrate it.
|
|
3644
|
+
expect(capture.readFilePaths.some((remotePath) => remotePath.endsWith("workspace-status.txt"))).toBe(true);
|
|
3645
|
+
});
|
|
3646
|
+
it("reads the bundle through readFile when the client has no native syncOut", async () => {
|
|
3647
|
+
const capture = { syncOutOperations: [], readFilePaths: [] };
|
|
3648
|
+
const { localWorkspaceDir, remoteWorkspaceDir } = await setupGitBackedWorkspace("paperclip-bundle-fallback-");
|
|
3649
|
+
const client = makeTransportClient(false, capture);
|
|
3650
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
3651
|
+
spec: gitSpec(remoteWorkspaceDir),
|
|
3652
|
+
adapterKey: "test-adapter",
|
|
3653
|
+
client,
|
|
3654
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
3655
|
+
});
|
|
3656
|
+
await commitInSandbox(remoteWorkspaceDir);
|
|
3657
|
+
await prepared.restoreWorkspace();
|
|
3658
|
+
expect(await git(localWorkspaceDir, ["log", "-1", "--pretty=%s"])).toBe("sandbox update");
|
|
3659
|
+
await expect(readFile(path.join(localWorkspaceDir, "remote-only.txt"), "utf8")).resolves.toBe("from sandbox\n");
|
|
3660
|
+
// Without native `syncOut` the fallback path is unchanged: no sync operation
|
|
3661
|
+
// ran and the bundle came back through `readFile`.
|
|
3662
|
+
expect(client.syncOut).toBeUndefined();
|
|
3663
|
+
expect(capture.syncOutOperations).toHaveLength(0);
|
|
3664
|
+
expect(capture.readFilePaths.some((remotePath) => remotePath.endsWith("git-delta.bundle"))).toBe(true);
|
|
3665
|
+
});
|
|
3666
|
+
it("retries the full bundle through the native branch when the delta misses its prerequisite", async () => {
|
|
3667
|
+
const capture = { syncOutOperations: [], readFilePaths: [] };
|
|
3668
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-bundle-retry-"));
|
|
3669
|
+
cleanupDirs.push(rootDir);
|
|
3670
|
+
// A standalone host repository, so the test controls object reachability. A
|
|
3671
|
+
// linked worktree shares the source object store, and the boundary commit
|
|
3672
|
+
// stays reachable there; a standalone repository lets `gc` prune it.
|
|
3673
|
+
const localWorkspaceDir = path.join(rootDir, "local-repo");
|
|
3674
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
3675
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
3676
|
+
await git(localWorkspaceDir, ["init"]);
|
|
3677
|
+
await git(localWorkspaceDir, ["checkout", "-b", "work"]);
|
|
3678
|
+
await git(localWorkspaceDir, ["config", "user.name", "Paperclip Test"]);
|
|
3679
|
+
await git(localWorkspaceDir, ["config", "user.email", "test@paperclip.dev"]);
|
|
3680
|
+
await writeFile(path.join(localWorkspaceDir, "tracked.txt"), "base\n", "utf8");
|
|
3681
|
+
await git(localWorkspaceDir, ["add", "-A"]);
|
|
3682
|
+
await git(localWorkspaceDir, ["commit", "-m", "base"]);
|
|
3683
|
+
const firstCommit = await git(localWorkspaceDir, ["rev-parse", "HEAD"]);
|
|
3684
|
+
await writeFile(path.join(localWorkspaceDir, "tracked.txt"), "second\n", "utf8");
|
|
3685
|
+
await git(localWorkspaceDir, ["add", "-A"]);
|
|
3686
|
+
await git(localWorkspaceDir, ["commit", "-m", "second"]);
|
|
3687
|
+
const stagedBase = await git(localWorkspaceDir, ["rev-parse", "HEAD"]);
|
|
3688
|
+
const client = makeTransportClient(true, capture);
|
|
3689
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
3690
|
+
spec: gitSpec(remoteWorkspaceDir),
|
|
3691
|
+
adapterKey: "test-adapter",
|
|
3692
|
+
client,
|
|
3693
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
3694
|
+
});
|
|
3695
|
+
await commitInSandbox(remoteWorkspaceDir);
|
|
3696
|
+
const sandboxHead = await git(remoteWorkspaceDir, ["rev-parse", "HEAD"]);
|
|
3697
|
+
// The host drops below the staged base commit and prunes it. The delta
|
|
3698
|
+
// bundle names that boundary commit as a prerequisite the host no longer
|
|
3699
|
+
// holds, so the import fails and forces the full-bundle retry.
|
|
3700
|
+
await git(localWorkspaceDir, ["reset", "--hard", firstCommit]);
|
|
3701
|
+
await git(localWorkspaceDir, ["reflog", "expire", "--expire=now", "--all"]);
|
|
3702
|
+
await git(localWorkspaceDir, ["gc", "--prune=now"]);
|
|
3703
|
+
await expect(git(localWorkspaceDir, ["cat-file", "-e", `${stagedBase}^{commit}`])).rejects.toThrow();
|
|
3704
|
+
await prepared.restoreWorkspace();
|
|
3705
|
+
// Two bundle exports rode native file mappings: the delta attempt and the
|
|
3706
|
+
// full-bundle retry. `readFile` never moved the bundle on either attempt.
|
|
3707
|
+
const mappings = bundleFileMappings(capture);
|
|
3708
|
+
expect(mappings).toHaveLength(2);
|
|
3709
|
+
expect(mappings.every((mapping) => mapping.kind === "file")).toBe(true);
|
|
3710
|
+
expect(capture.readFilePaths.some((remotePath) => remotePath.endsWith("git-delta.bundle"))).toBe(false);
|
|
3711
|
+
// The full bundle was self-contained: the host repository now holds the
|
|
3712
|
+
// sandbox head commit.
|
|
3713
|
+
await expect(git(localWorkspaceDir, ["cat-file", "-e", `${sandboxHead}^{commit}`])).resolves.toBe("");
|
|
3714
|
+
});
|
|
3715
|
+
it("reports the real transferred bytes for the native git-history export and workspace restore", async () => {
|
|
3716
|
+
const capture = { syncOutOperations: [], readFilePaths: [] };
|
|
3717
|
+
const { localWorkspaceDir, remoteWorkspaceDir } = await setupGitBackedWorkspace("paperclip-restore-bytes-");
|
|
3718
|
+
const client = makeTransportClient(true, capture);
|
|
3719
|
+
const prepared = await prepareSandboxManagedRuntime({
|
|
3720
|
+
spec: gitSpec(remoteWorkspaceDir),
|
|
3721
|
+
adapterKey: "test-adapter",
|
|
3722
|
+
client,
|
|
3723
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
3724
|
+
});
|
|
3725
|
+
// Add a sizeable, incompressible tracked file in the sandbox, so both the
|
|
3726
|
+
// packed git-history bundle and the restored working tree carry real,
|
|
3727
|
+
// non-trivial bytes well above the 0.1 MB rounding step. Random bytes,
|
|
3728
|
+
// not a repeated byte, so git's own pack compression cannot shrink the
|
|
3729
|
+
// bundle back down to a trivial size.
|
|
3730
|
+
await writeFile(path.join(remoteWorkspaceDir, "large.bin"), randomBytes(300 * 1024));
|
|
3731
|
+
await commitInSandbox(remoteWorkspaceDir);
|
|
3732
|
+
const lines = [];
|
|
3733
|
+
await prepared.restoreWorkspace((line) => { lines.push(line); });
|
|
3734
|
+
const exportLines = lines.filter((line) => line.includes("Exporting git history from environment"));
|
|
3735
|
+
const restoreLines = lines.filter((line) => line.includes("Restoring workspace from environment"));
|
|
3736
|
+
// Both terminal lines carry the real transferred byte total, not the
|
|
3737
|
+
// "0.0 MB" a discarded native `syncOut` result would leave behind.
|
|
3738
|
+
expect(exportLines.length).toBeGreaterThan(0);
|
|
3739
|
+
expect(exportLines.some((line) => /\(\d+\.\d\/\d+\.\d MB\)/.test(line))).toBe(true);
|
|
3740
|
+
expect(exportLines.some((line) => line.includes("(0.0/0.0 MB)"))).toBe(false);
|
|
3741
|
+
expect(restoreLines.length).toBeGreaterThan(0);
|
|
3742
|
+
expect(restoreLines.some((line) => /\(\d+\.\d\/\d+\.\d MB\)/.test(line))).toBe(true);
|
|
3743
|
+
expect(restoreLines.some((line) => line.includes("(0.0/0.0 MB)"))).toBe(false);
|
|
3744
|
+
});
|
|
3745
|
+
});
|
|
3746
|
+
//# sourceMappingURL=sandbox-managed-runtime.test.js.map
|