// G1 假零件偵測(component-gatekeeping SDD,R2) // // 判準(DECISIONS §1):零件若滿足任一,是假零件,該降級成 recipe / 工作流: // (a) contract 或 wasm binary 出現具體外部服務 URL / domain // (b) 宣告能力是 http_request 子集(打某固定 endpoint) // // Q2 決議(richblack 2026-05-29):兩者都「硬擋」(不是只 warn)。 // 理由:零件不該連外,連外即 recipe。這兩個 pattern 都是「該是 recipe 的東西偽裝成零件」。 // // 排除:auth_* primitive(credential 後端,DECISIONS §3b 不適用假零件判準)、http_request 自己。 import type { ComponentContract } from '../types'; // auth primitive 與 http_request 不適用假零件判準 const EXEMPT_IDS = new Set([ 'http_request', 'auth_static_key', 'auth_oauth2', 'auth_service_account', 'auth_mtls', ]); // 外部 URL / domain pattern。 // - 明確的 scheme://host // - 裸 domain(api.foo.com / foo.googleapis.com 之類) const URL_SCHEME_RE = /\bhttps?:\/\/[^\s"'`)]+/i; // 裸 domain:至少 host.tld,排除過於通用的誤判(如 stdin.json)——要求含常見 TLD 或 .xxx.yyy 多段 const BARE_DOMAIN_RE = /\b[a-z0-9][a-z0-9-]*(\.[a-z0-9-]+)*\.(com|org|net|dev|io|me|click|app|co|googleapis\.com|telegram\.org)\b/i; /** * 把 contract 的文字欄位攤平成一個字串,供 URL 掃描。 * 掃 description / display_name / input_schema / output_schema / tags / aliases。 */ function flattenContractText(contract: ComponentContract): string { const parts: string[] = [ contract.display_name ?? '', contract.description ?? '', JSON.stringify(contract.input_schema ?? {}), JSON.stringify(contract.output_schema ?? {}), (contract.tags ?? []).join(' '), (contract.aliases ?? []).join(' '), ]; return parts.join('\n'); } /** * 偵測投稿零件是否為假零件。 * 回 null = 通過;回字串 = 退稿原因(已含指回正路的訊息)。 */ export function detectFakeComponent( contract: ComponentContract, wasmBytes: Uint8Array, ): string | null { if (EXEMPT_IDS.has(contract.canonical_id)) { return null; } const pointToRecipe = '。這該是 API recipe(http_request + 固定設定)或工作流,不是零件。' + '零件 = 封閉邏輯(流程控制 / 資料處理),不連外部服務。見 DECISIONS §1。'; // (a) contract 文字含外部 URL / domain const contractText = flattenContractText(contract); const schemeHit = contractText.match(URL_SCHEME_RE); if (schemeHit) { return `偵測到 contract 含外部 URL:${schemeHit[0].slice(0, 80)}${pointToRecipe}`; } const domainHit = contractText.match(BARE_DOMAIN_RE); if (domainHit) { return `偵測到 contract 含外部 domain:${domainHit[0]}${pointToRecipe}`; } // (a') wasm binary 文字含外部 URL / domain(零件原碼把 endpoint 編進去) // 只掃可印 ASCII 區段以降低誤判;wasm 字串以 UTF-8 存放。 const wasmText = new TextDecoder('utf-8').decode(wasmBytes); const wasmSchemeHit = wasmText.match(URL_SCHEME_RE); if (wasmSchemeHit) { return `偵測到 wasm 內嵌外部 URL:${wasmSchemeHit[0].slice(0, 80)}${pointToRecipe}`; } // (b) http_request 子集:零件宣告自己只是「打某 API/endpoint」 // heuristic:description 描述「打/呼叫 ... API/endpoint」+ input 有 url-like 欄位 const desc = (contract.description ?? '') + ' ' + contract.display_name; const inputKeys = Object.keys(contract.input_schema ?? {}).join(' ').toLowerCase(); const hasUrlField = /\b(url|endpoint|api_url|base_url|host)\b/.test(inputKeys); const describesHttpCall = /(打|呼叫|call|fetch|request|POST|GET|PUT|DELETE)\s*.*(api|endpoint|url|https?)/i.test(desc); if (hasUrlField && describesHttpCall) { return `偵測到疑似 http_request 子集(input 有 url 欄位 + 描述為打 API)${pointToRecipe}`; } return null; }