Compare commits

...
Author SHA1 Message Date
Patrick Erichsen 2e5ffdc565 Merge pull request #1861 from openclaw/pe/rescan
feat: add owner rescan security surfaces
2026-04-28 16:32:26 -07:00
Peter Steinberger 4e13e729fb ci: narrow ClawSweeper dispatch cancellation 2026-04-28 11:53:07 +01:00
Peter Steinberger d17e100cca ci: harden clawsweeper dispatch workflow 2026-04-28 11:35:24 +01:00
Peter Steinberger c732b38569 ci: debounce clawsweeper dispatch metadata 2026-04-28 11:31:51 +01:00
Peter Steinberger 3701733797 fix: normalize vt engine stats before caching 2026-04-28 08:47:50 +01:00
Peter Steinberger b8ba595d06 docs: note vt code insight calibration 2026-04-28 08:40:09 +01:00
Peter Steinberger 232e429dee fix: clear uncorroborated vt suspicious state 2026-04-28 08:39:39 +01:00
Deepak Jain 932155cb8f docs: clarify static scan suppression gate 2026-04-28 08:39:17 +01:00
Deepak Jain d855d09ab0 fix: calibrate vt code insight moderation
Refs #1830
2026-04-28 08:39:17 +01:00
Peter Steinberger 16e87c147d ci: harden ClawSweeper dispatcher credentials 2026-04-28 06:48:40 +01:00
Peter Steinberger cbe22e70b9 ci: fix ClawSweeper dispatcher payload 2026-04-28 06:44:28 +01:00
Peter Steinberger 837331c967 ci: add ClawSweeper event dispatcher 2026-04-28 06:43:39 +01:00
Peter Steinberger 6ce443496d fix: keep package list queries single-page 2026-04-28 06:28:27 +01:00
Peter Steinberger 8fd4f3b051 ci: fix production smoke coverage 2026-04-28 06:18:40 +01:00
Peter Steinberger a2153909da fix: avoid plugin catalog query limits 2026-04-28 06:13:57 +01:00
Deepak Jain 75e1b4633e fix: constrain plugin catalog queries (#1842)
Refs #1699.

Use family-indexed plugin catalog paths instead of broad package scans:
- `/api/v1/plugins` merges separate `code-plugin` and `bundle-plugin` list streams with an endpoint-specific cursor.
- `/api/v1/plugins/search` searches both plugin families directly, dedupes, sorts, and limits results.
- Keeps generic `/api/v1/packages` behavior unchanged.

Validation:
- bunx vitest run convex/httpApiV1.handlers.test.ts convex/packages.public.test.ts
- bunx tsc --noEmit
- bunx tsc -p packages/schema/tsconfig.json --noEmit
- bunx tsc -p packages/clawhub/tsconfig.json --noEmit
- bun run lint
- git diff --check
2026-04-28 06:03:15 +01:00
16 changed files with 1066 additions and 185 deletions
@@ -0,0 +1,62 @@
name: ClawSweeper Dispatch
on:
issues:
types: [opened, reopened, edited, labeled, unlabeled]
pull_request_target: # zizmor: ignore[dangerous-triggers] maintainer-owned external dispatch; no checkout or untrusted PR code execution
types: [opened, reopened, synchronize, ready_for_review, edited, labeled, unlabeled]
permissions:
contents: read
concurrency:
group: clawsweeper-dispatch-${{ github.repository }}-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event.action == 'edited' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review' }}
jobs:
dispatch:
runs-on: ubuntu-latest
if: ${{ !(endsWith(github.actor, '[bot]') && (github.event.action == 'labeled' || github.event.action == 'unlabeled')) }}
env:
HAS_CLAWSWEEPER_APP_PRIVATE_KEY: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY != '' }}
CLAWSWEEPER_APP_CLIENT_ID: Iv23liOECG0slfuhz093
SUPERSEDES_IN_PROGRESS: ${{ (github.event.action == 'edited' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review') && 'true' || 'false' }}
steps:
- name: Debounce bursty metadata events
if: ${{ github.event.action == 'labeled' || github.event.action == 'unlabeled' }}
run: sleep 20
- name: Create ClawSweeper dispatch token
id: token
if: ${{ env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true' }}
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}
owner: openclaw
repositories: clawsweeper
- name: Dispatch exact ClawSweeper review
env:
GH_TOKEN: ${{ steps.token.outputs.token || secrets.OPENCLAW_GH_TOKEN }}
TARGET_REPO: ${{ github.repository }}
ITEM_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
ITEM_KIND: ${{ github.event_name == 'pull_request_target' && 'pull_request' || 'issue' }}
SOURCE_EVENT: ${{ github.event_name }}
SOURCE_ACTION: ${{ github.event.action }}
run: |
if [ -z "$GH_TOKEN" ]; then
echo "::notice::Skipping ClawSweeper dispatch because no dispatch credential is configured."
exit 0
fi
payload="$(jq -nc \
--arg target_repo "$TARGET_REPO" \
--argjson item_number "$ITEM_NUMBER" \
--arg item_kind "$ITEM_KIND" \
--arg source_event "$SOURCE_EVENT" \
--arg source_action "$SOURCE_ACTION" \
--argjson supersedes_in_progress "$SUPERSEDES_IN_PROGRESS" \
'{event_type:"clawsweeper_item",client_payload:{target_repo:$target_repo,item_number:$item_number,item_kind:$item_kind,source_event:$source_event,source_action:$source_action,supersedes_in_progress:$supersedes_in_progress}}')"
gh api repos/openclaw/clawsweeper/dispatches \
--method POST \
--input - <<< "$payload"
+1 -1
View File
@@ -163,7 +163,7 @@ jobs:
- name: Install Playwright browser
if: needs.validate-deploy-request.outputs.run_smoke == 'true'
run: bunx playwright install --with-deps chromium
run: bunx playwright install --with-deps chromium webkit
- name: Write authenticated storage state
if: needs.validate-deploy-request.outputs.run_smoke == 'true' && env.PLAYWRIGHT_AUTH_STORAGE_STATE_JSON != ''
+4
View File
@@ -2,6 +2,10 @@
## Unreleased
### Fixes
- Moderation: calibrate VirusTotal Code Insight suspicious verdicts so uncorroborated AI-only findings do not keep otherwise clean skills quarantined (#1830, #1841) (thanks @deepujain).
## 0.11.0 - 2026-04-28
### Changed
+261 -7
View File
@@ -55,6 +55,26 @@ function findRateLimitCallArgs(mock: ReturnType<typeof vi.fn>) {
return mock.mock.calls.map(([, args]) => args).find(isRateLimitArgs);
}
function makeCatalogItem(
name: string,
options: {
family: "code-plugin" | "bundle-plugin" | "skill";
updatedAt: number;
score?: number;
},
) {
return {
name,
displayName: name,
family: options.family,
channel: "community",
isOfficial: false,
createdAt: options.updatedAt,
updatedAt: options.updatedAt,
...(typeof options.score === "number" ? { score: options.score } : {}),
};
}
function makeCtx(partial: Record<string, unknown>) {
const partialRunQuery =
typeof partial.runQuery === "function"
@@ -2580,7 +2600,11 @@ describe("httpApiV1 handlers", () => {
});
it("packages search forwards executesCode and capabilityTag", async () => {
const runQuery = vi.fn().mockResolvedValue([]);
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if ("paginationOpts" in args) return { page: [], isDone: true, continueCursor: "" };
if ("query" in args) return [];
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
@@ -2592,10 +2616,9 @@ describe("httpApiV1 handlers", () => {
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
query: "test",
limit: 5,
executesCode: true,
capabilityTag: "tools",
paginationOpts: { cursor: null, numItems: 50 },
}),
);
expect(findRateLimitCallArgs(runMutation)).toMatchObject({
@@ -2623,6 +2646,123 @@ describe("httpApiV1 handlers", () => {
);
});
it("plugins list defaults to plugin package families", async () => {
const codePlugin = {
name: "code-plugin",
displayName: "Code Plugin",
family: "code-plugin",
channel: "community",
isOfficial: false,
createdAt: 20,
updatedAt: 200,
};
const bundlePlugin = {
name: "bundle-plugin",
displayName: "Bundle Plugin",
family: "bundle-plugin",
channel: "community",
isOfficial: false,
createdAt: 10,
updatedAt: 100,
};
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (args.family === "code-plugin") {
return { page: [codePlugin], isDone: true, continueCursor: "" };
}
if (args.family === "bundle-plugin") {
return { page: [bundlePlugin], isDone: true, continueCursor: "" };
}
throw new Error(`unexpected family ${String(args.family)}`);
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins?limit=7"),
);
expect(response.status).toBe(200);
expect((await response.json()).items.map((entry: { name: string }) => entry.name)).toEqual([
"code-plugin",
"bundle-plugin",
]);
const families = runQuery.mock.calls.map(([, args]) => (args as { family?: string }).family);
expect(families).toEqual(["code-plugin", "bundle-plugin"]);
for (const [, args] of runQuery.mock.calls) {
expect(args).toEqual(
expect.objectContaining({
paginationOpts: { cursor: null, numItems: 7 },
}),
);
}
});
it("plugins list paginates with separate plugin family cursors", async () => {
const codeNewest = makeCatalogItem("code-newest", {
family: "code-plugin",
updatedAt: 300,
});
const codeOlder = makeCatalogItem("code-older", {
family: "code-plugin",
updatedAt: 100,
});
const bundleMiddle = makeCatalogItem("bundle-middle", {
family: "bundle-plugin",
updatedAt: 200,
});
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
const pagination = args.paginationOpts as { cursor: string | null };
if (args.family === "code-plugin" && pagination.cursor === null) {
return { page: [codeNewest], isDone: false, continueCursor: "code-cursor" };
}
if (args.family === "code-plugin" && pagination.cursor === "code-cursor") {
return { page: [codeOlder], isDone: true, continueCursor: "" };
}
if (args.family === "bundle-plugin" && pagination.cursor === null) {
return { page: [bundleMiddle], isDone: true, continueCursor: "" };
}
throw new Error(`unexpected args ${JSON.stringify(args)}`);
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const firstResponse = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins?limit=1"),
);
expect(firstResponse.status).toBe(200);
const firstJson = await firstResponse.json();
expect(firstJson.items.map((entry: { name: string }) => entry.name)).toEqual(["code-newest"]);
expect(firstJson.nextCursor).toMatch(/^pkgplugins:/);
const secondUrl = new URL("https://example.com/api/v1/plugins");
secondUrl.searchParams.set("limit", "1");
secondUrl.searchParams.set("cursor", firstJson.nextCursor);
const secondResponse = await __handlers.listPluginsV1Handler(
makeCtx({ runQuery, runMutation }),
new Request(secondUrl),
);
expect(secondResponse.status).toBe(200);
const secondJson = await secondResponse.json();
expect(secondJson.items.map((entry: { name: string }) => entry.name)).toEqual([
"bundle-middle",
]);
const packageCalls = runQuery.mock.calls
.map(([, args]) => args as { family?: string; paginationOpts?: { cursor: string | null } })
.filter((args) => args.family === "code-plugin" || args.family === "bundle-plugin");
expect(
packageCalls.map((args) => ({
family: args.family,
cursor: args.paginationOpts?.cursor ?? null,
})),
).toEqual([
{ family: "code-plugin", cursor: null },
{ family: "bundle-plugin", cursor: null },
{ family: "code-plugin", cursor: "code-cursor" },
{ family: "bundle-plugin", cursor: null },
]);
});
it("packages search supports family=skill on the generic route", async () => {
const runQuery = vi.fn().mockResolvedValue([]);
const runMutation = vi.fn().mockResolvedValue(okRate());
@@ -2641,6 +2781,114 @@ describe("httpApiV1 handlers", () => {
);
});
it("plugins search defaults to plugin package families", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (args.family === "code-plugin") {
return {
page: [
{
name: "weather-code",
displayName: "Weather Code",
family: "code-plugin",
channel: "community",
isOfficial: false,
createdAt: 10,
updatedAt: 100,
},
],
isDone: true,
continueCursor: "",
};
}
if (args.family === "bundle-plugin") {
return {
page: [
{
name: "weather-bundle",
displayName: "Weather Bundle",
family: "bundle-plugin",
channel: "community",
isOfficial: false,
createdAt: 20,
updatedAt: 200,
},
],
isDone: true,
continueCursor: "",
};
}
throw new Error(`unexpected family ${String(args.family)}`);
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.pluginsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins/search?q=weather&limit=7"),
);
expect(response.status).toBe(200);
expect(
(await response.json()).results.map(
(entry: { package: { name: string } }) => entry.package.name,
),
).toEqual(["weather-bundle", "weather-code"]);
const families = runQuery.mock.calls.map(([, args]) => (args as { family?: string }).family);
expect(families).toEqual(["code-plugin", "bundle-plugin"]);
for (const [, args] of runQuery.mock.calls) {
expect(args).toEqual(
expect.objectContaining({
paginationOpts: { cursor: null, numItems: 50 },
}),
);
}
});
it("plugins search dedupes and sorts results from both plugin families", async () => {
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (args.family === "code-plugin") {
return {
page: [
makeCatalogItem("shared-plugin", { family: "code-plugin", updatedAt: 100 }),
makeCatalogItem("plugin-code", { family: "code-plugin", updatedAt: 50 }),
],
isDone: true,
continueCursor: "",
};
}
if (args.family === "bundle-plugin") {
return {
page: [
makeCatalogItem("plugin-bundle", { family: "bundle-plugin", updatedAt: 80 }),
makeCatalogItem("shared-plugin", { family: "bundle-plugin", updatedAt: 60 }),
],
isDone: true,
continueCursor: "",
};
}
throw new Error(`unexpected family ${String(args.family)}`);
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.pluginsGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/plugins/search?q=plugin&limit=3"),
);
expect(response.status).toBe(200);
expect(
(await response.json()).results.map(
(entry: { score: number; package: { family: string; name: string } }) => ({
family: entry.package.family,
name: entry.package.name,
}),
),
).toEqual([
{ family: "bundle-plugin", name: "plugin-bundle" },
{ family: "code-plugin", name: "plugin-code" },
{ family: "code-plugin", name: "shared-plugin" },
]);
});
it("packages list forwards viewerUserId for authenticated private package browsing", async () => {
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
const runQuery = vi.fn().mockResolvedValue({ page: [], isDone: true, continueCursor: "" });
@@ -2664,7 +2912,12 @@ describe("httpApiV1 handlers", () => {
it("packages search forwards viewerUserId for authenticated private package search", async () => {
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
const runQuery = vi.fn().mockResolvedValue([]);
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if ("userId" in args) return { _id: args.userId };
if ("paginationOpts" in args) return { page: [], isDone: true, continueCursor: "" };
if ("query" in args) return [];
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
@@ -2676,9 +2929,9 @@ describe("httpApiV1 handlers", () => {
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
query: "secret",
channel: "private",
viewerUserId: "users:owner",
paginationOpts: { cursor: null, numItems: 50 },
}),
);
});
@@ -2710,7 +2963,8 @@ describe("httpApiV1 handlers", () => {
if (query === internal.users.getByIdInternal) {
throw new Error("Table mismatch");
}
if ("query" in args && args.query === "secret") return [];
if ("paginationOpts" in args) return { page: [], isDone: true, continueCursor: "" };
if ("query" in args) return [];
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
@@ -2728,9 +2982,9 @@ describe("httpApiV1 handlers", () => {
expect(runQuery).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
query: "secret",
channel: "community",
viewerUserId: undefined,
paginationOpts: { cursor: null, numItems: 50 },
}),
);
});
+257 -18
View File
@@ -251,6 +251,11 @@ type UnifiedCatalogCursorState = {
skills: CatalogSourceCursorState;
};
type PluginCatalogCursorState = {
codePlugins: CatalogSourceCursorState;
bundlePlugins: CatalogSourceCursorState;
};
type CatalogPageResult = {
page: CatalogListItem[];
isDone: boolean;
@@ -265,6 +270,7 @@ type CatalogSourceState = {
};
const UNIFIED_CATALOG_CURSOR_PREFIX = "pkgcatalog:";
const PLUGIN_CATALOG_CURSOR_PREFIX = "pkgplugins:";
function defaultCatalogSourceCursorState(): CatalogSourceCursorState {
return { cursor: null, offset: 0, pageSize: null, done: false };
@@ -305,6 +311,42 @@ function decodeUnifiedCatalogCursor(raw: string | null | undefined): UnifiedCata
}
}
function encodePluginCatalogCursor(state: PluginCatalogCursorState) {
return `${PLUGIN_CATALOG_CURSOR_PREFIX}${JSON.stringify(state)}`;
}
function decodePluginCatalogCursor(raw: string | null | undefined): PluginCatalogCursorState {
const normalize = (
input: Partial<CatalogSourceCursorState> | undefined,
): CatalogSourceCursorState => ({
cursor: typeof input?.cursor === "string" ? input.cursor : null,
offset: typeof input?.offset === "number" && input.offset > 0 ? input.offset : 0,
pageSize: typeof input?.pageSize === "number" && input.pageSize > 0 ? input.pageSize : null,
done: input?.done === true,
});
if (!raw?.startsWith(PLUGIN_CATALOG_CURSOR_PREFIX)) {
return {
codePlugins: { ...defaultCatalogSourceCursorState(), cursor: raw ?? null },
bundlePlugins: defaultCatalogSourceCursorState(),
};
}
try {
const parsed = JSON.parse(
raw.slice(PLUGIN_CATALOG_CURSOR_PREFIX.length),
) as Partial<PluginCatalogCursorState>;
return {
codePlugins: normalize(parsed.codePlugins),
bundlePlugins: normalize(parsed.bundlePlugins),
};
} catch {
return {
codePlugins: defaultCatalogSourceCursorState(),
bundlePlugins: defaultCatalogSourceCursorState(),
};
}
}
function initCatalogSource(state: CatalogSourceCursorState): CatalogSourceState {
return {
state: { ...state },
@@ -321,7 +363,7 @@ function finalizeCatalogSource(source: CatalogSourceState): CatalogSourceCursorS
cursor: source.pageCursor,
offset: source.index,
pageSize: source.state.pageSize,
done: source.page.isDone,
done: false,
};
}
return {
@@ -369,6 +411,100 @@ function compareCatalogItems(a: CatalogListItem, b: CatalogListItem) {
return a.name.localeCompare(b.name);
}
const HTTP_PACKAGE_SEARCH_PAGE_SIZE = 50;
const HTTP_PACKAGE_SEARCH_SCAN_PAGES = 20;
function catalogSearchScore(item: CatalogListItem, queryText: string) {
const needle = queryText.toLowerCase();
const name = item.name.toLowerCase();
const display = item.displayName.toLowerCase();
const runtimeId = item.runtimeId?.toLowerCase() ?? "";
const summary = (item.summary ?? "").toLowerCase();
let score = 0;
if (name === needle) score += 200;
else if (name.startsWith(needle)) score += 120;
else if (name.includes(needle)) score += 80;
if (display === needle) score += 150;
else if (display.startsWith(needle)) score += 70;
else if (display.includes(needle)) score += 40;
if (runtimeId === needle) score += 180;
else if (runtimeId.startsWith(needle)) score += 90;
else if (runtimeId.includes(needle)) score += 45;
if (summary.includes(needle)) score += 20;
if ((item.capabilityTags ?? []).some((entry) => entry.toLowerCase().includes(needle))) {
score += 12;
}
if (item.isOfficial) score += 5;
return score;
}
function compareCatalogSearchEntries(a: CatalogSearchEntry, b: CatalogSearchEntry) {
return (
b.score - a.score ||
Number(b.package.isOfficial) - Number(a.package.isOfficial) ||
compareCatalogItems(a.package, b.package)
);
}
async function searchPackageCatalogByListing(
ctx: ActionCtx,
args: {
query: string;
limit: number;
family?: "skill" | "code-plugin" | "bundle-plugin";
channel?: "official" | "community" | "private";
isOfficial?: boolean;
executesCode?: boolean;
capabilityTag?: string;
viewerUserId?: Id<"users">;
},
): Promise<CatalogSearchEntry[]> {
const queryText = args.query.trim().toLowerCase();
if (!queryText) return [];
const matches: CatalogSearchEntry[] = [];
const seen = new Set<string>();
let cursor: string | null = null;
let done = false;
let loops = 0;
while (!done && loops < HTTP_PACKAGE_SEARCH_SCAN_PAGES) {
loops += 1;
const result: {
page: CatalogListItem[];
isDone: boolean;
continueCursor: string | null;
} = await runQueryRef(ctx, internalRefs.packages.listPageForViewerInternal, {
family: args.family,
channel: args.channel,
isOfficial: args.isOfficial,
executesCode: args.executesCode,
capabilityTag: args.capabilityTag,
viewerUserId: args.viewerUserId,
paginationOpts: { cursor, numItems: HTTP_PACKAGE_SEARCH_PAGE_SIZE },
});
for (const item of result.page) {
const key = `${item.family}:${item.name}`;
if (seen.has(key)) continue;
const score = catalogSearchScore(item, queryText);
if (score <= 0) continue;
seen.add(key);
matches.push({ score, package: item });
}
done = result.isDone;
cursor = result.continueCursor;
if (!cursor && !done) break;
}
return matches.sort(compareCatalogSearchEntries).slice(0, args.limit);
}
async function resolveSkillTags(
ctx: ActionCtx,
tags: Record<string, Id<"skillVersions">>,
@@ -501,7 +637,7 @@ async function listPackages(
ctx: ActionCtx,
request: Request,
family?: PackageListQueryArgs["family"],
options?: { includeSkills?: boolean },
options?: { includeSkills?: boolean; pluginFamilies?: Array<"code-plugin" | "bundle-plugin"> },
) {
const rate = await applyRateLimit(ctx, request, "read");
if (!rate.ok) return rate.response;
@@ -628,6 +764,84 @@ async function listPackages(
);
}
if (!effectiveFamily && options?.pluginFamilies?.length) {
const decodedCursor = decodePluginCatalogCursor(cursor);
const codePluginSource = initCatalogSource(decodedCursor.codePlugins);
const bundlePluginSource = initCatalogSource(decodedCursor.bundlePlugins);
const pageSize = limit;
const items: CatalogListItem[] = [];
const fetchPluginPage = async (
pluginFamily: "code-plugin" | "bundle-plugin",
pageCursor: string | null,
numItems: number,
) => {
const result = await runQueryRef<{
page: CatalogListItem[];
isDone: boolean;
continueCursor: string | null;
}>(ctx, internalRefs.packages.listPageForViewerInternal, {
family: pluginFamily,
channel,
isOfficial,
executesCode,
capabilityTag,
viewerUserId: viewerUserId ?? undefined,
paginationOpts: { cursor: pageCursor, numItems },
});
return {
page: result.page,
isDone: result.isDone,
continueCursor: result.continueCursor ?? "",
};
};
while (items.length < limit) {
const [codePluginCandidate, bundlePluginCandidate] = await Promise.all([
options.pluginFamilies.includes("code-plugin")
? ensureCatalogSourcePage(codePluginSource, pageSize, (pageCursor, numItems) =>
fetchPluginPage("code-plugin", pageCursor, numItems),
)
: Promise.resolve(null),
options.pluginFamilies.includes("bundle-plugin")
? ensureCatalogSourcePage(bundlePluginSource, pageSize, (pageCursor, numItems) =>
fetchPluginPage("bundle-plugin", pageCursor, numItems),
)
: Promise.resolve(null),
]);
if (!codePluginCandidate && !bundlePluginCandidate) break;
if (
!bundlePluginCandidate ||
(codePluginCandidate &&
compareCatalogItems(codePluginCandidate, bundlePluginCandidate) <= 0)
) {
items.push(codePluginCandidate!);
codePluginSource.index += 1;
} else {
items.push(bundlePluginCandidate);
bundlePluginSource.index += 1;
}
}
const nextState = {
codePlugins: finalizeCatalogSource(codePluginSource),
bundlePlugins: finalizeCatalogSource(bundlePluginSource),
};
const isDoneAll =
nextState.codePlugins.done &&
nextState.codePlugins.offset === 0 &&
nextState.bundlePlugins.done &&
nextState.bundlePlugins.offset === 0;
return json(
{
items,
nextCursor: isDoneAll ? null : encodePluginCatalogCursor(nextState),
},
200,
rate.headers,
);
}
const result = await runQueryRef<{
page: unknown[];
isDone: boolean;
@@ -653,7 +867,10 @@ export async function listPackagesV1Handler(ctx: ActionCtx, request: Request) {
}
export async function listPluginsV1Handler(ctx: ActionCtx, request: Request) {
return await listPackages(ctx, request, undefined, { includeSkills: false });
return await listPackages(ctx, request, undefined, {
includeSkills: false,
pluginFamilies: ["code-plugin", "bundle-plugin"],
});
}
export async function listCodePluginsV1Handler(ctx: ActionCtx, request: Request) {
@@ -1050,7 +1267,7 @@ async function getSkillVersionForRequest(
async function searchPackages(
ctx: ActionCtx,
request: Request,
options?: { includeSkills?: boolean },
options?: { includeSkills?: boolean; pluginFamilies?: Array<"code-plugin" | "bundle-plugin"> },
) {
const rate = await applyRateLimit(ctx, request, "read");
if (!rate.ok) return rate.response;
@@ -1093,10 +1310,34 @@ async function searchPackages(
},
);
} else if (family || !includeSkills) {
results = await runQueryRef<CatalogSearchEntry[]>(
ctx,
internalRefs.packages.searchForViewerInternal,
{
if (!family && options?.pluginFamilies?.length) {
const pluginResults = await Promise.all(
options.pluginFamilies.map((pluginFamily) =>
searchPackageCatalogByListing(ctx, {
query: queryText,
limit,
family: pluginFamily,
channel,
isOfficial,
executesCode,
capabilityTag,
viewerUserId: viewerUserId ?? undefined,
}),
),
);
const seen = new Set<string>();
results = pluginResults
.flat()
.filter((entry) => {
const key = `${entry.package.family}:${entry.package.name}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
})
.sort(compareCatalogSearchEntries)
.slice(0, limit);
} else {
results = await searchPackageCatalogByListing(ctx, {
query: queryText,
limit,
family,
@@ -1105,11 +1346,11 @@ async function searchPackages(
executesCode,
capabilityTag,
viewerUserId: viewerUserId ?? undefined,
},
);
});
}
} else {
const [packageResults, skillResults] = await Promise.all([
runQueryRef<CatalogSearchEntry[]>(ctx, internalRefs.packages.searchForViewerInternal, {
searchPackageCatalogByListing(ctx, {
query: queryText,
limit,
channel,
@@ -1135,12 +1376,7 @@ async function searchPackages(
seen.add(key);
return true;
})
.sort(
(a, b) =>
b.score - a.score ||
Number(b.package.isOfficial) - Number(a.package.isOfficial) ||
compareCatalogItems(a.package, b.package),
)
.sort(compareCatalogSearchEntries)
.slice(0, limit);
}
return json({ results }, 200, rate.headers);
@@ -1450,7 +1686,10 @@ export async function pluginsGetRouterV1Handler(ctx: ActionCtx, request: Request
const segments = getPathSegments(request, "/api/v1/plugins/");
if (segments.length === 0) return text("Not found", 404);
if (segments[0] === "search" && new URL(request.url).searchParams.has("q")) {
return await searchPackages(ctx, request, { includeSkills: false });
return await searchPackages(ctx, request, {
includeSkills: false,
pluginFamilies: ["code-plugin", "bundle-plugin"],
});
}
return text("Not found", 404);
}
+56
View File
@@ -578,4 +578,60 @@ describe("moderationEngine", () => {
expect(snapshot.reasonCodes).toContain("suspicious.env_credential_access");
expect(snapshot.reasonCodes).toContain("suspicious.vt_suspicious");
});
it("does not let uncorroborated VT Code Insight suspicious override clean local scans", () => {
const snapshot = buildModerationSnapshot({
staticScan: {
status: "clean",
reasonCodes: [],
findings: [],
summary: "",
engineVersion: "v2.1.1",
checkedAt: Date.now(),
},
vtAnalysis: {
status: "suspicious",
scanner: "code_insight",
source: "VirusTotal Code Insight",
engineStats: {
malicious: 0,
suspicious: 0,
harmless: 12,
undetected: 54,
},
},
llmStatus: "clean",
});
expect(snapshot.verdict).toBe("clean");
expect(snapshot.reasonCodes).toEqual([]);
});
it("keeps VT Code Insight suspicious when AV engines also report suspicious", () => {
const snapshot = buildModerationSnapshot({
staticScan: {
status: "clean",
reasonCodes: [],
findings: [],
summary: "",
engineVersion: "v2.1.1",
checkedAt: Date.now(),
},
vtAnalysis: {
status: "suspicious",
scanner: "code_insight",
source: "VirusTotal Code Insight",
engineStats: {
malicious: 0,
suspicious: 1,
harmless: 12,
undetected: 53,
},
},
llmStatus: "clean",
});
expect(snapshot.verdict).toBe("suspicious");
expect(snapshot.reasonCodes).toContain("suspicious.vt_suspicious");
});
});
+58 -3
View File
@@ -13,6 +13,22 @@ import {
} from "./moderationReasonCodes";
type TextFile = { path: string; content: string };
type VirusTotalEngineStats = {
malicious?: number;
suspicious?: number;
undetected?: number;
harmless?: number;
};
type VirusTotalAnalysis = {
status?: string;
scanner?: string;
source?: string;
engineStats?: VirusTotalEngineStats;
metadata?: {
stats?: VirusTotalEngineStats;
};
};
export type StaticScanInput = {
slug: string;
@@ -453,11 +469,42 @@ function dedupeEvidence(evidence: ModerationFinding[]) {
return out.slice(0, 40);
}
function addScannerStatusReason(reasonCodes: string[], scanner: "vt" | "llm", status?: string) {
function isStaticScanClean(staticScan: StaticScanResult | undefined) {
// Older moderation records can predate static scan persistence; absence means
// there are no static findings available to corroborate an external signal.
return !staticScan || staticScan.reasonCodes.length === 0 || staticScan.status === "clean";
}
function isAvEngineStatsClean(stats: VirusTotalEngineStats | undefined) {
if (!stats) return false;
return (stats.malicious ?? 0) === 0 && (stats.suspicious ?? 0) === 0;
}
function getVtEngineStats(analysis: VirusTotalAnalysis | undefined) {
return analysis?.engineStats ?? analysis?.metadata?.stats;
}
function isUncorroboratedVtCodeInsightSuspicious(params: {
vtAnalysis?: VirusTotalAnalysis;
staticScan?: StaticScanResult;
llmStatus?: string;
}) {
if (params.vtAnalysis?.scanner !== "code_insight") return false;
if (!isExternalScannerClean(params.llmStatus)) return false;
if (!isStaticScanClean(params.staticScan)) return false;
return isAvEngineStatsClean(getVtEngineStats(params.vtAnalysis));
}
function addScannerStatusReason(
reasonCodes: string[],
scanner: "vt" | "llm",
status?: string,
options: { suppressSuspicious?: boolean } = {},
) {
const normalized = status?.trim().toLowerCase();
if (normalized === "malicious") {
reasonCodes.push(`malicious.${scanner}_malicious`);
} else if (normalized === "suspicious") {
} else if (normalized === "suspicious" && !options.suppressSuspicious) {
reasonCodes.push(`suspicious.${scanner}_suspicious`);
}
}
@@ -534,6 +581,7 @@ function isExternalScannerClean(status: string | undefined): boolean {
export function buildModerationSnapshot(params: {
staticScan?: StaticScanResult;
vtAnalysis?: VirusTotalAnalysis;
vtStatus?: string;
llmStatus?: string;
sourceVersionId?: Id<"skillVersions">;
@@ -551,7 +599,14 @@ export function buildModerationSnapshot(params: {
}
const reasonCodes = [...staticCodes];
addScannerStatusReason(reasonCodes, "vt", params.vtStatus);
const vtStatus = params.vtStatus ?? params.vtAnalysis?.status;
addScannerStatusReason(reasonCodes, "vt", vtStatus, {
suppressSuspicious: isUncorroboratedVtCodeInsightSuspicious({
vtAnalysis: params.vtAnalysis,
staticScan: params.staticScan,
llmStatus: params.llmStatus,
}),
});
addScannerStatusReason(reasonCodes, "llm", params.llmStatus);
const normalizedCodes = normalizeReasonCodes(reasonCodes);
+10 -30
View File
@@ -956,36 +956,11 @@ describe("packages public queries", () => {
expect(result.page.map((entry) => entry.name)).toEqual(["official-demo"]);
});
it("keeps scanning official-only listings without a family filter", async () => {
const { ctx } = makeDigestCtx({
it("uses the official index for official-only listings without a family filter", async () => {
const { ctx, indexNames, paginate } = makeDigestCtx({
pages: [
{
page: [makeDigest("noise-1", { isOfficial: false })],
isDone: false,
continueCursor: "cursor:1",
},
{
page: [makeDigest("noise-2", { isOfficial: false })],
isDone: false,
continueCursor: "cursor:2",
},
{
page: [makeDigest("noise-3", { isOfficial: false })],
isDone: false,
continueCursor: "cursor:3",
},
{
page: [makeDigest("noise-4", { isOfficial: false })],
isDone: false,
continueCursor: "cursor:4",
},
{
page: [makeDigest("noise-5", { isOfficial: false })],
isDone: false,
continueCursor: "cursor:5",
},
{
page: [makeDigest("official-late", { isOfficial: true, updatedAt: 10 })],
page: [makeDigest("official-late", { isOfficial: true })],
isDone: true,
continueCursor: "",
},
@@ -998,6 +973,8 @@ describe("packages public queries", () => {
});
expect(result.page.map((entry) => entry.name)).toEqual(["official-late"]);
expect(indexNames).toEqual(["by_active_official_updated"]);
expect(paginate).toHaveBeenCalledTimes(1);
});
it("filters private packages and capability flags in public search", async () => {
@@ -1375,7 +1352,7 @@ describe("packages public queries", () => {
expect(ctx.db.query).not.toHaveBeenCalledWith("publisherMembers");
});
it("caps public list scans below the Convex read limit budget", async () => {
it("keeps public list pages to one paginated query per invocation", async () => {
const { ctx, paginate } = makeDigestCtx({
pages: Array.from({ length: 120 }, (_, index) => ({
page: [makeDigest(`noise-${index}`, { executesCode: false })],
@@ -1390,7 +1367,10 @@ describe("packages public queries", () => {
});
expect(result.page).toEqual([]);
expect(paginate).toHaveBeenCalledTimes(100);
expect(result.isDone).toBe(false);
expect(result.continueCursor).toBeTruthy();
expect(paginate).toHaveBeenCalledTimes(1);
expect(paginate).toHaveBeenCalledWith({ cursor: null, numItems: 100 });
});
it("caps public search scans below the Convex read limit budget", async () => {
+86 -86
View File
@@ -63,13 +63,27 @@ import {
finalizeInProgressRescanRequestsForTarget,
} from "./model/rescans/policy";
const MAX_PACKAGE_SCAN_DOCUMENTS = 30_000;
const MAX_PUBLIC_LIST_SCAN_PAGES = 200;
const MAX_PUBLIC_LIST_PAGE_SIZE = 200;
const MAX_SEARCH_PAGE_SIZE = 200;
const MAX_SEARCH_SCAN_DOCUMENTS = 1_000;
const MAX_SEARCH_SCAN_PAGES = 20;
const MAX_DIRECT_PACKAGE_SEARCH_CANDIDATES = 20;
const INITIAL_PACKAGE_VT_SCAN_DELAY_MS = 30_000;
const vtEngineStatsValidator = v.object({
malicious: v.optional(v.number()),
suspicious: v.optional(v.number()),
undetected: v.optional(v.number()),
harmless: v.optional(v.number()),
});
const vtAnalysisValidator = v.object({
status: v.string(),
verdict: v.optional(v.string()),
analysis: v.optional(v.string()),
source: v.optional(v.string()),
scanner: v.optional(v.string()),
engineStats: v.optional(vtEngineStatsValidator),
checkedAt: v.number(),
});
const internalRefs = internal as unknown as {
llmEval: {
evaluatePackageReleaseWithLlm: unknown;
@@ -1194,90 +1208,84 @@ async function listPackagePageImpl(
const targetCount = args.paginationOpts.numItems;
const collected: PublicPackageListItem[] = [];
const decodedCursor = decodePublicPageCursor(args.paginationOpts.cursor);
let cursor = decodedCursor.cursor;
let offset = decodedCursor.offset;
let pageSize = decodedCursor.pageSize;
let done = decodedCursor.done;
let loops = 0;
let remainingScanBudget = MAX_PACKAGE_SCAN_DOCUMENTS;
if (decodedCursor.done && decodedCursor.offset === 0) {
return { page: collected, isDone: true, continueCursor: "" };
}
const pageCursor = decodedCursor.cursor;
const offset = decodedCursor.offset;
const effectivePageSize = Math.min(
MAX_PUBLIC_LIST_PAGE_SIZE,
Math.max(
targetCount,
decodedCursor.pageSize ?? 0,
offset > 0 ? offset + targetCount : targetCount,
),
);
const family = args.family;
const channel = args.channel;
const isOfficial = args.isOfficial;
while (
(offset > 0 || !done) &&
collected.length < targetCount &&
loops < MAX_PUBLIC_LIST_SCAN_PAGES &&
remainingScanBudget > 0
) {
loops += 1;
const effectivePageSize = Math.min(
remainingScanBudget,
offset > 0 && pageSize
? Math.max(pageSize, offset + 1)
: Math.max(targetCount * 3, targetCount),
);
if (effectivePageSize <= 0) break;
remainingScanBudget -= effectivePageSize;
const pageCursor = cursor;
const builder = args.capabilityTag
? buildPackageCapabilityDigestQuery(ctx, {
capabilityTag: args.capabilityTag,
family,
channel,
isOfficial,
executesCode: args.executesCode,
})
: buildPackageDigestQuery(ctx, {
family,
channel,
isOfficial,
executesCode: args.executesCode,
});
const page: {
page: PackageDigestLike[];
isDone: boolean;
continueCursor: string;
} = await builder.order("desc").paginate({ cursor: pageCursor, numItems: effectivePageSize });
for (let index = offset; index < page.page.length; index += 1) {
const digest = page.page[index] as PackageDigestLike;
if (!(await canViewPackage(digest))) continue;
if (channel && digest.channel !== channel) continue;
if (typeof isOfficial === "boolean" && digest.isOfficial !== isOfficial) {
continue;
}
if (!digestMatchesFilters(digest, args)) continue;
collected.push(toPublicPackageListItem(digest));
if (collected.length >= targetCount) {
const nextOffset = index + 1;
if (nextOffset < page.page.length) {
cursor = pageCursor;
offset = nextOffset;
pageSize = effectivePageSize;
done = page.isDone;
} else {
cursor = page.continueCursor;
offset = 0;
pageSize = effectivePageSize;
done = page.isDone;
}
return {
page: collected,
isDone: done && offset === 0,
continueCursor: encodePublicPageCursor({ cursor, offset, pageSize, done }),
};
}
const builder = args.capabilityTag
? buildPackageCapabilityDigestQuery(ctx, {
capabilityTag: args.capabilityTag,
family,
channel,
isOfficial,
executesCode: args.executesCode,
})
: buildPackageDigestQuery(ctx, {
family,
channel,
isOfficial,
executesCode: args.executesCode,
});
const page: {
page: PackageDigestLike[];
isDone: boolean;
continueCursor: string;
} = await builder.order("desc").paginate({ cursor: pageCursor, numItems: effectivePageSize });
for (let index = offset; index < page.page.length; index += 1) {
const digest = page.page[index] as PackageDigestLike;
if (!(await canViewPackage(digest))) continue;
if (channel && digest.channel !== channel) continue;
if (typeof isOfficial === "boolean" && digest.isOfficial !== isOfficial) {
continue;
}
if (!digestMatchesFilters(digest, args)) continue;
collected.push(toPublicPackageListItem(digest));
if (collected.length >= targetCount) {
const nextOffset = index + 1;
const nextState =
nextOffset < page.page.length
? {
cursor: pageCursor,
offset: nextOffset,
pageSize: effectivePageSize,
done: page.isDone,
}
: {
cursor: page.continueCursor,
offset: 0,
pageSize: effectivePageSize,
done: page.isDone,
};
return {
page: collected,
isDone: nextState.done && nextState.offset === 0,
continueCursor: encodePublicPageCursor(nextState),
};
}
done = page.isDone;
cursor = page.continueCursor;
offset = 0;
pageSize = effectivePageSize;
}
return {
page: collected,
isDone: done,
continueCursor: encodePublicPageCursor({ cursor, offset, pageSize, done }),
isDone: page.isDone,
continueCursor: encodePublicPageCursor({
cursor: page.continueCursor,
offset: 0,
pageSize: effectivePageSize,
done: page.isDone,
}),
};
}
@@ -2406,15 +2414,7 @@ export const updateReleaseScanResultsInternal = internalMutation({
args: {
releaseId: v.id("packageReleases"),
sha256hash: v.optional(v.string()),
vtAnalysis: v.optional(
v.object({
status: v.string(),
verdict: v.optional(v.string()),
analysis: v.optional(v.string()),
source: v.optional(v.string()),
checkedAt: v.number(),
}),
),
vtAnalysis: v.optional(vtAnalysisValidator),
},
handler: async (ctx, args) => {
const release = await ctx.db.get(args.releaseId);
+19 -18
View File
@@ -12,6 +12,23 @@ const manualModerationOverride = v.object({
updatedAt: v.number(),
});
const vtEngineStatsValidator = v.object({
malicious: v.optional(v.number()),
suspicious: v.optional(v.number()),
undetected: v.optional(v.number()),
harmless: v.optional(v.number()),
});
const vtAnalysisValidator = v.object({
status: v.string(),
verdict: v.optional(v.string()),
analysis: v.optional(v.string()),
source: v.optional(v.string()),
scanner: v.optional(v.string()),
engineStats: v.optional(vtEngineStatsValidator),
checkedAt: v.number(),
});
const users = defineTable({
name: v.optional(v.string()),
image: v.optional(v.string()),
@@ -429,15 +446,7 @@ const skillVersions = defineTable({
createdAt: v.number(),
softDeletedAt: v.optional(v.number()),
sha256hash: v.optional(v.string()),
vtAnalysis: v.optional(
v.object({
status: v.string(),
verdict: v.optional(v.string()),
analysis: v.optional(v.string()),
source: v.optional(v.string()),
checkedAt: v.number(),
}),
),
vtAnalysis: v.optional(vtAnalysisValidator),
llmAnalysis: v.optional(
v.object({
status: v.string(),
@@ -704,15 +713,7 @@ const packageReleases = defineTable({
capabilities: packageCapabilitiesValidator,
verification: packageVerificationValidator,
sha256hash: v.optional(v.string()),
vtAnalysis: v.optional(
v.object({
status: v.string(),
verdict: v.optional(v.string()),
analysis: v.optional(v.string()),
source: v.optional(v.string()),
checkedAt: v.number(),
}),
),
vtAnalysis: v.optional(vtAnalysisValidator),
llmAnalysis: v.optional(
v.object({
status: v.string(),
+62
View File
@@ -436,4 +436,66 @@ describe("skills manual overrides", () => {
}),
);
});
it("clears legacy suspicious state when LLM corroborates clean VT Code Insight-only suspicious", async () => {
const now = 1_700_000_400_000;
vi.spyOn(Date, "now").mockReturnValue(now);
const skill = {
_id: "skills:1",
ownerUserId: "users:owner",
latestVersionId: "skillVersions:9",
softDeletedAt: undefined,
moderationStatus: "hidden",
moderationReason: "scanner.vt.suspicious",
moderationVerdict: "suspicious",
moderationFlags: ["flagged.suspicious"],
};
const version = {
_id: "skillVersions:9",
skillId: "skills:1",
staticScan: {
status: "clean",
reasonCodes: [],
findings: [],
summary: "",
engineVersion: "v2.1.1",
checkedAt: now - 200,
},
vtAnalysis: {
status: "suspicious",
scanner: "code_insight",
engineStats: {
malicious: 0,
suspicious: 0,
harmless: 12,
undetected: 54,
},
checkedAt: now - 100,
},
llmAnalysis: undefined,
};
const { ctx, patch } = makeCtx({ skill, version });
await updateVersionLlmAnalysisInternalHandler(ctx, {
versionId: "skillVersions:9",
llmAnalysis: {
status: "clean",
checkedAt: now,
},
});
expect(patch).toHaveBeenCalledWith(
"skills:1",
expect.objectContaining({
moderationStatus: "active",
moderationReason: "scanner.aggregate.clean",
moderationFlags: undefined,
moderationVerdict: "clean",
moderationReasonCodes: undefined,
isSuspicious: false,
}),
);
});
});
+86
View File
@@ -1266,6 +1266,92 @@ describe("skills anti-spam guards", () => {
);
});
it("vt suspicious escalation clears legacy quarantine for uncorroborated Code Insight", async () => {
const patch = vi.fn(async () => {});
const version = {
_id: "skillVersions:1",
skillId: "skills:1",
staticScan: {
status: "clean",
reasonCodes: [],
findings: [],
summary: "",
engineVersion: "v2.1.1",
checkedAt: Date.now(),
},
vtAnalysis: {
status: "suspicious",
scanner: "code_insight",
engineStats: {
malicious: 0,
suspicious: 0,
harmless: 12,
undetected: 54,
},
},
llmAnalysis: { status: "clean" },
};
const skill = {
_id: "skills:1",
slug: "doc-only",
ownerUserId: "users:owner",
latestVersionId: "skillVersions:1",
moderationStatus: "hidden",
moderationFlags: ["flagged.suspicious"],
moderationReason: "scanner.vt.suspicious",
};
const owner = {
_id: "users:owner",
role: "user",
deletedAt: undefined,
};
const db = {
get: vi.fn(async (id: string) => {
if (id === "skills:1") return skill;
if (id === "users:owner") return owner;
return null;
}),
query: vi.fn((table: string) => {
const globalStatsQuery = buildGlobalStatsQuery(table);
if (globalStatsQuery) return globalStatsQuery;
const digestQuery = buildDigestQuery(table);
if (digestQuery) return digestQuery;
if (table === "skillVersions") {
return {
withIndex: () => ({
unique: async () => version,
}),
};
}
throw new Error(`unexpected table ${table}`);
}),
patch,
insert: vi.fn(),
normalizeId: vi.fn(),
};
await escalateByVtHandler(
{ db, scheduler: { runAfter: vi.fn() } } as never,
{
sha256hash: "h".repeat(64),
status: "suspicious",
} as never,
);
expect(patch).toHaveBeenCalledWith(
"skills:1",
expect.objectContaining({
moderationStatus: "active",
moderationFlags: undefined,
moderationReason: "scanner.vt.clean",
moderationVerdict: "clean",
moderationReasonCodes: undefined,
isSuspicious: false,
}),
);
});
it("ignores vt escalation for non-latest versions", async () => {
const patch = vi.fn(async () => {});
const version = {
+54 -15
View File
@@ -144,8 +144,26 @@ const USER_MODERATION_REASON = "user.moderation";
const SKILL_CATALOG_CURSOR_PREFIX = "skillcat:";
const SKILL_CAPABILITY_TAG_SET = new Set<string>(SKILL_CAPABILITY_TAGS);
const vtEngineStatsValidator = v.object({
malicious: v.optional(v.number()),
suspicious: v.optional(v.number()),
undetected: v.optional(v.number()),
harmless: v.optional(v.number()),
});
const vtAnalysisValidator = v.object({
status: v.string(),
verdict: v.optional(v.string()),
analysis: v.optional(v.string()),
source: v.optional(v.string()),
scanner: v.optional(v.string()),
engineStats: v.optional(vtEngineStatsValidator),
checkedAt: v.number(),
});
function buildStructuredModerationPatch(params: {
staticScan?: Doc<"skillVersions">["staticScan"];
vtAnalysis?: Doc<"skillVersions">["vtAnalysis"];
vtStatus?: string;
llmStatus?: string;
sourceVersionId?: Id<"skillVersions">;
@@ -161,6 +179,7 @@ function buildStructuredModerationPatch(params: {
> {
const snapshot = buildModerationSnapshot({
staticScan: params.staticScan,
vtAnalysis: params.vtAnalysis,
vtStatus: params.vtStatus,
llmStatus: params.llmStatus,
sourceVersionId: params.sourceVersionId,
@@ -204,6 +223,9 @@ function resolveScannerModerationReason(params: {
const vtStatus = normalizeAnalysisStatus(params.vtStatus);
const llmStatus = normalizeAnalysisStatus(params.llmStatus);
if (params.verdict === "clean" && (vtStatus === "suspicious" || llmStatus === "suspicious")) {
return "scanner.aggregate.clean";
}
if (vtStatus === "malicious") return "scanner.vt.malicious";
if (llmStatus === "malicious") return "scanner.llm.malicious";
if (vtStatus === "suspicious") return "scanner.vt.suspicious";
@@ -226,6 +248,7 @@ function buildScannerModerationPatchFromVersion(params: {
}): SkillModerationPatch {
const structuredPatch = buildStructuredModerationPatch({
staticScan: params.version.staticScan,
vtAnalysis: params.version.vtAnalysis,
vtStatus: params.version.vtAnalysis?.status,
llmStatus: params.version.llmAnalysis?.status,
sourceVersionId: params.version._id,
@@ -4294,6 +4317,7 @@ export const escalateSkillByIdInternal = internalMutation({
const llmStatus = reasonMatch?.[1] === "llm" ? reasonMatch[2] : version?.llmAnalysis?.status;
const snapshot = buildModerationSnapshot({
staticScan: version?.staticScan,
vtAnalysis: version?.vtAnalysis,
vtStatus,
llmStatus,
sourceVersionId: version?._id,
@@ -4781,15 +4805,7 @@ export const updateVersionScanResultsInternal = internalMutation({
args: {
versionId: v.id("skillVersions"),
sha256hash: v.optional(v.string()),
vtAnalysis: v.optional(
v.object({
status: v.string(),
verdict: v.optional(v.string()),
analysis: v.optional(v.string()),
source: v.optional(v.string()),
checkedAt: v.number(),
}),
),
vtAnalysis: v.optional(vtAnalysisValidator),
},
handler: async (ctx, args) => {
const version = await ctx.db.get(args.versionId);
@@ -4913,11 +4929,6 @@ export const approveSkillByHashInternal = internalMutation({
const now = Date.now();
const qualityLocked = skill.moderationReason === "quality.low" && !isMalicious;
const nextModerationReason = qualityLocked
? "quality.low"
: bypassSuspicious
? `scanner.${args.scanner}.clean`
: `scanner.${args.scanner}.${args.status}`;
const nextModerationNotes = qualityLocked
? (skill.moderationNotes ??
"Quality gate quarantine is still active. Manual moderation review required.")
@@ -4925,6 +4936,7 @@ export const approveSkillByHashInternal = internalMutation({
const scanner = args.scanner.trim().toLowerCase();
const snapshot = buildModerationSnapshot({
staticScan: version.staticScan,
vtAnalysis: version.vtAnalysis,
vtStatus: scanner === "vt" ? args.status : version.vtAnalysis?.status,
llmStatus: scanner === "llm" ? args.status : version.llmAnalysis?.status,
sourceVersionId: version._id,
@@ -4935,6 +4947,16 @@ export const approveSkillByHashInternal = internalMutation({
: snapshot.reasonCodes;
const nextVerdict = verdictFromCodes(nextReasonCodes);
const nextLegacyFlags = legacyFlagsFromVerdict(nextVerdict);
if (nextVerdict === "clean" && !alreadyBlocked) {
newFlags = undefined;
}
const nextModerationReason = qualityLocked
? "quality.low"
: bypassSuspicious
? `scanner.${args.scanner}.clean`
: nextVerdict === "clean"
? "scanner.aggregate.clean"
: `scanner.${args.scanner}.${args.status}`;
const nextModerationStatus =
nextVerdict === "malicious" || qualityLocked ? "hidden" : "active";
@@ -5020,9 +5042,9 @@ export const escalateByVtInternal = internalMutation({
newFlags = ["flagged.suspicious"];
}
const nextModerationFlags = newFlags.length ? newFlags : undefined;
const snapshot = buildModerationSnapshot({
staticScan: version.staticScan,
vtAnalysis: version.vtAnalysis,
vtStatus: args.status,
llmStatus: version.llmAnalysis?.status,
sourceVersionId: version._id,
@@ -5032,6 +5054,13 @@ export const escalateByVtInternal = internalMutation({
? snapshot.reasonCodes.filter((code) => !code.startsWith("suspicious."))
: snapshot.reasonCodes;
const nextVerdict = verdictFromCodes(nextReasonCodes);
const nextLegacyFlags = legacyFlagsFromVerdict(nextVerdict);
const nextModerationFlags =
nextVerdict === "clean" && !alreadyBlocked
? undefined
: newFlags.length
? newFlags
: nextLegacyFlags;
const now = Date.now();
const basePatch: SkillModerationPatch = {
moderationFlags: nextModerationFlags,
@@ -5048,11 +5077,21 @@ export const escalateByVtInternal = internalMutation({
basePatch.moderationReason = normalizeScannerSuspiciousReason(
skill.moderationReason as string | undefined,
);
} else if (nextVerdict === "clean" && !alreadyBlocked) {
const existingReason = skill.moderationReason as string | undefined;
if (existingReason?.startsWith("scanner.") && existingReason.endsWith(".suspicious")) {
basePatch.moderationReason = normalizeScannerSuspiciousReason(existingReason);
}
}
// Only hide for malicious — suspicious stays visible with a flag
if (isMalicious) {
basePatch.moderationStatus = "hidden";
} else if (nextVerdict === "clean" && !alreadyBlocked) {
basePatch.moderationStatus = "active";
basePatch.hiddenAt = undefined;
basePatch.hiddenBy = undefined;
basePatch.lastReviewedAt = undefined;
}
basePatch.isSuspicious = computeIsSuspicious({
+20
View File
@@ -92,6 +92,26 @@ describe("vt activation fallback", () => {
});
describe("vt AV engine fallback verdicts", () => {
it("strips unsupported VT stat keys before caching", () => {
expect(
__test.normalizeVtEngineStats({
"confirmed-timeout": 0,
failure: 2,
harmless: 0,
malicious: 0,
suspicious: 0,
timeout: 0,
"type-unsupported": 10,
undetected: 64,
} as never),
).toEqual({
harmless: 0,
malicious: 0,
suspicious: 0,
undetected: 64,
});
});
it("maps engine verdicts in severity order", () => {
expect(
__test.statusFromAvStats({
+28
View File
@@ -166,6 +166,16 @@ type PackageReleaseScanDoc = Pick<
>;
type PackageScanDoc = Pick<Doc<"packages">, "family" | "isOfficial">;
function normalizeVtEngineStats(stats?: VTAnalysisStats | null) {
if (!stats) return undefined;
return {
malicious: stats.malicious,
suspicious: stats.suspicious,
undetected: stats.undetected,
harmless: stats.harmless,
};
}
function buildPackageUndetectedFallbackAnalysis(
release: PackageReleaseScanDoc,
pkg: PackageScanDoc,
@@ -203,11 +213,14 @@ function buildPackageScanAnalysisFromVtResult(
);
if (aiResult) {
const verdict = normalizeVerdict(aiResult.verdict);
const stats = vtResult.data.attributes.last_analysis_stats;
return {
status: verdictToStatus(verdict),
verdict: aiResult.verdict,
analysis: aiResult.analysis,
source: aiResult.source,
scanner: "code_insight",
engineStats: normalizeVtEngineStats(stats),
checkedAt: Date.now(),
};
}
@@ -511,6 +524,7 @@ export const scanWithVirusTotal = internalAction({
);
if (aiResult) {
const stats = existingFile.data.attributes.last_analysis_stats;
// File exists and has AI analysis - use the verdict
const verdict = normalizeVerdict(aiResult.verdict);
const status = verdictToStatus(verdict);
@@ -526,6 +540,8 @@ export const scanWithVirusTotal = internalAction({
verdict: aiResult.verdict,
analysis: aiResult.analysis,
source: aiResult.source,
scanner: "code_insight",
engineStats: normalizeVtEngineStats(stats),
checkedAt: Date.now(),
},
});
@@ -929,6 +945,7 @@ export const pollPendingScans = internalAction({
vtAnalysis: {
status,
source,
engineStats: normalizeVtEngineStats(stats),
checkedAt: Date.now(),
},
});
@@ -967,6 +984,7 @@ export const pollPendingScans = internalAction({
// We have a verdict - update the skill
const verdict = normalizeVerdict(aiResult.verdict);
const status = verdictToStatus(verdict);
const stats = vtResult.data.attributes.last_analysis_stats;
console.log(
`[vt:pollPendingScans] Hash ${sha256hash} verdict: ${verdict} -> status: ${status}`,
@@ -980,6 +998,8 @@ export const pollPendingScans = internalAction({
verdict: aiResult.verdict,
analysis: aiResult.analysis,
source: aiResult.source,
scanner: "code_insight",
engineStats: normalizeVtEngineStats(stats),
checkedAt: Date.now(),
},
});
@@ -1061,6 +1081,7 @@ async function requestRescan(apiKey: string, sha256hash: string): Promise<boolea
}
export const __test = {
normalizeVtEngineStats,
statusFromAvStats,
shouldActivateWhenVtUnavailable,
};
@@ -1264,6 +1285,7 @@ export const rescanActiveSkills = internalAction({
vtAnalysis: {
status,
source,
engineStats: normalizeVtEngineStats(stats),
checkedAt: Date.now(),
},
});
@@ -1293,6 +1315,7 @@ export const rescanActiveSkills = internalAction({
const verdict = normalizeVerdict(aiResult.verdict);
const status = verdictToStatus(verdict);
const stats = vtResult.data.attributes.last_analysis_stats;
await ctx.runMutation(internal.skills.updateVersionScanResultsInternal, {
versionId,
@@ -1301,6 +1324,8 @@ export const rescanActiveSkills = internalAction({
verdict: aiResult.verdict,
analysis: aiResult.analysis,
source: aiResult.source,
scanner: "code_insight",
engineStats: normalizeVtEngineStats(stats),
checkedAt: Date.now(),
},
});
@@ -1591,6 +1616,7 @@ export const backfillActiveSkillsVTCache = internalAction({
// Update the version with VT analysis
const verdict = normalizeVerdict(aiResult.verdict);
const status = verdictToStatus(verdict);
const stats = vtResult.data.attributes.last_analysis_stats;
await ctx.runMutation(internal.skills.updateVersionScanResultsInternal, {
versionId,
@@ -1600,6 +1626,8 @@ export const backfillActiveSkillsVTCache = internalAction({
verdict: aiResult.verdict,
analysis: aiResult.analysis,
source: aiResult.source,
scanner: "code_insight",
engineStats: normalizeVtEngineStats(stats),
checkedAt: Date.now(),
},
});
+2 -7
View File
@@ -1,7 +1,7 @@
import { expect, test } from "@playwright/test";
import { expectHealthyPage, trackRuntimeErrors } from "./helpers/runtimeErrors";
const navLabels = ["Skills", "Plugins", "Search"];
const navLabels = ["Skills", "Plugins"];
test("skills loads without error", async ({ page }) => {
const errors = trackRuntimeErrors(page);
@@ -13,7 +13,7 @@ test("skills loads without error", async ({ page }) => {
test("souls loads without error", async ({ page }) => {
const errors = trackRuntimeErrors(page);
await page.goto("/souls", { waitUntil: "domcontentloaded" });
await expect(page.locator("h1", { hasText: "Souls" })).toBeVisible();
await expect(page.locator("h1", { hasText: "SOUL.md discovery is on deck" })).toBeVisible();
await expectHealthyPage(page, errors);
});
@@ -35,11 +35,6 @@ test("header menu routes render", async ({ page }) => {
await expect(page).toHaveURL(/\/plugins(\?|$)/);
await expect(page.locator("h1", { hasText: "Plugins" })).toBeVisible();
}
if (label === "Search") {
await expect(page).toHaveURL(/\/skills(\?|$)/);
await expect(page.locator("h1", { hasText: "Skills" })).toBeVisible();
}
}
await expectHealthyPage(page, errors);