Harden dependency floors and update the Vitest toolchain to patched releases. - upgrade workspace vitest and @vitest/coverage-v8 dependencies to the 4.1 line across packages and plugins - pin transitive protobufjs via pnpm overrides and lockfile updates to patched versions - adapt Vitest configs, engine test helpers, and security-floor coverage for the new dependency baselines - add the published CLI changeset and related workspace/package metadata updates included in the task branch Files changed: .changeset/fn-6042-security-dependencies.md | 5 + AGENTS.md | 4 + Dockerfile | 3 + docs/PLUGIN_AUTHORING.md | 2 - package.json | 3 +- packages/cli/package.json | 4 +- packages/cli/vitest.config.ts | 2 +- packages/core/package.json | 4 +- packages/core/vitest.config.ts | 2 +- packages/dashboard/app/test/mockApi.ts | 4 +- packages/dashboard/package.json | 4 +- packages/dashboard/vitest.config.ts | 2 +- packages/desktop/package.json | 4 +- packages/desktop/vitest.config.ts | 2 +- packages/droid-cli/package.json | 2 +- packages/droid-cli/vitest.config.ts | 2 +- packages/engine/package.json | 4 +- .../engine/src/__tests__/executor-test-helpers.ts | 29 +- .../engine/src/__tests__/gridlock-detector.test.ts | 5 +- .../src/__tests__/heartbeat-scheduler.test.ts | 3 +- packages/engine/src/__tests__/scheduler.test.ts | 24 +- packages/engine/src/__tests__/self-healing.test.ts | 5 + packages/engine/tsconfig.json | 3 +- packages/engine/vitest.config.ts | 10 +- packages/i18n/package.json | 2 +- packages/i18n/vitest.config.ts | 7 + packages/mobile/package.json | 2 +- packages/mobile/vitest.config.ts | 2 +- packages/pi-claude-cli/package.json | 2 +- packages/pi-claude-cli/vitest.config.ts | 2 +- packages/pi-llama-cpp/package.json | 2 +- packages/pi-llama-cpp/vitest.config.ts | 2 +- packages/plugin-sdk/package.json | 2 +- packages/plugin-sdk/vitest.config.ts | 2 +- .../examples/fusion-plugin-auto-label/package.json | 2 +- .../fusion-plugin-auto-label/vitest.config.ts | 2 +- .../examples/fusion-plugin-ci-status/package.json | 2 +- .../fusion-plugin-ci-status/vitest.config.ts | 2 +- .../fusion-plugin-notification/package.json | 2 +- .../fusion-plugin-notification/vitest.config.ts | 2 +- .../fusion-plugin-settings-demo/package.json | 2 +- .../fusion-plugin-settings-demo/vitest.config.ts | 2 +- plugins/fusion-plugin-acp-runtime/package.json | 2 +- plugins/fusion-plugin-acp-runtime/vitest.config.ts | 2 +- plugins/fusion-plugin-agent-browser/package.json | 2 +- .../fusion-plugin-agent-browser/vitest.config.ts | 2 +- .../fusion-plugin-cli-printing-press/package.json | 2 +- .../vitest.config.ts | 2 +- .../package.json | 2 +- .../src/__tests__/orchestrator-live-output.test.ts | 4 +- .../vitest.config.ts | 2 +- plugins/fusion-plugin-cursor-runtime/package.json | 4 +- .../fusion-plugin-dependency-graph/package.json | 4 +- .../vitest.config.ts | 2 +- plugins/fusion-plugin-droid-runtime/package.json | 2 +- .../fusion-plugin-droid-runtime/vitest.config.ts | 2 +- plugins/fusion-plugin-even-cards/package.json | 2 +- plugins/fusion-plugin-even-cards/vitest.config.ts | 2 +- .../package.json | 2 +- .../vitest.config.ts | 2 +- plugins/fusion-plugin-hermes-runtime/package.json | 2 +- .../fusion-plugin-hermes-runtime/vitest.config.ts | 2 +- .../fusion-plugin-openclaw-runtime/package.json | 2 +- .../vitest.config.ts | 2 +- .../fusion-plugin-paperclip-runtime/package.json | 2 +- .../vitest.config.ts | 2 +- plugins/fusion-plugin-reports/package.json | 2 +- .../src/__tests__/review-panel.test.ts | 6 +- plugins/fusion-plugin-reports/vitest.config.ts | 2 +- plugins/fusion-plugin-roadmap/package.json | 2 +- plugins/fusion-plugin-roadmap/vitest.config.ts | 2 +- plugins/fusion-plugin-whatsapp-chat/package.json | 2 +- .../fusion-plugin-whatsapp-chat/vitest.config.ts | 2 +- pnpm-lock.yaml | 626 ++++++++------------- .../__tests__/dependency-security-floor.test.mjs | 95 ++++ 75 files changed, 475 insertions(+), 491 deletions(-) Fusion-Task-Id: FN-6042 Fusion-Task-Lineage: fff6a1cb-8937-435c-9a91-b7c7a59cc80e
96 lines
4.2 KiB
JavaScript
96 lines
4.2 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readdir, readFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
import YAML from "yaml";
|
|
|
|
const root = process.cwd();
|
|
|
|
const dependencyFloors = [
|
|
{ name: "protobufjs", minimum: "7.5.5", manifestSections: ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"], lockfileMatcher: /^protobufjs@(.*)$/ },
|
|
{ name: "vitest", minimum: "4.1.0", manifestSections: ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"], lockfileMatcher: /^vitest@(.*)$/ },
|
|
{ name: "@vitest/coverage-v8", minimum: "4.1.0", manifestSections: ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"], lockfileMatcher: /^@vitest\/coverage-v8@(.*)$/ },
|
|
];
|
|
|
|
const manifestRoots = ["packages", "plugins"];
|
|
const ignoredPathParts = new Set(["node_modules", "dist", "build", "coverage", ".turbo"]);
|
|
|
|
function compareVersions(a, b) {
|
|
const pa = a.split(".").map((part) => Number.parseInt(part, 10));
|
|
const pb = b.split(".").map((part) => Number.parseInt(part, 10));
|
|
for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
|
|
const ai = Number.isFinite(pa[i]) ? pa[i] : 0;
|
|
const bi = Number.isFinite(pb[i]) ? pb[i] : 0;
|
|
if (ai !== bi) return ai - bi;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function minVersionFromRange(range) {
|
|
const version = String(range).match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/u)?.[0];
|
|
return version?.split(/[+-]/u)[0] ?? null;
|
|
}
|
|
|
|
function assertRangeMeetsFloor({ location, name, range, minimum }) {
|
|
const minVersion = minVersionFromRange(range);
|
|
assert.ok(minVersion, `${location}: ${name} range ${range} must include an explicit semver version`);
|
|
assert.ok(compareVersions(minVersion, minimum) >= 0, `${location}: ${name} range ${range} is below required floor ${minimum}`);
|
|
}
|
|
|
|
async function collectPackageManifests(dir) {
|
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
const manifests = [];
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dir, entry.name);
|
|
if (ignoredPathParts.has(entry.name)) continue;
|
|
if (entry.isDirectory()) {
|
|
manifests.push(...(await collectPackageManifests(fullPath)));
|
|
} else if (entry.name === "package.json") {
|
|
manifests.push(fullPath);
|
|
}
|
|
}
|
|
return manifests;
|
|
}
|
|
|
|
async function sourceManifestPaths() {
|
|
const manifests = [path.join(root, "package.json")];
|
|
for (const manifestRoot of manifestRoots) {
|
|
manifests.push(...(await collectPackageManifests(path.join(root, manifestRoot))));
|
|
}
|
|
return manifests.sort();
|
|
}
|
|
|
|
test("source manifests keep vulnerable dependency floors out", async () => {
|
|
for (const manifestPath of await sourceManifestPaths()) {
|
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
const relativePath = path.relative(root, manifestPath);
|
|
for (const floor of dependencyFloors) {
|
|
for (const section of floor.manifestSections) {
|
|
const range = manifest[section]?.[floor.name];
|
|
if (range) assertRangeMeetsFloor({ location: `${relativePath} ${section}`, name: floor.name, range, minimum: floor.minimum });
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test("pnpm overrides pin transitive protobufjs to a safe floor", async () => {
|
|
const manifest = JSON.parse(await readFile(path.join(root, "package.json"), "utf8"));
|
|
assertRangeMeetsFloor({ location: "package.json pnpm.overrides", name: "protobufjs", range: manifest.pnpm?.overrides?.protobufjs, minimum: "7.5.5" });
|
|
});
|
|
|
|
test("lockfile resolutions satisfy dependency security floors", async () => {
|
|
const lockfile = YAML.parse(await readFile(path.join(root, "pnpm-lock.yaml"), "utf8"));
|
|
const packageKeys = Object.keys(lockfile.packages ?? {});
|
|
for (const floor of dependencyFloors) {
|
|
const matches = packageKeys
|
|
.map((key) => key.replace(/^\//u, ""))
|
|
.map((key) => key.match(floor.lockfileMatcher)?.[1])
|
|
.filter(Boolean)
|
|
.map((suffix) => suffix.split("(")[0]);
|
|
assert.ok(matches.length > 0, `pnpm-lock.yaml must include at least one ${floor.name} resolution`);
|
|
for (const version of matches) {
|
|
assert.ok(compareVersions(version, floor.minimum) >= 0, `pnpm-lock.yaml resolves ${floor.name}@${version}, below required floor ${floor.minimum}`);
|
|
}
|
|
}
|
|
});
|