mirror of https://github.com/openclaw/openclaw.git
fix: harden windows npm runtime path
This commit is contained in:
parent
92191fcd68
commit
86a3149b2e
|
|
@ -0,0 +1 @@
|
||||||
|
**/node_modules/
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
**/node_modules/
|
||||||
|
|
@ -218,6 +218,16 @@ function runPackDry(): PackResult[] {
|
||||||
return JSON.parse(raw) as 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() {
|
function checkPluginVersions() {
|
||||||
const rootPackagePath = resolve("package.json");
|
const rootPackagePath = resolve("package.json");
|
||||||
const rootPackage = JSON.parse(readFileSync(rootPackagePath, "utf8")) as PackageJson;
|
const rootPackage = JSON.parse(readFileSync(rootPackagePath, "utf8")) as PackageJson;
|
||||||
|
|
@ -422,9 +432,7 @@ function main() {
|
||||||
return paths.has(group) ? [] : [group];
|
return paths.has(group) ? [] : [group];
|
||||||
})
|
})
|
||||||
.toSorted();
|
.toSorted();
|
||||||
const forbidden = [...paths].filter((path) =>
|
const forbidden = collectForbiddenPackPaths(paths);
|
||||||
forbiddenPrefixes.some((prefix) => path.startsWith(prefix)),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (missing.length > 0 || forbidden.length > 0) {
|
if (missing.length > 0 || forbidden.length > 0) {
|
||||||
if (missing.length > 0) {
|
if (missing.length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ const {
|
||||||
resolveDiscordAllowlistConfigMock,
|
resolveDiscordAllowlistConfigMock,
|
||||||
resolveNativeCommandsEnabledMock,
|
resolveNativeCommandsEnabledMock,
|
||||||
resolveNativeSkillsEnabledMock,
|
resolveNativeSkillsEnabledMock,
|
||||||
|
voiceRuntimeModuleLoadedMock,
|
||||||
} = vi.hoisted(() => {
|
} = vi.hoisted(() => {
|
||||||
const createdBindingManagers: Array<{ stop: ReturnType<typeof vi.fn> }> = [];
|
const createdBindingManagers: Array<{ stop: ReturnType<typeof vi.fn> }> = [];
|
||||||
return {
|
return {
|
||||||
|
|
@ -103,6 +104,7 @@ const {
|
||||||
})),
|
})),
|
||||||
resolveNativeCommandsEnabledMock: vi.fn(() => true),
|
resolveNativeCommandsEnabledMock: vi.fn(() => true),
|
||||||
resolveNativeSkillsEnabledMock: vi.fn(() => false),
|
resolveNativeSkillsEnabledMock: vi.fn(() => false),
|
||||||
|
voiceRuntimeModuleLoadedMock: vi.fn(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -210,10 +212,13 @@ vi.mock("../voice/command.js", () => ({
|
||||||
createDiscordVoiceCommand: () => ({ name: "voice-command" }),
|
createDiscordVoiceCommand: () => ({ name: "voice-command" }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../voice/manager.js", () => ({
|
vi.mock("../voice/manager.runtime.js", () => {
|
||||||
|
voiceRuntimeModuleLoadedMock();
|
||||||
|
return {
|
||||||
DiscordVoiceManager: class DiscordVoiceManager {},
|
DiscordVoiceManager: class DiscordVoiceManager {},
|
||||||
DiscordVoiceReadyListener: class DiscordVoiceReadyListener {},
|
DiscordVoiceReadyListener: class DiscordVoiceReadyListener {},
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("./agent-components.js", () => ({
|
vi.mock("./agent-components.js", () => ({
|
||||||
createAgentComponentButton: () => ({ id: "btn" }),
|
createAgentComponentButton: () => ({ id: "btn" }),
|
||||||
|
|
@ -390,6 +395,7 @@ describe("monitorDiscordProvider", () => {
|
||||||
});
|
});
|
||||||
resolveNativeCommandsEnabledMock.mockClear().mockReturnValue(true);
|
resolveNativeCommandsEnabledMock.mockClear().mockReturnValue(true);
|
||||||
resolveNativeSkillsEnabledMock.mockClear().mockReturnValue(false);
|
resolveNativeSkillsEnabledMock.mockClear().mockReturnValue(false);
|
||||||
|
voiceRuntimeModuleLoadedMock.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stops thread bindings when startup fails before lifecycle begins", async () => {
|
it("stops thread bindings when startup fails before lifecycle begins", async () => {
|
||||||
|
|
@ -424,6 +430,38 @@ describe("monitorDiscordProvider", () => {
|
||||||
expect(reconcileAcpThreadBindingsOnStartupMock).toHaveBeenCalledTimes(1);
|
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 () => {
|
it("treats ACP error status as uncertain during startup thread-binding probes", async () => {
|
||||||
const { monitorDiscordProvider } = await import("./provider.js");
|
const { monitorDiscordProvider } = await import("./provider.js");
|
||||||
getAcpSessionStatusMock.mockResolvedValue({ state: "error" });
|
getAcpSessionStatusMock.mockResolvedValue({ state: "error" });
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,6 @@ import { resolveDiscordAccount } from "../accounts.js";
|
||||||
import { fetchDiscordApplicationId } from "../probe.js";
|
import { fetchDiscordApplicationId } from "../probe.js";
|
||||||
import { normalizeDiscordToken } from "../token.js";
|
import { normalizeDiscordToken } from "../token.js";
|
||||||
import { createDiscordVoiceCommand } from "../voice/command.js";
|
import { createDiscordVoiceCommand } from "../voice/command.js";
|
||||||
import { DiscordVoiceManager, DiscordVoiceReadyListener } from "../voice/manager.js";
|
|
||||||
import {
|
import {
|
||||||
createAgentComponentButton,
|
createAgentComponentButton,
|
||||||
createAgentSelectMenu,
|
createAgentSelectMenu,
|
||||||
|
|
@ -104,6 +103,17 @@ export type MonitorDiscordOpts = {
|
||||||
setStatus?: DiscordMonitorStatusSink;
|
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 {
|
function formatThreadBindingDurationForConfigLabel(durationMs: number): string {
|
||||||
const label = formatThreadBindingDurationLabel(durationMs);
|
const label = formatThreadBindingDurationLabel(durationMs);
|
||||||
return label === "disabled" ? "off" : label;
|
return label === "disabled" ? "off" : label;
|
||||||
|
|
@ -663,6 +673,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (voiceEnabled) {
|
if (voiceEnabled) {
|
||||||
|
const { DiscordVoiceManager, DiscordVoiceReadyListener } = await loadDiscordVoiceRuntime();
|
||||||
voiceManager = new DiscordVoiceManager({
|
voiceManager = new DiscordVoiceManager({
|
||||||
client,
|
client,
|
||||||
cfg,
|
cfg,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { DiscordVoiceManager, DiscordVoiceReadyListener } from "./manager.js";
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
collectAppcastSparkleVersionErrors,
|
collectAppcastSparkleVersionErrors,
|
||||||
collectBundledExtensionManifestErrors,
|
collectBundledExtensionManifestErrors,
|
||||||
collectBundledExtensionRootDependencyGapErrors,
|
collectBundledExtensionRootDependencyGapErrors,
|
||||||
|
collectForbiddenPackPaths,
|
||||||
} from "../scripts/release-check.ts";
|
} from "../scripts/release-check.ts";
|
||||||
|
|
||||||
function makeItem(shortVersion: string, sparkleVersion: string): string {
|
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"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue