diff --git a/README.md b/README.md index 4934496..1b93696 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,74 @@ # @torkbot/code-mode-sandbox Run [`@torkbot/code-mode`](https://github.com/torkbot/code-mode) programs -inside lifecycle-managed +with Node.js 24 inside [`@torkbot/sandbox`](https://github.com/torkbot/sandbox) microVMs. This package owns the integration between code mode's runtime contract and -Sandbox VM execution. It boots a caller-defined Sandbox machine, provides a -code-mode runtime backed by Node.js inside that machine, and couples the runtime -and VM lifecycles so they are closed together. +Sandbox VM execution. It adapts a caller-owned, booted `SandboxInstance` into +the execution host required by `Node24Runtime`; composing those values produces +a code-mode runtime backed by Node.js inside that machine. -The caller remains responsible for the Sandbox definition, including its image, -persistence, mounts, resources, and network access. The embedding application -remains responsible for machine identity, session reuse, and the runtime facts -presented to an agent. +The caller owns the Sandbox lifecycle. It chooses the image, persistence, +mounts, resources, network access, machine identity, and reuse policy; boots the +machine; keeps it open while the runtime is in use; and closes it afterward. +The Sandbox runtime host owns only the guest processes it launches through +`Node24Runtime`. This package does not boot, pool, reuse, or close Sandbox +machines. `@torkbot/code-mode` remains responsible for tool declarations, source validation, protocol routing, and telemetry. `@torkbot/sandbox` remains responsible for isolated VM execution. This package adds only the integration -required to use those capabilities together. +required to use those capabilities together. The runtime channel is the +Sandbox process pipe's standard readable and writable Web Streams pair. ## Install ```sh -npm install @torkbot/code-mode-sandbox +npm install @torkbot/code-mode @torkbot/code-mode-sandbox @torkbot/sandbox ``` +## Usage + +Define and boot the machine with Sandbox, then adapt that machine for code mode: + +```ts +import { createClient } from "@torkbot/code-mode"; +import { Node24Runtime } from "@torkbot/code-mode/node"; +import { createSandboxNodeRuntimeHost } from "@torkbot/code-mode-sandbox"; +import { defineSandbox } from "@torkbot/sandbox"; + +const definition = defineSandbox({ + rootfs: machineRootfs, + resources: { + cpus: 2, + memoryMiB: 2048, + }, +}); + +await using sandbox = await definition.boot({ + cwd: "/workspace", +}); + +const runtime = new Node24Runtime( + createSandboxNodeRuntimeHost({ + sandbox, + cwd: "/workspace", + nodePath: "/usr/bin/node", + }), +); + +const client = createClient({ + toolbox, + runtime, +}); +``` + +The absolute guest working directory and Node.js path are required because they +determine module resolution and the executable used for both validation and +execution. The Sandbox instance must remain open until every runtime instance +started through the composed runtime has finished. + ## Development Requires Node.js 24 or newer. diff --git a/package-lock.json b/package-lock.json index f8714b6..2aa4a07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,10 @@ "name": "@torkbot/code-mode-sandbox", "version": "0.0.0-dev", "license": "MIT", + "dependencies": { + "@torkbot/code-mode": "^0.2.0", + "@torkbot/sandbox": "^0.14.0" + }, "devDependencies": { "@types/node": "^24.12.0", "typescript": "^7.0.2" @@ -16,11 +20,188 @@ "node": ">=24.0.0" } }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@jitl/quickjs-ffi-types": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-ffi-types/-/quickjs-ffi-types-0.32.0.tgz", + "integrity": "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg==", + "license": "MIT" + }, + "node_modules/@jitl/quickjs-wasmfile-debug-asyncify": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-debug-asyncify/-/quickjs-wasmfile-debug-asyncify-0.32.0.tgz", + "integrity": "sha512-EX8zbXwGqCgAE764M+qvkHtyXDi/FUoMBea0JnES7vCM3P7a2+EOZOjGv85wtZ2sJhI1oJ+nekmqpOODFDY+hw==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, + "node_modules/@jitl/quickjs-wasmfile-debug-sync": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-debug-sync/-/quickjs-wasmfile-debug-sync-0.32.0.tgz", + "integrity": "sha512-LeYWrPGC1uNCTBWvibo3ZLJj0CSVNYUXvJpXMCmuQ5Sap2cCACc3uvGvYV4homHHBAzfw5akoTqMMS4YFRtw+Q==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, + "node_modules/@jitl/quickjs-wasmfile-release-asyncify": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-release-asyncify/-/quickjs-wasmfile-release-asyncify-0.32.0.tgz", + "integrity": "sha512-3oSwPfja12ICz4aIblB58cuY8JlEq5Txt8Cut4VLo+LH47QN+mzCnSgnbB03hWzg1LBcc+VyyI9UOag7a1NF+Q==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, + "node_modules/@jitl/quickjs-wasmfile-release-sync": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-release-sync/-/quickjs-wasmfile-release-sync-0.32.0.tgz", + "integrity": "sha512-BKNDI/TPBfGlLNGYpLrhcDGXmIk4xHm4MRAisOBnOzpXVn9HZWsfmMAc9WMBrAHjvvds6HOikKeaOBKdPdpVrg==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, + "node_modules/@mixmark-io/domino": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", + "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", + "license": "BSD-2-Clause" + }, + "node_modules/@mongodb-js/zstd": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/zstd/-/zstd-7.0.0.tgz", + "integrity": "sha512-mQ2s0pYYiav+tzCDR05Zptem8Ey2v8s11lri5RKGhTtL4COVCvVCk5vtyRYNT+9L8qSfyOqqefF9UtnW8mC5jA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "node-addon-api": "^8.5.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": ">= 20.19.0" + } + }, + "node_modules/@nodable/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@torkbot/code-mode": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@torkbot/code-mode/-/code-mode-0.2.0.tgz", + "integrity": "sha512-O/K3HNzdk4LdyUQO/hFSlthCsPZ06QfIqouuxCytm0ZWa3CSPCnSpmssAC6JL0E171huylbNRffFwWGmXOMNGg==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/node": "^24.12.0", + "amaro": "^1.1.10", + "bson": "^7.3.1", + "flatted": "^3.4.2", + "typebox": "^1.3.3", + "typescript": "^7.0.2" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@torkbot/sandbox": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@torkbot/sandbox/-/sandbox-0.14.0.tgz", + "integrity": "sha512-k0/cYoChOKvvp3brhnHfKM+basNjJKF8nA7g7RE3dVXFhk7/j7I8apXer3NHdOqpX/HR5O2Tg92FegNEceUkAQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "bson": "^7.2.0", + "just-bash": "^2.14.5" + }, + "bin": { + "sandbox": "dist/cli.js" + }, + "engines": { + "node": ">=24.0.0" + }, + "optionalDependencies": { + "@torkbot/sandbox-darwin-arm64": "0.14.0", + "@torkbot/sandbox-linux-x64-gnu": "0.14.0" + } + }, + "node_modules/@torkbot/sandbox-darwin-arm64": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@torkbot/sandbox-darwin-arm64/-/sandbox-darwin-arm64-0.14.0.tgz", + "integrity": "sha512-2mKl1yYyKSg1isj9tRWvz/PVBcmK40OLhT50Zt47jWrw5voQfGcDRFyRXsCBfEoPYwyeiKbETc4zMYAdTcStIw==", + "cpu": [ + "arm64" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@torkbot/sandbox-linux-x64-gnu": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@torkbot/sandbox-linux-x64-gnu/-/sandbox-linux-x64-gnu-0.14.0.tgz", + "integrity": "sha512-8D53OVBUjIpN6BO7iruCWs7q5HapRdipM81ObmfXv/WRZ41HXEmrEyZFqbxzL21iy2qbxCa4T8T/N58x8h5mkQ==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@types/node": { "version": "24.13.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.18.0" @@ -33,7 +214,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -50,7 +230,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -67,7 +246,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -84,7 +262,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -101,7 +278,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -118,7 +294,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -135,7 +310,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -152,7 +326,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -169,7 +342,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -186,7 +358,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -203,7 +374,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -220,7 +390,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -237,7 +406,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -254,7 +422,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -271,7 +438,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -288,7 +454,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -305,7 +470,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -322,7 +486,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -339,7 +502,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -356,7 +518,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -366,11 +527,885 @@ "node": ">=16.20.0" } }, + "node_modules/amaro": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/amaro/-/amaro-1.1.10.tgz", + "integrity": "sha512-ceFv+QA3SlhFsn0hu8Q8oyj36YZdIgoJFpyS2sGJGK2dyncwcMWuBlNzhXfc1oLWtbDWM2Ol2rrzOYa+HNyEjg==", + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bson": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.3.1.tgz", + "integrity": "sha512-h/C0qe6857pQhcSJHLfsR1uYGj98Ge3wKAD3Ed9KqH3wcVh+BM4Jq4xISD7vs9OPuT07n+q3QQVjslJ286j6ag==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC", + "optional": true + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-xml-builder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz", + "integrity": "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.0.tgz", + "integrity": "sha512-SLhnTEqE5QpJHq/6zl9bsmImEP2adv+y6Wy+cJa7nVTRzQh1OZfCe9k29M5xN74LWnu0xa1zrUrq3KnOKl92Fg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.2.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.1", + "xml-naming": "^0.3.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "license": "ISC" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT", + "optional": true + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT", + "optional": true + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC", + "optional": true + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/is-unsafe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz", + "integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/just-bash": { + "version": "2.14.5", + "resolved": "https://registry.npmjs.org/just-bash/-/just-bash-2.14.5.tgz", + "integrity": "sha512-MCBGnRlDeZ/MM7mcw+ZuSGFMBsggajrmKz6e/hrOAN7syvVZkjiY+Vh2wyCwN/CdcnAX5SxbiQB51n5nrQuX+g==", + "license": "Apache-2.0", + "dependencies": { + "diff": "^8.0.2", + "fast-xml-parser": "^5.7.3", + "file-type": "^21.2.0", + "ini": "^6.0.0", + "minimatch": "^10.1.1", + "modern-tar": "^0.7.3", + "papaparse": "^5.5.3", + "quickjs-emscripten": "^0.32.0", + "re2js": "^1.2.1", + "seek-bzip": "^2.0.0", + "smol-toml": "^1.6.0", + "sprintf-js": "^1.1.3", + "sql.js": "^1.13.0", + "turndown": "^7.2.2", + "yaml": "^2.8.2" + }, + "bin": { + "just-bash": "dist/bin/just-bash.js", + "just-bash-shell": "dist/bin/shell/shell.js" + }, + "optionalDependencies": { + "@mongodb-js/zstd": "^7.0.0", + "node-liblzma": "^2.0.3" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT", + "optional": true + }, + "node_modules/modern-tar": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz", + "integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT", + "optional": true + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.0.tgz", + "integrity": "sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-liblzma": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-liblzma/-/node-liblzma-2.2.0.tgz", + "integrity": "sha512-s0KzNOWwOJJgPG6wxg6cKohnAl9Wk/oW1KrQaVzJBjQwVcUGPQCzpR46Ximygjqj/3KhOrtJXnYMp/xYAXp75g==", + "hasInstallScript": true, + "license": "LGPL-3.0", + "optional": true, + "dependencies": { + "node-addon-api": "^8.5.0", + "node-gyp-build": "^4.8.4" + }, + "bin": { + "nxz": "lib/cli/nxz.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/oorabona" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/papaparse": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.4.tgz", + "integrity": "sha512-SwzWD9gl/ElwYLCI0nUja1mFJzjq2D8ziShfNBa7zCHzkOozeOGDwHWQ+tvCzEZcewecWZ5U7kUopDnG+DFYEQ==", + "license": "MIT" + }, + "node_modules/path-expression-matcher": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/quickjs-emscripten": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/quickjs-emscripten/-/quickjs-emscripten-0.32.0.tgz", + "integrity": "sha512-So0Sqw869y/S2oE3Nuc0uT3Dhqgvsj8FSrwBdsuTosVsG8ME5/OcudU1GxsrIFdFABgy17GHnTVO9TYV/bLQcA==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-wasmfile-debug-asyncify": "0.32.0", + "@jitl/quickjs-wasmfile-debug-sync": "0.32.0", + "@jitl/quickjs-wasmfile-release-asyncify": "0.32.0", + "@jitl/quickjs-wasmfile-release-sync": "0.32.0", + "quickjs-emscripten-core": "0.32.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/quickjs-emscripten-core": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/quickjs-emscripten-core/-/quickjs-emscripten-core-0.32.0.tgz", + "integrity": "sha512-QFnPfjFey8EqknSrSxe1hZrf1/8z7/6s1QzGOmKo6++02r7QRRX7ZoyNaZh7JuVjWsVW87KnQrbZqnHkOAzUyg==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC", + "optional": true + }, + "node_modules/re2js": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/re2js/-/re2js-1.3.3.tgz", + "integrity": "sha512-s/I5zEAo79SUK0Qw4dpZKpiMwbQ6Gz0KU2NRr7eaO4x/p2g7Vvmn3hdeXDg8VsaUjfj/ora+e9oi27LX/C9+mw==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/seek-bzip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-2.0.0.tgz", + "integrity": "sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==", + "license": "MIT", + "dependencies": { + "commander": "^6.0.0" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smol-toml": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz", + "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/sql.js": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz", + "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strnum": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", + "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/turndown": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.4.tgz", + "integrity": "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==", + "license": "MIT", + "dependencies": { + "@mixmark-io/domino": "^2.2.0" + }, + "engines": { + "node": ">=18", + "npm": ">=9" + } + }, + "node_modules/typebox": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.6.tgz", + "integrity": "sha512-Sc8RA0NCMEFmApHNU9ZMzqcpQj46She44J8ffpLM/bdhLNUZKq7DJumcLcsFx1gRmDfQPgCgOmFFJ7rcnfWNyA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc" @@ -401,12 +1436,67 @@ "@typescript/typescript-win32-x64": "7.0.2" } }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undici-types": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT", + "optional": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "optional": true + }, + "node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } } } } diff --git a/package.json b/package.json index 2d47295..0f37951 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,9 @@ "devDependencies": { "@types/node": "^24.12.0", "typescript": "^7.0.2" + }, + "dependencies": { + "@torkbot/code-mode": "^0.2.0", + "@torkbot/sandbox": "^0.14.0" } } - diff --git a/src/index.test.ts b/src/index.test.ts new file mode 100644 index 0000000..0bd1cdb --- /dev/null +++ b/src/index.test.ts @@ -0,0 +1,395 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { Node24Runtime } from "@torkbot/code-mode/node"; +import type { + SandboxExecOptions, + SandboxExecResult, + SandboxInstance, + SandboxProcess, + SandboxProcessExit, + SandboxSignal, + SandboxSpawnOptions, +} from "@torkbot/sandbox"; + +import { createSandboxNodeRuntimeHost } from "./index.ts"; + +test("Sandbox Node runtime host launches the code-mode bootstrap over Sandbox process pipes", async () => { + const process = createSandboxProcess({ + channelOutput: [new TextEncoder().encode("runtime output")], + }); + const sandbox = createRuntimeSandbox(process.instance); + const runtime = createRuntime(sandbox.instance); + const signal = AbortSignal.timeout(5_000); + + const instance = await runtime.start({ + payload: { + kind: "javascript-module", + source: "export async function startProgram() { /* payload sentinel */ }", + }, + signal, + }); + + assert.deepEqual(sandbox.execCalls(), [{ + command: "/usr/bin/node", + args: ["--version"], + options: { + cwd: "/workspace", + signal, + }, + }]); + assert.deepEqual(sandbox.spawnCalls(), [{ + command: "/usr/bin/node", + args: ["--input-type=module"], + options: { + cwd: "/workspace", + pipes: [3], + }, + }]); + assert.equal(process.stdinClosed(), true); + assert.match( + new TextDecoder().decode(joinBytes(process.stdinWrites())), + /payload sentinel/, + ); + assert.equal(instance.channel.readable, process.channelOutputStream); + assert.equal(instance.channel.writable, process.channelInputStream); + assert.deepEqual( + (await collect(instance.channel.readable)).map((chunk) => ( + new TextDecoder().decode(chunk) + )), + ["runtime output"], + ); + + const writer = instance.channel.writable.getWriter(); + try { + await writer.write(new TextEncoder().encode("host input")); + await writer.close(); + } finally { + writer.releaseLock(); + } + assert.equal(process.channelInputClosed(), true); + assert.equal( + new TextDecoder().decode(joinBytes(process.channelInputWrites())), + "host input", + ); + + await Promise.all([ + instance.terminate("test complete"), + instance.terminate("test complete again"), + ]); + assert.deepEqual(process.kills(), ["SIGTERM"]); + assert.deepEqual(await instance.finished, { kind: "closed" }); + assert.equal(sandbox.closeCalls(), 0); +}); + +test("Sandbox Node runtime host rejects launch when Sandbox omits the requested pipe", async () => { + const process = createSandboxProcess({ + includeChannel: false, + readyError: new Error("guest launch failed without fd 3"), + }); + const sandbox = createRuntimeSandbox(process.instance); + const runtime = createRuntime(sandbox.instance); + + await assert.rejects(runtime.start({ + payload: { + kind: "javascript-module", + source: "export async function startProgram() {}", + }, + signal: AbortSignal.timeout(5_000), + }), /did not create fd 3/); + assert.deepEqual(process.kills(), ["SIGTERM"]); +}); + +test("Sandbox Node runtime host rejects a non-Node-24 guest before spawning", async () => { + const process = createSandboxProcess(); + const sandbox = createRuntimeSandbox(process.instance, { + exitCode: 0, + stdout: "v23.11.0\n", + stderr: "", + }); + const runtime = createRuntime(sandbox.instance); + + await assert.rejects(runtime.start({ + payload: { + kind: "javascript-module", + source: "export async function startProgram() {}", + }, + signal: AbortSignal.timeout(5_000), + }), /requires Node\.js 24/); + assert.deepEqual(sandbox.spawnCalls(), []); +}); + +test("Sandbox Node runtime host cleans up and rejects Sandbox launch failures", async () => { + const process = createSandboxProcess({ + readyError: new Error("guest launch failed"), + }); + const sandbox = createRuntimeSandbox(process.instance); + const runtime = createRuntime(sandbox.instance); + + await assert.rejects(runtime.start({ + payload: { + kind: "javascript-module", + source: "export async function startProgram() {}", + }, + signal: AbortSignal.timeout(5_000), + }), /guest launch failed/); + assert.deepEqual(process.kills(), ["SIGTERM"]); +}); + +test("Sandbox Node runtime host reports unexpected guest process failures with stderr", async () => { + const process = createSandboxProcess({ stderr: "guest exploded" }); + const sandbox = createRuntimeSandbox(process.instance); + const runtime = createRuntime(sandbox.instance); + const instance = await runtime.start({ + payload: { + kind: "javascript-module", + source: "export async function startProgram() {}", + }, + signal: AbortSignal.timeout(5_000), + }); + + process.resolveExit({ exitCode: 1, signal: null }); + const finished = await instance.finished; + assert.equal(finished.kind, "failed"); + if (finished.kind === "failed") { + assert.match(finished.error.message, /exit code 1/); + assert.match(finished.error.message, /guest exploded/); + } +}); + +test("Sandbox Node runtime host aborts a launched process and resolves it as closed", async () => { + const process = createSandboxProcess(); + const sandbox = createRuntimeSandbox(process.instance); + const runtime = createRuntime(sandbox.instance); + const controller = new AbortController(); + const instance = await runtime.start({ + payload: { + kind: "javascript-module", + source: "export async function startProgram() {}", + }, + signal: controller.signal, + }); + + controller.abort(new Error("cancel runtime")); + + assert.deepEqual(await instance.finished, { kind: "closed" }); + assert.deepEqual(process.kills(), ["SIGTERM"]); +}); + +test("Sandbox Node runtime host requires absolute guest paths", () => { + const sandbox = createRuntimeSandbox(createSandboxProcess().instance); + + assert.throws(() => createSandboxNodeRuntimeHost({ + sandbox: sandbox.instance, + nodePath: "/usr/bin/node", + cwd: "workspace", + }), /cwd must be an absolute guest path/); + assert.throws(() => createSandboxNodeRuntimeHost({ + sandbox: sandbox.instance, + nodePath: "node", + cwd: "/workspace", + }), /nodePath must be an absolute guest path/); + assert.deepEqual(sandbox.execCalls(), []); + assert.deepEqual(sandbox.spawnCalls(), []); +}); + +function createRuntime(sandbox: SandboxInstance): Node24Runtime { + return new Node24Runtime(createSandboxNodeRuntimeHost({ + sandbox, + nodePath: "/usr/bin/node", + cwd: "/workspace", + })); +} + +type ObservedExec = { + readonly command: string; + readonly args: readonly string[] | undefined; + readonly options: SandboxExecOptions | undefined; +}; + +type ObservedSpawn = { + readonly command: string; + readonly args: readonly string[] | undefined; + readonly options: SandboxSpawnOptions | undefined; +}; + +function createRuntimeSandbox( + process: SandboxProcess, + versionResult: SandboxExecResult = { + exitCode: 0, + stdout: "v24.12.0\n", + stderr: "", + }, +): { + readonly instance: SandboxInstance; + execCalls(): readonly ObservedExec[]; + spawnCalls(): readonly ObservedSpawn[]; + closeCalls(): number; +} { + const execCalls: ObservedExec[] = []; + const spawnCalls: ObservedSpawn[] = []; + let closeCalls = 0; + const unsupported = (): never => { + throw new Error("not used by this runtime host test"); + }; + const close = async (): Promise => { + closeCalls += 1; + }; + + return { + instance: { + fs: { + stat: unsupported, + readDir: unsupported, + readFile: unsupported, + writeFile: unsupported, + mkdir: unsupported, + remove: unsupported, + rename: unsupported, + }, + async environmentFacts() { + return []; + }, + async exec(command, args, options): Promise { + execCalls.push({ command, args, options }); + return versionResult; + }, + spawn(command, args, options) { + spawnCalls.push({ command, args, options }); + return process; + }, + pty: unsupported, + close, + [Symbol.asyncDispose]: close, + }, + execCalls: () => execCalls, + spawnCalls: () => spawnCalls, + closeCalls: () => closeCalls, + }; +} + +function createSandboxProcess(options: { + readonly channelOutput?: readonly Uint8Array[]; + readonly includeChannel?: boolean; + readonly readyError?: Error; + readonly stderr?: string; +} = {}): { + readonly instance: SandboxProcess; + readonly channelInputStream: WritableStream; + readonly channelOutputStream: ReadableStream; + channelInputClosed(): boolean; + channelInputWrites(): readonly Uint8Array[]; + stdinClosed(): boolean; + stdinWrites(): readonly Uint8Array[]; + kills(): readonly SandboxSignal[]; + resolveExit(exit: SandboxProcessExit): void; +} { + const stdin = createWritableRecorder(); + const channelInput = createWritableRecorder(); + const channelOutput = readableFrom(options.channelOutput ?? []); + const exit = deferred(); + const kills: SandboxSignal[] = []; + const includeChannel = options.includeChannel ?? true; + + return { + instance: { + stdin: stdin.stream, + stdout: readableFrom([]), + stderr: readableFrom([ + new TextEncoder().encode(options.stderr ?? ""), + ]), + pipes: includeChannel + ? new Map([[3, { + input: channelInput.stream, + output: channelOutput, + }]]) + : new Map(), + ready: options.readyError === undefined + ? Promise.resolve() + : Promise.reject(options.readyError), + exit: exit.promise, + kill(signal = "SIGTERM") { + kills.push(signal); + exit.resolve({ exitCode: null, signal }); + }, + }, + channelInputStream: channelInput.stream, + channelOutputStream: channelOutput, + channelInputClosed: channelInput.closed, + channelInputWrites: channelInput.writes, + stdinClosed: stdin.closed, + stdinWrites: stdin.writes, + kills: () => kills, + resolveExit: exit.resolve, + }; +} + +function createWritableRecorder(): { + readonly stream: WritableStream; + closed(): boolean; + writes(): readonly Uint8Array[]; +} { + const writes: Uint8Array[] = []; + let closed = false; + return { + stream: new WritableStream({ + write(chunk) { + writes.push(chunk.slice()); + }, + close() { + closed = true; + }, + }), + closed: () => closed, + writes: () => writes, + }; +} + +function readableFrom( + chunks: readonly Uint8Array[], +): ReadableStream { + return new ReadableStream({ + start(controller) { + for (const chunk of chunks) { + controller.enqueue(chunk); + } + controller.close(); + }, + }); +} + +function deferred(): { + readonly promise: Promise; + resolve(value: T): void; +} { + let resolvePromise: ((value: T) => void) | undefined; + const promise = new Promise((resolve) => { + resolvePromise = resolve; + }); + return { + promise, + resolve(value) { + assert.ok(resolvePromise !== undefined); + resolvePromise(value); + }, + }; +} + +function joinBytes(chunks: readonly Uint8Array[]): Uint8Array { + const length = chunks.reduce((sum, chunk) => sum + chunk.length, 0); + const joined = new Uint8Array(length); + let offset = 0; + for (const chunk of chunks) { + joined.set(chunk, offset); + offset += chunk.length; + } + return joined; +} + +async function collect(values: AsyncIterable): Promise { + const collected: T[] = []; + for await (const value of values) { + collected.push(value); + } + return collected; +} diff --git a/src/index.ts b/src/index.ts index 638ec62..baccc0a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,269 @@ -export {}; +import type { + Node24RuntimeHost, + Node24RuntimeLaunchRequest, +} from "@torkbot/code-mode/node"; +import type { + RuntimeFinished, + RuntimeInstance, +} from "@torkbot/code-mode/runtime"; +import type { + SandboxInstance, + SandboxProcess, + SandboxProcessExit, +} from "@torkbot/sandbox"; +const maximumStderrLength = 64 * 1024; +const terminationGracePeriodMilliseconds = 1_000; + +export interface SandboxNodeRuntimeHostOptions { + readonly sandbox: SandboxInstance; + readonly nodePath: string; + readonly cwd: string; +} + +/** + * Creates the Sandbox execution host required by Node24Runtime. + * + * The caller must keep the Sandbox instance open while the runtime is in use + * and remains responsible for closing it. The host owns only the guest + * processes it launches through Node24Runtime. + */ +export function createSandboxNodeRuntimeHost( + options: SandboxNodeRuntimeHostOptions, +): Node24RuntimeHost { + requireAbsoluteGuestPath("cwd", options.cwd); + requireAbsoluteGuestPath("nodePath", options.nodePath); + + return { + readNodeVersion: (signal) => readSandboxNodeVersion(options, signal), + launchNode: (req) => launchSandboxNode(options, req), + }; +} + +function requireAbsoluteGuestPath(name: string, value: string): void { + if (!value.startsWith("/")) { + throw new TypeError(`${name} must be an absolute guest path`); + } +} + +async function readSandboxNodeVersion( + options: SandboxNodeRuntimeHostOptions, + signal: AbortSignal, +): Promise { + signal.throwIfAborted(); + const result = await options.sandbox.exec( + options.nodePath, + ["--version"], + { + cwd: options.cwd, + signal, + }, + ); + signal.throwIfAborted(); + + if (result.exitCode !== 0) { + throw new Error(formatVersionFailure(result.exitCode, result.stderr)); + } + return result.stdout.trim(); +} + +async function launchSandboxNode( + options: SandboxNodeRuntimeHostOptions, + req: Node24RuntimeLaunchRequest, +): Promise { + req.signal.throwIfAborted(); + const process = options.sandbox.spawn( + options.nodePath, + ["--input-type=module"], + { + cwd: options.cwd, + pipes: [req.channelFileDescriptor], + }, + ); + const pipe = process.pipes.get(req.channelFileDescriptor); + if (pipe === undefined) { + await stopPartialLaunch(process); + throw new Error( + `Sandbox Node.js runtime did not create fd ${req.channelFileDescriptor}`, + ); + } + + const stdout = drain(process.stdout); + const stderr = readTextTail(process.stderr); + let terminationRequested = false; + let finishedSettled = false; + let forceTerminationTimeout: ReturnType | undefined; + + const requestTermination = (): void => { + if (finishedSettled || terminationRequested) { + return; + } + terminationRequested = true; + process.kill("SIGTERM"); + forceTerminationTimeout ??= setTimeout(() => { + process.kill("SIGKILL"); + }, terminationGracePeriodMilliseconds); + forceTerminationTimeout.unref(); + }; + const abort = (): void => requestTermination(); + + if (req.signal.aborted) { + abort(); + } else { + req.signal.addEventListener("abort", abort, { once: true }); + } + + const finished: Promise = (async () => { + try { + const [launchError, exit, , stderrText] = await Promise.all([ + process.ready.then( + () => null, + (error: unknown) => errorFromUnknown(error), + ), + process.exit, + stdout, + stderr, + ]); + + if (terminationRequested) { + return { kind: "closed" }; + } + if (launchError !== null) { + return { kind: "failed", error: launchError }; + } + if (exit.exitCode === 0 && exit.signal === null) { + return { kind: "closed" }; + } + return { + kind: "failed", + error: new Error(formatProcessFailure(exit, stderrText)), + }; + } catch (error) { + return { + kind: "failed", + error: errorFromUnknown(error), + }; + } finally { + finishedSettled = true; + req.signal.removeEventListener("abort", abort); + if (forceTerminationTimeout !== undefined) { + clearTimeout(forceTerminationTimeout); + } + } + })(); + + try { + await process.ready; + await writeAndClose( + process.stdin, + new TextEncoder().encode(req.bootstrapSource), + ); + } catch (error) { + requestTermination(); + await finished; + if (req.signal.aborted) { + throw req.signal.reason; + } + throw error; + } + + if (req.signal.aborted) { + requestTermination(); + await finished; + throw req.signal.reason; + } + + const channel: RuntimeInstance["channel"] = { + readable: pipe.output, + writable: pipe.input, + }; + + return { + channel, + finished, + async terminate(_reason: string): Promise { + requestTermination(); + await finished; + }, + }; +} + +async function stopPartialLaunch(process: SandboxProcess): Promise { + process.kill("SIGTERM"); + const forceTerminationTimeout = setTimeout(() => { + process.kill("SIGKILL"); + }, terminationGracePeriodMilliseconds); + forceTerminationTimeout.unref(); + try { + await Promise.allSettled([ + process.ready, + process.exit, + drain(process.stdout), + drain(process.stderr), + ]); + } finally { + clearTimeout(forceTerminationTimeout); + } +} + +async function writeAndClose( + stream: WritableStream, + contents: Uint8Array, +): Promise { + const writer = stream.getWriter(); + try { + await writer.write(contents); + await writer.close(); + } finally { + writer.releaseLock(); + } +} + +async function drain(stream: ReadableStream): Promise { + for await (const _chunk of stream) { + // Drain output so an unconsumed stream cannot block process completion. + } +} + +async function readTextTail( + stream: ReadableStream, +): Promise { + const decoder = new TextDecoder(); + let text = ""; + for await (const chunk of stream) { + text = appendTextTail(text, decoder.decode(chunk, { stream: true })); + } + return appendTextTail(text, decoder.decode()); +} + +function appendTextTail(current: string, chunk: string): string { + if (chunk.length >= maximumStderrLength) { + return chunk.slice(-maximumStderrLength); + } + const overflow = current.length + chunk.length - maximumStderrLength; + return overflow > 0 + ? `${current.slice(overflow)}${chunk}` + : `${current}${chunk}`; +} + +function formatVersionFailure(exitCode: number, stderr: string): string { + const status = `Sandbox Node.js version check failed with exit code ${exitCode}`; + const detail = stderr.trim(); + return detail.length === 0 ? status : `${status}: ${detail}`; +} + +function formatProcessFailure( + exit: SandboxProcessExit, + stderr: string, +): string { + const status = [ + "Sandbox Node.js runtime failed with", + `exit code ${exit.exitCode}, signal ${exit.signal}`, + ].join(" "); + const detail = stderr.trim(); + return detail.length === 0 ? status : `${status}: ${detail}`; +} + +function errorFromUnknown(error: unknown): Error { + return error instanceof Error ? error : new Error(String(error)); +} diff --git a/src/package.test.ts b/src/package.test.ts new file mode 100644 index 0000000..778ff09 --- /dev/null +++ b/src/package.test.ts @@ -0,0 +1,24 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +test("package publishes the Sandbox integration against released contracts", async () => { + const packageJson = JSON.parse( + await readFile(new URL("../package.json", import.meta.url), "utf8"), + ) as { + readonly dependencies: Readonly>; + readonly exports: Readonly>; + readonly files: readonly string[]; + }; + const source = await readFile(new URL("./index.ts", import.meta.url), "utf8"); + + assert.equal(packageJson.dependencies["@torkbot/code-mode"], "^0.2.0"); + assert.equal(packageJson.dependencies["@torkbot/sandbox"], "^0.14.0"); + assert.deepEqual(Object.keys(packageJson.exports), ["."]); + assert.deepEqual(packageJson.files, ["dist"]); + assert.match(source, /from "@torkbot\/code-mode\/node"/); + assert.match(source, /export function createSandboxNodeRuntimeHost/); + assert.doesNotMatch(source, /extends Node24Runtime/); + assert.doesNotMatch(source, /WebByteWriter|readableChunks/); + assert.doesNotMatch(source, /@torkbot\/code-mode\/sandbox-node/); +});