93bb4d3327
- submitComponent 加 SubmitOptions(human_confirmation + gherkin_evidence + skip_acceptance) - G0 閘門:非 skip_acceptance 的新投稿,缺 human_confirmation(confirmed_by_human + 非空 reason_why_not_workflow)→ 退稿指回「先試工作流,需人類確認」 - human_confirmation + gherkin_evidence 寫進 KV metadata(軌跡可審) - components route 從 multipart/JSON 解析這些欄位傳入 - backfill(skip_acceptance)不受閘門影響 design 命名修正:投稿走既有 acr parts publish(非另建指令,符合「修改現有不重建」)。 待續: G0-CLI(acr parts publish 互動式問人類)、G4-CLI(本地跑 Gherkin + evidence)、 R5(MVP_COMPONENTS.txt 白名單 + 本機 hook)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
154 lines
6.1 KiB
TypeScript
154 lines
6.1 KiB
TypeScript
// 零件提交:沙盒驗收 → 派發 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<string> {
|
||
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<SandboxResult> {
|
||
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,
|
||
};
|
||
}
|