chore: remove duplicate credentials/ dir + add CLAUDE.md + .claude rules

credentials/ was a leftover duplicate — all credential routes already live
in cypher-executor/src/routes/credentials.ts. Adds the SDD protocol,
tech-stack, forbidden-list, component-architecture, and progress rules
that guide Phase 1-6 refactors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 16:53:26 +08:00
parent 40df4025a6
commit 6ee6fee8b9
26 changed files with 1053 additions and 4757 deletions
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# .claude/hooks/post-edit-remind-tasks.sh
# arcrun PostToolUse hook for Write / Edit / MultiEdit
#
# 職責:改完 code 後立刻提醒 CC 更新對應 tasks.md,避免批次更新
# 退出 code:不 block,只提醒(exit 0
set -o pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
# 只針對程式碼檔案提醒(不含 tasks.md / CLAUDE.md / rules/*.md 本身)
if [[ "$FILE_PATH" =~ \.(go|ts|tsx|js|jsx|py|rs|wasm)$ ]] && [[ "$FILE_PATH" != *"tasks.md"* ]]; then
cat >&2 <<EOF
📌 PostEdit 提醒(by arcrun hook
剛修改了:${FILE_PATH}
下一步動作:
1. 找到對應的 .agents/specs/*/tasks.md
2. 如果這個 task 已完成 → 立刻把 [ ] 改成 [x](或把進行中的 [🔄] 改成 [x])
3. 如果發現新的 sub-task → 立刻加入 tasks.md
4. 不要等到 session 結束才批次更新
違反 SDD 協議會在 Stop hook 被提醒,建議現在就處理。
EOF
fi
exit 0
+81
View File
@@ -0,0 +1,81 @@
#!/bin/bash
# .claude/hooks/pre-bash-guard.sh
# arcrun PreToolUse guard for Bash
#
# 職責:擋下會違反 CLAUDE rules 的 shell 指令
# 退出 code
# 0 = 允許
# 2 = 擋下(stderr 訊息會回傳給 CC)
set -o pipefail
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
block() {
local rule="$1"
local reason="$2"
local fix="$3"
cat >&2 <<EOF
❌ BLOCKED by arcrun CLAUDE rules
違反項:${rule}
指令:${CMD}
原因:${reason}
正確做法:${fix}
參考:.claude/rules/02-forbidden.md
EOF
exit 2
}
# ─────────────────────────────────────────────────────────────────────────────
# 規則 1.2 / 3.3:禁止用 mkdir 建立違規的 auth/credential worker 目錄
# ─────────────────────────────────────────────────────────────────────────────
if echo "$CMD" | grep -qE "mkdir.*((auth|credential|jwt|oauth)[-_]worker|new[-_](auth|credential|jwt|oauth))"; then
block "1.2/3.3" \
"偵測到嘗試建立新的 auth/credential/jwt/oauth Worker 目錄" \
"auth primitive 放在 registry/components/auth_*/;不需要另建 worker 目錄"
fi
# 禁止建立同名零件的平行目錄
if echo "$CMD" | grep -qE "mkdir.*/(gmail|telegram|google[-_]sheets|line[-_]notify|http[-_]request)[-_](v2|v3|new|worker|backup)"; then
block "3.3" \
"禁止為既有零件建立平行目錄(v2/new/worker/backup" \
"直接改 registry/components/<n>/main.go"
fi
# ─────────────────────────────────────────────────────────────────────────────
# 規則 1.3:禁止 wrangler init / generate auth-* credential-* jwt-*
# ─────────────────────────────────────────────────────────────────────────────
if echo "$CMD" | grep -qE "wrangler[[:space:]]+(init|generate).*[[:space:]](auth|credential|jwt|oauth)[-_]"; then
block "1.3" \
"禁止用 wrangler init/generate 建立 auth/credential/jwt Worker" \
"auth primitive 透過 component-worker-template/ 搭配 WASM binary 部署,不要 wrangler init"
fi
# ─────────────────────────────────────────────────────────────────────────────
# 規則 3.1Service Binding 新增警示
# ─────────────────────────────────────────────────────────────────────────────
# 偵測在 wrangler.toml 新增 [[services]] 的 echo/cat/sed 操作(非 100% 準確,但夠用)
if echo "$CMD" | grep -qE "echo.*\[\[services\]\].*>>"; then
block "3.1" \
"偵測到要在 wrangler.toml 新增 [[services]] binding" \
"零件串接一律走 HTTP URLcypher binding),不新增 service binding。若有特殊需求,先與 richblack 確認"
fi
# ─────────────────────────────────────────────────────────────────────────────
# 一般性危險指令
# ─────────────────────────────────────────────────────────────────────────────
if echo "$CMD" | grep -qE "rm[[:space:]]+-rf[[:space:]]+(/|/\*|~|\\\$HOME|\.)"; then
block "general" \
"偵測到危險的 rm -rf 指令" \
"明確指定要刪的目錄,不要對根目錄 / home / 當前目錄遞迴刪除"
fi
# 禁止 force push 到 main
if echo "$CMD" | grep -qE "git[[:space:]]+push.*--force.*(main|master)"; then
block "general" \
"禁止 force push 到 main/master" \
"用 feature branch,或和 richblack 確認後手動操作"
fi
exit 0
+187
View File
@@ -0,0 +1,187 @@
#!/bin/bash
# .claude/hooks/pre-write-guard.sh
# arcrun PreToolUse guard for Write / Edit / MultiEdit
#
# 職責:擋下會違反 CLAUDE rules 的檔案寫入操作
# 退出 code
# 0 = 允許
# 2 = 擋下(stderr 訊息會回傳給 CC)
#
# 依賴:jq
set -o pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
# 取得將要寫入的內容(Write: contentEdit: new_stringMultiEdit: edits[].new_string 全部串起來)
CONTENT=$(echo "$INPUT" | jq -r '
.tool_input.content
// .tool_input.new_string
// (.tool_input.edits // [] | map(.new_string // "") | join("\n"))
// ""
')
block() {
local rule="$1"
local reason="$2"
local fix="$3"
cat >&2 <<EOF
❌ BLOCKED by arcrun CLAUDE rules
違反項:${rule}
檔案:${FILE_PATH}
原因:${reason}
正確做法:${fix}
參考:.claude/rules/02-forbidden.md
EOF
exit 2
}
# ─────────────────────────────────────────────────────────────────────────────
# 規則 1.1registry/components/ 下不准 TS(除非是 AssemblyScript
# ─────────────────────────────────────────────────────────────────────────────
if [[ "$FILE_PATH" == *"registry/components/"* && "$FILE_PATH" == *.ts ]]; then
# 允許 asconfig.json 同目錄的 AssemblyScript
COMP_DIR=$(dirname "$FILE_PATH")
if [[ ! -f "$COMP_DIR/asconfig.json" ]]; then
block "1.1" \
"registry/components/ 下禁止 TypeScript(除非是 AssemblyScript 且同目錄有 asconfig.json" \
"零件必須用 TinyGo (main.go) 或 AssemblyScript 實作並編譯成 .wasm"
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# 規則 1.2:禁止在非法位置新增 auth/credential 實作
# ─────────────────────────────────────────────────────────────────────────────
# 合法位置:registry/components/auth_static_key | auth_oauth2 | auth_service_account | auth_mtls
if [[ "$FILE_PATH" =~ auth[-_](static[-_]key|oauth2|service[-_]account|mtls) ]]; then
if [[ "$FILE_PATH" != *"registry/components/auth_"* ]]; then
block "1.2" \
"auth primitive 實作只能放在 registry/components/auth_<type>/" \
"改去 registry/components/auth_static_key/ 等目錄,用 TinyGo 實作 main.go"
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# 規則 2.1:禁止新增含特定關鍵字的 TS 檔案(credential-injector / jwt-signer 等)
# ─────────────────────────────────────────────────────────────────────────────
if [[ "$FILE_PATH" == *.ts ]]; then
BASE=$(basename "$FILE_PATH")
# 既有的 credential-injector.ts / jwt-signer.ts 允許修改(為了刪除),但不准新增同名
if [[ "$BASE" =~ ^(credential[-_]injector|jwt[-_]signer)\.ts$ ]]; then
if [[ ! -f "$FILE_PATH" ]]; then
block "2.1" \
"禁止新增 ${BASE}(Phase 1-3 的目標是刪除此類檔案,不是重建)" \
"credential 注入 / JWT signing 屬於 WASM 零件職責,改去 registry/components/auth_*/"
fi
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# 規則 2.2cypher-executor TS 裡不准實作業務邏輯(只准 wasi-shim.ts 做 crypto
# ─────────────────────────────────────────────────────────────────────────────
if [[ "$FILE_PATH" == *"cypher-executor/src/"* && "$FILE_PATH" == *.ts ]]; then
BASE=$(basename "$FILE_PATH")
# crypto.subtle.decrypt:只准在 wasi-shim.ts
if echo "$CONTENT" | grep -qE "crypto\.subtle\.decrypt"; then
if [[ "$BASE" != "wasi-shim.ts" ]]; then
block "2.2" \
"AES-GCM 解密(crypto.subtle.decrypt)只准出現在 wasi-shim.ts 的 crypto_decrypt host function" \
"把解密邏輯移到 wasi-shim.ts 的 host function;或讓 WASM 零件透過 u6u.crypto_decrypt 呼叫"
fi
fi
# crypto.subtle.sign with RSASSA:只准在 wasi-shim.ts
if echo "$CONTENT" | grep -qE "crypto\.subtle\.sign.*RSASSA"; then
if [[ "$BASE" != "wasi-shim.ts" ]]; then
block "2.2" \
"RS256 簽章只准出現在 wasi-shim.ts 的 crypto_sign_rs256 host function" \
"把簽章移到 wasi-shim.ts;或讓 auth_service_account WASM 透過 u6u.crypto_sign_rs256 呼叫"
fi
fi
# Template 展開:{{secret.X}} 或 {{runtime.X}} 屬於 WASM 職責
if echo "$CONTENT" | grep -qE "\{\{(secret|runtime)\." ; then
block "2.2" \
"Template 展開({{secret.X}} / {{runtime.X}})屬於 WASM auth primitive 職責" \
"把這段邏輯改寫到 registry/components/auth_static_key/main.goTinyGo"
fi
# Hard-code 的 BUILTIN_API_RECIPES / BUILTIN_CREDENTIALS_MAP 新增
if echo "$CONTENT" | grep -qE "(BUILTIN_API_RECIPES|BUILTIN_CREDENTIALS_MAP)\s*[:=]"; then
# 允許「把它設成空物件」或「刪除」,但不准新增實作
if echo "$CONTENT" | grep -qE "BUILTIN_API_RECIPES.*=.*\{\s*[a-zA-Z]"; then
block "2.2" \
"禁止在 TS 裡新增 BUILTIN_API_RECIPES / BUILTIN_CREDENTIALS_MAP 實作" \
"API 呼叫邏輯屬於各自的 WASM 零件(gmail.wasm / telegram.wasm 等),cypher-executor 只做 routing"
fi
fi
# Hard-code API endpoint 實作
HARDCODED_APIS=(
"gmail\.googleapis\.com/gmail/v1/users/me/messages/send"
"api\.telegram\.org/bot.*sendMessage"
"sheets\.googleapis\.com/v4/spreadsheets"
"notify-api\.line\.me/api/notify"
)
for PATTERN in "${HARDCODED_APIS[@]}"; do
if echo "$CONTENT" | grep -qE "$PATTERN"; then
# 允許 wasi-shim.ts 裡的 http_request host function(它只是 proxy
if [[ "$BASE" != "wasi-shim.ts" ]]; then
block "2.2" \
"禁止在 cypher-executor TS 裡 hard-code API endpoint(偵測到: $PATTERN" \
"把 API 呼叫移到對應的 WASM 零件(registry/components/gmail/main.go 等)"
fi
fi
done
# exchangeGoogleJwt / 類似 token exchange function
if echo "$CONTENT" | grep -qE "(exchangeGoogleJwt|exchangeServiceAccountJwt|signGoogleJwt)"; then
if [[ "$BASE" != "wasi-shim.ts" ]]; then
block "2.2" \
"Token exchange 邏輯屬於 auth_service_account WASM 零件" \
"改到 registry/components/auth_service_account/main.go"
fi
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# 規則 3.3:禁止建立 *-v2 / new-* / *-worker 類複製貼上目錄
# ─────────────────────────────────────────────────────────────────────────────
if [[ "$FILE_PATH" =~ /(auth|credential|jwt|oauth|gmail|telegram|google-sheets|line-notify|http-request)[-_](v2|v3|new|worker|backup|temp)/ ]]; then
block "3.3" \
"禁止為同一零件建立平行目錄(v2/new/worker/backup 等)" \
"直接修改 registry/components/<name>/main.go 即可;需要版本管理請用 git branch"
fi
if [[ "$FILE_PATH" =~ /new-(auth|credential|jwt|oauth|gmail|telegram)/ ]]; then
block "3.3" \
"禁止為同一零件建立 new-<name>/ 平行目錄" \
"直接修改 registry/components/<name>/main.go"
fi
# ─────────────────────────────────────────────────────────────────────────────
# 規則 4.3:禁止自行在 .agents/specs/ 下建新 SDD 目錄
# ─────────────────────────────────────────────────────────────────────────────
if [[ "$FILE_PATH" == *".agents/specs/"* ]]; then
# 檢查是否在已知 SDD 目錄內
KNOWN_SDDS=(
".agents/specs/arcrun"
".agents/specs/u6u-core-mvp"
".agents/specs/u6u-platform-evolution"
)
IN_KNOWN=false
for K in "${KNOWN_SDDS[@]}"; do
if [[ "$FILE_PATH" == *"$K/"* ]]; then
IN_KNOWN=true
break
fi
done
if [[ "$IN_KNOWN" == "false" ]]; then
block "4.3" \
"禁止自行在 .agents/specs/ 下建立新的頂層 SDD 目錄" \
"先與 richblack 確認 SDD 範圍。若是現有 SDD 的補充檔案,請放到已知 SDD 目錄下"
fi
fi
exit 0
+72
View File
@@ -0,0 +1,72 @@
#!/bin/bash
# .claude/hooks/session-start-load-sdd.sh
# arcrun SessionStart hook
#
# 職責:session 啟動時注入當前進度、絕對禁令、SDD 位置,避免 CC 靠記憶猜測
# 輸出走 stdout,會注入到 CC 的 context
set -o pipefail
cat <<'EOF'
============================================================
🚨 arcrun 工作規範(SessionStart 注入)
============================================================
📌 絕對禁令(違反會被 pre-write / pre-bash hook 直接 block):
1. registry/components/ 下禁止 TypeScript 實作零件
→ 只能 TinyGo (main.go) 或 AssemblyScript,編譯成 .wasm
2. cypher-executor TS 裡禁止實作 credential/auth/JWT 業務邏輯
→ crypto.subtle.decrypt / sign 只准出現在 wasi-shim.ts 的 host function
→ {{secret.X}} template 展開屬於 WASM 零件職責
→ 禁止 hard-code gmail/telegram/sheets API endpoint
3. 禁止新增 Service Binding 綁零件
→ 零件串接走 HTTP URLcypher binding = YAML 裡的 URL 清單)
→ Cypher binding 不是 Cloudflare 的 binding 機制
4. 每個 WASM 零件 = 獨立 Worker = 公開 URL
→ 不是從 R2 動態讀(平台零件已 bundle 進各自 Worker
→ R2 只用於 Phase 5 的用戶自製零件(未啟用)
📋 任何 code 變動前,必須:
1. 讀 .agents/specs/arcrun/arcrun.md(總進度)
2. 讀對應 task 的 SDDdesign.md + tasks.md
3. 在回覆開頭宣告(格式見 .claude/rules/00-sdd-protocol.md):
📋 已讀 SDD<檔案清單>
🎯 本次對應 task<task 編號>
📐 本次 task 的 SDD 規範摘要:<重點>
🚧 執行範圍:修改/建立/刪除 <檔案>
4. 每完成一個 task,立刻更新 tasks.md 的 [x],不批次
🔥 當前進行中 PhaseCredential Primitives TS → WASM
SDD.agents/specs/arcrun/credential-primitives-wasm/
已完成:Phase 0.1-0.5(核心合併 + u6u-core 刪除)
未完成硬前置:
- Phase 0.6 wasi-shim 加 host functionskv_get / crypto_decrypt / crypto_sign_rs256
- Phase 0.7 component-loader WASM runner 路徑
未完成主要任務:
- Phase 1 auth_static_key WASM 零件(TinyGo
- Phase 2 auth_service_account WASM 零件
- Phase 3 刪除 cypher-executor 的三套違規 TS
* src/actions/credential-injector.ts(整檔刪)
* src/lib/jwt-signer.ts(整檔刪)
* src/lib/component-loader.ts 的 BUILTIN_API_RECIPES + BUILTIN_CREDENTIALS_MAP(整段刪)
⛔ 封測狀態:推遲(richblack 2026-04-19 決定)
原因:違規 TS 未清,不封測。
📚 詳細規範:
.claude/rules/00-sdd-protocol.md — SDD 協議
.claude/rules/01-tech-stack.md — 技術棧硬限制
.claude/rules/02-forbidden.md — 禁止清單(hook 強制)
.claude/rules/03-component-architecture.md — 零件架構(R2/cypher binding/service binding 的正確定義)
.claude/rules/04-current-progress.md — 當前進度 + SDD 索引
============================================================
EOF
exit 0
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
# .claude/hooks/stop-check-sync.sh
# arcrun Stop hook
#
# 職責:session 結束前檢查 code 變動是否有對應的 SDD / tasks.md 更新
# 退出 code:不 block,只警告
set -o pipefail
# 檢查 .agents/specs 下本次 session 是否有變動
SPECS_DIFF=$(git -C "$(pwd)" status --porcelain -- '.agents/specs/' 2>/dev/null | head -20)
CODE_DIFF=$(git -C "$(pwd)" status --porcelain -- '*.go' '*.ts' '*.tsx' '*.py' 'cypher-executor/' 'registry/' 'cli/' 2>/dev/null | head -20)
if [[ -n "$CODE_DIFF" && -z "$SPECS_DIFF" ]]; then
cat >&2 <<EOF
⚠️ Stop hook 警告(by arcrun hook
偵測到本 session 有程式碼變動,但 .agents/specs/ 下的 SDD 文件沒有任何變動。
未 commit 的程式碼變動:
$(echo "$CODE_DIFF" | head -10)
請在結束前確認:
1. 對應的 tasks.md 是否已更新 [x]
2. 是否有架構變動需要更新 design.md?
3. 是否有 SDD 範圍外的 change 未標記?
SDD 協議要求:code 和 SDD 必須同步更新。
參考:.claude/rules/00-sdd-protocol.md
EOF
fi
# 若有暫存的 tasks.md 變動,提醒 commit
TASKS_DIFF=$(git -C "$(pwd)" status --porcelain -- '.agents/specs/**/tasks.md' 2>/dev/null | head -5)
if [[ -n "$TASKS_DIFF" ]]; then
cat >&2 <<EOF
📝 提醒:tasks.md 有未 commit 的變動
$(echo "$TASKS_DIFF")
記得在結束前 commit。
EOF
fi
exit 0
+79
View File
@@ -0,0 +1,79 @@
# SDD 協議(每次啟動必讀)
## 第零原則:沒讀 SDD 不准動 code
任何 `.go` / `.ts` / `.tsx` / `.wasm` 相關變動,**必須**按以下順序執行。**不得簡化,不得跳過**。
### 步驟 1:讀總進度
先讀 `.agents/specs/arcrun/arcrun.md`,了解當前 Phase。
### 步驟 2:定位對應 SDD
根據任務性質找對應 SDD
| 任務類型 | 對應 SDD |
|---------|---------|
| Auth primitive WASM 零件(static_key/oauth2/service_account/mtls | `.agents/specs/arcrun/credential-primitives-wasm/` |
| 清除 cypher-executor 裡的 TS 業務邏輯 | `.agents/specs/arcrun/credential-primitives-wasm/` |
| WASI shim host functionskv_get / crypto_decrypt / crypto_sign_rs256 | `.agents/specs/arcrun/credential-primitives-wasm/` |
| Auth Recipe 系統(recipe schema、KV 格式) | `.agents/specs/arcrun/auth-recipe.md` |
| Landing Page | `.agents/specs/arcrun/landing-page.md` |
| CLI / SDKPython/JS | `.agents/specs/arcrun/sdk-and-website/` |
| u6u-core-mvp 整體架構 | `.agents/specs/u6u-core-mvp/` |
| u6u Platform Evolution | `.agents/specs/u6u-platform-evolution/` |
| Credential 長期規格(需求源) | `docs/user_requirements/credential_parts.md` |
`design.md``tasks.md` 兩份。
### 步驟 3:宣告(強制格式)
開始動手前,在回覆開頭**逐字**貼出以下宣告:
```
📋 已讀 SDD
- .agents/specs/arcrun/arcrun.md(當前 Phase<phase 名稱>
- <對應 SDD 的 design.md 路徑>
- <對應 SDD 的 tasks.md 路徑>
🎯 本次對應 task<task 編號,例如 "Phase 1.3 實作 auth_static_key main.go">
📐 本次 task 的 SDD 規範摘要:
- <重點 1>
- <重點 2>
- <重點 3>
🚧 執行範圍:
- 會修改:<檔案清單>
- 會建立:<檔案清單>
- 會刪除:<檔案清單>
```
**不做這個宣告 = 違反 SDD 協議 = 停手等 richblack**
### 步驟 4check tasks.md 狀態
動手前:在 tasks.md 把對應 task 的 `- [ ]` 改成 `- [🔄]`(進行中標記)。
完成後:改成 `- [x]`,不批次更新,每完成一個就立刻改。
## 什麼算「任務超出 SDD 範圍」?
以下情況屬於 **change**,不是 **modify****必須停手並與 richblack 確認**
- SDD 沒寫到的新功能
- 新增頂層目錄
- 新增新的 Worker(不管是 cypher-executor / registry / 零件 worker
- 修改架構決策(例如「改用 xxx 取代 yyy」)
- 跨多個子系統的連鎖修改
**停手不是怯懦,是專業**。猜錯方向比慢一小時更糟。
## 發現 SDD 本身有問題怎麼辦?
- SDD 和實作不一致 → 停手,列出矛盾點,與 richblack 確認哪一邊是對的
- SDD 規範之間互相矛盾(例如禁令 A 和設計 B 衝突)→ 停手,引用矛盾原文,與 richblack 確認
- **不可以自行猜哪個是對的**。CC 之前兩天就是這樣走錯的。
## 為什麼這個協議存在
arcrun 規範已經足夠細緻,CC 之前出錯不是因為不懂,而是因為**沒讀**或**讀了覺得「大概是這個意思」就動手**。SDD 協議強制把「先讀 → 定位 → 宣告 → 執行 → 更新」做成一條死規矩,沒有繞過去的路徑。
+70
View File
@@ -0,0 +1,70 @@
# 技術棧硬限制
## 三層語言對應(絕對不可混用)
| 層級 | 語言 | 位置 | 職責 |
|-----|------|------|------|
| 零件(Component | **TinyGo 或 AssemblyScript → WASM** | `registry/components/{name}/` | 所有業務邏輯 |
| 零件 Worker 包裝 | TypeScript(固定模板,不寫業務邏輯) | `.component-builds/{name}/` | WASI shimstdin/stdout JSON |
| Orchestration Worker | TypeScript + Hono | `cypher-executor/` | HTTP routing、workflow 執行排程、host functions |
| CLI | TypeScript + Node.js | `cli/` | `acr` 指令 |
| Python SDK | Python | `python-sdk/` | HTTP thin wrapper + client 端加密 |
| JS SDK | TypeScript + Web Crypto | `js-sdk/` | HTTP thin wrapper + client 端加密 |
| Frontend | React 19 + Vite + Tailwind v4 | `landing/` | Cloudflare Pages |
## 零件實作規範
### 只有兩種合法語言
- **TinyGo**`tinygo build -target=wasi -o {name}.wasm main.go`
- **AssemblyScript**`asc main.ts --target release -o {name}.wasm`
### I/O 模型
- **stdin**JSON input
- **stdout**JSON output
- 不用 HTTP server,不監聽 socketWASI preview1 沒 socket
### Host Functions(零件呼叫外部能力的唯一管道)
`u6u` namespace 下:
| Host Function | 用途 |
|---|---|
| `u6u.http_request` | 發 HTTP 請求 |
| `u6u.kv_get` | 讀 Cloudflare KVWorker 側依 key 前綴路由到正確 KV) |
| `u6u.crypto_decrypt` | AES-GCM 解密(encryption key 永不暴露給 WASM |
| `u6u.crypto_sign_rs256` | RSA-SHA256 簽章(PKCS8 bytes 傳入) |
**所有 host function 在 `cypher-executor/src/lib/wasi-shim.ts` 實作**。零件透過 WASI import 使用。
## 資料儲存
| 儲存 | 用途 | Key 格式 |
|-----|------|---------|
| Cloudflare KV `WEBHOOKS` | workflow 定義(cypher binding YAML | `webhook:{api_key}:{name}` |
| Cloudflare KV `CREDENTIALS_KV` | 加密 credential | `{api_key}:cred:{name}` |
| Cloudflare KV `RECIPES` | auth recipe / 動態 API recipe | `auth_recipe:{service}`, `rec_{hash}` |
| Cloudflare KV `USERS_KV` | 用戶資料 | `user:{api_key}` |
| Cloudflare KV `SESSIONS_KV` | session | `session:{token}` |
| Cloudflare KV `ANALYTICS_KV` | 執行分析 | `execution:{timestamp}:{id}` |
| Cloudflare KV `EXEC_CONTEXT` | workflow 執行中的 context | `ctx:{execution_id}:{node_id}` |
| Cloudflare R2 `WASM_BUCKET` | **只用於用戶自製零件**Phase 5 以後啟用) | `{api_key}:cmp:{hash}` |
**警告:R2 不存平台內建零件的 WASM**。平台零件已 bundle 進各自的 Worker binary`[[wasm_modules]]``import ... assert { type: 'webassembly' }`)。
## 加解密規範
- **演算法**AES-GCM 256-bit
- **加密位置**Client 端(CLI / Python SDK / JS SDK
- Python`cryptography` 套件
- JSWeb Crypto API`crypto.subtle`
- **解密位置**Server 端 **WASM primitive**(透過 host function `crypto_decrypt`
- cypher-executor TS **不解密**,只提供 host function
- `ENCRYPTION_KEY` 只在 Worker host function 內部讀取,**永不經 stdin / 回傳值傳給 WASM**
- **傳輸格式**`{ name, encrypted, iv }`iv base64、encrypted base64
## 網路部署
- **平台 API**`cypher.arcrun.dev`cypher-executor
- **每個零件**:獨立 WorkerURL 慣例 `{component-name-kebab}.arcrun.dev`
- 例:`auth-static-key.arcrun.dev``gmail.arcrun.dev`
- **Landing**`arcrun.dev`
- **部署工具**Wrangler
+132
View File
@@ -0,0 +1,132 @@
# 禁止行為清單(零容忍)
**這份清單由 `.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 檔案
**已存在但要刪**Phase 1-3 範圍):
- `cypher-executor/src/actions/credential-injector.ts` → 刪除(走 WASM auth primitive
- `cypher-executor/src/lib/jwt-signer.ts` → 刪除(RS256 移入 auth_service_account WASM
- `cypher-executor/src/lib/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 signingblock
### 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 routingHono 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]]`
15 個現有的 `SVC_*` 綁定(`cypher-executor/wrangler.toml`)是歷史遺產(邏輯零件效能優化),**保留但不新增**。
**Hook 會擋**bash 指令含 `wrangler tail` 以外、涉及 `[[services]]` 新增的 patternEdit 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 不可以自行在 `.agents/specs/` 底下建新目錄。
例外:在現有 SDD 目錄內新增 `requirements.md` / `design.md` / `tasks.md` 的單檔補充(需在 CLAUDE.md 已註記的 SDD 範圍內)。
---
## Hook Block 訊息格式
當 hook 擋住一個操作時,訊息格式統一為:
```
❌ BLOCKED by arcrun CLAUDE rules
違反項:<禁令編號,例如 2.2>
原因:<簡短說明>
正確做法:<該改去哪裡、該用什麼方式>
參考:.claude/rules/<對應檔案>
```
這樣 CC 拿到錯誤訊息後有機會自行導正,不是被擋死就愣住。
+147
View File
@@ -0,0 +1,147 @@
# 零件架構與部署模式(必讀,CC 最常搞錯的地方)
## 第一核心概念:每個 WASM 零件 = 一個獨立 Worker = 一個公開 URL
**不是**從 R2 即時載入 WASM 執行。
**不是**用 service binding 串零件。
**不是**一個 Worker 裡跑多個零件。
**是**:每個零件都是獨立部署的 Worker,每個都有自己的 URL,例如:
- `https://if-control.arcrun.dev`
- `https://gmail.arcrun.dev`
- `https://auth-static-key.arcrun.dev`
### 零件 Worker 的結構
```
registry/components/{name}/
├── main.go ← TinyGo 原始碼(實際零件邏輯)
├── component.contract.yaml ← 輸入/輸出規格
└── {name}.wasm ← TinyGo 編譯產物
```
部署時,透過 `component-worker-template/` 把 WASM 包進一個 Hono Worker
```
.component-builds/{name}/
├── package.json
├── wrangler.toml ← name = "arcrun-{name}"route = "{name}.arcrun.dev"
├── component.wasm ← 從 registry/components/{name}/ 複製過來
└── src/index.ts ← 固定的 WASI shimPOST / → stdin → WASM → stdout → JSON
```
**src/index.ts 是通用模板**,所有零件都用同一份。這個 TS 只做 WASI runtime,不是業務邏輯。
---
## R2WASM_BUCKET)的真正用途
R2 存 WASM 只是**用戶自製零件上傳**用的。
**平台內建零件不從 R2 讀取**——它們在部署時就已 bundle 進 Worker 的 binary(透過 `[[wasm_modules]]``import` with `assert { type: 'webassembly' }`)。
Phase 5(封測後)才會啟用「用戶 push 自製零件 → 存 R2 → 動態執行」這條路徑。
**結論:當 CC 問「怎麼從 R2 取出 WASM」時,幾乎都是走錯路徑**。平台零件是獨立 Worker,走 HTTP 呼叫,不是 R2 動態載入。
---
## Cypher binding 的正確定義
**Cypher binding 不是 Cloudflare 的任何 binding 機制。**
Cypher binding 是一張 YAML 清單,內容是「一個 workflow 要呼叫哪些零件 URL」。存放在:
- 本地:`workflow.yaml`(用戶寫的 workflow
- KV`WEBHOOKS` KV(用戶 `acr push` 後存入)
Cypher executor 執行 workflow 時:
1. 從 KV 讀出 workflow YAML
2. 按 graph 順序解析每個節點的 `component`
3. 用 HTTP fetch 打對應的零件 URL
4. 把 output 當作下個節點的 input
**這就是 Cypher binding——用 HTTP URL 把零件串起來,存在 YAML/KV 裡**
### 為什麼不能用 Service Binding
Service binding 需要 `wrangler.toml` 裡寫死 `[[services]]`,且要 redeploy 才生效。arcrun 是類 n8n 服務,用戶建立新 workflow 時**絕對不可能**要他 redeploy。所以 workflow 層一定要 HTTP。
### Service Binding 的僅存合法用途
只在 `cypher-executor` 和**平台內建邏輯零件之間**保留(效能優化,避免公網往返)。看 `cypher-executor/wrangler.toml` 裡的 15 個 `[[services]]` 綁定就是這個用途。
**禁止新增任何 Service Binding**。所有新零件(特別是 auth primitive)都走 HTTP URL 路徑。
---
## 零件之間怎麼串:實際流程
假設 workflow 是:webhook → gmail(要 auth)→ google_sheets(要 auth
```
用戶 POST https://cypher.arcrun.dev/webhooks/named/xxx/trigger
cypher-executorWorker)讀 workflow YAML
├─ 節點 1: component = gmail
│ a. 查 auth_recipe:gmail → primitive = static_key
│ b. HTTP POST https://auth-static-key.arcrun.dev
│ { action: "authenticate", api_key, service: "gmail" }
│ → 回傳 { auth_headers: { Authorization: "Bearer ..." } }
│ c. HTTP POST https://gmail.arcrun.dev
│ { to, subject, body, _auth_headers }
│ → gmail 零件 Worker 執行 WASM → 回傳 { success, data }
└─ 節點 2: component = google_sheets
... 相同模式
```
**cypher-executor 本身不做 credential 解密、不做 JWT signing、不做 auth header 組裝**。這些全在 auth primitive WASM 零件內,cypher-executor 只負責 HTTP routing 和工作流排程。
---
## 實際禁令(CC 看這裡)
### 禁止在 `registry/components/` 下建立 TypeScript 檔案
零件邏輯一律 TinyGo 或 AssemblyScript,編譯成 `.wasm`
### 禁止把 auth 邏輯寫在 `cypher-executor/src/` 裡
credential 解密、JWT signing、template 展開(`{{secret.X}}`)全部屬於 auth primitive WASM 零件的職責。cypher-executor 只呼叫它們。
### 禁止問「怎麼從 R2 取 WASM」
平台內建零件**不從 R2 取**。每個零件已部署成獨立 Worker,走 HTTP URL。用戶自製零件才用 R2(Phase 5,未啟用)。
### 禁止新增 Service Binding
15 個現有的 SVC_* 是歷史遺產(邏輯零件效能優化),不新增。新零件(尤其 auth primitive)一律走 HTTP URL。
### 禁止重建已存在的零件 Worker
要改 `gmail` 零件邏輯 → 改 `registry/components/gmail/main.go`,重新編譯 `.wasm`,重新部署對應 Worker。**不要**在 `cypher-executor/src/lib/` 或其他地方建「新的 gmail 實作」。
---
## 部署一個新零件的完整步驟(auth_static_key 為例)
1. 建立 `registry/components/auth_static_key/`
- `main.go`TinyGo 實作)
- `component.contract.yaml`IO 規格)
2. 編譯:`cd registry/components/auth_static_key && tinygo build -target=wasi -o auth_static_key.wasm main.go`
3. 建立 `.component-builds/auth_static_key/`
- 複製 `component-worker-template/src/index.ts`
- 複製 `component-worker-template/package.json`
- 新建 `wrangler.toml`
```toml
name = "arcrun-auth-static-key"
main = "src/index.ts"
compatibility_date = "2025-02-19"
[vars]
COMPONENT_ID = "auth_static_key"
[[routes]]
pattern = "auth-static-key.arcrun.dev/*"
zone_name = "arcrun.dev"
```
- 複製 `auth_static_key.wasm` 到此目錄為 `component.wasm`
4. `cd .component-builds/auth_static_key && pnpm install && pnpm deploy`
5. 驗證:`curl https://auth-static-key.arcrun.dev` → 應回 `{ok: true, component: "auth_static_key"}`
6. 在 cypher-executor 的 auth-dispatcher 註冊對應 URL(或用慣例 `{name}.arcrun.dev`
**這是唯一正確的部署流程**。任何偏離這個流程的「替代方案」都要先和 richblack 確認。
+76
View File
@@ -0,0 +1,76 @@
# 當前進度(SessionStart 會注入此檔重點)
> 更新時間:2026-04-19
> 權威來源:`.agents/specs/arcrun/credential-primitives-wasm/tasks.md`
> 此檔僅摘要,詳細狀態以 tasks.md 為準。
---
## 封測狀態
**原定明天封測,richblack 決定推遲**,原因:cypher-executor 有三套 TS 業務邏輯違反「零件一律 WASM」架構原則(Phase 1-3 要清除的程式碼),在清除前不封測。
---
## 目前 PhaseCredential Primitives TS → WASM
**SDD 位置**`.agents/specs/arcrun/credential-primitives-wasm/design.md` + `tasks.md`
### 已完成
- **Phase 0.10.5**:核心合併(u6u-core 併入 arcrun、21 個零件 contract 完整、刪除重複 `credentials/` 目錄、CREDENTIALS_KV binding 確認、刪除 `matrix/u6u-core/`
- `registry/components/` 下 21 個零件(邏輯 + API)都有 `main.go` + `.wasm`
### 進行中 / 未完成
| Task | 狀態 | 阻擋關係 |
|-----|------|---------|
| 0.6 wasi-shim 新增 `kv_get` / `crypto_decrypt` / `crypto_sign_rs256` host functions | ⬜ 未開始 | **Phase 1-3 的硬前置** |
| 0.7 component-loader 新增 WASM runner 路徑 | ⬜ 未開始 | **Phase 1-3 的硬前置** |
| 1.1-1.8 `auth_static_key` WASM 零件(TinyGo | ⬜ 未開始 | 涵蓋 80% 服務 |
| 2.1-2.6 `auth_service_account` WASM 零件(JWT signing | ⬜ 未開始 | Google Service Account 等 |
| 3.1-3.5 清除 `component-loader.ts``BUILTIN_API_RECIPES` | ⬜ 未開始 | 要先有 Phase 1-2 的 WASM 零件 |
| 4.1-4.4 `auth_oauth2` + `auth_mtls`(封測後) | ⬜ 未開始 | 非阻擋項 |
| 5.1-5.7 核心穩定驗證(全域搜尋確認無殘餘 TS) | ⬜ 未開始 | 封測啟動門檻 |
### Phase 1-3 要**徹底刪除**的 TS 檔案(不是搬、不是改,是刪)
| 檔案 | 違反什麼 |
|-----|---------|
| `cypher-executor/src/actions/credential-injector.ts` | AES 解密、template 展開、JWT 邏輯 —— 應在 WASM |
| `cypher-executor/src/lib/jwt-signer.ts` | RS256 JWT 簽章邏輯 —— 應在 `auth_service_account.wasm` |
| `cypher-executor/src/lib/component-loader.ts``BUILTIN_API_RECIPES`~100 行) | gmail/telegram/line/gsheets/http_request/cron 的 TS 實作 —— 應全部走對應 WASM 零件 |
---
## 下一個 session 第一件要做的事
**讀 `.agents/specs/arcrun/credential-primitives-wasm/tasks.md`**,然後決定從 Phase 0.6 還是 0.7 開始。
0.6host functions)和 0.7WASM runner)是並列的前置工作,哪個先都可以,但都要在 Phase 1 開始之前完成。
---
## SDD 索引
| 子系統 | SDD |
|--------|-----|
| **主要(正在動)** Credential Primitives WASM 改寫 | `.agents/specs/arcrun/credential-primitives-wasm/` |
| arcrun 總進度 | `.agents/specs/arcrun/arcrun.md` |
| Auth Recipe 系統(schema、預建 20 個服務) | `.agents/specs/arcrun/auth-recipe.md` |
| Landing Page | `.agents/specs/arcrun/landing-page.md` |
| SDK + Website | `.agents/specs/arcrun/sdk-and-website/design.md` |
| arcrun MVP 整體 | `.agents/specs/u6u-core-mvp/design.md` |
| Credential 長期規格(需求源) | `docs/user_requirements/credential_parts.md` |
| u6u Platform Evolution | `.agents/specs/u6u-platform-evolution/design.md` |
| Tech Stack 詳細 | `.agents/steerings/tech.md` |
---
## 技術備註(CC 常搞錯的點)
1. **每個 WASM 零件 = 獨立 Worker = 公開 URL**(例:`gmail.arcrun.dev`)。不是從 R2 動態讀。
2. **Cypher binding = YAML 裡寫 URL 清單**。不是 Cloudflare service binding。
3. **cypher-executor 只做 routing + host functions**。業務邏輯全在 WASM 零件。
4. **TinyGo 有限制**`crypto/rsa` 支援不全 → 用 host function `crypto_sign_rs256` 讓 Worker 代簽。
5. 詳見 `.claude/rules/03-component-architecture.md`
+61
View File
@@ -0,0 +1,61 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/session-start-load-sdd.sh",
"timeout": 5
}
]
}
],
"PreToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/pre-write-guard.sh",
"timeout": 5
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/pre-bash-guard.sh",
"timeout": 5
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/post-edit-remind-tasks.sh",
"timeout": 5
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/stop-check-sync.sh",
"timeout": 5
}
]
}
]
}
}
+70
View File
@@ -0,0 +1,70 @@
# CLAUDE.md — arcrun
> 本檔是**索引 + 最高原則**,詳細規範拆到 `.claude/rules/`。
> Hook 強制機制在 `.claude/hooks/`,違反會直接 blockexit 2)。
---
## 絕對鐵律(違反 = 停手)
1. **任何 code 變動前必須先讀對應 SDD**,在回覆開頭宣告已讀清單與對應 task 編號(格式見 `.claude/rules/00-sdd-protocol.md`
2. **零件只能用 TinyGo 或 AssemblyScript 編譯成 WASM**`registry/components/` 下禁止 TypeScript
3. **cypher-executor TS 禁止實作 credential / auth / JWT / template 展開業務邏輯**;這些全在 WASM 零件
4. **Cypher binding = YAML 裡的 URL 清單**,不是 Cloudflare service binding;零件串接走 HTTP URL
5. **每個 WASM 零件 = 獨立 Worker = 公開 URL**;不從 R2 動態讀(R2 只 Phase 5 啟用)
6. **修改現有程式碼,不是新建資料夾重做**
7. **每完成一個 task 立刻更新 tasks.md 的 `[x]`**,不批次
---
## 工作流程(強制)
開始任一任務,按順序:
1.`.agents/specs/arcrun/arcrun.md`(總進度)
2. 讀對應的 SDD `design.md` + `tasks.md`
3. 在回覆開頭貼出:
```
📋 已讀 SDD<清單>
🎯 本次對應 task<編號>
📐 本次 task 的 SDD 規範摘要:<重點>
🚧 執行範圍:修改/建立/刪除 <檔案>
```
4. 動手前把 tasks.md 對應 task 標為 `[🔄]`,完成後標 `[x]`
5. 完成後確認:是否需要同步更新 design.md?
找不到對應 SDD → **停手問 richblack**,不要自行建立。
---
## 詳細規範索引
| 檔案 | 內容 |
|-----|------|
| `.claude/rules/00-sdd-protocol.md` | SDD 讀取協議(強制流程) |
| `.claude/rules/01-tech-stack.md` | 技術棧硬限制(語言/儲存/加解密) |
| `.claude/rules/02-forbidden.md` | 禁止清單(hook 強制執行) |
| `.claude/rules/03-component-architecture.md` | 零件架構(R2 用途 / cypher binding / service binding 邊界) |
| `.claude/rules/04-current-progress.md` | 當前進度 + SDD 索引 |
---
## SDD 位置速查
| 子系統 | 路徑 |
|-------|------|
| **進行中** Credential Primitives WASM | `.agents/specs/arcrun/credential-primitives-wasm/` |
| arcrun 總進度 | `.agents/specs/arcrun/arcrun.md` |
| Auth Recipe 系統 | `.agents/specs/arcrun/auth-recipe.md` |
| Landing Page | `.agents/specs/arcrun/landing-page.md` |
| SDK + Website | `.agents/specs/arcrun/sdk-and-website/` |
| arcrun MVP 整體 | `.agents/specs/u6u-core-mvp/` |
| u6u Platform Evolution | `.agents/specs/u6u-platform-evolution/` |
| Credential 長期規格(需求源) | `docs/user_requirements/credential_parts.md` |
| Tech Stack 詳細 | `.agents/steerings/tech.md` |
---
## 封測狀態
**推遲**richblack 2026-04-19 決定)。先完成 Phase 1-3 清除違規 TS,再啟動封測。
-2805
View File
File diff suppressed because it is too large Load Diff
-21
View File
@@ -1,21 +0,0 @@
{
"name": "arcrun-credentials",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"hono": "^4.7.0"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.14.2",
"@cloudflare/workers-types": "^4.20250219.0",
"fast-check": "^4.6.0",
"typescript": "^5.7.0",
"vitest": "^4.1.4"
}
}
-1648
View File
File diff suppressed because it is too large Load Diff
@@ -1,37 +0,0 @@
// createCredential:儲存 credentialname + secret + type),加密後存入 KV
import type { Context } from 'hono';
import type { Bindings, CredentialRecord } from '../types';
import { encrypt } from './crypto';
function slugify(name: string): string {
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || `cred-${Date.now()}`;
}
export async function handleCreateCredential(c: Context<{ Bindings: Bindings }>) {
const body = await c.req.json().catch(() => null);
if (!body) return c.json({ success: false, error: '無效的 JSON body' }, 400);
const { name, secret, type } = body;
if (!name) return c.json({ success: false, error: 'name 必填' }, 400);
if (!secret) return c.json({ success: false, error: 'secret 必填' }, 400);
if (!type) return c.json({ success: false, error: 'type 必填(例: api_key, bearer_token, google_oauth' }, 400);
// 使用 ENCRYPTION_KEY secret;若未設定,使用 fallback(開發環境)
const hexKey = c.env.ENCRYPTION_KEY || '0'.repeat(64);
const { encrypted, iv } = await encrypt(String(secret), hexKey);
const id = slugify(String(name));
const record: CredentialRecord = {
id,
name: String(name),
type: String(type),
encrypted_secret: encrypted,
iv,
created_at: Date.now(),
};
await c.env.CREDENTIALS_KV.put(`cred:${id}`, JSON.stringify(record));
return c.json({ success: true, data: { id, name: record.name, type: record.type } }, 201);
}
-28
View File
@@ -1,28 +0,0 @@
// cryptoAES-GCM 加解密工具(Web Crypto API
/** 從 hex 字串匯入 AES-GCM key */
async function importKey(hexKey: string): Promise<CryptoKey> {
const raw = new Uint8Array(hexKey.match(/.{1,2}/g)!.map(b => parseInt(b, 16)));
return crypto.subtle.importKey('raw', raw, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
}
/** 加密 plaintext,回傳 { encrypted, iv }(均為 base64 */
export async function encrypt(plaintext: string, hexKey: string): Promise<{ encrypted: string; iv: string }> {
const key = await importKey(hexKey);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(plaintext);
const cipherBuf = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
return {
encrypted: btoa(String.fromCharCode(...new Uint8Array(cipherBuf))),
iv: btoa(String.fromCharCode(...iv)),
};
}
/** 解密,回傳 plaintext */
export async function decrypt(encrypted: string, iv: string, hexKey: string): Promise<string> {
const key = await importKey(hexKey);
const ivBuf = Uint8Array.from(atob(iv), c => c.charCodeAt(0));
const cipherBuf = Uint8Array.from(atob(encrypted), c => c.charCodeAt(0));
const plainBuf = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: ivBuf }, key, cipherBuf);
return new TextDecoder().decode(plainBuf);
}
@@ -1,16 +0,0 @@
// deleteCredential:刪除指定 credentialby name or id
import type { Context } from 'hono';
import type { Bindings } from '../types';
export async function handleDeleteCredential(c: Context<{ Bindings: Bindings }>) {
const name = c.req.param('name');
const key = `cred:${name}`;
const existing = await c.env.CREDENTIALS_KV.get(key);
if (!existing) {
return c.json({ success: false, error: `找不到 credential: ${name}` }, 404);
}
await c.env.CREDENTIALS_KV.delete(key);
return c.json({ success: true, data: { deleted: name } });
}
@@ -1,21 +0,0 @@
// getCredentialSecret:解密並回傳 secret(內部使用,Cypher Executor inject 用)
// 此端點只接受內部呼叫(需 Authorization: Bearer <INTERNAL_TOKEN>
import type { Context } from 'hono';
import type { Bindings, CredentialRecord } from '../types';
import { decrypt } from './crypto';
export async function handleGetSecret(c: Context<{ Bindings: Bindings }>) {
const name = c.req.param('name');
const raw = await c.env.CREDENTIALS_KV.get(`cred:${name}`);
if (!raw) return c.json({ success: false, error: `找不到 credential: ${name}` }, 404);
const record = JSON.parse(raw) as CredentialRecord;
const hexKey = c.env.ENCRYPTION_KEY || '0'.repeat(64);
try {
const secret = await decrypt(record.encrypted_secret, record.iv, hexKey);
return c.json({ success: true, data: { id: record.id, type: record.type, secret } });
} catch {
return c.json({ success: false, error: '解密失敗,請確認 ENCRYPTION_KEY 是否正確' }, 500);
}
}
@@ -1,18 +0,0 @@
// listCredentials:列出所有 credential(只回傳 id/name/type,不含 secret
import type { Context } from 'hono';
import type { Bindings, CredentialRecord, CredentialSummary } from '../types';
export async function handleListCredentials(c: Context<{ Bindings: Bindings }>) {
const { keys } = await c.env.CREDENTIALS_KV.list({ prefix: 'cred:' });
const summaries: CredentialSummary[] = [];
for (const key of keys) {
const raw = await c.env.CREDENTIALS_KV.get(key.name);
if (!raw) continue;
const record = JSON.parse(raw) as CredentialRecord;
summaries.push({ id: record.id, name: record.name, type: record.type, created_at: record.created_at });
}
summaries.sort((a, b) => b.created_at - a.created_at);
return c.json({ success: true, data: { credentials: summaries, count: summaries.length } });
}
-26
View File
@@ -1,26 +0,0 @@
// u6u-credentials Worker — Credential 儲存與注入
// index.ts 只做路由宣告,業務邏輯在 actions/INV Layer 1
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import type { Bindings } from './types';
import { handleCreateCredential } from './actions/createCredential';
import { handleListCredentials } from './actions/listCredentials';
import { handleDeleteCredential } from './actions/deleteCredential';
import { handleGetSecret } from './actions/getCredentialSecret';
const app = new Hono<{ Bindings: Bindings }>();
app.use('*', cors());
// Health check
app.get('/', c => c.json({ service: 'u6u-credentials', version: '1.0.0', status: 'ok' }));
// POST /credentials — 建立 credential(加密存入 KV
// GET /credentials — 列出所有 credential(不含 secret
// DELETE /credentials/:name — 刪除 credential
// GET /credentials/:name/secret — 取得解密 secretCypher Executor inject 用)
app.post ('/credentials', handleCreateCredential);
app.get ('/credentials', handleListCredentials);
app.delete('/credentials/:name', handleDeleteCredential);
app.get ('/credentials/:name/secret', handleGetSecret);
export default app;
-24
View File
@@ -1,24 +0,0 @@
// u6u-credentials Worker 型別定義
export type Bindings = {
CREDENTIALS_KV: KVNamespace;
ENCRYPTION_KEY: string; // hex-encoded 256-bit AES keywrangler secret
ENVIRONMENT: string;
};
export interface CredentialRecord {
id: string; // 用 name slugify 生成
name: string; // 用戶命名(human-readable
type: string; // api_key / bearer_token / google_oauth / telegram_bot_token / ...
encrypted_secret: string; // AES-GCM base64 encrypted
iv: string; // base64 IV
created_at: number;
}
// 對外回傳(不含 secret
export interface CredentialSummary {
id: string;
name: string;
type: string;
created_at: number;
}
@@ -1,79 +0,0 @@
// Preservation Tests — AES-GCM Credential Round-Trip
// Task 2: 確認基線行為(修復前執行,預期通過)
//
// **Validates: Requirements 3.9**
import { SELF } from 'cloudflare:test';
import { describe, it, expect } from 'vitest';
import * as fc from 'fast-check';
// ─────────────────────────────────────────────────────────────────────────────
// Property: Credential round-trip
//
// For all non-zero credential name/secret pairs,
// POST /credentials → GET /credentials/:name/secret returns the same secret.
// This validates AES-GCM encrypt → decrypt round-trip correctness.
//
// **Validates: Requirements 3.9**
// ─────────────────────────────────────────────────────────────────────────────
describe('Preservation: AES-GCM credential round-trip', () => {
it('property: POST /credentials then GET /credentials/:name/secret returns original secret', async () => {
// Generate non-empty name and secret pairs
const nameArb = fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0);
const secretArb = fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.length > 0);
await fc.assert(
fc.asyncProperty(nameArb, secretArb, async (name, secret) => {
// Use a unique suffix to avoid collisions between runs
const uniqueName = `${name}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
// POST /credentials — store encrypted credential
const createRes = await SELF.fetch('http://localhost/credentials', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: uniqueName, secret, type: 'api_key' }),
});
expect(createRes.status).toBe(201);
const created = await createRes.json() as Record<string, unknown>;
expect(created.success).toBe(true);
const credId = (created.data as Record<string, unknown>).id as string;
// GET /credentials/:name/secret — retrieve and decrypt
const getRes = await SELF.fetch(`http://localhost/credentials/${credId}/secret`);
expect(getRes.status).toBe(200);
const retrieved = await getRes.json() as Record<string, unknown>;
expect(retrieved.success).toBe(true);
// The decrypted secret must equal the original
const retrievedSecret = (retrieved.data as Record<string, unknown>).secret as string;
expect(retrievedSecret).toBe(secret);
}),
{ numRuns: 5 }
);
});
it('example: specific name/secret round-trip preserves secret', async () => {
const name = 'preservation-test-key';
const secret = 'my-super-secret-api-key-12345';
const createRes = await SELF.fetch('http://localhost/credentials', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, secret, type: 'bearer_token' }),
});
expect(createRes.status).toBe(201);
const created = await createRes.json() as Record<string, unknown>;
const credId = (created.data as Record<string, unknown>).id as string;
const getRes = await SELF.fetch(`http://localhost/credentials/${credId}/secret`);
expect(getRes.status).toBe(200);
const retrieved = await getRes.json() as Record<string, unknown>;
expect((retrieved.data as Record<string, unknown>).secret).toBe(secret);
});
});
-12
View File
@@ -1,12 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"types": ["@cloudflare/workers-types"],
"strict": true,
"noEmit": true
},
"include": ["src/**/*"]
}
-10
View File
@@ -1,10 +0,0 @@
import { cloudflareTest } from '@cloudflare/vitest-pool-workers';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
cloudflareTest({
wrangler: { configPath: './wrangler.toml' },
}),
],
});
-12
View File
@@ -1,12 +0,0 @@
name = "arcrun-credentials"
main = "src/index.ts"
compatibility_date = "2025-02-19"
compatibility_flags = ["nodejs_compat"]
[[kv_namespaces]]
binding = "CREDENTIALS_KV"
id = "e7f4320f88d343f187e35e3543dd74c9"
[vars]
ENVIRONMENT = "production"
# ENCRYPTION_KEY 透過 wrangler secret set 設定