diff --git a/package-lock.json b/package-lock.json index f3417b8..26aae58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@electron-toolkit/utils": "^3.0.0", "@fontsource-variable/geist": "^5.2.9", "@fontsource-variable/geist-mono": "^5.2.8", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/client": "^2.0.0", "@pierre/diffs": "^1.2.11", "ai": "^5.0.210", "better-sqlite3": "^12.11.1", @@ -1313,18 +1313,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, "node_modules/@iconify/types": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", @@ -2116,68 +2104,36 @@ "@chevrotain/types": "~11.1.1" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "node_modules/@modelcontextprotocol/client": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0.tgz", + "integrity": "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", + "@modelcontextprotocol/core": "2.0.0", "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" + "zod": "^4.2.0" }, "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": ">=20" } }, - "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==", + "node_modules/@modelcontextprotocol/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz", + "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==", "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" + "zod": "^4.2.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=20" } }, - "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/@npmcli/fs": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", @@ -3648,53 +3604,6 @@ "dev": true, "license": "ISC" }, - "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/accepts/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/accepts/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/accepts/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -3767,45 +3676,6 @@ "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/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -4215,59 +4085,6 @@ "bluebird": "^3.5.5" } }, - "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": { - "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": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/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/boolean": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", @@ -4440,15 +4257,6 @@ "node": ">= 10.0.0" } }, - "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/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -4581,6 +4389,7 @@ "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==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4590,22 +4399,6 @@ "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/caniuse-lite": { "version": "1.0.30001799", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", @@ -5007,28 +4800,6 @@ "dev": true, "license": "ISC" }, - "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/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -5049,15 +4820,6 @@ "url": "https://opencollective.com/express" } }, - "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/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -5065,23 +4827,6 @@ "dev": true, "license": "MIT" }, - "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/cose-base": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", @@ -5816,15 +5561,6 @@ "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", @@ -6039,6 +5775,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -6056,12 +5793,6 @@ "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/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -6398,15 +6129,6 @@ "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/encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", @@ -6473,6 +6195,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6482,6 +6205,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6491,6 +6215,7 @@ "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==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -6581,12 +6306,6 @@ "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", @@ -6610,15 +6329,6 @@ "url": "https://opencollective.com/unified" } }, - "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", @@ -6656,101 +6366,6 @@ "dev": true, "license": "Apache-2.0" }, - "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.5.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", - "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", - "license": "MIT", - "dependencies": { - "ip-address": "^10.2.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/express/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/express/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/express/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/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -6792,6 +6407,7 @@ "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": { @@ -6801,22 +6417,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-uri": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", - "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -6889,27 +6489,6 @@ "node": ">=10" } }, - "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/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -6957,24 +6536,6 @@ "node": ">= 6" } }, - "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", @@ -7034,6 +6595,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7084,6 +6646,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -7108,6 +6671,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -7243,6 +6807,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7315,6 +6880,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7350,6 +6916,7 @@ "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==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -7530,15 +7097,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hono": { - "version": "4.12.27", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", - "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -7607,26 +7165,6 @@ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "license": "BSD-2-Clause" }, - "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/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -7839,20 +7377,12 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, "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-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -7949,12 +7479,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -8108,12 +7632,6 @@ "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-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -8773,6 +8291,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9060,27 +8579,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "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/mermaid": { "version": "11.15.0", "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.15.0.tgz", @@ -10126,27 +9624,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "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/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -10157,18 +9634,6 @@ "node": ">= 0.4" } }, - "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", @@ -10326,15 +9791,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "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-data-parser": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", @@ -10394,16 +9850,6 @@ "node": ">=16 || 14 >=14.17" } }, - "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/pe-library": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", @@ -10614,19 +10060,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -10656,22 +10089,6 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", - "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/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -10684,50 +10101,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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/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": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/raw-body/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/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -11058,15 +10431,6 @@ "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/resedit": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", @@ -11225,22 +10589,6 @@ "points-on-path": "^0.2.1" } }, - "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/rw": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", @@ -11317,57 +10665,6 @@ "license": "MIT", "optional": true }, - "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/send/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/send/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/serialize-error": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", @@ -11384,25 +10681,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -11416,12 +10694,6 @@ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, - "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/sharp": { "version": "0.35.2", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz", @@ -11520,78 +10792,6 @@ "node": ">=20" } }, - "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/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -11811,15 +11011,6 @@ "node": ">= 6" } }, - "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/streamdown": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/streamdown/-/streamdown-2.5.0.tgz", @@ -12188,15 +11379,6 @@ "tmp": "^0.2.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/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -12269,62 +11451,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/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/type-is/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/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -12467,15 +11593,6 @@ "node": ">= 4.0.0" } }, - "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/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -12552,15 +11669,6 @@ "uuid": "dist-node/bin/uuid" } }, - "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/verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", @@ -12924,15 +12032,6 @@ "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - }, "node_modules/zustand": { "version": "5.0.14", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", diff --git a/package.json b/package.json index b7a7012..5262c36 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@electron-toolkit/utils": "^3.0.0", "@fontsource-variable/geist": "^5.2.9", "@fontsource-variable/geist-mono": "^5.2.8", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/client": "^2.0.0", "@pierre/diffs": "^1.2.11", "ai": "^5.0.210", "better-sqlite3": "^12.11.1", diff --git a/src/main/db/migrations.ts b/src/main/db/migrations.ts index 6d88a65..b496b93 100644 --- a/src/main/db/migrations.ts +++ b/src/main/db/migrations.ts @@ -81,6 +81,19 @@ const REPAIR_SCHEMA_SQL = /* sql */ ` config TEXT NOT NULL DEFAULT '{}', enabled INTEGER NOT NULL DEFAULT 1, created_at INTEGER NOT NULL + , origin TEXT NOT NULL DEFAULT 'user'); + CREATE TABLE IF NOT EXISTS mcp_trust ( + id TEXT NOT NULL, + fingerprint TEXT NOT NULL, + provenance TEXT NOT NULL, + scope TEXT NOT NULL DEFAULT '', + decision TEXT NOT NULL, + decided_at INTEGER NOT NULL, + PRIMARY KEY (id, fingerprint, scope) + ); + CREATE TABLE IF NOT EXISTS mcp_trusted_workspaces ( + path TEXT PRIMARY KEY, + trusted_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS messages ( id TEXT PRIMARY KEY, @@ -474,6 +487,76 @@ export const MIGRATIONS: Migration[] = [ // deliberately structure-only and runs on every open). One DELETE, once. /* sql */ ` DELETE FROM settings WHERE key = 'web_search_api_key'; + `, + + // ---- v23: MCP server trust (consent before Roxy spawns anything) ---- + // A local MCP server is an arbitrary command run with the user’s own + // privileges, and two of the three places Roxy reads server definitions from + // are not the user: a workspace `.roxy/mcp.json` arrives with `git clone`, and + // the `mcp` agent tool is driven by a model that reads web pages and issues. + // Both used to connect automatically, which made "clone a repo, send any + // message" enough to execute attacker-chosen code. + // + // Consent is keyed by a FINGERPRINT of what actually executes (argv, cwd, env + // var NAMES, never their values) rather than by server id, so renaming an entry + // keeps its approval while swapping its command revokes it. `scope` is the + // workspace a decision belongs to (empty string = anywhere), so approving `db` + // in one repo never pre-approves a different repo's `db`. It is NOT NULL with a + // default because SQLite treats NULLs as distinct in a PRIMARY KEY, which would + // let duplicate global rows accumulate for the same decision. + // + // Two tables, not one: individual decisions, and whole workspaces trusted + // wholesale, which must also cover servers added to that project later and so + // cannot be expressed as a set of per-server rows. + /* sql */ ` + CREATE TABLE IF NOT EXISTS mcp_trust ( + id TEXT NOT NULL, + fingerprint TEXT NOT NULL, + provenance TEXT NOT NULL, + scope TEXT NOT NULL DEFAULT '', + decision TEXT NOT NULL, + decided_at INTEGER NOT NULL, + PRIMARY KEY (id, fingerprint, scope) + ); + CREATE TABLE IF NOT EXISTS mcp_trusted_workspaces ( + path TEXT PRIMARY KEY, + trusted_at INTEGER NOT NULL + ); + `, + + // ---- v24: record WHO added each MCP server row ---- + // The `mcp` agent tool writes to `mcp_servers`, the same table Settings writes + // to. Without this column, a server the MODEL added became indistinguishable + // from one the user typed in the moment it was persisted - so the consent gate + // would read it back as self-consenting and launch it unprompted. That would + // turn the trust check into a formality a single prompt-injection can bypass. + // + // Existing rows are backfilled to 'user': they predate the agent tool being + // gated, and they are all rows a human could see and remove in Settings. + (db) => { + addColumnIfMissing(db, 'mcp_servers', 'origin', "TEXT NOT NULL DEFAULT 'user'") + }, + + // ---- v25: OAuth credentials for remote MCP servers ---- + // A remote MCP server is an HTTP API that may hold the user's issues, calendar + // or database, so the spec protects it with OAuth 2.1 + PKCE + dynamic client + // registration. The SDK runs that dance but needs the host to persist what it + // earns. + // + // `payload` is encrypted at rest via secure.ts (OS keychain when available) - + // a refresh token is a long-lived bearer credential for a third-party account, + // and the settings table is not a place to keep one. `kind` separates the + // three lifetimes: the client registration and tokens persist, while the PKCE + // verifier is single-use and deleted the moment the code is exchanged. + /* sql */ ` + CREATE TABLE IF NOT EXISTS mcp_oauth ( + server_id TEXT NOT NULL, + kind TEXT NOT NULL, + payload TEXT NOT NULL, + encrypted INTEGER NOT NULL DEFAULT 0, + updated_at INTEGER NOT NULL, + PRIMARY KEY (server_id, kind) + ); ` ] @@ -528,6 +611,38 @@ export function repairSchema(db: Database): void { db.exec( 'CREATE TABLE IF NOT EXISTS pinned_models (provider_id TEXT NOT NULL, model TEXT NOT NULL, pinned_at INTEGER NOT NULL, PRIMARY KEY (provider_id, model))' ) + // v23 MCP trust. Structure only: these tables record CONSENT, so a lost row + // must degrade to asking the user again, never to inventing an approval. + // v24's provenance column on mcp_servers (see the migration for why it is + // security-relevant, not cosmetic). + addColumnIfMissing(db, 'mcp_servers', 'origin', "TEXT NOT NULL DEFAULT 'user'") + // v25 OAuth credentials for remote MCP servers. Structure only: a lost row + // means the user signs in again, never a fabricated credential. + db.exec(` + CREATE TABLE IF NOT EXISTS mcp_oauth ( + server_id TEXT NOT NULL, + kind TEXT NOT NULL, + payload TEXT NOT NULL, + encrypted INTEGER NOT NULL DEFAULT 0, + updated_at INTEGER NOT NULL, + PRIMARY KEY (server_id, kind) + ); + `) + db.exec(` + CREATE TABLE IF NOT EXISTS mcp_trust ( + id TEXT NOT NULL, + fingerprint TEXT NOT NULL, + provenance TEXT NOT NULL, + scope TEXT NOT NULL DEFAULT '', + decision TEXT NOT NULL, + decided_at INTEGER NOT NULL, + PRIMARY KEY (id, fingerprint, scope) + ); + CREATE TABLE IF NOT EXISTS mcp_trusted_workspaces ( + path TEXT PRIMARY KEY, + trusted_at INTEGER NOT NULL + ); + `) // `projects` is derived state — one row per workspace folder its sessions use // — so a restored table can be rebuilt from the chats themselves, exactly as // v13 did on first upgrade. Only when empty, so a hand-ordered project list is diff --git a/src/main/db/repo.ts b/src/main/db/repo.ts index b79421a..40025dc 100644 --- a/src/main/db/repo.ts +++ b/src/main/db/repo.ts @@ -1,6 +1,7 @@ import { randomUUID } from 'node:crypto' import { resolveSeed } from '../../shared/providers' import { normalizeServerConfig, type McpServerConfig, type McpServerRecord } from '../../shared/mcp' +import type { McpTrustEntry, McpTrustStore } from '../../shared/mcp-trust' import { DEFAULT_BRANCH_PREFIX, normalizeBranchPrefix } from '../../shared/branch' import { DEFAULT_LANGUAGE, normalizeLanguage } from '../../shared/i18n' import type { Language } from '../../shared/i18n' @@ -251,6 +252,9 @@ export function resetAll(): void { DELETE FROM providers; DELETE FROM integrations; DELETE FROM mcp_servers; + DELETE FROM mcp_oauth; + DELETE FROM mcp_trust; + DELETE FROM mcp_trusted_workspaces; DELETE FROM activity; DELETE FROM settings;` ) @@ -1409,6 +1413,7 @@ interface McpServerRow { config: string enabled: number created_at: number + origin: string | null } /** Every configured MCP server (a bad/legacy config row is skipped, not thrown). */ @@ -1420,30 +1425,53 @@ export function listMcpServers(): McpServerRecord[] { for (const row of rows) { const config = normalizeServerConfig(safeParse(row.config)) if (!config) continue - out.push({ id: row.id, config, enabled: row.enabled > 0 }) + // Anything that isn't exactly 'user' is treated as agent-added. Unknown + // values must fall to the side that still requires consent. + out.push({ + id: row.id, + config, + enabled: row.enabled > 0, + origin: row.origin === 'user' ? 'user' : 'agent' + }) } return out } -/** Create or replace a server by id (name). Returns the stored record. */ +/** + * Create or replace a server by id (name). Returns the stored record. + * + * `origin` records WHO is writing (see McpServerRecord.origin). It defaults to + * 'agent' so a new call site cannot accidentally mint a self-consenting row: + * being wrongly asked to approve a server is a papercut, being wrongly NOT asked + * is arbitrary code execution. + * + * An existing row's origin is preserved on conflict unless the writer is the + * user: the user editing a config is a fresh human decision and upgrades the + * row, while the agent rewriting one must never launder it into 'user'. + */ export function upsertMcpServer(input: { id: string config: McpServerConfig enabled?: boolean + origin?: 'user' | 'agent' }): McpServerRecord { const id = input.id.trim() if (!id) throw new Error('MCP server id is required') const config = normalizeServerConfig(input.config) if (!config) throw new Error('Invalid MCP server config') const enabled = input.enabled === false ? 0 : 1 + const origin = input.origin ?? 'agent' getDb() .prepare( - `INSERT INTO mcp_servers(id, config, enabled, created_at) - VALUES(?, ?, ?, ?) - ON CONFLICT(id) DO UPDATE SET config = excluded.config, enabled = excluded.enabled` + `INSERT INTO mcp_servers(id, config, enabled, created_at, origin) + VALUES(?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + config = excluded.config, + enabled = excluded.enabled, + origin = CASE WHEN excluded.origin = 'user' THEN 'user' ELSE mcp_servers.origin END` ) - .run(id, JSON.stringify(config), enabled, Date.now()) - return { id, config, enabled: enabled > 0 } + .run(id, JSON.stringify(config), enabled, Date.now(), origin) + return { id, config, enabled: enabled > 0, origin } } export function deleteMcpServer(id: string): void { @@ -1456,6 +1484,225 @@ export function setMcpServerEnabled(id: string, enabled: boolean): void { .run(enabled ? 1 : 0, id) } +// ---- MCP trust (consent to actually RUN a server) ---------------------------- +// See src/shared/mcp-trust.ts for what a fingerprint is and why consent is keyed +// by it rather than by server name. This layer is storage only: it holds no +// policy and makes no decisions. +// +// Every read degrades to "nothing is trusted" rather than throwing. A corrupt or +// missing trust store must fail CLOSED - the user gets asked again, which is +// merely annoying, instead of an approval being fabricated for a command nobody +// approved. + +interface McpTrustRow { + id: string + fingerprint: string + provenance: string + scope: string + decision: string + decided_at: number +} + +/** Empty string is how "applies anywhere" is stored; see the v23 migration. */ +const GLOBAL_SCOPE = '' + +/** Narrow one untrusted row; anything unrecognised is dropped (fail closed). */ +function toTrustEntry(row: McpTrustRow): McpTrustEntry | null { + if (row.decision !== 'allow' && row.decision !== 'deny') return null + const p = row.provenance + if (p !== 'user' && p !== 'workspace' && p !== 'agent' && p !== 'import') return null + return { + id: row.id, + fingerprint: row.fingerprint, + provenance: p, + scope: row.scope === GLOBAL_SCOPE ? null : row.scope, + decision: row.decision, + decidedAt: row.decided_at + } +} + +/** + * Whether the user opted into confirming each new MCP server BEFORE it runs. + * + * Off by default: choosing to install a server is itself the decision, so the + * default posture is to run it and disclose what it exposed. This is the + * stricter opt-in for shared machines and untrusted repos. + * + * Its own accessor rather than a field on `AppSettings` because it is read on + * the connect path, not from a cached settings object the renderer also writes. + */ +export function getMcpConfirmBeforeRun(): boolean { + const row = getDb() + .prepare('SELECT value FROM settings WHERE key = ?') + .get('mcp_confirm_before_run') as { value: string } | undefined + return row?.value === '1' +} + +/** Opt into confirming each new server before it runs (default: off). */ +export function setMcpConfirmBeforeRun(enabled: boolean): void { + setSetting('mcp_confirm_before_run', enabled ? '1' : null) +} + +/** The whole trust store, for the pure resolver in shared/mcp-trust.ts. */ +export function getMcpTrustStore(): McpTrustStore { + try { + const entries = ( + getDb().prepare('SELECT * FROM mcp_trust ORDER BY decided_at ASC').all() as McpTrustRow[] + ) + .map(toTrustEntry) + .filter((e): e is McpTrustEntry => e !== null) + const workspaces = ( + getDb() + .prepare('SELECT path, trusted_at FROM mcp_trusted_workspaces ORDER BY trusted_at ASC') + .all() as { path: string; trusted_at: number }[] + ).map((r) => ({ path: r.path, trustedAt: r.trusted_at })) + return { entries, workspaces } + } catch { + // Fail closed: an unreadable store means "ask", never "allow". + return { entries: [], workspaces: [] } + } +} + +/** Remember one decision. Re-deciding the same (id, fingerprint, scope) replaces it. */ +export function recordMcpTrust(entry: McpTrustEntry): void { + getDb() + .prepare( + `INSERT INTO mcp_trust(id, fingerprint, provenance, scope, decision, decided_at) + VALUES(?, ?, ?, ?, ?, ?) + ON CONFLICT(id, fingerprint, scope) DO UPDATE SET + decision = excluded.decision, + provenance = excluded.provenance, + decided_at = excluded.decided_at` + ) + .run( + entry.id, + entry.fingerprint, + entry.provenance, + entry.scope ?? GLOBAL_SCOPE, + entry.decision, + entry.decidedAt + ) +} + +/** + * Forget decisions for a server id. + * + * With a `scope` argument this is scoped, so revoking a server in one project + * cannot silently revoke the same-named server the user approved in another. + * Omit it to forget the id everywhere (what "Revoke" in Settings does). + */ +export function revokeMcpTrust(id: string, scope?: string | null): void { + if (scope === undefined) { + getDb().prepare('DELETE FROM mcp_trust WHERE id = ?').run(id) + return + } + getDb() + .prepare('DELETE FROM mcp_trust WHERE id = ? AND scope = ?') + .run(id, scope ?? GLOBAL_SCOPE) +} + +// ---- MCP OAuth (credentials for remote servers) ------------------------------ +// Encrypted at rest via secure.ts. Every read fails CLOSED to `null`: a +// credential that cannot be decrypted (keychain moved, profile copied to another +// machine) must send the user back through sign-in, never surface a broken token +// that fails confusingly on the wire. + +type McpOAuthKind = 'client' | 'tokens' | 'verifier' + +function readOAuth(serverId: string, kind: McpOAuthKind): T | null { + try { + const row = getDb() + .prepare('SELECT payload, encrypted FROM mcp_oauth WHERE server_id = ? AND kind = ?') + .get(serverId, kind) as { payload: string; encrypted: number } | undefined + if (!row) return null + return JSON.parse(decryptSecret({ data: row.payload, encrypted: row.encrypted > 0 })) as T + } catch { + return null + } +} + +function writeOAuth(serverId: string, kind: McpOAuthKind, value: unknown): void { + const secure = encryptSecret(JSON.stringify(value)) + getDb() + .prepare( + `INSERT INTO mcp_oauth(server_id, kind, payload, encrypted, updated_at) + VALUES(?, ?, ?, ?, ?) + ON CONFLICT(server_id, kind) DO UPDATE SET + payload = excluded.payload, + encrypted = excluded.encrypted, + updated_at = excluded.updated_at` + ) + .run(serverId, kind, secure.data, secure.encrypted ? 1 : 0, Date.now()) +} + +/** The dynamic client registration earned for this server, if any. */ +export function getMcpOAuthClient(serverId: string): T | null { + return readOAuth(serverId, 'client') +} + +export function saveMcpOAuthClient(serverId: string, info: unknown): void { + writeOAuth(serverId, 'client', info) +} + +/** The access/refresh tokens for this server, if any. */ +export function getMcpOAuthTokens(serverId: string): T | null { + return readOAuth(serverId, 'tokens') +} + +export function saveMcpOAuthTokens(serverId: string, tokens: unknown): void { + writeOAuth(serverId, 'tokens', tokens) +} + +/** + * The in-flight PKCE code verifier. + * + * Read-once: the verifier is single-use by design, and leaving it behind after + * the exchange means a stale secret sitting in the database for a flow that + * already completed. + */ +export function getMcpOAuthVerifier(serverId: string): string | null { + const v = readOAuth(serverId, 'verifier') + if (v) { + getDb().prepare("DELETE FROM mcp_oauth WHERE server_id = ? AND kind = 'verifier'").run(serverId) + } + return v +} + +export function saveMcpOAuthVerifier(serverId: string, verifier: string): void { + writeOAuth(serverId, 'verifier', verifier) +} + +/** Whether this server has usable stored tokens (drives the "signed in" badge). */ +export function hasMcpOAuth(serverId: string): boolean { + return getMcpOAuthTokens(serverId) !== null +} + +/** Forget every credential for a server (sign out / server removed). */ +export function clearMcpOAuth(serverId: string): void { + getDb().prepare('DELETE FROM mcp_oauth WHERE server_id = ?').run(serverId) +} + +/** Trust a whole workspace: every server it declares, now and in the future. */ +export function trustMcpWorkspace(path: string): void { + getDb() + .prepare( + `INSERT INTO mcp_trusted_workspaces(path, trusted_at) VALUES(?, ?) + ON CONFLICT(path) DO UPDATE SET trusted_at = excluded.trusted_at` + ) + .run(path, Date.now()) +} + +/** + * Withdraw workspace-wide trust. + * + * Individual per-server decisions are deliberately left alone: each was made + * about one specific command, and this action means "stop blanket-trusting NEW + * servers here", not "forget everything I ever approved". + */ +export function untrustMcpWorkspace(path: string): void { + getDb().prepare('DELETE FROM mcp_trusted_workspaces WHERE path = ?').run(path) +} + // ---- Usage / cost ------------------------------------------------------------ interface UsageRow { diff --git a/src/main/harness/agent.ts b/src/main/harness/agent.ts index 5fb2083..3aaff1d 100644 --- a/src/main/harness/agent.ts +++ b/src/main/harness/agent.ts @@ -62,9 +62,16 @@ import { mcpInstructions, mcpToolSchemas, mcpToolTitle, + mcpServerSummaries, + mcpToolDefinition, isMcpTool } from '../services/mcp' -import type { McpServerRecord } from '../../shared/mcp' +import { + gateRecords as gateMcpRecords, + discloseConnected as discloseMcpConnected, + type McpCandidate +} from '../services/mcp-trust' +import { uiResourceUri } from '../../shared/mcp-apps' import { SKILL_TOOL_NAME, SKILL_TOOL_DESCRIPTION } from '../../shared/skills' import { listSkills, skillInstructions } from '../services/skills' import { findGitRoot } from '../services/workspace' @@ -496,14 +503,43 @@ function stripAnsi(s: string): string { return s.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '') } +/** + * The server id embedded in a namespaced MCP tool name. + * + * `mcp____`; the server half is everything between the prefix and + * the final separator, so a server id containing the separator still resolves. + */ +function mcpServerIdOf(qualified: string): string { + const rest = qualified.slice('mcp__'.length) + const cut = rest.lastIndexOf('__') + return cut > 0 ? rest.slice(0, cut) : rest +} + /** * The MCP servers to connect for a turn: DB-configured globals overlaid by any * workspace `.roxy/mcp.json` entries (workspace wins on an id collision). + * + * Each record keeps its PROVENANCE, because that is what decides whether it may + * run at all. A DB row was typed by the user in Settings and is self-consenting; + * a workspace entry arrived with `git clone` and is attacker-controlled, so it + * has to clear the consent gate first (see services/mcp-trust.ts). + * + * The workspace-wins overlay is kept - a project overriding a global server by + * name is a legitimate thing to want - but the override INHERITS the workspace + * provenance, so shadowing a trusted global name cannot launder an untrusted + * command into a self-consenting one. */ -function gatherMcpRecords(cwd: string): McpServerRecord[] { - const byId = new Map() - for (const r of repo.listMcpServers()) byId.set(r.id, r) - for (const r of loadWorkspaceMcpServers(cwd)) byId.set(r.id, r) +function gatherMcpCandidates(cwd: string): McpCandidate[] { + const byId = new Map() + for (const record of repo.listMcpServers()) { + // A DB row is only self-consenting when a human wrote it; rows the `mcp` + // tool added still have to clear the gate (see McpServerRecord.origin). + const provenance = record.origin === 'agent' ? 'agent' : 'user' + byId.set(record.id, { record, provenance, workspace: null }) + } + for (const record of loadWorkspaceMcpServers(cwd)) { + byId.set(record.id, { record, provenance: 'workspace', workspace: cwd }) + } return [...byId.values()] } @@ -987,14 +1023,30 @@ export async function runAgentTurn(opts: RunTurnOptions): Promise { let mcpSchemas: ReturnType = [] let mcpInfo: string | undefined if (toolCapable && cwd && !readOnly) { - const records = gatherMcpRecords(cwd) - if (records.length) { - await ensureMcpConnected(records, cwd) - // Scope to THIS turn's records so a workspace's `.roxy/mcp.json` servers can't - // leak into a different workspace's chat (the pool is process-global). - const ids = new Set(records.map((r) => r.id)) - mcpSchemas = mcpToolSchemas(ids) - mcpInfo = mcpInstructions(ids) + const candidates = gatherMcpCandidates(cwd) + if (candidates.length) { + // Consent gate BEFORE connect: `ensureMcpConnected` spawns processes, so a + // server the user has not approved must never reach it. Servers that are + // denied (or whose prompt times out) are simply dropped, exactly like one + // that fails to spawn - a turn is never blocked on them. + const { records, disclose } = await gateMcpRecords(candidates) + if (records.length) { + await ensureMcpConnected(records, cwd) + // Scope to THIS turn's records so a workspace's `.roxy/mcp.json` servers can't + // leak into a different workspace's chat (the pool is process-global). + const ids = new Set(records.map((r) => r.id)) + mcpSchemas = mcpToolSchemas(ids) + mcpInfo = mcpInstructions(ids) + // Tell the user what any newly-seen server actually turned out to be. + // Deliberately AFTER connecting: the tool list is the useful part of the + // notice, and it does not exist until the handshake has happened. + if (disclose.size) { + for (const summary of mcpServerSummaries(new Set(disclose.keys()))) { + const candidate = disclose.get(summary.id) + if (candidate) discloseMcpConnected(candidate, summary.tools, summary.error) + } + } + } } } @@ -1364,13 +1416,25 @@ async function runLoop(o: LoopOptions): Promise { run.end() signal.removeEventListener('abort', cascade) } + // Does this tool ship its own UI? Resolved here from the definition the + // server sent at discovery, so the renderer never reasons about MCP + // metadata - it is simply told that a view exists and where it lives. + // + // Only on success: a view is handed the tool result at initialize, and + // mounting one for a failed call would render an interface around an + // error the user is better off reading as text. + const uiUri = + result.ok && isMcpTool(tc.name) + ? uiResourceUri(mcpToolDefinition(tc.name)?._meta) + : undefined emitTool({ type: 'tool-end', callId: tc.id, output: result.output, ok: result.ok, image: result.image, - diff: result.diff + diff: result.diff, + ...(uiUri ? { mcpApp: { serverId: mcpServerIdOf(tc.name), resourceUri: uiUri } } : {}) }) // Count the call. `recordTool` collapses the name through the closed // vocabulary, so an MCP tool reports as the literal `mcp` and its @@ -1393,7 +1457,11 @@ async function runLoop(o: LoopOptions): Promise { // workspace's MCP schemas and merge them into the tool list so the new tools // are callable on the very next model step (this turn), not only next message. if (cwd && !readOnly && others.some((tc) => tc.name === 'mcp')) { - const ids = new Set(gatherMcpRecords(cwd).map((r) => r.id)) + // No gate here: this only re-reads schemas from servers ALREADY in the + // warm pool, and nothing reaches that pool without passing the consent + // check in the `mcp` tool itself. An unapproved server contributes no + // connection, so it contributes no tools. + const ids = new Set(gatherMcpCandidates(cwd).map((c) => c.record.id)) liveMcpTools = mcpToolSchemas(ids) liveTools = [...baseTools, ...liveMcpTools] } diff --git a/src/main/harness/tools.ts b/src/main/harness/tools.ts index 8f6bc74..4166295 100644 --- a/src/main/harness/tools.ts +++ b/src/main/harness/tools.ts @@ -39,8 +39,14 @@ import { normalizeServerConfig, qualifyToolName, type McpServerConfig, + type McpServerRecord, type McpServerSummary } from '../../shared/mcp' +import { + ensureTrusted as ensureMcpTrusted, + discloseConnected as discloseMcpConnected, + type McpCandidate +} from '../services/mcp-trust' import { loadSkill, listSkills, @@ -287,9 +293,15 @@ export async function runTool( default: // Tools contributed by connected MCP servers use namespaced names // (`mcp____`) — route them to the MCP pool. An MCP round - // trip has its own 120s timeout and no signal, so the turn stops - // waiting rather than blocking Stop for up to two minutes. - if (isMcpTool(name)) return await untilAborted(ctx.signal, () => callMcpTool(name, input)) + // trip carries the server's own timeout budget. + // The signal goes BOTH ways now: `untilAborted` stops the turn waiting, + // and passing it into `callMcpTool` cancels the request itself, so the + // SDK emits `notifications/cancelled` and a well-behaved server can + // abandon its work instead of finishing into a void. Without the inner + // half, Stop only stopped us listening - the server kept going. + if (isMcpTool(name)) { + return await untilAborted(ctx.signal, () => callMcpTool(name, input, ctx.signal)) + } return { ok: false, output: `Unknown tool: ${name}` } } } catch (e) { @@ -1460,8 +1472,26 @@ async function runMcpTool(input: Record, cwd: string): Promise< 'mcp add: provide a LOCAL server "command" (argv array, e.g. ["npx","-y","@modelcontextprotocol/server-filesystem","/dir"]) OR a REMOTE "url".' } } - repo.upsertMcpServer({ id, config, enabled: true }) - const summary = await reconnectMcpServer({ id, config, enabled: true }, cwd) + // Check BEFORE persisting. Normally this is a no-op that returns "run it", + // but when it does interrupt - a known server id now pointing at a + // different command - a declined answer must leave NOTHING behind. Writing + // first would park the rejected config in the DB, where the next `enable` + // or `reconnect` would read it back and be blocked by the very swap the + // user just refused. + const record: McpServerRecord = { id, config, enabled: true, origin: 'agent' } + const candidate = { record, provenance: 'agent' as const, workspace: cwd || null } + const { allowed, disclose } = await ensureMcpTrusted(candidate) + if (!allowed) { + return { + ok: false, + output: `MCP server "${id}" was not added: the user declined to run it.` + } + } + repo.upsertMcpServer({ id, config, enabled: true, origin: 'agent' }) + const summary = await reconnectMcpServer(record, cwd) + // Show the user what the agent just wired up: the tool list is the whole + // point of the notice, and it only exists once the server has connected. + if (disclose) discloseMcpConnected(candidate, summary.tools, summary.error) return summarizeMcpConnect('Added', id, summary) } case 'reconnect': @@ -1526,11 +1556,40 @@ function mcpListServers(): ToolResult { return { ok: true, output: `MCP servers (${records.length}):\n${lines.join('\n')}` } } +/** + * Consent check shared by the connect-causing actions (`add`, `reconnect`, + * `enable`). All three end in a spawned process, so all three are gated - a gate + * on `add` alone would be trivially bypassed by adding a server while the user + * declines, then calling `reconnect`. + */ +/** + * Trust check shared by the connect-causing actions (`add`, `reconnect`, + * `enable`). Normally a no-op that returns "run it"; it only interrupts when a + * trusted entry's command changed, or when the user opted into confirming. + */ +async function mcpTrustCheck( + rec: McpServerRecord, + cwd: string +): Promise<{ allowed: boolean; disclose: boolean; candidate: McpCandidate }> { + const candidate: McpCandidate = { + record: rec, + provenance: rec.origin === 'agent' ? 'agent' : 'user', + workspace: cwd || null + } + const { allowed, disclose } = await ensureMcpTrusted(candidate) + return { allowed, disclose, candidate } +} + async function mcpReconnectServer(id: string, cwd: string): Promise { const rec = repo.listMcpServers().find((r) => r.id === id) if (!rec) return { ok: false, output: `mcp reconnect: no server named "${id}". Run action:"list".` } + const { allowed, disclose, candidate } = await mcpTrustCheck(rec, cwd) + if (!allowed) { + return { ok: false, output: `MCP server "${id}" was not connected: the user declined it.` } + } const summary = await reconnectMcpServer(rec, cwd) + if (disclose) discloseMcpConnected(candidate, summary.tools, summary.error) return summarizeMcpConnect('Reconnected', id, summary) } @@ -1542,12 +1601,20 @@ async function mcpSetServerEnabled(id: string, enabled: boolean, cwd: string): P output: `mcp ${enabled ? 'enable' : 'disable'}: no server named "${id}". Run action:"list".` } } - repo.setMcpServerEnabled(id, enabled) + // Disabling is always allowed: it only ever REMOVES a capability. if (!enabled) { + repo.setMcpServerEnabled(id, false) await disposeConnection(id) return { ok: true, output: `Disabled MCP server "${id}" and disconnected it.` } } - const summary = await reconnectMcpServer({ ...rec, enabled: true }, cwd) + const enabledRec = { ...rec, enabled: true } + const { allowed, disclose, candidate } = await mcpTrustCheck(enabledRec, cwd) + if (!allowed) { + return { ok: false, output: `MCP server "${id}" was not enabled: the user declined it.` } + } + repo.setMcpServerEnabled(id, true) + const summary = await reconnectMcpServer(enabledRec, cwd) + if (disclose) discloseMcpConnected(candidate, summary.tools, summary.error) return summarizeMcpConnect('Enabled', id, summary) } diff --git a/src/main/index.ts b/src/main/index.ts index 06d0ee2..8c1895f 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -14,6 +14,9 @@ import { cleanupToolOutputs } from './services/tool-output-store' import { cancelAllBackgroundJobs } from './services/background-tasks' import { shutdownAllLsp } from './services/lsp' import { shutdownAllMcp } from './services/mcp' +import { cancelAllConsent } from './services/mcp-trust' +import { registerSandboxScheme, serveSandbox } from './services/mcp-app-sandbox' +import { closeAllMcpApps } from './services/mcp-apps' import { shutdownRemote } from './services/remote' import { shutdownCliProxy } from './services/cliproxy' import { initAutoUpdater } from './services/updater' @@ -97,6 +100,11 @@ async function warmCatalogThenBackfill(): Promise { backfillUsageFromHistory() } +// Custom scheme for MCP App views. MUST be registered before the app is ready: +// privileges are locked in at that moment, and a view loaded on a non-standard +// origin would not get the same-origin isolation the sandbox depends on. +registerSandboxScheme() + app.whenReady().then(() => { electronApp.setAppUserModelId('com.roxy.app') // Give the agent's browser window the Roxy icon too (no asset import in the @@ -133,6 +141,7 @@ app.whenReady().then(() => { // backfilled rows can be priced (else they'd all cost $0). Best-effort + async. void warmCatalogThenBackfill() + serveSandbox() const mainWindow = createWindow() initAutoUpdater(mainWindow) @@ -162,6 +171,10 @@ app.on('will-quit', () => { cancelAllBackgroundJobs() closeAllBrowsers() shutdownAllLsp() + // Resolve any open consent prompt as a DENY before the window goes away, so + // an awaiting connect unwinds instead of hanging until its timeout. + closeAllMcpApps() + cancelAllConsent() void shutdownAllMcp() shutdownRemote() // The Codex sidecar holds the user's subscription tokens - never leave it diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 244d0c7..f2e62ad 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -1,4 +1,5 @@ import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron' +import { randomUUID } from 'node:crypto' import { CHANNELS } from '../../shared/ipc' import type { Language } from '../../shared/i18n' import type { SessionConfigPatch } from '../../shared/session-config' @@ -70,7 +71,30 @@ import { cancelSubagentRunsFor } from '../services/subagent-stream' import { cancelToolCall, cancelToolCallsFor } from '../services/tool-runs' -import { mcpServerSummaries, reconnectMcpServer, disposeConnection } from '../services/mcp' +import { + mcpServerSummaries, + reconnectMcpServer, + disposeConnection, + signInMcpServer, + signOutMcpServer, + isMcpSignedIn +} from '../services/mcp' +import { + ensureTrusted as ensureMcpTrusted, + discloseConnected as discloseMcpConnected, + resolveConsent as resolveMcpConsent, + setConfirmBeforeRun as setMcpConfirmBeforeRun, + trustPolicy as mcpTrustPolicy +} from '../services/mcp-trust' +import type { McpConsentResponse, McpProvenance } from '../../shared/mcp-trust' +import { + launchMcpApp, + handleMcpAppRequest, + closeMcpApp, + setMcpAppTheme, + setMcpAppApprover, + type BridgeRequest +} from '../services/mcp-apps' import { listSkills, refreshSkills, @@ -148,6 +172,46 @@ function abortSession(sessionId: string): void { for (const controller of sessionControllers.get(sessionId) ?? []) controller.abort() } +// ---- MCP App tool-call approvals ----------------------------------------- +// A view is untrusted code, so it does not get to invoke tools silently just +// because the model invoked one. The broker asks; this relays the question to +// the renderer and the answer back. +const appApprovals = new Map void>() + +/** Answer one pending approval (from the renderer). */ +function resolveAppApproval(res: { requestId: string; allowed: boolean }): void { + const resolve = appApprovals.get(res.requestId) + if (!resolve) return + appApprovals.delete(res.requestId) + resolve(res.allowed) +} + +/** Ask the user; denies when there is no window to ask, or after two minutes. */ +function requestAppApproval(req: { + sessionId: string + serverId: string + toolName: string + args: unknown +}): Promise { + const win = BrowserWindow.getAllWindows().find((w) => !w.isDestroyed()) + if (!win) return Promise.resolve(false) + const requestId = randomUUID() + return new Promise((resolve) => { + const timer = setTimeout(() => { + appApprovals.delete(requestId) + // Unanswered is denied: a prompt that expires into a yes is one an + // attacker wins by waiting. + resolve(false) + }, 120_000) + timer.unref?.() + appApprovals.set(requestId, (allowed) => { + clearTimeout(timer) + resolve(allowed) + }) + win.webContents.send(CHANNELS.mcpAppApprovalRequest, { requestId, ...req }) + }) +} + /** Merge persisted MCP server rows with their live connection status for the UI. */ function listMcpServersWithStatus(): McpServerView[] { const statusById = new Map(mcpServerSummaries().map((s) => [s.id, s])) @@ -159,7 +223,10 @@ function listMcpServersWithStatus(): McpServerView[] { enabled: rec.enabled, status: live?.status ?? 'disabled', tools: live?.tools ?? [], - error: live?.error + error: live?.error, + era: live?.era, + // Only meaningful for remote servers; local ones do not authenticate. + signedIn: rec.config.type === 'remote' ? isMcpSignedIn(rec.id) : undefined } }) } @@ -354,7 +421,10 @@ export function registerIpc(): void { // ---- MCP servers (Phase 13) ---- ipcMain.handle(CHANNELS.mcpList, () => listMcpServersWithStatus()) ipcMain.handle(CHANNELS.mcpUpsert, (_e, input: UpsertMcpServerInput) => { - repo.upsertMcpServer(input) + // This channel is only reachable from Settings / the MCP page, i.e. a human + // typing a config. That IS the consent, so the row is marked 'user' and the + // gate will not re-ask for it. + repo.upsertMcpServer({ ...input, origin: 'user' }) return listMcpServersWithStatus() }) ipcMain.handle(CHANNELS.mcpRemove, async (_e, id: string) => { @@ -371,10 +441,73 @@ export function registerIpc(): void { }) ipcMain.handle(CHANNELS.mcpReconnect, async (_e, id: string) => { const rec = repo.listMcpServers().find((r) => r.id === id) - if (rec) await reconnectMcpServer(rec, app.getPath('home')) + if (rec) { + const candidate = { + record: rec, + provenance: (rec.origin === 'agent' ? 'agent' : 'user') as McpProvenance, + workspace: null + } + const { allowed, disclose } = await ensureMcpTrusted(candidate) + if (allowed) { + const summary = await reconnectMcpServer(rec, app.getPath('home')) + // Reconnecting a server the user has never been shown is still worth a + // receipt - it is the first time its tools become knowable. + if (disclose) discloseMcpConnected(candidate, summary.tools, summary.error) + } + } return listMcpServersWithStatus() }) + ipcMain.handle(CHANNELS.mcpSignIn, async (_e, id: string) => { + const rec = repo.listMcpServers().find((r) => r.id === id) + if (rec) await signInMcpServer(rec, app.getPath('home')) + return listMcpServersWithStatus() + }) + ipcMain.handle(CHANNELS.mcpSignOut, async (_e, id: string) => { + await signOutMcpServer(id) + return listMcpServersWithStatus() + }) + + // ---- MCP Apps (server-supplied UI in a sandboxed frame) ---- + // The renderer is a courier: it relays JSON-RPC frames from a view and + // mounts what comes back. It never holds an MCP client, so a compromised + // view (or renderer) still cannot reach a server directly. + setMcpAppApprover(requestAppApproval) + ipcMain.handle( + CHANNELS.mcpAppLaunch, + (_e, input: { serverId: string; toolName: string; resourceUri: string }) => + launchMcpApp(input.serverId, input.toolName, input.resourceUri) + ) + ipcMain.handle(CHANNELS.mcpAppRequest, (_e, req: BridgeRequest) => handleMcpAppRequest(req)) + ipcMain.handle(CHANNELS.mcpAppClose, (_e, sessionId: string) => closeMcpApp(sessionId)) + ipcMain.on(CHANNELS.mcpAppTheme, (_e, theme: unknown) => setMcpAppTheme(theme)) + ipcMain.on(CHANNELS.mcpAppApprovalRespond, (_e, res: { requestId: string; allowed: boolean }) => + resolveAppApproval(res) + ) + + // ---- MCP trust (consent before Roxy spawns a server) ---- + ipcMain.on(CHANNELS.mcpConsentRespond, (_e, response: McpConsentResponse) => { + resolveMcpConsent(response) + }) + ipcMain.handle(CHANNELS.mcpTrustList, () => { + const store = repo.getMcpTrustStore() + return { entries: store.entries, workspaces: store.workspaces, policy: mcpTrustPolicy() } + }) + ipcMain.handle( + CHANNELS.mcpTrustRevoke, + (_e, target: { kind: 'server'; id: string } | { kind: 'workspace'; path: string }) => { + if (target.kind === 'workspace') repo.untrustMcpWorkspace(target.path) + else repo.revokeMcpTrust(target.id) + const store = repo.getMcpTrustStore() + return { entries: store.entries, workspaces: store.workspaces, policy: mcpTrustPolicy() } + } + ) + ipcMain.handle(CHANNELS.mcpTrustGetPolicy, () => mcpTrustPolicy()) + ipcMain.handle(CHANNELS.mcpTrustSetPolicy, (_e, confirmBeforeRun: boolean) => { + setMcpConfirmBeforeRun(confirmBeforeRun) + return mcpTrustPolicy() + }) + // ---- skills (SKILL.md discovery) ---- ipcMain.handle(CHANNELS.skillsList, (_e, cwd?: string) => discoverSkillViews(cwd)) ipcMain.handle(CHANNELS.skillsRefresh, (_e, cwd?: string) => { diff --git a/src/main/services/mcp-app-sandbox.ts b/src/main/services/mcp-app-sandbox.ts new file mode 100644 index 0000000..14aaa39 --- /dev/null +++ b/src/main/services/mcp-app-sandbox.ts @@ -0,0 +1,267 @@ +/** + * The MCP Apps sandbox origin — where untrusted, server-supplied HTML runs. + * + * ## The problem + * + * An MCP App is arbitrary HTML+JS written by whoever wrote the MCP server. It + * has to execute for the feature to exist at all. The entire job of this module + * is making sure that when it executes, it is somewhere that can hurt nothing. + * + * Roxy's renderer runs with `contextIsolation: true` but `sandbox: false` and a + * preload that exposes `window.roxy` — the full IPC surface. An iframe on the + * app's own origin could reach that, plus `localStorage`, IndexedDB, and the + * session's cookie jar. So the view must not share Roxy's origin, and "must not" + * has to be enforced by the browser, not by our own care. + * + * ## The design + * + * A custom `roxy-mcp-app://` protocol, registered as a standard scheme, serves + * exactly one document: the sandbox proxy. Because it is a distinct scheme with + * its own dedicated origin, the same-origin policy does the enforcement for us — + * the proxy cannot touch Roxy's window, storage, or preload even if it tries. + * + * Inside it, the proxy writes the view's HTML into a NESTED iframe with the + * resource's CSP applied. That is the "double iframe" SEP-1865 requires: + * + * Roxy renderer ->