Files
Arcrun/registry/src/actions/submitComponent.ts
T
Leo 93bb4d3327 feat(registry): Phase 3 G0 人類閘門(submit 端強制人類確認 + 舉證)
- 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>
2026-05-29 18:03:49 +08:00

154 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 零件提交:沙盒驗收 → 派發 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,
};
}