b223a69884
leo 2026-08-12 實撞:藏書地圖回 0 個庫,同一分鐘 KBDB 裡有 1854 條三元組,
`arcrun_whoami` 顯示 admin/全部知識庫、`kbdb_search` 也查得到——只有地圖那格是空的。
病根(不是資料掉了,是讀寫兩端各拿一個來源):
寫入端 owner_id = `~/.arcrun/config.yaml` 的 `api_key`(CLI push/小幫手上傳/MCP,
leo = `bfezv28v`)
讀取端過濾 = `portalTenant(env) = env.CONSOLE_TENANT || "leo"`
——repo toml 帶的**官方 prod 值**,而 `acr` 從來不注入 CONSOLE_TENANT
⇒ 那個 `"leo"` 不是理論邊角,是每台 self-hosted 實例的實際行為,1854 條全被濾掉。
與 #105(`env.MCP_OWNER_NAMESPACE || "leo"`)同一句話,換一個檔案。
租戶字串該從哪裡來(本票的核心判斷):
**從「寫入這批知識的那一方」來,不是從一份手抄的環境變數預設值來。**
不是「掛到每個帳號上」——portal 帳號共用同一台實例的知識庫(design D-2),
帳號之間的差別是 libraries 權限不是 owner_id;複製一份到帳號上只是多一個會過期的副本。
#105 真正的教訓是:過濾用的租戶字串要有單一權威來源、解析不到要誠實失敗、且要能機械驗證。
修法:
1. 唯一產地 `cypher-executor/src/lib/tenant.ts`
- `knowledgeOwner(env)` → branded `TenantId`:`ARCRUN_NAMESPACE` → `CONSOLE_TENANT` →
丟 `TenantUnresolvedError`。**沒有字面預設值**——`|| 'leo'` 正是把「這台機器沒設定」
偽裝成「你沒有資料」的元凶。
- `accountTenant(env)` → 普通 `string`(帳號子 namespace `{tenant}::portal` 與 cypher
自己寫的設定用它)。**回 string 是刻意的**:型別上就不可能流進知識資料面。
- 資料面過濾一律經 `ownerQuery()` / `ownerField()`,只吃 `TenantId`。
2. 值的正解由 CLI 從真相源導出:`acr update` 把 config 的 `api_key` 注入成 `ARCRUN_NAMESPACE`,
但**先驗再寫**(`GET /kbdb/map?owner_id=<api_key>` 查得到庫才寫;查不到/問不到就一個字
都不動)。無條件覆蓋會把「知識本來就在 CONSOLE_TENANT 底下」的一鍵安裝實例指向空的那一格
——那是 #97/#106 那類「更新一次把人家的東西弄不見」,比原本的 bug 更糟。
未注入時回退 CONSOLE_TENANT ⇒ 對官方 prod 與未更新的實例,這次改動是惰性的。
3. 空地圖分四態(沿 #100「讀不到就說讀不到」):no_library_grant/filtered_out/
scope_mismatch/confirmed_empty。scope_mismatch 以前不存在,所以設定錯誤被畫成
「你沒有資料」。回應仍不含租戶字串(design §3.3 紅線)。
4. 同族一起修(同一道閘一次抓到):console-dashboard 4 處、console-auth 1 處
——console 首頁的規模數字與藏書地圖對 leo 也一直是空的。
留下的閘(規則存在但沒機制驗證=會再犯第三次):
· 型別閘:TenantId 只能由 tenant.ts 產出 → 拿隨手一個 string 去過濾,tsc 當場不給過。
· 出貨閘:scripts/build-worker-artifacts.mjs 編 tier2 成品前先掃,違規 → 編不出成品。
· 閘自己可測:規則是純函式(tenant-source-rules.mjs),tests/tenant-gate.test.ts
逐條驗「5 種壞例子會擋」+「11 種合法寫法零誤攔」;掃描範圍只有 src/,擋不到自己。
規範寫入 .claude/rules/02-forbidden.md 第六類、system-dev/wiki/mistakes.md #26。
沒動:庫權限過濾(一字未改,回歸測試釘住)、帳號資料落點、任何金鑰、租戶字串仍不下發前端。
驗證:
cypher vitest 441 綠 / 14 紅,14 紅與 base commit e05518a 逐字相同(既有)
tsc 5 個既有錯誤,零新增
cli node:test 60/60 綠(含本次新增 12 條);tsc 零錯誤
閘 壞例子實跑 exit 1;build 實跑「建置中止」;乾淨時實跑通過
端到端 ◐ 未驗:需部署到 leo21c,那道閘要 leo 親手解(見 PR ③)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
189 lines
10 KiB
Markdown
189 lines
10 KiB
Markdown
# 禁止行為清單(零容忍)
|
||
|
||
**這份清單由 `.claude/hooks/*.sh` 強制執行。違反會 block 工具呼叫(exit 2)**。
|
||
|
||
---
|
||
|
||
## 第一類:零件實作層級的禁令
|
||
|
||
### 1.1 禁止在 `registry/components/` 下建立 TypeScript 檔案
|
||
零件**只能**用 TinyGo(`.go`)或 AssemblyScript(`.ts` 但需 `asconfig.json`)實作,並編譯成 `.wasm`。
|
||
cypher-executor/registry Worker 或 `.component-builds/` 內的 TS 不算零件邏輯,那是 WASI shim。
|
||
|
||
**Hook 會擋**:新增 `registry/components/*/{檔案}.ts`(除非目錄內有 `asconfig.json` 明確標記為 AssemblyScript)。
|
||
|
||
### 1.2 禁止建立新的 `auth_*` 目錄以外的 auth 實作
|
||
所有 auth 邏輯只能在:
|
||
- `registry/components/auth_static_key/`
|
||
- `registry/components/auth_oauth2/`
|
||
- `registry/components/auth_service_account/`
|
||
- `registry/components/auth_mtls/`
|
||
|
||
**不可以**出現 `cypher-executor/src/auth-primitive/`、`cypher-executor/src/lib/auth-*.ts`、`auth-worker/`、`credential-worker/` 等目錄。
|
||
|
||
**Hook 會擋**:`mkdir` 或 `Write` 到上述違規路徑。
|
||
|
||
### 1.3 禁止用 `wrangler init/generate` 建立 auth/credential/jwt 相關的 TS Worker
|
||
Auth primitive 必須透過 `component-worker-template/` 搭配 WASM binary 部署。
|
||
|
||
**Hook 會擋**:bash 指令含 `wrangler (init|generate) ... auth_`、`... credential_`、`... jwt_` 的 pattern。
|
||
|
||
---
|
||
|
||
## 第二類:cypher-executor TS 的禁令
|
||
|
||
### 2.1 禁止新增任何 credential / auth / jwt 相關的 TS 檔案
|
||
**清除已完成**(2026-07-20,credential-primitives-wasm 卷已封存)。下列曾違規的 TS **均已不存在**,
|
||
列此僅為「禁止重新引入」的清單:
|
||
- ~~`cypher-executor/src/actions/credential-injector.ts`~~ → 已刪(auth 走 WASM primitive)
|
||
- ~~`cypher-executor/src/lib/jwt-signer.ts`~~ → 已刪(RS256 在 auth_service_account WASM)
|
||
- ~~`component-loader.ts` 的 `BUILTIN_API_RECIPES` / `BUILTIN_CREDENTIALS_MAP`~~ → 已整段刪
|
||
|
||
**重新建立上述任一者 = 違規**。
|
||
|
||
**Hook 會擋**:新增任何路徑含以下關鍵字的 `.ts` 檔案:
|
||
- `credential-injector`、`credential_injector`
|
||
- `jwt-signer`、`jwt_signer`
|
||
- `auth-dispatcher` 的 TS 若嘗試在裡面實作 credential 解密 / template 展開 / JWT signing,block
|
||
|
||
### 2.2 禁止在 cypher-executor 任何 TS 裡實作以下邏輯
|
||
這些邏輯全部屬於 WASM 零件職責:
|
||
|
||
- AES-GCM 解密(`crypto.subtle.decrypt`)— 只准出現在 `wasi-shim.ts` 的 `crypto_decrypt` host function
|
||
- RSA-SHA256 簽章(`crypto.subtle.sign` with RSASSA-PKCS1-v1_5)— 只准出現在 `wasi-shim.ts` 的 `crypto_sign_rs256` host function
|
||
- Template 展開(`{{secret.X}}` / `{{runtime.X}}` 替換)— 只能在 WASM 零件內
|
||
- PEM → PKCS8 解析
|
||
- JWT header/payload/signature 組裝
|
||
- Token exchange(拿 service account JWT 換 access_token)
|
||
- 具體 API call 實作(例如 gmail send / telegram sendMessage / google sheets append)
|
||
|
||
**Hook 會擋**:
|
||
- Write/Edit 到 `cypher-executor/src/` 下的 `.ts` 時,內容含:
|
||
- `crypto\.subtle\.decrypt` 且檔名不是 `wasi-shim.ts`
|
||
- `crypto\.subtle\.sign.*RSASSA` 且檔名不是 `wasi-shim.ts`
|
||
- `interpolateTemplate`、`\{\{secret\.` 的模板邏輯
|
||
- `BUILTIN_API_RECIPES`、`BUILTIN_CREDENTIALS_MAP`(新增用)
|
||
- `gmail.googleapis.com/gmail/v1/users/me/messages/send` 類 hard-code API URL
|
||
- `api.telegram.org/bot.*sendMessage`
|
||
- `sheets.googleapis.com/v4/spreadsheets`
|
||
- `notify-api.line.me/api/notify`
|
||
|
||
### 2.3 cypher-executor TS 的合法職責(允許)
|
||
- HTTP routing(Hono routes)
|
||
- workflow 執行排程(`graph-executor.ts`)
|
||
- 呼叫 WASM 零件(透過 HTTP fetch 到對應 Worker URL,或 Service Binding fallback)
|
||
- 提供 host function(`wasi-shim.ts` 的 `kv_get` / `crypto_decrypt` / `crypto_sign_rs256`)
|
||
- KV/R2/Service Binding 存取封裝
|
||
|
||
---
|
||
|
||
## 第三類:架構層級的禁令
|
||
|
||
### 3.1 禁止新增 Service Binding
|
||
**Cypher binding 不是 Cloudflare service binding**。它是 YAML/KV 裡的 URL 清單。
|
||
|
||
零件串接(workflow 層)一律走 HTTP URL,不走 `[[services]]`。
|
||
|
||
13 個現有的 `SVC_*` 綁定(`cypher-executor/wrangler.toml`,邏輯零件)是歷史遺產(效能優化),**保留但不新增**。
|
||
|
||
> **2026-06-06 註**(來源:credential-primitives-wasm Phase 7,該卷已封存於 `system-dev/docs/3-specs/archive/`;**本註記述的規則仍現行有效**):self-hosted 的 cypher 與 auth worker 同在 `{sub}.workers.dev` zone,cypher `fetch()` 打 auth 觸發 CF **same-zone 1042**(壓測階段 11)。**未用 service binding 解**(評估後廢:service binding 靜態、加/改要重 deploy cypher)。改用 **`global_fetch_strictly_public` compatibility flag**(cypher wrangler.toml)讓 same-zone fetch 走公網前門 → 同 zone 也通,**auth 維持 HTTP fetch、不加 binding**。故本禁令不變。
|
||
|
||
**Hook 會擋**:bash 指令含 `wrangler tail` 以外、涉及 `[[services]]` 新增的 pattern;Edit wrangler.toml 新增 `[[services]]` 區塊時警告確認。
|
||
|
||
### 3.2 禁止以「從 R2 取 WASM」為設計
|
||
平台內建零件已 bundle 進各自 Worker,不從 R2 取。
|
||
R2 只在 Phase 5(用戶自製零件)啟用。
|
||
|
||
**Hook 會警告**:TS 中出現 `env.WASM_BUCKET.get(` 的新增 code(除非在明確標註的 Phase 5 user-submit 路徑中)。
|
||
|
||
### 3.3 禁止複製貼上 Worker 程式碼到新目錄
|
||
要改 `gmail` 零件 → 改 `registry/components/gmail/main.go`,重新編譯、部署。
|
||
**不准**新建 `gmail-v2/`、`new-gmail/`、`gmail-worker/` 等目錄。
|
||
|
||
**Hook 會擋**:`mkdir` 或 `Write` 到 `{component-name}-v2/`、`new-{component-name}/`、`{component-name}-worker/` 類路徑。
|
||
|
||
### 3.4 禁止在 SDK 內做 server 職責
|
||
- **禁止**:SDK 裡做 server 端解密、credential-injector 重實作、workflow executor、auth recipe 解析
|
||
- **允許**:SDK 做 HTTP thin wrapper + client 端加密(AES-GCM)
|
||
|
||
---
|
||
|
||
## 第四類:流程層級的禁令
|
||
|
||
### 4.1 禁止沒讀 SDD 就動 code
|
||
見 `00-sdd-protocol.md`。
|
||
|
||
### 4.2 禁止批次更新 tasks.md
|
||
每完成一個 task 就立刻 mark `- [x]`。不准「先全部做完再一次更新」。
|
||
|
||
### 4.3 禁止新建 SDD 而不事先與 richblack 確認
|
||
SDD 屬於架構決策,必須人確認。CC 不可以自行在 `docs/3-specs/` 底下建新目錄。
|
||
例外:在現有 SDD 目錄內新增 `requirements.md` / `design.md` / `tasks.md` 的單檔補充(需在 CLAUDE.md 已註記的 SDD 範圍內)。
|
||
|
||
---
|
||
|
||
## 第五類:薄殼原則的禁令(詳見 07-thin-shell.md)
|
||
|
||
### 5.1 禁止在薄殼介面(cli/src/、arcrun-mcp/src/)實作業務邏輯
|
||
能力只實作一次,放在 API(cypher-executor 端點)。薄殼只做介面轉換 + 暴露。
|
||
|
||
**Hook 會擋**(規則 7.x,範圍 `cli/src/` 與 `arcrun-mcp/src/` 的 .ts/.js):
|
||
- 7.1 新增 `seedApiRecipes` / `seedAuthRecipes` / `seedRecipes` 編排函式(seed 是 API 行為,§4.1 反例)
|
||
- 7.2 介面層拼裝 upsert(同函式內 PATCH + POST + upsert/找則改否則建 字樣)
|
||
- 7.3 client 端「全部成功才做下一步」gate(`deployFullyOk` 類,§4.1 反例)
|
||
|
||
### 5.2 禁止用**零件**補 API 缺的能力(污染零件庫)
|
||
缺能力 → 先看 07 §3.5 自力救濟階梯分流:**自家** API 缺 → 補 API endpoint;**第三方** API 缺(gsheets filter / 無 upsert)→ 走 **workflow/code-node 補丁**(合法,是資料產物非介面層 TS,hook 範圍外);純計算 → code-node;真需新穩定能力才建零件 PR。
|
||
禁的是「在介面層拼裝」(5.1,hook 擋)與「亂建零件污染零件庫」,**不是禁任何補丁**(hook 偵測不到語意,靠 code review)。
|
||
|
||
### 5.3 禁止同一 API 能力在不同介面用不同參數簽名 / 連不同帳號
|
||
`validate` 在 CLI 吃 YAML、MCP 卻要 `api_key`+`graph` = 底層分歧。薄殼差異只能來自介面慣例。
|
||
所有薄殼讀同一份身份來源(見 07 §4)。
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## 第六類:租戶字串來源(Arcrun#108/#105 同族)
|
||
|
||
### 6.1 靜態租戶字串不得用於資料面過濾
|
||
**知識資料面的 `owner_id`(三元組/entries/records/藏書地圖/工作流 KV)必須與寫入端同源。**
|
||
寫入端只有一個真相源=使用者 `~/.arcrun/config.yaml` 的 `api_key`(=實例 namespace,
|
||
CLI push/小幫手上傳/MCP 都用它)。讀取端拿另一份手抄的環境變數預設值 → 全被過濾掉。
|
||
|
||
實害:`portalTenant(env) = env.CONSOLE_TENANT || "leo"` 讓 leo 的 **1854 條三元組被過濾成 0 個庫**
|
||
(#108);前一天 `ownerNamespace(env) = env.MCP_OWNER_NAMESPACE || "leo"` 是同一句話(#105)。
|
||
|
||
**規則**:
|
||
1. `cypher-executor/src/lib/tenant.ts` 是租戶字串的**唯一產地**。
|
||
`CONSOLE_TENANT` / `ARCRUN_NAMESPACE` 只能在該檔被讀取。
|
||
2. 知識資料面用 `knowledgeOwner(env)`(回 `TenantId`),過濾一律經
|
||
`ownerQuery()` / `ownerField()`——它們只吃 `TenantId`,`tsc` 就擋掉「隨手一個 string」。
|
||
3. 帳號層用 `accountTenant(env)`(回 `string`,**刻意不是 TenantId**):帳號子 namespace
|
||
`{tenant}::portal` 與 cypher 自己寫的設定用它,型別上不可能流進知識資料面。
|
||
4. 身分解析路徑上**不准有字面預設值**。解析不到 → 丟 `TenantUnresolvedError`,
|
||
誠實回「讀不到」(不是「你沒有」,#100 同一條)。
|
||
|
||
**機械強制**(規則存在但沒機制驗證=它會再犯第三次):
|
||
- 出貨閘:`scripts/build-worker-artifacts.mjs` 編 tier2 成品前先掃,違規 → **編不出成品**。
|
||
- 本機自查:`cd cypher-executor && npm run check:tenant`(`npm test` 也會先跑它)。
|
||
- 規則本體:`cypher-executor/scripts/tenant-source-rules.mjs`(純函式);
|
||
閘自己的測試:`cypher-executor/tests/tenant-gate.test.ts`(壞例子會擋+合法寫法零誤攔)。
|
||
|
||
> 尚未接上 PreToolUse hook(`.claude/hooks/` 為受保護檔案,需人類加入)。
|
||
> 要加的話:檢查器已備妥 `--stdin <相對路徑>` 模式,可在寫入前擋。
|
||
|
||
## Hook Block 訊息格式
|
||
|
||
當 hook 擋住一個操作時,訊息格式統一為:
|
||
|
||
```
|
||
❌ BLOCKED by arcrun CLAUDE rules
|
||
違反項:<禁令編號,例如 2.2>
|
||
原因:<簡短說明>
|
||
正確做法:<該改去哪裡、該用什麼方式>
|
||
參考:.claude/rules/<對應檔案>
|
||
```
|
||
|
||
這樣 CC 拿到錯誤訊息後有機會自行導正,不是被擋死就愣住。
|