Files
Arcrun/system-dev/docs/2-architecture/07-thin-shell.md
uncle6me-web 5d00e71275 chore: D22 落地——docs/SDD/wiki/CLAUDE.md 進 repo(Gitea private 預設全 push)
頂層 D22 決策(leo 2026-07-03 拍板):推什麼由開發環境歸屬決定,
Gitea private=除機敏值/build 產物/.github 外全 push。
解 T1.5 卡點:雲端工人 clone 拿得到 credential-store-migration.md,可就地改寫 SDD。
機敏掃描兩輪通過(新增 189 檔約 2.1MB,node_modules/dist/wasm 照舊排除)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:13:33 +08:00

134 lines
9.2 KiB
Markdown
Raw Permalink 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.
# 薄殼原則(鐵律)— 能力長在 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`)。
-**種子資料(清單)放 server**`cypher-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 防線不變,本次只釐清「資料方式自救」是合法路徑。
---
## 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.md`SDD 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「不可能繞過」)。