// 零件提交:沙盒驗收 → 派發 hash id → 寫入 SUBMISSIONS_KV // Requirements: 2.1, 2.2, 2.3 // SDD: matrix/arcrun/.agents/specs/component-registry-canon/design.md Phase 1.5 // // 2026-05-07:移除 R2 寫入。cypher-executor 已不從 R2 動態載 wasm(每個零件 = 獨立 Worker)。 // R2 是 dead storage,移除避免誤導 AI 以為零件部署需要 wasm bytes。 // // KV key 設計: // comp:{hash_id}:{version} → 零件元數據 JSON // idx:{canonical_id} → hash_id 反查索引(canonical_id → hash_id) // // hash_id 派發規則: // hash_id = 'cmp_' + sha256(canonical_id).slice(0, 8) // 相同 canonical_id 永遠得到相同 hash_id(冪等) import { runSandboxAcceptance } from './sandboxAcceptance'; import type { GherkinEvidence } from './sandboxAcceptance'; import type { ComponentContract, SandboxResult, Bindings } from '../types'; // ── G0 人類閘門(component-gatekeeping SDD R4)───────────────────────────────── // // 建零件不是 AI 能自己決定的事。submit 預設拒絕,除非帶人類確認 + 舉證。 // 把關點在「建立零件的 API」本身(此處)→ CLI/MCP/Python/JS 四路全收斂到這關。 // 誠實限制:AI 技術上能偽造 confirmed_by_human:true。靠 reason 留記錄(軌跡可審)+ // mindset 明示「絕不代替人類確認建零件」+ 純 WASI 沙箱框死能力,讓偽造成明確越界。 export interface HumanConfirmation { confirmed_by_human: true; // 必須為 literal true reason_why_not_workflow: string; // 非空:AI 舉證「為何工作流做不到」 confirmed_at: string; // ISO timestamp } export interface SubmitOptions { /** backfill 既有零件用:跳過人類閘門 + 沙盒(這些是已驗、已部署的存量) */ skip_acceptance?: boolean; /** 人類確認 + 舉證(新投稿必填,除非 skip_acceptance) */ human_confirmation?: HumanConfirmation; /** 投稿者本地跑 Gherkin 的結果(CLI 上傳,registry 存證可審) */ gherkin_evidence?: GherkinEvidence[]; } function gateError(contract: ComponentContract, reason: string): SandboxResult { return { success: false, failed_step: 'fake_component_scan', // 復用最前步驟欄位語意:未過閘門 reason, guide_anchor: '#human-gate', component_hash_id: '', canonical_id: contract.canonical_id, version: contract.version, }; } // ── hash id 生成 ───────────────────────────────────────────────────────────── async function deriveHashId(canonicalId: string): Promise { const encoder = new TextEncoder(); const data = encoder.encode(canonicalId); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); return 'cmp_' + hex.slice(0, 8); } // ── 主流程 ──────────────────────────────────────────────────────────────────── export async function submitComponent( wasmBytes: Uint8Array, contract: ComponentContract, env: Bindings, options: SubmitOptions = {}, ): Promise { const { skip_acceptance, human_confirmation, gherkin_evidence } = options; // 0. G0 人類閘門(新投稿必經;backfill 既有存量 skip) if (!skip_acceptance) { if ( !human_confirmation || human_confirmation.confirmed_by_human !== true || !human_confirmation.reason_why_not_workflow || human_confirmation.reason_why_not_workflow.trim() === '' ) { return gateError( contract, '建零件需人類確認。請用 `acr component create`(會互動式問你),' + '並說明「為何這件事無法用工作流達成」。預設假設工作流 / recipe 能做——先試工作流。', ); } } // 1. 沙盒驗收(靜態把關 + gherkin_evidence 一致性;backfill 仍可選擇性跳過 wasm 驗收) if (!skip_acceptance) { const sandboxResult = runSandboxAcceptance(wasmBytes, contract, gherkin_evidence); if (!sandboxResult.success) { return sandboxResult; } } // 2. 派發 hash id const hashId = await deriveHashId(contract.canonical_id); const kvKey = `comp:${hashId}:${contract.version}`; // 3. 冪等 const existing = await env.SUBMISSIONS_KV.get(kvKey); if (existing) { return { success: true, component_hash_id: hashId, canonical_id: contract.canonical_id, version: contract.version, }; } // 4. 寫入 metadata const record = { component_hash_id: hashId, canonical_id: contract.canonical_id, display_name: contract.display_name, category: contract.category, version: contract.version, wasi_target: contract.wasi_target, stability: contract.stability, runtime_compat: contract.runtime_compat, component_type: contract.component_type ?? 'wasm', constraints: contract.constraints, input_schema: contract.input_schema, output_schema: contract.output_schema, gherkin_tests: contract.gherkin_tests, description: contract.description ?? '', aliases: contract.aliases ?? [], tags: contract.tags ?? [], success_rate: 1, avg_duration_ms: 0, call_count: 0, visibility: 'public' as const, status: 'active' as const, submitted_at: new Date().toISOString(), deprecated_at: null, // G0 軌跡可審:人類為何建此零件(舉證)+ 投稿者本地 Gherkin 結果證據 human_confirmation: human_confirmation ?? null, gherkin_evidence: gherkin_evidence ?? null, }; await env.SUBMISSIONS_KV.put(kvKey, JSON.stringify(record)); await env.SUBMISSIONS_KV.put(`idx:${contract.canonical_id}`, hashId); return { success: true, component_hash_id: hashId, canonical_id: contract.canonical_id, version: contract.version, }; }