Skip to content

Commit 86a3149

Browse files
committed
fix: harden windows npm runtime path
1 parent 92191fc commit 86a3149

7 files changed

Lines changed: 81 additions & 8 deletions

File tree

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
**/node_modules/

extensions/.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
**/node_modules/

scripts/release-check.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,16 @@ function runPackDry(): PackResult[] {
218218
return JSON.parse(raw) as PackResult[];
219219
}
220220

221+
export function collectForbiddenPackPaths(paths: Iterable<string>): string[] {
222+
return [...paths]
223+
.filter(
224+
(path) =>
225+
forbiddenPrefixes.some((prefix) => path.startsWith(prefix)) ||
226+
/(^|\/)node_modules\//.test(path),
227+
)
228+
.toSorted();
229+
}
230+
221231
function checkPluginVersions() {
222232
const rootPackagePath = resolve("package.json");
223233
const rootPackage = JSON.parse(readFileSync(rootPackagePath, "utf8")) as PackageJson;
@@ -422,9 +432,7 @@ function main() {
422432
return paths.has(group) ? [] : [group];
423433
})
424434
.toSorted();
425-
const forbidden = [...paths].filter((path) =>
426-
forbiddenPrefixes.some((prefix) => path.startsWith(prefix)),
427-
);
435+
const forbidden = collectForbiddenPackPaths(paths);
428436

429437
if (missing.length > 0 || forbidden.length > 0) {
430438
if (missing.length > 0) {

src/discord/monitor/provider.test.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const {
3636
resolveDiscordAllowlistConfigMock,
3737
resolveNativeCommandsEnabledMock,
3838
resolveNativeSkillsEnabledMock,
39+
voiceRuntimeModuleLoadedMock,
3940
} = vi.hoisted(() => {
4041
const createdBindingManagers: Array<{ stop: ReturnType<typeof vi.fn> }> = [];
4142
return {
@@ -103,6 +104,7 @@ const {
103104
})),
104105
resolveNativeCommandsEnabledMock: vi.fn(() => true),
105106
resolveNativeSkillsEnabledMock: vi.fn(() => false),
107+
voiceRuntimeModuleLoadedMock: vi.fn(),
106108
};
107109
});
108110

@@ -210,10 +212,13 @@ vi.mock("../voice/command.js", () => ({
210212
createDiscordVoiceCommand: () => ({ name: "voice-command" }),
211213
}));
212214

213-
vi.mock("../voice/manager.js", () => ({
214-
DiscordVoiceManager: class DiscordVoiceManager {},
215-
DiscordVoiceReadyListener: class DiscordVoiceReadyListener {},
216-
}));
215+
vi.mock("../voice/manager.runtime.js", () => {
216+
voiceRuntimeModuleLoadedMock();
217+
return {
218+
DiscordVoiceManager: class DiscordVoiceManager {},
219+
DiscordVoiceReadyListener: class DiscordVoiceReadyListener {},
220+
};
221+
});
217222

218223
vi.mock("./agent-components.js", () => ({
219224
createAgentComponentButton: () => ({ id: "btn" }),
@@ -390,6 +395,7 @@ describe("monitorDiscordProvider", () => {
390395
});
391396
resolveNativeCommandsEnabledMock.mockClear().mockReturnValue(true);
392397
resolveNativeSkillsEnabledMock.mockClear().mockReturnValue(false);
398+
voiceRuntimeModuleLoadedMock.mockClear();
393399
});
394400

395401
it("stops thread bindings when startup fails before lifecycle begins", async () => {
@@ -424,6 +430,38 @@ describe("monitorDiscordProvider", () => {
424430
expect(reconcileAcpThreadBindingsOnStartupMock).toHaveBeenCalledTimes(1);
425431
});
426432

433+
it("does not load the Discord voice runtime when voice is disabled", async () => {
434+
const { monitorDiscordProvider } = await import("./provider.js");
435+
436+
await monitorDiscordProvider({
437+
config: baseConfig(),
438+
runtime: baseRuntime(),
439+
});
440+
441+
expect(voiceRuntimeModuleLoadedMock).not.toHaveBeenCalled();
442+
});
443+
444+
it("loads the Discord voice runtime only when voice is enabled", async () => {
445+
resolveDiscordAccountMock.mockReturnValue({
446+
accountId: "default",
447+
token: "cfg-token",
448+
config: {
449+
commands: { native: true, nativeSkills: false },
450+
voice: { enabled: true },
451+
agentComponents: { enabled: false },
452+
execApprovals: { enabled: false },
453+
},
454+
});
455+
const { monitorDiscordProvider } = await import("./provider.js");
456+
457+
await monitorDiscordProvider({
458+
config: baseConfig(),
459+
runtime: baseRuntime(),
460+
});
461+
462+
expect(voiceRuntimeModuleLoadedMock).toHaveBeenCalledTimes(1);
463+
});
464+
427465
it("treats ACP error status as uncertain during startup thread-binding probes", async () => {
428466
const { monitorDiscordProvider } = await import("./provider.js");
429467
getAcpSessionStatusMock.mockResolvedValue({ state: "error" });

src/discord/monitor/provider.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ import { resolveDiscordAccount } from "../accounts.js";
4848
import { fetchDiscordApplicationId } from "../probe.js";
4949
import { normalizeDiscordToken } from "../token.js";
5050
import { createDiscordVoiceCommand } from "../voice/command.js";
51-
import { DiscordVoiceManager, DiscordVoiceReadyListener } from "../voice/manager.js";
5251
import {
5352
createAgentComponentButton,
5453
createAgentSelectMenu,
@@ -104,6 +103,17 @@ export type MonitorDiscordOpts = {
104103
setStatus?: DiscordMonitorStatusSink;
105104
};
106105

106+
type DiscordVoiceManager = import("../voice/manager.js").DiscordVoiceManager;
107+
108+
type DiscordVoiceRuntimeModule = typeof import("../voice/manager.runtime.js");
109+
110+
let discordVoiceRuntimePromise: Promise<DiscordVoiceRuntimeModule> | undefined;
111+
112+
async function loadDiscordVoiceRuntime(): Promise<DiscordVoiceRuntimeModule> {
113+
discordVoiceRuntimePromise ??= import("../voice/manager.runtime.js");
114+
return await discordVoiceRuntimePromise;
115+
}
116+
107117
function formatThreadBindingDurationForConfigLabel(durationMs: number): string {
108118
const label = formatThreadBindingDurationLabel(durationMs);
109119
return label === "disabled" ? "off" : label;
@@ -663,6 +673,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
663673
}
664674

665675
if (voiceEnabled) {
676+
const { DiscordVoiceManager, DiscordVoiceReadyListener } = await loadDiscordVoiceRuntime();
666677
voiceManager = new DiscordVoiceManager({
667678
client,
668679
cfg,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { DiscordVoiceManager, DiscordVoiceReadyListener } from "./manager.js";

test/release-check.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
collectAppcastSparkleVersionErrors,
44
collectBundledExtensionManifestErrors,
55
collectBundledExtensionRootDependencyGapErrors,
6+
collectForbiddenPackPaths,
67
} from "../scripts/release-check.ts";
78

89
function makeItem(shortVersion: string, sparkleVersion: string): string {
@@ -150,3 +151,15 @@ describe("collectBundledExtensionManifestErrors", () => {
150151
]);
151152
});
152153
});
154+
155+
describe("collectForbiddenPackPaths", () => {
156+
it("flags nested node_modules leaking into npm pack output", () => {
157+
expect(
158+
collectForbiddenPackPaths([
159+
"dist/index.js",
160+
"extensions/tlon/node_modules/.bin/tlon",
161+
"node_modules/.bin/openclaw",
162+
]),
163+
).toEqual(["extensions/tlon/node_modules/.bin/tlon", "node_modules/.bin/openclaw"]);
164+
});
165+
});

0 commit comments

Comments
 (0)