diff --git a/.agents/specs/component-gatekeeping/design.md b/.agents/specs/component-gatekeeping/design.md index 0d13193..cceb3e6 100644 --- a/.agents/specs/component-gatekeeping/design.md +++ b/.agents/specs/component-gatekeeping/design.md @@ -57,14 +57,14 @@ interface SubmitRequest { 若 skip_acceptance(backfill 既有零件)→ 跳過 G0(這些是已驗、已部署的存量,不是新投稿) 否則(新投稿): 若無 human_confirmation 或 reason_why_not_workflow 空 → 403: - "建零件需人類確認。請用 `acr component create`(會互動式問你), + "建零件需人類確認。請用 `acr parts publish`(會互動式問你), 並說明為何工作流做不到。預設假設工作流能做——先試工作流 / recipe。" 記錄 reason_why_not_workflow 進 KV metadata(軌跡可審) ``` ### 1.3 四路收斂(CLI / MCP / Python / JS) - 它們建零件都呼叫 registry submit endpoint → G0 在 endpoint,自動四路通管。 -- **CLI `acr component create`**:強制互動式 prompt 問人類「(1) 工作流為何做不到?(2) 確認要建零件?」,把答案組成 `human_confirmation` 送出。非互動環境(AI 直跑)`acr` 偵測 stdin 非 TTY → 拒絕並提示「需人類互動」。 +- **CLI `acr parts publish`**:強制互動式 prompt 問人類「(1) 工作流為何做不到?(2) 確認要建零件?」,把答案組成 `human_confirmation` 送出。非互動環境(AI 直跑)`acr` 偵測 stdin 非 TTY → 拒絕並提示「需人類互動」。 - MCP / Python / JS lib:傳 `human_confirmation` 才能成功;它們的 SDK 文件註明此欄位需人類提供。 - **誠實限制**(寫進 mindset + 文件):AI 技術上能偽造 `confirmed_by_human:true`。靠 reason 留記錄 + mindset 明示「絕不代替人類確認建零件」+ 軌跡可審,讓偽造成明確越界,不聲稱不可能繞過。 @@ -92,7 +92,7 @@ interface SubmitRequest { - 剩下唯一一致 venue = **投稿者本地機器**(有 tinygo + 能跑 wasm,與現有 build 流程同環境)。 ### 4.1 正確設計:Gherkin 在投稿指令本地跑 -零件投稿走一個**獨立 CLI 指令**(暫名 `acr component submit`;「本地或公共都是投稿」): +零件投稿走一個**獨立 CLI 指令**(既有指令;「本地或公共都是投稿」): 1. 本地 `tinygo build`(或讀已 build 的 .wasm)。 2. **本地跑 Gherkin**:對每個 `gherkin_tests[]`,用 Node 的 WebAssembly + 同一份 wasi-shim instantiate wasm,given→stdin→run→比對 then_contains。Node 環境能 runtime 編譯 wasm(不像 CF Workers)。 3. 任一 scenario 失敗 → 投稿指令本地就擋下,不送出。 @@ -148,10 +148,10 @@ interface SubmitRequest { ## 7. 範圍邊界 -- **動 registry TS**(sandboxAcceptance / submitComponent / routes / types)+ **CLI**(acr component create)+ **hook**。 +- **動 registry TS**(sandboxAcceptance / submitComponent / routes / types)+ **CLI**(acr parts publish,既有指令)+ **hook**。 - 不動 cypher-executor 執行路徑、不動既有零件 wasm。 - backfill 路徑(skip_acceptance)保持可用,不被新閘門擋(存量零件不需人類閘門)。 -- CLI/MCP/Python/JS 四路:本期至少做 CLI `acr component create` + registry endpoint 強制;MCP/Python/JS 補 `human_confirmation` 欄位支援(薄)。 +- CLI/MCP/Python/JS 四路:本期至少做 CLI `acr parts publish` + registry endpoint 強制;MCP/Python/JS 補 `human_confirmation` 欄位支援(薄)。 ## 8. 驗收標準 diff --git a/.agents/specs/component-gatekeeping/tasks.md b/.agents/specs/component-gatekeeping/tasks.md index 40d6281..51c4d28 100644 --- a/.agents/specs/component-gatekeeping/tasks.md +++ b/.agents/specs/component-gatekeeping/tasks.md @@ -31,13 +31,16 @@ - [x] 5.2 cold_start / runtime_compat 列入 unimplemented_steps,submit 回應明示 ## G0 人類閘門(R4,核心) -- [ ] 0.1 submit 請求增 `human_confirmation { confirmed_by_human, reason_why_not_workflow, confirmed_at }` -- [ ] 0.2 submit 邏輯:非 skip_acceptance 的新投稿,無 human_confirmation/空 reason → 403 指回正路 -- [ ] 0.3 reason_why_not_workflow 寫進 KV metadata(軌跡可審) -- [ ] 0.4 CLI `acr component create`:互動式問人類(工作流為何做不到 + 確認),非 TTY 拒絕 +- [x] 0.1 submit 請求增 `human_confirmation`(SubmitOptions in submitComponent.ts)+ route 解析(multipart/JSON 皆支援) +- [x] 0.2 submit 邏輯:非 skip_acceptance 的新投稿,無 human_confirmation/空 reason → gateError(指回正路) +- [x] 0.3 human_confirmation + gherkin_evidence 寫進 KV metadata(軌跡可審) +- [ ] 0.4 CLI `acr parts publish`(既有指令):互動式問人類(工作流為何做不到 + 確認),非 TTY 拒絕 - [ ] 0.5 MCP / Python lib / JS lib 補 human_confirmation 欄位支援(薄) - [ ] 0.6 誠實限制寫進 mindset Skill(步驟 7)+ SDK 文件 +> 命名修正(2026-05-29):投稿走**既有** `acr parts publish`(cli/src/commands/parts.ts), +> 非另建 acr component create(符合「修改現有不重建」)。G0-CLI(0.4)與 G4-CLI 合併在此指令做。 + ## R5 白名單 + 本機 hook - [ ] 5.3 `registry/MVP_COMPONENTS.txt`(現役 22 個 canonical_id) - [ ] 5.4 submit 過閘門成功 → 自動 append canonical_id 進白名單(Q3) diff --git a/registry/src/actions/submitComponent.ts b/registry/src/actions/submitComponent.ts index d271e81..4827c29 100644 --- a/registry/src/actions/submitComponent.ts +++ b/registry/src/actions/submitComponent.ts @@ -14,8 +14,43 @@ // 相同 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 { @@ -33,11 +68,32 @@ export async function submitComponent( wasmBytes: Uint8Array, contract: ComponentContract, env: Bindings, + options: SubmitOptions = {}, ): Promise { - // 1. 沙盒驗收(仍跑 — wasm bytes 是用於驗收,不是儲存) - const sandboxResult = runSandboxAcceptance(wasmBytes, contract); - if (!sandboxResult.success) { - return 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 @@ -80,6 +136,9 @@ export async function submitComponent( 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)); diff --git a/registry/src/routes/components.ts b/registry/src/routes/components.ts index ef90227..7ec7b38 100644 --- a/registry/src/routes/components.ts +++ b/registry/src/routes/components.ts @@ -7,6 +7,7 @@ import { Hono } from 'hono'; import type { Bindings } from '../types'; import { validateContract } from '../actions/validateContract'; import { submitComponent } from '../actions/submitComponent'; +import type { SubmitOptions } from '../actions/submitComponent'; import { indexOnlyComponent } from '../actions/indexOnlyComponent'; const app = new Hono<{ Bindings: Bindings }>(); @@ -15,6 +16,8 @@ app.post('/', async c => { // 接受 multipart/form-data:contract(JSON 字串)+ wasm(binary) let contract: unknown; let wasmBytes: Uint8Array; + // G0/G4:人類確認 + 舉證 + 本地 Gherkin evidence(multipart 或 JSON 皆可帶) + const submitOptions: SubmitOptions = {}; const contentType = c.req.header('content-type') ?? ''; @@ -37,6 +40,12 @@ app.post('/', async c => { } wasmBytes = new Uint8Array(await wasmFile.arrayBuffer()); + + const hc = formData.get('human_confirmation'); + if (typeof hc === 'string') { try { submitOptions.human_confirmation = JSON.parse(hc); } catch { /* ignore */ } } + const ge = formData.get('gherkin_evidence'); + if (typeof ge === 'string') { try { submitOptions.gherkin_evidence = JSON.parse(ge); } catch { /* ignore */ } } + if (formData.get('skip_acceptance') === 'true') submitOptions.skip_acceptance = true; } else { // 也支援純 JSON(用於測試,wasm 以 base64 傳入) let body: Record; @@ -62,6 +71,10 @@ app.post('/', async c => { for (let i = 0; i < binaryStr.length; i++) { wasmBytes[i] = binaryStr.charCodeAt(i); } + + if (body.human_confirmation) submitOptions.human_confirmation = body.human_confirmation as SubmitOptions['human_confirmation']; + if (body.gherkin_evidence) submitOptions.gherkin_evidence = body.gherkin_evidence as SubmitOptions['gherkin_evidence']; + if (body.skip_acceptance === true) submitOptions.skip_acceptance = true; } // 驗證 contract 格式 @@ -76,8 +89,8 @@ app.post('/', async c => { }, 422); } - // 執行沙盒驗收 + 寫入 KBDB + 上傳 R2 - const result = await submitComponent(wasmBytes, validation.contract!, c.env); + // 執行沙盒驗收(含 G0 人類閘門)+ 寫入 metadata + const result = await submitComponent(wasmBytes, validation.contract!, c.env, submitOptions); if (!result.success) { return c.json(result, 422);