diff --git a/ATTRIBUTIONS-Node.md b/ATTRIBUTIONS-Node.md index c22dc30aa..67197561b 100644 --- a/ATTRIBUTIONS-Node.md +++ b/ATTRIBUTIONS-Node.md @@ -25777,7 +25777,7 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ``` -## body-parser - 2.3.0 +## body-parser - 2.4.0 **Repository URL**: https://github.com/expressjs/body-parser **License Type(s)**: MIT @@ -35192,7 +35192,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -## qs - 6.16.0 +## qs - 6.16.1 **Repository URL**: https://github.com/ljharb/qs **License Type(s)**: BSD-3-Clause @@ -37490,7 +37490,7 @@ If the Work includes a "NOTICE" text file as part of its distribution, then any END OF TERMS AND CONDITIONS ``` -## undici - 6.29.0 +## undici - 6.29.1 **Repository URL**: https://github.com/nodejs/undici **License Type(s)**: MIT @@ -37552,7 +37552,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -## undici - 8.11.2 +## undici - 8.12.3 **Repository URL**: https://github.com/nodejs/undici **License Type(s)**: MIT @@ -38324,7 +38324,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -## zod - 4.6.5 +## zod - 4.6.6 **Repository URL**: https://github.com/colinhacks/zod **License Type(s)**: MIT diff --git a/adapters/typescript/common/package.json b/adapters/typescript/common/package.json index 0aedac7c2..b4c77bc68 100644 --- a/adapters/typescript/common/package.json +++ b/adapters/typescript/common/package.json @@ -46,7 +46,7 @@ "@agentclientprotocol/sdk": "1.5.1", "ajv": "8.20.0", "nemo-fabric-adapter-contract": "0.6.0", - "zod": "4.1.8" + "zod": "4.6.6" }, "devDependencies": { "@types/node": "22.19.19", diff --git a/adapters/typescript/common/test/validation-limits.test.mjs b/adapters/typescript/common/test/validation-limits.test.mjs new file mode 100644 index 000000000..f1d352449 --- /dev/null +++ b/adapters/typescript/common/test/validation-limits.test.mjs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert/strict"; +import test from "node:test"; +import { z } from "zod"; + +test("rejects oversized constrained arrays before accumulating element errors", () => { + const result = z.array(z.string()).max(1).safeParse([1, 2, 3, 4]); + assert.equal(result.success, false); + assert.equal(result.error.issues.length, 1); + assert.equal(result.error.issues[0].code, "too_big"); +}); diff --git a/adapters/typescript/package-lock.json b/adapters/typescript/package-lock.json index d4780bfb0..5e2421de8 100644 --- a/adapters/typescript/package-lock.json +++ b/adapters/typescript/package-lock.json @@ -95,7 +95,7 @@ "@agentclientprotocol/sdk": "1.5.1", "ajv": "8.20.0", "nemo-fabric-adapter-contract": "0.6.0", - "zod": "4.1.8" + "zod": "4.6.6" }, "devDependencies": { "@types/node": "22.19.19", @@ -105,6 +105,15 @@ "node": ">=20.18.3" } }, + "common/node_modules/zod": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.6.tgz", + "integrity": "sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "droid": { "name": "nemo-fabric-adapters-droid", "version": "0.6.0", @@ -2248,9 +2257,9 @@ } }, "node_modules/@modelcontextprotocol/client/node_modules/zod": { - "version": "4.6.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", - "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.6.tgz", + "integrity": "sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==", "dev": true, "license": "MIT", "funding": { @@ -2271,9 +2280,9 @@ } }, "node_modules/@modelcontextprotocol/core/node_modules/zod": { - "version": "4.6.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", - "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.6.tgz", + "integrity": "sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==", "dev": true, "license": "MIT", "funding": { @@ -4601,19 +4610,19 @@ } }, "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==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.4.0.tgz", + "integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==", "dev": true, "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^2.0.0", + "content-type": "^2.1.0", "debug": "^4.4.3", "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", + "iconv-lite": "^0.7.3", "on-finished": "^2.4.1", - "qs": "^6.15.2", + "qs": "^6.16.1", "raw-body": "^3.0.2", "type-is": "^2.1.0" }, @@ -7138,9 +7147,9 @@ } }, "node_modules/node-gyp/node_modules/undici": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz", - "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==", + "version": "6.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.1.tgz", + "integrity": "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==", "dev": true, "license": "MIT", "engines": { @@ -7677,9 +7686,9 @@ "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==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz", + "integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -8440,9 +8449,9 @@ } }, "node_modules/undici": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz", - "integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==", + "version": "8.12.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.3.tgz", + "integrity": "sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==", "dev": true, "license": "MIT", "engines": { diff --git a/adapters/typescript/package.json b/adapters/typescript/package.json index 961b78917..39d609282 100644 --- a/adapters/typescript/package.json +++ b/adapters/typescript/package.json @@ -28,8 +28,14 @@ }, "overrides": { "@opencode/core": { - "@modelcontextprotocol/client": "2.2.0", - "@modelcontextprotocol/core": "2.2.0" + "@modelcontextprotocol/client": { + ".": "2.2.0", + "zod": "4.6.6" + }, + "@modelcontextprotocol/core": { + ".": "2.2.0", + "zod": "4.6.6" + } }, "@parcel/watcher": "2.6.0" }, diff --git a/adapters/typescript/scripts/audit-dependencies.mjs b/adapters/typescript/scripts/audit-dependencies.mjs index 0fcd2ad34..39d132c6e 100644 --- a/adapters/typescript/scripts/audit-dependencies.mjs +++ b/adapters/typescript/scripts/audit-dependencies.mjs @@ -2,12 +2,32 @@ // SPDX-License-Identifier: Apache-2.0 import { spawnSync } from "node:child_process"; -import { dirname } from "node:path"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const packageRoot = dirname(dirname(fileURLToPath(import.meta.url))); -export function checkAudit(report) { +// Temporary CI exception; removal and exposure assessment: NVIDIA/NeMo-Fabric#485. +// Only this advisory and these installed versions are excepted. Never force Zod 4 +// into SDKs that require Zod 3. Compatible copies are patched separately. +const zodAdvisory = "https://github.com/advisories/GHSA-mrhx-c7xc-m3w5"; +const allowedPackages = { + "@factory/droid-sdk": { "node_modules/@factory/droid-sdk": "0.9.1" }, + "@qwen-code/sdk": { "node_modules/@qwen-code/sdk": "0.1.16" }, + "gitlab-ai-provider": { "node_modules/gitlab-ai-provider": "6.18.0" }, + zod: { + "node_modules/@factory/droid-sdk/node_modules/zod": "3.25.76", + "node_modules/@qwen-code/sdk/node_modules/zod": "3.25.76", + "node_modules/gitlab-ai-provider/node_modules/zod": "3.25.76", + "node_modules/zod": "4.1.8", + }, +}; +for (const name of ["core", "plugin", "plugin-browser", "sdk", "server", "simulation"]) { + allowedPackages[`@opencode/${name}`] = { [`node_modules/@opencode/${name}`]: "2.0.23" }; +} + +export function checkAudit(report, lockfile = {}) { if ( report.error || !report.vulnerabilities || @@ -15,15 +35,59 @@ export function checkAudit(report) { ) { throw new Error("npm audit did not return a complete vulnerability report"); } - const severe = Object.values(report.vulnerabilities).filter( - (finding) => finding.severity === "high" || finding.severity === "critical", + const severe = Object.entries(report.vulnerabilities).filter( + ([, finding]) => finding.severity === "high" || finding.severity === "critical", ); if ( - severe.length !== 0 || - report.metadata.vulnerabilities.high !== 0 || - report.metadata.vulnerabilities.critical !== 0 + report.metadata.vulnerabilities.critical !== 0 || + report.metadata.vulnerabilities.high !== severe.length || + severe.some(([, finding]) => finding.severity === "critical") ) { - throw new Error("npm audit reported a high- or critical-severity finding"); + throw new Error("Unexpected high/critical audit counts or critical finding"); + } + const severeNames = new Set(severe.map(([name]) => name)); + // npm reports parent-package metavulnerabilities, including cyclic OpenCode + // dependencies. Each severe package must trace to the approved Zod cause. + function reachesZod(name, visiting = new Set()) { + if (name === "zod") return true; + if (visiting.has(name)) return false; + const next = new Set(visiting).add(name); + return report.vulnerabilities[name].via.some( + (cause) => typeof cause === "string" && severeNames.has(cause) && reachesZod(cause, next), + ); + } + for (const [name, finding] of severe) { + const allowedNodes = allowedPackages[name]; + if ( + finding.name !== name || + !allowedNodes || + !Array.isArray(finding.nodes) || + finding.nodes.length === 0 || + !Array.isArray(finding.via) || + finding.via.length === 0 || + finding.nodes.some((path) => !Object.hasOwn(allowedNodes, path) || + lockfile.packages?.[path]?.version !== allowedNodes[path]) + ) { + throw new Error(`Unapproved audit package, path or version: ${name}`); + } + for (const cause of finding.via) { + if (typeof cause === "string") { + if (!report.vulnerabilities[cause] || (name === "zod")) { + throw new Error(`Unrecognized audit dependency: ${name} -> ${cause}`); + } + } else if ( + cause.severity === "critical" || + (cause.severity === "high" && (name !== "zod" || cause.name !== "zod" || + cause.url !== zodAdvisory || cause.range !== "<4.6.6")) + ) { + throw new Error(`Advisory outside the temporary Zod exception: ${name}`); + } + } + if (!reachesZod(name) || (name === "zod" && !finding.via.some( + (cause) => cause.severity === "high" && cause.url === zodAdvisory, + ))) { + throw new Error(`High-severity finding has no approved Zod cause: ${name}`); + } } return report.metadata.vulnerabilities; } @@ -47,9 +111,11 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) { } catch { throw new Error(`npm audit failed without JSON output: ${audit.stderr}`); } - const counts = checkAudit(report); + const lockfile = JSON.parse(readFileSync(join(packageRoot, "package-lock.json"), "utf8")); + const counts = checkAudit(report, lockfile); console.log( - `npm audit: ${counts.high} high and ${counts.critical} critical; ` + - `${counts.moderate} moderate and ${counts.low} low findings remain.`, + `npm audit: ${counts.high} high covered by temporary Zod CI exception (#485); ` + + `${counts.critical} critical; ` + + `${counts.moderate} moderate and ${counts.low} low findings remain.`, ); } diff --git a/adapters/typescript/scripts/audit-dependencies.test.mjs b/adapters/typescript/scripts/audit-dependencies.test.mjs index 583586e4c..45feebc83 100644 --- a/adapters/typescript/scripts/audit-dependencies.test.mjs +++ b/adapters/typescript/scripts/audit-dependencies.test.mjs @@ -36,3 +36,83 @@ test("rejects a severe finding omitted from the summary counts", () => { changed.vulnerabilities.other = { name: "other", severity: "high" }; assert.throws(() => checkAudit(changed)); }); + +const advisory = { + name: "zod", + severity: "high", + url: "https://github.com/advisories/GHSA-mrhx-c7xc-m3w5", + range: "<4.6.6", +}; +const zodPath = "node_modules/@factory/droid-sdk/node_modules/zod"; +const exceptionLock = { + packages: { + [zodPath]: { version: "3.25.76" }, + "node_modules/@factory/droid-sdk": { version: "0.9.1" }, + "node_modules/@opencode/core": { version: "2.0.23" }, + "node_modules/@opencode/plugin": { version: "2.0.23" }, + }, +}; +function exceptionReport() { + const findings = { + zod: { name: "zod", severity: "high", nodes: [zodPath], via: [advisory] }, + "@factory/droid-sdk": { + name: "@factory/droid-sdk", severity: "high", + nodes: ["node_modules/@factory/droid-sdk"], via: ["zod", "uuid"], + }, + uuid: { name: "uuid", severity: "moderate", via: [] }, + "@opencode/core": { + name: "@opencode/core", severity: "high", + nodes: ["node_modules/@opencode/core"], via: ["@opencode/plugin", "zod"], + }, + "@opencode/plugin": { + name: "@opencode/plugin", severity: "high", + nodes: ["node_modules/@opencode/plugin"], via: ["@opencode/core"], + }, + }; + return structuredClone({ + vulnerabilities: findings, + metadata: { vulnerabilities: { high: 4, critical: 0, moderate: 1, low: 0 } }, + }); +} + +test("allows only the known Zod advisory and propagated SDK findings, including cycles", () => { + assert.equal(checkAudit(exceptionReport(), exceptionLock).high, 4); +}); + +test("allows removal of a patched dependency from the audit report", () => { + const changed = exceptionReport(); + delete changed.vulnerabilities["@factory/droid-sdk"]; + changed.metadata.vulnerabilities.high--; + assert.equal(checkAudit(changed, exceptionLock).high, 3); +}); + +for (const [description, mutate] of [ + ["a second high advisory on Zod", (r) => r.vulnerabilities.zod.via.push({ ...advisory, url: "https://github.com/advisories/OTHER" })], + ["a new high advisory on an approved parent", (r) => r.vulnerabilities["@opencode/core"].via.push({ ...advisory })], + ["a changed advisory range", (r) => { r.vulnerabilities.zod.via[0].range = "*"; }], + ["an additional Zod install path", (r) => r.vulnerabilities.zod.nodes.push("node_modules/other/node_modules/zod")], + ["an additional SDK install path", (r) => r.vulnerabilities["@factory/droid-sdk"].nodes.push("other")], + ["a critical finding", (r) => { r.vulnerabilities.zod.severity = "critical"; r.metadata.vulnerabilities.high--; r.metadata.vulnerabilities.critical++; }], + ["an unrelated high package", (r) => { r.vulnerabilities.other = { name: "other", severity: "high", nodes: ["node_modules/other"], via: ["zod"] }; r.metadata.vulnerabilities.high++; }], + ["an unresolved dependency reference", (r) => r.vulnerabilities["@factory/droid-sdk"].via.push("missing")], + ["a cycle without an approved cause", (r) => { r.vulnerabilities["@opencode/core"].via = ["@opencode/plugin"]; }], + ["a missing Zod advisory", (r) => { r.vulnerabilities.zod.via = []; }], + ["an audit service error", (r) => { r.error = { code: "E503" }; }], + ["an inconsistent high count", (r) => { r.metadata.vulnerabilities.high--; }], +]) { + test(`rejects ${description}`, () => { + const changed = exceptionReport(); + mutate(changed); + assert.throws(() => checkAudit(changed, exceptionLock)); + }); +} + +for (const path of [zodPath, "node_modules/@factory/droid-sdk"]) { + test(`rejects a changed or missing locked version at ${path}`, () => { + const changed = structuredClone(exceptionLock); + changed.packages[path].version = "99.0.0"; + assert.throws(() => checkAudit(exceptionReport(), changed)); + delete changed.packages[path]; + assert.throws(() => checkAudit(exceptionReport(), changed)); + }); +}