diff --git a/convex/httpApiV1/skillsV1.ts b/convex/httpApiV1/skillsV1.ts index 358233d61..098eb0590 100644 --- a/convex/httpApiV1/skillsV1.ts +++ b/convex/httpApiV1/skillsV1.ts @@ -348,6 +348,37 @@ export async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request) if (!version) return text('Version not found', 404, rate.headers) if (version.softDeletedAt) return text('Version not available', 410, rate.headers) + // Map llmAnalysis to security status + let security = undefined + if (version.llmAnalysis) { + const analysis = version.llmAnalysis + let status: "clean" | "suspicious" | "malicious" | "pending" | "error" + switch (analysis.verdict) { + case 'benign': + status = 'clean' + break + case 'suspicious': + status = 'suspicious' + break + case 'malicious': + status = 'malicious' + break + default: + status = analysis.status === 'error' ? 'error' : 'pending' + } + + const hasWarnings = analysis.verdict === 'suspicious' || + analysis.verdict === 'malicious' || + (analysis.dimensions?.some((d: any) => d.rating !== 'ok') ?? false) + + security = { + status, + hasWarnings, + checkedAt: analysis.checkedAt ?? null, + model: analysis.model || null, + } + } + return json( { skill: { slug: skill.slug, displayName: skill.displayName }, @@ -362,6 +393,7 @@ export async function skillsGetRouterV1Handler(ctx: ActionCtx, request: Request) sha256: file.sha256, contentType: file.contentType ?? null, })), + security, }, }, 200, diff --git a/packages/clawdhub/src/cli/commands/inspect.ts b/packages/clawdhub/src/cli/commands/inspect.ts index bbbe58e62..4f4f5abc8 100644 --- a/packages/clawdhub/src/cli/commands/inspect.ts +++ b/packages/clawdhub/src/cli/commands/inspect.ts @@ -130,6 +130,22 @@ export async function cmdInspect(opts: GlobalOpts, slug: string, options: Inspec if (shouldPrintMeta && versionResult?.version) { printVersionSummary(versionResult.version) + + // Display security info if available + const version = versionResult.version as { security?: any } + if (version.security) { + const sec = version.security + console.log(`\nSecurity: ${sec.status.toUpperCase()}`) + if (sec.hasWarnings) { + console.log(`⚠️ This skill has security warnings`) + } + if (sec.checkedAt) { + console.log(`Checked: ${new Date(sec.checkedAt).toLocaleDateString()}`) + } + if (sec.model) { + console.log(`Model: ${sec.model}`) + } + } } if (versionsList?.items && Array.isArray(versionsList.items)) { diff --git a/packages/schema/src/schemas.ts b/packages/schema/src/schemas.ts index a1e7469a4..3cf6b17ed 100644 --- a/packages/schema/src/schemas.ts +++ b/packages/schema/src/schemas.ts @@ -208,6 +208,13 @@ export const ApiV1SkillVersionListResponseSchema = type({ nextCursor: 'string|null', }) +export const SecurityStatusSchema = type({ + status: '"clean" | "suspicious" | "malicious" | "pending" | "error"', + hasWarnings: 'boolean', + checkedAt: 'number|null', + model: 'string|null', +}) + export const ApiV1SkillVersionResponseSchema = type({ version: type({ version: 'string', @@ -215,6 +222,7 @@ export const ApiV1SkillVersionResponseSchema = type({ changelog: 'string', changelogSource: '"auto"|"user"|null?', files: 'unknown?', + security: SecurityStatusSchema.optional(), }).or('null'), skill: type({ slug: 'string',