Files
Arcrun/.claude/rules/07-thin-shell.md
T
uncle6me-web fad5da0e17 rules/07 §3.6:自舉例外——能力只實作一次,但不一定要是 HTTP API
代補 PR #111 的 agent 想加但改不動的段落(該檔受保護)。

立這條的原因不是理論:#97 的修法一開始寫在 cli/src/lib/resource-resolver.ts
(能力住在介面層,違反 §0)⇒ 安裝器拿不到它 ⇒ 同一個 bug 只修了一半,
走 acr 的人有保護、走 install.arcrun.dev 的人沒有——而所有真實用戶走後者。

leo 2026-08-12:「根本就不應該在 CLI,我要的是一個大家都可以用到的規則。」

記三件,都是為了不讓下一個人「修正」回去:
① 為什麼不放 cypher API(自舉/輸入是使用者自己的帳號狀態/它是純函式)
② §0 的正確讀法是「只准有一份、不准住在單一介面裡」,放 API 只是常見手段
③ npm 打包例外:cli/ 下的逐位元組副本由 sync --check 機械擋漂移,不是第二份實作

📍 repo:matrix/arcrun(shared/resource-rule/、cli/、.claude/rules/07)
   + products/arcrun-rag(安裝器待接上,已派工)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 23:50:53 +08:00

12 KiB
Raw Blame History

薄殼原則(鐵律)— 能力長在 API,介面只暴露

來源:docs/壓測報告.md §5.4/§5.5(設計者本人於壓測中釐清)+ DECISIONS §1。 違反此原則的典型後果:每改一個能力要同步多份介面、介面間漂移(壓測 §5.1:CLI 改了讀 全域/專案/.env,MCP 沒跟上,兩者打不同帳號)、能力被某個介面綁架後別的介面用不到。 這條由 .claude/hooks/pre-write-guard.sh 規則 7.x 部分強制(見下「hook 強制範圍」)。


0. 一句話

所有能力(business logic)只實作一次,放在 APIcypher-executor HTTP 端點)。 CLI / MCP / Python lib / JS lib 全是薄殼:只做「介面轉換 + 暴露」,不含任何商業邏輯。

   CLI ─┐
   MCP ─┤   ← 全是薄殼:參數解析 / 格式轉換 / 暴露,不含商業邏輯
   Python lib ─┤
   JS lib ─┘
        ↓ 全部呼叫同一個
   ┌──────────────────────────────┐
   │  API(唯一真相,能力都在這)        │
   └──────────────────────────────┘

1. 什麼是「能力下沉到 API」(正例 vs 反例)

正例:upsert

  • API 提供 upsert 端點(內部 GET 找→有則 update 無則 insert)。CLI/MCP/lib 只呼叫它。
  • 在 MCP 裡自製「先 call update API、失敗再 call insert API」的拼裝邏輯。
  • 在 recipe 層拼湊 upsertrecipe/零件補 API 缺的能力 = 走歪;正解是補在 API)。

正例:seed recipe(壓測 §4.1 的反例修正)

  • API 在「部署/註冊完成」時保證 recipe 就緒(seed 是 API 行為,由一個端點完成 POST /init/seed)。
  • 種子資料(清單)放 servercypher-executor/src/lib/*-seeds.ts。「裝好後預設有哪些 recipe」 是 API 的能力,種子資料是這能力的一部分。薄殼只呼叫 /init/seed 一次。
  • 在 CLI init.ts 裡用迴圈 POST 11 個 recipe + 客戶端「全部成功才 seed」的 if 判斷 (這正是 §4.1 seed 永遠不被 seed 的根因:邏輯被寫進了某個介面)。

種子資料檔(*-seeds.ts)是一個普遍類別,不是某個零件的特例。 它含 endpoint / {{template}} 字串(recipe 的資料欄位),rule 02 §2.2 hook 對整類 *-seeds.ts 豁免 endpoint/template 檢查——因為那是「資料宣告」不是「呼叫實作」。新增任何 xxx-seeds.ts 自動適用,不需為個別零件/recipe 改 hook(richblack 原則:不為單一零件改全域規則)。

判準口訣

「這段邏輯換一個介面(CLI→MCP)要不要重寫?」 要重寫 → 它是能力,該在 API。 不用重寫(只是把 API 回傳值換個格式印出來)→ 它是薄殼該做的事。


2. 薄殼「允許」做的事(窮舉)

  1. 解析介面慣例的輸入(CLI 吃檔案路徑 / MCP 吃 JSON 參數)→ 轉成 API 期望的 payload。
  2. 呼叫 APIHTTP fetch / service binding)。
  3. 把 API 回傳值轉成該介面的輸出格式(CLI 印彩色文字 / MCP 回 structured JSON)。
  4. client 端加密(AES-GCM)——唯一例外,因 API 期望收到已加密 payload(見 rule 01 加解密)。
  5. 讀取「身份設定」(哪個帳號 / 哪個 cypher URL)——但所有薄殼必須讀同一份身份來源 (見 §4 統一帳號來源)。

3. 薄殼「禁止」做的事

  1. 在薄殼介面(CLI/MCP/lib)裡用多個 API 呼叫拼裝出一個 API 沒有的能力(upsert / seed / 任何 N-step 編排)。

    界線(2026-06-26 收窄,issue #4:禁的是「把編排邏輯寫進介面層 TS」,不是禁「用資料方式(workflow/code-node)自救」。 自家 API 缺能力 → 補進 API(你能改);第三方 API 缺能力 → 走 workflow/code-node 補丁是合法的(你改不了第三方 API,不能被規則卡死)。詳見 §3.5 自力救濟階梯。

  2. 用零件補 API 缺的能力 → 污染零件庫(缺能力 → 先看 §3.5 階梯,真需新穩定能力才走零件 PR)。

    這條的原始精神(要保留):當初是 AI 把多步驟工作寫成零件污染零件庫,才訂此禁令。禁的是「亂建零件」,不是「禁止任何補丁」。

  3. 寫死判斷來補 API 缺口(例:deployFullyOk 那種 client 端 gate)。
  4. 同一 API 能力在不同介面用不同參數簽名(validate 在 CLI 吃 YAML、在 MCP 卻要 api_key+graph = 底層分歧,違反「同一 API」)。差異只能來自介面慣例(檔案路徑 vs 字串),不能來自底層實作。
  5. 任一薄殼連的帳號 / 後端與別的薄殼不同(CLI 連自架、MCP 連平台 = 違反「同一 API」前提)。

3.5 缺能力時怎麼補:自力救濟階梯(普世規則,issue #4)

問題:§3 舊版預設「缺能力 → 去補 API」,這預設 API 是你能改的。對第三方 API(如 Google Sheets:一次只能倒全部、輸出前無法 filter)不成立——若 Google 不開該 API、規則又禁用 workflow「倒出來自己篩」,用戶被自己的規則卡死。 解法:把「補丁」分層,維持零件庫最小,但開放「用資料方式自救」的合法路徑。

主界線(一句話)「那個 API 你能不能改?」 自家 API 缺能力 → 補 API;第三方 API 缺能力 → workflow/code-node 補丁。

情況 正解 為何
能打既有 API recipe(沒有就建 recipe 單一 API 呼叫的封裝
自家 APIKBDB / cypher)缺能力 補進 API + 可同時發 issue 你能改,能力該長在 API
第三方 API 缺能力(gsheets filter / 無 upsert API 可投稿的 workflow 補丁 + 發 issue 建議原廠加 API 你改不了第三方 API,但不能被卡死
非 call-api 的純計算(如整篇文章轉大寫) code-node(空白 code 零件內寫 JS recipe/workflow 都做不到,又不該為此建一堆專用零件
真需新穩定能力(極少數) 自建零件 → PR 維持零件庫最小,只有非用零件不可才建

三個配套原則

  1. 補丁 workflow 可像 recipe 一樣被呼叫,但明示它是 workflow、且可投稿(呼應 wishlist C6「工作流即零件」)。讓 AI 一遇阻就「用資料方式」自救,而非建零件。
  2. code-node:原廠不提供某純計算時,AI 至少能用一個空白 code 零件寫 JS 自救(呼應 wishlist C1;架構決策:JS 在 CF Workers isolate 跑,不嵌 QuickJS/Rust)。
  3. 補丁是過渡:原廠出 API 後,補丁 workflow 因效能較差自然被減少使用、淘汰。

upsert 範例:有些服務原廠提供 upsert API(→ recipe 直接打),有些沒有(→ 做一個 upsert workflow 達成,而非建專用零件)。§1 把 upsert 當「該補進 API」的正例——那只對自家 API 成立;對改不了的第三方 API,arcrun 端永遠補不進去,正解是 workflow 補丁。

與 hook 的關係:§3.1 禁的「介面層拼裝」由 pre-write-guard.sh 7.x 擋(範圍 cli/src/arcrun-mcp/src/ 的 TS)。workflow/code-node 補丁是資料產物(YAML / 空白零件內的 JS),不是介面層 TS → 本就在 hook 範圍外,合法不被擋。 hook 防線不變,本次只釐清「資料方式自救」是合法路徑。


3.6 自舉例外:能力該「只實作一次」,但不一定要是 HTTP API2026-08-12 立)

立這條的原因:Arcrun#97(更新把使用者的工作流與登入弄不見)的修法一開始寫在 cli/src/lib/resource-resolver.ts ——能力住在介面層,違反 §0。 後果不是理論:安裝器(arcrun-rag)拿不到它,於是同一個 bug 只修了一半acr 的人有保護、走 install.arcrun.dev 的人沒有——而所有真實用戶走後者。 leo 2026-08-12:「根本就不應該在 CLI,我要的是一個大家都可以用到的規則。

修法(PR #111)把它搬到 shared/resource-rule/:一份零依賴 ESM acr 與安裝器共用。它刻意不是 cypher 的 API 端點,三個理由:

為什麼不放 API 說明
自舉 這條規則要在「決定怎麼裝」的當下用得到,而安裝器的工作正是把 cypher 生出來。放進 cypher = 要先有雞才能有蛋。
輸入是使用者自己的帳號狀態 判斷依據是使用者 CF 帳號上的綁定。送去平台託管的 worker 換答案 ⇒ ①「能不能安裝」綁在平台是否活著 ②使用者的帳號拓撲交給第三方。
它根本不需要是服務 這是純函式,唯一的 IO 由呼叫端注入。§0 要求「能力只實作一次」,不是「能力一定要是 HTTP」。

🔴 所以本檔 §0 的正確讀法是:能力只准有一份,且不准住在任何單一介面裡。 「放 API」是達成它的常見手段,不是唯一手段。 判準仍然是那句口訣:「這段邏輯換一個介面要不要重寫?」要 → 它是能力。

📌 給下一個人:看到 shared/ 底下的純函式不要「修正」成 API 端點—— 先讀 shared/resource-rule/README.md §2,那裡記著評估過並否決的其他形態 (共用 npm 套件=自舉問題換位置;做成零件=要用 TinyGo 重寫一次,那才是第二份實作)。

📌 打包例外acr 是獨立 npm 套件,npm pack 打不進套件目錄外的檔案 ⇒ cli/ 下必須有一份逐位元組副本。那不是第二份實作—— scripts/sync-resource-rule.mjs --check 一有漂移就 exit 1,且 buildtest 都會先跑它 (同 cli/harness/ 的既有慣例)。手改副本 build 紅 publish 擋下。


4. 統一帳號來源(薄殼共用同一身份)

所有薄殼讀同一份身份設定:

  • self-hosted~/.arcrun/config.yaml / 專案層 .arcrun.yaml / ARCRUN_*CLOUDFLARE_* env(見 config-layering.md)。
  • standard:平台 api_key。

MCP 目前的已知違反(壓測 §5.2):MCP 用 Cloudflare service binding 焊死平台 arcrun-cypher-executor self-hosted 用戶用 MCP 連不到自己的 cypher。修法見 docs/3-specs/arcrun/sdk-and-website/mcp-account-source.mdSDD proposal)。


5. 出貨順序(最低出貨標)

  • CLI + MCP 兩個薄殼先到位(AI 偏好 MCP,故 MCP 不可長期落後),且兩者覆蓋同一組 API 能力
  • Python / JS lib 隨後補。
  • 出貨順序由「介面被誰用」決定,不是由「哪個好做」決定。
  • 介面進度本來就會不一致(薄殼模型的預期狀態)——這本身不是 bug。 bug 是「底層 API 能力不齊 / 介面含了不該含的邏輯 / 帳號來源不統一」這三者。

6. hook 強制範圍(與「靠人判斷」的邊界)

pre-write-guard.sh 規則 7.x 能擋的是語法層可偵測的反例:

  • CLI/MCP 檔案內出現「迴圈 POST 多個 recipe」「先 update 失敗再 insert」這類拼裝 pattern 的明顯特徵 → 警告/擋。
  • 新增 seedApiRecipes / seedAuthRecipes 這類「seed 邏輯寫在介面層」的函式 → 擋(改去 API)。

hook 擋不了的(需 CC 自律 + code review):

  • 把商業邏輯藏在看似無害的 helper 裡。
  • recipe 層拼裝(recipe 是資料,hook 不解析語意)。 → 故本檔是 mindset,hook 是底線;兩者都不可省。誠實限制見 mindset §7(不假裝 hook「不可能繞過」)。