mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-15 01:12:11 +00:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e5ffdc565 | ||
|
|
4e13e729fb | ||
|
|
d17e100cca | ||
|
|
c732b38569 | ||
|
|
3701733797 | ||
|
|
b8ba595d06 | ||
|
|
232e429dee | ||
|
|
932155cb8f | ||
|
|
d855d09ab0 | ||
|
|
16e87c147d | ||
|
|
cbe22e70b9 | ||
|
|
837331c967 | ||
|
|
6ce443496d | ||
|
|
8fd4f3b051 | ||
|
|
a2153909da | ||
|
|
75e1b4633e |
@@ -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"
|
||||
@@ -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 != ''
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user