mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
Closes CLAW-526.\n\nSummary:\n- create pending skill versions and plugin releases that remain hidden until TruffleHog and ClawScan pass\n- preserve older CLI response compatibility while newer CLI output explains pending security checks\n- run prepublication worker promotion/blocking for skills and plugins\n- add local-auth coverage for clean skill/plugin publish and secret-positive skill rejection\n\nValidation on PR head d2482434:\n- local: bunx tsc -p packages/schema/tsconfig.json --noEmit\n- local: bunx tsc -p packages/clawhub/tsconfig.json --noEmit\n- local: bunx vitest run convex/lib/skillPublish.test.ts convex/publishAttempts.test.ts convex/skills.versions.public.test.ts convex/packages.public.test.ts packages/schema/src/schemas.test.ts scripts/security/run-prepublication-worker.test.ts scripts/security/prepublication-worker-workflow.test.ts\n- local: bun run ci:static\n- local: bun run ci:types-build && bun run ci:packages\n- GitHub: pr-gates, static, unit, packages, types-build, e2e-http, old-cli-publish, playwright-smoke, secret scanning, CodeQL, and Vercel preview passed\n\nKnown CI note:\n- unrelated local-auth shards continued to rotate failures under the already-diagnosed local Convex starvation issue; ignored per maintainer instruction.
168 lines
4.9 KiB
TypeScript
168 lines
4.9 KiB
TypeScript
import type { Id } from "../_generated/dataModel";
|
|
import {
|
|
isSecurityScanStatusBlockedFromPublic,
|
|
normalizeSecurityScanStatus,
|
|
} from "./securityScanPolicy";
|
|
|
|
export type SkillFileModerationInfo = {
|
|
isPendingScan?: boolean | null;
|
|
isMalwareBlocked?: boolean | null;
|
|
isHiddenByMod?: boolean | null;
|
|
isRemoved?: boolean | null;
|
|
sourceVersionId?: Id<"skillVersions"> | string | null;
|
|
};
|
|
|
|
type SkillVersionSecuritySource = {
|
|
_id: Id<"skillVersions"> | string;
|
|
publicationStatus?: "pending" | "published" | "blocked" | null;
|
|
llmAnalysis?: {
|
|
status?: string | null;
|
|
verdict?: string | null;
|
|
} | null;
|
|
};
|
|
|
|
type SkillModerationSource = {
|
|
moderationStatus?: string | null;
|
|
moderationReason?: string | null;
|
|
moderationFlags?: string[] | null;
|
|
moderationVerdict?: string | null;
|
|
moderationSourceVersionId?: Id<"skillVersions"> | string | null;
|
|
};
|
|
|
|
type SkillFileAccessBlock = {
|
|
status: number;
|
|
message: string;
|
|
};
|
|
|
|
function isPendingSkillModerationReason(reason: string | null | undefined) {
|
|
const normalized = reason?.trim().toLowerCase();
|
|
return (
|
|
normalized === "pending.scan" ||
|
|
normalized === "pending.scan.stale" ||
|
|
normalized === "scanner.vt.pending" ||
|
|
normalized === "scanner.llm.pending"
|
|
);
|
|
}
|
|
|
|
export function getSkillFileModerationInfoFromSkill(
|
|
skill: SkillModerationSource,
|
|
): SkillFileModerationInfo {
|
|
const isPendingScan =
|
|
skill.moderationStatus === "hidden" && isPendingSkillModerationReason(skill.moderationReason);
|
|
const isMalwareBlocked =
|
|
skill.moderationVerdict === "malicious" ||
|
|
(skill.moderationFlags?.includes("blocked.malware") ?? false);
|
|
return {
|
|
isPendingScan,
|
|
isMalwareBlocked,
|
|
isHiddenByMod: skill.moderationStatus === "hidden" && !isPendingScan && !isMalwareBlocked,
|
|
isRemoved: skill.moderationStatus === "removed",
|
|
sourceVersionId: skill.moderationSourceVersionId ?? null,
|
|
};
|
|
}
|
|
|
|
export function getPublicSkillFileAccessBlock(
|
|
moderationInfo: SkillFileModerationInfo | null | undefined,
|
|
): SkillFileAccessBlock | null {
|
|
if (moderationInfo?.isMalwareBlocked) {
|
|
return {
|
|
status: 403,
|
|
message:
|
|
"Blocked: this skill has been flagged as malicious by ClawScan and cannot be downloaded.",
|
|
};
|
|
}
|
|
if (moderationInfo?.isPendingScan) {
|
|
return {
|
|
status: 423,
|
|
message:
|
|
"This skill is pending a ClawScan security review. Please try again in a few minutes.",
|
|
};
|
|
}
|
|
if (moderationInfo?.isRemoved) {
|
|
return { status: 410, message: "This skill has been removed by a moderator." };
|
|
}
|
|
if (moderationInfo?.isHiddenByMod) {
|
|
return { status: 403, message: "This skill is currently unavailable." };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function getPublicSkillVersionAccessBlock(
|
|
moderationInfo: SkillFileModerationInfo | null | undefined,
|
|
versionId: Id<"skillVersions"> | string,
|
|
fallbackModeratedVersionId?: Id<"skillVersions"> | string | null,
|
|
): SkillFileAccessBlock | null {
|
|
const block = getPublicSkillFileAccessBlock(moderationInfo);
|
|
if (!block) return null;
|
|
if (moderationInfo?.isRemoved || moderationInfo?.isHiddenByMod) return block;
|
|
|
|
const moderatedVersionId = moderationInfo?.sourceVersionId ?? fallbackModeratedVersionId;
|
|
return moderatedVersionId === versionId ? block : null;
|
|
}
|
|
|
|
export function getPublicSkillVersionDownloadBlock(
|
|
moderationInfo: SkillFileModerationInfo | null | undefined,
|
|
version: SkillVersionSecuritySource,
|
|
fallbackModeratedVersionId?: Id<"skillVersions"> | string | null,
|
|
): SkillFileAccessBlock | null {
|
|
const moderationBlock = getPublicSkillVersionAccessBlock(
|
|
moderationInfo,
|
|
version._id,
|
|
fallbackModeratedVersionId,
|
|
);
|
|
if (moderationBlock) return moderationBlock;
|
|
|
|
const scanStatus = normalizeSecurityScanStatus(
|
|
version.llmAnalysis?.verdict ?? version.llmAnalysis?.status,
|
|
);
|
|
if (isSecurityScanStatusBlockedFromPublic(scanStatus)) {
|
|
return {
|
|
status: 403,
|
|
message:
|
|
"Blocked: this skill version has been flagged as malicious by ClawScan and cannot be downloaded.",
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function isSkillVersionForSkill(
|
|
version: { skillId?: Id<"skills"> | string | null } | null | undefined,
|
|
skillId: Id<"skills"> | string,
|
|
) {
|
|
return version?.skillId === skillId;
|
|
}
|
|
|
|
export function isPublishedSkillVersion(
|
|
version:
|
|
| {
|
|
publicationStatus?: string | null;
|
|
}
|
|
| null
|
|
| undefined,
|
|
) {
|
|
return Boolean(
|
|
version &&
|
|
(version.publicationStatus === undefined || version.publicationStatus === "published"),
|
|
);
|
|
}
|
|
|
|
export function isPublicSkillVersionAvailableForSkill(
|
|
version:
|
|
| {
|
|
skillId?: Id<"skills"> | string | null;
|
|
softDeletedAt?: number | null;
|
|
publicationStatus?: string | null;
|
|
}
|
|
| null
|
|
| undefined,
|
|
skillId: Id<"skills"> | string,
|
|
) {
|
|
return Boolean(
|
|
version &&
|
|
!version.softDeletedAt &&
|
|
isPublishedSkillVersion(version) &&
|
|
isSkillVersionForSkill(version, skillId),
|
|
);
|
|
}
|