mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
fix(imessage): surface Full Disk Access probe failures
Preserve canonical iMessage Full Disk Access probe failures through non-sensitive health snapshots and status output, promote imsg denial banners to the public remediation message, and add a narrow audit exception for the reviewed Mistral advisory false-positive.
This commit is contained in:
@@ -231,6 +231,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/logging: install console capture before foreground Gateway fast-path parsing and suppress known libsignal session dumps even in verbose mode, preventing raw terminal logs from printing WhatsApp session key material. (#76306) Thanks @rubencu.
|
||||
- Exec approvals: keep `exec.approval.list` on the lightweight policy-summary path so listing pending approvals no longer loads the rich tree-sitter command explainer. (#76943) Thanks @rubencu.
|
||||
- Agents: surface concise default-visible warnings when `exec`/`bash` tool calls fail after the assistant claims success, while keeping raw stderr hidden unless verbose details are enabled. Fixes #60497. (#80003) Thanks @jbetala7.
|
||||
- Channels/iMessage: keep redacted failed probe details in non-sensitive health snapshots so Full Disk Access failures no longer appear as configured/OK in status output. Fixes #79795.
|
||||
- Agents: stop blank model-emitted tool calls before dispatch while preserving id-based tool-name recovery, preventing Kimi/NVIDIA blank-name retry loops without creating a callable `_blank` sentinel. Fixes #34129. (#56391) Thanks @smartchainark.
|
||||
- Agents/Telegram: deliver the canonical final assistant answer instead of replaying accumulated pre-tool text blocks, preventing duplicate Telegram replies and raw-looking tool-output fragments from leaking into chat delivery. Fixes #79621 and #79986. Thanks @nonzeroclaw and @dudaefj.
|
||||
- Auto-reply/TUI: keep fallback timeout recovery deliverable after a primary model lifecycle error by emitting fallback progress and deferring terminal TUI errors until recovery has a chance to finish. Fixes #80000. (#80009) Thanks @TurboTheTurtle.
|
||||
|
||||
@@ -39,6 +39,9 @@ type PendingRequest = {
|
||||
timer?: NodeJS.Timeout;
|
||||
};
|
||||
|
||||
export const PUBLIC_IMESSAGE_FULL_DISK_ACCESS_ERROR =
|
||||
"imsg cannot access ~/Library/Messages/chat.db. Grant Full Disk Access to the Gateway/launcher process and restart Gateway.";
|
||||
|
||||
function isTestEnv(): boolean {
|
||||
if (process.env.NODE_ENV === "test") {
|
||||
return true;
|
||||
@@ -47,6 +50,14 @@ function isTestEnv(): boolean {
|
||||
return Boolean(vitest);
|
||||
}
|
||||
|
||||
export function normalizeIMessageFullDiskAccessError(message: string): string | undefined {
|
||||
const normalized = normalizeLowercaseStringOrEmpty(message);
|
||||
if (!normalized.includes("full disk access") || !normalized.includes("chat.db")) {
|
||||
return undefined;
|
||||
}
|
||||
return PUBLIC_IMESSAGE_FULL_DISK_ACCESS_ERROR;
|
||||
}
|
||||
|
||||
export class IMessageRpcClient {
|
||||
private readonly cliPath: string;
|
||||
private readonly dbPath?: string;
|
||||
@@ -58,6 +69,7 @@ export class IMessageRpcClient {
|
||||
private child: ChildProcessWithoutNullStreams | null = null;
|
||||
private reader: Interface | null = null;
|
||||
private nextId = 1;
|
||||
private publicProcessError: string | null = null;
|
||||
|
||||
constructor(opts: IMessageRpcClientOptions = {}) {
|
||||
this.cliPath = opts.cliPath?.trim() || "imsg";
|
||||
@@ -100,7 +112,9 @@ export class IMessageRpcClient {
|
||||
if (!line.trim()) {
|
||||
continue;
|
||||
}
|
||||
this.runtime?.error?.(`imsg rpc: ${line.trim()}`);
|
||||
const trimmed = line.trim();
|
||||
this.recordProcessDiagnostic(trimmed);
|
||||
this.runtime?.error?.(`imsg rpc: ${trimmed}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -116,12 +130,7 @@ export class IMessageRpcClient {
|
||||
});
|
||||
|
||||
child.on("close", (code, signal) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
const reason = signal ? `signal ${signal}` : `code ${code}`;
|
||||
this.failAll(new Error(`imsg rpc exited (${reason})`));
|
||||
} else {
|
||||
this.failAll(new Error("imsg rpc closed"));
|
||||
}
|
||||
this.failAll(this.buildCloseError(code, signal));
|
||||
this.closedResolve?.();
|
||||
});
|
||||
}
|
||||
@@ -210,6 +219,7 @@ export class IMessageRpcClient {
|
||||
try {
|
||||
parsed = JSON.parse(line) as IMessageRpcResponse<unknown>;
|
||||
} catch (err) {
|
||||
this.recordProcessDiagnostic(line);
|
||||
const detail = formatErrorMessage(err);
|
||||
this.runtime?.error?.(`imsg rpc: failed to parse ${line}: ${detail}`);
|
||||
return;
|
||||
@@ -257,6 +267,21 @@ export class IMessageRpcClient {
|
||||
}
|
||||
}
|
||||
|
||||
private recordProcessDiagnostic(line: string): void {
|
||||
this.publicProcessError ??= normalizeIMessageFullDiskAccessError(line) ?? null;
|
||||
}
|
||||
|
||||
private buildCloseError(code: number | null, signal: NodeJS.Signals | null): Error {
|
||||
if (this.publicProcessError) {
|
||||
return new Error(this.publicProcessError);
|
||||
}
|
||||
if (code !== 0 && code !== null) {
|
||||
const reason = signal ? `signal ${signal}` : `code ${code}`;
|
||||
return new Error(`imsg rpc exited (${reason})`);
|
||||
}
|
||||
return new Error("imsg rpc closed");
|
||||
}
|
||||
|
||||
private failAll(err: Error) {
|
||||
for (const [key, pending] of this.pending.entries()) {
|
||||
if (pending.timer) {
|
||||
|
||||
@@ -50,6 +50,22 @@ describe("createIMessageRpcClient", () => {
|
||||
);
|
||||
expect(spawnMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("promotes Full Disk Access rpc banners to the public probe error", async () => {
|
||||
const { IMessageRpcClient, PUBLIC_IMESSAGE_FULL_DISK_ACCESS_ERROR } =
|
||||
await import("./client.js");
|
||||
const client = new IMessageRpcClient();
|
||||
const internals = client as unknown as {
|
||||
handleLine: (line: string) => void;
|
||||
buildCloseError: (code: number | null, signal: NodeJS.Signals | null) => Error;
|
||||
};
|
||||
|
||||
internals.handleLine(
|
||||
"imsg cannot access /Users/alice/Library/Messages/chat.db. Grant Full Disk Access to the Gateway/launcher process and restart Gateway.",
|
||||
);
|
||||
|
||||
expect(internals.buildCloseError(1, null).message).toBe(PUBLIC_IMESSAGE_FULL_DISK_ACCESS_ERROR);
|
||||
});
|
||||
});
|
||||
|
||||
describe("imessage setup status", () => {
|
||||
|
||||
@@ -23,6 +23,16 @@ const NESTED_MAPPING_ENTRY_INDENT = 8;
|
||||
const SNAPSHOT_SECTIONS = ["dependencies", "optionalDependencies"];
|
||||
const IMPORTER_SECTIONS = ["dependencies", "optionalDependencies"];
|
||||
const LOCAL_REFERENCE_PREFIXES = ["file:", "link:", "portal:", "workspace:"];
|
||||
// GitHub's GHSA-3q49-cfcf-g5fm feed includes an overbroad ">=0" range alongside
|
||||
// the compromised @mistralai/mistralai versions. Keep the production audit
|
||||
// blocking for the compromised releases while allowing our pinned 2.2.1 lock.
|
||||
const AUDIT_ADVISORY_VERSION_OVERRIDES = [
|
||||
{
|
||||
packageName: "@mistralai/mistralai",
|
||||
advisoryIds: new Set(["1118204", "GHSA-3q49-cfcf-g5fm"]),
|
||||
unaffectedVersions: new Set(["2.2.1"]),
|
||||
},
|
||||
];
|
||||
|
||||
export function normalizeAuditLevel(level) {
|
||||
const normalized = String(level ?? "").toLowerCase();
|
||||
@@ -560,7 +570,34 @@ function normalizeSeverity(severity) {
|
||||
return severity.toLowerCase();
|
||||
}
|
||||
|
||||
export function filterFindingsBySeverity(advisoriesByPackage, minSeverity) {
|
||||
function advisoryMatchesOverride(advisory, override) {
|
||||
const advisoryId = String(advisory?.id ?? "");
|
||||
const advisoryUrl = typeof advisory?.url === "string" ? advisory.url : "";
|
||||
return (
|
||||
override.advisoryIds.has(advisoryId) ||
|
||||
[...override.advisoryIds].some((id) => advisoryUrl.includes(id))
|
||||
);
|
||||
}
|
||||
|
||||
function shouldSuppressAdvisoryFinding({ packageName, advisory, versionsByPackage }) {
|
||||
if (!versionsByPackage) {
|
||||
return false;
|
||||
}
|
||||
const override = AUDIT_ADVISORY_VERSION_OVERRIDES.find(
|
||||
(candidate) =>
|
||||
candidate.packageName === packageName && advisoryMatchesOverride(advisory, candidate),
|
||||
);
|
||||
if (!override) {
|
||||
return false;
|
||||
}
|
||||
const resolvedVersions = versionsByPackage.get(packageName);
|
||||
if (!resolvedVersions || resolvedVersions.size === 0) {
|
||||
return false;
|
||||
}
|
||||
return [...resolvedVersions].every((version) => override.unaffectedVersions.has(version));
|
||||
}
|
||||
|
||||
export function filterFindingsBySeverity(advisoriesByPackage, minSeverity, versionsByPackage) {
|
||||
const threshold = normalizeAuditLevel(minSeverity);
|
||||
const findings = [];
|
||||
|
||||
@@ -576,6 +613,9 @@ export function filterFindingsBySeverity(advisoriesByPackage, minSeverity) {
|
||||
if ((SEVERITY_RANK[severity] ?? -1) < SEVERITY_RANK[threshold]) {
|
||||
continue;
|
||||
}
|
||||
if (shouldSuppressAdvisoryFinding({ packageName, advisory, versionsByPackage })) {
|
||||
continue;
|
||||
}
|
||||
findings.push({
|
||||
packageName,
|
||||
id: advisory.id ?? "unknown",
|
||||
@@ -651,7 +691,8 @@ export async function runPnpmAuditProd({
|
||||
const normalizedMinSeverity = normalizeAuditLevel(minSeverity);
|
||||
const lockfilePath = path.join(rootDir, "pnpm-lock.yaml");
|
||||
const lockfileText = await readFile(lockfilePath, "utf8");
|
||||
const payload = createBulkAdvisoryPayload(collectProdResolvedPackagesFromLockfile(lockfileText));
|
||||
const versionsByPackage = collectProdResolvedPackagesFromLockfile(lockfileText);
|
||||
const payload = createBulkAdvisoryPayload(versionsByPackage);
|
||||
const payloadEntries = Object.entries(payload);
|
||||
|
||||
if (payloadEntries.length === 0) {
|
||||
@@ -669,7 +710,11 @@ export async function runPnpmAuditProd({
|
||||
Object.assign(advisoryResults, chunkResults);
|
||||
}
|
||||
|
||||
const findings = filterFindingsBySeverity(advisoryResults, normalizedMinSeverity);
|
||||
const findings = filterFindingsBySeverity(
|
||||
advisoryResults,
|
||||
normalizedMinSeverity,
|
||||
versionsByPackage,
|
||||
);
|
||||
if (findings.length === 0) {
|
||||
stdout.write(
|
||||
`No ${normalizedMinSeverity} or higher advisories found for production dependencies.\n`,
|
||||
|
||||
@@ -42,6 +42,12 @@ type DiscordHealthAccount = {
|
||||
configured: boolean;
|
||||
};
|
||||
|
||||
type IMessageHealthAccount = {
|
||||
accountId: string;
|
||||
enabled: boolean;
|
||||
configured: boolean;
|
||||
};
|
||||
|
||||
async function loadFreshHealthModulesForTest() {
|
||||
vi.doMock("../config/config.js", () => ({
|
||||
getRuntimeConfig: () => testConfig,
|
||||
@@ -405,6 +411,43 @@ function createDiscordHealthPlugin(): HealthTestPlugin {
|
||||
};
|
||||
}
|
||||
|
||||
function createIMessageHealthPlugin(): HealthTestPlugin {
|
||||
return {
|
||||
...createChannelTestPluginBase({ id: "imessage", label: "iMessage" }),
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: (_cfg, accountId) => ({
|
||||
accountId: accountId?.trim() || "default",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
}),
|
||||
inspectAccount: (_cfg, accountId) => ({
|
||||
accountId: accountId?.trim() || "default",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
}),
|
||||
isEnabled: (account) => (account as IMessageHealthAccount).enabled,
|
||||
isConfigured: (account) => (account as IMessageHealthAccount).configured,
|
||||
},
|
||||
status: {
|
||||
buildChannelSummary: ({ snapshot }) => ({
|
||||
accountId: snapshot.accountId,
|
||||
configured: Boolean(snapshot.configured),
|
||||
...(snapshot.probe && typeof snapshot.probe === "object" ? { probe: snapshot.probe } : {}),
|
||||
}),
|
||||
probeAccount: async () => ({
|
||||
ok: false,
|
||||
error:
|
||||
"imsg cannot access /Users/alice/Library/Messages/chat.db. Grant Full Disk Access to the Gateway/launcher process and restart Gateway. privateApi=/tmp/openclaw/private.sock",
|
||||
privateApi: {
|
||||
rpcCommand: "imsg rpc --json",
|
||||
diagnostics: "sensitive transport details",
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("getHealthSnapshot", () => {
|
||||
beforeAll(async () => {
|
||||
({
|
||||
@@ -744,6 +787,75 @@ describe("getHealthSnapshot", () => {
|
||||
expect(telegram.accounts?.default?.channelAccessToken).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps redacted failed probes in non-sensitive health snapshots", async () => {
|
||||
healthPluginsForTest = [createIMessageHealthPlugin()];
|
||||
testConfig = { channels: { imessage: { enabled: true } } };
|
||||
testStore = {};
|
||||
|
||||
const snap = await getHealthSnapshot({
|
||||
timeoutMs: 25,
|
||||
includeSensitive: false,
|
||||
});
|
||||
const imessage = snap.channels.imessage as {
|
||||
configured?: boolean;
|
||||
probe?: {
|
||||
ok?: boolean;
|
||||
error?: string;
|
||||
privateApi?: unknown;
|
||||
};
|
||||
accounts?: Record<
|
||||
string,
|
||||
{
|
||||
probe?: {
|
||||
ok?: boolean;
|
||||
error?: string;
|
||||
privateApi?: unknown;
|
||||
};
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
expect(imessage.configured).toBe(true);
|
||||
expect(imessage.probe).toMatchObject({
|
||||
ok: false,
|
||||
error:
|
||||
"imsg cannot access ~/Library/Messages/chat.db. Grant Full Disk Access to the Gateway/launcher process and restart Gateway.",
|
||||
});
|
||||
expect(imessage.probe?.privateApi).toBeUndefined();
|
||||
expect(imessage.accounts?.default?.probe).toMatchObject({
|
||||
ok: false,
|
||||
error:
|
||||
"imsg cannot access ~/Library/Messages/chat.db. Grant Full Disk Access to the Gateway/launcher process and restart Gateway.",
|
||||
});
|
||||
expect(imessage.accounts?.default?.probe?.privateApi).toBeUndefined();
|
||||
});
|
||||
|
||||
it("omits generic failed probe errors from non-sensitive health snapshots", async () => {
|
||||
testConfig = { channels: { telegram: { botToken: "bad-token" } } };
|
||||
testStore = {};
|
||||
vi.stubEnv("DISCORD_BOT_TOKEN", "");
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => {
|
||||
throw new Error("network down with private diagnostic");
|
||||
}),
|
||||
);
|
||||
|
||||
const snap = await getHealthSnapshot({
|
||||
timeoutMs: 25,
|
||||
includeSensitive: false,
|
||||
});
|
||||
const telegram = snap.channels.telegram as {
|
||||
configured?: boolean;
|
||||
probe?: unknown;
|
||||
accounts?: Record<string, { probe?: unknown }>;
|
||||
};
|
||||
|
||||
expect(telegram.configured).toBe(true);
|
||||
expect(telegram.probe).toBeUndefined();
|
||||
expect(telegram.accounts?.default?.probe).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns structured telegram probe errors", async () => {
|
||||
testConfig = { channels: { telegram: { botToken: "bad-token" } } };
|
||||
testStore = {};
|
||||
|
||||
@@ -227,6 +227,29 @@ describe("healthCommand", () => {
|
||||
const lines = formatHealthChannelLines(summary, { accountMode: "default" });
|
||||
expect(lines).toStrictEqual(["WhatsApp: auth stabilizing"]);
|
||||
});
|
||||
|
||||
it("formats iMessage probe failures as failed health lines", () => {
|
||||
const summary = createHealthSummary({
|
||||
channels: {
|
||||
imessage: {
|
||||
accountId: "default",
|
||||
configured: true,
|
||||
probe: {
|
||||
ok: false,
|
||||
error:
|
||||
"imsg cannot access ~/Library/Messages/chat.db. Grant Full Disk Access to the Gateway/launcher process and restart Gateway.",
|
||||
},
|
||||
},
|
||||
},
|
||||
channelOrder: ["imessage"],
|
||||
channelLabels: { imessage: "iMessage" },
|
||||
});
|
||||
|
||||
const lines = formatHealthChannelLines(summary, { accountMode: "default" });
|
||||
expect(lines).toContain(
|
||||
"iMessage: failed (unknown) - imsg cannot access ~/Library/Messages/chat.db. Grant Full Disk Access to the Gateway/launcher process and restart Gateway.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatHealthCheckFailure", () => {
|
||||
|
||||
@@ -70,6 +70,43 @@ const debugHealth = (...args: unknown[]) => {
|
||||
}
|
||||
};
|
||||
|
||||
const PUBLIC_IMESSAGE_FULL_DISK_ACCESS_ERROR =
|
||||
"imsg cannot access ~/Library/Messages/chat.db. Grant Full Disk Access to the Gateway/launcher process and restart Gateway.";
|
||||
|
||||
const redactIMessageProbeErrorMessage = (message: string): string => {
|
||||
const trimmed = message.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
return trimmed.replaceAll(
|
||||
/\/Users\/[^/\s]+\/Library\/Messages\/chat\.db/g,
|
||||
"~/Library/Messages/chat.db",
|
||||
);
|
||||
};
|
||||
|
||||
const buildNonSensitiveProbeFailure = (
|
||||
channelId: string,
|
||||
probe: unknown,
|
||||
): Record<string, unknown> | undefined => {
|
||||
const record = asNullableRecord(probe);
|
||||
if (channelId !== "imessage" || !record || record.ok !== false) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof record.error !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const error = redactIMessageProbeErrorMessage(record.error);
|
||||
if (
|
||||
!/\bimsg\b/i.test(error) ||
|
||||
!error.includes("~/Library/Messages/chat.db") ||
|
||||
!/\bFull Disk Access\b/i.test(error)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return { ok: false, error: PUBLIC_IMESSAGE_FULL_DISK_ACCESS_ERROR };
|
||||
};
|
||||
|
||||
const formatDurationParts = (ms: number): string => {
|
||||
if (!Number.isFinite(ms)) {
|
||||
return "unknown";
|
||||
@@ -453,13 +490,15 @@ export async function getHealthSnapshot(params?: {
|
||||
const runtimeSnapshot =
|
||||
params?.runtimeSnapshot?.channelAccounts[plugin.id]?.[accountId] ??
|
||||
(accountId === defaultAccountId ? params?.runtimeSnapshot?.channels[plugin.id] : undefined);
|
||||
const nonSensitiveProbeFailure = buildNonSensitiveProbeFailure(plugin.id, probe);
|
||||
const snapshotProbe = includeSensitive ? probe : nonSensitiveProbeFailure;
|
||||
const snapshot: ChannelAccountSnapshot = await buildChannelAccountSnapshotFromAccount({
|
||||
plugin,
|
||||
cfg,
|
||||
accountId,
|
||||
account: snapshotAccount,
|
||||
runtime: runtimeSnapshot,
|
||||
probe: includeSensitive ? probe : undefined,
|
||||
probe: snapshotProbe,
|
||||
enabledFallback: enabled,
|
||||
configuredFallback: configured,
|
||||
});
|
||||
@@ -499,7 +538,13 @@ export async function getHealthSnapshot(params?: {
|
||||
record.probe = probe;
|
||||
}
|
||||
if (!includeSensitive) {
|
||||
delete record.probe;
|
||||
const summaryProbeFailure = buildNonSensitiveProbeFailure(plugin.id, record.probe);
|
||||
const safeProbeFailure = summaryProbeFailure ?? nonSensitiveProbeFailure;
|
||||
if (safeProbeFailure) {
|
||||
record.probe = safeProbeFailure;
|
||||
} else {
|
||||
delete record.probe;
|
||||
}
|
||||
}
|
||||
if (record.lastProbeAt === undefined && lastProbeAt) {
|
||||
record.lastProbeAt = lastProbeAt;
|
||||
|
||||
@@ -166,6 +166,57 @@ snapshots:
|
||||
]);
|
||||
});
|
||||
|
||||
it("suppresses the overbroad Mistral malware advisory for the pre-compromise locked version", () => {
|
||||
const versionsByPackage = new Map([["@mistralai/mistralai", new Set(["2.2.1"])]]);
|
||||
const findings = filterFindingsBySeverity(
|
||||
{
|
||||
"@mistralai/mistralai": [
|
||||
{
|
||||
id: "1118204",
|
||||
severity: "critical",
|
||||
title: "Malware in @mistralai/mistralai",
|
||||
vulnerable_versions: ">=0",
|
||||
url: "https://github.com/advisories/GHSA-3q49-cfcf-g5fm",
|
||||
},
|
||||
],
|
||||
},
|
||||
"high",
|
||||
versionsByPackage,
|
||||
);
|
||||
|
||||
expect(findings).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps the Mistral malware advisory blocking for compromised resolved versions", () => {
|
||||
const versionsByPackage = new Map([["@mistralai/mistralai", new Set(["2.2.4"])]]);
|
||||
const findings = filterFindingsBySeverity(
|
||||
{
|
||||
"@mistralai/mistralai": [
|
||||
{
|
||||
id: "1118204",
|
||||
severity: "critical",
|
||||
title: "Malware in @mistralai/mistralai",
|
||||
vulnerable_versions: ">=0",
|
||||
url: "https://github.com/advisories/GHSA-3q49-cfcf-g5fm",
|
||||
},
|
||||
],
|
||||
},
|
||||
"high",
|
||||
versionsByPackage,
|
||||
);
|
||||
|
||||
expect(findings).toEqual([
|
||||
{
|
||||
id: "1118204",
|
||||
packageName: "@mistralai/mistralai",
|
||||
severity: "critical",
|
||||
title: "Malware in @mistralai/mistralai",
|
||||
url: "https://github.com/advisories/GHSA-3q49-cfcf-g5fm",
|
||||
vulnerableVersions: ">=0",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns a failing exit code when bulk advisories include high severity findings", async () => {
|
||||
const tempDir = await mkdtemp(path.join(tmpdir(), "openclaw-audit-prod-"));
|
||||
await writeFile(
|
||||
|
||||
Reference in New Issue
Block a user