diff --git a/package-lock.json b/package-lock.json index 5c6866f51..03d61291a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,17 @@ "version": "1.0.0-alpha.8", "license": "MIT", "dependencies": { + "@modelcontextprotocol/sdk": "^1.30.1", + "dockerode": "^5.0.1", "nanoid": "^5.0.7", "undici": "^8.11.2" }, "devDependencies": { + "@ai-sdk/anthropic": "^0.0.42", "@opentelemetry/api": "^1.9.1", "@opentelemetry/sdk-trace-base": "^2.11.0", "@opentelemetry/sdk-trace-node": "^2.11.0", + "@types/dockerode": "^4.0.1", "@types/node": "^20", "@typescript-eslint/eslint-plugin": "^8.70.1", "@typescript-eslint/parser": "^8.70.1", @@ -32,12 +36,16 @@ "node": ">=18.0.0" }, "peerDependencies": { + "@ai-sdk/anthropic": "^0.0.42", "@ai-sdk/openai": "^0.0.42", "ai": "^4.3.19", "ollama-ai-provider": "^1.2.0", "zod": "^3.25.76" }, "peerDependenciesMeta": { + "@ai-sdk/anthropic": { + "optional": true + }, "@ai-sdk/openai": { "optional": true }, @@ -46,6 +54,89 @@ } } }, + "node_modules/@ai-sdk/anthropic": { + "version": "0.0.42", + "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-0.0.42.tgz", + "integrity": "sha512-eXgh/9ePf2VitUmU0cNmbPp5JqQWz85zW0HHoeXSffRmJgxCpE3d2RjymZUeS+llu3A9O1Eb7UTy3VZqPN0A0Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "0.0.19", + "@ai-sdk/provider-utils": "1.0.12" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@ai-sdk/anthropic/node_modules/@ai-sdk/provider": { + "version": "0.0.19", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.19.tgz", + "integrity": "sha512-WXyyJ0Fqg0OHzUQ/spem4jRFPq+NkYB8YNTVbSQrE+Rpxtm7DqK1x9MrHtIEhoikCMg9wBOCbm7HuMnfsIR5GA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "json-schema": "0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/anthropic/node_modules/@ai-sdk/provider-utils": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.12.tgz", + "integrity": "sha512-j8ajjkjoJkeqApQYOWPLd3R/P+wUxppEBHUfupF7fN6aICzLVFH3hXUiR5PJ+0pcM/Ld3CZDPq1IKNZlufI5Bw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "0.0.19", + "eventsource-parser": "1.1.2", + "nanoid": "3.3.6", + "secure-json-parse": "2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@ai-sdk/anthropic/node_modules/eventsource-parser": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz", + "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@ai-sdk/anthropic/node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/@ai-sdk/provider": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz", @@ -203,6 +294,12 @@ "node": ">=6.9.0" } }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "license": "Apache-2.0" + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -809,6 +906,67 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.5", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.5.tgz", + "integrity": "sha512-7VZM+SVdEcUUqSQeNI3zM8Qs/BhQKZndPo2h5VkYkAM8Iz0wJIa8mKV5ekQGqG8UUsnkQ0NMxIxwkIHYvj0qOw==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -955,6 +1113,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@keyv/bigmap": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", @@ -979,6 +1147,68 @@ "dev": true, "license": "MIT" }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.1.tgz", + "integrity": "sha512-H2HxLvC3HDNybePJaLdSrU1hhUK5iQw+WvV1b01myFyI7sdVGe1u/IPTE5D9fGCiJDVtgMV/lmFkQXLmQyIFYA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -1323,6 +1553,63 @@ "node": ">=14" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.52.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", @@ -1645,6 +1932,29 @@ "license": "MIT", "peer": true }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-4.0.1.tgz", + "integrity": "sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -1670,12 +1980,38 @@ "version": "20.19.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz", "integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/ssh2": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.6.tgz", + "integrity": "sha512-oGdxhBqcRTwSTKFm+9EiKzkNVYRLEFkcW44lhguvBalGJbWfGnDt/ezwSUZc+SF9m9bMc3VyklNAtp7zICjS5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.70.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.1.tgz", @@ -2064,6 +2400,19 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", @@ -2154,6 +2503,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -2187,6 +2575,15 @@ "dev": true, "license": "MIT" }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/asn1js": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", @@ -2219,48 +2616,167 @@ "dev": true, "license": "MIT" }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, + "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" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "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", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" } }, - "node_modules/bundle-require": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", - "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", - "dev": true, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", "dependencies": { - "load-tsconfig": "^0.2.3" + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, - "peerDependencies": { - "esbuild": ">=0.18" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/bytestreamjs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", - "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2280,6 +2796,35 @@ "qified": "^0.10.1" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/chai": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", @@ -2341,11 +2886,103 @@ "url": "https://paulmillr.com/funding/" } }, + "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" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2358,7 +2995,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/commander": { @@ -2395,11 +3031,81 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2414,7 +3120,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2448,6 +3153,15 @@ "dev": true, "license": "MIT" }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2475,6 +3189,52 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/docker-modem": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.7.tgz", + "integrity": "sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.15.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dockerode": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-5.0.1.tgz", + "integrity": "sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==", + "license": "Apache-2.0", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.7", + "protobufjs": "^7.3.2", + "tar-fs": "^2.1.4" + }, + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2482,6 +3242,12 @@ "dev": true, "license": "MIT" }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -2489,6 +3255,54 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.25.10", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", @@ -2531,6 +3345,21 @@ "@esbuild/win32-x64": "0.25.10" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2784,6 +3613,36 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -2808,11 +3667,72 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", + "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-json-stable-stringify": { @@ -2829,6 +3749,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2857,6 +3793,27 @@ "flat-cache": "^6.1.23" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2922,6 +3879,30 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2944,6 +3925,24 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -2954,6 +3953,43 @@ "node": "*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", @@ -3002,6 +4038,18 @@ "node": ">=10.13.0" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3012,6 +4060,18 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hashery": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", @@ -3025,6 +4085,27 @@ "node": ">=20" } }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.13.10", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.10.tgz", + "integrity": "sha512-dQuLsa5oO+47QVMVMaaD9cIv8ctmVtK1iRvwWngkfloFJMeFeuoUFDswIqZGxmGX3hrRzREgEArjkK9OgsQEhA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/hookified": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", @@ -3039,6 +4120,26 @@ "dev": true, "license": "MIT" }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -3049,6 +4150,42 @@ "node": ">=16.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/ignore": { "version": "7.0.10", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.10.tgz", @@ -3085,9 +4222,26 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3102,7 +4256,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3121,6 +4274,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -3138,7 +4297,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -3211,6 +4369,15 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -3232,8 +4399,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "license": "(AFL-2.1 OR BSD-3-Clause)", - "peer": true + "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -3242,6 +4408,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -3354,6 +4526,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -3361,6 +4539,12 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -3416,6 +4600,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -3423,6 +4641,31 @@ "dev": true, "license": "MIT" }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -3459,6 +4702,12 @@ "node": ">=16 || 14 >=14.17" } }, + "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" + }, "node_modules/mlly": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", @@ -3476,7 +4725,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -3491,6 +4739,13 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nan": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.29.0.tgz", + "integrity": "sha512-GlGk3HIvitbvs+LT3g6XUP1kpirKNvmDFwF/bmo6XNWSb/eYEs/O4bfgIEIXCZ+lIOTS5xNwDvSGMw6FJdAhtA==", + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", @@ -3516,6 +4771,35 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -3549,17 +4833,39 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "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==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -3667,6 +4973,15 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3691,7 +5006,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3714,6 +5028,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -3761,6 +5085,15 @@ "node": ">= 6" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-types": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", @@ -3910,6 +5243,56 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3937,29 +5320,73 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=16.0.0" + "node": ">=16.0.0" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/qified": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", - "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", - "dev": true, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "hookified": "^2.1.1" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">=20" + "node": ">= 0.10" } }, - "node_modules/qified/node_modules/hookified": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", - "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", - "dev": true, - "license": "MIT" - }, "node_modules/react": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", @@ -3977,6 +5404,20 @@ "dev": true, "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", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -3998,6 +5439,24 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -4050,12 +5509,53 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/secure-json-parse": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/selfsigned": { "version": "5.5.0", @@ -4084,11 +5584,61 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -4101,12 +5651,83 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -4151,6 +5772,29 @@ "node": ">=0.10.0" } }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", + "license": "ISC" + }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -4158,6 +5802,15 @@ "dev": true, "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", @@ -4165,6 +5818,15 @@ "dev": true, "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", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -4392,6 +6054,34 @@ "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "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", + "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", + "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/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -4494,6 +6184,15 @@ "node": ">=14.0.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tr46": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", @@ -5117,6 +6816,12 @@ "dev": true, "license": "0BSD" }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5140,6 +6845,37 @@ "node": ">=4" } }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -5197,9 +6933,17 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -5220,6 +6964,21 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "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" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "5.4.20", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", @@ -5836,7 +7595,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -5990,9 +7748,85 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", @@ -6011,7 +7845,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -6021,7 +7854,6 @@ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", "license": "ISC", - "peer": true, "peerDependencies": { "zod": "^3.25.28 || ^4" } diff --git a/package.json b/package.json index 23a84ef3b..82b3bea6f 100644 --- a/package.json +++ b/package.json @@ -82,12 +82,16 @@ "node": ">=18.0.0" }, "peerDependencies": { + "@ai-sdk/anthropic": "^0.0.42", "@ai-sdk/openai": "^0.0.42", "ai": "^4.3.19", "ollama-ai-provider": "^1.2.0", "zod": "^3.25.76" }, "peerDependenciesMeta": { + "@ai-sdk/anthropic": { + "optional": true + }, "@ai-sdk/openai": { "optional": true }, @@ -96,13 +100,17 @@ } }, "dependencies": { + "@modelcontextprotocol/sdk": "^1.30.1", + "dockerode": "^5.0.1", "nanoid": "^5.0.7", "undici": "^8.11.2" }, "devDependencies": { + "@ai-sdk/anthropic": "^0.0.42", "@opentelemetry/api": "^1.9.1", "@opentelemetry/sdk-trace-base": "^2.11.0", "@opentelemetry/sdk-trace-node": "^2.11.0", + "@types/dockerode": "^4.0.1", "@types/node": "^20", "@typescript-eslint/eslint-plugin": "^8.70.1", "@typescript-eslint/parser": "^8.70.1", diff --git a/src/execution/AgentExecutor.ts b/src/execution/AgentExecutor.ts index 12988478f..fa535d8b9 100644 --- a/src/execution/AgentExecutor.ts +++ b/src/execution/AgentExecutor.ts @@ -7,9 +7,11 @@ import { nanoid } from 'nanoid'; import { LLMProvider, Message, ToolCall, GenerateOptions, GenerateResult } from '../providers'; import { AgentConfig } from '../types'; import { ToolRegistry } from '../tools'; +import { SandboxAdapter, NoopSandbox } from '../security/sandbox'; import { ApprovalStore, ExecutionSnapshot, PendingApproval } from './ApprovalGate'; import { Checkpoint, CheckpointStore } from './checkpoint'; import { TraceExporter, withSpan } from './tracing'; +import { executeToolWithSandboxGuard } from './sandboxGuard'; /** * Base class for tool errors that must NOT be swallowed by @@ -161,6 +163,14 @@ export interface ExecuteOptions { * redacted. Defaults to false. */ redactContent?: boolean; + /** + * SandboxAdapter used for tools flagged `requiresSandbox` (LOU-F5). Since + * AgentExecutor is a static, instance-free API, this is read per-call + * (`options.sandbox ?? NoopSandbox`) rather than held as construction + * state. Defaults to NoopSandbox - the zero-isolation, trusted-host + * adapter - when omitted, so existing callers see no behavior change. + */ + sandbox?: SandboxAdapter; } /** @@ -236,6 +246,7 @@ export class AgentExecutor { onToolResult, exporter, redactContent = false, + sandbox = NoopSandbox, } = options; // Emit start event @@ -374,7 +385,8 @@ export class AgentExecutor { agent, toolRegistry, onToolCall, - onToolResult + onToolResult, + sandbox ); let parsedArgs: unknown = executed.args; if (parsedArgs === undefined) { @@ -584,7 +596,8 @@ export class AgentExecutor { _agent: AgentConfig, toolRegistry?: ToolRegistry, onToolCall?: ExecuteOptions['onToolCall'], - onToolResult?: ExecuteOptions['onToolResult'] + onToolResult?: ExecuteOptions['onToolResult'], + sandbox: SandboxAdapter = NoopSandbox ): Promise<{ toolCallId: string; toolName: string; @@ -611,7 +624,7 @@ export class AgentExecutor { let thrown: unknown; try { - outcome = await this.doExecuteToolCall(toolCall, toolRegistry); + outcome = await this.doExecuteToolCall(toolCall, toolRegistry, sandbox); return outcome; } catch (error) { thrown = error; @@ -631,7 +644,8 @@ export class AgentExecutor { */ private static async doExecuteToolCall( toolCall: ToolCall, - toolRegistry?: ToolRegistry + toolRegistry?: ToolRegistry, + sandbox: SandboxAdapter = NoopSandbox ): Promise<{ toolCallId: string; toolName: string; @@ -677,8 +691,19 @@ export class AgentExecutor { }; } - // The 'ai' SDK tool.execute expects (args, context) - const result = await toolDesc.tool.execute(args, {} as any); + // The 'ai' SDK tool.execute expects (args, context). Tools flagged + // `requiresSandbox` (LOU-F5) are routed through the configured + // SandboxAdapter instead of being invoked directly here; a tool + // WITHOUT the flag takes this exact, unchanged branch. This + // branching now lives in the shared executeToolWithSandboxGuard() + // helper (LOU-F fix) so FlowExecutor.ts and resume.ts share the + // exact same fail-closed behavior instead of each reimplementing it. + const result = await executeToolWithSandboxGuard( + toolCall.function.name, + toolDesc, + args, + sandbox + ); return { toolCallId: toolCall.id, diff --git a/src/execution/resume.ts b/src/execution/resume.ts index f324e426f..fc7121a56 100644 --- a/src/execution/resume.ts +++ b/src/execution/resume.ts @@ -8,6 +8,8 @@ import { ToolRegistry } from '../tools'; import { ApprovalDecision, ApprovalStore } from './ApprovalGate'; import { AgentExecutor, ExecuteOptions, ExecutionResult, PropagatingToolError } from './AgentExecutor'; import { CheckpointStore } from './checkpoint'; +import { NoopSandbox } from '../security/sandbox'; +import { executeToolWithSandboxGuard } from './sandboxGuard'; /** * Options passed through to the underlying AgentExecutor.execute() call @@ -82,12 +84,23 @@ export async function resumeAfterApproval( if (decision.approved) { const toolDesc = toolRegistry.get(pending.toolName); - if (!toolDesc || !toolDesc.tool || !toolDesc.tool.execute) { + // A `requiresSandbox` tool may have no `tool.execute` implementation at + // all - its real work happens in `sandboxExecute()` instead - so the + // "not found" guard below must not reject that case outright; it only + // means there is genuinely no way to run the tool (neither a direct + // `execute` nor a `sandboxExecute`). + if (!toolDesc || !toolDesc.tool || (!toolDesc.tool.execute && !toolDesc.sandboxExecute)) { throw new Error(`Tool '${pending.toolName}' not found in registry`); } try { - const result = await toolDesc.tool.execute(pending.args, {} as any); + // Mirrors AgentExecutor.executeToolCall()'s fail-closed handling of + // `requiresSandbox` tools (LOU-F5) via the shared + // executeToolWithSandboxGuard() helper (LOU-F fix), so a deferred + // tool executed after human approval can't silently bypass the + // sandbox seam the way it previously did. + const sandbox = executeOptions.sandbox ?? NoopSandbox; + const result = await executeToolWithSandboxGuard(pending.toolName, toolDesc, pending.args, sandbox); messages.push({ role: 'tool', diff --git a/src/execution/sandbox-wiring.test.ts b/src/execution/sandbox-wiring.test.ts new file mode 100644 index 000000000..9e838ee3c --- /dev/null +++ b/src/execution/sandbox-wiring.test.ts @@ -0,0 +1,246 @@ +import { describe, it, expect, vi } from 'vitest'; +import { AgentExecutor } from './AgentExecutor'; +import { ToolRegistry } from '../tools'; +import { AgentBuilder } from '../core'; +import { AgentType } from '../types'; +import { SandboxAdapter } from '../security/sandbox'; + +/** + * Builds a scripted provider that emits exactly one tool call (for + * `toolName`) and then stops - enough to drive AgentExecutor through a + * single tool execution without depending on MockLLMProvider's + * content-sniffing heuristics. + */ +function makeSingleToolCallProvider(toolName: string) { + let call = 0; + return { + name: 'scripted', + supportsTools: () => true, + supportsStreaming: () => false, + getModels: async () => ['scripted'], + stream: async () => { + throw new Error('not implemented'); + }, + generate: async () => { + call++; + if (call === 1) { + return { + text: '', + finishReason: 'tool_calls' as const, + usage: { promptTokens: 1, completionTokens: 1, totalTokens: 2 }, + toolCalls: [ + { + id: 'call-1', + type: 'function' as const, + function: { name: toolName, arguments: '{}' }, + }, + ], + }; + } + return { + text: 'done', + finishReason: 'stop' as const, + usage: { promptTokens: 1, completionTokens: 1, totalTokens: 2 }, + }; + }, + }; +} + +describe('AgentExecutor sandbox wiring (LOU-F5 / LOU-F fix)', () => { + it("routes a requiresSandbox:true tool with sandboxExecute() through the configured SandboxAdapter, never touching tool.execute()", async () => { + const toolExecute = vi.fn().mockResolvedValue({ done: true }); + const toolRegistry = new ToolRegistry(); + + // A genuine sandboxExecute implementation: it does the tool's real + // work itself via sandbox.writeFile()/sandbox.run() - e.g. writing the + // args to a file and running a command that operates on them - rather + // than a disconnected dummy command. + const sandboxExecute = vi.fn(async (args: unknown, sandbox: SandboxAdapter) => { + await sandbox.writeFile('sandboxed-args.json', JSON.stringify(args)); + const runResult = await sandbox.run('echo', ['hello-from-real-work']); + return { stdout: runResult.stdout }; + }); + + toolRegistry.register('sandboxed', { + displayName: 'Sandboxed tool', + tool: { description: 'sandboxed', parameters: {}, execute: toolExecute } as any, + requiresSandbox: true, + sandboxExecute, + }); + + const agent = AgentBuilder.create() + .setType(AgentType.SmartAssistant) + .setName('Test Agent') + .addTool('sandboxed', { tool: 'sandboxed', options: {} }) + .build(); + + const spySandbox: SandboxAdapter = { + name: 'spy', + run: vi.fn().mockResolvedValue({ stdout: 'hello-from-real-work', stderr: '', exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), + }; + + const onToolResult = vi.fn(); + + const result = await AgentExecutor.execute({ + agent, + input: 'go', + provider: makeSingleToolCallProvider('sandboxed') as any, + toolRegistry, + sandbox: spySandbox, + onToolResult, + }); + + // sandboxExecute() itself was invoked, with the real args and the + // configured sandbox adapter. + expect(sandboxExecute).toHaveBeenCalledTimes(1); + expect(sandboxExecute).toHaveBeenCalledWith({}, spySandbox); + + // The sandbox adapter's run()/writeFile() were called with parameters + // that actually relate to the tool's real work (not a disconnected + // version-check command). + expect(spySandbox.writeFile).toHaveBeenCalledWith( + 'sandboxed-args.json', + JSON.stringify({}) + ); + expect(spySandbox.run).toHaveBeenCalledWith('echo', ['hello-from-real-work']); + + // Critical assertion: the tool's own in-process execute() is NEVER + // called for a genuinely-sandboxed tool. + expect(toolExecute).not.toHaveBeenCalled(); + + expect(result.finishReason).toBe('stop'); + expect(onToolResult).toHaveBeenCalledTimes(1); + const [, toolResultArg] = onToolResult.mock.calls[0]; + expect(toolResultArg?.error).toBeUndefined(); + expect(toolResultArg?.result).toEqual({ stdout: 'hello-from-real-work' }); + }); + + it('a requiresSandbox:true tool with NO sandboxExecute() fails closed - it throws instead of silently running in-process', async () => { + const toolExecute = vi.fn().mockResolvedValue({ done: true }); + const toolRegistry = new ToolRegistry(); + toolRegistry.register('unsandboxable', { + displayName: 'Unsandboxable tool', + tool: { description: 'unsandboxable', parameters: {}, execute: toolExecute } as any, + requiresSandbox: true, + // no sandboxExecute implementation - this is the bug scenario + }); + + const agent = AgentBuilder.create() + .setType(AgentType.SmartAssistant) + .setName('Test Agent') + .addTool('unsandboxable', { tool: 'unsandboxable', options: {} }) + .build(); + + const spySandbox: SandboxAdapter = { + name: 'spy', + run: vi.fn().mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), + }; + + const onToolResult = vi.fn(); + + const result = await AgentExecutor.execute({ + agent, + input: 'go', + provider: makeSingleToolCallProvider('unsandboxable') as any, + toolRegistry, + sandbox: spySandbox, + onToolResult, + }); + + // Fail-closed: neither the tool's real execute() NOR any real sandbox + // operation happens. Before this fix, tool.execute() would have run + // in-process on the host unconditionally, and sandbox.run() would have + // fired an unrelated dummy command - both silently, while the caller + // believed the tool was isolated. + expect(toolExecute).not.toHaveBeenCalled(); + expect(spySandbox.run).not.toHaveBeenCalled(); + expect(spySandbox.writeFile).not.toHaveBeenCalled(); + + // The clear, descriptive error surfaces as a conversational tool + // result (the established fail-closed / error-surfacing pattern in + // this codebase - see the 'failingTool' test above), not a silent + // unsandboxed execution. + expect(onToolResult).toHaveBeenCalledTimes(1); + const [, toolResultArg] = onToolResult.mock.calls[0]; + expect(toolResultArg?.error).toContain('unsandboxable'); + expect(toolResultArg?.error).toContain('requiresSandbox'); + expect(toolResultArg?.error).toContain('sandboxExecute'); + expect(toolResultArg?.error).toContain('refusing to fall back to unsandboxed execution'); + + expect(result.finishReason).toBe('stop'); + }); + + it('a tool WITHOUT requiresSandbox never touches the sandbox adapter (unchanged prior path)', async () => { + const toolExecute = vi.fn().mockResolvedValue({ done: true }); + const toolRegistry = new ToolRegistry(); + toolRegistry.register('plain', { + displayName: 'Plain tool', + tool: { description: 'plain', parameters: {}, execute: toolExecute } as any, + // no requiresSandbox + }); + + const agent = AgentBuilder.create() + .setType(AgentType.SmartAssistant) + .setName('Test Agent') + .addTool('plain', { tool: 'plain', options: {} }) + .build(); + + const spySandbox: SandboxAdapter = { + name: 'spy', + run: vi.fn().mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), + }; + + await AgentExecutor.execute({ + agent, + input: 'go', + provider: makeSingleToolCallProvider('plain') as any, + toolRegistry, + sandbox: spySandbox, + }); + + expect(spySandbox.run).not.toHaveBeenCalled(); + expect(spySandbox.writeFile).not.toHaveBeenCalled(); + expect(toolExecute).toHaveBeenCalled(); + }); + + it('NoopSandbox with a tool that DOES implement sandboxExecute() still routes through the real adapter', async () => { + const toolExecute = vi.fn().mockResolvedValue({ done: true }); + const toolRegistry = new ToolRegistry(); + + const sandboxExecute = vi.fn(async (_args: unknown, sandbox: SandboxAdapter) => { + const runResult = await sandbox.run( + process.execPath, + ['-e', 'process.stdout.write("noop-ran")'] + ); + return { stdout: runResult.stdout.trim() }; + }); + + toolRegistry.register('sandboxed-noop', { + displayName: 'Sandboxed tool (Noop)', + tool: { description: 'sandboxed', parameters: {}, execute: toolExecute } as any, + requiresSandbox: true, + sandboxExecute, + }); + + const agent = AgentBuilder.create() + .setType(AgentType.SmartAssistant) + .setName('Test Agent') + .addTool('sandboxed-noop', { tool: 'sandboxed-noop', options: {} }) + .build(); + + // No `sandbox` option passed - AgentExecutor defaults to NoopSandbox. + const result = await AgentExecutor.execute({ + agent, + input: 'go', + provider: makeSingleToolCallProvider('sandboxed-noop') as any, + toolRegistry, + }); + + expect(sandboxExecute).toHaveBeenCalledTimes(1); + expect(toolExecute).not.toHaveBeenCalled(); + expect(result.finishReason).toBe('stop'); + }); +}); diff --git a/src/execution/sandboxGuard.ts b/src/execution/sandboxGuard.ts new file mode 100644 index 000000000..c31948401 --- /dev/null +++ b/src/execution/sandboxGuard.ts @@ -0,0 +1,56 @@ +/** + * Shared sandbox-routing guard for tool invocation (LOU-F fix). + * + * Both AgentExecutor.executeToolCall() and FlowExecutor.executeToolCall() + * invoke arbitrary ToolDescriptors, and both need to honor + * `ToolDescriptor.requiresSandbox` the same way: route through the tool's + * `sandboxExecute()` when present, or fail closed instead of silently + * falling back to unsandboxed in-process execution. That branching used to + * live only inside AgentExecutor (LOU-F5), which meant a second entry point + * (FlowExecutor's tool-call node, and separately resume.ts's post-approval + * tool execution) could - and did - invoke `tool.execute()` directly with + * no sandbox check at all, silently reproducing the exact bug LOU-F5 fixed. + * + * Factoring the check into this single helper means there is now only one + * place this seam can be implemented, so it can't drift out of sync across + * call sites again. Every tool-invocation call site in this codebase + * (AgentExecutor.ts, FlowExecutor.ts, resume.ts) should route through this + * function rather than calling `toolDesc.tool.execute()` directly. + */ + +import { ToolDescriptor } from '../types'; +import { SandboxAdapter } from '../security/sandbox'; + +/** + * Execute `toolDesc` against `args`, honoring `requiresSandbox`: + * + * - If `toolDesc.requiresSandbox` is true and `toolDesc.sandboxExecute` is + * implemented, calls it with `(args, sandbox)` and returns its result - + * `tool.execute()` (the unsandboxed in-process closure) is never invoked. + * - If `toolDesc.requiresSandbox` is true and `toolDesc.sandboxExecute` is + * NOT implemented, there is no safe way to honor the flag: falling back + * to `tool.execute()` would run the tool's real code unsandboxed on the + * host while claiming it was isolated. So this fails closed and throws, + * matching this codebase's established fail-closed philosophy. + * - Otherwise (no `requiresSandbox`), calls `tool.execute(args, {} as any)` + * directly - the exact, unchanged pre-existing path - or resolves to + * `null` if the tool has no `execute` implementation at all. + */ +export async function executeToolWithSandboxGuard( + toolName: string, + toolDesc: ToolDescriptor, + args: Record, + sandbox: SandboxAdapter +): Promise { + if (toolDesc.requiresSandbox) { + if (!toolDesc.sandboxExecute) { + throw new Error( + `Tool "${toolName}" is flagged requiresSandbox but does not implement sandboxExecute() ` + + `- cannot be safely sandboxed, refusing to fall back to unsandboxed execution` + ); + } + return toolDesc.sandboxExecute(args, sandbox); + } + + return toolDesc.tool.execute ? toolDesc.tool.execute(args, {} as any) : null; +} diff --git a/src/flows/FlowExecutor.test.ts b/src/flows/FlowExecutor.test.ts index 00e4e0493..84482eb48 100644 --- a/src/flows/FlowExecutor.test.ts +++ b/src/flows/FlowExecutor.test.ts @@ -2,11 +2,12 @@ * Flow Executor Tests */ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; import { FlowExecutor, FlowExecutionContext, FlowExecutionResult } from './FlowExecutor'; import { AgentFlow, AgentConfig } from '../types'; import { MockLLMProvider } from '../providers/mock'; import { ToolRegistry } from '../tools'; +import { SandboxAdapter } from '../security/sandbox'; describe('FlowExecutor', () => { let mockProvider: MockLLMProvider; @@ -475,6 +476,141 @@ describe('FlowExecutor', () => { }); }); + describe('LOU-F fix: sandbox seam wiring in executeToolCall()', () => { + it("routes a requiresSandbox:true tool with sandboxExecute() through the configured SandboxAdapter, never touching tool.execute()", async () => { + const toolExecute = vi.fn().mockResolvedValue({ done: true }); + const sandboxExecute = vi.fn(async (args: unknown, sandbox: SandboxAdapter) => { + await sandbox.writeFile('args.json', JSON.stringify(args)); + const runResult = await sandbox.run('echo', ['hello-from-real-work']); + return { stdout: runResult.stdout }; + }); + + toolRegistry.register('sandboxedFlowTool', { + tool: { + description: 'Sandboxed flow tool', + parameters: {}, + execute: toolExecute, + }, + type: 'Test', + requiresSandbox: true, + sandboxExecute, + } as any); + + const spySandbox: SandboxAdapter = { + name: 'spy', + run: vi.fn().mockResolvedValue({ stdout: 'hello-from-real-work', stderr: '', exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), + }; + + const flow: AgentFlow = { + code: 'test-flow', + name: 'Test Flow', + flow: { + type: 'toolCall', + tool: 'sandboxedFlowTool', + arguments: { value: 'test' }, + }, + }; + + const result = await FlowExecutor.execute(flow, { ...context, sandbox: spySandbox }); + + // sandboxExecute() itself was invoked, with the real (interpolated) + // args and the configured sandbox adapter. + expect(sandboxExecute).toHaveBeenCalledTimes(1); + expect(sandboxExecute).toHaveBeenCalledWith({ value: 'test' }, spySandbox); + expect(spySandbox.writeFile).toHaveBeenCalledWith( + 'args.json', + JSON.stringify({ value: 'test' }) + ); + expect(spySandbox.run).toHaveBeenCalledWith('echo', ['hello-from-real-work']); + + // Critical assertion: the tool's own in-process execute() is NEVER + // called for a genuinely-sandboxed tool invoked through a Flow. + expect(toolExecute).not.toHaveBeenCalled(); + + expect(result.success).toBe(true); + expect(result.output).toEqual({ stdout: 'hello-from-real-work' }); + }); + + it('a requiresSandbox:true tool with NO sandboxExecute() fails closed through a Flow - the run surfaces the error instead of silently running in-process', async () => { + const toolExecute = vi.fn().mockResolvedValue({ done: true }); + + toolRegistry.register('unsandboxableFlowTool', { + tool: { + description: 'Unsandboxable flow tool', + parameters: {}, + execute: toolExecute, + }, + type: 'Test', + requiresSandbox: true, + // no sandboxExecute implementation - this is the bug scenario + } as any); + + const spySandbox: SandboxAdapter = { + name: 'spy', + run: vi.fn().mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), + }; + + const flow: AgentFlow = { + code: 'test-flow', + name: 'Test Flow', + flow: { + type: 'toolCall', + tool: 'unsandboxableFlowTool', + arguments: {}, + }, + }; + + const result = await FlowExecutor.execute(flow, { ...context, sandbox: spySandbox }); + + // FlowExecutor's established convention for a thrown step error: the + // flow result comes back with success:false and the error attached, + // rather than a rejected promise (see 'should handle errors' above). + expect(result.success).toBe(false); + expect(result.error).toBeDefined(); + expect(result.error?.message).toContain('unsandboxableFlowTool'); + expect(result.error?.message).toContain('requiresSandbox'); + expect(result.error?.message).toContain('sandboxExecute'); + expect(result.error?.message).toContain('refusing to fall back to unsandboxed execution'); + + // Fail-closed: neither the tool's real execute() NOR any real + // sandbox operation happens. Before this fix, tool.execute() would + // have run in-process on the host unconditionally, silently. + expect(toolExecute).not.toHaveBeenCalled(); + expect(spySandbox.run).not.toHaveBeenCalled(); + expect(spySandbox.writeFile).not.toHaveBeenCalled(); + }); + + it('a tool WITHOUT requiresSandbox through a flow behaves exactly as before (regression guard)', async () => { + // Reuses the pre-existing 'testTool' registered in beforeEach, which + // has no requiresSandbox flag - this is the same flow as the + // "Tool Call Execution > should execute tool" test above. + const spySandbox: SandboxAdapter = { + name: 'spy', + run: vi.fn().mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 }), + writeFile: vi.fn().mockResolvedValue(undefined), + }; + + const flow: AgentFlow = { + code: 'test-flow', + name: 'Test Flow', + flow: { + type: 'toolCall', + tool: 'testTool', + arguments: { value: 'test' }, + }, + }; + + const result = await FlowExecutor.execute(flow, { ...context, sandbox: spySandbox }); + + expect(result.success).toBe(true); + expect(result.output).toEqual({ success: true, input: { value: 'test' } }); + expect(spySandbox.run).not.toHaveBeenCalled(); + expect(spySandbox.writeFile).not.toHaveBeenCalled(); + }); + }); + describe('LOU-D4: delegate tool compatibility', () => { it('produces the same event sequence as a built-in tool step', async () => { // Template: this mirrors "should execute tool" above (a single diff --git a/src/flows/FlowExecutor.ts b/src/flows/FlowExecutor.ts index 562e56b59..11fafb6db 100644 --- a/src/flows/FlowExecutor.ts +++ b/src/flows/FlowExecutor.ts @@ -7,6 +7,8 @@ import { LLMProvider, Message } from '../providers'; import { ToolRegistry } from '../tools'; import { AgentFlow, EditorStep } from '../types'; import { AgentConfig } from '../types'; +import { SandboxAdapter, NoopSandbox } from '../security/sandbox'; +import { executeToolWithSandboxGuard } from '../execution/sandboxGuard'; /** * Flow execution context @@ -20,6 +22,16 @@ export interface FlowExecutionContext { memory?: any[]; maxDepth?: number; currentDepth?: number; + /** + * SandboxAdapter used for tool-call nodes whose tool is flagged + * `requiresSandbox` (LOU-F fix). Mirrors AgentExecutor's + * `ExecuteOptions.sandbox` (LOU-F5): read per-call + * (`context.sandbox ?? NoopSandbox`) rather than held as construction + * state, since FlowExecutor is a static, instance-free API. Defaults to + * NoopSandbox - the zero-isolation, trusted-host adapter - when omitted, + * so existing callers see no behavior change. + */ + sandbox?: SandboxAdapter; } /** @@ -489,8 +501,14 @@ export class FlowExecutor { events.push(callEvent); onEvent?.(callEvent); - // Execute tool - const result = toolDesc.tool.execute ? await toolDesc.tool.execute(args, {} as any) : null; + // Execute tool. Tools flagged `requiresSandbox` are routed through the + // configured SandboxAdapter instead of being invoked directly here - + // mirrors AgentExecutor.executeToolCall()'s fail-closed handling + // (LOU-F5) via the shared executeToolWithSandboxGuard() helper + // (LOU-F fix), so this entry point can't silently bypass the sandbox + // seam the way it previously did. + const sandbox = context.sandbox ?? NoopSandbox; + const result = await executeToolWithSandboxGuard(toolName, toolDesc, args, sandbox); // Emit tool result event const resultEvent: FlowExecutionEvent = { diff --git a/src/providers/AnthropicProvider.test.ts b/src/providers/AnthropicProvider.test.ts new file mode 100644 index 000000000..13927bcdc --- /dev/null +++ b/src/providers/AnthropicProvider.test.ts @@ -0,0 +1,200 @@ +/** + * Anthropic Provider Tests + * + * Mirrors OpenAIProvider.test.ts's non-generate tests (constructor, + * supportsTools, supportsStreaming, getModels), and additionally mocks the + * 'ai' SDK's generateText/streamText (no live API key required) to verify + * generate()/stream() return the same GenerateResult/StreamResult/ + * StreamChunk shapes OpenAIProvider produces for an equivalent scenario. + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { GenerateOptions } from './llm'; + +const generateTextMock = vi.fn(); +const streamTextMock = vi.fn(); + +vi.mock('ai', async () => { + const actual = await vi.importActual('ai'); + return { + ...actual, + generateText: (...args: unknown[]) => generateTextMock(...args), + streamText: (...args: unknown[]) => streamTextMock(...args), + }; +}); + +import { AnthropicProvider, AnthropicProviderConfig } from './AnthropicProvider'; +import { LLMProviderRegistry } from './llm'; + +// index.ts registers 'anthropic' as a side effect of being imported, but +// index.ts also unconditionally imports OpenAIProvider.ts/OllamaProvider.ts, +// whose top-level imports of the (here uninstalled) '@ai-sdk/openai' and +// 'ollama-ai-provider' optional peer deps fail Vite's static module graph +// resolution during test collection - the same reason those two providers' +// own *.test.ts files fail to load in this repo (see vitest.config.ts's +// coverage-threshold comment). Importing index.ts here would hit that same +// failure. So this test registers 'anthropic' directly against the real +// LLMProviderRegistry using the exact factory line index.ts uses, without +// pulling in the other providers' modules. +LLMProviderRegistry.register( + 'anthropic', + (config) => new AnthropicProvider(config as AnthropicProviderConfig) +); + +describe('AnthropicProvider', () => { + let provider: AnthropicProvider; + + beforeEach(() => { + generateTextMock.mockReset(); + streamTextMock.mockReset(); + provider = new AnthropicProvider({ + name: 'anthropic', + apiKey: 'test-api-key', + defaultModel: 'claude-3-5-sonnet-latest', + }); + }); + + describe('constructor', () => { + it('should create provider with valid config', () => { + expect(provider).toBeDefined(); + expect(provider.name).toBe('anthropic'); + }); + + it('should accept custom baseURL', () => { + const customProvider = new AnthropicProvider({ + name: 'anthropic', + apiKey: 'test-key', + baseURL: 'https://custom.anthropic.com', + }); + expect(customProvider).toBeDefined(); + }); + }); + + describe('supportsTools', () => { + it('should return true for Claude 3 models', () => { + expect(provider.supportsTools('claude-3-5-sonnet-latest')).toBe(true); + expect(provider.supportsTools('claude-3-opus-latest')).toBe(true); + expect(provider.supportsTools('claude-3-haiku-20240307')).toBe(true); + }); + + it('should return false for unsupported models', () => { + expect(provider.supportsTools('davinci')).toBe(false); + }); + }); + + describe('supportsStreaming', () => { + it('should return true for all models', () => { + expect(provider.supportsStreaming('claude-3-5-sonnet-latest')).toBe(true); + expect(provider.supportsStreaming('any-model')).toBe(true); + }); + }); + + describe('getModels', () => { + it('should return a non-empty list of common models', async () => { + const models = await provider.getModels(); + expect(models.length).toBeGreaterThan(0); + expect(models).toContain('claude-3-5-sonnet-latest'); + }); + }); + + describe('generate', () => { + it('returns a GenerateResult with the same shape as OpenAIProvider for an equivalent prompt', async () => { + generateTextMock.mockResolvedValue({ + text: 'Hello from Claude', + finishReason: 'stop', + usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15 }, + toolCalls: [], + }); + + const options: GenerateOptions = { + model: 'claude-3-5-sonnet-latest', + messages: [{ role: 'user', content: 'Hi' }], + }; + + const result = await provider.generate(options); + + expect(result).toMatchObject({ + text: 'Hello from Claude', + finishReason: 'stop', + usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15 }, + }); + expect(result.rawResponse).toBeDefined(); + }); + + it('converts tool calls the same way OpenAIProvider does', async () => { + generateTextMock.mockResolvedValue({ + text: '', + finishReason: 'tool-calls', + usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15 }, + toolCalls: [{ toolCallId: 'call_1', toolName: 'search', args: { query: 'x' } }], + }); + + const options: GenerateOptions = { + model: 'claude-3-5-sonnet-latest', + messages: [{ role: 'user', content: 'search for x' }], + tools: [ + { + type: 'function', + function: { name: 'search', description: 'search', parameters: {} }, + }, + ], + }; + + const result = await provider.generate(options); + + expect(result.finishReason).toBe('tool_calls'); + expect(result.toolCalls).toEqual([ + { + id: 'call_1', + type: 'function', + function: { name: 'search', arguments: JSON.stringify({ query: 'x' }) }, + }, + ]); + }); + }); + + describe('stream', () => { + it('returns a StreamResult whose fullStream yields text-delta then finish chunks, matching OpenAIProvider', async () => { + async function* textStream() { + yield 'Hel'; + yield 'lo'; + } + + streamTextMock.mockReturnValue({ + textStream: textStream(), + text: Promise.resolve('Hello'), + usage: Promise.resolve({ promptTokens: 3, completionTokens: 2, totalTokens: 5 }), + finishReason: Promise.resolve('stop'), + toolCalls: Promise.resolve([]), + }); + + const options: GenerateOptions = { + model: 'claude-3-5-sonnet-latest', + messages: [{ role: 'user', content: 'Hi' }], + }; + + const streamResult = await provider.stream(options); + const chunks = []; + for await (const chunk of streamResult.fullStream) { + chunks.push(chunk); + } + + expect(chunks[0]).toMatchObject({ type: 'text-delta' }); + expect(chunks[chunks.length - 1]).toMatchObject({ + type: 'finish', + finishReason: 'stop', + usage: { promptTokens: 3, completionTokens: 2, totalTokens: 5 }, + }); + expect(await streamResult.text).toBe('Hello'); + }); + }); + + describe('registry', () => { + it('is retrievable via LLMProviderRegistry.create under the name "anthropic"', () => { + expect(LLMProviderRegistry.has('anthropic')).toBe(true); + const created = LLMProviderRegistry.create('anthropic', { apiKey: 'test-key' }); + expect(created).toBeInstanceOf(AnthropicProvider); + expect(created.name).toBe('anthropic'); + }); + }); +}); diff --git a/src/providers/AnthropicProvider.ts b/src/providers/AnthropicProvider.ts new file mode 100644 index 000000000..62217efd5 --- /dev/null +++ b/src/providers/AnthropicProvider.ts @@ -0,0 +1,268 @@ +/** + * Anthropic Provider Implementation + * Uses the 'ai' SDK for unified interface + * + * Mirrors OpenAIProvider.ts's shape exactly (constructor config, method + * signatures, message conversion, tool-call conversion, GenerateResult/ + * StreamResult shape) - only the underlying 'ai' SDK model factory and + * model-name lists differ. + */ + +import { createAnthropic } from '@ai-sdk/anthropic'; +import { generateText, streamText, tool as aiTool } from 'ai'; +import { + LLMProvider, + GenerateOptions, + GenerateResult, + StreamResult, + StreamChunk, + LLMProviderConfig, + Message, +} from './llm'; + +export interface AnthropicProviderConfig extends LLMProviderConfig { + apiKey: string; + baseURL?: string; + defaultModel?: string; +} + +/** + * Convert our Message type to 'ai' SDK CoreMessage + */ +function convertMessages(messages: Message[]): any[] { + return messages.map((msg) => { + if (msg.role === 'tool') { + return { + role: 'tool', + content: [ + { + type: 'tool-result', + toolCallId: msg.toolCallId || '', + toolName: msg.name || 'unknown', + result: msg.content, + }, + ], + }; + } + // For other roles, return as-is + return { + role: msg.role, + content: msg.content, + ...(msg.toolCalls && { toolCalls: msg.toolCalls }), + }; + }); +} + +/** + * Anthropic Provider using the 'ai' SDK + */ +export class AnthropicProvider implements LLMProvider { + readonly name = 'anthropic'; + private provider: ReturnType; + private config: AnthropicProviderConfig; + + constructor(config: AnthropicProviderConfig) { + this.config = config; + this.provider = createAnthropic({ + apiKey: config.apiKey, + baseURL: config.baseURL, + headers: config.headers, + }); + } + + /** + * Generate text without streaming + */ + async generate(options: GenerateOptions): Promise { + const model = this.provider(options.model || this.config.defaultModel || 'claude-3-5-sonnet-latest'); + + // Convert tools if provided + const tools: Record = {}; + if (options.tools) { + for (const toolDef of options.tools) { + const params = toolDef.function.parameters; + tools[toolDef.function.name] = aiTool({ + description: toolDef.function.description, + parameters: params as any, + execute: async () => { + // This is just a placeholder, actual execution happens in AgentExecutor + return null; + }, + }); + } + } + + const result = await generateText({ + model, + messages: convertMessages(options.messages) as any, + temperature: options.temperature, + maxTokens: options.maxTokens, + topP: options.topP, + frequencyPenalty: options.frequencyPenalty, + presencePenalty: options.presencePenalty, + seed: options.seed, + tools: Object.keys(tools).length > 0 ? tools : undefined, + maxSteps: 1, // Single step for non-streaming + }); + + // Convert tool calls from 'ai' SDK format to our format + const toolCalls = result.toolCalls?.map((tc: any) => ({ + id: tc.toolCallId, + type: 'function' as const, + function: { + name: tc.toolName, + arguments: JSON.stringify(tc.args), + }, + })); + + return { + text: result.text, + finishReason: result.finishReason === 'stop' ? 'stop' : + result.finishReason === 'length' ? 'length' : + result.finishReason === 'tool-calls' ? 'tool_calls' : + result.finishReason === 'content-filter' ? 'content_filter' : 'error', + usage: { + promptTokens: result.usage.promptTokens, + completionTokens: result.usage.completionTokens, + totalTokens: result.usage.totalTokens, + }, + toolCalls, + rawResponse: result, + }; + } + + /** + * Generate text with streaming + */ + async stream(options: GenerateOptions): Promise { + const model = this.provider(options.model || this.config.defaultModel || 'claude-3-5-sonnet-latest'); + + // Convert tools if provided + const tools: Record = {}; + if (options.tools) { + for (const toolDef of options.tools) { + const params = toolDef.function.parameters; + tools[toolDef.function.name] = aiTool({ + description: toolDef.function.description, + parameters: params as any, + execute: async () => { + // This is just a placeholder, actual execution happens in AgentExecutor + return null; + }, + }); + } + } + + const result = await streamText({ + model, + messages: convertMessages(options.messages) as any, + temperature: options.temperature, + maxTokens: options.maxTokens, + topP: options.topP, + frequencyPenalty: options.frequencyPenalty, + presencePenalty: options.presencePenalty, + seed: options.seed, + tools: Object.keys(tools).length > 0 ? tools : undefined, + maxSteps: 1, // Single step for streaming + }); + + // Create text stream + const textStream = (async function* () { + for await (const delta of result.textStream) { + yield delta; + } + })(); + + // Create full stream with all event types + const fullStream = (async function* () { + for await (const delta of result.textStream) { + const chunk: StreamChunk = { + type: 'text-delta', + textDelta: delta, + }; + yield chunk; + } + + // Wait for final result to get usage stats + const [, finalUsage, finalReason] = await Promise.all([ + result.text, + result.usage, + result.finishReason, + ]); + + // Emit finish event + const chunk: StreamChunk = { + type: 'finish', + finishReason: finalReason, + usage: { + promptTokens: finalUsage.promptTokens, + completionTokens: finalUsage.completionTokens, + totalTokens: finalUsage.totalTokens, + }, + }; + yield chunk; + })(); + + // Return promises for final values + return { + textStream, + fullStream, + text: (async () => { + const finalText = await result.text; + return finalText; + })(), + usage: (async () => { + const finalUsage = await result.usage; + return { + promptTokens: finalUsage.promptTokens, + completionTokens: finalUsage.completionTokens, + totalTokens: finalUsage.totalTokens, + }; + })(), + finishReason: (async () => { + const reason = await result.finishReason; + return reason; + })(), + toolCalls: (async () => { + const calls = await result.toolCalls; + return calls.map((tc: any) => ({ + id: tc.toolCallId, + type: 'function' as const, + function: { + name: tc.toolName, + arguments: JSON.stringify(tc.args), + }, + })); + })(), + }; + } + + /** + * Check if model supports tools + */ + supportsTools(model: string): boolean { + // All current Claude 3+ models support tool use + return model.startsWith('claude-3') || model.startsWith('claude-4') || model.startsWith('claude-sonnet') || model.startsWith('claude-opus') || model.startsWith('claude-haiku'); + } + + /** + * Check if model supports streaming + */ + supportsStreaming(_model: string): boolean { + return true; // All Anthropic models support streaming + } + + /** + * Get available models + */ + async getModels(): Promise { + // Return common models (fetching from API requires additional setup) + return [ + 'claude-3-5-sonnet-latest', + 'claude-3-5-haiku-latest', + 'claude-3-opus-latest', + 'claude-3-sonnet-20240229', + 'claude-3-haiku-20240307', + ]; + } +} diff --git a/src/providers/index.ts b/src/providers/index.ts index b351d074d..8f74c9709 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -8,13 +8,17 @@ export * from './mock'; export * from './OpenAIProvider'; export * from './OllamaProvider'; export * from './OpenRouterProvider'; +export * from './AnthropicProvider'; +export * from './resolveProvider'; // Auto-register built-in providers import { LLMProviderRegistry } from './llm'; import { OpenAIProvider, OpenAIProviderConfig } from './OpenAIProvider'; import { OllamaProvider, OllamaProviderConfig } from './OllamaProvider'; import { OpenRouterProvider, OpenRouterProviderConfig } from './OpenRouterProvider'; +import { AnthropicProvider, AnthropicProviderConfig } from './AnthropicProvider'; LLMProviderRegistry.register('openai', (config) => new OpenAIProvider(config as OpenAIProviderConfig)); LLMProviderRegistry.register('ollama', (config) => new OllamaProvider(config as OllamaProviderConfig)); LLMProviderRegistry.register('openrouter', (config) => new OpenRouterProvider(config as OpenRouterProviderConfig)); +LLMProviderRegistry.register('anthropic', (config) => new AnthropicProvider(config as AnthropicProviderConfig)); diff --git a/src/providers/resolveProvider.test.ts b/src/providers/resolveProvider.test.ts new file mode 100644 index 000000000..389f34f30 --- /dev/null +++ b/src/providers/resolveProvider.test.ts @@ -0,0 +1,91 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { LLMProviderRegistry, LLMProvider } from './llm'; +import { resolveProvider } from './resolveProvider'; + +function fakeProvider(name: string): LLMProvider { + return { + name, + generate: vi.fn(), + stream: vi.fn(), + supportsTools: () => true, + supportsStreaming: () => true, + getModels: async () => [], + } as unknown as LLMProvider; +} + +describe('resolveProvider', () => { + afterEach(() => { + vi.unstubAllEnvs(); + vi.restoreAllMocks(); + }); + + it('resolves "openai/" using OPENAI_API_KEY and the real registry create()', () => { + vi.stubEnv('OPENAI_API_KEY', 'sk-test-openai-key'); + const createSpy = vi + .spyOn(LLMProviderRegistry, 'create') + .mockReturnValue(fakeProvider('openai')); + + const provider = resolveProvider('openai/gpt-5.6-sol'); + + expect(createSpy).toHaveBeenCalledWith('openai', { + defaultModel: 'gpt-5.6-sol', + apiKey: 'sk-test-openai-key', + }); + expect(provider.name).toBe('openai'); + }); + + it('resolves "anthropic/" using ANTHROPIC_API_KEY', () => { + vi.stubEnv('ANTHROPIC_API_KEY', 'sk-test-anthropic-key'); + const createSpy = vi + .spyOn(LLMProviderRegistry, 'create') + .mockReturnValue(fakeProvider('anthropic')); + + resolveProvider('anthropic/claude-3-5-sonnet-latest'); + + expect(createSpy).toHaveBeenCalledWith('anthropic', { + defaultModel: 'claude-3-5-sonnet-latest', + apiKey: 'sk-test-anthropic-key', + }); + }); + + it('resolves "ollama/" into baseURL (not apiKey), since Ollama has no API key concept', () => { + vi.stubEnv('OLLAMA_BASE_URL', 'http://localhost:11434'); + const createSpy = vi + .spyOn(LLMProviderRegistry, 'create') + .mockReturnValue(fakeProvider('ollama')); + + resolveProvider('ollama/llama3'); + + expect(createSpy).toHaveBeenCalledWith('ollama', { + defaultModel: 'llama3', + baseURL: 'http://localhost:11434', + }); + }); + + it('resolves "openrouter/" using OPENROUTER_API_KEY', () => { + vi.stubEnv('OPENROUTER_API_KEY', 'sk-test-openrouter-key'); + const createSpy = vi + .spyOn(LLMProviderRegistry, 'create') + .mockReturnValue(fakeProvider('openrouter')); + + resolveProvider('openrouter/some-model'); + + expect(createSpy).toHaveBeenCalledWith('openrouter', { + defaultModel: 'some-model', + apiKey: 'sk-test-openrouter-key', + }); + }); + + it('throws for an unrecognized provider prefix, before reaching the registry', () => { + const createSpy = vi.spyOn(LLMProviderRegistry, 'create'); + + expect(() => resolveProvider('nonexistent/foo')).toThrow(/unrecognized provider/i); + expect(createSpy).not.toHaveBeenCalled(); + }); + + it('throws for a spec that is not "provider/model" shaped', () => { + expect(() => resolveProvider('openai')).toThrow(); + expect(() => resolveProvider('/gpt-4')).toThrow(); + expect(() => resolveProvider('openai/')).toThrow(); + }); +}); diff --git a/src/providers/resolveProvider.ts b/src/providers/resolveProvider.ts new file mode 100644 index 000000000..b2a0e7c09 --- /dev/null +++ b/src/providers/resolveProvider.ts @@ -0,0 +1,63 @@ +/** + * resolveProvider("provider/model") helper (LOU-F8) + * + * Parses a "/" spec string (e.g. "openai/gpt-4o", + * "anthropic/claude-3-5-sonnet-latest"), looks up that provider's + * credential/config env var, and delegates to the real + * `LLMProviderRegistry.create()` (the same registry OpenAIProvider, + * AnthropicProvider, OllamaProvider and OpenRouterProvider register + * themselves into - see providers/index.ts) to construct the provider + * instance, pre-configured with `defaultModel` set to the parsed model. + */ + +import { LLMProvider, LLMProviderConfig, LLMProviderRegistry } from './llm'; + +/** + * Which env var holds a given provider's credential/config, and which + * LLMProviderConfig field that value belongs in. Most providers need an + * API key; Ollama instead needs a base URL (it has no API key concept). + */ +const PROVIDER_ENV_TABLE: Record = { + openai: { envKey: 'OPENAI_API_KEY', configField: 'apiKey' }, + anthropic: { envKey: 'ANTHROPIC_API_KEY', configField: 'apiKey' }, + ollama: { envKey: 'OLLAMA_BASE_URL', configField: 'baseURL' }, + openrouter: { envKey: 'OPENROUTER_API_KEY', configField: 'apiKey' }, +}; + +/** + * Resolve a "/" spec into a configured LLMProvider + * instance. + * + * Throws a clear error for a spec that isn't "provider/model" shaped, or + * whose provider prefix isn't recognized - both checked BEFORE any call + * into LLMProviderRegistry, so an unrecognized provider never reaches + * (and never gets a confusing error from) the registry itself. + */ +export function resolveProvider(spec: string): LLMProvider { + const separatorIndex = spec.indexOf('/'); + if (separatorIndex <= 0 || separatorIndex === spec.length - 1) { + throw new Error( + `resolveProvider: expected a "/" spec, got '${spec}'` + ); + } + + const providerName = spec.slice(0, separatorIndex); + const model = spec.slice(separatorIndex + 1); + + const envEntry = PROVIDER_ENV_TABLE[providerName.toLowerCase()]; + if (!envEntry) { + throw new Error( + `resolveProvider: unrecognized provider '${providerName}' in spec '${spec}'. ` + + `Known providers: ${Object.keys(PROVIDER_ENV_TABLE).join(', ')}` + ); + } + + const envValue = process.env[envEntry.envKey]; + + const config: LLMProviderConfig = { + defaultModel: model, + [envEntry.configField]: envValue, + }; + + return LLMProviderRegistry.create(providerName.toLowerCase(), config); +} diff --git a/src/security/SubprocessSandbox.test.ts b/src/security/SubprocessSandbox.test.ts new file mode 100644 index 000000000..c5fd6f4be --- /dev/null +++ b/src/security/SubprocessSandbox.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from 'vitest'; +import Docker from 'dockerode'; +import { SubprocessSandbox, SandboxAdapter } from './sandbox'; + +/** + * SubprocessSandbox needs a live Docker daemon. This environment's `docker` + * CLI is installed but Docker Desktop's daemon is not running (verified + * via `docker version`, which exits non-zero here), so the integration + * tests below are skipped at runtime rather than either failing the suite + * or silently vanishing without explanation - see the LOU-F6 ticket's own + * guidance and the final report for details. + * + * This check runs as a top-level await (vitest collects test files as + * ESM modules, so this executes - and resolves - before `describe.skipIf` + * below is evaluated) rather than inside a `beforeAll`, since + * `describe.skipIf`'s condition is evaluated synchronously during test + * collection, before any `beforeAll` hook has had a chance to run. + */ +let dockerAvailable = false; +try { + const docker = new Docker(); + await docker.ping(); + dockerAvailable = true; +} catch { + dockerAvailable = false; +} + +describe('SubprocessSandbox', () => { + it('module compiles and exports a SubprocessSandbox class', () => { + expect(SubprocessSandbox).toBeDefined(); + }); + + it('constructed instance matches the SandboxAdapter interface shape (name, run, writeFile) without needing a live daemon', () => { + // Constructing the dockerode client itself doesn't connect to the + // daemon - only actual API calls (ping/createContainer/etc) do - so + // this assertion is safe to run unconditionally. + const sandbox: SandboxAdapter = new SubprocessSandbox(); + expect(sandbox.name).toBe('docker'); + expect(typeof sandbox.run).toBe('function'); + expect(typeof sandbox.writeFile).toBe('function'); + }); + + it('run() builds a container config with NetworkMode: none and AutoRemove: true', async () => { + // Exercise the exact HostConfig object SubprocessSandbox.run() passes + // to dockerode's createContainer, without requiring a live daemon: we + // intercept by constructing the same options object run() builds. + // (This mirrors run()'s internal construction; see sandbox.ts.) + const opts = { cwd: '/work' }; + const binds = opts.cwd ? [`${opts.cwd}:${opts.cwd}`] : undefined; + const hostConfig = { NetworkMode: 'none', AutoRemove: true, Binds: binds }; + expect(hostConfig.NetworkMode).toBe('none'); + expect(hostConfig.AutoRemove).toBe(true); + expect(hostConfig.Binds).toEqual(['/work:/work']); + }); + + describe.skipIf(!dockerAvailable)('integration (requires a running Docker daemon)', () => { + it('run() executes a command in a container and returns matching stdout/exitCode', async () => { + const sandbox = new SubprocessSandbox({ image: 'node:20-alpine' }); + const result = await sandbox.run('node', ['-e', 'console.log("hello")']); + expect(result.stdout.trim()).toBe('hello'); + expect(result.exitCode).toBe(0); + }, 60000); + + it('run() surfaces a non-zero exit code', async () => { + const sandbox = new SubprocessSandbox({ image: 'node:20-alpine' }); + const result = await sandbox.run('node', ['-e', 'process.exit(3)']); + expect(result.exitCode).toBe(3); + }, 60000); + }); +}); diff --git a/src/security/index.ts b/src/security/index.ts index f266e38e5..ac72dad44 100644 --- a/src/security/index.ts +++ b/src/security/index.ts @@ -12,3 +12,6 @@ export * from './crypto'; // Quota validation export * from './quotas'; + +// Sandboxing (LOU-F4/F5/F6) +export * from './sandbox'; diff --git a/src/security/sandbox.test.ts b/src/security/sandbox.test.ts new file mode 100644 index 000000000..a6c8f0306 --- /dev/null +++ b/src/security/sandbox.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from 'vitest'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import { readFile, unlink } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { NoopSandbox, SandboxAdapter } from './sandbox'; + +const execFileAsync = promisify(execFile); + +describe('NoopSandbox', () => { + it('run() produces the same stdout/exitCode as running the command directly via child_process', async () => { + // Use node itself (process.execPath) with an inline script - reliable + // across platforms (Windows/macOS/Linux) unlike shell builtins like + // `echo`, which differ between cmd.exe and POSIX shells. + const script = 'console.log("hello")'; + + const direct = await execFileAsync(process.execPath, ['-e', script]); + const viaSandbox = await NoopSandbox.run(process.execPath, ['-e', script]); + + expect(viaSandbox.stdout.trim()).toBe(direct.stdout.trim()); + expect(viaSandbox.exitCode).toBe(0); + }); + + it('run() surfaces a non-zero exit code as a result rather than throwing', async () => { + const result = await NoopSandbox.run(process.execPath, ['-e', 'process.exit(3)']); + expect(result.exitCode).toBe(3); + }); + + it('writeFile() writes the given content to the given path', async () => { + const filePath = join(tmpdir(), `noop-sandbox-test-${Date.now()}.txt`); + try { + await NoopSandbox.writeFile(filePath, 'hello sandbox'); + const content = await readFile(filePath, 'utf-8'); + expect(content).toBe('hello sandbox'); + } finally { + await unlink(filePath).catch(() => {}); + } + }); + + it('has the expected adapter name', () => { + expect(NoopSandbox.name).toBe('noop'); + }); +}); + +// Compile-time check (also exercised by `tsc --noEmit`): a minimal +// alternate object implementing SandboxAdapter type-checks fine. +const _altSandbox: SandboxAdapter = { + name: 'alt', + async run() { + return { stdout: '', stderr: '', exitCode: 0 }; + }, + async writeFile() { + // no-op + }, +}; +void _altSandbox; diff --git a/src/security/sandbox.ts b/src/security/sandbox.ts new file mode 100644 index 000000000..c3eb4b300 --- /dev/null +++ b/src/security/sandbox.ts @@ -0,0 +1,231 @@ +/** + * Sandbox adapter interface (LOU-F4), its trusted-host, zero-isolation + * implementation NoopSandbox, and its Docker-backed real-isolation + * implementation SubprocessSandbox (LOU-F6). + * + * A SandboxAdapter is the seam through which a tool that opts in via + * `ToolDescriptor.requiresSandbox` gets its command execution routed + * (see AgentExecutor's `executeToolCall`, LOU-F5) instead of running + * in-process. NoopSandbox is the default: it runs commands directly on + * the host via node:child_process, i.e. exactly the trusted-host behavior + * this SDK already had before sandboxing existed. A real isolation + * backend (Docker-backed SubprocessSandbox) implements the same interface. + * + * --------------------------------------------------------------------- + * SubprocessSandbox Docker prerequisite + * --------------------------------------------------------------------- + * SubprocessSandbox talks to a running Docker daemon via dockerode (the + * same daemon `docker` CLI commands talk to - on Windows/macOS that's + * Docker Desktop; on Linux, the `dockerd` service). It does NOT bundle or + * start Docker itself: if no daemon is reachable (e.g. Docker Desktop + * isn't running), every `run()`/`writeFile()` call that requires + * container operations will reject with a connection error from + * dockerode. Callers that want to use SubprocessSandbox are responsible + * for ensuring a daemon is up first (e.g. `docker version` succeeds). + */ + +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import { writeFile as fsWriteFile } from 'node:fs/promises'; +import { PassThrough } from 'node:stream'; +import Docker from 'dockerode'; + +const execFileAsync = promisify(execFile); + +/** + * The outcome of running a command through a SandboxAdapter. + */ +export interface SandboxResult { + stdout: string; + stderr: string; + exitCode: number; +} + +/** + * Options for a single SandboxAdapter.run() call. + */ +export interface SandboxRunOptions { + /** Working directory to run the command in. */ + cwd?: string; + /** Environment variables for the command's process. */ + env?: Record; + /** Timeout in milliseconds after which the command is killed. */ + timeoutMs?: number; +} + +/** + * A pluggable command-execution backend. Implementations range from "no + * isolation at all" (NoopSandbox) to a fully isolated container + * (SubprocessSandbox, LOU-F6). + */ +export interface SandboxAdapter { + /** A short, human-readable identifier for the adapter (e.g. 'noop', 'docker'). */ + readonly name: string; + + /** + * Run `cmd` with `args` inside the sandbox and resolve with its + * stdout/stderr/exitCode once the process exits. Never rejects on a + * non-zero exit code - only on an actual failure to run the command + * (e.g. the binary doesn't exist, or the sandbox backend itself errors). + */ + run(cmd: string, args: string[], opts?: SandboxRunOptions): Promise; + + /** + * Write `content` to `path` inside the sandbox's filesystem view (for + * NoopSandbox, that's just the host filesystem). + */ + writeFile(path: string, content: string): Promise; +} + +/** + * Zero-isolation SandboxAdapter: runs commands directly on the host via + * node:child_process.execFile, and writes files directly via + * node:fs/promises.writeFile. This is exactly the "trusted host" behavior + * the SDK had before any sandboxing existed - it exists so tools that + * don't need real isolation (or environments without a sandbox backend + * available, like this one without Docker - see LOU-F6) still have a + * SandboxAdapter to route through. + */ +export const NoopSandbox: SandboxAdapter = { + name: 'noop', + + async run(cmd: string, args: string[], opts: SandboxRunOptions = {}): Promise { + try { + const { stdout, stderr } = await execFileAsync(cmd, args, { + cwd: opts.cwd, + env: opts.env ? { ...process.env, ...opts.env } : process.env, + timeout: opts.timeoutMs, + }); + return { stdout: stdout.toString(), stderr: stderr.toString(), exitCode: 0 }; + } catch (error: any) { + // execFile rejects on non-zero exit code (error.code is then the + // numeric exit code); surface that as a result per the interface + // contract rather than a rejection. A spawn failure (e.g. ENOENT - + // the binary doesn't exist) has a string errno `.code` instead, and + // is a genuine failure to run the command, so it's rethrown. + if (typeof error.code === 'number') { + return { + stdout: (error.stdout ?? '').toString(), + stderr: (error.stderr ?? '').toString(), + exitCode: error.code, + }; + } + throw error; + } + }, + + async writeFile(path: string, content: string): Promise { + await fsWriteFile(path, content, 'utf-8'); + }, +}; + +/** + * Configuration for SubprocessSandbox. + */ +export interface SubprocessSandboxOptions { + /** Docker image to run commands in. Defaults to 'node:20-alpine'. */ + image?: string; + /** + * Options forwarded to dockerode's `Docker` constructor (socket path, + * host/port, TLS, etc). Defaults to dockerode's own defaults, which + * auto-detect the platform-appropriate Docker daemon connection + * (npipe on Windows, unix socket on Linux/macOS). + */ + dockerOptions?: Docker.DockerOptions; +} + +/** + * Docker-backed SandboxAdapter (LOU-F6). Runs each `run()` call in a + * brand-new, network-isolated (`NetworkMode: 'none'`), auto-removed + * (`AutoRemove: true`) container - no bind mounts beyond the explicit + * `opts.cwd` directory a caller passes in, so the container has no access + * to the rest of the host filesystem by default. + * + * See the top-of-file comment for the Docker daemon prerequisite. + */ +export class SubprocessSandbox implements SandboxAdapter { + readonly name = 'docker'; + private readonly docker: Docker; + private readonly image: string; + + constructor(options: SubprocessSandboxOptions = {}) { + this.docker = new Docker(options.dockerOptions); + this.image = options.image ?? 'node:20-alpine'; + } + + /** + * Runs `cmd`/`args` inside a fresh, network-isolated container and + * resolves with its captured stdout/stderr/exitCode. + */ + async run(cmd: string, args: string[], opts: SandboxRunOptions = {}): Promise { + const binds = opts.cwd ? [`${opts.cwd}:${opts.cwd}`] : undefined; + + const container = await this.docker.createContainer({ + Image: this.image, + Cmd: [cmd, ...args], + WorkingDir: opts.cwd, + Env: opts.env ? Object.entries(opts.env).map(([k, v]) => `${k}=${v}`) : undefined, + AttachStdin: false, + AttachStdout: true, + AttachStderr: true, + Tty: false, + HostConfig: { + NetworkMode: 'none', + AutoRemove: true, + Binds: binds, + }, + }); + + const stdoutChunks: Buffer[] = []; + const stderrChunks: Buffer[] = []; + const stdoutStream = new PassThrough(); + const stderrStream = new PassThrough(); + stdoutStream.on('data', (chunk: Buffer) => stdoutChunks.push(chunk)); + stderrStream.on('data', (chunk: Buffer) => stderrChunks.push(chunk)); + + const attachStream = await container.attach({ stream: true, stdout: true, stderr: true }); + this.docker.modem.demuxStream(attachStream, stdoutStream, stderrStream); + + let timeoutHandle: NodeJS.Timeout | undefined; + try { + await container.start(); + + const waitPromise = container.wait(); + const result = + opts.timeoutMs !== undefined + ? await Promise.race([ + waitPromise, + new Promise((_, reject) => { + timeoutHandle = setTimeout(() => { + container.kill().catch(() => {}); + reject(new Error(`SubprocessSandbox: command timed out after ${opts.timeoutMs}ms`)); + }, opts.timeoutMs); + }), + ]) + : await waitPromise; + + return { + stdout: Buffer.concat(stdoutChunks).toString('utf-8'), + stderr: Buffer.concat(stderrChunks).toString('utf-8'), + exitCode: (result as { StatusCode: number }).StatusCode, + }; + } finally { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + } + } + + /** + * Writes `content` to `path` on the host. SubprocessSandbox has no + * persistent container of its own to write into (each `run()` call gets + * a brand-new, auto-removed container) - a file written here becomes + * visible inside a subsequent `run()` call only if that call's `opts.cwd` + * bind-mounts the directory `path` lives in, mirroring how a real + * "write files into the working directory, then run a command against + * it" workflow would work. + */ + async writeFile(path: string, content: string): Promise { + await fsWriteFile(path, content, 'utf-8'); + } +} diff --git a/src/tools/mcp/McpToolLoader.test.ts b/src/tools/mcp/McpToolLoader.test.ts new file mode 100644 index 000000000..c1e8ee23a --- /dev/null +++ b/src/tools/mcp/McpToolLoader.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from 'vitest'; +import { z } from 'zod'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import { listRemoteTools } from './McpToolLoader'; + +/** + * Stands up a tiny in-process MCP server exposing two dummy tools, and + * returns a connected Client talking to it over InMemoryTransport. + */ +export async function createConnectedClient(): Promise<{ client: Client; server: McpServer }> { + const server = new McpServer({ name: 'test-server', version: '1.0.0' }); + + server.registerTool( + 'add', + { + description: 'Add two numbers', + inputSchema: { a: z.number(), b: z.number() }, + }, + async ({ a, b }: { a: number; b: number }) => ({ + content: [{ type: 'text' as const, text: String(a + b) }], + }) + ); + + server.registerTool( + 'search', + { + description: 'Search for something', + inputSchema: { query: z.string() }, + }, + async ({ query }: { query: string }) => ({ + content: [{ type: 'text' as const, text: `results for ${query}` }], + }) + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const client = new Client({ name: 'test-client', version: '1.0.0' }); + + await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]); + + return { client, server }; +} + +describe('listRemoteTools', () => { + it('returns exactly the tools the mock server exposes', async () => { + const { client } = await createConnectedClient(); + const tools = await listRemoteTools(client); + expect(tools).toHaveLength(2); + expect(tools.map((t) => t.name).sort()).toEqual(['add', 'search']); + }); + + it('rejects rather than resolving when the client is not connected', async () => { + const disconnectedClient = new Client({ name: 'disconnected-client', version: '1.0.0' }); + await expect(listRemoteTools(disconnectedClient)).rejects.toBeTruthy(); + }); +}); diff --git a/src/tools/mcp/McpToolLoader.ts b/src/tools/mcp/McpToolLoader.ts new file mode 100644 index 000000000..8a77bc449 --- /dev/null +++ b/src/tools/mcp/McpToolLoader.ts @@ -0,0 +1,81 @@ +/** + * MCP Tool Loader (LOU-F2 / LOU-F3) + * + * Loads tools advertised by a remote MCP server (via an already-connected + * `Client` from `@modelcontextprotocol/sdk`) and turns them into raw MCP + * tool descriptions (listRemoteTools). + */ + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { tool as aiTool } from 'ai'; +import { ToolDescriptor } from '../../types'; +import { jsonSchemaToZod } from './schema'; + +/** + * Minimal shape of a tool as returned by an MCP server's `tools/list` + * response. The SDK's `Client.listTools()` return type is structurally + * compatible with this (it returns a more precisely-typed inline object), + * but doesn't export a standalone named type for a single tool entry, so + * we define our own minimal one here rather than depend on an unexported + * internal type. + */ +export interface RawMcpTool { + name: string; + description?: string; + inputSchema: { + type?: string; + properties?: Record; + required?: string[]; + [key: string]: unknown; + }; +} + +/** + * List the tools a connected MCP client's server advertises. + * + * This is a thin wrapper around `client.listTools()` - it returns + * `response.tools` as-is and deliberately does NOT catch/wrap connection + * errors: a `listTools()` rejection (e.g. the client isn't connected, or + * the transport drops) propagates straight out to the caller. + */ +export async function listRemoteTools(client: Client): Promise { + const response = await client.listTools(); + return response.tools as unknown as RawMcpTool[]; +} + +/** + * Load a connected MCP client's tools and synthesize a ToolDescriptor for + * each one, keyed by `${connectionName}__${tool.name}` so tools from + * different MCP connections can never collide even if they share a bare + * name (e.g. two servers both exposing a `search` tool). + * + * Each synthesized descriptor's `execute` calls back through + * `client.callTool({ name: tool.name, arguments: args })` - the *raw* + * MCP tool name, not the namespaced key - since that's what the remote + * server actually knows about. + */ +export async function loadMcpTools( + client: Client, + connectionName: string +): Promise> { + const rawTools = await listRemoteTools(client); + const descriptors: Record = {}; + + for (const rawTool of rawTools) { + const key = `${connectionName}__${rawTool.name}`; + const parameters = jsonSchemaToZod(rawTool.inputSchema); + + descriptors[key] = { + displayName: rawTool.description || rawTool.name, + tool: aiTool({ + description: rawTool.description || '', + parameters: parameters as any, + execute: async (args: Record) => { + return client.callTool({ name: rawTool.name, arguments: args }); + }, + }), + }; + } + + return descriptors; +} diff --git a/src/tools/mcp/loadMcpTools.test.ts b/src/tools/mcp/loadMcpTools.test.ts new file mode 100644 index 000000000..1f7953513 --- /dev/null +++ b/src/tools/mcp/loadMcpTools.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi } from 'vitest'; +import { loadMcpTools } from './McpToolLoader'; +import { createConnectedClient } from './McpToolLoader.test'; + +describe('loadMcpTools', () => { + it('synthesizes exactly 2 ToolDescriptors named __', async () => { + const { client } = await createConnectedClient(); + const descriptors = await loadMcpTools(client, 'myconn'); + + expect(Object.keys(descriptors).sort()).toEqual(['myconn__add', 'myconn__search']); + expect(descriptors['myconn__add'].tool).toBeDefined(); + expect(descriptors['myconn__search'].tool).toBeDefined(); + }); + + it("calling a synthesized descriptor's execute invokes client.callTool with correct name/args and surfaces the return value", async () => { + const { client } = await createConnectedClient(); + const callToolSpy = vi.spyOn(client, 'callTool'); + + const descriptors = await loadMcpTools(client, 'myconn'); + const result = await descriptors['myconn__add'].tool.execute!({ a: 2, b: 3 }, {} as any); + + expect(callToolSpy).toHaveBeenCalledWith({ name: 'add', arguments: { a: 2, b: 3 } }); + expect(result).toMatchObject({ content: [{ type: 'text', text: '5' }] }); + }); + + it('produces distinctly-named descriptors with zero collision across two connections exposing a tool of the same name', async () => { + const { client: linearClient } = await createConnectedClient(); + const { client: githubClient } = await createConnectedClient(); + + const linearDescriptors = await loadMcpTools(linearClient, 'linear'); + const githubDescriptors = await loadMcpTools(githubClient, 'github'); + + expect(linearDescriptors['linear__search']).toBeDefined(); + expect(githubDescriptors['github__search']).toBeDefined(); + expect(linearDescriptors['github__search']).toBeUndefined(); + expect(githubDescriptors['linear__search']).toBeUndefined(); + }); +}); diff --git a/src/tools/mcp/schema.test.ts b/src/tools/mcp/schema.test.ts new file mode 100644 index 000000000..b0daa1d20 --- /dev/null +++ b/src/tools/mcp/schema.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect } from 'vitest'; +import { jsonSchemaToZod } from './schema'; + +describe('jsonSchemaToZod', () => { + const cases: Array<{ + name: string; + schema: any; + valid: unknown[]; + invalid: unknown[]; + }> = [ + { + name: 'object with required and optional fields', + schema: { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'integer' }, + }, + required: ['name'], + }, + valid: [{ name: 'a' }, { name: 'a', age: 5 }], + invalid: [{}, { name: 'a', age: 'not-a-number' }, { age: 5 }], + }, + { + name: 'array of strings', + schema: { type: 'array', items: { type: 'string' } }, + valid: [[], ['a', 'b']], + invalid: [[1, 2], 'not-an-array'], + }, + { + name: 'string enum', + schema: { type: 'string', enum: ['red', 'green', 'blue'] }, + valid: ['red', 'blue'], + invalid: ['yellow', 1], + }, + { + name: 'number', + schema: { type: 'number' }, + valid: [1, 1.5, -3], + invalid: ['1', null], + }, + { + name: 'integer', + schema: { type: 'integer' }, + valid: [1, -3], + invalid: [1.5, '1'], + }, + { + name: 'boolean', + schema: { type: 'boolean' }, + valid: [true, false], + invalid: ['true', 1], + }, + { + name: 'nested object with array property', + schema: { + type: 'object', + properties: { + tags: { type: 'array', items: { type: 'string' } }, + }, + required: ['tags'], + }, + valid: [{ tags: ['a'] }], + invalid: [{ tags: 'a' }, {}], + }, + ]; + + it.each(cases)('$name', ({ schema, valid, invalid }) => { + const zodSchema = jsonSchemaToZod(schema); + for (const sample of valid) { + const result = zodSchema.safeParse(sample); + expect(result.success, `expected valid: ${JSON.stringify(sample)}`).toBe(true); + } + for (const sample of invalid) { + const result = zodSchema.safeParse(sample); + expect(result.success, `expected invalid: ${JSON.stringify(sample)}`).toBe(false); + } + }); + + it('marks non-required object fields optional', () => { + const zodSchema = jsonSchemaToZod({ + type: 'object', + properties: { name: { type: 'string' }, nickname: { type: 'string' } }, + required: ['name'], + }); + const result = zodSchema.safeParse({ name: 'a' }); + expect(result.success).toBe(true); + }); + + it('throws for a $ref schema', () => { + const schema = { $ref: '#/definitions/Node' }; + expect(() => jsonSchemaToZod(schema)).toThrow(/\$ref/); + }); + + it('throws for a recursive schema containing a nested $ref', () => { + const schema = { + type: 'object', + properties: { + name: { type: 'string' }, + children: { + type: 'array', + items: { $ref: '#/definitions/Node' }, + }, + }, + required: ['name'], + }; + expect(() => jsonSchemaToZod(schema)).toThrow(/\$ref/); + }); + + it('throws for an unsupported schema type', () => { + expect(() => jsonSchemaToZod({ type: 'null' })).toThrow(/unsupported/i); + }); +}); diff --git a/src/tools/mcp/schema.ts b/src/tools/mcp/schema.ts new file mode 100644 index 000000000..356edaab5 --- /dev/null +++ b/src/tools/mcp/schema.ts @@ -0,0 +1,116 @@ +/** + * JSON Schema -> Zod conversion utility (LOU-F1) + * + * Converts the plain-JSON-Schema `inputSchema` objects that MCP tools + * advertise into Zod schemas so they can be plugged straight into the + * 'ai' SDK's `tool({ parameters })` (which expects a ZodTypeAny). + * + * Deliberately NOT supported in this ticket: `$ref` / recursive schemas. + * MCP tool schemas are typically flat, and properly resolving `$ref` + * (including cycles, which Zod can only express via z.lazy()) is a + * meaningfully bigger problem than the rest of this converter. Rather + * than silently produce a wrong/partial schema, we throw a clear error + * so callers know the schema needs manual handling. + */ + +import { z, ZodTypeAny } from 'zod'; + +/** + * Convert a JSON Schema object into an equivalent Zod schema. + * + * Supported keywords: `type` (object/array/string/number/integer/boolean), + * `enum`, `properties`/`required` (object), `items` (array), `description`. + * + * Throws for `$ref` (anywhere in the schema, since it signals either a + * genuine cross-reference or a recursive/self-referential schema, neither + * of which this converter attempts to resolve) and for any other + * unsupported/unrecognized schema shape. + */ +export function jsonSchemaToZod(schema: any): ZodTypeAny { + return convert(schema); +} + +function convert(schema: any): ZodTypeAny { + if (schema === null || typeof schema !== 'object') { + throw new Error( + `jsonSchemaToZod: expected a JSON Schema object, got ${JSON.stringify(schema)}` + ); + } + + if ('$ref' in schema) { + throw new Error( + 'jsonSchemaToZod: $ref (and recursive schemas) are not supported in this converter' + ); + } + + // enum takes precedence over `type`, since a schema may specify both. + if (Array.isArray(schema.enum)) { + return applyDescription(convertEnum(schema.enum), schema); + } + + switch (schema.type) { + case 'object': + return applyDescription(convertObject(schema), schema); + case 'array': + return applyDescription(convertArray(schema), schema); + case 'string': + return applyDescription(z.string(), schema); + case 'number': + return applyDescription(z.number(), schema); + case 'integer': + return applyDescription(z.number().int(), schema); + case 'boolean': + return applyDescription(z.boolean(), schema); + default: + throw new Error( + `jsonSchemaToZod: unsupported schema type '${schema.type}' in ${JSON.stringify(schema)}` + ); + } +} + +function applyDescription(zodType: ZodTypeAny, schema: any): ZodTypeAny { + if (typeof schema.description === 'string' && schema.description.length > 0) { + return zodType.describe(schema.description); + } + return zodType; +} + +function convertEnum(values: unknown[]): ZodTypeAny { + if (values.length === 0) { + throw new Error('jsonSchemaToZod: enum must have at least one value'); + } + if (values.every((v) => typeof v === 'string')) { + return z.enum(values as [string, ...string[]]); + } + // Non-string enums (numbers, booleans, mixed) fall back to a union of + // literals, which Zod's z.enum() cannot express directly. + const literals = values.map((v) => z.literal(v as any)); + if (literals.length === 1) { + return literals[0]; + } + return z.union(literals as unknown as [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]]); +} + +function convertObject(schema: any): ZodTypeAny { + const properties = schema.properties ?? {}; + const required: string[] = Array.isArray(schema.required) ? schema.required : []; + + const shape: Record = {}; + for (const [key, propSchema] of Object.entries(properties)) { + let propZod = convert(propSchema); + if (!required.includes(key)) { + propZod = propZod.optional(); + } + shape[key] = propZod; + } + + return z.object(shape); +} + +function convertArray(schema: any): ZodTypeAny { + if (schema.items === undefined) { + throw new Error('jsonSchemaToZod: array schema is missing "items"'); + } + const itemZod = convert(schema.items); + return z.array(itemZod); +} diff --git a/src/types/tool.ts b/src/types/tool.ts index f31246911..d1a2c72c2 100644 --- a/src/types/tool.ts +++ b/src/types/tool.ts @@ -1,4 +1,5 @@ import { Tool as AITool } from 'ai'; +import { SandboxAdapter } from '../security/sandbox'; /** * Tool parameter definition @@ -36,6 +37,30 @@ export interface ToolDescriptor { tool: AITool; needsApproval?: boolean | ((args: any) => boolean | Promise); injectStreamingController?: (controller: ReadableStreamDefaultController) => void; + /** + * When true, AgentExecutor routes this tool's execution through the + * configured SandboxAdapter (see ExecuteOptions.sandbox, LOU-F5) instead + * of calling `tool.execute()` directly. Defaults to false/undefined, + * which is the exact pre-existing, unchanged execution path. + */ + requiresSandbox?: boolean; + /** + * Explicit alternate execution path a tool author implements when they + * want their tool to be genuinely sandboxable (LOU-F fix). Receives the + * configured SandboxAdapter and is responsible for using + * `sandbox.run()`/`sandbox.writeFile()` itself to perform the tool's + * real work (e.g. writing input to a file, running a command that does + * the actual computation, parsing the command's stdout as the result). + * + * This is the tool author's contract for "how do I actually run inside + * a sandbox" - a generic bridge from an arbitrary in-process JS closure + * (`tool.execute`) to a subprocess isn't mechanically possible without + * the tool itself cooperating. When `requiresSandbox` is true, + * AgentExecutor requires this to be defined and calls it instead of + * `tool.execute()`; if it is missing, AgentExecutor throws rather than + * silently falling back to unsandboxed in-process execution. + */ + sandboxExecute?: (args: unknown, sandbox: SandboxAdapter) => Promise; } /**