Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions convex/githubSkillSync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1636,6 +1636,110 @@ describe("applyGitHubSkillVerificationResultHandler", () => {
updatedAt: 123,
});
});

it("retains padding-only static evidence while keeping GitHub installability clean", async () => {
const { db, tables } = createDb({
skills: [
{
_id: "skills:padded",
slug: "padded",
displayName: "Padded",
ownerUserId: "users:nvidia",
ownerPublisherId: "publishers:nvidia",
installKind: "github",
githubSourceId: "githubSkillSources:nvidia",
githubPath: "skills/padded",
githubCurrentCommit: "4".repeat(40),
githubCurrentContentHash: "padded-hash",
githubCurrentStatus: "present",
githubScanStatus: "pending",
tags: {},
stats: { downloads: 0, stars: 0, installsCurrent: 0, installsAllTime: 0, versions: 0 },
moderationStatus: "active",
moderationReason: "pending.scan",
createdAt: 1,
updatedAt: 1,
},
],
githubSkillSources: [
{
_id: "githubSkillSources:nvidia",
repo: "NVIDIA/skills",
ownerUserId: "users:nvidia",
ownerPublisherId: "publishers:nvidia",
defaultBranch: "main",
createdAt: 1,
updatedAt: 1,
},
],
globalStats: [
{
_id: "globalStats:default",
key: "default",
activeSkillsCount: 10,
updatedAt: 1,
},
],
});

const staticScan = {
status: "suspicious" as const,
reasonCodes: ["suspicious.context_padding_truncation"],
findings: [
{
code: "suspicious.context_padding_truncation",
severity: "warn" as const,
file: "SKILL.md",
line: 2,
message:
"Extreme context padding hides later artifact content from context-limited review.",
evidence:
"2100 consecutive blank/whitespace-only lines before later artifact content; content resumes at line 2102.",
},
],
summary: "Detected: suspicious.context_padding_truncation",
engineVersion: "test-engine",
checkedAt: 456,
};

const promoted = await applyGitHubSkillVerificationResultHandler({ db } as never, {
skillId: "skills:padded" as never,
contentHash: "padded-hash",
scanStatus: "clean",
staticScan,
now: 789,
});

expect(promoted).toEqual({ ok: true, promoted: true });
expect(tables.skills[0]).toMatchObject({
githubScanStatus: "clean",
moderationStatus: "active",
moderationReason: "scanner.aggregate.suspicious",
moderationVerdict: "suspicious",
moderationFlags: ["flagged.suspicious"],
moderationReasonCodes: ["suspicious.context_padding_truncation"],
moderationEvidence: [
expect.objectContaining({
code: "suspicious.context_padding_truncation",
file: "SKILL.md",
}),
],
moderationSummary: "Detected: suspicious.context_padding_truncation",
moderationEngineVersion: "test-engine",
moderationEvaluatedAt: 456,
isSuspicious: true,
});
expect(resolveInstallFromTables(tables, "padded")).toMatchObject({
ok: true,
installKind: "github",
github: {
repo: "NVIDIA/skills",
path: "skills/padded",
commit: "4".repeat(40),
contentHash: "padded-hash",
},
});
});
});

describe("verifyGitHubSkillHandler", () => {
Expand Down Expand Up @@ -1704,6 +1808,84 @@ describe("verifyGitHubSkillHandler", () => {
}),
);
});

it("keeps context-padding-only findings from blocking GitHub-backed installs", async () => {
const commit = "4".repeat(40);
const zip = zipSync({
"skills-main/skills/padded/SKILL.md": new TextEncoder().encode(
`# Padded\n${"\n".repeat(2_100)}Use the configured API.\n`,
),
});
const snapshot = await buildGitHubSkillSourceSnapshot({
repo: "NVIDIA/skills",
defaultBranch: "main",
commit,
entries: stripGitHubZipRoot(__test.unzipToEntries(zip)),
});
const contentHash = snapshot.skills[0]?.contentHash;
if (!contentHash) throw new Error("missing fixture hash");

const runMutation = vi.fn(async () => ({ ok: true, promoted: false }));
const ctx = {
runQuery: vi.fn(async () => ({
skill: {
_id: "skills:padded",
slug: "padded",
displayName: "Padded",
summary: "Padded skill",
githubPath: "skills/padded",
githubCurrentCommit: commit,
githubCurrentContentHash: contentHash,
githubCurrentStatus: "present",
},
source: {
_id: "githubSkillSources:nvidia",
repo: "NVIDIA/skills",
defaultBranch: "main",
},
})),
runMutation,
};
const fetcher = vi.fn(async (input: RequestInfo | URL) => {
const url =
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
if (url.startsWith("https://api.github.com/")) {
return new Response(JSON.stringify({ sha: commit }), {
headers: { "content-type": "application/json" },
});
}
if (url.startsWith("https://codeload.github.com/")) {
return new Response(zip, { headers: { "content-length": String(zip.byteLength) } });
}
return new Response("not found", { status: 404 });
});

const result = await verifyGitHubSkillHandler(
ctx as never,
{ skillId: "skills:padded" as never, contentHash },
fetcher as unknown as typeof fetch,
);

expect(result).toMatchObject({ ok: true, scanStatus: "clean" });
expect(runMutation).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
skillId: "skills:padded",
contentHash,
scanStatus: "clean",
staticScan: expect.objectContaining({
status: "suspicious",
reasonCodes: ["suspicious.context_padding_truncation"],
findings: [
expect.objectContaining({
code: "suspicious.context_padding_truncation",
file: "SKILL.md",
}),
],
}),
}),
);
});
});

describe("recordGitHubSkillSourceSyncAttemptHandler", () => {
Expand Down
69 changes: 67 additions & 2 deletions convex/githubSkillSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from "./lib/githubSkillSync";
import { adjustGlobalPublicSkillsCount, getPublicSkillVisibilityDelta } from "./lib/globalStats";
import { runStaticModerationScan } from "./lib/moderationEngine";
import { REASON_CODES } from "./lib/moderationReasonCodes";
import { Events, logErrorEvent, logEvent } from "./lib/observabilityEvents";
import { isOfficialPublisher } from "./lib/officialPublishers";
import { requirePublisherRole } from "./lib/publishers";
Expand All @@ -41,6 +42,60 @@ const MAX_STATIC_SCAN_TEXT_FILE_BYTES = 256 * 1024;
const DEFAULT_SOURCE_SYNC_BATCH_SIZE = 20;
const MAX_SOURCE_SYNC_BATCH_SIZE = 50;

const githubStaticScanFindingValidator = v.object({
code: v.string(),
severity: v.union(v.literal("info"), v.literal("warn"), v.literal("critical")),
file: v.string(),
line: v.number(),
message: v.string(),
evidence: v.string(),
});

const githubStaticScanResultValidator = v.object({
status: v.union(v.literal("clean"), v.literal("suspicious"), v.literal("malicious")),
reasonCodes: v.array(v.string()),
findings: v.array(githubStaticScanFindingValidator),
summary: v.string(),
engineVersion: v.string(),
checkedAt: v.number(),
});

type GitHubStaticScanResult = ReturnType<typeof runStaticModerationScan>;

function githubScanStatusFromStaticScan(staticScan: ReturnType<typeof runStaticModerationScan>) {
if (
staticScan.status === "suspicious" &&
staticScan.reasonCodes.length > 0 &&
staticScan.reasonCodes.every((code) => code === REASON_CODES.CONTEXT_PADDING_TRUNCATION)
) {
return "clean" as const;
}
return staticScan.status;
}

function retainedStaticScanModerationPatch(
staticScan: GitHubStaticScanResult | undefined,
scanStatus: GitHubSkillScanStatus,
) {
if (!staticScan || staticScan.status === scanStatus || staticScan.reasonCodes.length === 0) {
return {};
}

return {
moderationReason: `scanner.aggregate.${staticScan.status}`,
moderationVerdict: staticScan.status,
moderationFlags:
staticScan.status === "malicious" ? ["blocked.malware"] : ["flagged.suspicious"],
moderationReasonCodes: staticScan.reasonCodes,
moderationEvidence: staticScan.findings.length ? staticScan.findings : undefined,
moderationSummary: staticScan.summary,
moderationEngineVersion: staticScan.engineVersion,
moderationEvaluatedAt: staticScan.checkedAt,
moderationSourceVersionId: undefined,
isSuspicious: staticScan.status !== "clean",
};
}

type SourceForSync = Pick<
Doc<"githubSkillSources">,
"_id" | "repo" | "ownerPublisherId" | "defaultBranch"
Expand Down Expand Up @@ -808,6 +863,7 @@ export type ApplyGitHubSkillVerificationResultArgs = {
skillId: Id<"skills">;
contentHash: string;
scanStatus: GitHubSkillScanStatus;
staticScan?: GitHubStaticScanResult;
now?: number;
};

Expand All @@ -834,11 +890,16 @@ export async function applyGitHubSkillVerificationResultHandler(
const now = args.now ?? Date.now();
const promote = args.scanStatus === "clean";
const moderation = githubBackedSkillModeration(args.scanStatus);
const retainedStaticScanModeration = retainedStaticScanModerationPatch(
args.staticScan,
args.scanStatus,
);
const previousSkill = { ...skill };
const patch = {
githubScanStatus: args.scanStatus,
updatedAt: now,
...moderation,
...retainedStaticScanModeration,
};
await ctx.db.patch(args.skillId, patch);
const nextSkill = { ...previousSkill, ...patch };
Expand All @@ -853,6 +914,7 @@ export const applyGitHubSkillVerificationResultInternal = internalMutation({
skillId: v.id("skills"),
contentHash: v.string(),
scanStatus: githubSkillScanStatusValidator,
staticScan: v.optional(githubStaticScanResultValidator),
now: v.optional(v.number()),
},
handler: applyGitHubSkillVerificationResultHandler,
Expand Down Expand Up @@ -895,15 +957,18 @@ export async function verifyGitHubSkillHandler(
fileContents: listGitHubSkillTextContents(entries, discovered.path),
});

const scanStatus = githubScanStatusFromStaticScan(staticScan);

await ctx.runMutation(internal.githubSkillSync.applyGitHubSkillVerificationResultInternal, {
skillId: target.skill._id,
contentHash: args.contentHash,
scanStatus: staticScan.status,
scanStatus,
staticScan,
});

return {
ok: true as const,
scanStatus: staticScan.status,
scanStatus,
};
}

Expand Down
69 changes: 69 additions & 0 deletions convex/lib/moderationEngine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,75 @@ describe("moderationEngine", () => {
expect(result.status).toBe("clean");
});

it("flags extreme blank-line context padding before later skill content", () => {
const content =
"# CSV Summarizer\n" +
"\n".repeat(2_500) +
"FAKE_PAYLOAD_MARKER: late instructions hidden after padding.\n";
const result = runStaticModerationScan({
slug: "csv-summarizer",
displayName: "CSV Summarizer",
summary: "Summarizes CSV files",
frontmatter: {},
metadata: {},
files: [{ path: "SKILL.md", size: content.length }],
fileContents: [{ path: "SKILL.md", content }],
});

expect(result.reasonCodes).toContain("suspicious.context_padding_truncation");
expect(result.status).toBe("suspicious");
expect(result.findings).toContainEqual(
expect.objectContaining({
code: "suspicious.context_padding_truncation",
file: "SKILL.md",
line: 2,
evidence: expect.stringContaining("2500 consecutive blank/whitespace-only lines"),
}),
);
});

it("flags long whitespace runs before later text content", () => {
const content =
"Visible setup notes" +
" ".repeat(40_000) +
"FAKE_PAYLOAD_MARKER: late content hidden on the same line.";
const result = runStaticModerationScan({
slug: "formatter",
displayName: "Formatter",
summary: "Formats text",
frontmatter: {},
metadata: {},
files: [{ path: "notes.txt", size: content.length }],
fileContents: [{ path: "notes.txt", content }],
});

expect(result.reasonCodes).toContain("suspicious.context_padding_truncation");
expect(result.findings).toContainEqual(
expect.objectContaining({
code: "suspicious.context_padding_truncation",
file: "notes.txt",
line: 1,
evidence: expect.stringContaining("40000 consecutive whitespace characters"),
}),
);
});

it("does not flag ordinary sparse markdown spacing", () => {
const content = ["# Demo", ...Array.from({ length: 200 }, () => ""), "## Usage"].join("\n");
const result = runStaticModerationScan({
slug: "demo",
displayName: "Demo",
summary: "A normal integration skill",
frontmatter: {},
metadata: {},
files: [{ path: "SKILL.md", size: content.length }],
fileContents: [{ path: "SKILL.md", content }],
});

expect(result.reasonCodes).not.toContain("suspicious.context_padding_truncation");
expect(result.status).toBe("clean");
});

it("flags hardcoded API secrets in skill documentation and redacts every evidence copy", () => {
const exposedSecret = "ak_live_1234567890abcdefSECRET";
const result = runStaticModerationScan({
Expand Down
Loading
Loading