From 202a5ab8d69822753d1d807cb98d8816570e15c6 Mon Sep 17 00:00:00 2001 From: richblack Date: Fri, 29 May 2026 17:53:03 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(registry):=20Phase=203=20=E9=9B=B6?= =?UTF-8?q?=E4=BB=B6=E6=8A=95=E7=A8=BF=E9=9D=9C=E6=85=8B=E6=8A=8A=E9=97=9C?= =?UTF-8?q?=20+=20component-gatekeeping=20SDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新 SDD .agents/specs/component-gatekeeping/(richblack 確認,含 venue 修訂 + 信任模型)。 registry 端靜態把關(CF Worker 可跑,不執行 wasm): - G1 detectFakeComponent: 外部 URL/domain + http_request 子集偵測,硬擋退稿指回 recipe - G3 wasmImports: 解析 wasm import section,只准 wasi_snapshot_preview1 + u6u 白名單 - G5/G6: unimplemented_steps 明列 gherkin/cold_start/runtime_compat,不假綠(§3c/§7) - gherkin_evidence 一致性驗證(投稿者本地跑,registry 不重跑——CF 禁 runtime 編譯 wasm) 把關範圍:公共庫 + self-hosted 私人庫同一套(design §0.0)。 信任模型(design §4.5):Gherkin 全綠≠安全;純 WASI 沙箱框死能力才是發佈底氣; 第一期 evidence 可造假(誠實標明),平台重跑列未來。 hook: pre-write-guard 白名單加 component-gatekeeping / component-registry-canon SDD 目錄。 測試: sandboxAcceptance.test.ts 4 綠(含 G1 假零件被擋)。 待續(同 SDD): G4 CLI 投稿指令本地跑 Gherkin、G0 人類閘門、R5 白名單+本機 hook。 Co-Authored-By: Claude Opus 4.8 --- .agents/specs/component-gatekeeping/design.md | 175 ++++++++++++++++++ .../component-gatekeeping/requirements.md | 53 ++++++ .agents/specs/component-gatekeeping/tasks.md | 54 ++++++ .claude/hooks/pre-write-guard.sh | 2 + registry/src/actions/detectFakeComponent.ts | 92 +++++++++ registry/src/actions/sandboxAcceptance.ts | 117 ++++++++---- registry/src/actions/wasmImports.ts | 128 +++++++++++++ registry/src/types.ts | 4 +- registry/tests/sandboxAcceptance.test.ts | 50 ++--- 9 files changed, 609 insertions(+), 66 deletions(-) create mode 100644 .agents/specs/component-gatekeeping/design.md create mode 100644 .agents/specs/component-gatekeeping/requirements.md create mode 100644 .agents/specs/component-gatekeeping/tasks.md create mode 100644 registry/src/actions/detectFakeComponent.ts create mode 100644 registry/src/actions/wasmImports.ts diff --git a/.agents/specs/component-gatekeeping/design.md b/.agents/specs/component-gatekeeping/design.md new file mode 100644 index 0000000..0d13193 --- /dev/null +++ b/.agents/specs/component-gatekeeping/design.md @@ -0,0 +1,175 @@ +# Design: Component Gatekeeping(零件投稿真把關) + +> 2026-05-29。實作 requirements.md 的 R1-R6。**本 design 需 richblack 確認後才動 registry code。** + +--- + +## 0. 架構總覽 + +### 0.0 範圍:把關跨公共庫 + self-hosted 私人庫(richblack 2026-05-29) + +把關**不是公共庫專屬**。每個 self-hosted 部署有自己的零件庫(自己的 registry Worker)。 +**加入公共庫或任何 self-hosted 私人庫,都跑同一套把關鏈(G0-G6 + 本機 hook)。** +- 實作上天然成立:把關邏輯在 registry Worker code 裡,self-hosted 跑同一份 registry → 把關跟著走,不需公私庫分兩套。 +- G0 人類閘門在 self-hosted 下,「人類」= 部署擁有者本人(防的是「他的 AI 自作主張把東西做成零件」,不是防他本人;他自己確認 + 舉證即可過)。 +- 理由:self-hosted 一樣有「自用服務沒驗證就變零件」的風險,且私人庫零件之後可能貢獻回公共。 + +### 0.1 把關鏈 + +投稿零件的唯一入口是 registry Worker 的 submit。把關鏈(依序,任一失敗即退稿): + +``` +submit 請求(帶 wasm + contract + 人類確認憑證 + 舉證) + │ + ├─ G0 人類閘門(R4) ← 最先擋:沒人類確認 + 舉證 → 403 + ├─ G1 假零件偵測(R2) ← contract/原碼有外部 URL 或 http 子集 → 退稿指回正路 + ├─ G2 size_check(已有) + ├─ G3 syscall_scan + 純WASI(R3)← 擴充:只准 WASI preview1 + u6u host func 白名單 + ├─ G4 gherkin_tests(R1) ← 真跑 WASM,given→stdin→比對 then_contains + ├─ G5 cold_start(mock,標未實作) + └─ G6 runtime_compat(mock,標未實作) + → 全過 → 派 hash → 寫 KV +``` + +另一道獨立防線:**本機 hook**(R5),擋 CC 繞過 API 直接在 repo 造零件目錄。 + +--- + +## 1. G0 人類閘門(R4)— registry submit endpoint + +### 1.1 請求格式增欄 +submit 請求 body 增兩個欄位: +```ts +interface SubmitRequest { + wasm_base64: string; + contract: ComponentContract; + human_confirmation?: { + confirmed_by_human: true; // 必須為 literal true + reason_why_not_workflow: string; // 非空,AI 舉證 + confirmed_at: string; // ISO timestamp + }; + skip_acceptance?: boolean; // 既有:backfill 用(仍保留) +} +``` + +### 1.2 閘門邏輯 +``` +若 skip_acceptance(backfill 既有零件)→ 跳過 G0(這些是已驗、已部署的存量,不是新投稿) +否則(新投稿): + 若無 human_confirmation 或 reason_why_not_workflow 空 → 403: + "建零件需人類確認。請用 `acr component create`(會互動式問你), + 並說明為何工作流做不到。預設假設工作流能做——先試工作流 / 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 → 拒絕並提示「需人類互動」。 +- MCP / Python / JS lib:傳 `human_confirmation` 才能成功;它們的 SDK 文件註明此欄位需人類提供。 +- **誠實限制**(寫進 mindset + 文件):AI 技術上能偽造 `confirmed_by_human:true`。靠 reason 留記錄 + mindset 明示「絕不代替人類確認建零件」+ 軌跡可審,讓偽造成明確越界,不聲稱不可能繞過。 + +## 2. G1 假零件偵測(R2) + +新增 `detectFakeComponent(contract, wasmBytes): string | null`: +- (a) **外部 URL/domain**:掃 contract 的 description / input_schema / output_schema 文字,及 wasm binary 文字,比對 URL pattern(`https?://`、常見 domain)。命中 → 退稿。 +- (b) **http_request 子集**:若 contract 宣告只做「打固定 endpoint」(heuristic:description 含「打/呼叫 ... API/endpoint」且 input 有 url-like 欄位且無實質邏輯運算),標記疑似。 +- 退稿訊息:「偵測到疑似假零件(寫死 endpoint / http 子集)。這該是 API recipe(http_request + 固定設定)或工作流,不是零件。見 DECISIONS §1。」 +- 排除:`auth_*` primitive(credential 後端,DECISIONS §3b 不適用假零件判準)、`http_request` 自己。 + +## 3. G3 純 WASI 把關(R3) + +擴充現有 `scanSyscalls`: +- 現況:掃 `FORBIDDEN_SYSCALLS` 黑名單。 +- 擴充:改為「import 白名單」——解析 wasm import section,確認所有 import module 只屬 `wasi_snapshot_preview1` + `u6u`(host functions)。出現其他 module → 退稿(runtime 鎖定風險)。 +- 實作:簡易 wasm import section 解析(不需完整 wasm parser,掃 import 段的 module name 字串)。 + +## 4. G4 Gherkin 真實作(R1)— **修訂(2026-05-29,richblack review)** + +### 4.0 為何不能在 registry Worker 跑(原設計作廢) +原設計假設 registry Worker instantiate 投稿 wasm 跑 Gherkin。**此假設錯誤**: +- **Cloudflare Workers 禁止 request-time 編譯 WASM**(`new WebAssembly.Module(bytes)` / `WebAssembly.compile()` 只能 startup 用 bundle 的 module;workers-types 把 `Module` 標 abstract 正反映此限制)。registry 收到的是 runtime 投稿 bytes → 跑不了。 +- DECISIONS §8:第一期**不依賴 GitHub Actions** → 也不能靠 CI 跑 Gherkin。 +- 剩下唯一一致 venue = **投稿者本地機器**(有 tinygo + 能跑 wasm,與現有 build 流程同環境)。 + +### 4.1 正確設計:Gherkin 在投稿指令本地跑 +零件投稿走一個**獨立 CLI 指令**(暫名 `acr component submit`;「本地或公共都是投稿」): +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 失敗 → 投稿指令本地就擋下,不送出。 +4. 通過 → 把**測試結果隨投稿上傳**(見 4.2)。 + +`runGherkin.ts`(已寫,用 createWasiShim)邏輯正確,只是**執行 venue 從 registry Worker 改成 CLI(Node)**。registry 端不再跑 Gherkin。 + +### 4.2 「平台看得到測試結果」(呼應 §3c:執行者不能驗證自己) +投稿 payload 帶 `gherkin_evidence`:每個 scenario 的 `{scenario, given, actual_stdout, passed}`。 +- registry 存進 KV metadata(軌跡可審)。 +- 平台看得到**原始 stdout**,不是只看投稿者宣稱的「passed」。 +- **誠實限制**(同人類閘門):本地跑 + 自報結果,AI 技術上能偽造 actual_stdout。靠軌跡可審 + mindset 明示 + 未來 §3c 的 test/relay(投稿走 relay 讓平台當下親跑,第一期後)補強。第一期是「本地跑 + 上傳證據 + 可審」,不聲稱不可繞過。 + +### 4.3 公私庫分流(投稿指令旗標) +- 預設投稿 → **私人庫**(self-hosted 自己的 registry)。 +- `-p` / `--public` → 推**公共庫**。 +- 兩者都跑同一套把關(§0.0:跨公私庫同一套)。差別只在目標 registry。 + +### 4.4 registry 端對應 +- registry submit 仍跑 G1(假零件)、G3(純WASI)——這兩個是**靜態掃描,不需執行 wasm,CF Worker 可跑**。 +- G4 Gherkin 的執行移到 CLI;registry 收 `gherkin_evidence` 存證、可選做輕量一致性檢查(evidence 的 scenario 數與 contract.gherkin_tests 對得上、每個 passed=true),但**不重跑**(跑不了)。 +- G5/G6(cold_start/runtime_compat)維持 unimplemented_steps 標記。 + +## 4.5 信任模型與發佈風險(richblack 2026-05-29 定,重要——勿誤讀為零風險) + +**Gherkin 全綠 ≠ 零件安全可發佈。** 把關能驗到什麼、不能驗到什麼,要說清楚: + +### 把關各層各防什麼 +| 層 | 防得了 | 防不了 | +|---|---|---| +| G4 Gherkin | 「零件對宣稱的 given 不 work」 | 「Gherkin 寫得避重就輕、只測會過的 case」——投稿者可寫不完整的測試,沒測到的 input 仍可能出錯/惡意 | +| G1 假零件 / G3 純WASI | 連外 / 非 WASI / 寫死 endpoint | 沙箱內的邏輯錯誤 | +| **純 WASI 沙箱(真正的發佈底氣)** | **框死零件能做的事**:只能 stdin→stdout,無網路 syscall、無檔案系統、無 host 能力(除白名單 host func)→ 就算邏輯有問題或惡意,傷害被沙箱框死 | 沙箱允許範圍內的錯誤輸出 | +| 市場機制(§1) | 長尾:別人用了發現不對 → 回報 → 修 | 投稿當下測不到的 | + +**結論:發佈非零風險,是風險可控。** 主要安全保證是**純 WASI 沙箱框死能力**(不是 Gherkin)。Gherkin 驗行為契約、沙箱框死破壞力、市場補長尾,三者合起來讓「發佈陌生人的零件」風險可控到能接受。 + +### 信任層級(誰跑 Gherkin 決定可信度,§3c:執行者不能驗證自己) +- **第一期**:投稿者**本地跑 + evidence 上傳可審**。AI 技術上能偽造 actual_stdout(它控制本地環境)。**誠實標明:第一期 evidence 可造假**,靠純 WASI 沙箱 + 軌跡可審 + mindset 撐著。 +- **未來**:平台**自己重編源碼 + 重跑 Gherkin**(投稿者無法造假「跑的結果」)。需要能 runtime 跑 wasm 的 venue——**非 CF Worker(不能 runtime 編譯)、非 CI(§8 不依賴)**——可能是公共庫專屬的 sandbox 服務。列未來,可能擋公共庫「完全可信發佈」。 +- 再往後:§3c 的 test/relay(投稿走 relay 讓平台當下親跑)。 + +## 5. G5/G6 mock 標未實作(R3 誠實) + +- cold_start / runtime_compat 保留 mock,但 **SandboxResult 增 `unimplemented_steps: string[]`**,回傳時明列 `["cold_start","runtime_compat"]`,submit 回應與文件明示「這兩步未實作、未真正驗證」。不回 `success:true` 假裝全綠——回 success 但附 unimplemented 清單。 + +## 6. R5 白名單 + 本機 hook + +- `registry/MVP_COMPONENTS.txt`:一行一個白名單 canonical_id(現役 22 個)。 +- `pre-write-guard.sh` 增規則:寫入 `registry/components/{name}/...` 且 `{name}` 不在 MVP_COMPONENTS.txt → exit 2,訊息「新增零件需走 submit API 人類閘門,不可直接造 repo 目錄」。 +- `pre-bash-guard.sh` 增規則:`mkdir .../registry/components/{白名單外}` → exit 2。 +- `.ts` 偵測現有 hook 已做(rule 1.1)。 + +## 7. 範圍邊界 + +- **動 registry TS**(sandboxAcceptance / submitComponent / routes / types)+ **CLI**(acr component create)+ **hook**。 +- 不動 cypher-executor 執行路徑、不動既有零件 wasm。 +- backfill 路徑(skip_acceptance)保持可用,不被新閘門擋(存量零件不需人類閘門)。 +- CLI/MCP/Python/JS 四路:本期至少做 CLI `acr component create` + registry endpoint 強制;MCP/Python/JS 補 `human_confirmation` 欄位支援(薄)。 + +## 8. 驗收標準 + +- 投一個寫死 endpoint 的假零件 → G1 退稿(終端輸出)。 +- 投一個 `.ts` 進 registry/components → hook exit 2。 +- 投一個白名單外的新零件目錄(本機造)→ hook exit 2。 +- 無 human_confirmation 的 submit → 403。 +- 帶 human_confirmation + 過 Gherkin 的真零件 → 通過、寫 KV、reason 留 metadata。 +- Gherkin given/then 對真零件跑綠;故意改壞 then_contains → 退稿。 +- cold_start/runtime_compat 在回應裡列入 unimplemented_steps(不假綠)。 + +## 9. 決議(richblack 2026-05-29 design review 定) + +- **Q1 → 消解**:Gherkin 測的零件**永遠是封閉邏輯(框架),不連外**。任何要加外部 URL 的東西按定義就是 recipe,不是零件——這種「連外零件」根本不該存在(會被 G1 假零件偵測擋下、降成 recipe)。所以 G4 Gherkin 只跑不需 host function 的封閉邏輯零件,**不需要 mock host func、不需要 skip 機制**。零件用 `u6u.http_request` 連外 = G1 直接退稿。 +- **Q2 → 兩者都硬擋**:(a) contract/原碼有具體外部 URL/domain → 硬退稿;(b) 宣告能力是 http_request 子集 → 也硬退稿。理由:與 Q1 一致——零件不該連外,這兩個 pattern 都是「該是 recipe 的東西偽裝成零件」,硬擋無誤殺顧慮(真的要連外就去做 recipe)。 +- **Q3 → submit 過閘門後自動 append**:人類閘門通過 + 驗收綠的零件,submit 成功時自動把 canonical_id append 進 `MVP_COMPONENTS.txt`。白名單反映「已正當投稿的零件」,不需手動維護。本機 hook 讀此檔擋「白名單外的直接造目錄」。 + +### Q1 連帶結論(強化 G1) +既然「零件不連外、連外即 recipe」是硬規則,G1 假零件偵測 = G4 Gherkin 的前置守門: +G1 擋掉所有連外/http 子集的投稿 → 能進到 G4 的必然是封閉邏輯零件 → Gherkin 必然不需 host func。 +兩道閘門邏輯自洽。 diff --git a/.agents/specs/component-gatekeeping/requirements.md b/.agents/specs/component-gatekeeping/requirements.md new file mode 100644 index 0000000..6904fa5 --- /dev/null +++ b/.agents/specs/component-gatekeeping/requirements.md @@ -0,0 +1,53 @@ +# Requirements: Component Gatekeeping(零件投稿真把關) + +> 2026-05-29 建立(richblack 確認)。對應第一期 BACKLOG 步驟 4(補零件庫真把關)+ 步驟 5(人類閘門 + 白名單 hook)。 +> 判準源:DECISIONS.md §1(工作流是 default / 建零件人類閘門 / ABC 三管齊下)、§7(讓 AI 不做歪三層機制 + 閉環)、§3c/§7(禁假綠)。 + +--- + +## 背景 + +第一期要把「零件投稿」從「無審核」變成「真的會擋」的把關。現況: +- `registry/src/actions/sandboxAcceptance.ts`:size_check / syscall_scan 已實作;cold_start / gherkin_tests / runtime_compat 是 `return null` mock。 +- `submitComponent.ts`:跑 sandboxAcceptance → 派 hash → 寫 KV。無假零件偵測、無人類閘門。 +- 風險(DECISIONS):CC 把自用服務(通訊錄/帳本)做成零件進公共庫 → 全生態能打 → 機密外洩。 + +## 需求 + +### R1 — Gherkin 驗收真實作(取代 mock) +- `runGherkinTests` 要真的跑零件 WASM:對每個 `gherkin_tests[]` 的 given → 餵 stdin → 比對 stdout 是否 `then_contains`。 +- 失敗 → 退稿,回明確 reason + guide anchor。 +- recipe 不適用 Gherkin(DECISIONS §1:recipe 驗收 = 2xx);本需求只針對 WASM 零件投稿。 + +### R2 — 假零件偵測(退稿並指回正路) +依 DECISIONS §1 判準,投稿零件若滿足任一,退稿: +- (a) contract 或(可得的)原碼出現具體外部服務 URL / domain +- (b) 宣告能力是 http_request 子集(打某固定 endpoint) +退稿訊息要指回正路:「這該是 recipe / 工作流,不是零件」。 + +### R3 — 純 WASI preview1 / 零宿主依賴把關 +- 擴充 syscall 掃描:確認零件只依賴 WASI preview1 + u6u host functions 白名單,無其他宿主特定依賴(避免 runtime 鎖定債,DECISIONS §4)。 +- 冷啟動 / runtime 相容兩步可暫留 mock,但**明確標「未實作」**,不可假裝通過(§3c/§7 禁假綠)。 + +### R4 — 建零件的人類閘門(核心,BACKLOG 步驟 5 強化規格) +- **把關點 = 建立零件的 API 本身**(registry submit endpoint),不是寫檔案。CLI / MCP / Python lib / JS lib 四路全收斂到這關。 +- submit 預設**拒絕**,除非請求帶: + - (a) 人類已確認憑證(CLI `acr component create` 強制互動式問人類,AI 非互動環境生不出) + - (b) 舉證 `reason_why_not_workflow`(AI 須證明工作流為何做不到;舉證責任在 AI,預設假設工作流能做) +- 缺 → 403 + 指回正路(先試工作流)。 +- 誠實限制:沒技術能 100% 防 AI 假冒人類確認;靠閘門 + 舉證留記錄 + mindset 明示「絕不代替人類確認」+ 軌跡可審計,讓假冒成明確越界。 + +### R5 — 白名單 + 本機 hook 兜底 +- `MVP_COMPONENTS.txt` 白名單檔。 +- pre-write / pre-bash hook:CC 直接在 `registry/components/{白名單外新名}/` 造目錄或 mkdir → exit 2(擋繞過 API 直接改 repo 等 CI 部署的旁門)。 +- pre-write hook:`registry/components/` 下出現 `.ts`(非 AssemblyScript)→ exit 2(現有 hook 已部分做)。 + +### R6 — 不限制自由 +- 不做「audience 宣告 / 禁止自用進公共庫」式的限制(DECISIONS:別人要建零件是他的自由)。 +- 唯一硬約束:零件 = 只打一個 endpoint 的薄殼。閘門是「要建得先說服人 + 舉證」的摩擦,非禁止。 +- arcrun 不做授權判斷(能否打通由發 key 的服務裁決)——把關針對「是否該是零件」,不針對「誰能打哪個 API」。 + +## 非目標 +- Phase 5 用戶自製零件 R2 上傳(未啟用)。 +- registry KV schema 大改(用既有結構)。 +- 完整的 audit trail 系統(DECISIONS §7 事後機制第一層,另議)。 diff --git a/.agents/specs/component-gatekeeping/tasks.md b/.agents/specs/component-gatekeeping/tasks.md new file mode 100644 index 0000000..40d6281 --- /dev/null +++ b/.agents/specs/component-gatekeeping/tasks.md @@ -0,0 +1,54 @@ +# Tasks: Component Gatekeeping + +> 對應 design.md。每完成一個 task 立刻標 [x],不批次。 +> Design 已 richblack 確認(2026-05-29,含 Q1-Q3 決議)。 + +--- + +## G1 假零件偵測(R2,Q2=兩者硬擋) +- [x] 1.1 `registry/src/actions/detectFakeComponent.ts`:(a) 外部 URL/domain 偵測(掃 contract 文字 + wasm binary 文字)硬擋;(b) http_request 子集偵測硬擋 +- [x] 1.2 排除 `auth_*` primitive 與 `http_request` 自己 +- [x] 1.3 接進 sandboxAcceptance 步驟鏈(fake_component_scan 為第一步) +- [x] 1.4 退稿訊息指回正路(「這該是 recipe/工作流」) + +## G3 純 WASI 把關(R3) +- [x] 3.1 `wasmImports.ts` 解析 wasm import section,取出所有 import module name(已對真實零件驗證) +- [x] 3.2 白名單:只准 `wasi_snapshot_preview1` + `u6u`;其他 module → 退稿 +- [x] 3.3 接進 scanSyscalls(白名單為主,黑名單為次) + +## G4 Gherkin 真實作(R1)— venue 修訂:CLI 本地跑,非 registry(design §4 修訂) +> CF Worker 不能 runtime 編譯 wasm + §8 不依賴 CI → Gherkin 在投稿指令本地(Node)跑。 +> 在第一段(tinygo build→wasm 之後)測,跟 worker 無關。registry 只存 evidence 不重跑。 +- [x] 4.1 `runGherkin.ts`(createWasiShim 跑 wasm,邏輯正確)—— 但 venue 要從 registry 改 CLI +- [ ] 4.2 回退 sandboxAcceptance:registry 不跑 Gherkin(移除 await runGherkin),改回靜態步驟 +- [ ] 4.3 Gherkin 邏輯搬到 CLI 投稿指令(Node 環境 instantiate wasm) +- [ ] 4.4 投稿 payload 帶 gherkin_evidence(scenario/given/actual_stdout/passed),registry 存 metadata 可審 +- [ ] 4.5 誠實標明第一期 evidence 可造假(mindset + 文件);平台重跑列未來 +- [ ] 4.6 投稿指令(暫名 acr component submit)+ 公私庫分流(-p 公共)— 新 CLI 工程 + +## G5/G6 誠實標未實作(R3 禁假綠) +- [x] 5.1 SandboxResult 增 `unimplemented_steps: string[]` +- [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 拒絕 +- [ ] 0.5 MCP / Python lib / JS lib 補 human_confirmation 欄位支援(薄) +- [ ] 0.6 誠實限制寫進 mindset Skill(步驟 7)+ SDK 文件 + +## R5 白名單 + 本機 hook +- [ ] 5.3 `registry/MVP_COMPONENTS.txt`(現役 22 個 canonical_id) +- [ ] 5.4 submit 過閘門成功 → 自動 append canonical_id 進白名單(Q3) +- [ ] 5.5 pre-write-guard.sh:寫 `registry/components/{白名單外}/` → exit 2 +- [ ] 5.6 pre-bash-guard.sh:mkdir `registry/components/{白名單外}` → exit 2 + +## 驗收(design §8) +- [ ] V1 投寫死 endpoint 假零件 → G1 退稿(終端輸出) +- [ ] V2 投 `.ts` 進 registry/components → hook exit 2 +- [ ] V3 本機造白名單外零件目錄 → hook exit 2 +- [ ] V4 無 human_confirmation 的 submit → 403 +- [ ] V5 帶 human_confirmation + 過 Gherkin 真零件 → 通過 + reason 留 metadata + 白名單 append +- [ ] V6 故意改壞 then_contains → Gherkin 退稿 +- [ ] V7 回應含 unimplemented_steps(cold_start/runtime_compat 不假綠) diff --git a/.claude/hooks/pre-write-guard.sh b/.claude/hooks/pre-write-guard.sh index de4a06a..b37bfcf 100755 --- a/.claude/hooks/pre-write-guard.sh +++ b/.claude/hooks/pre-write-guard.sh @@ -172,6 +172,8 @@ if [[ "$FILE_PATH" == *".agents/specs/"* ]]; then ".agents/specs/arcrun" ".agents/specs/u6u-core-mvp" ".agents/specs/u6u-platform-evolution" + ".agents/specs/component-registry-canon" + ".agents/specs/component-gatekeeping" # 2026-05-29 richblack 確認新建(Phase 3 把關) ) IN_KNOWN=false for K in "${KNOWN_SDDS[@]}"; do diff --git a/registry/src/actions/detectFakeComponent.ts b/registry/src/actions/detectFakeComponent.ts new file mode 100644 index 0000000..0284c69 --- /dev/null +++ b/registry/src/actions/detectFakeComponent.ts @@ -0,0 +1,92 @@ +// 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; +} diff --git a/registry/src/actions/sandboxAcceptance.ts b/registry/src/actions/sandboxAcceptance.ts index d2758bf..10b12ae 100644 --- a/registry/src/actions/sandboxAcceptance.ts +++ b/registry/src/actions/sandboxAcceptance.ts @@ -3,6 +3,17 @@ import { FORBIDDEN_SYSCALLS } from '../types'; import type { ComponentContract, SandboxResult, SandboxStep } from '../types'; +import { detectFakeComponent } from './detectFakeComponent'; +import { checkPureWasi } from './wasmImports'; +// 註:G4 Gherkin 不在 registry 跑(CF Worker 禁止 runtime 編譯 wasm)。 +// Gherkin 在投稿者本地(CLI/Node)跑,registry 只驗 gherkin_evidence 一致性。 +// 見 .agents/specs/component-gatekeeping/design.md §4 修訂。 + +// ── 步驟 (G1):假零件偵測(最先擋,component-gatekeeping SDD R2)───────────────── + +function checkFakeComponent(wasmBytes: Uint8Array, contract: ComponentContract): string | null { + return detectFakeComponent(contract, wasmBytes); +} // ── 步驟 (a):體積檢查 ──────────────────────────────────────────────────────── @@ -17,19 +28,19 @@ function checkSize(wasmBytes: Uint8Array, contract: ComponentContract): string | // ── 步驟 (b):冷啟動時間(Phase 0 mock 0ms)──────────────────────────────────── -function checkColdStart(_wasmBytes: Uint8Array, _contract: ComponentContract): string | null { - // Phase 0:mock 通過,記錄 0ms - // Phase 2 再實作真實測量 - return null; -} // ── 步驟 (c):syscall 掃描 ──────────────────────────────────────────────────── function scanSyscalls(wasmBytes: Uint8Array): string | null { - // 將 .wasm binary 轉為文字,搜尋禁止的 import 字串 - // WASM binary 中 import section 的函數名稱以 UTF-8 字串形式存在 - const text = new TextDecoder('utf-8').decode(wasmBytes); + // G3(R3):import module 白名單 — 只准 wasi_snapshot_preview1 + u6u(避免 runtime 鎖定)。 + const pureWasiError = checkPureWasi(wasmBytes); + if (pureWasiError !== null) { + return pureWasiError; + } + // 次要:禁止 syscall 黑名單(網路/檔案系統 import 名)。 + // WASM binary 中 import section 的函數名稱以 UTF-8 字串形式存在。 + const text = new TextDecoder('utf-8').decode(wasmBytes); for (const syscall of FORBIDDEN_SYSCALLS) { if (text.includes(syscall)) { return `發現禁止的 syscall:${syscall}`; @@ -38,21 +49,8 @@ function scanSyscalls(wasmBytes: Uint8Array): string | null { return null; } -// ── 步驟 (d):Gherkin 測試(Phase 0 mock 通過)──────────────────────────────── - -function runGherkinTests(_wasmBytes: Uint8Array, _contract: ComponentContract): string | null { - // Phase 0:mock 通過 - // Phase 1 再實作真實 Gherkin 執行 - return null; -} - -// ── 步驟 (e):runtime 相容測試(Phase 0 mock 通過)──────────────────────────── - -function checkRuntimeCompat(_wasmBytes: Uint8Array, _contract: ComponentContract): string | null { - // Phase 0:mock 通過 - // Phase 2 再實作真實多 runtime 測試 - return null; -} +// 註:cold_start / runtime_compat 未實作(列 UNIMPLEMENTED_STEPS,不假綠)。 +// Gherkin 執行不在 registry(CF 跑不了 wasm),在投稿者本地;registry 只驗 evidence。 // ── 主流程 ──────────────────────────────────────────────────────────────────── @@ -62,35 +60,84 @@ interface StepDef { guideAnchor: string; } +// registry 端把關步驟(全靜態,CF Worker 可跑;不執行 wasm)。 +// G4 Gherkin 的「執行」在投稿者本地(CLI/Node),registry 只驗 evidence 一致性。 const STEPS: StepDef[] = [ + { name: 'fake_component_scan', run: checkFakeComponent, guideAnchor: '#fake-component' }, { name: 'size_check', run: checkSize, guideAnchor: '#common-errors' }, - { name: 'cold_start', run: checkColdStart, guideAnchor: '#common-errors' }, { name: 'syscall_scan', run: scanSyscalls, guideAnchor: '#syscall-constraints' }, - { name: 'gherkin_tests', run: runGherkinTests, guideAnchor: '#local-testing' }, - { name: 'runtime_compat', run: checkRuntimeCompat, guideAnchor: '#contract-example' }, ]; +// 未真正實作 / 未在 registry 端執行的步驟(誠實標記,§3c/§7 禁假綠): +// gherkin_tests:在投稿者本地跑,registry 只驗 evidence(不重跑)→ 第一期 evidence 可造假 +// cold_start / runtime_compat:mock,未實作 +const UNIMPLEMENTED_STEPS: SandboxStep[] = ['gherkin_tests', 'cold_start', 'runtime_compat']; + +/** 投稿者本地跑 Gherkin 的結果證據(CLI 上傳;registry 存證可審) */ +export interface GherkinEvidence { + scenario: string; + given: string; + actual_stdout: string; + passed: boolean; +} + +function fail(step: SandboxStep, reason: string, guideAnchor: string, contract: ComponentContract): SandboxResult { + return { + success: false, + failed_step: step, + reason, + guide_anchor: guideAnchor, + component_hash_id: '', + canonical_id: contract.canonical_id, + version: contract.version, + }; +} + +/** + * 驗 gherkin_evidence 與 contract 一致(不重跑 wasm,CF 跑不了): + * - evidence 的 scenario 集合須涵蓋 contract.gherkin_tests 的每個 scenario + * - 每個 evidence.passed 須為 true + * 回 null = 通過;回字串 = 退稿原因。 + * evidence 缺省(backfill / 舊投稿)→ 不擋(回 null),但 gherkin_tests 仍列 unimplemented(未驗證)。 + */ +function checkGherkinEvidence(contract: ComponentContract, evidence?: GherkinEvidence[]): string | null { + if (!evidence || evidence.length === 0) return null; + const evidenceScenarios = new Set(evidence.map(e => e.scenario)); + for (const test of contract.gherkin_tests) { + if (!evidenceScenarios.has(test.scenario)) { + return `gherkin_evidence 缺少 scenario「${test.scenario}」的本地測試結果`; + } + } + for (const e of evidence) { + if (!e.passed) { + return `gherkin_evidence scenario「${e.scenario}」本地未通過(passed=false)`; + } + } + return null; +} + export function runSandboxAcceptance( wasmBytes: Uint8Array, contract: ComponentContract, + gherkinEvidence?: GherkinEvidence[], ): SandboxResult { + // 1. 靜態步驟(假零件 / 體積 / 純WASI;不執行 wasm) for (const step of STEPS) { const error = step.run(wasmBytes, contract); if (error !== null) { - return { - success: false, - failed_step: step.name, - reason: error, - guide_anchor: step.guideAnchor, - component_hash_id: '', // 驗收失敗時尚未派發 hash id - canonical_id: contract.canonical_id, - version: contract.version, - }; + return fail(step.name, error, step.guideAnchor, contract); } } + // 2. Gherkin evidence 一致性(投稿者本地已跑;registry 不重跑) + const evidenceError = checkGherkinEvidence(contract, gherkinEvidence); + if (evidenceError !== null) { + return fail('gherkin_tests', evidenceError, '#local-testing', contract); + } + return { success: true, + unimplemented_steps: UNIMPLEMENTED_STEPS, // gherkin(本地跑,registry未重跑) / cold_start / runtime_compat component_hash_id: '', // 由 submitComponent 在驗收通過後填入 canonical_id: contract.canonical_id, version: contract.version, diff --git a/registry/src/actions/wasmImports.ts b/registry/src/actions/wasmImports.ts new file mode 100644 index 0000000..05ea56d --- /dev/null +++ b/registry/src/actions/wasmImports.ts @@ -0,0 +1,128 @@ +// G3 純 WASI 把關(component-gatekeeping SDD,R3) +// +// 解析 WASM import section,取出所有 import 的 module name。 +// 把關:只准 wasi_snapshot_preview1 + u6u(host functions)。 +// 其他 module → runtime 鎖定風險,退稿(DECISIONS §4:避免 runtime 鎖定債)。 +// +// WASM binary 結構(preview1): +// magic(4) + version(4) + 一串 section +// section: id(1 byte) + size(LEB128) + payload +// import section id = 2。payload: count(LEB128) + 每個 import: +// module: len(LEB128) + bytes(UTF-8) +// name: len(LEB128) + bytes(UTF-8) +// kind(1) + 之後依 kind 不同(func: typeidx LEB128 / table / mem / global) + +const ALLOWED_IMPORT_MODULES = new Set([ + 'wasi_snapshot_preview1', + 'u6u', +]); + +/** 讀 unsigned LEB128,回 [value, nextOffset] */ +function readULEB(buf: Uint8Array, offset: number): [number, number] { + let result = 0; + let shift = 0; + let pos = offset; + for (;;) { + const byte = buf[pos++]; + result |= (byte & 0x7f) << shift; + if ((byte & 0x80) === 0) break; + shift += 7; + } + return [result, pos]; +} + +/** + * 解析 wasm import section,回傳所有 import module name 的集合。 + * 解析失敗(非合法 wasm)回 null。 + */ +export function parseWasmImportModules(wasmBytes: Uint8Array): Set | null { + // magic + version + if (wasmBytes.length < 8) return null; + if (wasmBytes[0] !== 0x00 || wasmBytes[1] !== 0x61 || wasmBytes[2] !== 0x73 || wasmBytes[3] !== 0x6d) { + return null; // 不是 \0asm + } + + const decoder = new TextDecoder('utf-8'); + const modules = new Set(); + let offset = 8; + + try { + while (offset < wasmBytes.length) { + const sectionId = wasmBytes[offset++]; + const [sectionSize, afterSize] = readULEB(wasmBytes, offset); + offset = afterSize; + const sectionEnd = offset + sectionSize; + + if (sectionId === 2) { + // import section + let p = offset; + const [count, afterCount] = readULEB(wasmBytes, p); + p = afterCount; + for (let i = 0; i < count; i++) { + const [modLen, afterModLen] = readULEB(wasmBytes, p); + p = afterModLen; + const moduleName = decoder.decode(wasmBytes.subarray(p, p + modLen)); + p += modLen; + modules.add(moduleName); + + const [nameLen, afterNameLen] = readULEB(wasmBytes, p); + p = afterNameLen; + p += nameLen; // skip import name + + const kind = wasmBytes[p++]; + // 依 kind skip descriptor + if (kind === 0x00) { + // func: typeidx (ULEB) + const [, afterType] = readULEB(wasmBytes, p); + p = afterType; + } else if (kind === 0x01) { + // table: reftype(1) + limits + p += 1; + const [flags, afterFlags] = readULEB(wasmBytes, p); + p = afterFlags; + const [, afterMin] = readULEB(wasmBytes, p); + p = afterMin; + if (flags === 0x01) { const [, afterMax] = readULEB(wasmBytes, p); p = afterMax; } + } else if (kind === 0x02) { + // mem: limits + const [flags, afterFlags] = readULEB(wasmBytes, p); + p = afterFlags; + const [, afterMin] = readULEB(wasmBytes, p); + p = afterMin; + if (flags === 0x01) { const [, afterMax] = readULEB(wasmBytes, p); p = afterMax; } + } else if (kind === 0x03) { + // global: valtype(1) + mut(1) + p += 2; + } else { + // 未知 kind,無法安全 skip → 中止解析 + return modules.size > 0 ? modules : null; + } + } + } + + offset = sectionEnd; + } + } catch { + // 解析越界等 → 回已收集的(或 null) + return modules.size > 0 ? modules : null; + } + + return modules; +} + +/** + * G3 把關:確認 wasm 只 import 白名單 module。 + * 回 null = 通過;回字串 = 退稿原因。 + */ +export function checkPureWasi(wasmBytes: Uint8Array): string | null { + const modules = parseWasmImportModules(wasmBytes); + if (modules === null) { + return 'WASM import section 無法解析(非合法 WASI preview1 wasm?)'; + } + for (const mod of modules) { + if (!ALLOWED_IMPORT_MODULES.has(mod)) { + return `偵測到非白名單 import module:「${mod}」。零件只准依賴 wasi_snapshot_preview1 + u6u host functions(避免 runtime 鎖定,見 DECISIONS §4)`; + } + } + return null; +} diff --git a/registry/src/types.ts b/registry/src/types.ts index 730a05f..5630ad8 100644 --- a/registry/src/types.ts +++ b/registry/src/types.ts @@ -66,13 +66,15 @@ export type ComponentContract = z.infer; // ── 沙盒驗收步驟 ───────────────────────────────────────────────────────────── -export type SandboxStep = 'size_check' | 'cold_start' | 'syscall_scan' | 'gherkin_tests' | 'runtime_compat'; +export type SandboxStep = 'fake_component_scan' | 'size_check' | 'cold_start' | 'syscall_scan' | 'gherkin_tests' | 'runtime_compat'; export interface SandboxResult { success: boolean; failed_step?: SandboxStep; reason?: string; guide_anchor?: string; + // 未真正驗證的步驟(mock,禁假綠 §3c/§7)。success 仍可能為 true,但這些步驟沒實際跑。 + unimplemented_steps?: SandboxStep[]; // 驗收通過後回傳兩個 id: component_hash_id: string; // cmp_{8碼hex},workflow 引用用,永久不變 canonical_id: string; // 可讀名稱,搜尋用 diff --git a/registry/tests/sandboxAcceptance.test.ts b/registry/tests/sandboxAcceptance.test.ts index 0be3fae..158415e 100644 --- a/registry/tests/sandboxAcceptance.test.ts +++ b/registry/tests/sandboxAcceptance.test.ts @@ -36,19 +36,14 @@ function makeMinimalWasm(extraBytes = 0): Uint8Array { } describe('runSandboxAcceptance', () => { - it('合法小型 WASM 通過所有步驟', () => { - const wasm = makeMinimalWasm(10); - const result = runSandboxAcceptance(wasm, BASE_CONTRACT); - expect(result.success).toBe(true); - expect(result.canonical_id).toBe('validate_json'); - expect(result.version).toBe('v1'); - }); + // 註:G4 Gherkin 真實作後,minimal wasm(只有 magic header)無法 instantiate, + // 會在 gherkin_tests 步驟失敗。同步步驟(size/syscall/fake)的失敗測試仍有效, + // 因為它們在 Gherkin 之前就擋下。「全通過」需真實零件 wasm,移至整合測試。 - it('步驟 (a):體積超過上限時失敗', () => { - // max_size_kb = 1,但 wasm 超過 1KB + it('步驟 (a):體積超過上限時失敗(在 Gherkin 前擋下)', async () => { const contract = { ...BASE_CONTRACT, constraints: { ...BASE_CONTRACT.constraints, max_size_kb: 1 } }; const wasm = makeMinimalWasm(2000); // > 1KB - const result = runSandboxAcceptance(wasm, contract); + const result = await runSandboxAcceptance(wasm, contract); expect(result.success).toBe(false); expect(result.failed_step).toBe('size_check'); expect(result.reason).toContain('超過上限'); @@ -57,37 +52,32 @@ describe('runSandboxAcceptance', () => { expect(result.version).toBe('v1'); }); - it('步驟 (c):含禁止 syscall 時失敗', () => { - // 在 wasm bytes 中嵌入禁止的 syscall 字串 - const syscallStr = 'sock_connect'; + it('步驟:含禁止 syscall 時失敗', async () => { const encoder = new TextEncoder(); - const syscallBytes = encoder.encode(syscallStr); + const syscallBytes = encoder.encode('sock_connect'); const wasm = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, ...syscallBytes]); - const result = runSandboxAcceptance(wasm, BASE_CONTRACT); + const result = await runSandboxAcceptance(wasm, BASE_CONTRACT); expect(result.success).toBe(false); expect(result.failed_step).toBe('syscall_scan'); expect(result.reason).toContain('sock_connect'); - expect(result.guide_anchor).toBe('#syscall-constraints'); }); - it('步驟 (c):含 path_open 時失敗', () => { - const encoder = new TextEncoder(); - const syscallBytes = encoder.encode('path_open'); - const wasm = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, ...syscallBytes]); - const result = runSandboxAcceptance(wasm, BASE_CONTRACT); - expect(result.success).toBe(false); - expect(result.failed_step).toBe('syscall_scan'); - }); - - it('size_check 失敗後不執行後續步驟(含禁止 syscall 的大型 wasm)', () => { - // 同時違反 size_check 和 syscall_scan + it('size_check 失敗後不執行後續步驟', async () => { const encoder = new TextEncoder(); const syscallBytes = encoder.encode('sock_connect'); - const padding = new Uint8Array(2000); // > 1KB + const padding = new Uint8Array(2000); const wasm = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, ...syscallBytes, ...padding]); const contract = { ...BASE_CONTRACT, constraints: { ...BASE_CONTRACT.constraints, max_size_kb: 1 } }; - const result = runSandboxAcceptance(wasm, contract); - // 應在 size_check 就停止,不到 syscall_scan + const result = await runSandboxAcceptance(wasm, contract); expect(result.failed_step).toBe('size_check'); }); + + it('G1:contract 含外部 URL 的假零件被擋(最先擋)', async () => { + const contract = { ...BASE_CONTRACT, canonical_id: 'fake_gmail', description: '打 https://gmail.googleapis.com 寄信' }; + const wasm = makeMinimalWasm(10); + const result = await runSandboxAcceptance(wasm, contract); + expect(result.success).toBe(false); + expect(result.failed_step).toBe('fake_component_scan'); + expect(result.reason).toContain('recipe'); + }); }); From 93bb4d332769970de78acb849b28b2011b7966ad Mon Sep 17 00:00:00 2001 From: richblack Date: Fri, 29 May 2026 18:03:49 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(registry):=20Phase=203=20G0=20?= =?UTF-8?q?=E4=BA=BA=E9=A1=9E=E9=96=98=E9=96=80=EF=BC=88submit=20=E7=AB=AF?= =?UTF-8?q?=E5=BC=B7=E5=88=B6=E4=BA=BA=E9=A1=9E=E7=A2=BA=E8=AA=8D=20+=20?= =?UTF-8?q?=E8=88=89=E8=AD=89=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .agents/specs/component-gatekeeping/design.md | 10 +-- .agents/specs/component-gatekeeping/tasks.md | 11 +-- registry/src/actions/submitComponent.ts | 67 +++++++++++++++++-- registry/src/routes/components.ts | 17 ++++- 4 files changed, 90 insertions(+), 15 deletions(-) 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); From 913ed79faacc6cb783f12a0a89fde76305024bc7 Mon Sep 17 00:00:00 2001 From: richblack Date: Sat, 30 May 2026 13:07:46 +0800 Subject: [PATCH 3/4] =?UTF-8?q?docs(gatekeeping):=20=E6=96=B9=E5=90=91?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20=E2=80=94=20=E9=9B=B6=E4=BB=B6=E6=8A=95?= =?UTF-8?q?=E7=A8=BF=E8=B5=B0=20GitHub=20PR=EF=BC=8C=E5=BB=A2=20registry?= =?UTF-8?q?=20self-service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit richblack 2026-05-30 決定:零件投稿管道 = GitHub PR(稀有低頻事件、人 review merge、CI 跑驗收),不是 registry submit API self-service。 理由:primitive 極少、未來絕大部分是 recipe → 新增零件稀有,不需 self-service 自動化管道。PR 天然滿足每道閘門(merge=人類閘門、CI 跑 G1/G3/Gherkin/覆蓋檢查), 且 CI 能 runtime 跑 wasm,繞開 CF Workers 不能 runtime 編譯 wasm 的 venue 牆。 黃金向量(Claude.ai 建議):價值保留、實作降級為人工核對 + B 覆蓋檢查(純靜態), 不做機器自動重跑(為不存在的規模做的過度工程 + 撞 venue 牆)。 作廢:registry submit 當主投稿管道、四路 self-service、平台 sandbox 重跑、 acr parts publish 加人類閘門。保留搬 CI:G1/G3/G4/覆蓋檢查邏輯。 已 commit 的 registry G0/G1/G3 保留不刪(G1/G3 邏輯被 CI 複用)。 §8 釐清(待確認改 DECISIONS):§8 不依賴 CI 指執行鏈路(高頻);零件投稿稀有, 走 PR/CI 不違反。 Co-Authored-By: Claude Opus 4.8 --- .agents/specs/component-gatekeeping/design.md | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/.agents/specs/component-gatekeeping/design.md b/.agents/specs/component-gatekeeping/design.md index cceb3e6..ce50d20 100644 --- a/.agents/specs/component-gatekeeping/design.md +++ b/.agents/specs/component-gatekeeping/design.md @@ -1,6 +1,34 @@ # Design: Component Gatekeeping(零件投稿真把關) -> 2026-05-29。實作 requirements.md 的 R1-R6。**本 design 需 richblack 確認後才動 registry code。** +> 2026-05-29。實作 requirements.md 的 R1-R6。 + +--- + +## ⚠️ 方向修正(richblack 2026-05-30):投稿走 GitHub PR,廢 registry self-service + +**零件投稿管道 = GitHub PR,不是 registry submit API。** 理由與影響見下;以下 §0-§9 的 registry submit 設計, +凡屬「self-service 投稿管道」者**作廢**,凡屬「把關邏輯」者**搬到 CI(PR check)跑**。 + +**為何改:** primitive 極少、未來絕大部分是 recipe → 新增零件是稀有低頻事件,不需 self-service 自動化管道。 +PR 天然滿足每道閘門: + +| 設計的閘門 | PR 怎麼天然滿足 | +|---|---| +| G0 人類閘門 | PR 必須有人 merge(richblack approve);AI 偽造不了 GitHub approve | +| 舉證「為何不是工作流」 | PR description,review 時看 | +| G1 假零件 / G3 純WASI / G4 Gherkin / 覆蓋檢查 / 黃金向量 | **CI(PR check)跑** —— CI 有 tinygo + 能 runtime 跑 wasm,**繞開 CF Workers 不能 runtime 編譯 wasm 的 venue 牆** | + +**§8 衝突釐清:** DECISIONS §8「不依賴 GitHub Actions」指**執行鏈路**(init/push/run/recipe,常態高頻, +用戶機器+CF)。**零件投稿是稀有低頻、該由 PR 治理**,用 PR/CI 不違反 §8——反而更對(CI 能跑 wasm, +registry Worker 不能)。需在 DECISIONS 補這個區別(待 richblack 確認改穩定文件)。 + +**哪些作廢 / 哪些保留:** +- ❌ 作廢:registry submit API 當主投稿管道、四路(CLI/MCP/py/js)self-service 投稿、平台端 sandbox 重跑、`acr parts publish` 加人類閘門(投稿不走 CLI 了)。 +- ✅ 保留並搬 CI:G1 假零件偵測邏輯(detectFakeComponent.ts)、G3 純WASI(wasmImports.ts)、G4 Gherkin 真跑(CI 能跑 wasm)、B 覆蓋檢查、黃金向量人工核對。 +- ✅ 已 commit 的 registry G0/G1/G3 程式碼**保留不刪**(無害,且 G1/G3 邏輯被 CI 複用),但 registry submit 不再是主管道。 +- ✅ R5 本機 hook(擋 CC 直接造零件目錄)仍要 —— 它擋的是「繞過 PR 直接改 repo」,與 PR 管道互補。 + +**以下 §0-§9 為原 design,閱讀時套用上述修正。** --- @@ -139,12 +167,27 @@ interface SubmitRequest { - cold_start / runtime_compat 保留 mock,但 **SandboxResult 增 `unimplemented_steps: string[]`**,回傳時明列 `["cold_start","runtime_compat"]`,submit 回應與文件明示「這兩步未實作、未真正驗證」。不回 `success:true` 假裝全綠——回 success 但附 unimplemented 清單。 +## 5.5 黃金向量:人工核對 + B 覆蓋檢查(richblack 2026-05-30 定) + +Claude.ai 建議用「黃金向量 + 把關自己重跑」自動驗收,防放水的 Gherkin。**價值保留,實作降級**,理由: + +- **primitive 極少、未來絕大部分是 recipe**(人類閘門 + 工作流優先把零件擋在源頭)。現役 17 白名單 + cron/platform_crypto,未來極少新增。 +- 「把關自動重跑向量」是為「零件大量增加」做的規模化基建——但零件不會大量增加 → 為不存在的規模做自動化 = 過度工程(DECISIONS 附錄「會不會累積成債」判準)。 +- 且「把關自己重跑 wasm」撞 venue 牆(CF 不能 runtime 編譯 wasm,同 §4.0)→ 需平台端 sandbox(第一期沒有)。 + +**降級後做法:** +- **A 黃金向量 conformance → 人工核對**:黃金向量當「人類閘門時用 CC 核對 primitive 的對照表」。新增 primitive(極稀有)時,人類閘門已要你親自確認,那一刻用 CC 本地跑向量核對(本地有 tinygo + 能跑 wasm,繞開 venue 牆)、人工凍結。**不做機器自動重跑。** +- **B 覆蓋檢查 hook → 現在做(純靜態、不可造假、成本低、價值與零件數無關)**:靜態 parse contract:`input_schema` 每個 required 欄位至少出現在一個 Gherkin given、`output_schema` 每個欄位至少被一個 then_contains 斷言。缺 → exit 2 指出漏哪個欄位。擋「只測 happy path、不碰宣告過的行為面」。 +- **初始向量來源(信任根:寫向量≠寫實作)**:另起 session 從 contract 語義寫、不看實作原碼(primitive 語義客觀如 add(2,2)=4)。人工核對用,不急、新增 primitive 時逐個補,不必 21 個一次到位。 +- 殘留(誠實):向量/覆蓋檢查擋不住「列出的 case 全對、沒列到的 case 錯」。交給「用」——出 bug 補進向量,永不 regress。是會長大的網,非設一次完美。 + ## 6. R5 白名單 + 本機 hook - `registry/MVP_COMPONENTS.txt`:一行一個白名單 canonical_id(現役 22 個)。 - `pre-write-guard.sh` 增規則:寫入 `registry/components/{name}/...` 且 `{name}` 不在 MVP_COMPONENTS.txt → exit 2,訊息「新增零件需走 submit API 人類閘門,不可直接造 repo 目錄」。 - `pre-bash-guard.sh` 增規則:`mkdir .../registry/components/{白名單外}` → exit 2。 - `.ts` 偵測現有 hook 已做(rule 1.1)。 +- **B 覆蓋檢查**(5.5):可放 registry submit 的靜態驗收(不需跑 wasm,CF 可跑)或 pre-write hook,擋宣告過的欄位沒被 Gherkin 測到。 ## 7. 範圍邊界 From 7d8cbe4299ef23a3969fe796d7c0b4655066daf9 Mon Sep 17 00:00:00 2001 From: richblack Date: Sat, 30 May 2026 13:42:46 +0800 Subject: [PATCH 4/4] =?UTF-8?q?docs:=20Phase=203=20=E6=94=B6=E5=B0=BE=20?= =?UTF-8?q?=E2=80=94=20=C2=A78=20=E9=87=90=E6=B8=85=E9=9B=B6=E4=BB=B6?= =?UTF-8?q?=E6=8A=95=E7=A8=BF=E8=B5=B0=20PR=E3=80=81BACKLOG=20=E8=A8=98?= =?UTF-8?q?=E6=96=B0=E9=9C=80=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DECISIONS §8 釐清:執行鏈路(高頻)不依賴 CI;零件投稿(稀有)走 PR/CI 是例外、不違反精神 (CF Workers 不能 runtime 編譯 wasm,CI 是唯一能跑 wasm 又執行者碰不到的 venue) - BACKLOG 步驟 4 收尾(投稿改 PR,標已做/不做/未來搬 CI) - BACKLOG 步驟 5 被 PR 方向取代;新增步驟 5b 資料外流警示(先做,需新 SDD) - BACKLOG 待決策加:用戶 API 保護機制(入站認證,資安優勢)、recipe/part/function 架構釐清 - component-gatekeeping tasks.md 收尾狀態 Co-Authored-By: Claude Opus 4.8 --- .agents/specs/component-gatekeeping/tasks.md | 16 +++++ BACKLOG.md | 69 ++++++++++++-------- DECISIONS.md | 17 +++-- 3 files changed, 69 insertions(+), 33 deletions(-) diff --git a/.agents/specs/component-gatekeeping/tasks.md b/.agents/specs/component-gatekeeping/tasks.md index 51c4d28..49f9b92 100644 --- a/.agents/specs/component-gatekeeping/tasks.md +++ b/.agents/specs/component-gatekeeping/tasks.md @@ -5,6 +5,22 @@ --- +## ⚠️ 收尾狀態(2026-05-30,方向修正後) + +投稿改走 **GitHub PR**(廢 registry self-service,見 design 頂部「方向修正」)。本 SDD 收尾於: +- **已完成且 commit**:G1(detectFakeComponent)、G3(wasmImports)、G5/G6(unimplemented_steps)、 + G0 registry 人類閘門(保留不刪)。測試 15 綠。 +- **改去向**:G4 Gherkin / 覆蓋檢查 → 未來接 CI PR check(CI 能跑 wasm)。G1/G3 邏輯可複用。 +- **不做**:CI PR check(richblack:人工 review 就夠,primitive 極少)、R5 本機 hook + (PR/merge + G1 + 沙箱已防「未經同意變公共零件」,hook 過度工程)、registry self-service、 + acr parts publish 加閘門、平台 sandbox 重跑。 +- **黃金向量**:人工核對(另起 session 從語義寫),不急、不機器自動化。 +- **轉出範圍**:真正的裸奔風險在「資料外流」(recipe/webhook 把資料送出去),不分公私庫 + → 另開新 SDD「資料外流警示」(API 層警示 + AI 動手前 hook)。用戶 API 保護(入站認證) + + recipe/part/function 架構釐清 → 記 BACKLOG 待決策。 + +--- + ## G1 假零件偵測(R2,Q2=兩者硬擋) - [x] 1.1 `registry/src/actions/detectFakeComponent.ts`:(a) 外部 URL/domain 偵測(掃 contract 文字 + wasm binary 文字)硬擋;(b) http_request 子集偵測硬擋 - [x] 1.2 排除 `auth_*` primitive 與 `http_request` 自己 diff --git a/BACKLOG.md b/BACKLOG.md index e51a350..9fcc98e 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -57,40 +57,41 @@ - [~] 降級後 registry/components/ 現 22 個:17 白名單 primitive(流程6+資料7+http_request1+auth3) + cron/platform_crypto(引擎能力)+ claude_api/km_writer/kbdb_upsert_block(deferred) -### 步驟 4 — 補零件庫真把關(原步驟 3,第一期最大工程) -- [ ] `sandboxAcceptance.ts` 的 Gherkin 驗收:目前是 `return null` 空殼,要真實作 -- [ ] 新增「假零件偵測」:primitive 子集偵測 + 寫死 endpoint 偵測 → 自動退稿並告知正路 -- [ ] 把關目標擴充:不只「安全 + Gherkin」,還要「純 WASI preview1、零宿主特定依賴」 - (這是避免「runtime 鎖定債」的唯一時機,見 DECISIONS §4) -- [ ] 冷啟動 / runtime 相容兩步可暫留 mock,但要明確標「未實作」,不可假裝通過 +### 步驟 4 — 補零件庫真把關(2026-05-30 收尾:投稿改走 PR) -### 步驟 5 — 建零件的人類閘門 + 白名單 hook(原步驟 4) +> 方向修正:零件投稿走 **GitHub PR**(人 merge=閘門、CI 跑把關),廢 registry self-service。 +> SDD:`.agents/specs/component-gatekeeping/`(含完整決策過程)。 -> 強化規格(richblack 2026-05-29 定,原因見下):**建零件不是 AI 能自己決定的事**。 -> 風險:自用服務(通訊錄/帳本)連驗證都沒設好就變成零件進公共庫 → 全 arcrun 生態都能打 → 機密外洩。 -> 這種安全/意圖判斷機器做不了,必須人看。規範會被忘記,hook 不會(DECISIONS §7:把判準寫成機械紅燈)。 +- [x] 假零件偵測(detectFakeComponent.ts):外部 URL/domain + http_request 子集,硬擋退稿指回 recipe +- [x] 純 WASI 把關(wasmImports.ts):import module 白名單(只准 wasi_snapshot_preview1 + u6u) +- [x] cold_start / runtime_compat / gherkin 標 unimplemented_steps(不假綠) +- [x] G0 registry 人類閘門(已 commit;投稿改 PR 後非主管道,保留不刪) +- [ ] Gherkin 真跑 + 覆蓋檢查 → 未來接 **CI PR check**(CI 能跑 wasm,繞 CF venue 牆)。 + richblack:人工 review 就夠,primitive 極少 → CI 暫不做,邏輯已寫好隨時能搬。 +- [x] **不做 R5 本機 hook**:PR/merge + 假零件偵測 + 純WASI 沙箱已防「未經同意變公共零件」→ hook 過度工程。 +- [ ] 黃金向量:人工核對(另起 session 從語義寫,不機器自動化)— 不急 -**把關點 = 「建立零件的 API」本身,不是「寫檔案」。** 零件建立有四條入口(CLI `acr` / MCP / Python lib / JS lib), -它們不靠本機寫檔,是呼叫 registry Worker 的「投稿/建立零件」endpoint。所有路都要收斂到這一關。 +### 步驟 5 — ~~建零件的人類閘門 + 白名單 hook~~(2026-05-30 被 PR 方向取代) -- [ ] **主閘門:registry Worker「建立零件」API** —— 預設**拒絕**,除非請求帶: - (a) 人類已確認的明示憑證(CLI `acr component create` 強制互動式問人類,AI 在非互動環境生不出); - (b) 舉證欄位 `reason_why_not_workflow`(AI 必須說明「為何這件事無法用工作流達成」,舉證責任在 AI, - 預設假設工作流能做)。沒帶 → 403 + 指回正路(先試工作流)。 -- [ ] **四路 client 配合**:CLI / MCP / Python lib / JS lib 的「建零件」都改成必經主閘門(帶人類確認 + 舉證) -- [ ] **本機 hook 兜底**:pre-write / pre-bash hook 偵測「CC 直接在 `registry/components/{白名單外新名}/` 造目錄 - 或 mkdir」→ exit 2(擋繞過 API、直接改 repo 造零件等 CI 部署這條旁門) -- [ ] **誠實限制(要寫進 mindset)**:沒有技術能 100% 防 AI 假冒人類確認(它能塞任何 flag)。 - 靠「閘門 + 舉證留記錄 + mindset 明示『絕不可代替人類確認建零件』+ 軌跡可審計」讓假冒成為明確越界, - 不是聲稱「不可能繞過」。 -- [ ] `MVP_COMPONENTS.txt` 白名單檔 -- [ ] pre-write hook:`registry/components/` 目錄清單 ≠ 白名單 → exit 2 -- [ ] pre-write hook:`registry/components/` 底下出現 `.ts` → exit 2 +> **取代說明**:原規劃「registry submit API 人類閘門 + 四路 client + 本機 hook」。 +> 2026-05-30 改:**零件投稿走 GitHub PR**(人 merge = 天然人類閘門,AI 偽造不了 GitHub approve)。 +> → registry self-service 主閘門、四路 client、本機 hook **都不做**(PR/merge + 假零件偵測 + +> 純WASI 沙箱已防「未經同意變公共零件」,hook 過度工程)。並進步驟 4。 +> +> **真正的裸奔風險不在零件,在「資料外流」**(recipe/webhook 把資料送出去,不分公私庫)。 +> → 另開新 SDD「資料外流警示」(見下方第一期新增項)。 -> **配套(讓 AI 不選難路,ABC 三管齊下,與本閘門合為四道)**: -> A 審核當場擋零件(步驟 4 假零件偵測,§7 層二)+ B 工作流範本好寫(步驟 7 acr new,§7 層一) -> + C mindset 明示「工作流是 default,零件是稀有例外」(步驟 7 mindset Skill,§7 層三)。 -> 原理:難路走的當下要痛、易路選的當下要爽、事先有聲音說易路是 default。CC 把自用服務錯做成零件,正因這三者當時全缺。 +**ABC 配套仍有效**(讓 AI 不選難路):A 假零件偵測(步驟 4)+ B 工作流範本(步驟 7 acr new) ++ C mindset「工作流是 default、零件稀有例外」(步驟 7 mindset Skill)。 + +### 步驟 5b — 資料外流警示(2026-05-30 新增,richblack:先做) + +> 風險根源:arcrun 讓「產生 API」變很簡單(資料+webhook trigger=API)→ AI 可能不知不覺把含個資 +> 的東西變成可被呼叫的 endpoint。不分公私庫(私人=公司用也會把個資 POST 到公司群)。 + +- [ ] 新建 SDD(requirements/design 交 richblack review 才動 code) +- [ ] **API 層警示**:任何「把資料送出去」的動作(recipe push / webhook 等)不論哪條路都警示/需人類同意 +- [ ] **hook**:AI 動手做這類動作前先警告(防在前;API 層防在送出前,兩道互補) ### 步驟 6 — 搬家(拆 matrix)(原步驟 5) - [ ] 先給 kbdb、ghost(identity/personality-system/persona-sdk) 各自 `git init` + 建 GitHub repo @@ -112,6 +113,16 @@ ## 第一期之後 / 待決策(不要現在做) +- [ ] **用戶自己的 API 保護機制(入站認證)**(richblack 2026-05-30)— 做成零件或功能: + (1) 用戶可發 API Key 給別人;(2) 不同權限設定。現況缺口:webhook 只有 X-Arcrun-API-Key + (誰有我的 key 誰能打),沒有「發受限 key 給別人 / per-caller 認證 / rate limit」。 + **這是 arcrun 資安優勢**:n8n 用簡單 USN/PWD 做不好,AI coding 又怕資安沒做好,我們系統幫搞定、 + 一個動作能用。注意區分:credential/auth recipe 是出站(呼叫別人 API 帶我的 token),這是入站(保護我的 API)。 +- [ ] **架構詞彙釐清(recipe / part / function / 工作流組合)**(richblack 2026-05-30,要思考一下): + - recipe:用零件去打的設置文件(endpoint+設定) + - part(零件):需 PR + update/upgrade + - function:把一批功能做好可用 cypher 拉進來(例:API 認證,我做好幾個讓用戶拉)。是 part+recipe? + - **多零件/工作流組合成新工作流**(第一個走完連第二個)—「好像還沒有」,要思考 - [ ] **砦 `injectCredentials` 舊路 + `BUILTIN_CREDENTIALS_MAP`** — credential 系統現為新舊兩路並存, 舊路是 TS 裡解密的半成品(註解自認 Phase 1.9 刪)。是獨立清理,**不擋降級**。見 DECISIONS §3b - [ ] **決策:開源版 cypher-executor 是否保留「KBDB block 展開」功能** diff --git a/DECISIONS.md b/DECISIONS.md index ccf6f0b..e9833e2 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -313,11 +313,20 @@ arcrun-gui 拖拉畫布、arcrun-mcp 命名大改、新 primitive、Gemini/Codex --- -## 8. 不依賴 GitHub Actions +## 8. 執行鏈路不依賴 GitHub Actions(零件投稿例外,2026-05-30 釐清) -Arcrun 第一期的執行鏈路(init / push / run / 零件投稿)全在「用戶機器 + Cloudflare」之間, -不經過 GitHub Actions。零件編譯 / 測試本地跑即可(TinyGo 編譯、Gherkin 測試都是本地一行指令)。 -理由見 §0 第一條。 +Arcrun 第一期的**執行鏈路**(init / push / run / recipe)全在「用戶機器 + Cloudflare」之間, +不經過 GitHub Actions。這是常態高頻動作,用戶不該被 CI 卡住。理由見 §0 第一條。 + +**零件投稿是例外,走 GitHub PR + CI(2026-05-30):** +- 零件投稿是**稀有低頻**事件(primitive 極少、未來絕大部分是 recipe;建零件要過人類閘門)。 +- 稀有事件用 PR 治理最自然:**PR 必須有人 merge = 人類閘門**(AI 偽造不了 GitHub approve); + 把關(假零件偵測 / 純WASI / Gherkin / 覆蓋檢查)由 **CI(PR check)跑**。 +- 為何非 CI 不可:**CF Workers 禁止 request-time 編譯 WASM** → registry Worker 跑不了 Gherkin / 向量; + CI 有 tinygo + 能 runtime 跑 wasm,是唯一既能跑 wasm 又「執行者碰不到」的 venue。 +- **這不違反 §8 精神**:§8 防的是「高頻執行鏈路被 CI 卡住」;零件投稿稀有、且該由 PR 把關, + 用 PR/CI 反而更對。兩者是不同性質的事。 +- 詳見 `.agents/specs/component-gatekeeping/`。 ---