From 3b1f190938aa8b591fa88f7d1d40498b7411ef3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Fri, 3 Jul 2026 17:16:24 +0800 Subject: [PATCH] fix: block inconclusive skillhub installs --- src/server/__tests__/skill-market.test.ts | 31 +++++++++++++++++++ .../services/skillMarket/skillhubAdapter.ts | 24 ++++++++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/server/__tests__/skill-market.test.ts b/src/server/__tests__/skill-market.test.ts index 4e57f9ea..84223ab7 100644 --- a/src/server/__tests__/skill-market.test.ts +++ b/src/server/__tests__/skill-market.test.ts @@ -184,9 +184,40 @@ describe('skill market source normalization', () => { slug: 'skill-vetter', trustState: 'benign', trustSummary: '安全,无风险', + installEligibility: { status: 'installable' }, }) }) + it('blocks SkillHub details when security reports are missing', () => { + const detail = normalizeSkillHubDetail({ + skill: { + slug: 'unreviewed-skill', + displayName: 'Unreviewed Skill', + }, + }) + + expect(detail.trustState).toBe('unknown') + expect(detail.installEligibility.status).toBe('blocked') + expect(detail.installEligibility.reason).toMatch(/security report is missing or inconclusive/i) + }) + + it('blocks SkillHub details when security reports are mixed or inconclusive', () => { + const detail = normalizeSkillHubDetail({ + securityReports: { + community: { status: 'benign', statusText: 'safe' }, + staticAnalysis: { status: 'pending-review', statusText: 'Scanner still reviewing.' }, + }, + skill: { + slug: 'mixed-report-skill', + displayName: 'Mixed Report Skill', + }, + }) + + expect(detail.trustState).toBe('unknown') + expect(detail.installEligibility.status).toBe('blocked') + expect(detail.installEligibility.reason).toMatch(/security report is missing or inconclusive/i) + }) + it('uses malicious SkillHub report summaries for blocked details', () => { const detail = normalizeSkillHubDetail({ securityReports: { diff --git a/src/server/services/skillMarket/skillhubAdapter.ts b/src/server/services/skillMarket/skillhubAdapter.ts index 520d5c9d..8b817c79 100644 --- a/src/server/services/skillMarket/skillhubAdapter.ts +++ b/src/server/services/skillMarket/skillhubAdapter.ts @@ -45,6 +45,13 @@ const ALLOWED_EXTERNAL_URL_HOSTS = new Set([ 'raw.githubusercontent.com', ]) +const SKILLHUB_DETAIL_INSTALLABLE_TRUST_STATES = new Set([ + 'clean', + 'benign', + 'signed', + 'official', +]) + function requiresApiKey(labels?: { requires_api_key?: string }) { return labels?.requires_api_key === 'true' } @@ -93,6 +100,19 @@ function trustFromReports(reports?: Record report.statusText)?.statusText } } +function installEligibilityFromTrustState(trustState: SkillMarketTrustState): SkillMarketDetail['installEligibility'] { + if (SKILLHUB_DETAIL_INSTALLABLE_TRUST_STATES.has(trustState)) { + return { status: 'installable' } + } + if (trustState === 'blocked') { + return { status: 'blocked', reason: 'SkillHub security report blocked this skill.' } + } + if (trustState === 'warning') { + return { status: 'blocked', reason: 'SkillHub security report returned warnings.' } + } + return { status: 'blocked', reason: 'SkillHub security report is missing or inconclusive.' } +} + export function normalizeSkillHubList(payload: SkillHubListResponse): SkillMarketListResult { const items = (payload.data?.skills ?? []) .filter((item) => item.slug && item.name) @@ -155,8 +175,6 @@ export function normalizeSkillHubDetail(payload: SkillHubDetailResponse): SkillM installed: false, files: [], riskLabels: requiresApiKey(skill.labels) ? ['requires-api-key'] : [], - installEligibility: trust.trustState === 'blocked' - ? { status: 'blocked', reason: 'SkillHub security report blocked this skill.' } - : { status: 'installable' }, + installEligibility: installEligibilityFromTrustState(trust.trustState), } }