fix: harden windows npm runtime path

This commit is contained in:
Peter Steinberger 2026-03-12 22:03:10 +00:00
parent 92191fcd68
commit 86a3149b2e
No known key found for this signature in database
7 changed files with 81 additions and 8 deletions

1
.npmignore Normal file
View File

@ -0,0 +1 @@
**/node_modules/

1
extensions/.npmignore Normal file
View File

@ -0,0 +1 @@
**/node_modules/

View File

@ -218,6 +218,16 @@ function runPackDry(): PackResult[] {
return JSON.parse(raw) as PackResult[];
}
export function collectForbiddenPackPaths(paths: Iterable<string>): string[] {
return [...paths]
.filter(
(path) =>
forbiddenPrefixes.some((prefix) => path.startsWith(prefix)) ||
/(^|\/)node_modules\//.test(path),
)
.toSorted();
}
function checkPluginVersions() {
const rootPackagePath = resolve("package.json");
const rootPackage = JSON.parse(readFileSync(rootPackagePath, "utf8")) as PackageJson;
@ -422,9 +432,7 @@ function main() {
return paths.has(group) ? [] : [group];
})
.toSorted();
const forbidden = [...paths].filter((path) =>
forbiddenPrefixes.some((prefix) => path.startsWith(prefix)),
);
const forbidden = collectForbiddenPackPaths(paths);
if (missing.length > 0 || forbidden.length > 0) {
if (missing.length > 0) {

View File

@ -36,6 +36,7 @@ const {
resolveDiscordAllowlistConfigMock,
resolveNativeCommandsEnabledMock,
resolveNativeSkillsEnabledMock,
voiceRuntimeModuleLoadedMock,
} = vi.hoisted(() => {
const createdBindingManagers: Array<{ stop: ReturnType<typeof vi.fn> }> = [];
return {
@ -103,6 +104,7 @@ const {
})),
resolveNativeCommandsEnabledMock: vi.fn(() => true),
resolveNativeSkillsEnabledMock: vi.fn(() => false),
voiceRuntimeModuleLoadedMock: vi.fn(),
};
});
@ -210,10 +212,13 @@ vi.mock("../voice/command.js", () => ({
createDiscordVoiceCommand: () => ({ name: "voice-command" }),
}));
vi.mock("../voice/manager.js", () => ({
vi.mock("../voice/manager.runtime.js", () => {
voiceRuntimeModuleLoadedMock();
return {
DiscordVoiceManager: class DiscordVoiceManager {},
DiscordVoiceReadyListener: class DiscordVoiceReadyListener {},
}));
};
});
vi.mock("./agent-components.js", () => ({
createAgentComponentButton: () => ({ id: "btn" }),
@ -390,6 +395,7 @@ describe("monitorDiscordProvider", () => {
});
resolveNativeCommandsEnabledMock.mockClear().mockReturnValue(true);
resolveNativeSkillsEnabledMock.mockClear().mockReturnValue(false);
voiceRuntimeModuleLoadedMock.mockClear();
});
it("stops thread bindings when startup fails before lifecycle begins", async () => {
@ -424,6 +430,38 @@ describe("monitorDiscordProvider", () => {
expect(reconcileAcpThreadBindingsOnStartupMock).toHaveBeenCalledTimes(1);
});
it("does not load the Discord voice runtime when voice is disabled", async () => {
const { monitorDiscordProvider } = await import("./provider.js");
await monitorDiscordProvider({
config: baseConfig(),
runtime: baseRuntime(),
});
expect(voiceRuntimeModuleLoadedMock).not.toHaveBeenCalled();
});
it("loads the Discord voice runtime only when voice is enabled", async () => {
resolveDiscordAccountMock.mockReturnValue({
accountId: "default",
token: "cfg-token",
config: {
commands: { native: true, nativeSkills: false },
voice: { enabled: true },
agentComponents: { enabled: false },
execApprovals: { enabled: false },
},
});
const { monitorDiscordProvider } = await import("./provider.js");
await monitorDiscordProvider({
config: baseConfig(),
runtime: baseRuntime(),
});
expect(voiceRuntimeModuleLoadedMock).toHaveBeenCalledTimes(1);
});
it("treats ACP error status as uncertain during startup thread-binding probes", async () => {
const { monitorDiscordProvider } = await import("./provider.js");
getAcpSessionStatusMock.mockResolvedValue({ state: "error" });

View File

@ -48,7 +48,6 @@ import { resolveDiscordAccount } from "../accounts.js";
import { fetchDiscordApplicationId } from "../probe.js";
import { normalizeDiscordToken } from "../token.js";
import { createDiscordVoiceCommand } from "../voice/command.js";
import { DiscordVoiceManager, DiscordVoiceReadyListener } from "../voice/manager.js";
import {
createAgentComponentButton,
createAgentSelectMenu,
@ -104,6 +103,17 @@ export type MonitorDiscordOpts = {
setStatus?: DiscordMonitorStatusSink;
};
type DiscordVoiceManager = import("../voice/manager.js").DiscordVoiceManager;
type DiscordVoiceRuntimeModule = typeof import("../voice/manager.runtime.js");
let discordVoiceRuntimePromise: Promise<DiscordVoiceRuntimeModule> | undefined;
async function loadDiscordVoiceRuntime(): Promise<DiscordVoiceRuntimeModule> {
discordVoiceRuntimePromise ??= import("../voice/manager.runtime.js");
return await discordVoiceRuntimePromise;
}
function formatThreadBindingDurationForConfigLabel(durationMs: number): string {
const label = formatThreadBindingDurationLabel(durationMs);
return label === "disabled" ? "off" : label;
@ -663,6 +673,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
}
if (voiceEnabled) {
const { DiscordVoiceManager, DiscordVoiceReadyListener } = await loadDiscordVoiceRuntime();
voiceManager = new DiscordVoiceManager({
client,
cfg,

View File

@ -0,0 +1 @@
export { DiscordVoiceManager, DiscordVoiceReadyListener } from "./manager.js";

View File

@ -3,6 +3,7 @@ import {
collectAppcastSparkleVersionErrors,
collectBundledExtensionManifestErrors,
collectBundledExtensionRootDependencyGapErrors,
collectForbiddenPackPaths,
} from "../scripts/release-check.ts";
function makeItem(shortVersion: string, sparkleVersion: string): string {
@ -150,3 +151,15 @@ describe("collectBundledExtensionManifestErrors", () => {
]);
});
});
describe("collectForbiddenPackPaths", () => {
it("flags nested node_modules leaking into npm pack output", () => {
expect(
collectForbiddenPackPaths([
"dist/index.js",
"extensions/tlon/node_modules/.bin/tlon",
"node_modules/.bin/openclaw",
]),
).toEqual(["extensions/tlon/node_modules/.bin/tlon", "node_modules/.bin/openclaw"]);
});
});