diff --git a/.claude/commands/issue-handle.md b/.claude/commands/issue-handle.md new file mode 100644 index 0000000..4023c6f --- /dev/null +++ b/.claude/commands/issue-handle.md @@ -0,0 +1,80 @@ +--- +description: 處理本 repo 的 GitHub issue(讀/回/結案),跨 repo 發要先問人 +--- + +# /issue-handle — GitHub issue 處理指引 + +你(CC)可以、也該主動用內建的 `gh` CLI 讀寫**自己 repo** 的 GitHub issue。 +很多人不知道這件事——`gh` 已內建認證,零開發、零外部依賴。issue 同源於 repo, +比 Notion / Sheets 更適合做交辦與待辦,不必引入外部 SaaS。 + +這份指引分四層,界線要守住。 + +--- + +## 1. 讀 / 回 / 結案(普世基本功 — 直接做,不用問) + +對**自己這個 repo**,主動處理 open issue: + +```bash +gh issue list --state open # 看有哪些待辦 +gh issue view # 讀完整內容 +# …實作… +gh issue comment --body "[<本 repo> CC] 做了什麼、怎麼決定的、改了哪些檔" +gh issue close # 確認解決後結案 +``` + +回覆要有料:說清楚**做了什麼、為什麼這樣決定、動了哪些檔**,而不是只回「done」。 +issue 作者(可能是另一個 repo 的 CC,或人類)要靠你的回覆判斷對不對。 +跨 repo 的 issue/comment 開頭一律署名 `[<本 repo> CC]`(見第 3 節鐵律)。 + +--- + +## 2. 發 issue 給「別的 repo」(要先問人 — 不可擅自) + +當你發現**別的 repo** 有值得修正的地方時: + +> ❌ 不要擅自 `gh issue create -R other/repo …` +> ✅ 先問人類:「我發現 X repo 有 Y 問題,要我幫你去那邊發 issue 嗎?」得到同意才發。 + +理由:通用 template 不知道使用者對那個 repo 有沒有權限、想不想發。留一道人類確認最安全。 +(對**自己 repo** 開 issue 記待辦則可直接做——那是自己的 repo。) + +--- + +## 3. 跨 repo 署名(鐵律 — 絕不可漏) + +所有 repo(mira / graph-plugin / ingest-plugin / Arcrun / template…)共用**同一個 GitHub 帳號**發 issue/comment, +所以 issue/comment 的 author **全顯示同一個帳號、看不出是哪個 repo 的 CC 發的**。 + +> **跨 repo 的 issue/comment 一律在開頭署名 `[<本 repo> CC]`**,靠內容署名溯源。 + +- 收件方 CC 回報:`[graph-plugin CC]` / `[mira CC]` / `[ingest CC]` / `[arcrun CC]`… +- 總管下令/追問:`[InkStoneCo 總管]` +- 署名放 comment **第一行或標題式開頭**(既有的「## 回報(graph CC)」即合格)。 + +為什麼只能這樣:GitHub issue/comment 的 author = 發送帳號,**沒有 per-repo 身份這設定**; +`git config user.name` 只影響 commit 作者、不影響 issue/comment author; +給每個 repo 開獨立帳號 = 多帳號自動化 = 踩下方第 4 節 flag 鐵律,**不可**。 +身份只能在**內容層自報**。本 repo 名稱 → 看 `git remote -v` 或 repo 根目錄名。 + +--- + +## 4. flag 安全界線(最重要 — 絕不可越) + +**「有事才讀」,禁止自動輪詢。** + +- ✅ 想看 issue → 當下主動 `gh issue list`。 +- 🚫 **禁止**掛 GitHub Actions / cron / webhook 去**自動輪詢** issue。 + +為什麼這條是硬底線:自動輪詢 + 事件 fan-out 正是會觸發 GitHub 異常偵測、 +害你的 token 被 rate limit 砍掉的流量模式。template 給很多人用,這條界線必須守住, +保護使用者不踩雷。需要「定期檢查」就由人類主動跑這個指令,不要自動化。 + +--- + +## (可選)用 label 分來源 + +若想用同一套流程同時管「內部交辦」與「外部回報」,可建兩個 label: +`internal`(協作/交辦)、`user`(外部使用者回報),靠 label 分流。 +這對通用 template 不預設——你的 repo 需要再建。 diff --git a/.claude/commands/sdd-check.md b/.claude/commands/sdd-check.md new file mode 100644 index 0000000..fb4bfde --- /dev/null +++ b/.claude/commands/sdd-check.md @@ -0,0 +1,55 @@ +# /sdd-check — 確認當前任務有沒有對應 SDD + +動手前執行。確保 CC 有全局觀,不會在沒有設計文件的情況下猛衝。 + +--- + +## 執行流程 + +### 第一步:理解任務 + +確認使用者要做什麼: +- 涉及哪個子系統? +- 是新功能還是修改現有功能? +- 影響範圍? + +### 第二步:尋找對應 SDD + +在 `system-dev/docs/3-specs/` 下尋找對應的子系統目錄,確認有沒有: +- `design.md`(設計文件) +- `tasks.md`(任務清單) + +### 第三步:根據結果回應 + +**情況 A:找到對應 SDD** +``` +✅ 找到 SDD:system-dev/docs/3-specs/[子系統]/ +📋 design.md:[確認] +📋 tasks.md:[確認,列出相關 task] +🎯 對應 task:[編號和描述] +繼續嗎? +``` + +**情況 B:找不到 SDD,任務明確** +``` +⚠️ 找不到對應 SDD +任務:[描述] +建議在 system-dev/docs/3-specs/[建議子系統名]/ 建立 SDD + +要我幫你起草 design.md 嗎?(需要你確認後才動手) +``` + +**情況 C:找不到 SDD,任務模糊** +``` +⚠️ 找不到對應 SDD,而且任務範圍不夠清楚 +請先回答: +1. 這個功能屬於哪個子系統? +2. 完成的標準是什麼? +3. 有沒有不能動的邊界? +``` + +### 注意 + +- 找不到 SDD **不等於可以直接動手** +- 小修改(修 bug、改文字)可以豁免,但要明確說「這是小修改,範圍是 X」 +- 新功能、架構變動、跨模組的修改 → 一定要有 SDD diff --git a/.claude/commands/wiki-capture.md b/.claude/commands/wiki-capture.md new file mode 100644 index 0000000..d10e13b --- /dev/null +++ b/.claude/commands/wiki-capture.md @@ -0,0 +1,69 @@ +# /wiki-capture — 把對話結論存進 wiki + +把這次對話中產生的決策、誤解釐清、或重要結論存入 wiki。 +解決「討論過了但知識消失」的問題。 + +--- + +## 執行流程 + +### 第零步:機敏檢查(寫入前一律先過) + +把任何內容寫進 wiki 前,先確認**不含**密碼 / API 金鑰 / 私鑰 / 連線字串帳密 / 個資(身分證、信用卡)。 +- 命中 → 不要記「值」,改記「位置」(例:「DB 密碼放 `.env`,不入 wiki」) +- 來源整檔機敏 → 提醒使用者加進 `system-dev/wiki/.wikiignore` +- 真要保留示範格式 → 該行尾加 `wiki-secret-ok` 標記 +> 這是協議層自律。最後一道 `wiki-secret-scan.sh` hook 會在寫入 `system-dev/wiki/` 時機械攔截,但別依賴它兜底——當場就不要把機敏值帶進來。 + +### 第一步:辨識對話中的可記錄內容 + +掃描當前對話,找出: + +| 類型 | 判斷標準 | 存到哪 | +|------|---------|-------| +| 架構決策 | 「為什麼選A不選B」「我們決定用X」 | `decisions-summary.md` + `system-dev/docs/2-architecture/decisions/` | +| CC 的誤解被糾正 | CC 說了某件事,使用者說「不是,是...」 | `mistakes.md` | +| 重要狀態更新 | 完成了某件事、阻擋了某件事 | `status.md` | +| 技術發現 | 踩到坑、找到解法、重要行為確認 | `mistakes.md` 或對應 SDD | + +### 第二步:列出清單給使用者確認 + +格式: +``` +這次對話我整理了以下內容要存入 wiki: + +1. [MISTAKE] CC 誤解了 X,正確是 Y +2. [DECISION] 決定用 A 不用 B,原因是 C +3. [STATUS] 完成了 task 2.3,下一步是 2.4 + +確認後存入,有需要修改的嗎? +``` + +**停下來等確認。** + +### 第三步:寫入 + +確認後,依照格式寫入對應檔案: + +**mistakes.md 格式:** +``` +⚠️ MISTAKE: [錯誤描述] + 症狀: [CC 的表現] + 正確做法: [應該怎麼做] + 原因: [背景] + 日期: [YYYY-MM-DD] +``` + +**decisions-summary.md 格式:** +``` +## [主題] — [YYYY-MM-DD] +**結論**:[一句話] +**原因**:[簡短說明] +**詳細**:system-dev/docs/2-architecture/decisions/[檔名] +``` + +重大決策同時在 `system-dev/docs/2-architecture/decisions/` 建立 ADR 檔案。 + +### 第四步:確認 + +告知存到哪些檔案,共幾條記錄。 diff --git a/.claude/commands/wiki-init.md b/.claude/commands/wiki-init.md new file mode 100644 index 0000000..c9b0ef6 --- /dev/null +++ b/.claude/commands/wiki-init.md @@ -0,0 +1,230 @@ +# /wiki-init — 初始化或接入 LLM Wiki 系統 + +初始化這個專案的 LLM Wiki 記憶系統。 +新專案建立空白結構,已有專案掃描現有文件並**改寫**成 wiki。 + +--- + +## 核心概念:wiki 是 AI 改寫過的記憶,不是原文索引 + +記憶系統的目的,是讓 AI **之後讀得快**。但人類寫的原始文件——不管是 vault 的隨手記、開發專案的會議記錄、規格草稿、散落的 `.md`——天生是亂的:重複、流水帳、半成品、口語。 + +如果 wiki 只是一份 `[[原文檔名]]` 指回原文的**索引**,那每次未來要用都得重新解析那團亂,等於沒省到。**wiki 的價值在於「改寫一次,之後每次讀都便宜」**。 + +所以原則對**所有專案**一致(不分 vault 或一般開發): + +> 人類寫的原文是 **SSoT**(真理來源,永遠唯讀)。 +> 但實際要長期保存、被 AI 反覆讀的是 **AI 改寫整理過的 wiki**。 +> **AI 是總編輯**——把原文改寫成自包含、概念原子化、互相連結、適於 AI 讀的知識條目。 + +唯一例外:原文是**不可改動的正式文件**(簽署過的規格、法規、合約),必須逐字讀原文——這種才在 wiki 裡用指針指回去,並註明「逐字依原文」。除此之外,一律改寫。 + +**raw source 永遠唯讀**:所有產出只往 `system-dev/wiki/` 寫,絕不改動、搬移、重新命名原文。 + +--- + +## 執行流程 + +### 第一步:偵測專案狀態 + +檢查以下項目,判斷是新專案還是已有專案: +- 根目錄有沒有 `system-dev/wiki/` +- 根目錄有沒有 `docs/`(或 vault 的 `pages/`、`journals/`、根目錄 `.md`) +- 有沒有散落的 `.md` 檔案 + +同時**偵測 raw source 路徑**(同 install.sh 邏輯): +- 根目錄有 `logseq/` → Logseq vault,raw source = `pages/` + `journals/` +- 根目錄有 `.obsidian/` → Obsidian vault,raw source = 根目錄所有 `.md` +- 都沒有 → 一般專案,raw source = `docs/` 下所有 `.md`(及散落的 `.md`) + +**新專案**(幾乎空的)→ 直接建立結構,跳到第三步 +**已有專案**(有文件)→ 執行第二步 + +### 第二步:已有專案的掃描(已有專案才執行) + +1. 遞迴找出 raw source 裡所有 `.md` 檔案 +2. **先套用 `system-dev/wiki/.wikiignore`**:命中 pattern 的檔案整個排除,不讀不編入。 + - 若 `.wikiignore` 不存在,從範本建立一份(預設排除 `.env`/`*.pem`/`*secret*` 等) + - 被排除的檔案在清單裡標「🚫 .wikiignore 排除」,**不可被覆蓋** +3. 對其餘檔案標注**改寫計畫**:會萃取成哪些 wiki 條目。一份原文可能拆成多個概念原子條目,多份相關原文也可能合併成一條。 +4. 列出清單給使用者確認,**停下來等確認** + +> **量大時建議用 Haiku 改寫**:逐份原文「改寫成 wiki 格式」是重複、機械、判斷成本低的工作——正適合 Haiku。原文數量多(如數十、上百份)時,主動建議: +> 「共 N 份原文要改寫,這類逐份萃取很適合用 Haiku 並行處理(便宜、夠快)。要我派 Haiku subagent 改寫嗎?」 +> 得同意後,用 Task / subagent 把每份原文(或每批)丟給 Haiku 改寫,主模型只負責切分概念、定條目邊界、最後審稿與互連。 + +> 機敏防護(三層): +> - **L1 .wikiignore**:整檔排除(這一步) +> - **L2 行內標記**:檔案要編入但某段不要 → 遇到 `` … `` 之間的內容**略過**,只留「(此處機敏,已略過)」 +> - **L3 hook**:萬一機敏值仍被寫進 wiki,`wiki-secret-scan.sh` 會 exit 2 擋下 +> 編入任何檔案前,先檢查是否含密碼/金鑰/個資——有就改記「位置」而非「值」。 + +### 第三步:建立缺少的結構 + +只建立不存在的目錄和檔案,**已有的一律不動**。 + +wiki 採**三層 + 標籤橫切**架構(183 卡實證,issue #8): + +``` +system-dev/wiki/ +├── INDEX.md ← 索引:多角度視圖的家(標籤角度、決策角度、…) +├── TAXONOMY.md ← 標籤字典(cards 的分類元資料,受控擴充) +├── status.md ← [push] 時態狀態:當前進度、下一步 +├── mistakes.md ← [push] 踩過的坑、被糾正的誤解(防不自覺盲區) +├── principles.md ← [push] 跨全局的設計原則(行動前必服從) +└── cards/ ← [pull] 一切知識內容:原文摘要、AI 筆記、決策、概念… + └── / ← 儲存桶(分類由 frontmatter 標籤承載) + ├── 00-INDEX.md ← 桶子索引(固定名,容器:只連不重寫,H2/H3 分節) + └── <概念全名>.md ← 概念原子卡(一概念一檔,自包含) +``` + +> **[push] / [pull] 是這套 wiki 的核心判準——因為 wiki 主要是給 AI(CC)看的。** +> 見下方「核心判準:push vs pull」。`decisions-summary.md` 已**降級為 cards + INDEX 決策視圖**(決策是知識內容=card);既有的 decisions-summary 若存在,保留為相容,不刪。 + +關鍵原則:**資料夾只是儲存桶,分類由 frontmatter 標籤承載**。資料夾名不該硬繼承原稿目錄——原稿目錄是「人為了整理草稿」分的,wiki 連分類都該由 AI 重新組織。 + +> **桶子索引固定叫 `00-INDEX.md`**(issue #6):`00-` 前綴讓它排序最前、一眼可辨(像 README 之於資料夾),AI 載入任何 `cards//` 一律先讀它,不必猜。檔內 H1 仍寫主題名(如 `# PKM 知識管理`),語意不丟。 + +一般專案仍可同時建 `system-dev/docs/` 分類樹(SDD 等): +``` +system-dev/docs/{1-vision,2-architecture/decisions,3-specs,4-guides,5-records/{incidents,test-reports},6-user} +``` +(純 PKM vault 不需要 `system-dev/docs/` 分類樹時,只建 `system-dev/wiki/`。) + +檔案(不存在才建): +- `system-dev/wiki/INDEX.md`、`TAXONOMY.md` +- `system-dev/wiki/status.md`、`mistakes.md`、`principles.md`(三個 push 檔) +- `system-dev/docs/README.md`(一般專案才需要) + +--- + +## 核心判準:push vs pull(wiki 是給 AI 看的) + +整理任何內容前,先判斷它該 **push** 還 **pull**——判準是「**CC 做事時會不會被動看見**」: + +- **push**:CC 行動前必須主動出現在 context(session 開始就由 hook 注入)。給「CC 不會主動去查、但不看就出事」的東西。 +- **pull**:CC 想到要查、或載入相關卡時才看見。給「CC 面對它時自然會查」的知識。 + +**為什麼這是核心**:mistakes 防的是 CC「不自覺的盲區」——一個你不知道存在的錯,你不會主動去檢索它。靠 CC 自覺去查自己沒自覺的盲區是自相矛盾的,所以 pull 對盲區失效,**必須 push**。原則同理:沒被推到眼前的準繩,CC 設計時很可能沒想到要服從就做了。 + +| 內容 | push/pull | 注入形態(hook)| +|------|-----------|----------------| +| **status** | push | **全文**——CC 必須知道精確的下一步,摘要會漏 task 編號 | +| **principles** | push | **全文(一行一條)**——短而硬的約束,漏一條就違反;≤15 條,超過代表該下放成 card | +| **mistakes** | push | **標題清單 + 一行症狀**,全文按需 pull——量可能大,摘要足以觸發「我正撞到某條」的認出 | +| **decisions、原文摘要、概念知識、一切其餘** | pull | 寫成 cards;CC 面對時自然會查,INDEX 提供角度入口 | + +**principles 維護規則**:一行一條精煉準繩(如「不污染用戶根目錄」「目標用戶 low-code」「wiki 主要給 AI 看」)。發現新的跨全局原則 → append 一行;超過 ~15 條代表某些該合併或下放成 card。**累積原則只改 principles.md,不必問用戶開新檔。** + +### 第四步:訪談(每次一個問題) + +依序問: +1. 這個專案做什麼?(一句話) +2. 有哪些絕對不能違反的限制?(技術棧、架構原則等) +3. 現在進行到哪個階段? +4. 有沒有 CC 曾經犯過的錯要先記下來? + +把答案填進 `CLAUDE.md`(如果存在)或建立新的。 + +### 第五步:改寫成 wiki(AI 當總編輯) + +(第二步確認後執行) + +**不搬動原文**。逐份讀 raw source,改寫萃取成 `cards//` 裡的自包含原子卡: + +- **概念原子化**:一張卡講一個概念,不是一篇原文對一張卡。原文太雜就拆,多份相關原文就合。 +- **自包含**:讀卡就懂,不必回去翻原文。把口語、重複、流水帳改寫成結構化知識,**不寫「詳見原文」**。 +- **保留來源指針**:每卡標 `**來源**:原文相對路徑`,為可追溯,不是要使用者回去讀。 +- **frontmatter 標籤分類**(見下方):分類走 frontmatter `tags:`,不靠資料夾、不靠行內 `#tag`。 +- **互相連結(typed-edge 三元組)**:`## 關聯` 不只列裸 `[[頁面]]`,改寫成帶語義的三元組(見下方)。 +- **萃 gloss(node 一句說明)**:frontmatter 放 `gloss:` —— 這張卡(= 一個 entity / graph node)的一句話定義,供下游語義 normalize(見下方)。 + +卡片格式(每張卡): +```markdown +--- +tags: [知識管理, AI協作, 方法論] +gloss: 一句話定義這個概念是什麼(給下游語義 normalize 用,選填、deep tier 才產) +--- +# 概念全名 + +← [[/00-INDEX]] + +**來源**:`[raw source 相對路徑]` +**最後更新**:YYYY-MM-DD + +## 摘要 +[一句話核心] + +## 重點 +- [自包含改寫的要點,不依賴原文] + +## 實體 +> 本卡內文的關鍵實體(也是 graph node)。名+描述供下游 embedding normalize。集中放、一行一個、不縮排、不重複。 +- **原子筆記**(atomic note/卡片原子化)— 每張卡只承載一個不可再分論點的知識記錄單元。 +- **傳統筆記**(大鍋炒筆記)— 把多主題混雜在同一篇、難精確引用的記錄方式。 + +## 關聯 +### 內文知識關係(內文實體間;端點=上方 `## 實體` 正規名,一字不差) +- 原子筆記 >> 對立於 >> 傳統筆記 +### 卡片關係(卡對卡) +- [[本卡]] >> 謂詞(動詞短語) >> [[他卡]] +``` + +**麵包屑用帶路徑 wikilink**(issue #7):H1 次行放 `← [[/00-INDEX]]` 指回桶子索引。 +桶子索引固定名 `00-INDEX` 跨桶會撞名,故**指 00-INDEX 一律帶路徑**(`[[pkm/00-INDEX]]`,Logseq 原生支援、下游 ingest 也能對應到具體檔)。普通卡片間連結仍用裸 `[[卡名]]`(卡名唯一,不需路徑)。 + +**frontmatter 標籤分類**(issue #8): +- **用 frontmatter `tags:` 而非行內 `#tag`**:卡片內文常大量用 `#`(講筆記法時的 `#猜想`、`#book100`),分類標籤若也行內 `#`,下游 ingest 無法區分「分類」與「內文範例」會污染 graph。frontmatter 與內文完全分開,零歧義。 +- **用標籤而非資料夾分類**:資料夾=強制單一歸屬;標籤=多重歸屬。一張卡可同時屬知識管理+AI協作+架構設計,硬塞一個資料夾會在其他檢索角度漏掉。 +- **雙軸 taxonomy**(寫進 `TAXONOMY.md` 當字典;**受控擴充**,非凍結): + - 領域(主軸,1-3 個):如 知識管理/學習認知/AI協作/生產力/系統設計/工具教學 + - 形態(副軸,0-2 個):方法論/工具實作/觀點主張/架構設計/案例經驗 + - 一般開發專案的軸可不同(如 子系統/層級/決策類型),由 AI 依專案性質提出、寫進 TAXONOMY.md。 + - **遇到現有軸裝不下的內容**:先查是否只是現有標籤的同義詞;確實是新軸才加進 TAXONOMY.md(附定義)再用——**禁止繞過字典在卡片直接冒新標籤**。字典是 per-repo,跨 repo 不必共用。 + +**typed-edge 規則**(issue #5/#11,把「關係」也預編譯,下游 ingest 直接 parse 出帶類型的有向邊): +- **重點抓內文實體關係,不只卡對卡**:卡對卡(`[[卡A]] >> 謂詞 >> [[卡B]]`)只是既有雙鏈加動詞、資訊量幾乎沒增加;價值在內文概念關係(`原子筆記 >> 對立於 >> 傳統筆記`,A/B 是內文概念非卡標題)。 +1. **方向性**:`A >> 謂詞 >> B` 必須讀成「A(謂詞)B」一句通順的話;A、B 順序就是主→賓真實方向。 +2. **謂詞用動詞 / 動詞短語**(反駁、奠基於、犧牲)。**禁名詞當謂詞**——`>> 存儲格式 >>`、`>> 操作體驗 >>` 讀不通,是錯的。 +3. **謂詞自由但別太天馬行空**:「參考/參照」皆可(下游 embed 自動聚類),別寫「瞄了一眼」這種抓不到同義的。 +4. **內文三元組端點用裸文字**(非 `[[wikilink]]`),避免 Logseq 紅色斷鏈;卡對卡那層才用 `[[]]`。 +5. **向後相容**:純 `[[A]]` 仍合法(視為無類型邊),盡量補謂詞。 + +> **★ 硬自檢(Haiku 量產必備)★** 內文三元組端點必須與 `## 實體` 某粗體正規名【一字不差】。**寫完逐條把 A、B 拿去 `## 實體` 比對**,沒有完全相同的 → 這條錯了,改用實體表已有的詞、或把端點補進 `## 實體` 再指它。禁止端點帶括號註解/整句補語/形容詞短語。(實證:光寫規則 Haiku 會略過,端點對不齊 14 條;寫成自檢動作後 14→0。跑 12 張才暴露。) +> `>>` 是分隔語法,repo 可自選符號,但全程一致。 + +**萃 gloss 規則**(issue #9/#11,把「node 的一句說明」也預編譯,供下游 KBDB 語義 normalize): +- **gloss = 這個 entity / graph node 是什麼的一句話**。下游對「entity 名 + gloss」一起做 embedding 求相似度,自動歸一同義詞(比只對名字準、比手維護 alias 表自動)。 +- **兩層 gloss**:① frontmatter `gloss:` 描述卡標題這個 node;② `## 實體` 每行描述句描述內文實體 node。**內文實體也是 graph node、也需描述句**才能 normalize(`黃仁勳` vs `Jensen Huang` 靠描述拉近向量)。 +- **實體要描述、謂詞不用**:實體同義詞字面差遠需描述拉近;謂詞同義詞字面本就近,裸詞 embed 自動聚類。 +- **在知識生產的當下、由 local CC 建**:gloss 跟三元組同階段萃,**不留給下游 ingest 臨時補**——下游只有單檔 / 跨庫視角,編不出貼合的 gloss(=胡扯)。 +- **選填、deep tier 才產**:淺萃(只要結構)時不浪費;deep 改寫時每張卡補。 +- **gloss ≠ 摘要**:`gloss` 是給機器 normalize 的定義句(「X 是…」);`## 摘要` 是給人讀的核心一句。 +- **格式對齊下游 envelope**:frontmatter `gloss:` 與 `## 實體` 詞條對應下游 ingest envelope 的 `nodes[].gloss`,ingest 直接取用。 + +**INDEX.md 是標籤視圖**(非資料夾列表),`00-INDEX.md` 是桶內容器(只連不重寫,H2/H3 分節)。 +頂層索引指桶子索引帶路徑:`[[pkm/00-INDEX]]`。 + +> 與 claude.ai Cowork 的 `system-dev/docs/SKILL.md` 改寫邏輯一致,兩條路徑(CC / Cowork)產出同一種 wiki。 + +### 第六步:完成報告 + 驗證 + +完成後**驗證原文 0 動**(踩過的坑,issue #8): +``` +git status --short pages/ journals/ # 或一般專案的 docs/ ——須 0 新增 0 修改 +``` + +> **改寫時必守**(subagent 尤其): +> 1. **絕不寫入 raw source**:subagent 目標一律給絕對路徑到 `cards//`,明寫「絕不寫入 pages/journals/docs 原稿」;事後用上面的 `git status` 驗。 +> 2. **檔名 = 卡片全名**,否則 `[[全名]]` 對不到檔。冒號用全形「:」、斜線用全形「/」,**全程一種字元**,避免 `/`、`∕`、`:` 混用斷鏈。 +> 3. **量大用 Haiku 並行改寫**,主模型只切概念邊界+審稿+修跨資料夾斷鏈。 + +告知: +``` +✅ wiki-init 完成 +建立了:[列出新建的目錄和檔案] +跳過了:[列出已有因此不動的] +改寫了:[N 份原文 → M 張原子卡、K 條 typed-edge、M 條 gloss(deep tier)] +原文驗證:pages/ journals/ git status 0 異動 ✅ +下一步:用 /wiki-capture 把重要決策存進 wiki +``` diff --git a/.claude/commands/wiki-recall.md b/.claude/commands/wiki-recall.md new file mode 100644 index 0000000..2159a5e --- /dev/null +++ b/.claude/commands/wiki-recall.md @@ -0,0 +1,58 @@ +# /wiki-recall — Session 開始,手動接關 + +開新對話時接上次進度。**Fallback 命令**:SessionStart hook 沒啟動時手動接關;要完整脈絡時也用。 + +> 主路徑是 SessionStart hook 自動注入 status 重點,不靠你打命令。 +> 這支命令應對 hook 失效,以及需要比「status 重點」更完整脈絡的時候。 + +--- + +## 命名閉環 + +init(建) → update(存,session 末) ↔ **recall(接,session 初)** → capture(隨時存結論) + +--- + +## 執行流程 + +### 第一步:讀 status.md(當前進度) + +讀 `system-dev/wiki/status.md`,掌握: +- 正在做什麼、阻擋點 +- 下次 session 第一件事 +- 待負責人確認、已知問題 + +### 第二步:讀 decisions-summary.md(為什麼這樣做) + +讀 `system-dev/wiki/decisions-summary.md`,掌握相關的架構決策——避免重新討論已定案的事。 + +### 第三步:讀 mistakes.md(別重犯) + +讀 `system-dev/wiki/mistakes.md`,掌握已知誤解 + 快速檢查清單。 + +### 第四步:掃 wishlist / HANDOFF(如果有) + +- `docs/wishlist.md`:待補功能 +- 任何 `HANDOFF.md` / 交接note:上一棒留下的脈絡 + +### 第五步:回報接關結果 + +``` +📍 接關完成 +🔄 上次正在做:[status 的「正在做」] +🎯 下次第一件事:[status 的「下次 session 第一件事」] +⚠️ 待確認:[如有] +``` + +--- + +## 鐵律:快照非即時狀態 + +status / wiki 是 **point-in-time 快照,不是即時狀態**。 + +接關 = 讀快照 **+ 核實快照**,**不盲信**。 + +> 實例:某專案 status 曾寫「待 A 收尾 X」,實際 X 早已完成。 +> 照舊資訊行動會去催一件已完成的事。 + +動手前,先用當前 code / git / 檔案核實快照寫的事項是否仍成立。發現落差 → 先更新 status,再動手。 diff --git a/.claude/commands/wiki-update.md b/.claude/commands/wiki-update.md new file mode 100644 index 0000000..1d5ecfe --- /dev/null +++ b/.claude/commands/wiki-update.md @@ -0,0 +1,50 @@ +# /wiki-update — Session 結束,更新狀態 + +每次 session 結束時執行。更新 status.md,確保下次 session 能無縫接上。 + +--- + +## 執行流程 + +### 第一步:整理這次 session 的結果 + +從對話中提取: +- 完成了哪些 tasks(標記為 [x]) +- 進行中但未完成的(標記為 [🔄]) +- 遇到什麼問題或阻擋 +- 下次應該從哪裡開始 + +### 第二步:更新 tasks.md + +把對應 SDD 的 tasks.md 狀態更新(如果這次有動到的話)。 + +### 第三步:更新 status.md + +用以下格式覆蓋 status.md: + +```markdown +# 當前狀態 +> 更新時間:[YYYY-MM-DD] + +## 正在做 +- [🔄] [task 描述] — 阻擋點:[如果有] + +## 下次 session 第一件事 +[具體的第一個動作,越具體越好] + +## 待負責人確認 +- [描述] — 等待:[什麼決定] + +## 已知問題 +| 問題 | 優先級 | 狀態 | +|------|--------|------| +| [問題] | 🔴/🟡/⚪ | [狀態] | +``` + +### 第四步:如果有新的誤解或決策 + +順帶執行 `/wiki-capture` 的邏輯,把這次的誤解和決策也存進去。 + +### 第五步:確認 + +告知 status.md 更新完成,下次 session 從哪裡開始。 diff --git a/.claude/hooks/post-edit-remind-tasks.sh b/.claude/hooks/post-edit-remind-tasks.sh new file mode 100755 index 0000000..8b907e1 --- /dev/null +++ b/.claude/hooks/post-edit-remind-tasks.sh @@ -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 <&2 </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.1:Service Binding 新增警示 +# ───────────────────────────────────────────────────────────────────────────── +# 偵測在 wrangler.toml 新增 [[services]] 的 echo/cat/sed 操作(非 100% 準確,但夠用) +if echo "$CMD" | grep -qE "echo.*\[\[services\]\].*>>"; then + block "3.1" \ + "偵測到要在 wrangler.toml 新增 [[services]] binding" \ + "零件串接一律走 HTTP URL(cypher 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 + +# ───────────────────────────────────────────────────────────────────────────── +# 資料外流警示(data-exfil-warning SDD R2):AI 動手把資料變成可被外部呼叫前先擋 +# `acr push`(部署 webhook)/ `acr recipe push`(定義資料去向)= 暴露面動作。 +# 不含 `acr creds push`(上傳加密 credential 是保護,非暴露)。 +# +# 信任修正(2026-05-30):無「旗標放行」捷徑——AI 自己能加的旗標 = 自己批准自己。 +# 這類動作一律擋,必須由人類在終端機親自執行(CLI 會跳互動、要人類輸入資源名確認)。 +# ───────────────────────────────────────────────────────────────────────────── +# 只在「指令本身就是執行 acr push / acr recipe push」時擋(錨定到指令開頭, +# 允許前置 cd .. && 或環境變數)。避免誤判 git commit -m "...acr push..." 這類 +# 「字串裡剛好提到 acr push」的情況(commit message / echo / grep 不該被擋)。 +if echo "$CMD" | grep -qE "(^|&&|;|\|)[[:space:]]*(cd[[:space:]][^&;|]*(&&|;)[[:space:]]*)?([A-Za-z_]+=[^[:space:]]*[[:space:]]+)*acr[[:space:]]+(recipe[[:space:]]+)?push\b" \ + && ! echo "$CMD" | grep -qE "acr[[:space:]]+creds[[:space:]]+push\b"; then + cat >&2 <<'EOF' +❌ BLOCKED:資料外流警示(arcrun data-exfil-warning) +原因:acr push / acr recipe push 會把資料/workflow 變成「可被外部呼叫」(暴露面)。 + 這種動作你(AI)不能自行執行,也沒有旗標捷徑——需人類明示知情同意(法律憑證)。 +正確做法: + - 把這件事交給人類:請人類在終端機親自執行(CLI 會跳互動、要人類輸入資源名確認) + - 人類第一次確認後 server 會記住,之後同資源不用再確認 +參考:docs/3-specs/data-exfil-warning/ +EOF + exit 2 +fi + +exit 0 diff --git a/.claude/hooks/pre-cold-startup-check.sh b/.claude/hooks/pre-cold-startup-check.sh new file mode 100755 index 0000000..24867b2 --- /dev/null +++ b/.claude/hooks/pre-cold-startup-check.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# Harness: Cold 啟動環境檢查 +# 用途:init 完成後強制檢查環境就緒,不靠提醒,違反條件直接擋住(exit 2) +# 由 acr init 末尾調用 + +set -e + +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")" +ARCRUN_CONFIG="${HOME}/.arcrun/config.yaml" + +# 顏色 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +FAILED=0 + +echo -e "${YELLOW}🔍 冷啟動環境檢查${NC}" +echo "" + +# 檢查 1: config.yaml 存在 +if [ ! -f "$ARCRUN_CONFIG" ]; then + echo -e "${RED}❌ 缺少 $ARCRUN_CONFIG${NC}" + echo " 執行:acr init" + FAILED=1 +else + echo -e "${GREEN}✓ config.yaml 存在${NC}" +fi + +# 檢查 2: api_key 有值 +if grep -q "^api_key:" "$ARCRUN_CONFIG" 2>/dev/null; then + api_key=$(grep "^api_key:" "$ARCRUN_CONFIG" | cut -d' ' -f2) + if [ -z "$api_key" ] || [ "$api_key" = "null" ]; then + echo -e "${RED}❌ api_key 未設${NC}" + FAILED=1 + else + echo -e "${GREEN}✓ api_key 已設${NC}" + fi +else + echo -e "${RED}❌ config.yaml 缺少 api_key${NC}" + FAILED=1 +fi + +# 檢查 3: .mcp.json 存在且指向自己(self-hosted 用) +if grep -q "mode.*self-hosted" "$ARCRUN_CONFIG" 2>/dev/null; then + if [ ! -f ".mcp.json" ]; then + echo -e "${RED}❌ .mcp.json 缺失(self-hosted 必需)${NC}" + echo " 執行:acr init 會自動生成" + FAILED=1 + else + # 檢查 mcp.json 是否指向自己(不是官方 URL) + if grep -q "mcp.arcrun.dev" ".mcp.json" && ! grep -q "workers.dev" ".mcp.json"; then + echo -e "${RED}❌ .mcp.json 指向官方 mcp.arcrun.dev(應指向自己的 worker)${NC}" + FAILED=1 + else + echo -e "${GREEN}✓ .mcp.json 配置正確${NC}" + fi + fi +fi + +# 檢查 4: acr 能執行 +if ! command -v acr &>/dev/null; then + echo -e "${RED}❌ acr 未裝或不在 PATH${NC}" + echo " 執行:npm i -g arcrun" + FAILED=1 +else + echo -e "${GREEN}✓ acr 可執行${NC}" +fi + +# 檢查 5: D1 database 存在(如果 self-hosted) +if grep -q "mode.*self-hosted" "$ARCRUN_CONFIG" 2>/dev/null; then + if grep -q "d1_database_id" "$ARCRUN_CONFIG" 2>/dev/null; then + db_id=$(grep "d1_database_id" "$ARCRUN_CONFIG" | cut -d' ' -f2) + if [ -z "$db_id" ]; then + echo -e "${RED}❌ D1 database_id 未設${NC}" + echo " 執行:acr init 會建立 D1,或手動設定 d1_database_id" + FAILED=1 + else + echo -e "${GREEN}✓ D1 database_id 已設${NC}" + fi + fi +fi + +echo "" +if [ $FAILED -eq 0 ]; then + echo -e "${GREEN}✅ 環境就緒${NC}" + exit 0 +else + echo -e "${RED}❌ 環境檢查失敗${NC}" + echo "" + echo "修復步驟:" + echo "1. 檢查 ~/.arcrun/config.yaml 是否完整" + echo "2. 若 self-hosted,確認 .mcp.json 指向自己的 mcp worker(不是官方)" + echo "3. 若缺少 D1,重跑 acr init --self-hosted --account-id=... --api-token=..." + echo "" + exit 2 +fi diff --git a/.claude/hooks/pre-write-guard.sh b/.claude/hooks/pre-write-guard.sh new file mode 100755 index 0000000..d7841ae --- /dev/null +++ b/.claude/hooks/pre-write-guard.sh @@ -0,0 +1,257 @@ +#!/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: content;Edit: new_string;MultiEdit: 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 </" \ + "改去 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.2:cypher-executor TS 裡不准實作業務邏輯(只准 wasi-shim.ts 做 crypto) +# ───────────────────────────────────────────────────────────────────────────── +if [[ "$FILE_PATH" == *"cypher-executor/src/"* && "$FILE_PATH" == *.ts ]]; then + BASE=$(basename "$FILE_PATH") + + # 通用類別:種子資料檔(*-seeds.ts)= installer 要灌進 KV 的 recipe 種子清單。 + # 其中的 endpoint / {{template}} 字串是「資料欄位」(宣告 recipe 打哪 / 怎麼注入), + # 會被序列化寫進 RECIPES KV,**不是** TS 裡的呼叫 / 展開實作;真正執行仍走零件 / WASM primitive。 + # 故整類豁免 endpoint / template 字串檢查。這是「種子資料檔」這個普遍類別的規則, + # 不是為某個零件 / recipe 開的特例(richblack 2026-06-06 原則:不為單一零件改全域規則)。 + IS_SEED_DATA_FILE=false + if [[ "$BASE" == *-seeds.ts ]]; then + IS_SEED_DATA_FILE=true + fi + + # 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 職責 + # 例外:種子資料檔(*-seeds.ts)裡的 {{secret.X}} / {{runtime.X}} 是「資料字面值」而非 TS 展開邏輯, + # 真正的展開仍在 WASM auth primitive 內完成。 + if [[ "$IS_SEED_DATA_FILE" == "false" ]] && echo "$CONTENT" | grep -qE "\{\{(secret|runtime)\." ; then + block "2.2" \ + "Template 展開({{secret.X}} / {{runtime.X}})屬於 WASM auth primitive 職責" \ + "把這段邏輯改寫到 registry/components/auth_static_key/main.go(TinyGo)" + 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 實作 + # 例外:種子資料檔(*-seeds.ts)的 endpoint 字串(sheets.googleapis.com 等)是 recipe 的 + # 「資料欄位」(宣告這個 recipe 預設打哪),會被序列化寫進 RECIPES KV,**不是** TS 裡的呼叫實作; + # 真正的 API 呼叫仍走原本零件 / http_request 路徑。 + 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)+ 種子資料檔(*-seeds.ts) + if [[ "$BASE" != "wasi-shim.ts" && "$IS_SEED_DATA_FILE" == "false" ]]; 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//main.go 即可;需要版本管理請用 git branch" +fi +if [[ "$FILE_PATH" =~ /new-(auth|credential|jwt|oauth|gmail|telegram)/ ]]; then + block "3.3" \ + "禁止為同一零件建立 new-/ 平行目錄" \ + "直接修改 registry/components//main.go" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# 規則 7.x:薄殼原則(能力長在 API,介面只暴露)— 見 .claude/rules/07-thin-shell.md +# 只擋語法層可偵測的反例;recipe 層拼裝 / 藏在 helper 的邏輯需 code review 把關(07 §6)。 +# 範圍:薄殼介面 = cli/src/ 與 arcrun-mcp/src/(不含 cypher-executor,那是 API 本體)。 +# ───────────────────────────────────────────────────────────────────────────── +block7() { + cat >&2 <\"(執行已授權任務的必要步驟);(3) 白名單放行後才寫 design/tasks。若是現有 SDD 的補充檔,改放已知 SDD 目錄下。" + fi +fi + +exit 0 diff --git a/.claude/hooks/pre-write-guard.template.sh b/.claude/hooks/pre-write-guard.template.sh new file mode 100755 index 0000000..abe5030 --- /dev/null +++ b/.claude/hooks/pre-write-guard.template.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# PreToolUse hook 範本骨架 —— 專案自訂禁令(預設空殼,不攔任何東西) +# +# ⚠️ 定位(讀清楚再用): +# 這支跟其他三支 hook 不同——它不是「裝上就生效的警察」,而是一個「按需手填的 +# 空插槽」。預設狀態下 FORBIDDEN_PATTERNS 是空的,它【不攔任何東西】。 +# 別誤以為裝了它就有保護——空殼 = 沒保護。 +# +# 🤖 有 CC 在場的話,通常不需要這個範本: +# 直接叫你的 CC「幫我寫一支 guard hook,禁止改 X」。CC 現寫的條件邏輯, +# 表達力遠勝這裡的 glob FORBIDDEN_PATTERNS(例如「禁子 repo 的 code 但放行 .md」 +# 這種細緻規則,glob 寫不出來,CC 的條件判斷寫得出來)。 +# 這個範本只對「不靠 CC、想自己手動 DIY bash」的用戶有價值。 +# +# 要啟用(手動 DIY 路線): +# 1. 在下面 FORBIDDEN_PATTERNS 填禁改的路徑/檔名 pattern +# 2. 到 .claude/settings.json 的 PreToolUse 加掛這支 +# +# 掛在 PreToolUse(matcher: Write|Edit)。stdin 收 JSON:{ tool_name, tool_input:{ file_path } } +# 命中禁令 → exit 2 擋。 +# +# 誠實限制:只擋直接寫檔。bash 繞道、helper 間接改動擋不到。留痕可審 ≠ 技術防偽。 + +set -euo pipefail + +# ── 專案自訂:禁改的 pattern(一行一個,case glob 語法)────── +# 範例(已註解,啟用前請改成自己的): +# "*/db/schema.sql" # 禁手改 schema +# "*/migrations/*" # migration 一旦建立不可改 +FORBIDDEN_PATTERNS=( + # "*/your/protected/path/*" +) + +# 沒設任何禁令 → 空殼狀態,安靜放行。 +# (不在這裡 print——PreToolUse 每次 Write/Edit 都會跑,每次喊話會洗版。 +# 「這是空殼」的提醒改由 install.sh / update.sh 安裝時告知,那裡用戶一定看得到。) +[ ${#FORBIDDEN_PATTERNS[@]} -eq 0 ] && exit 0 + +INPUT=$(cat) + +if command -v jq >/dev/null 2>&1; then + FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty') +else + FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//') +fi + +[ -z "$FILE_PATH" ] && exit 0 + +for pattern in "${FORBIDDEN_PATTERNS[@]}"; do + # shellcheck disable=SC2254 + case "$FILE_PATH" in + $pattern) + cat >&2 </dev/null 2>&1; then + FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty') +else + FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//') +fi + +# 拿不到路徑 → 不擋(容錯,寧可放過也不誤殺) +[ -z "$FILE_PATH" ] && exit 0 + +# 只管 code 檔。docs/markdown/設定檔等放行。 +case "$FILE_PATH" in + *.ts|*.tsx|*.js|*.jsx|*.go|*.py|*.rs|*.java|*.rb|*.php|*.c|*.cpp|*.h|*.hpp|*.swift|*.kt) ;; + *) exit 0 ;; +esac + +# 改 SDD 自己 / 測試檔 → 放行 +case "$FILE_PATH" in + *system-dev/docs/3-specs/*) exit 0 ;; + *_test.*|*.test.*|*.spec.*|*/tests/*|*/test/*) exit 0 ;; +esac + +# system-dev/docs/3-specs/ 下完全沒有 design.md → 攔 +SDD_COUNT=0 +if [ -d "system-dev/docs/3-specs" ]; then + SDD_COUNT=$(find system-dev/docs/3-specs -name 'design.md' -not -path '*TEMPLATE*' 2>/dev/null | wc -l | tr -d ' ') +fi + +if [ "$SDD_COUNT" -eq 0 ]; then + cat >&2 <&2 +exit 0 diff --git a/.claude/hooks/session-start-load-sdd.sh b/.claude/hooks/session-start-load-sdd.sh new file mode 100755 index 0000000..a4cf830 --- /dev/null +++ b/.claude/hooks/session-start-load-sdd.sh @@ -0,0 +1,73 @@ +#!/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 URL(cypher binding = YAML 裡的 URL 清單) + → Cypher binding 不是 Cloudflare 的 binding 機制 + + 4. 每個 WASM 零件 = 獨立 Worker = 公開 URL + → 不是從 R2 動態讀(平台零件已 bundle 進各自 Worker) + → R2 只用於 Phase 5 的用戶自製零件(未啟用) + +📋 任何 code 變動前,必須: + + 1. 讀 docs/3-specs/arcrun/arcrun.md(總進度) + 2. 讀對應 task 的 SDD(design.md + tasks.md) + 3. 在回覆開頭宣告(格式見 .claude/rules/00-sdd-protocol.md): + 📋 已讀 SDD:<檔案清單> + 🎯 本次對應 task: + 📐 本次 task 的 SDD 規範摘要:<重點> + 🚧 執行範圍:修改/建立/刪除 <檔案> + 4. 每完成一個 task,立刻更新 tasks.md 的 [x],不批次 + +🔥 當前進行中 Phase:Credential Primitives TS → WASM + + SDD:docs/3-specs/arcrun/credential-primitives-wasm/ + 已完成:Phase 0.1-0.5(核心合併 + u6u-core 刪除) + 未完成硬前置: + - Phase 0.6 wasi-shim 加 host functions(kv_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 索引 + .claude/rules/06-mindset.md — mindset(為什麼層):工作流是default/零件稀有/AI→工具/誠實不假綠 + +============================================================ +EOF + +exit 0 diff --git a/.claude/hooks/session-start-recall.sh b/.claude/hooks/session-start-recall.sh new file mode 100755 index 0000000..16df4c0 --- /dev/null +++ b/.claude/hooks/session-start-recall.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# SessionStart hook — 開 session 自動注入 status.md 重點 +# wishlist §1 主路徑:不靠 CC 自覺、不用人說,開 session 就把進度推到眼前。 +# +# 掛在 settings.json 的 SessionStart(matcher: startup|resume|clear)。 +# stdout 會被當成 context 注入給 CC。 +# +# 鐵律:status 是 point-in-time 快照,非即時狀態。 +# 這個 hook 只負責「把快照推到眼前」,核實快照是 CC 的責任——下面的提醒就是要它別盲信。 + +set -euo pipefail + +STATUS_FILE="system-dev/wiki/status.md" +PRINCIPLES_FILE="system-dev/wiki/principles.md" +MISTAKES_FILE="system-dev/wiki/mistakes.md" + +# ── 舊結構防呆(1.9.0 遷移第 2 層保險)── +# 新版 wiki 收進 system-dev/。若偵測到舊位置 .claude/wiki/ 還在、但新位置沒 status, +# 代表使用者升級了規則卻沒跑遷移(low-code 使用者常見)→ 出聲提示,讓 CC 當場可代為遷移, +# 不要默默用不到新結構而出錯。 +if [ -d ".claude/wiki" ] && [ ! -f "$STATUS_FILE" ]; then + echo "⚠️ 偵測到舊版 wiki 結構(.claude/wiki/),尚未遷移到 system-dev/wiki/。" + echo " 請跑:bash system-dev/scripts/update.sh" + echo " 或直接叫我(CC):「幫我把 wiki 遷移到 system-dev/」——我可以代為搬移。" + echo " (未遷移時接關與 /wiki-init 會找錯位置。)" + exit 0 +fi + +# 三個 push 檔都沒有 → 安靜退出,不干擾還沒 /wiki-init 的專案 +if [ ! -f "$STATUS_FILE" ] && [ ! -f "$PRINCIPLES_FILE" ] && [ ! -f "$MISTAKES_FILE" ]; then + exit 0 +fi + +# ── push 1/3:principles(全文,行動前必服從)── +# 放最前:原則是「會被遺忘的盲區」,要第一眼看見。全文成本低(一行一條、≤15 條)。 +if [ -f "$PRINCIPLES_FILE" ] && grep -q '^- ' "$PRINCIPLES_FILE" 2>/dev/null; then + echo "════════════════════════════════════════════════" + echo "📐 設計原則(行動前必服從,來自 principles.md)" + echo "════════════════════════════════════════════════" + grep '^- ' "$PRINCIPLES_FILE" # 只注入原則條目本身,不含說明區 + # context 保護:原則應 ≤15 條(push 全文)。超過 → 提示該合併或下放成 card,不截斷(截斷會漏原則)。 + P_COUNT=$(grep -c '^- ' "$PRINCIPLES_FILE") + if [ "$P_COUNT" -gt 15 ]; then + echo "" + echo "(⚠️ principles 已 ${P_COUNT} 條 > 15:請考慮合併相近原則、或把較細的下放成 card)" + fi + echo "" +fi + +# ── push 2/3:status(全文,當前進度)── +if [ -f "$STATUS_FILE" ]; then + echo "════════════════════════════════════════════════" + echo "📍 接關:上次進度(來自 $STATUS_FILE 快照)" + echo "════════════════════════════════════════════════" + echo "" + cat "$STATUS_FILE" +fi + +# ── push 3/3:mistakes(標題清單 + 一行症狀,全文按需展開)── +# 不全文注入(可能累積很多、含長 context)。只給「標題行」讓 CC 認出「我正撞到某條」→ 再去讀全文。 +if [ -f "$MISTAKES_FILE" ] && grep -q 'MISTAKE' "$MISTAKES_FILE" 2>/dev/null; then + echo "" + echo "────────────────────────────────────────────────" + echo "⚠️ 已知踩坑(標題;撞到時讀 $MISTAKES_FILE 全文)" + grep -E 'MISTAKE' "$MISTAKES_FILE" | sed 's/^/ /' +fi + +echo "" +echo "────────────────────────────────────────────────" +echo "⚠️ 以上是 point-in-time 快照,非即時狀態。" +echo " 動手前先核實:快照寫的事項是否真的還沒做完?" +echo " 需要完整脈絡(decisions / mistakes / SDD)→ 執行 /wiki-recall" +echo "════════════════════════════════════════════════" + +# ── 遷移殘留偵測(1.9.x):wiki 已搬到 system-dev/,但 CLAUDE.md 內容裡可能還寫舊路徑 ── +# update.sh 鐵則「絕不碰 CLAUDE.md」(用戶資料),所以遷移不會自動改它內容裡的死引用。 +# 這裡只「偵測 + 提示 CC 代修」——由 CC(看得懂語義、不誤傷用戶自寫內容)來改,不讓腳本盲改。 +if [ -f "CLAUDE.md" ] && grep -q '\.claude/wiki' CLAUDE.md 2>/dev/null; then + echo "" + echo "🔧 偵測到 CLAUDE.md 內仍有舊路徑 \`.claude/wiki/\`(wiki 已遷移到 system-dev/wiki/)。" + echo " 這些是遷移後的死引用,會讓你(CC)找錯位置。" + echo " 請主動幫使用者把 CLAUDE.md 裡的 \`.claude/wiki/\` 改成 \`system-dev/wiki/\`、" + echo " 工具 docs 路徑改成 \`system-dev/docs/\`(但 raw source 宣告的 \`docs/\` 保留不動)。" +fi + +exit 0 diff --git a/.claude/hooks/stop-check-sync.sh b/.claude/hooks/stop-check-sync.sh new file mode 100755 index 0000000..6d03b5d --- /dev/null +++ b/.claude/hooks/stop-check-sync.sh @@ -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 + +# 檢查 docs/3-specs 下本次 session 是否有變動 +SPECS_DIFF=$(git -C "$(pwd)" status --porcelain -- 'docs/3-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 </dev/null | head -5) +if [[ -n "$TASKS_DIFF" ]]; then + cat >&2 </dev/null 2>&1; then + FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty') + # Write 用 content;Edit 用 new_string。兩個都抓,合起來掃。 + CONTENT=$(printf '%s' "$INPUT" | jq -r '[.tool_input.content, .tool_input.new_string] | map(select(. != null)) | join("\n")') +else + FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//') + # 無 jq 時內容解析不可靠(JSON 跳脫),退回掃整包 INPUT,寧可多掃不漏掃 + CONTENT="$INPUT" +fi + +# 拿不到路徑 → 不擋(容錯,寧可放過也不誤殺) +[ -z "$FILE_PATH" ] && exit 0 + +# 只管寫進 wiki 的動作。其他路徑放行(這支專責 wiki 洩漏,不是全域 secret scanner) +case "$FILE_PATH" in + *system-dev/wiki/*) ;; + *) exit 0 ;; +esac + +[ -z "$CONTENT" ] && exit 0 + +# 行內豁免:若該段內容已被標記為刻意保留(例:範例文件要示範格式),略過該行 +# 標記:行尾加 # wiki-secret-ok (或 ) +# 先把標記過的行抽掉再掃。 +SCAN=$(printf '%s' "$CONTENT" | grep -v -E 'wiki-secret-ok' || true) +[ -z "$SCAN" ] && exit 0 + +# ── 機敏特徵 pattern。一行一類,命中即攔。────────────────────────── +# 設計取捨:偏向高訊號 pattern(有明確結構的金鑰/標記),降低偽陽。 +# 純「password=xxx」這類也納入,因為那正是使用者最擔心的場景。 +HITS="" + +check() { + local label="$1" regex="$2" + # -e 讓以 - 開頭的 pattern(如 PEM 的 -----BEGIN)不被當成選項。 + # grep 無命中回傳 1,在 set -e 下會中止 → 用 if 包住吸收掉。 + if printf '%s' "$SCAN" | grep -qiE -e "$regex"; then + HITS="${HITS} + • ${label}" + fi +} + +# 密碼/密鑰賦值(password = ..., secret: ..., api_key=...) +check "密碼/密鑰賦值 (password/secret/api_key/token = ...)" \ + '(pass(word)?|secret|api[_-]?key|access[_-]?key|auth[_-]?token|priv(ate)?[_-]?key)[[:space:]]*[:=][[:space:]]*[^[:space:]<>"'"'"']{6,}' + +# 私鑰 PEM 區塊 +check "私鑰檔內容 (BEGIN ... PRIVATE KEY)" \ + '-----BEGIN[[:space:]].*PRIVATE KEY-----' + +# 常見雲端/服務金鑰前綴 +check "服務金鑰特徵 (AWS/GitHub/Slack/Google/Stripe 等)" \ + '(AKIA[0-9A-Z]{16}|gh[pousr]_[0-9A-Za-z]{20,}|xox[baprs]-[0-9A-Za-z-]{10,}|AIza[0-9A-Za-z_-]{20,}|sk_(live|test)_[0-9A-Za-z]{16,})' + +# JWT +check "JWT token" \ + 'eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}' + +# 連線字串內嵌帳密 (proto://user:pass@host) +check "連線字串內嵌帳密 (proto://user:pass@host)" \ + '[a-z][a-z0-9+.-]*://[^[:space:]:/@]+:[^[:space:]:/@]+@' + +# 台灣身分證字號(個資)。BSD/GNU grep 都支援 ERE,避免 \b(BSD 不認),改用字元類邊界。 +check "台灣身分證字號 (個資)" \ + '(^|[^A-Za-z0-9])[A-Z][12][0-9]{8}([^0-9]|$)' + +# 信用卡號(個資,粗略 13-16 連續數字,可含空格/連字號分隔)。避免 PCRE,用 ERE 近似。 +check "疑似信用卡號 (個資)" \ + '(^|[^0-9])[0-9]{4}[ -]?[0-9]{4}[ -]?[0-9]{4}[ -]?[0-9]{0,4}([^0-9]|$)' + +# Email 不擋(wiki 常需記聯絡人),手機號也不擋(偽陽太高)——刻意留白。 + +if [ -n "$HITS" ]; then + cat >&2 <) +- <對應 SDD 的 design.md 路徑> +- <對應 SDD 的 tasks.md 路徑> + +🎯 本次對應 task: + +📐 本次 task 的 SDD 規範摘要: +- <重點 1> +- <重點 2> +- <重點 3> + +🚧 執行範圍: +- 會修改:<檔案清單> +- 會建立:<檔案清單> +- 會刪除:<檔案清單> +``` + +**不做這個宣告 = 違反 SDD 協議 = 停手等 richblack**。 + +### 步驟 4:check tasks.md 狀態 + +動手前:在 tasks.md 把對應 task 的 `- [ ]` 改成 `- [🔄]`(進行中標記)。 +完成後:改成 `- [x]`,不批次更新,每完成一個就立刻改。 + +## 什麼算「任務超出 SDD 範圍」? + +以下情況屬於 **change**,不是 **modify**,**必須停手並與 richblack 確認**: + +- SDD 沒寫到的新功能 +- 新增頂層目錄 +- 新增新的 Worker(不管是 cypher-executor / registry / 零件 worker) +- 修改架構決策(例如「改用 xxx 取代 yyy」) +- 跨多個子系統的連鎖修改 + +**停手不是怯懦,是專業**。猜錯方向比慢一小時更糟。 + +## 新增 SDD 的完整程序(richblack 確認後怎麼往下走) + +> 2026-06-03 補:之前協議只寫「停手等確認」,沒寫「確認後怎麼做」,導致 CC 被 +> `pre-write-guard.sh` 規則 4.3 擋下後不知正路、卡死。這節補完整程序。 + +當「新增一個 SDD 子系統」(在 `docs/3-specs/` 下開新頂層目錄): + +1. **停手,向 richblack 說明要新建哪個 SDD、為什麼**(change,見上節)。 +2. **取得 richblack 明確確認**後,依序: + 1. **更新白名單**:在 `.claude/hooks/pre-write-guard.sh` 的 `KNOWN_SDDS` 陣列加一行 + `"docs/3-specs/<新目錄名>" # YYYY-MM-DD richblack 確認新建(<一句用途>)`。 + (這步是「執行 richblack 已授權任務的必要步驟」,不是 AI 擅自放寬 guardrail—— + 授權脈絡要明確,分類器才放行;沒有明確授權就改白名單 = 越界。) + 2. **建目錄 + 寫 design.md / tasks.md**(白名單放行後才寫得進去)。 +3. **若沒先更新白名單就 Write** → 規則 4.3 會擋你(這是對的,表示你跳了步驟 2.i)。 + +**為什麼白名單而非全放行**:開新 SDD 子系統 = 宣告新架構範圍,是稀有的人類決策點。 +白名單讓「AI 自己無中生有開子系統」必停下(AI 改不了白名單,除非 richblack 明確授權該任務)。 +已許可的目錄內寫檔零摩擦。 + +## 發現 SDD 本身有問題怎麼辦? + +- SDD 和實作不一致 → 停手,列出矛盾點,與 richblack 確認哪一邊是對的 +- SDD 規範之間互相矛盾(例如禁令 A 和設計 B 衝突)→ 停手,引用矛盾原文,與 richblack 確認 +- **不可以自行猜哪個是對的**。CC 之前兩天就是這樣走錯的。 + +## 為什麼這個協議存在 + +arcrun 規範已經足夠細緻,CC 之前出錯不是因為不懂,而是因為**沒讀**或**讀了覺得「大概是這個意思」就動手**。SDD 協議強制把「先讀 → 定位 → 宣告 → 執行 → 更新」做成一條死規矩,沒有繞過去的路徑。 diff --git a/.claude/rules/01-tech-stack.md b/.claude/rules/01-tech-stack.md new file mode 100644 index 0000000..a12cb8d --- /dev/null +++ b/.claude/rules/01-tech-stack.md @@ -0,0 +1,76 @@ +# 技術棧硬限制 + +## 三層語言對應(絕對不可混用) + +| 層級 | 語言 | 位置 | 職責 | +|-----|------|------|------| +| 零件(Component) | **TinyGo 或 AssemblyScript → WASM** | `registry/components/{name}/` | 所有業務邏輯 | +| 零件 Worker 包裝 | TypeScript(固定模板,不寫業務邏輯) | `.component-builds/{name}/` | WASI shim,stdin/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,不監聽 socket(WASI preview1 沒 socket) + +### Host Functions(零件呼叫外部能力的唯一管道) +在 `u6u` namespace 下: + +| Host Function | 用途 | +|---|---| +| `u6u.http_request` | 發 HTTP 請求 | +| `u6u.kv_get` | 讀 Cloudflare KV(Worker 側依 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` 套件 + - JS:Web 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) +- **Landing**:`arcrun.dev` +- **每個零件 Worker**: + - **對內(cypher-executor 用來 fetch component,避開同 zone 死鎖)**:`arcrun-{kebab}.{WORKER_SUBDOMAIN}.workers.dev` + - 例:`arcrun-kbdb-get.uncle6-me.workers.dev` + - cypher-executor 從 `wrangler.toml [vars] WORKER_SUBDOMAIN` 組出此 URL + - **對外(可選,零件對全網開放被 curl 用)**:`{kebab}.arcrun.dev` + - 例:`gmail.arcrun.dev`、`kbdb-get.arcrun.dev` + - 仍允許保留,但**禁止 cypher-executor 透過此 URL fetch**(會撞同 zone 自循環,見 [docs/incidents/2026-05-13-cypher-outbound-522.md](../../docs/incidents/2026-05-13-cypher-outbound-522.md)) + - **新增 component worker 部署清單**:`name = "arcrun-{kebab}"` + `[[routes]]` 對外(可選)+ dashboard 啟用 workers.dev(必須) +- **部署工具**:Wrangler diff --git a/.claude/rules/02-forbidden.md b/.claude/rules/02-forbidden.md new file mode 100644 index 0000000..350887c --- /dev/null +++ b/.claude/rules/02-forbidden.md @@ -0,0 +1,154 @@ +# 禁止行為清單(零容忍) + +**這份清單由 `.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 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)**: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)。 + +--- + +## Hook Block 訊息格式 + +當 hook 擋住一個操作時,訊息格式統一為: + +``` +❌ BLOCKED by arcrun CLAUDE rules +違反項:<禁令編號,例如 2.2> +原因:<簡短說明> +正確做法:<該改去哪裡、該用什麼方式> +參考:.claude/rules/<對應檔案> +``` + +這樣 CC 拿到錯誤訊息後有機會自行導正,不是被擋死就愣住。 diff --git a/.claude/rules/03-component-architecture.md b/.claude/rules/03-component-architecture.md new file mode 100644 index 0000000..bb56621 --- /dev/null +++ b/.claude/rules/03-component-architecture.md @@ -0,0 +1,165 @@ +# 零件架構與部署模式(必讀,CC 最常搞錯的地方) + +## 第一核心概念:每個 WASM 零件 = 一個獨立 Worker = **兩個** URL + +**不是**從 R2 即時載入 WASM 執行。 +**不是**用 service binding 串零件。 +**不是**一個 Worker 裡跑多個零件。 + +**是**:每個零件都是獨立部署的 Worker,每個都有**兩個 URL**: + +| URL 類型 | Pattern | 用途 | +|---|---|---| +| 對內(cypher-executor 用)| `arcrun-{kebab}.{WORKER_SUBDOMAIN}.workers.dev` | cypher-executor fetch component 走這個,避開同 zone 自循環死鎖(P0 #9)| +| 對外(直接 curl 用,可選)| `{kebab}.arcrun.dev` | 用戶單獨打 component 測試或 self-hosted 用法 | + +例:`kbdb_get` 零件: +- 對內:`arcrun-kbdb-get.uncle6-me.workers.dev`(cypher-executor 走這個) +- 對外:`kbdb-get.arcrun.dev`(用戶 / 直 curl) + +**為什麼這樣設計**:CF Workers 「同 zone 自循環防護」會讓綁 `cypher.arcrun.dev/*` 的 cypher-executor fetch 同 zone `*.arcrun.dev` 撞 522。完整事件報告:[docs/incidents/2026-05-13-cypher-outbound-522.md](../../docs/incidents/2026-05-13-cypher-outbound-522.md)。改走 workers.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 shim(POST / → stdin → WASM → stdout → JSON) +``` + +**src/index.ts 是通用模板**,所有零件都用同一份。這個 TS 只做 WASI runtime,不是業務邏輯。 + +--- + +## R2(WASM_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` 裡的 13 個 `[[services]]` 綁定就是這個用途。 + +**禁止新增任何 Service Binding**。所有新零件(含 auth primitive)都走 HTTP URL 路徑。 + +**same-zone 1042 的解(credential-primitives-wasm Phase 7,2026-06-06)**:self-hosted 的 cypher 與 auth worker 同在 `{sub}.workers.dev` zone,cypher `fetch()` 打 auth 觸發 CF **1042**(官方 docs:「fetch from another Worker on the **same zone**」;官方 cypher 在 `cypher.arcrun.dev`、打 `*.workers.dev` 屬跨 zone 故不踩——非官方有 flag)。**解法不是 service binding**(評估後廢:靜態、加/改要重 deploy),而是 cypher wrangler.toml 加 **`global_fetch_strictly_public` flag**——讓 same-zone fetch 走公網前門 → 同 zone 也通。auth 維持 HTTP fetch、不加 binding。官方加此 flag 行為不變(本就跨 zone),self-host 被修好 → **官方與 self-host 共用同一份 toml**。 + +**仍禁止**:為**用戶自製 / 服務專屬零件**(`gmail-worker`、`notion-worker` 之類)新增 binding——那些是 recipe 的事,不該有 binding。**workflow 層(用戶串零件)一律 HTTP URL 不變。** + +--- + +## 零件之間怎麼串:實際流程 + +假設 workflow 是:webhook → gmail(要 auth)→ google_sheets(要 auth) + +``` +用戶 POST https://cypher.arcrun.dev/webhooks/named/xxx/trigger + │ + ▼ +cypher-executor(Worker)讀 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 +13 個現有的 SVC_*(邏輯零件)是歷史遺產,不新增。新零件(含 auth primitive)一律走 HTTP URL。self-hosted 的 same-zone 1042 用 `global_fetch_strictly_public` flag 解,不靠新增 binding(見上「合法用途」段,Phase 7)。 + +### 禁止重建已存在的零件 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. **Dashboard 啟用 workers.dev URL**(必須,否則 cypher-executor fetch 不到): + - Workers & Pages → `arcrun-auth-static-key` → Settings → Domains & Routes → workers.dev → Enable + - 啟用後 URL:`arcrun-auth-static-key.{WORKER_SUBDOMAIN}.workers.dev` +6. 驗證對外:`curl https://auth-static-key.arcrun.dev` → 應回 `{ok: true, component: "auth_static_key"}` +7. 驗證對內:`curl https://arcrun-auth-static-key.{WORKER_SUBDOMAIN}.workers.dev` → 應同樣回 200 +8. cypher-executor 透過 `wasmWorkerUrl()` 自動組對內 URL 呼叫(不用手動註冊) + +**這是唯一正確的部署流程**。任何偏離這個流程的「替代方案」都要先和 richblack 確認。 + +**Step 5 為什麼必須**:見 arcrun.md P0 #9(2026-05-13)。cypher-executor 走對內 URL 避開同 zone 自循環死鎖;若 workers.dev 未啟用,cypher-executor fetch 該 component 會 404。 diff --git a/.claude/rules/04-current-progress.md b/.claude/rules/04-current-progress.md new file mode 100644 index 0000000..c1e67df --- /dev/null +++ b/.claude/rules/04-current-progress.md @@ -0,0 +1,77 @@ +# 當前進度(SessionStart 會注入此檔重點) + +> 更新時間:2026-04-19 +> 權威來源:`docs/3-specs/arcrun/credential-primitives-wasm/tasks.md` +> 此檔僅摘要,詳細狀態以 tasks.md 為準。 + +--- + +## 封測狀態 + +**原定明天封測,richblack 決定推遲**,原因:cypher-executor 有三套 TS 業務邏輯違反「零件一律 WASM」架構原則(Phase 1-3 要清除的程式碼),在清除前不封測。 + +--- + +## 目前 Phase:Credential Primitives TS → WASM + +**SDD 位置**:`docs/3-specs/arcrun/credential-primitives-wasm/design.md` + `tasks.md` + +### 已完成 + +- **Phase 0.1–0.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 第一件要做的事 + +**讀 `docs/3-specs/arcrun/credential-primitives-wasm/tasks.md`**,然後決定從 Phase 0.6 還是 0.7 開始。 + +0.6(host functions)和 0.7(WASM runner)是並列的前置工作,哪個先都可以,但都要在 Phase 1 開始之前完成。 + +--- + +## SDD 索引 + +| 子系統 | SDD | +|--------|-----| +| **主要(正在動)** Credential Primitives WASM 改寫 | `docs/3-specs/arcrun/credential-primitives-wasm/` | +| **LI (LLM Interface)** — AI 操盤手使用體驗(2026-05-16 新建,mira dogfood 痛點轉化) | `docs/3-specs/llm-interface/` | +| arcrun 總進度 | `docs/3-specs/arcrun/arcrun.md` | +| Auth Recipe 系統(schema、預建 20 個服務) | `docs/3-specs/arcrun/auth-recipe.md` | +| Landing Page | `docs/3-specs/arcrun/landing-page.md` | +| SDK + Website | `docs/3-specs/arcrun/sdk-and-website/design.md` | +| arcrun MVP 整體 | `docs/3-specs/arcrun-core-mvp/design.md` | +| Credential 長期規格(需求源) | `docs/user_requirements/credential_parts.md` | +| Platform Evolution | `docs/3-specs/arcrun-platform-evolution/design.md` | +| Tech Stack 詳細 | `docs/3-specs/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`。 diff --git a/.claude/rules/05-deploy-convention.md b/.claude/rules/05-deploy-convention.md new file mode 100644 index 0000000..e48b4ad --- /dev/null +++ b/.claude/rules/05-deploy-convention.md @@ -0,0 +1,138 @@ +# 部署慣例(CI/CD) + +> **核心原則:新增 Worker = 新目錄 + `wrangler.toml`,不用改 workflow。** + +`.github/workflows/deploy.yml` 是**通用掃描式** workflow,不該為每個 Worker 手寫 job。 + +--- + +## Workflow 如何找到要部署的 Worker? + +``` +find . -name 'wrangler.toml' -not -path '*/node_modules/*' -not -name 'wrangler.test.toml' +``` + +每一個命中的目錄 = 一個部署單位。無論是: + +- `cypher-executor/` (orchestration Worker) +- `registry/` (合約管理 Worker) +- `.component-builds/{name}/` (零件 Worker,25+ 個) +- 未來新增的任何 Worker + +**無需改 workflow,只要符合掃描規則就會自動部署**。 + +--- + +## 觸發邏輯 + +| 觸發 | 部署範圍 | +|------|---------| +| `push` 到 main | diff 涉及的 Worker 目錄才部署 | +| `push` 到 main + 改 `registry/components/{name}/` | 連動 rebuild `.component-builds/{name}/component.wasm` 再 deploy | +| `workflow_dispatch` + `force_all=true` | 全部 Worker | +| `workflow_dispatch` + `only=a,b,c` | 只部署指定清單 | +| `push` 但 base sha 不可及(首次) | 全部 Worker | + +--- + +## 新增 Worker 的步驟 + +### 如果是新 WASM 零件 Worker + +1. 在 `registry/components/{new_name}/` 建 `main.go` + `component.contract.yaml` +2. 在 `.component-builds/{new_name}/` 建 Worker 模板: + - `wrangler.toml`(name/routes/bindings) + - `package.json`(hono + workers-types + wrangler 即可,參考 `auth_static_key/package.json`) + - `tsconfig.json`(可直接複製) + - `src/index.ts`(WASI shim,方案 A:import `../../cypher-executor/src/lib/wasi-shim`) +3. 本地跑 `pnpm install` 產 `pnpm-lock.yaml` +4. 本地跑 `tinygo build -target=wasi -o {new_name}.wasm main.go` 先驗證 build 通過 +5. Commit push → CI 自動 rebuild WASM + deploy + +### 如果是新 orchestration/service Worker + +1. 在 repo 根建新目錄(類似 `cypher-executor/`) +2. `wrangler.toml` + `package.json` + `pnpm-lock.yaml` + `src/index.ts` + `tsconfig.json` +3. Push → CI 自動部署 + +--- + +## Runtime Secret 管理 + +**CI 只提供 Cloudflare 驗證,不碰 runtime secret**。 + +- GH Actions secrets:`CLOUDFLARE_API_TOKEN`、`CLOUDFLARE_ACCOUNT_ID`(一次性設好) +- Runtime secret(例:`ENCRYPTION_KEY`、`OPENAI_KEY`、`GOOGLE_API_KEY`): + - **由 richblack 一次性手動** `wrangler secret put ` 設進各 Worker + - 不進 CI,不進 `wrangler.toml` `[vars]` + - 需要的 Worker:`auth_static_key`、`auth_service_account`(兩個都要 `ENCRYPTION_KEY`) + +--- + +## Lockfile 規範 + +- **統一使用 pnpm**。新增 Worker 只放 `pnpm-lock.yaml`,不要 `package-lock.json` +- 若新建 Worker 時用 `npm install` 產出 `package-lock.json`,**刪掉它**,改跑 `pnpm install` +- `cypher-executor/` 和 `registry/` 的 `package-lock.json` 已於 2026-04-20 刪除 + +**現存例外**(歷史遺產,混合期不強制遷移): +- `.component-builds/{if_control, switch, ... 16 個舊邏輯零件}/` 仍是 `package-lock.json`,workflow 有 fallback 分支(`pnpm install --no-frozen-lockfile`)可跑 +- `builtins/`、`landing/` 同上 + +**新增 Worker 一律 pnpm,不要製造新的混合情況**。 + +--- + +## WASM 來源 + +> **⚠️ 慣例變更(richblack 2026-06-02,self-hosted 開源策略)**: +> 原慣例「`.component-builds/{name}/component.wasm` 不 commit 進 repo」**已推翻**。 +> 現在 **commit `.component-builds/*/component.wasm` 進 repo**,因為 self-host 用戶 / `acr init --self-hosted` +> 從 GitHub(codeload tarball)直接拿這份 wasm 部署到自己的 CF——repo 必須自帶可部署的 wasm。 +> 決策依據:`docs/3-specs/arcrun/sdk-and-website/self-hosted-init.md §6`。 + +### 現行規則(2026-06-02 起) + +- **`.component-builds/*/component.wasm` → commit 進 repo**(部署來源)。`.gitignore` 用否定規則放行: + ``` + *.wasm # 預設排除 + !.component-builds/**/component.wasm # 例外放行部署物 + ``` +- **`registry/components/*.wasm` → 仍不 commit**(build 中間產物,部署不直接用,`.gitignore` 仍排除)。 +- 本地開發 build:`cd registry/components/{name} && tinygo build -target=wasi -o {name}.wasm main.go && cp {name}.wasm ../../../.component-builds/{name}/component.wasm`,**然後 commit `.component-builds/{name}/component.wasm`**。 +- CI(deploy.yml):仍在 deploy 前自動 rebuild + copy(部署 prod 用最新 source;與 repo 內 commit 的 wasm 不衝突——前者給 CI deploy prod,後者給 self-host 用戶當部署來源)。 + +### 誠實 trade-off(mindset §7) + +commit wasm 進 repo → 每次 rebuild 在 git 歷史累積二進位,**repo 長期會膨脹**。 +可接受(self-host 體驗優先),未來若膨脹過劇再考慮 git-lfs / 按需安裝(self-hosted-init.md §6.6)。 + +--- + +## 並行度 + +`max-parallel: 5` — 避免觸發 Cloudflare Workers API rate limit。 + +Worker 數量 > 5 時,deploy 會分批跑。25 個 Worker 大約 5 輪 × ~30 秒 = 2-3 分鐘可完成全部。 + +--- + +## 禁止事項 + +1. **禁止**為新 Worker 手動加 deploy job 到 `deploy.yml`。通用掃描會自動處理,手加就是重複工作。 +2. **禁止**把 runtime secret(API key / encryption key / credential)放進 GH Actions secrets 或 `wrangler.toml` `[vars]`,只能用 `wrangler secret put`。 +3. **禁止**在 CI 裡跑不必要的測試阻擋 deploy。測試在 PR / 本地跑,`main` 推上去就 deploy(trunk-based)。若要測試關,開新 workflow 檔,不要污染 deploy workflow。 +4. **禁止**跳過 TinyGo rebuild 直接 deploy 舊 `.wasm`。CI 的 rebuild 步驟是確保部署的是最新 source。 + +--- + +## 驗證指令 + +本地模擬 CI 的掃描結果: + +```bash +find . -name 'wrangler.toml' -not -path '*/node_modules/*' -not -name 'wrangler.test.toml' \ + | xargs -n1 dirname | sort -u +``` + +應列出 ~25 個目錄。任何「我新增了 Worker 但沒被 deploy」的問題,先跑這條確認目錄被掃到。 diff --git a/.claude/rules/06-mindset.md b/.claude/rules/06-mindset.md new file mode 100644 index 0000000..d79700c --- /dev/null +++ b/.claude/rules/06-mindset.md @@ -0,0 +1,60 @@ +# arcrun mindset(給 CC 的世界觀 — 走歪前先讀這個) + +> 這是「為什麼」層,不是禁令層(禁令在 02-forbidden)。 +> 違反禁令會被 hook 擋;違反 mindset 不會被擋,但會讓你做出「技術上對、方向上錯」的東西。 +> 來源:2026-05 richblack 與 CC 的一連串判準釐清(權威記錄在 DECISIONS.md)。 +> +> 註:這是步驟 7「arcrun-mindset Skill」的前身。Skill 正式建立前,本檔是 CC 的 mindset 來源。 + +--- + +## 1. 工作流是 default,零件是稀有例外 + +AI 開發時的預設順序: +1. **預設寫工作流**(串服務 / 自用 / 給少數人用 → 全部先工作流,用 http_request + 流程控制 primitive 串)。 +2. **要打的服務不提供串接但有 API,且有必要讓全 arcrun 生態重用** → 才建零件(零件 = API 薄殼,只打一個 endpoint)。 +3. **建零件前必問「你有必要嗎?」**:看到「有 API 可包成零件」≠「該包」。自用 → 工作流。 + +**CC 的典型走歪**:把「需要一個能力」直接翻成「做一個零件」,把「能包」當「該包」。 +mira 的 claude_api / km_writer 就是這樣被錯做成零件的(其實是自用服務膠水,該是工作流)。 + +## 2. Arcrun 是 AI 呼叫的工具(AI → 工具),不是工具回頭呼叫 AI + +需要 AI 判斷 / 轉換時,是**操盤的 CC 自己做**,再呼叫工作流做確定性的下一步。 +**不要在工作流裡放零件回頭呼叫一個 LLM**。n8n 需要 AI 節點是因為它沒大腦;arcrun 的大腦就是 CC。 +(ai_transform_compile/run 因此被刪除。) + +## 3. arcrun 不做授權判斷 + +「能不能打通」由發 API key 的服務裁決,不是 arcrun。401/403 是對方服務在行使授權,不是 arcrun 的 bug。 +auth_recipe 只定義「怎麼認證」,不含「誰准用」清單。不要加「arcrun 替用戶擋掉某些 endpoint」的功能。 + +## 4. 零件投稿走 GitHub PR(人 merge = 人類閘門) + +零件投稿不是 registry self-service,是 GitHub PR。人 merge = 天然人類閘門(AI 偽造不了 GitHub approve), +把關(假零件偵測 / 純WASI / Gherkin)由 CI PR check 跑(CI 能 runtime 跑 wasm,CF Worker 不能)。 +§8「不依賴 CI」指執行鏈路(高頻);零件投稿稀有,走 PR/CI 是例外、不違反。 + +## 5. 發佈安全的底氣是純 WASI 沙箱,不是 Gherkin + +Gherkin 全綠 ≠ 零件安全(投稿者可寫避重就輕的 Gherkin)。真正框死破壞力的是**純 WASI 沙箱** +(零件只能 stdin→stdout、無網路 syscall、無檔案系統)。Gherkin 驗契約 + 沙箱框死 + 市場補長尾 = 風險可控,非零風險。 + +## 6. 暴露 / 送資料的動作 → 人類明示同意(資料外流警示) + +把資料 / workflow 變成「可被外部呼叫」(部署 webhook、recipe push)= 暴露面 → 需人類明示同意,不分公私庫。 +**不禁止**用戶公開(他的自由),但要**確定他自己明示同意**(不是 AI 替他決定)。 +警示同時是「保護措施入口」(提示可加 API Key / 權限 / 限流)。 + +## 7. 誠實限制(最重要的 mindset:不假裝、不假綠) + +- **AI 技術上能偽造人類確認**(confirmed_by_human、exposure_consent、gherkin_evidence 都能塞)。 + 這些機制的價值是**法律歸責 + 軌跡可審**,不是技術防偽。**絕不在文件 / 程式裡聲稱「不可能繞過」。** +- **絕不代替人類做有風險的確認**(建零件、暴露資料)。非 TTY(你直跑)就拒絕,不要自己塞 flag 假裝人類同意了 —— 那是明確越界。 +- **禁假綠**(DECISIONS §3c/§7):stub / 未實作就回 success:false 或明確標 unimplemented,不要回傳假資料假裝成功。 + 缺 credential 打不到 2xx 就誠實標「未驗收:缺 X」,不 mock 充綠燈。 +- **完成 = 客觀證據**(編譯 exit code / HTTP status + trace),不是口頭宣布「我做好了」。 + +--- + +詳細判準與來龍去脈見 `DECISIONS.md`。每條都有對應的慘痛教訓,不是憑空規定。 diff --git a/.claude/rules/07-thin-shell.md b/.claude/rules/07-thin-shell.md new file mode 100644 index 0000000..6a6f995 --- /dev/null +++ b/.claude/rules/07-thin-shell.md @@ -0,0 +1,133 @@ +# 薄殼原則(鐵律)— 能力長在 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)只實作一次,放在 API(cypher-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 層拼湊 upsert(recipe/零件補 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. 呼叫 API(HTTP 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 呼叫的封裝 | +| **自家** API(KBDB / 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「不可能繞過」)。 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..89a69ba --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,66 @@ +{ + "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 + }, + { + "type": "command", + "command": "bash .claude/hooks/wiki-secret-scan.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 + } + ] + } + ] + } +} diff --git a/.gitignore b/.gitignore index 60c77dd..8266d88 100644 --- a/.gitignore +++ b/.gitignore @@ -23,21 +23,13 @@ docs/test_credentials/ *.sa.json *-service-account*.json -# ── 開發痕跡 / 思考過程:不對外(本機保留供開發,但不進公開 repo)── -# richblack 2026-06-03:用戶要的是「使用 arcrun」,不是「開發 arcrun」。 -# 開發規範(.claude/CLAUDE.md/AGENTS.md)、思考過程(.agents/docs/DECISIONS/BACKLOG)只在本機。 -# 「用戶 CC harness」另由 acr install-harness 安裝進用戶專案(不是放在這個 repo 給人 clone)。 -.claude/ -.agents/ -docs/ -landing/ +# ── 開發痕跡進 repo(D22 翻案 2026-07-03,InkStoneCo 頂層決策)── +# 舊規則(2026-06-03「開發痕跡不對外」)立於 GitHub 終將公開的前提。 +# 現 repo 真身在自有 Gitea private,clone 者是雲端工人:docs/SDD、wiki、 +# CLAUDE.md、.claude/、.agents/ 都必須進 repo(缺=雲端斷糧,T1.5 卡點實證)。 +# 未來上 GitHub 公開=一次性發佈篩選,不在日常 gitignore 綁死。 +# 例外仍不推:機敏值(本檔各處既有規則照舊)+ .github/(防 Actions 復燃,D20 同源) .github/ -CLAUDE.md -AGENTS.md -DECISIONS.md -BACKLOG.md -BETA_TEST.md -CONTRIBUTING.md # macOS .DS_Store diff --git a/BACKLOG.md b/BACKLOG.md new file mode 100644 index 0000000..cd34064 --- /dev/null +++ b/BACKLOG.md @@ -0,0 +1,238 @@ +# Arcrun 待辦(BACKLOG.md) + +> 流動的待辦清單。做完一項就劃掉(`[x]`)或移到「已完成」。 +> 穩定的決策在 `DECISIONS.md`。 +> +> 原則:一次只推進一步。不在「整理」的慣性裡順手做下一件事。 +> +> 最後更新:2026-06-10(與 `.claude/wiki/status.md` 對齊) + +--- + +## 🔥 當前優先序(權威來源:`.claude/wiki/status.md` 2026-06-09) + +> 此區是「現在該做什麼」的頂層視圖。下方「第一期步驟」多已完成(見各步驟 `[x]`), +> 真正待推進的兩個 P0 在 2026-05 版 BACKLOG 完全沒記,補在這裡。 + +### 🔴 P0(用戶一用就撞 / 威脅核心承諾) + +- [~] **credential 注入 401 修復** — `{{credential.X}}` 注入失敗,用戶被迫把 token 明文寫進 workflow。 + **根因**:此語法系統沒實裝(三條 template 路徑都不認 `credential.` namespace)。 + **修法**(design §8,richblack 2026-06-10 確認):auth_static_key 加 `resolve_credentials` action(WASM 解密) + + graph-executor `resolveCredentialRefs` 偵測回填(不碰 ENCRYPTION_KEY,rule 02 §2.2)。 + **8.1-8.4 done**(tinygo build OK + tsc 0 + §2.2 自檢綠)。**待 8.5 端對端驗收**(部署 + 真 OpenAI key + 全新帳號打 2xx)。 +- [ ] **§8 P1/P2 recipe/workflow list 遷 D1** — CF KV list 免費僅 1000/日,不修=用戶用一用就 429, + 免費承諾破功。D1 現已可建(依賴解除),架構拍板走 kbdb `/entries` HTTP 雙寫不加 binding。 + **大、易出錯 → 另開乾淨 session + 壓測**,不要在雜事 session 順手做。 + +### 🟡 P1(封測門檻 / 技術債) + +- [ ] credential-primitives-wasm **Phase 0.7**(component-loader WASM runner)→ **Phase 1-2**(auth_static_key / auth_service_account WASM 零件)。0.6 已完成。 +- [ ] **Phase 3** 清除違規 TS(credential-injector.ts / jwt-signer.ts / BUILTIN_*)—— 須先有 Phase 1-2 WASM 頂上。 +- [ ] 4 份 inline http_request host fn 抽共用 helper(dedup;假綠修是逐份改的)。 + +### ⚪ P2(不擋封測) + +- [ ] `arcrun.dev/llms.txt` serve(landing/public 補檔) +- [ ] ENCRYPTION_KEY 冪等性、MCP account-source、recipe submit uuid 回傳 +- [ ] 文件遷移階段二/三 + wiki modules/ +- [ ] 下方第一期殘項:步驟 2(acr recipe test)、步驟 5b(資料外流警示 SDD)、步驟 6(搬家拆 matrix) + +--- + +## 第一期(鎖定範圍,依序做,不跳) + +### 步驟 1 — 清 cypher-executor 的 KBDB 污染 +- [x] 刪除 `cypher-executor/src/lib/kbdb-partner.ts` +- [x] `auth.ts` 移除 kbdb-partner import 與 3 處呼叫 +- [x] `wrangler.toml` 移除 `KBDB_BASE_URL` 與 `KBDB_INTERNAL_TOKEN` 註解 +- [x] `component-loader.ts` 白名單移除 `claude_api` + 6 個 `kbdb_*` +- [x] `component-loader.ts` 修正說謊的 doc comment / 中段註解 +- [x] `graph-executor.ts` 註解的 kbdb 範例替換、`types.ts` 移除 `KBDB_BASE_URL` 型別 +- [x] `tsc --noEmit` 編譯通過 +- [x] **commit 第一步**(commit message 寫清楚範圍) + +### 步驟 2 — `acr recipe test`:recipe 形式的 curl + +> 目的很簡單:把一個早就存在的 API endpoint(如 KBDB)變成 Arcrun recipe。 +> 流程:看 API 文件 → 申請 API Key → 設進 credential → 寫 recipe → 用 test 打一次。 +> `test` 就是「用 recipe 形式 curl 一次」——打通(2xx)就成了。 +> 不寫記錄、不是入庫關卡、不防任何東西。防作弊/公共庫那些是第二期。 +> +> 順序:先 2c(為 KBDB 建 auth recipe,否則 test 打 KBDB 會 401)→ 再 2a。 + +- [ ] **2c. 為 KBDB 建 auth recipe(static_key)** — `POST /auth-recipes`,建 `auth_recipe:kbdb`。 + 讓 auth-dispatcher 能把 KBDB API Key 注進 test 的請求。static_key 新路已支援(見 DECISIONS §3b)。 +- [ ] **2a. `acr recipe test [--input k=v]` 指令** — 藄殼:組單節點 ExecutionGraph + → 走現有 `/execute` → 顯示 HTTP status 與回應。就是 recipe 形式的 curl。 + 不碰 cypher-executor、不改 RecipeDefinition 型別、不寫任何記錄。 + +### 步驟 3 — 降級假零件成 recipe(2026-05-29 大部分完成) + +> 進度(2026-05-29):registry/components/ 33 → 22。引擎加了 `auth_service`(多 recipe 共用 auth) +> 與 `inject.path`(endpoint 可插 secret,解 telegram URL-path token)。詳見 auth-recipe.md §七 + DECISIONS §1。 + +- [x] KBDB 降級:建 `kbdb_get/create_block/patch_block/delete/ingest` 5 recipe(共用 auth_service=kbdb),刪 5 零件目錄 + - 驗收:get 200 / create 201 / ingest 201 / delete 200 綠;**patch 403 = KBDB PATCH org-check bug**(已交 kbdb/docs,非 recipe 問題) + - `kbdb_upsert_block` **未降**:KBDB 無 upsert endpoint,是 client 拼湊 → 已交 KBDB feature request 出 `POST /blocks/upsert`,源碼暫留 +- [x] `gmail`/`telegram`/`line_notify`/`google_sheets` 降級:建 recipe(gmail_send / telegram_send / line_notify_send / google_sheets_read|append)+ 補 auth_recipe(line_notify / telegram),刪 4 零件目錄 + - telegram_send:✅ auth 注入驗收(inject.path,400 chat not found = 過認證) + - gmail/sheets/line:⚠️ **未驗收:缺 credential**(google SA JSON / line token),recipe + auth 鏈路本身已驗正確 +- [x] `claude_api` + `km_writer` **未降,標 deferred**:是 Mira 自用服務的膠水,不該是公共零件/recipe → 交 Mira refactor 文件(收成工作流後刪),源碼暫留 +- [x] `ai_transform_compile` + `ai_transform_run` **刪除**:Arcrun 是 AI 呼叫的工具、不該內嵌 AI 節點回頭呼叫 AI(誤搬 n8n 心智模型,見 DECISIONS) +- [x] 灰色地帶裁決:`cron` / `platform_crypto` 留(引擎能力,組工作流必要) +- [ ] KBDB recipe 採 Supabase 模式:顯示在公共零件庫,要用需申請 API Key(**未做**,屬服務側/封測後) +- [x] 「降級」定義:萃取 endpoint/method/auth 寫成 API recipe → 刪零件目錄(已照做) +- [ ] **遺留:orphaned 部署 worker 待 `wrangler delete`**(richblack 手動,rule 05): + arcrun-kbdb-{get,create-block,patch-block,delete,ingest} / arcrun-{gmail,telegram,line-notify,google-sheets} / arcrun-ai-transform-{compile,run} +- [~] 降級後 registry/components/ 現 22 個:17 白名單 primitive(流程6+資料7+http_request1+auth3) + + cron/platform_crypto(引擎能力)+ claude_api/km_writer/kbdb_upsert_block(deferred) + +### 步驟 4 — 補零件庫真把關(2026-05-30 收尾:投稿改走 PR) + +> 方向修正:零件投稿走 **GitHub PR**(人 merge=閘門、CI 跑把關),廢 registry self-service。 +> SDD:`.agents/specs/component-gatekeeping/`(含完整決策過程)。 + +- [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 從語義寫,不機器自動化)— 不急 + +### 步驟 5 — ~~建零件的人類閘門 + 白名單 hook~~(2026-05-30 被 PR 方向取代) + +> **取代說明**:原規劃「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「資料外流警示」(見下方第一期新增項)。 + +**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 + (它們現在沒有自己的 .git,是被 inkstone-matrix 追蹤的子目錄——刪 matrix/.git 前必須先安置) +- [ ] 分揀 matrix 的孤兒檔案(清單見下方附錄) +- [ ] arcrun / kbdb / ghost 各自 `git status` 確認乾淨、已 push +- [ ] GitHub 上 `inkstone-matrix` 設為 archived(不刪) +- [ ] 本機刪 `matrix/.git`,matrix 降級成普通資料夾 +- [ ] 比對兩份 cypher-executor(matrix 頂層 vs arcrun 底下),釐清死活 + +### 步驟 7 — 收尾(原步驟 6) +- [x] `arcrun-mindset` Skill(給 AI 操盤手的世界觀,見 DECISIONS §7 層三) + → `skills/arcrun-mindset/SKILL.md`(2026-05-30)。涵蓋:工作流是default/零件稀有、 + AI→工具、arcrun不做授權、暴露需人類同意、誠實(不假綠/不假防偽/不代替人類確認/完成=客觀證據)。 + 同步 `.claude/rules/06-mindset.md`(arcrun repo 內 CC 開發時讀)。 +- [ ] 會回嘴的 CLI(acr 偵測走歪 → exit 2 + 指回正路,見 DECISIONS §7 層二) + → 部分已做:pre-bash hook 擋假零件/暴露動作 + 指回正路;CLI 本身的 exit-2 回嘴待補 +- [ ] README 重寫成單一路徑(砍掉「玩法一/二/三」三選一) +- [ ] `acr init --self-hosted`:貼 CF token → 自動建 KV、部署 Worker、自動 workers.dev + subdomain、寫回 config(用戶自己建 token,CLI 不代管) + +--- + +## 第一期之後 / 待決策(不要現在做) + +- [ ] **用戶自己的 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 展開」功能** + (`recipe-expander.ts` 仍讀 `env.KBDB_BASE_URL`、`prompt-recipe-schema.ts` 的 `kbdb_block` 型別) + 按定位應移除(屬 KBDB 訂閱層),但牽涉型別,需單獨想清楚 +- [ ] **決策:開源版遙測去 KBDB 化** + (`telemetry.ts` 的 `kbdbCreateBlockUrl` + fetch 到 `arcrun-kbdb-create-block`) + 開源版遙測寫去哪?本地?關掉?寫用戶自己的 ANALYTICS_KV? +- [ ] 新增 `kv_store` 暫存 primitive:解決「workflow 中途暫存」需求(像 n8n data table)。 + 用 Arcrun 執行環境內建暫存,不依賴外部服務 → 可為 primitive。 + 介面要抽象(CF 上是 KV,wazero 上是本地檔/記憶體),才能三層通用 +- [ ] 「專案」概念落地:CLI 的 `acr project` 指令、專案=引用 workflow 的三元組 +- [ ] 公共零件庫 KBDB 語義搜尋:AI 查意圖 → 回傳零件選項;self-hosted 用本地關鍵字比對 +- [ ] 事後機制第二層:不變式測試套件(核心原則寫成自動測試,接 CI / pre-commit) +- [ ] 事後機制第一層:執行軌跡完整化(基於現有 `executions.ts`) +- [ ] `acr project init`:把閉環三件(mindset Skill + CLAUDE.md 判準 + exit-2 hook + + 不變式測試)一次裝進用戶專案 +- [ ] 視覺化 Skill:固定格式的「給人看的圖」(取代 arcrun-gui 拖拉畫布)。 + 目的是「讓人看完放心地說『跑吧』」,是信任產出物不是美術產出物 +- [ ] arcrun-gui 處置:降級為「零件庫 / recipe 貢獻者牆」展示站,掛 arcrun.dev(非工具) +- [ ] arcrun-mcp 對齊:`u6u_*` → arcrun 命名、`finally.click` → arcrun.dev、 + 移除 GUIDE.md 教 `api_config` 的反模式、確認 MCP 是薄殼 +- [ ] `acr update` 設計:常態只拉 recipe;動到 primitive 一定要用戶確認,不可默默 deploy +- [ ] recipe 「貢獻回公眾」的路徑:別人測好的 recipe 怎麼變成所有人能用(飛輪的關鍵) + +--- + +## 附錄:matrix 孤兒檔案分揀清單(步驟 5 用) + +**移進 arcrun:** +- `.agents/specs/arcrun/`、`.agents/specs/u6u-core-mvp/`、`.agents/specs/u6u-platform-evolution/` +- `.agents/steerings/`(tech.md / coding-rules.md / product.md / structure.md) +- `docs/user_requirements/arcrun/` +- `cypher-executor/`(matrix 頂層那份——先比對死活) + +**移進 arcrun-gui(若保留):** +- `.agents/specs/u6u-gui-platform/`、`.agents/specs/prototype-editor/`(先看一眼確認) + +**移進 ghost / 其他:** +- `ghost.md`、`identity/`、`personality-system/`、`persona-sdk/` → ghost +- `.agents/specs/matrix-admin-api-fixes/`、`inkstone-admin/` → inkstone-admin + +**需判斷(多半過時,留在 archived 的 inkstone-matrix 即可):** +- `API-CATALOG.md`、`AVM-PROTOCOL.md`、`MODULE-CONTRACT.md`、頂層 `CLAUDE.md` / `GEMINI.md` +- `docs/user_requirements/rearrange.md` / `test.md` +- `.claude/` / `.github/` / `.vscode/` / `.wrangler/` / `.swarm/` / `ruvector.db` + +## 未來方向:本機零帳戶試玩(local playground)— leo 2026-06-15 提,記錄不插隊 + +**洞察**:self-hosted 最低門檻入口=裝 wrangler → 本機 miniflare 跑 arcrun → **連 CF 帳戶都不用**先玩玩看。 +**現有障礙**(為何現在不開箱即用): +- `acr init --self-hosted` 第一步強制貼 CF Account ID + API Token(self-hosted-init.md),無「無帳戶本機模式」入口。 +- `wasmWorkerUrl()`(component-loader.ts:59)寫死組 `https://arcrun-{x}.{subdomain}.workers.dev`,假設零件部署在 workers.dev,非 localhost。 +- 多 worker(cypher/kbdb/registry + 零件)串接 + service binding 要本機編排。 +**要做**(未來,非現在): +- `acr init --local`:跳過 CF token,subdomain/URL 走 localhost。 +- `wasmWorkerUrl()` local 模式組 `http://localhost:port`。 +- 多 worker 用 wrangler 本機多服務模式編排(D1/KV miniflare 模擬,已支援)。 +**現況可行的最小片**:單 worker(kbdb)`wrangler dev` + D1 模擬本機跑得起來(=KBDB 插件整合測試路徑)。全系統一鍵本機跑才是新功能。 +**優先級**:未來方向,不插現在隊列(mira 解耦 + KBDB 缺口優先)。 + +## 未來方向:SaaS ⇄ self-hosted 雙向遷移(一等公民)— issue #3 待辦 2 / wishlist C7(2026-06-24) + +**緣由**:14-E(leo 把 KBDB 資料從官方 SaaS 搬回 leo21c self-hosted,458,357 筆 gap=0 完成)暴露框架級缺口。 +過程因 `KBDB_BASE_URL` fallback bug(已修,issue #2 commit 9c4333d)+ **沒有遷移工具**,資料默默寫錯庫 +(~11 萬筆 `owner_id=leo` 誤寫進官方 prod kbdb,清理 SOP 見 `docs/5-records/2026-06-24-official-kbdb-cleanup-leo-misdelete.md`)。 +今天是 leo dogfood 踩了能修;**若是未來真實用戶 = 數據災難 + 信任崩塌**。 + +**缺口本質**:arcrun 沒有「SaaS ⇄ self-hosted 雙向遷移」的框架級一等公民支持。 +leo 今天靠 dump + 手寫翻譯腳本 + 階梯式踩雷(`d1 import` 被 wrangler 4 移除 / statement 切斷 / +SQLite 深度上限 / SQLITE_TOOBIG / 超大 content)硬趟過去。 + +**要做**(backlog,不急): +- `acr migrate` 一等公民路徑:schema 翻譯 + 資料層整包灌 + 驗數 + **隔離保證(絕不寫錯庫)**。 +- 遷移前**庫歸屬驗證**(probe:寫一筆確認落在預期庫)→ 把今天的 fallback 災難從源頭擋掉。 +- **雙向**:self-hosted → SaaS(上雲)和 SaaS → self-hosted(下雲)都要順。 +**牽動**:arcrun-cli + cypher-executor + kbdb。**優先級**:未來方向,不插現在隊列。 +**頂層願景源**:`docs/1-vision/product-wishlist.md` C7。 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..16f84ce --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,97 @@ +# CLAUDE.md — arcrun + +> **上游約束(InkStoneCo 總管)**:本 repo 是 InkStoneCo 大專案的子專案,受頂層知識庫約束。 +> 動工前讀 `github.com/uncle6me-web/InkStoneCo` 的 CLAUDE.md + `docs/3-specs/`。 +> 關鍵鐵律:禁止跨 repo 同步 Actions / Mira≠Arcrun 分層(arcrun.dev 是框架域名,非產品入口)/ self-hosted 用 namespace 明碼非 api key / 部署繞開 GitHub(wrangler 直推)。 +> 本 repo 的待整合交棒見 `docs/HANDOFF-matrix-rearrange.md`。 + +> **與 InkStoneCo 總管的溝通走本 repo 的 GitHub issue**:總管交辦框架 bug / 跨專案需求 → +> 在本 repo 開 issue;CC 用 `gh` 讀(`gh issue list/view`)、修完在同一 issue `comment` 回報修法 + 驗證證據。 +> issue thread = 雙向溝通 + 永久記錄。**有事才讀,禁自動輪詢**(flag 安全界線)。 +> 結案時機由「end-to-end 實證綠燈」決定——尚待人/總管確認的留 open。詳見 `/issue-handle` skill。 +> 發 issue 給**別的 repo** 要先問人類,不可擅自。 + +> 本檔是**索引 + 最高原則**,詳細規範拆到 `.claude/rules/`。 +> Hook 強制機制在 `.claude/hooks/`,違反會直接 block(exit 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(含 auth primitive)。self-hosted same-zone 1042 用 `global_fetch_strictly_public` flag 解,不新增 binding(credential-primitives-wasm Phase 7) +5. **每個 WASM 零件 = 獨立 Worker = 公開 URL**;不從 R2 動態讀(R2 只 Phase 5 啟用) +6. **修改現有程式碼,不是新建資料夾重做** +7. **每完成一個 task 立刻更新 tasks.md 的 `[x]`**,不批次 +8. **薄殼原則**:能力只實作一次放在 API;CLI/MCP/lib 是薄殼,不得自帶業務邏輯 / 拼裝 API / 用 recipe 補 API 缺口(見 `.claude/rules/07-thin-shell.md`,hook 7.x 部分強制) + +--- + +## 工作流程(強制) + +開始任一任務,按順序: + +1. **讀 wiki**(3 分鐘快速進度)→ `.claude/wiki/status.md` +2. 讀 `docs/3-specs/arcrun/arcrun.md`(總進度) +3. 讀對應的 SDD `design.md` + `tasks.md` +4. 在回覆開頭貼出: + ``` + 📋 已讀 SDD:<清單> + 🎯 本次對應 task:<編號> + 📐 本次 task 的 SDD 規範摘要:<重點> + 🚧 執行範圍:修改/建立/刪除 <檔案> + ``` +5. 動手前把 tasks.md 對應 task 標為 `[🔄]`,完成後標 `[x]` +6. 完成後確認:是否需要同步更新 design.md? + +找不到對應 SDD → **停手問 richblack**,不要自行建立。 + +--- + +## Wiki(每次 session 的讀取順序) + +| 檔案 | 時機 | 用途 | +|------|------|------| +| `.claude/wiki/status.md` | session 開始第一件事 | 當前 Phase、進度、下一步 | +| `.claude/wiki/mistakes.md` | 做新功能前 | 10 大常犯錯誤 + 快速檢查清單 | +| `.claude/wiki/decisions-summary.md` | 遇到設計判斷時 | 6 大架構決策 + trade-off | +| `.claude/wiki/INDEX.md` | 找不到東西時 | wiki 導引 + 快速導航 | + +--- + +## 詳細規範索引 + +| 檔案 | 內容 | +|-----|------| +| `.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 索引 | +| `.claude/rules/05-deploy-convention.md` | 部署慣例(新 Worker = 新目錄 + wrangler.toml) | +| `.claude/rules/06-mindset.md` | mindset(為什麼層) | +| `.claude/rules/07-thin-shell.md` | 薄殼原則鐵律(能力長在 API,介面只暴露) | + +--- + +## SDD 位置速查 + +| 子系統 | 路徑 | +|-------|------| +| **進行中** Credential Primitives WASM | `docs/3-specs/arcrun/credential-primitives-wasm/` | +| arcrun 總進度 | `docs/3-specs/arcrun/arcrun.md` | +| Auth Recipe 系統 | `docs/3-specs/arcrun/auth-recipe.md` | +| Landing Page | `docs/3-specs/arcrun/landing-page.md` | +| SDK + Website | `docs/3-specs/arcrun/sdk-and-website/` | +| arcrun MVP 整體 | `docs/3-specs/arcrun-core-mvp/` | +| Platform Evolution | `docs/3-specs/arcrun-platform-evolution/` | +| Credential 長期規格(需求源) | `docs/user_requirements/credential_parts.md` | +| Tech Stack 詳細 | `docs/3-specs/tech.md` | + +--- + +## 封測狀態 + +**推遲**(richblack 2026-04-19 決定)。先完成 Phase 1-3 清除違規 TS,再啟動封測。 diff --git a/docs/HANDOFF-config-scope-and-vectorize.md b/docs/HANDOFF-config-scope-and-vectorize.md new file mode 100644 index 0000000..8c899bf --- /dev/null +++ b/docs/HANDOFF-config-scope-and-vectorize.md @@ -0,0 +1,100 @@ +# HANDOFF: arcrun config 寫入端 + 讓 AI 看懂 scope + KBDB Vectorize 開關 + +> 來源:InkStoneCo 頂層總管,2026-06-15。mira self-hosted dogfood 踩出的兩條框架缺口。 +> 兩條都是 `change`,走 SDD 協議(先讀對應 SDD、宣告、改 tasks.md)。 +> 觸發實證:mira(一個 AI)被交代遷移時**沒看懂 scope 存在**,差點誤部官方帳號。 + +--- + +## 缺口 1:config 讀分層完整、寫只能全域 + AI 看不懂 scope + +### C.1 補 `acr init` 專案層寫入端 + +`cli/src/lib/config.ts` 讀取分層完整(`findProjectConfig()` env > 專案層 > 全域,8/8 測試),但**寫入只有 `saveConfig()`(config.ts:208)寫死全域** `~/.arcrun/config.yaml`(config.ts:38)。`acr init` 4 處呼叫(init.ts:93/142/278)全寫全域。 +- → `acr init` 無法寫專案層。leo 意圖 project scope 卻被迫污染全域 → 怕誤打官方 uncle6me。 + +**修法**: +- `saveConfig()` 加 scope 參數(`'global' | 'project'`);project 寫 `./.arcrun.yaml`(`process.cwd()`)。 +- `acr init` / `acr config set` 加 `--project` flag。 +- 預設是否翻轉(init 預設寫專案層)由 richblack 定(影響既有使用者)。 +- 參考既有範式:`mcp-setup.ts` §7.8 已有 project scope 寫 `.mcp.json` 概念,比照。 + +### C.2 ★ 改 `cli/harness/CLAUDE.block.md` 讓操作 AI 看懂 scope(最重要) + +**病根**:arcrun 是 AI 操作的工具(mindset §2),但 harness block(全文 41 行)對 scope / 帳號歸屬 / 誤部風險 **0 字提及**。AI 讀完完全不知道有 scope 這層 → mira 把「資料遷移」跟「在哪 scope 部署」混為一談。 + +在 harness block 加「部署落點與帳號安全」段: +1. **scope 模型**:分層 `env > 專案層 .arcrun.yaml > 全域 ~/.arcrun/config.yaml`。**部到哪個 CF 帳號取決於當前資料夾解析到哪份設定**。動手前先 `acr config --where`。 +2. **多帳號警示**:同機可能多個 CF 帳號(dogfood / prod);`.env` 可能並存多組 `CLOUDFLARE_*`(含 `*_UNCLE6` 後綴)。部署/遷移前必須明確目標 account_id,挑錯=打到正式環境(不可逆)。 +3. **遷移 ≠ scope**:搬資料(A 帳號→B 帳號)和「我用哪個帳號」是兩件獨立的事。 +4. **fallback 陷阱**:任一層都沒 `mode` → `acr` 靜默當 `local`(config.ts:176),不報錯。看到「成功」但無雲端 trace 先查 `acr config --where`。 +5. **無設定就停手**:`--where` 顯示無設定 / fallback local 而意圖 self-hosted → 停下問人。 + +> harness block 是 AI 的 mindset 入口,比 CLI `--help` 重要(AI 偏好讀 harness)。scope 安全屬「動手前世界觀」,必在這層。 + +**對應 SDD**:`docs/3-specs/arcrun/sdk-and-website/config-layering.md`。 + +--- + +## 缺口 2(任務 D):KBDB Vectorize(embed)無環境開關 + +### 病根 + +KBDB 分 `base`(D1 only,免費)+ `embed` module(Vectorize+AI binding,語意搜尋)。但 `acr init/update` 完全沒處理 embed(grep `vectorize|embed` 在 `cli/src/` = 0 命中);只活在 `kbdb/src/index.ts:5` 註解。→ **沒有 config 欄位 / 指令讓使用者或 AI 表達「要不要開 Vectorize」**。 + +### 設計哲學(leo 2026-06-15):降級要優雅,但「完整體驗」要有門 + +> 「最好是我自己這套可以語義搜尋;不開就降級;**但想完整體驗卻沒有門**。」 + +- **預設降級且能跑**:沒開 embed → base 仍可關鍵字搜尋(不是壞掉)。 +- **降級要明示不能無聲**:跑 base 時要讓人知道「語意搜尋未開」。無聲降級=用戶以為這就是全部=bug。 +- **★ 升級要有門**:config 欄位 + `acr` 提示「想語意搜尋?這樣開」。現在連「有更完整版本可選」都不可見。 + +### 修法(config 欄位 + 部署條件注入,比照 MULTI_TENANT) + +1. **config 加欄位**:`ArcrunConfig`(config.ts:11)加 `kbdb_embed?: boolean`(預設 false=base 免費)。leo dogfood 設 true。 +2. **部署條件注入**:`acr update` 部 KBDB worker 時 `kbdb_embed: true` → 注入 Vectorize+AI binding + 部 embed module;false 只部 base。比照 `deploy.ts:395` MULTI_TENANT 注入、`deploy.ts:380` WORKER_SUBDOMAIN 注入。 +3. **降級可見**:`acr config --where` / 部署摘要顯示「KBDB: base(語意搜尋未開)」或「embed(Vectorize 啟用)」;base 時提示升級門。 +4. **AI 認知**:harness block(C.2 那段)一併說「KBDB 預設 base(D1 免費僅關鍵字);語意搜尋需 `kbdb_embed: true`(Vectorize 計費)。功能需語意搜尋但這套是 base → 別假裝有,明示降級指出升級門」。呼應 mindset §7 誠實。 + +**對應 SDD**:KBDB 模組化屬 `docs/3-specs/` kbdb spec;部署注入屬 `sdk-and-website/self-hosted-init.md`。 + +--- + +## 任務 E(★ 急,擋 mira 14-E load):給 mira「leo21c kbdb worker 實際綁的 D1 id」 + +> 總管裁定(2026-06-15):mira 14-E load 撞 Workers 每日 10萬 request 牆(逐筆 API 病根)→ 改走 `wrangler d1 import`(資料層搬遷=用戶 CF 權限,繞 API)。**但 mira 需要 D1 id 才能灌對庫。** + +**為什麼 mira 自己查不到**: +- cypher-executor **無** D1 binding,轉發給 **kbdb worker**。 +- kbdb worker 的 D1 id 是 **deploy.ts 部署時動態注入**(`kbdb/wrangler.toml` 靜態值 `0c580910…` 是官方 prod,self-hosted 部署會覆蓋)。 +- → leo21c 上 kbdb worker 實際綁的 D1 id ≠ mira `wrangler` 直連的 `arcrun-kbdb`(`1099d0f3…`)。這是「wrangler 查 0 筆、cypher 有 11 萬筆」的真相(兩個同名不同 id 的 D1)。 + +**你要做**:用 leo21c CF token 查 leo21c 上 **kbdb worker 部署時實際注入的 D1 database_id**(看 deploy.ts 注入邏輯 / 查 leo21c worker runtime binding),告訴 mira。mira 拿到才能 `wrangler d1 import <該 id>` 灌對庫。 + +## 任務 F(B,框架缺口,非急):cypher `/kbdb/entries` 補批次端點 + +> 總管裁定 B:應用層大批寫入該有批次 API。**未來每個 self-hosted 用戶大批匯入都撞 10萬 request 牆**(不只 mira)——逐筆 POST 每筆 1 request,~5萬筆燒光免費 worker 當日配額。 + +- `POST /kbdb/entries` 收陣列(`[{...},{...}]` 或 `{entries:[...]}`),1 request 寫 N 筆 → 45.8萬筆只要 ~900 requests。 +- 守鐵律(只轉發 API、不開 SQL/建表;owner_id 仍自動注入每筆)。 +- 對應 SDD:kbdb-proxy 屬既有範圍。 + +## 任務 G(雜項,mira 14-E 踩出的 cypher 框架特性,記著) + +mira load 踩出,記給框架修(非急): +1. CF WAF 1010:Python urllib 預設 UA 被擋 → 文件提醒帶 UA。 +2. 重複 id → 500(非 409):UNIQUE 衝突宜回 409 友善處理。 +3. 零星單筆 500:某些 block 穩定 500、同 content 換 id 卻 200=邊緣 bug(疑 metadata_json/refs_json 某字元觸發,未定位)。 +4. `?id=` query 被忽略(回前 100 筆不過濾);查單筆只能 `/entries/:id` 路徑——文件宜明示。 +5. **DELETE `/kbdb/entries/:id` route 缺**(OPTIONS 卻宣告支援 DELETE,CORS 宣告與實作不一致)。 +6. cypher 綁的 D1 與同名 `arcrun-kbdb` database_id 不一致(見任務 E)——self-hosted 文件宜說明「wrangler 直連 ≠ cypher 綁的庫」。 + +## 共通教訓 + +任務 C、D 同類:**把「該是環境設定的選擇」從 code 註解提升到 AI/使用者可表達的層級**。arcrun 是 AI 操作的工具,凡影響「部到哪、開什麼功能」的選擇都必須在 config + harness 可見,不能只活在實作裡。 +任務 E/F 同類:**資料層搬遷(用戶 CF 權限 d1 import)vs 應用層大批寫入(cypher 批次 API)是不同層級,都該存在**——逐筆 API 推大批資料是把「資料遷移」做成「應用層寫入」,撞 worker 請求上限。 + +## 依賴 + +mira 側(建專案層 .arcrun.yaml + 移全域)先行,見 mira repo `docs/HANDOFF-arcrun-config-project-scope.md`。本框架補強讓「未來不再污染全域 / AI 不再看不懂 scope」,是根治,非阻擋 mira 當前解耦。 diff --git a/docs/HANDOFF-matrix-rearrange.md b/docs/HANDOFF-matrix-rearrange.md new file mode 100644 index 0000000..71495f3 --- /dev/null +++ b/docs/HANDOFF-matrix-rearrange.md @@ -0,0 +1,151 @@ +# HANDOFF: Matrix 重整交棒給 arcrun(2026-06-13) + +來源:InkStoneCo 頂層 `.agents/specs/matrix-rearrange/`。本檔是該重整交給 arcrun 的待辦清單。 +**指針式考古**:整合素材真身在 InkStoneCo `_archive/`,照路徑去挖,不複製進此檔。 + +--- + +## 1. cypher-executor 整合進 arcrun 後**關掉**(leo 2026-06-13:整合後只剩 arcrun) + +`matrix/cypher-executor` 是 diverged 副本,**整合進 arcrun/cypher-executor 後就關掉/封存**,之後 cypher 只有 arcrun 一份。 + +### 如何整合(勘查 2026-06-13) + +**A. matrix 版獨有、arcrun 缺的 5 檔 → 補進 arcrun**(皆非 SaaS 遺留,是 self-hosted 核心): +- `src/actions/version-selector.ts`:零件版本選擇策略 floating/stable/pinned +- `src/actions/autoPublishMissing.ts`:missing 零件用 Workers AI 自動生成上架 +- `src/lib/component-dispatcher.ts`:雙模式路由 wasm/cypher_binding/service_binding +- `src/lib/wasm-executor.ts`:WASM 執行 +- `src/routes/proxy.ts`:proxy 路由 + +**B. 兩邊都有但內容不同的 10 檔 → 逐檔比對合併**(取較完整/正確的一邊,保留 arcrun 較新的 auth 演進): +`cypher-handlers` / `execution-evaluator` / `execution-logger` / `graph-builder` / `search-nodes` / `triplet-parser` / `webhook-graph-resolver` / `webhook-handlers` / `graph-executor` / `index` + +**C. 保留 arcrun 獨有的**(不要被舊版覆蓋):`credential-injector` / `auth-dispatcher` / `auth-recipe-seeds` / `api-recipe-seeds`(arcrun 較新的 Auth Recipe 演進)。 + +**素材真身**:`matrix/cypher-executor/`(降級後仍在原地)。整合完成、驗證通過後,**封存 matrix/cypher-executor 進 `_archive/`,cypher 之後只有 arcrun 一份**。先讀對應 SDD 再動。 + +## 2. KBDB 插件化 + 補 CLI/MCP 薄殼(arcrun 端只留基本盤 + 暴露能力) + +`arcrun/kbdb` **留 3 表基本盤 + API**(已完整:templates/entries/records/search)。它是刻意設計的基本盤(0001_base.sql 註釋 plugin model),**不升 v3、不加 blocks 表**。triplet/graph 由 `matrix/kbdb-graph-plugin` 抽成獨立 repo KBDB-graph。 + +**KBDB 鐵律(leo 2026-06-14)**:任何人不准動表;新類型=建 template(走 API);插件/AI/人全走 API,禁 SQL;基本盤不提供建表 API。詳見頂層 `DECISION-kbdb-v3-baseplane.md`。 + +**arcrun 端待辦(核實:CLI/MCP 現在完全沒 KBDB 能力)**: +- **補 MCP 薄殼**(AI 用,含插件):`kbdb_create_template`(name+slots)、`kbdb_create_record`(填 slot)、`kbdb_query`/`kbdb_search` 等,調基本盤現有 API。**不提供建表 tool,只給 template/slot**——類 Supabase 萬用表,AI 想建表時只有 template/slot 可用。 +- **補 CLI 薄殼**(人用,後補):對應命令。 +- 能力真身在基本盤 API(已有),CLI/MCP 只薄殼暴露(arcrun 薄殼原則)。 + +對方交棒見 `matrix/kbdb-graph-plugin/docs/HANDOFF-kbdb-plugin.md`。 + +## 3. leo21c self-hosted 部署(Mira dogfood 用) + +leo 用 leo21c CF 帳號部署 self-hosted arcrun(`MULTI_TENANT=false`),Mira 改 dogfood 這套。 +依現有 `scripts/local-deploy.sh` + `docs/3-specs/arcrun/sdk-and-website/self-hosted-init.md`。namespace 明碼非 api key。 + +### 3b. ⚠️ MCP self-hosted 認證失敗(mira CC 2026-06-14 回報,跨專案問題) + +**一句話**:MCP worker 還走舊 partner-key 認證(`mcp/src/middleware/partner-auth.ts`,每個端點都掛 `partnerAuthMiddleware`),但 self-hosted 認證是 namespace 明碼。導致 Claude Code 連 self-hosted MCP 一律 401。**CLI 全通**(走 cypher-executor,已支援 `MULTI_TENANT=false`)。 + +**根因**:MCP 的 `partner-auth.ts` 沒跟上 cypher-executor 的 self-hosted 認證改版。這是 `07-thin-shell.md §4` 已記的「MCP 帳號來源違反」的具體症狀(SDD proposal 已存在:`docs/3-specs/arcrun/sdk-and-website/mcp-account-source.md`)。 + +**診斷證據(curl `arcrun-mcp.leo21c.workers.dev/mcp`,mira CC 實測)**: +- GET /mcp → 200(worker 活);MCP initialize 無 auth → 401;Bearer ak_(舊 uncle6 key)→ 401 Invalid partner key;Bearer leo(namespace 明碼)→ 401;X-Namespace header → 401 +- cypher-executor / → 200(CLI 走這條,通) +- `acr mcp-setup` 生成的 `.mcp.json` 是裸的(無 headers),就算有 key 也沒地方帶 + +**修法(三選一,arcrun 端決策)**: +1. MCP worker 加 self-hosted 認證:接受 namespace 明碼(與 cypher-executor 一致),或 self-hosted 模式 MCP 免 partner key。 +2. `acr mcp-setup` 把有效認證寫進 `.mcp.json` headers(前提:self-hosted 有可用 key 機制)。 +3. 對齊完成前,官方文件明說 self-hosted MCP 暫不可用、請用 CLI(避免使用者困惑)。 + +**影響範圍**:任何 self-hosted dogfood(mira、未來 product)都踩,非 mira 獨有。屬 arcrun 框架側待修,走 SDD 協議(對應 `mcp-account-source.md`,動工前宣告)。 +**mira 現況**:全靠 acr CLI 即可推進,不卡。`.mcp.json` 留著等上游修好自動能連。 + +--- + +### 3b-2. ⚠️ 第一次端到端實測:修補 code 對,但 `MULTI_TENANT` 沒注入 MCP worker(2026-06-14 晚,mira 推 leo21c + 總管核實) + +mira 把 release@main 推上 leo21c(deployment 14:31,version 7de919d6),**仍 401 `Invalid or expired partner key`**。總管核實了真因(不是 code bug,是部署機制 bug): + +- ✅ **修補 code 在且正確**:`mcp/src/middleware/partner-auth.ts:19` `if (c.env.MULTI_TENANT === 'false')` 確實擋在 partner-key 查詢之前。 +- ☠️ **真因:`mcp/wrangler.toml` 的 `MULTI_TENANT = "false"` 是注釋掉的(第 13-14 行 `# [vars]` / `# MULTI_TENANT`)**。部署後 worker 的 `c.env.MULTI_TENANT === undefined ≠ 'false'` → if 不成立 → 走 partner-key 查詢 → 401。 +- 🔍 **更深層:self-hosted 部署(`acr init`/`acr update`)沒把 `MULTI_TENANT=false` 注入 MCP worker 的 vars**。`cli/src/` grep `MULTI_TENANT` 只有 config 定義 + 註釋,**無「部署時注入 worker env」的 code**。對照:cypher-executor 通是因它把 key 當不驗證 opaque(不依賴 MULTI_TENANT);MCP 依賴此 env 才走 namespace 分支,故漏注入就斷。 +- 類比:self-hosted-init.md 注入了 `WORKER_SUBDOMAIN`,但**漏注入 `MULTI_TENANT`**(同一類部署注入機制的缺口)。 + +**修法(arcrun 端,二選一或都做)**: +1. `acr init/update` 部署 MCP(及需要的 worker)時,依 config `multi_tenant: false` 注入 `MULTI_TENANT=false` 到 worker vars(與注入 WORKER_SUBDOMAIN 同套機制,self-hosted-init.md)。 +2. 短期:文件指引 self-hosted 用戶手動 `wrangler secret put MULTI_TENANT`(或取消 mcp/wrangler.toml 那兩行註釋)——但這違反「用戶零填寫」,①才是正解。 + +**驗收**:leo21c MCP worker 設好 MULTI_TENANT=false 後,`curl -H "Authorization: Bearer leo" .../mcp` initialize → 200(非 401)。 + +## 4. arcrun-gui 併入 + arcrun.dev 降官網 + +arcrun-gui 不再獨立,GUI 併入 arcrun repo(用戶下載即有)。arcrun.dev 降級為框架官網,移除 /mira/ 寄居(Mira 搬 mira.uncle6.me)。 + +## 5. arcrun-mcp → 已整進 arcrun/mcp,**直接關掉**(leo 2026-06-13) + +勘查確認(2026-06-13):`arcrun/mcp` 已是真身,**比 matrix/arcrun-mcp 多 `arcrun_recipe.ts` + `arcrun_whoami.ts`,且 matrix/arcrun-mcp 無任何 arcrun/mcp 缺的東西**(`Only in arcrun-mcp` 為空)= **已全部整進去**。 +動作:`matrix/arcrun-mcp` **直接關掉**——封存進 `_archive/` 即可,不遷帳號(舊 repo richblack/arcrun-mcp 留歷史)。無需整合,arcrun/mcp 就是現役。 + +## 6. KBDB 資料層遷移:3 個框架缺口(mira CC 2026-06-14 回報 + 總管核實,擋 mira 遷移) + +mira 實測 leo21c self-hosted KBDB:**好消息 entries 表 block-compatible**(content/entry_type/parent_id/page_name/refs_json/tags_json/task_status/metadata_json = mira block 模型),河道/wiki/triplet 可直接落 entries,不必改資料模型。但卡 3 個 arcrun 缺口: + +**① 主缺口(擋遷移):cypher proxy 漏 `/kbdb/entries`** — 核實屬實。 +- `cypher-executor/src/routes/kbdb-proxy.ts` 只實作 `/kbdb/templates` + `/kbdb/records`(注釋自稱含 entries,實際沒有)。基本盤 `arcrun/kbdb` 有 `/entries`(index.ts:18),proxy 沒轉發。 +- 結果 mira 三者湊不齊:直連 kbdb worker /entries=有 block CRUD 但裸開無隔離;cypher /kbdb/*=有認證+owner_id 隔離但無 /entries。 +- **修法**:比照 `/kbdb/records` 的 owner_id 注入模式,補 `/kbdb/entries`(POST/GET filters/GET :id/PATCH/DELETE)。守鐵律(只轉發 API,不開 SQL/建表)。補好 mira `_kbdb_client.py` 改走 `cypher.leo21c/kbdb/entries` + X-Arcrun-API-Key namespace 隔離 → 完成解耦。 + +**② MCP 的 KBDB service binding 壞** — 核實屬實。 +- mcp/wrangler.toml 有 `{ binding="KBDB", service="arcrun-kbdb" }`,kbdb-client 用 `env.KBDB.fetch`,但 mira 報 `Cannot read properties of undefined (reading 'fetch')` = **`env.KBDB` undefined**(self-hosted 部署時 binding 沒正確建/worker 名對不上 leo21c 的 kbdb)。 +- 連帶:官方回報管道 `arcrun_report_feedback`(MCP tool)也因此送不出 → 這份回報只能靠總管轉。**修這個才恢復 self-hosted 的 MCP 回報能力。** +- **修法**:self-hosted 部署確保 KBDB service binding 正確指向 leo21c kbdb worker(或改 HTTP fetch via KBDB_BASE_URL,與插件同模式,避免 self-hosted service binding 名稱耦合)。 + +**③(非阻擋)mcp-setup namespace 不一致** — 核實屬實。 +- `cli/src/commands/mcp-setup.ts:53` 用 `config.api_key`。self-hosted 若 api_key 存的是舊 ak_(非 namespace),則 MCP 與 CLI 讀寫不同分區。 +- **修法**:self-hosted 下 mcp-setup 優先用 NAMESPACE(config.namespace),與 CLI 同一分區。 + +對應 SDD:mira 端記在 mira SDD 14-A(標 🚧 待對端);arcrun 端走協議(kbdb-proxy 屬既有 SDD 範圍)。 + +--- + +## 6b. ⚠️ 部署斷層:code 已補但 leo21c 未重部署(總管本地模擬核實 2026-06-15) + +總管不靠 Hetzner、從本機直接 curl leo21c 端點驗證,發現 **①② 的 code 已 commit 進 arcrun repo,但對應 worker 沒部署到 leo21c CF**: + +| 證據(本機 curl leo21c,namespace=leo) | 結果 | 判讀 | +|---|---|---| +| `GET cypher/kbdb/templates` | 200 | cypher-executor 活著(舊版) | +| `GET cypher/kbdb/entries` | **404** | 新 route 未上線(`b1e302b` 的 `/kbdb/entries` 沒部署) | +| `GET cypher/kbdb/records` | **404** | 連既有 records proxy 都 404 → leo21c 上的 cypher 版本落後 | +| 直連 `kbdb.leo21c/entries`(裸開) | 200 | kbdb worker 本體活著,entries 表在 | +| arcrun repo `kbdb-proxy.ts` | 含完整 `/kbdb/entries` CRUD(行 144-184) | **source 正確,純粹是沒 deploy** | + +**結論**:缺口①②的 code 修正屬實(commit `b1e302b` `/kbdb/entries` + `1af7655` KBDB service binding),但**卡在「部署到 leo21c」這一步**。在 leo21c cypher-executor 重新部署前,mira `_kbdb_client.py` 即使改好也 smoke 必 404。 + +**交棒 task(arcrun CC,依協議走既有 SDD + 鐵律「部署繞開 GitHub、wrangler 直推 CF」)**: +1. 確認 leo21c 帳號(`CLOUDFLARE_API_TOKEN` 指 leo21c `51a01bfa…`)下 cypher-executor 是哪個版本、為何落後(`acr update` 漏部署 cypher?還是只部署了部分 worker?)。 +2. 重新部署 cypher-executor(+ 確認 mcp worker 的 KBDB service binding 一併上線,缺口②)到 leo21c。 +3. 自驗:`curl -H 'X-Arcrun-API-Key: leo' https://arcrun-cypher-executor.leo21c.workers.dev/kbdb/entries?limit=1` 應回 200(非 404)。回 200 才算缺口①真正清空,mira 14-A 才解鎖。 + +**接力鏈**:arcrun 部署 cypher(6b)→ 端點 200 → mira 改 `_kbdb_client.py`(14A.1)→ smoke 讀寫 leo21c → 解耦完成。 + +### 6b-解決(arcrun CC 2026-06-15,已部署 + 自驗 200) + +**根因不是 GitHub lag**(`origin/main` == 本地 4d6e77f,含 `/kbdb/entries` route)。兩層真因: +1. **`acr update` 的 content-hash manifest 跳過機制**(`cli/src/lib/deploy.ts:198-225`)把 cypher 當「未變動」跳過 → 落後。解:`acr update --force` 清空 manifest 強制全部重部。 +2. **`.env` line 3 的 `CLOUDFLARE_ACCOUNT_ID=58309bb9…`(官方帳號)被 CLI 載入並覆蓋 config.yaml 的 leo21c `51a01bfa…`**(env > 全域 config,`config.ts:174`)→ leo21c token 對官方帳號認證 → KV 解析「Authentication error」→ update 中止。解:部署時 `CLOUDFLARE_ACCOUNT_ID=51a01bfa… node cli/dist/index.js update --force` 強制 account 對齊 leo21c token。 + - ⚠️ **遺留陷阱**:repo `.env` 是「官方帳號」部署脈絡用的;對 leo21c self-hosted 部署必須覆蓋 `CLOUDFLARE_ACCOUNT_ID`,否則 leo21c token vs 官方 account 不匹配。見記憶 [[cf-account-official-vs-loadtest]]。 + +**部署結果**:23/23 worker 全部 ✓(含 cypher-executor / kbdb / mcp),seed ✓(10 API + 23 auth recipe),cron index migrate ✓。用本地 build CLI 1.3.12(全域 acr 仍 1.3.11,未 npm publish)。 + +**自驗(本機 curl leo21c,namespace=leo)**: +- `GET /kbdb/entries?limit=1` → **200** `{"success":true,"entries":[],"count":0}`(真轉發 kbdb worker,非假綠)✅ ← 缺口①清空,**mira 14-A 解鎖** +- `GET /kbdb/templates` → 200 ✅ +- `GET /kbdb/records?limit=1` → **404(非回歸,by design)**:proxy 只有 `POST /records`、`GET /records/by-template/:t`、`GET /records/:id`,**本就無 bare list route**。HANDOFF 原以 records 404 當「cypher 舊版」訊號,但該路由從未存在。 +- 缺口②:MCP `initialize`(`Bearer leo`)→ **200**(非 401,`MULTI_TENANT=false` 已注入,KBDB binding 隨 mcp worker 上線)✅ + +--- + +> 每項動工前依 `.claude/rules/00-sdd-protocol.md` 宣告已讀 SDD。本 HANDOFF 是「有哪些事」,不是「繞過 SDD 的捷徑」。 diff --git a/docs/HANDOFF-mira-repair.md b/docs/HANDOFF-mira-repair.md new file mode 100644 index 0000000..a167554 --- /dev/null +++ b/docs/HANDOFF-mira-repair.md @@ -0,0 +1,120 @@ +# 交辦文件:修復 Mira 的 arcrun workflow(給 Mira 的 CC) + +> 建立:2026-06-03(由 arcrun 端的 CC 撰寫) +> 對象:接手修復 mira 的 CC +> 位置:`/Users/youlinhsieh/Documents/tech_projects/InkStoneCo/polaris/mira/arcrun/*.yaml` +> +> **這份文件已把調查做完**:每個 workflow 用的 component 都對照過 arcrun prod 現況, +> 標清楚「哪些不用改、哪些要改、哪些要降級」。你照著改即可,不必重跑調查。 + +--- + +## 0. 為什麼 mira 壞了(一句話) + +arcrun 做了大整修:把「打固定 endpoint 卻被做成零件」的**假零件降級成 recipe + 刪掉零件目錄** +(33 → 22 個零件,DECISIONS §1:零件 = endpoint 薄殼,打固定 API 的是 recipe 不是零件)。 +mira 當初**自己把一堆東西錯做成假零件**,整修後那些零件名的解析方式變了,所以 mira workflow 斷了。 + +好消息:**arcrun prod(cypher.arcrun.dev)活著、降級後的 recipe 都在 KV**(已驗證), +mira 大部分 workflow 只需小改,不需重寫。 + +--- + +## 1. 前提(已驗證,你不用重查) + +- ✅ cypher-executor prod 活著(`cypher.arcrun.dev`) +- ✅ 降級 recipe 在 prod KV:`kbdb_get` / `kbdb_create_block` / `kbdb_patch_block` / + `kbdb_ingest` / `kbdb_delete` / `telegram_send` / `gmail_send` / `google_sheets_*` / `line_notify_send` +- mira 跑在你(richblack)的 prod 帳號上,**不依賴 self-host installer**(那是給外部工程師的另一條線) + +--- + +## 2. 逐 component 分類(mira 6 個 workflow 全部掃過) + +| component | 狀態 | 動作 | +|---|---|---| +| `kbdb_get` | ✅ prod recipe 存在 | **不用改**(`component: kbdb_get` 走解析鏈 step 6 查 `recipe:kbdb_get`,正確)| +| `kbdb_create_block` | ✅ prod recipe 存在 | **不用改** | +| `kbdb_patch_block` | ✅ prod recipe 存在 | **不用改** | +| `cron` | ✅ 引擎能力 | **不用改** | +| `http_request` | ✅ primitive | **不用改** | +| `if_control` / `set` / `filter` | ✅ primitive | **不用改** | +| `trigger_workflow` | ✅ 平台 orchestration | **不用改** | +| `comp_passthrough` | ✅ 引擎內建純函式(constants.ts:43 `(ctx)=>ctx`)| **不用改** | +| **`telegram`** | ⚠️ prod **沒有** `telegram` recipe,只有 `telegram_send` | **要改**:`component: telegram` → `component: telegram_send`(見 §3)| +| **`claude_api`** | ❌ 錯做成零件(非薄殼)| **要降級**(見 §4)| +| **`kbdb_upsert_block`** | ❌ 錯做成零件(非薄殼)| **要降級**(見 §4)| + +--- + +## 3. 要改:`telegram` → `telegram_send` + +**影響的檔**(4 個用到 `component: telegram`): +- `project_detector.yaml` +- `agent_feedback_weekly_review.yaml` +- `wiki_synthesis.yaml` +- `wiki_giveup_scanner.yaml` + +**改法**:把 `component: telegram` 改成 `component: telegram_send`。 + +⚠️ **不只是改名**——`telegram_send` recipe 的介面:endpoint 是 +`https://api.telegram.org/bot{{auth.bot_token}}/sendMessage`,body 帶 `chat_id` + `text`, +auth 走 `auth_service: telegram`(static_key path 注入)。確認 mira 的節點 config 傳的是 +`chat_id` / `text`,且有設好 telegram 的 credential(`acr creds push`)。 + +--- + +## 4. 要降級:`claude_api` / `kbdb_upsert_block`(錯做成零件) + +> ⚠️ **這兩個不是「待刪」,是「做錯了」**(richblack 2026-06-03 定性): +> 它們**不是 endpoint 薄殼,是把工作流硬塞進零件**(違反 DECISIONS §1)。 +> arcrun 已把這兩個 wasm 排除在 self-host 部署來源外(不 commit 進 repo)。 +> mira 要把它們**還原成本來該有的樣子**:工作流 / recipe。 + +### 4.1 `claude_api`(影響:agent_feedback_weekly_review / project_detector / wiki_synthesis) + +**問題**:arcrun 是「AI 呼叫的工具」,**工作流裡不該有零件回頭呼叫 LLM**(mindset §2 / +DECISIONS:n8n 需要 AI 節點是因為它沒大腦,arcrun 的大腦就是操盤的 CC)。 +`claude_api` 把「呼叫 Claude」做成零件,方向就錯了。 + +**正解**(mira 要自己設計,arcrun 端只給方向): +- 需要 AI 判斷/轉換的步驟,應該是**操盤的 CC(mira 自己)做**,再呼叫 workflow 做確定性的下一步。 +- 若真的需要在 workflow 內打 Claude API(例如非同步 cron 場景無 CC 在場),那它是**打一個固定外部 + endpoint(api.anthropic.com)= recipe**,不是零件 → 建一個 `claude_api` 的 **API recipe** + (http_request + endpoint + auth_service),不是零件目錄。 +- 哪條對,mira 的 CC 要依 mira 的實際場景判斷(這是 mira 的設計決策,不是 arcrun 能替決的)。 + +### 4.2 `kbdb_upsert_block`(影響:agent_feedback_weekly_review / wiki_synthesis) + +**問題**:upsert 的邏輯(找到則 PATCH、沒找到則 POST)被整段塞進零件。 +按 DECISIONS §1,upsert 應該是 **KBDB API 那邊提供的 endpoint**,零件/recipe 只該「驅動它」。 + +**正解(兩條,mira/KBDB 端決定)**: +- **首選**:KBDB API 出一個 `POST /blocks/upsert` endpoint(richblack 已交 KBDB feature request) + → 然後在 arcrun 建一個 `kbdb_upsert` **recipe**(打那個 endpoint),mira workflow 用 recipe。 +- **過渡**:若 KBDB 還沒出 upsert endpoint,把「GET 找 → 有則 kbdb_patch_block、無則 kbdb_create_block」 + 這段**用 workflow 表達**(mira workflow 層用 if/branch 串現有的 kbdb_get + patch + create recipe), + 而不是塞進一個零件。 + +--- + +## 5. 修復順序建議 + +1. **先改 telegram**(§3)——機械改名 + 確認介面,最快,4 個檔。 +2. **驗證 kbdb_* / 其他不用改的 workflow 真的跑通**——`acr run ` 或 trigger, + 確認 §2 標「不用改」的真的 2xx(誠實驗證,不假設)。 +3. **再處理 claude_api / kbdb_upsert_block 降級**(§4)——這兩個要 mira 依場景做設計決策,較花時間。 + +## 6. 驗收(客觀證據,mindset §7) + +- 改完的 workflow `acr run` / trigger → HTTP 2xx + execution trace 證明跑通,不是口頭宣布。 +- claude_api / kbdb_upsert_block 降級後:workflow 不再引用這兩個零件名(grep 確認)。 +- 缺 credential 打不通就誠實標「未驗收:缺 X」,不 mock 充綠燈。 + +--- + +## 7. 參考 + +- arcrun 決策:`matrix/arcrun/DECISIONS.md`(§1 零件 vs recipe、§3b credential) +- arcrun mindset:`matrix/arcrun/.claude/rules/06-mindset.md`(§1 工作流是 default、§2 AI→工具) +- arcrun 故障/整修脈絡:`matrix/arcrun/docs/HANDOFF-self-host-harness.md` diff --git a/docs/HANDOFF-self-host-harness.md b/docs/HANDOFF-self-host-harness.md new file mode 100644 index 0000000..4f9f976 --- /dev/null +++ b/docs/HANDOFF-self-host-harness.md @@ -0,0 +1,212 @@ +# 交辦文件:完成 arcrun self-hosted harness(給接手的 CC) + +> 建立:2026-06-01(由前一個 CC 調查後撰寫) +> 對象:接手的外部 CC +> 目的:把 arcrun 補到「任何 CC 在自己的 CF 帳號上 self-host 後就能順暢開發、且不可能重蹈 mira 的錯」的程度。 +> +> **先讀**:`DECISIONS.md`(穩定決策)、`.claude/rules/06-mindset.md`(mindset)、`BACKLOG.md`(流動待辦)。 +> 本文件不取代它們,只是把「今天要做的三件事」連同已查證的實況整理好,讓你不用重跑調查。 + +--- + +## 0. 戰法已轉變(最重要的背景) + +richblack 2026-06-01 決定:**從 SaaS 改成 self-hosted 開源策略。** + +這直接改變 harness 的成功定義: + +- **舊定義**:在 richblack 的 prod 帳號(`cypher.arcrun.dev`)上能跑。 +- **新定義**:**任何 CC 在自己的 CF 帳號上 `acr init --self-hosted` 後就能跑通一個含 recipe 的 workflow,而且寫錯時會被程式擋住。** + +richblack 會用另一個 CF 帳號實測 self-host。所以「self-hosted 一鍵起得來」從「第一期重要但非阻擋項」**升為今日第一優先**。 + +### arcrun 現在的核心心智(richblack 2026-06-01 校準,比 DECISIONS §1 更硬) + +- 核心**零件數量少、由 richblack 維護、不接受 CC 自製**(可投稿 PR,人 merge = 閘門)。 +- 其他人做的一律是 **recipe**(= http_request + 一組 YAML 設定,不用 deploy)。 +- arcrun 是**一套給 CC 的 harness**:事前提醒 CC 能用什麼 / 不能做什麼,事後用程式擋住讓它**無法犯錯**。 +- **你不用管 mira。** mira 是錯誤做法的源頭(見 §1),它自己會修。你的目標是讓**任何** CC 都能用,且絕不會發生 mira 的錯。 + +--- + +## 1. mira 故障 = 症狀樣本(已定位,不用你修) + +mira(`/Users/youlinhsieh/Documents/tech_projects/InkStoneCo/polaris/mira/arcrun/*.yaml`)的 workflow 寫 `component: kbdb_get` / `claude_api` / `telegram` 等。這些是 **mira 當初自己錯做的「假零件」**(DECISIONS §1 判準:打固定 endpoint 的東西是假零件,該是 recipe)。 + +本次整修(BACKLOG 步驟3)已把這些假零件**降級成 recipe + 刪掉零件目錄**(registry/components 從 33 → 22)。所以 mira 斷了。 + +**這證明的事**:mira 的錯,正是當時 harness 沒擋住的漏洞。零件刪了,但 harness 還缺「**事前告訴 CC 別這樣做 + 事後擋住 CC 這樣做**」的完整機制 → 下一個 CC 還會犯同樣的錯。**這就是你要補的(§3 task 2)。** + +--- + +## 2. 已查證的實況(你不用重查,2026-06-01 實打 prod) + +### 2.1 降級後的 recipe 鏈路是「活的」✅ + +實打 `https://cypher.arcrun.dev/recipes`(richblack prod)確認以下 recipe 都在 KV: + +| canonical_id | hash | endpoint | auth_service | +|---|---|---|---| +| `kbdb_get` | rec_4c7dcf9b | `https://kbdb.finally.click{{_path}}` | kbdb | +| `gmail_send` | rec_cd426129 | gmail.googleapis.com/.../send | google_gmail_sa | +| `google_sheets_append` | rec_9fd1b662 | sheets.googleapis.com{{_path}} | google_sheets_sa | + +→ **「對的用法」(`component: kbdb_get` 走解析鏈 step 6 查 `recipe:kbdb_get`)本身是通的。** 不需要重建 recipe。 + +> 注意:這是 richblack 的 prod KV。**self-host 的新帳號 KV 是空的**,需要 seed 這些 recipe(見 §3 task 1 的 seed 步驟)。 + +### 2.2 component-loader 解析鏈(`cypher-executor/src/lib/component-loader.ts`) + +`resolveComponent` 依序嘗試 8 層(行號近似,以實際檔案為準): + +``` +0. 平台 orchestration 零件(trigger_workflow) line ~88 +1. 內建零件(純 JS) line ~96 +2. 外部 URL(http(s)://...) line ~100 +3. cmp_hash → WEBHOOKS KV idx → 邏輯 Worker line ~105 +4. rec_hash → RECIPES KV idx → recipe 執行 line ~115 +5. 邏輯零件 canonical_id → Service Binding (SVC_*) line ~122 +5.5 auth recipe (auth_recipe:{service}) line ~127 +6. KV recipe canonical_id → RECIPES KV → fetch 外部 API line ~130 ← kbdb_get 等降級 recipe 命中這層 +7. WASM HTTP runner(白名單 WASM_HTTP_RUNNER_IDS) line ~134 +8. 找不到 → 報錯 line ~142 +``` + +`WASM_HTTP_RUNNER_IDS` 白名單(line ~36)現只剩:`http_request` / `cron` / 4 個 `auth_*` primitive。 +→ `claude_api`、`kbdb_upsert_block`(BACKLOG 標 deferred、源碼暫留)**不在白名單也沒 recipe** → 用到它們的 workflow 會落到 step 8 報錯。這是 mira 自己的問題,不在你範圍。 + +### 2.3 `acr init --self-hosted` 現況:純手動問答,差很遠 + +`cli/src/commands/init.ts` 的 `initSelfHosted()`(line 105-131)**只是問 6 個問題後寫進 config**: +要求 CC 自己**事先**部署好 Worker、建好 KV、再手填 Account ID / cypher URL / 兩個 KV namespace ID / WASM bucket / CF token。 + +BACKLOG 步驟7 要的是「**貼 CF token → 自動建 KV、部署 Worker、自動 workers.dev、寫回 config**」。**這是最大缺口,task 1 的主體。** + +config 讀取端已支援 self-hosted(`cli/src/lib/config.ts:52` 已能用 `cypher_executor_url`),所以你只要把「自動部署」這段補上,config/執行端不用動。 + +### 2.4 CI/CD 已是通用掃描式(可重用於 self-host 部署) + +`.github/workflows/deploy.yml` 掃所有含 `wrangler.toml` 的目錄自動部署(見 `.claude/rules/05-deploy-convention.md`)。 +self-host 自動部署可以參考同一套掃描邏輯(`find . -name wrangler.toml`),對每個目錄跑 `wrangler deploy`。 + +--- + +## 3. 今天要做的三件事(按序,全在 harness 主線) + +> richblack 指示:「全部要做」(含 `acr init --self-hosted`)。 +> 三件都做完 = 今天可交付:外部 CC 能 self-host 起來、用對的方式開發、犯錯被擋。 + +### 🔴 Task 1:完成 `acr init --self-hosted` 一鍵自動化(第一優先) + +> ✅ **實作狀態(2026-06-02,已大致完成)**:定稿形態為 **installer 模式**(richblack 拍板)—— +> 用戶只做:申請 CF 帳號 → 裝 wrangler → 裝 acr → `acr init --self-hosted`(貼 token),其餘自動。 +> 已實作(typecheck 過):`cli/src/lib/api-recipe-seeds.ts`(10 recipe 種子)、`cf-api.ts` 的 +> `CfAccountClient`(建 KV/R2/查 subdomain/驗 token)、`deploy.ts`(常數 + downloadAndDeploy)、 +> `initSelfHosted()` 改寫、`acr update`、`cypher-executor/scripts/seed-api-recipes.ts`。 +> **唯一剩餘前置(13.6)**:repo 沒有含預編譯 wasm 的 GitHub release(.wasm 不 commit,rule 05)→ +> `downloadAndDeploy()` 目前**誠實回 implemented:false 不假裝部署**(mindset §7)。建 KV/R2/seed/config +> 已可跑;release 產製管道補上後部署即自動化。定稿設計見 SDD `self-hosted-init.md`(含 §6 前置依賴)。 +> **以下原始子步驟保留供對照**;KBDB recipe 採 Supabase 模式進 seed(richblack 2026-06-02)。 + +**目標**:CC 只需提供「CF Account ID + CF API Token」,CLI 自動完成其餘一切。 + +**SDD**:定稿 `docs/3-specs/arcrun/sdk-and-website/self-hosted-init.md`(installer 模式,已與 richblack 對齊)。 + +**子步驟**: +1. 改 `cli/src/commands/init.ts` 的 `initSelfHosted()`: + - 收 CF Account ID + CF API Token(要 KV Edit + Workers Scripts Edit + R2 權限)。 + - 用 CF API(或 shell out `wrangler`)**自動建 7 個 KV namespace**:WEBHOOKS / CREDENTIALS_KV / RECIPES / USERS_KV / SESSIONS_KV / ANALYTICS_KV / EXEC_CONTEXT(清單見 `.claude/rules/01-tech-stack.md` 資料儲存表)+ R2 WASM_BUCKET。 + - **自動部署所有 Worker**:cypher-executor + registry + 22 個 `.component-builds/*`。可重用 §2.4 的 `wrangler.toml` 掃描。每個 worker 的 `wrangler.toml` 已含 `workers_dev = true`(BACKLOG 步驟 P1#2 已做),部署後 workers.dev URL 自動啟用。 + - **把 cypher-executor 的 `[vars] WORKER_SUBDOMAIN` 改成 CC 自己的帳號 subdomain**(self-host 關鍵,見 P0 #9:cypher-executor 走 `arcrun-{name}.{subdomain}.workers.dev` 對內 URL)。 + - **seed 降級 recipe + auth recipe 進 RECIPES KV**:新帳號 KV 是空的。把 §2.1 那些 recipe(kbdb_get/gmail_send/...)+ auth recipe seed 寫進去。auth recipe seed 已有 `cypher-executor/scripts/seed-auth-recipes.ts`,API recipe 需確認有對應 seed 機制(routes/recipes.ts 是動態 push,可能要寫一份 seed 腳本或用 `acr recipe push`)。 + - 寫回 config(現有欄位已足夠)。 +2. **runtime secret 不進 CLI 自動化**:`ENCRYPTION_KEY` 等由 CC 自己 `wrangler secret put`(rule 05 禁止 secret 進自動化流程)。CLI 應在最後**印出提示**告訴 CC 要手動 put 哪些 secret 到哪些 worker。 + +**驗收(客觀證據,不是口頭宣布 — mindset §7)**: +- richblack 用全新 CF 帳號跑 `acr init --self-hosted` → 全程無手動建 KV / 部署。 +- 跑完後 `acr push` 一個含 `component: kbdb_get`(或 http_request + 自建 recipe)的 workflow → trigger → HTTP 2xx + execution trace 證明跑通。 + +### 🔴 Task 2(已重定義 2026-06-01):封鎖自製零件 + recipe 入庫把關 + +> ✅ **實作狀態(2026-06-02,第一期部分完成)**:(1) 封鎖自製零件 = 靠 GitHub PR 人 merge,無需新做 +> (矛盾已釐清)。(2a) 資料外流提醒 = **既有實作已涵蓋**(recipe.ts `obtainExposureConsent` + exposure-warning.ts, +> 非 TTY 拒絕)。(2b) 打通檢查 = **新增** `probeRecipeEndpoint`(recipe.ts,typecheck 過):push 後實打 +> endpoint,提醒級不硬擋,含 {{模板}} 誠實說明待 run 才知,401/403 標「多半缺 credential 非 bug」。 +> 公共庫 relay 檢核(--public)= 第一期後。SDD `recipe-push-gatekeeping.md` + tasks.md W2。 + +> ⚠️ **方向修正(richblack 2026-06-01)**:原 Task 2「acr validate 擋假零件名」**作廢**。 +> 理由:自製/修改零件的路已封鎖(CC 造不出零件)→「擋假零件」這件事不存在;workflow 引用 +> recipe(`component: kbdb_get`)是合法且未來唯一的擴充方式,不該被當假零件擋。 +> 把關點**從 workflow validate 移到 recipe 入庫(push)那一刻**。 +> 已動的 yaml-parser.ts(LEGAL_PRIMITIVES / findSuspectComponents)**已回退**。 + +**新目標**: +1. **封鎖自製零件**:靠「零件投稿走 GitHub PR + 人 merge」天然閘門(DECISIONS §8)。零件數量少、 + 絕大多數是 recipe → 不為零件 PR 蓋自動化把關(量少,人工檢查即可;爆量才回頭想自動化)。 +2. **recipe 入庫把關**(CC 唯一能擴充的是 recipe,一律用 push,自有庫/公共庫同一套指令): + - **自有庫(self-hosted)= 提醒級**:(a) 資料外流提醒——會讓資料/服務對外可見的動作需人類明示同意; + (b) 打通檢查——push 時實打 endpoint 回報 2xx 與否(誠實標原因,不假綠,不硬擋)。 + - **公共庫 = 維護者 relay 檢核**(實際打通、真收到成功回傳)— 第一期後。 + +**SDD**:已寫 design 給 richblack review → +`docs/3-specs/component-gatekeeping/recipe-push-gatekeeping.md`(+ tasks.md W2 節)。**review 通過才動 code。** + +**動到的檔案(待 review)**:`cli/src/commands/recipe.ts`(push 加提醒 + 打通檢查)、確認 data-exfil hook 涵蓋 recipe push 路徑。 + +**驗收**: +- `acr recipe push` 會產對外 webhook 的東西 → 印資料外流警示 + 要人類同意;非 TTY → 拒絕。 +- `acr recipe push` endpoint 可達的 recipe → 回報「✓ HTTP 2xx」。 +- `acr recipe push` 缺 credential → 回報「⚠️ 未打通:缺 credential」(誠實),仍允許 push。 +- workflow 引用 recipe(`component: kbdb_get`)**不被任何 validate 步驟當假零件擋**。 + +### 🔴 Task 3:README 重寫成單一路徑 — harness「事前提醒」 + +**目標**:self-hosted 開源後,README 是外界 CC 唯一入口。砍掉「玩法一/二/三」三選一,講清楚單一正確路徑。 + +**子步驟**(改根 `README.md`): +1. 砍三選一玩法,留**一條路**:`acr init --self-hosted` → 寫 workflow(primitive 串 + recipe)→ `acr push` → trigger。 +2. 明示心智(呼應 mindset §1):「零件就這固定幾個由我們維護、不接受自製;要打外部 API 就寫 recipe;要編排就寫工作流。」 +3. 連到 `.claude/rules/06-mindset.md` / arcrun-mindset Skill,讓 CC 一開始就有正確世界觀。 + +**驗收**:README 讀完,一個沒看過 arcrun 的 CC 知道:能用什麼、不能自製零件、打外部 API 要寫 recipe、怎麼 self-host 起來。 + +--- + +## 4. 今天「不要做」的(避免你走偏) + +| 項目 | 為何不做 | +|---|---| +| 修 mira | richblack 明示不用管,mira 自己修 | +| 步驟2 `acr recipe test` / relay / credits | DECISIONS §3c 明確劃為服務側、非第一期 | +| 步驟6 搬家拆 matrix | 純 repo 整理,不影響 CC 能否用 | +| 砍 `injectCredentials` 舊路 / `BUILTIN_CREDENTIALS_MAP` | 獨立清理,不擋交付(DECISIONS §3b / BACKLOG「第一期之後」)| +| 新 primitive / Gherkin 真跑 / 入站認證 | richblack 已標「不要現在做」 | + +--- + +## 5. 鐵律提醒(違反會被 hook block) + +- 任何 code 變動前先讀對應 SDD + 在回覆開頭宣告(`.claude/rules/00-sdd-protocol.md`)。 +- `registry/components/` 下禁止 TS;cypher-executor TS 禁止 credential/auth/JWT 業務邏輯(`.claude/rules/02-forbidden.md`,hook 強制)。 +- 每完成一個 task 立刻更新對應 tasks.md / BACKLOG.md 的 `[x]`,不批次。 +- 誠實限制(mindset §7):stub / 未完成就標 unimplemented,**不假綠**;完成 = 客觀證據(exit code / HTTP status + trace),不是口頭宣布。 + +--- + +## 6. 交棒:CLAUDE.block.md / README 表達優化(InkStoneCo 總管 2026-06-15) + +**背景**:總管與 leo 釐清 Mira 自製零件衝突時,實測一個現象——**連 Opus 讀著 `cli/harness/CLAUDE.block.md` + rules/,都把 recipe 誤解了四輪**(以為 recipe 是私有腳本、以為缺能力可用工作流拼裝補)。leo 的標準是「**Haiku 都能輕易理解**」。block 是被 `acr install-harness` 裝進各子 repo(含 mira)的**唯一入口**,細節雖在 `rules/` 但子 repo CC 預設只看 block → block 不夠清楚 = 下一個 CC 還會誤解。**這是表達缺口,不是讀者問題。** + +**請 arcrun CC 做(改 `cli/harness/CLAUDE.block.md`,順帶對齊 §3 README)**:把現有「不要自製零件 / 打外部 API 寫 recipe」兩句,補成「該做什麼 + 做完去哪 + 已違規的怎麼退場」。三個已核實的盲點要講清楚(論證在下,成品文案你按自己 rules 寫): + +1. **recipe 是公共資產,不是私人腳本**。`acr recipe push` = 投稿公共庫(`DEFAULT_PUBLIC_LIBRARY_URL`)。公私是**屬性**(UUID 身份模型,`cypher-executor/src/routes/recipes.ts` §7.5.5:身份=uuid、公私/author 是屬性、多作者版本並存),不是兩條路。**「發現缺某 stock recipe → 自己做一個 push 上去」是被鼓勵的,不必問人。** 現 block 只寫「寫 recipe」「不用部署」→ 讀者誤以為私有自用、不上傳(正好反了)。 + +2. **缺能力 → 補 API,不准用 recipe/工作流拼裝**。這條 rules 其實寫得很對(07-thin-shell §1 upsert 正反例、02-forbidden §5.2、01 §8),但 **block 完全沒提** → 子 repo CC 看不到。請把「缺單一 API 能力 = 去補 API endpoint,不拼裝」這條判準帶進 block(口訣:「換個介面要不要重寫?要→是能力該在 API」)。 + +3. **零件的退場路徑空白**。現 block 只說「不要自製零件」,沒說「**已經有自製零件的(如 mira 的 6 個)該怎麼辦**」。請補:已自製零件 → 映射到 stock(`claude_api`→自己做 AI 判斷別放工作流,mindset §2;`kbdb_*`→走已備的 `acr kbdb` 薄殼/MCP)或改寫成 recipe;無 stock 對應的單 API 缺口 → 寫 recipe 投稿。 + +**驗收(leo 標準)**:改完後,一個 **Haiku** 級 CC 讀 block 就能答對:recipe 要不要投稿(要)、缺 upsert 怎麼辦(補 API 不拼裝)、已有自製零件怎麼退場(映射 stock / 改 recipe)。 + +**連帶(非 arcrun 範圍,總管已記)**:「真正可複用的多步驟業務流程能否投稿共享」是 wishlist C6(InkStoneCo 頂層),與「拼裝補 API 缺口」是兩回事(C6 紅線已劃清),arcrun 不必現在處理。 + +> 注:本交棒由總管寫**判準與論證**,**成品文案 + hook(若要)由 arcrun CC 按自己 rules 產出**。總管不直接改 arcrun 實作檔(職責邊界 D9)。 diff --git a/docs/user_requirements/ADR-lib-and-landingPage/arcrun-pages-spec.md b/docs/user_requirements/ADR-lib-and-landingPage/arcrun-pages-spec.md new file mode 100644 index 0000000..1f1ba11 --- /dev/null +++ b/docs/user_requirements/ADR-lib-and-landingPage/arcrun-pages-spec.md @@ -0,0 +1,340 @@ +# arcrun.dev Pages 規格 + +> **讀者**:CC(可直接照做) +> **部署**:Cloudflare Pages + Workers +> **語言**:英文為主,中文切換 +> **技術棧**:Astro(靜態生成)+ Cloudflare Pages + D1(使用統計) + +--- + +## 0. 這個 Pages 的三個角色 + +1. **門面**:第一次看到 arcrun 的人,30 秒內要懂「這是什麼、對我有什麼用」 +2. **轉換漏斗**:工程師 → 試用 lib → 申請 API Key;小白 → 看榮譽牆 → 問 AI 能不能用 +3. **社群磁鐵**:榮譽牆讓工程師有動機貢獻 recipe,貢獻越多服務越多,用戶越多 + +--- + +## 1. 網站結構(五個頁面) + +``` +arcrun.dev/ +├── / 首頁(門面 + 轉換) +├── /docs 用法文件 +├── /integrations 榮譽牆(服務目錄) +├── /api Swagger UI(原始 API) +└── /changelog 版本記錄 +``` + +--- + +## 2. 首頁(/) + +### 2.1 Hero Section + +**英文**: +``` +Stop fighting OAuth. +One API key. Every service. Works anywhere. + +arcrun handles Google, Notion, GitHub, Slack authentication +so your Python / JS code doesn't have to. + +[Get API Key — Free] [View on GitHub] +``` + +**中文切換後**: +``` +不要再跟 OAuth 搏鬥了。 +一個 API Key,接通所有服務,在哪跑都行。 + +[免費取得 API Key] [查看 GitHub] +``` + +語言切換按鈕放右上角,用 `?lang=zh` query param,Cloudflare Worker 記住偏好存 cookie。 + +### 2.2 三行說清楚(Why arcrun) + +``` +┌────────────────────┬────────────────────┬────────────────────┐ +│ Before │ │ After │ +│ │ │ │ +│ 40 行 OAuth 程式 │ →→→ │ 1 行 │ +│ GCP Console 設定 │ │ arcrun.auth.bind │ +│ debug 兩天 │ │ ("google_drive") │ +└────────────────────┴────────────────────┴────────────────────┘ +``` + +### 2.3 Code Demo(互動式 tab) + +三個 tab 切換:Python / JavaScript / HTTP(給 n8n 小白) + +**Python tab**: +```python +pip install arcrun-auth + +from arcrun import auth + +# 就這樣,Google Drive 認證完成 +drive = auth.bind("google_drive") +resp = drive.get("/files") +``` + +**JavaScript tab**: +```javascript +npm install arcrun-auth + +import { auth } from 'arcrun-auth' + +const drive = await auth.bind('google_drive') +const resp = await drive.get('/files') +``` + +**HTTP tab(給 n8n 用戶)**: +``` +POST https://api.arcrun.dev/v1/auth/bind +Authorization: Bearer YOUR_API_KEY +Content-Type: application/json + +{ + "service": "google_drive", + "secret": "{{ $env.GOOGLE_SA_JSON }}" +} +``` +下方加一行小字:「n8n 用戶:用 HTTP Request 節點貼上這段,不需要安裝任何東西」 + +### 2.4 數字牆(social proof) + +``` +127 個認證服務 1,247,832 次呼叫 89 位貢獻者 +``` + +這三個數字從 D1 即時讀,每小時更新一次(Cloudflare KV cache)。 + +### 2.5 CTA + +``` +[免費取得 API Key] +註冊後立即可用,不需要信用卡 +``` + +--- + +## 3. 榮譽牆(/integrations)★ 核心頁面 + +### 3.1 頁面頂部 + +``` +127 個已驗證的認證服務 +由社群工程師貢獻並測試,每個 recipe 都有真實使用數據 + +[搜尋服務...] [全部] [AI] [Google] [社群媒體] [生產力] [台灣] +``` + +### 3.2 服務卡片 + +每個 recipe 一張卡: + +``` +┌──────────────────────────────────────────┐ +│ [圖示] Google Drive ★ 官方 │ +│ │ +│ 認證方式:Service Account │ +│ 貢獻者:@richblack ──→ GitHub profile │ +│ 驗證日期:2026-03-15 │ +│ │ +│ 使用次數:██████████ 12,847 次 │ +│ │ +│ [查看 Recipe] [複製 Python 範例] │ +└──────────────────────────────────────────┘ + +┌──────────────────────────────────────────┐ +│ [圖示] OpenRouter │ +│ │ +│ 認證方式:API Key (Header) │ +│ 貢獻者:@some_engineer ──→ GitHub │ +│ 驗證日期:2026-04-01 │ +│ │ +│ 使用次數:██░░░░░░░░ 89 次 │ +│ │ +│ [查看 Recipe] [複製 Python 範例] │ +└──────────────────────────────────────────┘ +``` + +badge 規則: +- `★ 官方`:arcrun 團隊維護 +- `✓ 社群驗證`:100+ 次使用 + 30 天無錯誤回報 +- `🆕 新加入`:30 天內合併的 PR + +### 3.3 貢獻者排行(頁面底部) + +``` +Top Contributors + +🥇 @some_engineer 23 個 recipe 89,234 次呼叫 +🥈 @another_dev 15 個 recipe 45,123 次呼叫 +🥉 @third_person 8 個 recipe 12,456 次呼叫 +... + +[我也要貢獻 →] (連到 CONTRIBUTING.md) +``` + +### 3.4 「我要貢獻」的 CTA + +``` +找不到你要的服務? + +大部分 API Key 類的服務,填一份 YAML 就能加進來。 +把 API 文件丟給 AI,五分鐘生成,開 PR 送出。 + +[查看 Recipe 格式] [開始貢獻] +``` + +--- + +## 4. 用法文件(/docs) + +### 結構 + +``` +快速開始 +├── 取得 API Key +├── Python 安裝與第一個範例 +├── JavaScript 安裝與第一個範例 +└── 直接用 HTTP(n8n / 任何工具) + +認證方式 +├── API Key 類服務 +├── OAuth2 類服務 +├── Google Service Account +└── mTLS + +進階用法 +├── 多帳號(multi-instance) +├── 只取 token(escape hatch) +└── 錯誤處理 + +貢獻 Recipe +├── Recipe YAML 格式說明 +├── 讓 AI 幫你寫 Recipe +└── 提交流程 +``` + +### 「讓 AI 幫你寫 Recipe」這一節特別重要 + +```markdown +## 讓 AI 幫你寫 Recipe + +把下面這段丟給 Claude / ChatGPT, +再把目標服務的 API 文件一起貼進去: + +--- +請根據以下 API 文件, +生成一份符合 arcrun recipe schema 的 YAML。 +Schema 文件:https://arcrun.dev/docs/recipe-schema +目標服務:[貼上 API 文件] +--- + +AI 生成後,你只需要: +1. 把 YAML 存成 recipes/community/服務名.yaml +2. 跑 `acr recipe test 服務名.yaml` +3. 開 PR + +通常整個過程不超過十分鐘。 +``` + +這一節讓「貢獻門檻」從「工程師才能做」變成「任何人叫 AI 做」。 + +--- + +## 5. API 文件(/api) + +直接嵌入 Swagger UI,連到 `https://api.arcrun.dev/swagger.json`。 + +頁面頂部加一行說明: +``` +這是 arcrun 的原始 API。 +Python / JS lib 是它的包裝, +任何能發 HTTP request 的工具都能直接用。 +``` + +這一句話讓 n8n 用戶、Make 用戶、甚至 Excel 用戶都知道「我也能用」。 + +--- + +## 6. 技術實作 + +### 6.1 技術選型 + +**Astro**(靜態生成)是首選,原因: +- 頁面大部分是靜態內容(docs / recipe 卡片),Astro 的 SSG 完美對應 +- 動態數字(使用次數、貢獻者排行)用 Astro 的 `client:load` island 局部更新 +- 部署到 Cloudflare Pages 零配置 + +**不用 Next.js**,因為你已在 Cloudflare 生態,Next.js 的 SSR 在 CF Pages 有摩擦。Astro + CF Pages 是更自然的組合。 + +### 6.2 資料來源 + +| 資料 | 來源 | 更新頻率 | +|---|---|---| +| Recipe 清單、metadata | GitHub repo `recipes/` 目錄 | CI merge 時觸發 rebuild | +| 使用次數 | Cloudflare D1(API call log) | 每小時從 D1 聚合 → KV cache | +| 貢獻者排行 | 同上 | 每小時 | +| 總呼叫次數 | 同上 | 每小時 | + +### 6.3 多語言 + +用 Astro 的 i18n routing: +- `/` → 英文 +- `/zh/` → 中文 + +語言切換按鈕寫入 cookie `arcrun_lang`,CF Worker 在 edge 讀 cookie 做 redirect。 +不用 JS framework 的 i18n library,保持輕量。 + +### 6.4 部署流程 + +``` +GitHub push to main + → GitHub Actions 跑 astro build + → 產出 dist/ + → 自動部署到 Cloudflare Pages + → Pages 掛 arcrun.dev domain +``` + +recipe YAML 有變動時(PR merge)額外觸發一次 rebuild。 + +--- + +## 7. CC 的實作任務 + +### Phase 1:靜態骨架(3-5 天) + +- [ ] Astro 專案初始化,設定 CF Pages 部署 +- [ ] 首頁 Hero + Code Demo tab(靜態版,數字先寫死) +- [ ] `/integrations` 靜態版(先手動列 5-10 個服務) +- [ ] `/docs` 基本結構(快速開始 + Python 範例) +- [ ] `/api` 嵌入 Swagger UI +- [ ] 中英切換機制 + +### Phase 2:動態資料(3-5 天) + +- [ ] D1 schema:`recipe_calls(recipe_id, count, last_updated)` +- [ ] CF Worker:API call 時寫入 D1 +- [ ] 每小時聚合 Worker:D1 → KV cache(總數 / per recipe / per contributor) +- [ ] 首頁數字牆:從 KV 讀即時數字 +- [ ] `/integrations` 卡片:使用次數從 KV 讀,進度條動態顯示 + +### Phase 3:社群功能(2-3 天) + +- [ ] 貢獻者排行從 KV 讀 +- [ ] Recipe 頁面:點「查看 Recipe」展開 YAML +- [ ] 點「複製 Python 範例」自動生成對應 code snippet +- [ ] GitHub PR merge webhook → 觸發 Pages rebuild + +--- + +## 8. 一個不能省的細節 + +榮譽牆的貢獻者欄位**一定要連到他的 GitHub profile**,不是只顯示名字。 + +工程師貢獻的動機之一是「這個會出現在我的公開作品集」。連到 GitHub 就意味著他的 followers 可能看到他貢獻了 arcrun,這比任何 badge 都有效。 diff --git a/docs/user_requirements/ADR-lib-and-landingPage/arcrun-py-strategy-analysis.md b/docs/user_requirements/ADR-lib-and-landingPage/arcrun-py-strategy-analysis.md new file mode 100644 index 0000000..45783e7 --- /dev/null +++ b/docs/user_requirements/ADR-lib-and-landingPage/arcrun-py-strategy-analysis.md @@ -0,0 +1,486 @@ +# arcrun-py:Python Lib 策略分析 + +> **核心問題**:arcrun 的 auth 層要不要獨立成 Python lib?AI 會主動選它嗎? +> **決策前提**:本地為主雲端選配 + 免費引流 SaaS + 三維度分析 + +--- + +## 0. TL;DR + +做。但要做對——它不是「arcrun SDK」,是 **獨立的 OAuth 地獄解藥**,SaaS 帳號只是 optional power-up。 + +**產品名建議**:`arcrun-auth`(PyPI 套件名),import 時叫 `authkit` 或 `arc`。 + +**定位**:`pip install arcrun-auth` → 一行搞定 Google/GitHub/Slack/Notion/... 的認證。本地 keyring 存 secret,零註冊可用。註冊 arcrun 帳號才能多機同步 / 團隊共用 / 獲得平台代管的 OAuth App。 + +--- + +## 1. 你的直覺:為什麼這可能是大招 + +你觀察到的現象: +- **寫個「本地檔案 → Google Drive 備份」這種小腳本**,光認證花 2 天,日後還反覆壞。 +- **這個痛點 99% 的 Python 使用者都遇過**。Stack Overflow 上 Google OAuth 的問題累計回答數十萬。 +- **AI 寫程式時,這部分它也不會寫**——訓練資料裡的標準範本就是 `InstalledAppFlow.from_client_secrets_file()`,還是要人類先去 GCP Console 建 OAuth Client、下載 JSON、設 redirect URI、加測試用戶。 + +**如果 arcrun 能把這整套濃縮成 `auth.bind("google_drive")`,它就是開發者工具裡的「自來水」——沒人在乎水源廠長怎麼運作,大家只想打開水龍頭有水喝。** + +而且你無意中踩到一個時代轉折點:**AI 在寫程式,AI 不想 debug OAuth**。這個 lib 是給 AI 用的,比給人類用的更重要。 + +--- + +## 2. 三維度分析:AI 到底會不會選 arcrun-auth? + +### 維度一:零註冊摩擦(最重要) + +**現實**:AI coding assistant 預設會產出訓練資料裡最常見的程式碼。Google Drive 的標準答案就是官方 quickstart——那段 40 行的 `InstalledAppFlow` 樣板碼,加上「去 GCP Console 建 OAuth Client」的一串步驟。 + +**AI 選擇替代 lib 的條件**(觀察 Copilot / Cursor / Claude Code 的實際行為): + +| 條件 | 是否能壓制預設答案 | +|---|---| +| Lib 名稱出現在 prompt 裡 | ✅ 絕對會用 | +| 專案已有 `pyproject.toml` 宣告該 lib | ✅ 會用 | +| 該 lib 是 PyPI 下載量 Top N(訓練資料權重高) | ⚠️ 或許 | +| 使用者口頭說「用 XX 比較好」 | ✅ 會用(單次) | +| 該 lib 明顯更短更容易 | ⚠️ 有機會但不保證 | + +**結論**:**零註冊摩擦是生死線**。 + +如果 `pip install arcrun-auth` 之後必須先去註冊 arcrun 帳號、拿 API token、設環境變數——AI 看到這一步就會放棄,回去寫 Google 官方那套,因為「官方的至少不用註冊第二家廠商」。 + +**對策**: +- 本地模式必須是**零配置可用**的 default path。 +- Secret 存在 `~/.arcrun/secrets.db`(加密,用系統 keyring 保護 master key)。 +- 註冊 arcrun 帳號是 **opt-in** 的升級,不是必要。 +- 第一次用 OAuth 時,**平台提供免費共用的 OAuth App**(下面 §3 詳細說)。 + +這個設計讓 AI 看到的心智路徑是: + +``` +目標:備份檔案到 Google Drive +選項 A:google-api-python-client(官方,但使用者要先去 GCP 設 OAuth) +選項 B:arcrun-auth(pip install 完直接可用) +``` + +在這個對比下,AI 會選 B——**特別是它被告知「使用者不想 debug 認證」或類似的 prompt 時**。 + +### 維度二:跟 stdlib / 主流 lib 的對比 + +Python Google API 的主流選項: + +| Lib | PyPI 週下載(估) | AI 熟悉度 | 認證難度 | +|---|---|---|---| +| `google-api-python-client` | ~30M | 極高 | 高(要自己設 OAuth) | +| `pydrive2` | ~200k | 中 | 高 | +| `gspread`(只 Sheets) | ~2M | 高 | 高(OAuth 或 SA) | +| **arcrun-auth** | 0 | 0 | **極低** | + +**冷啟動難題**:新 lib 要進入 AI 的選擇集合,需要: + +1. **量**:PyPI 下載 + GitHub stars 進到「被訓練資料收錄」的級別(大概 GitHub 5k+ stars 是門檻)。 +2. **品牌**:有代表性部落格文、教學影片、官方 API 文件連結到它。 +3. **簡潔**:代碼範例比主流短 5 倍以上,讓使用者「一眼就想用」。 + +**arcrun 的優勢**: +- 你本來就在教 n8n 課程,有現成學員管道可以鋪「這是 n8n 的 Python 版 auth」。 +- 「AI 寫程式的 auth lib」是個還沒被佔領的定位詞。`langchain` 做了 LLM 層,但 auth 層還沒有明顯贏家。 +- Claude Code 對 lib 選擇特別敏感——它會實際讀 `pyproject.toml` 並尊重已有宣告。 + +**對策**: +- **第一批 adopter 是你的學員**(n8n 課 + AI 自動化課),他們會在實戰中用,累積 GitHub issues 和 blog post。 +- **SEO 主打**:「Python Google Drive OAuth 簡化」「AI 自動化 Python 認證」這些長尾關鍵字現在沒有明顯答主。 +- **Claude Code 優化**:寫一份 `AGENTS.md` 或 `.cursorrules` 範本,示範怎麼在 prompt 裡引導 AI 選 arcrun-auth。 + +### 維度三:痛點強度(OAuth 地獄避免) + +**量化你那兩天 debug**: + +| 階段 | 時間成本 | 典型錯誤 | +|---|---|---| +| GCP 註冊 + 啟用 API | 15 min | 找不到哪個 API | +| 建 OAuth Client ID | 15 min | Desktop / Web / iOS 選錯 | +| 設 OAuth Consent Screen | 30 min | External / Internal 選錯;scope 加錯 | +| 加測試用戶 | 10 min | 漏加自己的 email | +| 寫 Python flow | 30 min | `run_local_server` vs `run_console` | +| 第一次跑遇到 `redirect_uri_mismatch` | 30-120 min | port 衝突、URI 沒加 | +| Token 過期處理 | 60 min | `creds.expired` 與 `refresh_token` 沒保存 | +| Service Account 模式(如果需要) | 120 min | domain-wide delegation 設定 | +| **合計** | **5-8 hrs(順的人)** | **2 天(不順的人,你當時的情況)** | + +**arcrun-auth 對應版本**: + +```python +from arcrun import auth + +# 首次執行:自動打開瀏覽器完成 OAuth,結果存本地 keyring +drive = auth.bind("google_drive") + +# 直接呼叫 API +drive.post("/upload/drive/v3/files", params={"uploadType": "media"}, + data=open("backup.zip", "rb")) +``` + +**時間成本:首次 2 min,之後 0 min**。 + +這個壓倒性的體驗差距是產品的核心競爭力。**只要使用者試過一次,就不會再回去寫 `InstalledAppFlow`**——即使 AI 預設會產出官方版本。 + +--- + +## 3. 關鍵設計決策 + +### 3.1 OAuth App 誰擁有?(核心問題) + +傳統做法:使用者自己去 GCP Console 註冊自己的 OAuth App,拿 client_id/client_secret。**這就是痛點來源**。 + +arcrun-auth 要消滅這步,只有兩條路: + +**Option A:平台提供共用 OAuth App(推薦 default)** +- arcrun 註冊一個 Google OAuth App,命名類似「arcrun Auth Broker」。 +- 所有 arcrun-auth 使用者共用這個 App 的 client_id/secret。 +- 使用者在 Google 授權頁面看到的是「arcrun Auth Broker 想存取您的 Google Drive」。 +- **好處**:使用者零配置,arcrun 品牌曝光。 +- **成本**:Google 有 OAuth App 的限額(Verified App 才能超過 100 users),需要申請 Google OAuth Verification(要提供隱私政策、網域驗證、可能要付 $75 安全審查)。 + +**Option B:使用者 BYO OAuth App** +- 企業客戶或注重稽核的人需要這個。 +- 在 `~/.arcrun/config.toml` 放自己的 client_id/secret。 + +**Option C:arcrun SaaS 代管**(付費) +- 使用者註冊 arcrun 帳號,平台幫你管 OAuth App、token、團隊共用、audit log。 +- 這是付費 tier 的主要價值。 + +**建議**:A + B + C 三種都支援,默認 A;免費無限制 B;付費享受 C。 + +### 3.2 Secret 儲存層級(本地為主雲端選配) + +``` +優先級 1 (default):本地 keyring + - macOS Keychain / Windows Credential Manager / Linux libsecret + - zero config,安全性靠 OS + +優先級 2 (opt-in):本地加密檔 + - ~/.arcrun/secrets.enc + - master key 走 keyring 或 passphrase + - 給沒有 keyring 的環境(Docker、CI) + +優先級 3 (opt-in):arcrun 雲端 + - 多機同步、團隊共用、audit log + - 需註冊 arcrun 帳號 + - 本地 lib 只保存 arcrun API token,實際 service secret 存雲端 +``` + +### 3.3 Secret 初始化流程 + +**靜態 key 模式(Notion、OpenAI、Stripe...)**: + +```bash +# 選項 A:互動式 +$ arcrun setup notion +? Notion Integration Token (hidden): *** +✓ Testing connection... OK +✓ Saved to keyring as notion/default + +# 選項 B:環境變數 +$ export ARCRUN_NOTION_TOKEN=secret_xxx +$ python script.py # arcrun-auth 自動讀 + +# 選項 C:程式碼內 +notion = auth.bind("notion", secret={"token": os.environ["NOTION_TOKEN"]}) +``` + +**OAuth 模式(Google、GitHub、Slack...)**: + +```python +drive = auth.bind("google_drive") +# 如果是第一次: +# 1. 本地啟動一個臨時 HTTP server (http://localhost:random_port) +# 2. 開瀏覽器到 Google authorize URL +# 3. 使用者點同意 +# 4. Google redirect 到 localhost,lib 接到 code +# 5. 換 token,存 keyring +# 6. 回傳可用的 client +``` + +這個流程和 `InstalledAppFlow.run_local_server()` 本質上一樣——但差別是: +- **Client ID 不用使用者自己去 GCP Console 註冊**(由 arcrun 平台提供)。 +- **Scope 由 recipe 宣告**(不用使用者自己查文件)。 +- **Token 儲存自動化**(不是散落在 `token.json`)。 + +### 3.4 Recipe 來源 + +Python lib 和 Cloudflare Worker 版本**共用同一份 recipe YAML**。 + +``` +arcrun-recipes/ # GitHub repo,公開 +├── recipes/ +│ ├── official/ +│ │ ├── google_drive.yaml +│ │ ├── notion.yaml +│ │ └── ... +│ └── community/ +│ └── ... +``` + +Python lib 啟動時檢查本地 `~/.arcrun/recipes/` 快取,過期就從 GitHub 或 arcrun 平台 API 拉最新。 + +**這是關鍵架構優勢**:recipe 寫一次,Web 和 CLI 和 Python lib 全部受益。社群貢獻一份 Notion recipe,所有 runtime 自動支援。 + +--- + +## 4. API 設計(Python 版) + +### 4.1 最簡路徑 + +```python +from arcrun import auth + +# 取得認證好的 HTTP client(基於 httpx) +client = auth.bind("google_drive") + +# 相對 base_url 的路徑 +resp = client.get("/files", params={"q": "name = 'backup.zip'"}) +files = resp.json()["files"] + +# 上傳 +client.post("/upload/drive/v3/files", + params={"uploadType": "multipart"}, + files={"file": ("backup.zip", open("backup.zip", "rb"))}) +``` + +### 4.2 進階:非同步 + +```python +from arcrun import auth + +async with auth.bind_async("google_drive") as client: + resp = await client.get("/files") +``` + +### 4.3 進階:多 instance + +```python +# 同一個服務,多個帳號 +personal = auth.bind("google_drive", instance="personal") +work = auth.bind("google_drive", instance="work") +``` + +### 4.4 進階:直接取 token(給不想透過 wrapper 的情況) + +```python +# 取 raw access token,自己丟進任何 lib +token = auth.get_token("google_drive") +# 丟給 googleapiclient: +from googleapiclient.discovery import build +from google.oauth2.credentials import Credentials +creds = Credentials(token=token.access_token) +service = build("drive", "v3", credentials=creds) +``` + +這個 escape hatch 很重要——不強制使用者放棄他熟悉的官方 lib,只是把**認證這一層**剝離出來。這是你想要的「避免麻煩直接用 arcrun 的 auth 功能」的精確實作。 + +### 4.5 服務發現 + +```python +# CLI +$ arcrun list +google_drive Google Drive OAuth2 +notion Notion API Key +github GitHub OAuth2 +openai OpenAI API Key +... + +# Python +from arcrun import auth +auth.list_services() # 回傳 dict +``` + +--- + +## 5. 商業模式:免費引流 → SaaS 付費 + +### 5.1 免費永久可用(本地模式) + +| 功能 | 免費 | 付費 | +|---|---|---| +| `pip install arcrun-auth` | ✅ | ✅ | +| 本地 keyring 儲存 secret | ✅ | ✅ | +| 所有 recipe 可用 | ✅ | ✅ | +| 平台代管 OAuth App(免自己註冊) | ✅ | ✅ | +| 單機使用 | ✅ | ✅ | + +### 5.2 付費 tier 提供的 + +| 功能 | 免費 | Pro ($9/mo) | Team ($29/user/mo) | +|---|---|---|---| +| 多機同步 secret | ❌ | ✅ | ✅ | +| 團隊共用 credential | ❌ | ❌ | ✅ | +| Audit log(誰在何時用了什麼 secret) | ❌ | ✅ | ✅ | +| Secret rotation 提醒 | ❌ | ✅ | ✅ | +| 企業 OAuth App BYO | ❌ | ✅ | ✅ | +| SSO / SCIM | ❌ | ❌ | ✅ | +| arcrun SaaS 整合(workflow runtime) | ❌ | ⚠️ 受限 | ✅ | +| Priority 社群 recipe 審核 | ❌ | ✅ | ✅ | + +**商業邏輯**: +- 免費 tier 足夠「個人開發者 + 側邊小專案」的體驗完整。 +- 付費 tier 解決「我的小工具變公司在用」的延伸需求。 +- arcrun SaaS workflow 客戶自動享受所有 Python lib 特權。 + +### 5.3 為什麼這策略會 work? + +參考模型: +- **Sentry**:error tracking,開源 SDK,hosted service 付費。SDK 下載數 = 付費轉換引流。 +- **PostHog**:product analytics,同樣策略。 +- **Supabase**:auth + db,開源 self-hosted,hosted 服務付費。 + +共同特徵: +1. SDK 免費下載、單機/本地能完整 work。 +2. 從本地升級到 hosted 是「多人/多機/審計」的自然需求。 +3. SDK 本身就是最強的 marketing(開發者口耳相傳 + 訓練資料擴散)。 + +--- + +## 6. 為什麼 AI 會主動選 arcrun-auth(機率分析) + +讓我給你一個分層的答案。 + +### 6.1 會主動選的場景(高機率) + +| 場景 | 原因 | +|---|---| +| 使用者在 prompt 說「用最簡單的方式認證」 | AI 權衡 UX,arcrun-auth 明顯勝 | +| 專案已有 `arcrun-auth` 在 `pyproject.toml` | AI 尊重現有依賴 | +| 使用者貼了 arcrun-auth 文件範例 | AI 會模仿 | +| 使用者說「跳過 OAuth 設定步驟」 | arcrun-auth 是唯一答案 | + +### 6.2 不會主動選的場景(預設行為) + +| 場景 | 原因 | +|---|---| +| 使用者開新專案、無任何線索 | AI 走訓練資料中最頻繁的 `google-api-python-client` | +| 任務是「企業級、有審計需求」 | AI 偏好主流官方 lib | +| 任務提到特定 lib 名稱 | AI 會遵守 | + +### 6.3 轉化策略 + +要讓 AI 的「預設路徑」變成 arcrun-auth,需要: + +1. **種子內容**:寫 20+ 篇「用 arcrun-auth 做 X」的 blog post,讓下一輪模型訓練看到。 +2. **官方文件 SEO**:搶「python google drive oauth 簡化」等長尾關鍵字。 +3. **Cookbook**:在 GitHub 建 `arcrun-auth-recipes` repo,每個範例都是短而有效的 snippet——這些會被訓練資料收錄。 +4. **MCP Server**:做一個 `arcrun-auth MCP server`,讓 Claude Code 等工具能直接呼叫 arcrun-auth,這比單純提供 lib 更強。 +5. **教學整合**:你的 n8n 課程直接示範「從 n8n 到 arcrun-auth 的 Python 版本」。 + +--- + +## 7. 實作範圍(MVP) + +### Phase 1:核心 lib(2-3 週) + +- [ ] `arcrun-auth` PyPI 骨架(pyproject.toml + src layout) +- [ ] Recipe loader(從 GitHub 或平台 API 拉 YAML) +- [ ] `auth.bind(service_id, instance?)` → httpx Client +- [ ] Static key primitive(Notion / OpenAI / Stripe 當試金石) +- [ ] Keyring 整合 + 本地加密檔 fallback +- [ ] CLI:`arcrun setup `, `arcrun list`, `arcrun test` + +### Phase 2:OAuth2(2 週) + +- [ ] OAuth2 primitive(authorization_code + PKCE) +- [ ] 本地 callback server(類似 `InstalledAppFlow.run_local_server`) +- [ ] 共用平台 OAuth App 的 proxy 機制 + - Lib 呼叫 `https://auth.arcrun.com/oauth/redirect` + - 平台把 code 交換後回傳 token + - 或者直接把平台 client_id 硬編在 recipe 裡(更簡單但要處理配額) +- [ ] Token refresh 自動化 +- [ ] Recipe:Google Drive / Gmail / GitHub / Slack + +### Phase 3:Service Account(1-2 週) + +- [ ] Google Service Account(JWT signing) +- [ ] AWS SigV4 +- [ ] Recipe 繼承(`extends: _google_base`) + +### Phase 4:雲端同步(2 週) + +- [ ] `arcrun login` → 綁定雲端帳號 +- [ ] Secret sync 協議(本地加密後上傳,平台只存密文) +- [ ] 多機同步 +- [ ] Audit log + +### Phase 5:AI 生態整合(1-2 週) + +- [ ] MCP server(讓 Claude Code 能直接用) +- [ ] VS Code Extension(一鍵設定 credential) +- [ ] `AGENTS.md` 範本(引導 AI 選 arcrun-auth) + +--- + +## 8. 風險與坑 + +### 8.1 Google OAuth Verification + +**問題**:共用 OAuth App 要申請 Google Verification,否則會有「未驗證 App」警告 + 100 user 上限。 + +**對策**: +- MVP 階段接受警告頁面(使用者自己點「進階 → 前往」)。 +- 到 user 量接近 100 時申請 Verification。 +- 企業客戶走 BYO OAuth App 路徑,不受影響。 +- 若平台 OAuth App 卡關,有 fallback:lib 自動引導使用者建自己的 OAuth App(提供 CLI wizard)。 + +### 8.2 其他服務的 OAuth App 政策 + +- **GitHub**:免費建 OAuth App,無上限。✅ +- **Slack**:免費建,但安裝到使用者 workspace 需管理員同意。⚠️ +- **Microsoft / Azure**:相對嚴格,需 tenant admin consent。⚠️ +- **Notion**:Internal Integration 可以完全走 API key,免 OAuth。✅(最簡單) + +### 8.3 keyring 在 Linux server / Docker 的問題 + +Linux server 沒 GUI keyring daemon。對策: +- Fallback 到加密檔案(用 env var 或 CLI 互動提供 master key)。 +- Docker 場景有 `docker secret`、Kubernetes Secret,lib 支援直接讀這些來源。 + +### 8.4 競品 + +目前沒有完全對標的產品,但相鄰玩家: +- **[keyring](https://pypi.org/project/keyring/)**:只做儲存,不做認證流程。我們用它當底層。 +- **[httpx-auth](https://pypi.org/project/httpx-auth/)**:只做認證,不做 secret 管理,也沒有 recipe。 +- **[authlib](https://pypi.org/project/authlib/)**:OAuth 實作 lib,低階,還是要自己組。 +- **各家 SDK(google-auth, slack-sdk)**:綁特定家,不 unify。 + +**arcrun-auth 的差異化定位**: +> **Unified credential broker for AI-era Python apps** +> 一個 lib 搞定所有服務、所有認證機制、所有 secret 儲存後端。 + +--- + +## 9. 最後的判斷 + +### 9.1 這個 lib 該不該做? + +**該做**。原因: +1. 你描述的痛點是真的,而且規模巨大(Python + Google API 下載量是千萬級)。 +2. 技術可行,也跟既有 arcrun 架構共用 recipe,邊際成本低。 +3. 對 arcrun SaaS 是完美引流——免費 lib 的使用者是精準的付費轉換潛在客戶。 +4. 時間窗口正確:AI 寫程式時代剛開始,這個定位還沒被佔領。 + +### 9.2 跟主 SaaS 的優先順序 + +**建議**:**主 SaaS 的 credential 系統先做(前一份規劃),arcrun-auth 當後續 Phase**。 + +原因: +- Cloudflare Worker 版的 primitives + recipes 是基礎建設,Python lib 是其 consumer。 +- 先做 Python lib 會逼你在 recipe schema 上做二次修改,不划算。 +- 主 SaaS 的 recipe 累積到 20-30 個服務後,開放 Python lib 體驗最好。 + +時程建議: +- **Month 1-2**:主 SaaS 的 4 個 primitive + 15 個 recipe(前一份規劃)。 +- **Month 3-4**:arcrun-auth Phase 1-2(static key + OAuth2),私人 alpha。 +- **Month 5**:公開 release,寫部落格、SEO、社群推廣。 +- **Month 6+**:雲端同步、MCP、AI 生態整合。 + +### 9.3 一句話總結 + +> **arcrun-auth 不是「arcrun 的 Python 綁定」,是「OAuth 地獄的解藥」**。SaaS 是延伸。這個敘事才能在 AI 寫程式的時代站住腳。 + diff --git a/docs/user_requirements/ADR-lib-and-landingPage/strategy.md b/docs/user_requirements/ADR-lib-and-landingPage/strategy.md new file mode 100644 index 0000000..a22fb7b --- /dev/null +++ b/docs/user_requirements/ADR-lib-and-landingPage/strategy.md @@ -0,0 +1,55 @@ +# Arcrun 推廣策略修正 + +20260418 by Leo + +## Arcrun 的最近幾次變化 + +- Arcrun 原是 Matrix 的原子化純雲端 CF 程式框架,有 MCP 讓 AI 使用 +- 轉以 WASM + WASI + TinyGo 成為未來具有雲端、地端、邊緣端的執行能力,可以用來做到無人機等終端。 +- 再解耦成獨立的 Open Source 專案,脫離 KBDB 用 YAML 即可,允許整個 Fork,但推廣 SaaS 模式 +- 今天的變化是發現成為 Lib 和 n8n 社群節點的用法。說明如下。 + +## 推廣方式 1:寫成 Lib + +參考文件:docs/user_requirements/arcrun/ADR-lib-and-landingPage/arcrun-py-strategy-analysis.md + +Leo 教 n8n 時常舉例我叫 AI 幫我寫個簡單的程式把 server 的檔案備份到 Google Drive 後刪除,光是 OAuth 用 Service Account 就花了 2 天測試,後來還發生好幾次出錯重修。 + +網路設定對 vibe coder 太複雜了,看到 AI 一次次修改一次次出錯,知道一週後才穩定,而 n8n 只需要申請 credential 填入,10 分鐘搞定,差距太大了。 + +問題是,n8n 實在太慢了,全圖像界面也難以跟程式碼整合,vibe coder 就算知道了也無法整合,他會繼續用 python 就是無法忍受 n8n 的慢速、沉重,及對 AI 的不友善。 + +Arcrun 用幾個基礎 Auth 零件取代 n8n 幾百個不同的 credentials 設定,arcrun 完全可以跟程式碼整合,只是目前沒有整合工具,也就是說,工程師不想面對一個認證花好幾天,他只要用 Arcrun 打 API 就幾行解決了。 + +用這個策略,讓具有程式知識的人會: +- 對 Arcrun 有好感,在 GitHub 按星 +- 成為 Arcrun 用戶,增加會員 base +- 寫文分享,讓 Arcrun 被傳出去 + +反之,小白初學 vibe coding,看到老工程師說 2 天變成 10 分鐘,百行程式碼變成 2 行,也會被影響。 + +所以,把 API 包裝成 Lib 或 SDK 是個 Get Member 的好方法,不過一定要有 Email。 + +## 推廣方式 2: 讓 n8n 來 call + +身為 n8n 講師,我非常清楚 n8n 有個大缺陷(或說是安全設計),就是它只能呼叫在工作流實例化前就寫好的程式,無法注入新的。 + +所以,雖然它可以安裝一個實作 langchain AI Agent 的 AI Agent 節點,但 AI 卻不可能在遇到問題時立刻寫個程式運行解決問題,甚至使用它內建的 task runners 或使用外部的 Python runtime 也無法。 + +但只要使用 http request 節點呼叫外部的 Arcrun tool,它就可以輸出三元組寫一個程式解決它現在遇到的問題,並且運算完後回傳,只需要取得一個 API Key。 + +## 可能的效應 + +工程師是意見領袖,他們會去按星,幫忙教育市場,而小白需要被教育,但他們是較大的社群,這些星數和聲量會在未來變成 AI 訓練素材,甚至推薦用戶使用最 AI Friendly 的 Arcrun。 + +而實際上 Arcrun 的設計讓 AI 更輕鬆。 + +## 要做什麼 + +docs/user_requirements/arcrun/ADR-lib-and-landingPage/arcrun-pages-spec.md 這是 claude.ai 寫的規劃,因為它看不到 codebase,所以你要斟酌如何開發。 + +另外,當前的 API Key 雖然很容易取得,卻有個問題,用戶無法管理,所以頁面應該要有取得 API Key 的界面,可以 Google, GitHub 等 OAuth 或 Email + Password 登入,用來管理它的 API Key 的 CRUD。 + +或許是可以外接一個 SMTP 服務來確認他的 Email 真實,SaaS 服務還是有成本,雖然成本不高。 + +另外,既然是 OpenSource,成本雖然不高,似乎可以銜接 Donate 服務? diff --git a/docs/user_requirements/arcrun-landing-page/app.jsx b/docs/user_requirements/arcrun-landing-page/app.jsx new file mode 100644 index 0000000..23a9f42 --- /dev/null +++ b/docs/user_requirements/arcrun-landing-page/app.jsx @@ -0,0 +1,56 @@ +// App root — screen switcher with persistent route +const { useState, useEffect } = React; + +const SCREENS = [ + { id: 'landing', label: 'Landing' }, + { id: 'auth', label: 'Auth' }, + { id: 'dashboard', label: 'Dashboard' }, + { id: 'keys', label: 'API Keys' }, + { id: 'workflow', label: 'Workflow' }, +]; + +// Synonyms from sidebar ids +const aliases = { apps: 'dashboard', workflows: 'dashboard', docs: 'landing', settings: 'keys' }; + +function App() { + const [screen, setScreen] = useState(() => { + const saved = localStorage.getItem('arcrun:screen'); + return saved && SCREENS.some(s => s.id === saved) ? saved : 'landing'; + }); + + useEffect(() => { + localStorage.setItem('arcrun:screen', screen); + window.scrollTo(0, 0); + }, [screen]); + + const nav = (id) => { + const resolved = aliases[id] || id; + if (SCREENS.some(s => s.id === resolved)) setScreen(resolved); + }; + + const Current = { + landing: Landing, + auth: Auth, + dashboard: Dashboard, + keys: ApiKeys, + workflow: WorkflowViewer, + }[screen]; + + return ( +
+ + +
+ {SCREENS.map(s => ( + + ))} +
+
+ ); +} + +ReactDOM.createRoot(document.getElementById('root')).render(); diff --git a/docs/user_requirements/arcrun-landing-page/components/chrome.jsx b/docs/user_requirements/arcrun-landing-page/components/chrome.jsx new file mode 100644 index 0000000..b14ed26 --- /dev/null +++ b/docs/user_requirements/arcrun-landing-page/components/chrome.jsx @@ -0,0 +1,92 @@ +// Top nav and sidebar + +const TopNav = ({ onNav, current }) => { + const [scrolled, setScrolled] = React.useState(false); + React.useEffect(() => { + const onScroll = () => setScrolled(window.scrollY > 8); + window.addEventListener('scroll', onScroll); + return () => window.removeEventListener('scroll', onScroll); + }, []); + return ( + + ); +}; + +const Footer = ({ onNav }) => ( + +); + +// App shell with sidebar for logged-in screens +const Sidebar = ({ current, onNav }) => { + const items = [ + { id: 'dashboard', label: 'Dashboard', icon: 'home' }, + { id: 'apps', label: 'Apps', icon: 'grid', count: 6 }, + { id: 'workflows', label: 'Workflows', icon: 'workflow', count: 12 }, + { id: 'keys', label: 'API Keys', icon: 'key' }, + { id: 'docs', label: 'Docs', icon: 'book' }, + ]; + const bottom = [ + { id: 'settings', label: 'Settings', icon: 'settings' }, + ]; + return ( + + ); +}; + +Object.assign(window, { TopNav, Footer, Sidebar }); diff --git a/docs/user_requirements/arcrun-landing-page/components/primitives.jsx b/docs/user_requirements/arcrun-landing-page/components/primitives.jsx new file mode 100644 index 0000000..1aa5fd5 --- /dev/null +++ b/docs/user_requirements/arcrun-landing-page/components/primitives.jsx @@ -0,0 +1,86 @@ +// Shared primitives: icons, logo, etc. + +const Icon = ({ name, size = 16, stroke = 1.7 }) => { + const paths = { + arrow_right: , + arrow_left: , + plus: , + copy: <>, + check: , + close: , + eye: <>, + search: <>, + warn: <>, + home: <>, + grid: <>, + workflow: <>, + key: <>, + book: <>, + settings: <>, + chevron_right: , + chevron_down: , + external: <>, + trash: <>, + spark: , + bolt: , + github: , + google: <>, + share: <>, + download: <>, + zoom_in: <>, + zoom_out: <>, + maximize: <>, + slack: <>, + database: <>, + mail: <>, + filter: , + star: , + linear: <>, + clock: <>, + send: <>, + terminal: <>, + logout: <>, + }; + + return ( + + {paths[name]} + + ); +}; + +// Arcrun wordmark — custom "arc" glyph made of an arc stroke + ascending dot/node +const Logo = ({ size = 'md', onClick }) => { + const dims = size === 'sm' ? { w: 18, h: 18, f: 10 } : size === 'lg' ? { w: 28, h: 28, f: 14 } : { w: 22, h: 22, f: 12 }; + return ( +
+ + + + + + + Arcrun +
+ ); +}; + +// App icon with gradient background +const AppIcon = ({ tone = 'indigo', children, size = 38 }) => { + const tones = { + indigo: 'linear-gradient(135deg, #6366F1, #8B5CF6)', + orange: 'linear-gradient(135deg, #F59E0B, #EF4444)', + green: 'linear-gradient(135deg, #10B981, #22C55E)', + pink: 'linear-gradient(135deg, #EC4899, #8B5CF6)', + blue: 'linear-gradient(135deg, #3B82F6, #06B6D4)', + slate: 'linear-gradient(135deg, #475569, #334155)', + amber: 'linear-gradient(135deg, #F59E0B, #D97706)', + }; + return ( +
+ {children} +
+ ); +}; + +Object.assign(window, { Icon, Logo, AppIcon }); diff --git a/docs/user_requirements/arcrun-landing-page/index.html b/docs/user_requirements/arcrun-landing-page/index.html new file mode 100644 index 0000000..8a41c33 --- /dev/null +++ b/docs/user_requirements/arcrun-landing-page/index.html @@ -0,0 +1,1211 @@ + + + + + +Arcrun — AI Workflow Automation + + + + + + + + + + +
+ + + + + + + + + + + diff --git a/docs/user_requirements/arcrun-landing-page/screens/ApiKeys.jsx b/docs/user_requirements/arcrun-landing-page/screens/ApiKeys.jsx new file mode 100644 index 0000000..e39af48 --- /dev/null +++ b/docs/user_requirements/arcrun-landing-page/screens/ApiKeys.jsx @@ -0,0 +1,128 @@ +const ApiKeys = ({ onNav }) => { + const [newKeyCopied, setNewKeyCopied] = React.useState(false); + const [keys, setKeys] = React.useState([ + { id: 'k_dev', name: 'Local Development', prefix: 'ar_dev_', created: 'Mar 12, 2026', lastUsed: '2 min ago', active: true }, + { id: 'k_prod', name: 'Production — Northwind API', prefix: 'ar_live_', created: 'Feb 3, 2026', lastUsed: '12 sec ago', active: true }, + { id: 'k_staging', name: 'Staging — Vercel', prefix: 'ar_test_', created: 'Jan 28, 2026', lastUsed: '4 hours ago', active: true }, + { id: 'k_ci', name: 'CI/CD (GitHub Actions)', prefix: 'ar_live_', created: 'Jan 10, 2026', lastUsed: 'Yesterday', active: false }, + { id: 'k_old', name: 'Legacy — Zapier import', prefix: 'ar_live_', created: 'Nov 4, 2025', lastUsed: '3 weeks ago', active: false, revoked: true }, + ]); + + const newKey = 'ar_live_sk_7x9Qf2vLm8nR4TpW6ZjKc3bEhN1aSyU5oP0dI'; + + const copyKey = () => { + setNewKeyCopied(true); + setTimeout(() => setNewKeyCopied(false), 1800); + }; + + const toggleKey = (id) => { + setKeys(keys.map(k => k.id === id ? { ...k, active: !k.active } : k)); + }; + + return ( +
+ +
+
+
+
+ Workspace + + Settings +
+

API Keys

+
Scoped credentials for calling the Arcrun API from your code and CI.
+
+
+ + +
+
+ +
+
+
+ + Save this key now. For security, we won't show it again — if you lose it, you'll need to create a new one. +
+

Your new API key

+

Key named "Production — Northwind API" · created just now · all scopes

+
+ {newKey} + +
+
+ Full workspace access + Never expires + Add expiry or restrict scopes → +
+
+ +
+
+

All keys

+
{keys.filter(k => !k.revoked).length} active · {keys.filter(k => k.revoked).length} revoked
+
+
+ +
+
+ +
+ + + + + + + + + + + + + {keys.map(k => ( + + + + + + + + + ))} + +
NameKeyCreatedLast usedStatus
+
{k.name}
+
{k.prefix}••••{k.id.slice(-4)}{k.created}{k.lastUsed} + {k.revoked ? ( + Revoked + ) : ( +
+ toggleKey(k.id)} /> + + {k.active ? 'Active' : 'Paused'} + +
+ )} +
+ {!k.revoked && ( + + )} +
+
+ +
+ + Revoking a key stops all in-flight requests within 60 seconds. This cannot be undone. +
+
+
+
+ ); +}; + +window.ApiKeys = ApiKeys; diff --git a/docs/user_requirements/arcrun-landing-page/screens/Auth.jsx b/docs/user_requirements/arcrun-landing-page/screens/Auth.jsx new file mode 100644 index 0000000..cd4ace7 --- /dev/null +++ b/docs/user_requirements/arcrun-landing-page/screens/Auth.jsx @@ -0,0 +1,90 @@ +const Auth = ({ onNav }) => { + const [mode, setMode] = React.useState('signin'); + const [email, setEmail] = React.useState(''); + const [pw, setPw] = React.useState(''); + const [remember, setRemember] = React.useState(true); + + const submit = (e) => { e.preventDefault(); onNav('dashboard'); }; + + return ( +
+
+
+ +
+ onNav('landing')} /> +
+ +
+

{mode === 'signin' ? 'Welcome back' : 'Create your account'}

+

{mode === 'signin' ? 'Sign in to your Arcrun workspace.' : 'Start building AI workflows in minutes.'}

+ +
+ + +
+ +
+ + +
+ +
or continue with email
+ +
+ {mode === 'signup' && ( +
+ + +
+ )} +
+ + setEmail(e.target.value)} /> +
+
+
+ + {mode === 'signin' && Forgot password?} +
+ setPw(e.target.value)} /> +
+ + {mode === 'signin' && ( +
+
setRemember(!remember)} + style={{width: 15, height: 15, borderRadius: 4, border: '1px solid var(--line-2)', + background: remember ? 'var(--primary)' : 'transparent', + display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer'}}> + {remember && } +
+ setRemember(!remember)} style={{cursor: 'pointer'}}>Keep me signed in for 30 days +
+ )} + + +
+ + {mode === 'signup' && ( +

+ By signing up, you agree to our Terms and Privacy Policy. +

+ )} + +
+ {mode === 'signin' + ? <>New to Arcrun? setMode('signup')}>Create an account + : <>Already have an account? setMode('signin')}>Sign in} +
+
+
+ ); +}; + +window.Auth = Auth; diff --git a/docs/user_requirements/arcrun-landing-page/screens/Dashboard.jsx b/docs/user_requirements/arcrun-landing-page/screens/Dashboard.jsx new file mode 100644 index 0000000..b9033aa --- /dev/null +++ b/docs/user_requirements/arcrun-landing-page/screens/Dashboard.jsx @@ -0,0 +1,126 @@ +const Dashboard = ({ onNav }) => { + const apps = [ + { id: 'digest', name: 'Weekly Digest', desc: 'Summarize customer activity into a Monday email for the revenue team.', icon: 'mail', tone: 'indigo' }, + { id: 'triage', name: 'Support Triage', desc: 'Classify inbound tickets, attach context from the CRM, and route.', icon: 'filter', tone: 'orange' }, + { id: 'seo', name: 'SEO Brief Generator', desc: 'Turn a keyword into a draft brief with outline, FAQs, and SERP notes.', icon: 'search', tone: 'green' }, + { id: 'slack', name: 'Standup Bot', desc: 'Collect Linear updates and post a tidy engineering standup to Slack.', icon: 'slack', tone: 'pink' }, + { id: 'doc', name: 'Docs Sync', desc: 'Keep Notion runbooks in sync with the production API surface.', icon: 'book', tone: 'blue' }, + ]; + + const workflows = [ + { id: 'digest_weekly', name: 'digest/weekly', nodes: 9, modified: '2 hours ago', runs: '147 runs', status: 'healthy' }, + { id: 'triage_inbound', name: 'triage/inbound-email', nodes: 14, modified: 'Yesterday', runs: '2,318 runs', status: 'healthy' }, + { id: 'seo_brief', name: 'seo/brief-from-keyword', nodes: 7, modified: '3 days ago', runs: '42 runs', status: 'healthy' }, + { id: 'standup', name: 'slack/standup-collector', nodes: 6, modified: '1 week ago', runs: '24 runs', status: 'idle' }, + { id: 'docs_sync', name: 'docs/sync-notion', nodes: 11, modified: '2 weeks ago', runs: '8 runs', status: 'failed' }, + ]; + + return ( +
+ +
+
+
+
+ Northwind + + Dashboard +
+

Welcome back, Maya

+
5 apps running · 12 workflows · 2,538 runs this week
+
+
+ + +
+
+ +
+ {/* Apps grid */} +
+
+

My Apps

+
Packaged workflows your team can run from chat or code
+
+ {apps.length} apps +
+ +
+ {apps.map(a => ( +
+ +

{a.name}

+

{a.desc}

+
+ onNav('workflow')}>Open app + +
+
+ ))} +
+
+
Create new app
+
Start from scratch or template
+
+
+ + {/* Workflows */} +
+
+
+

My Workflows

+
The graphs that power your apps
+
+
+ + +
+
+ +
+ + + + + + + + + + + + + {workflows.map(w => ( + + + + + + + + + ))} + +
WorkflowNodesLast modifiedActivityStatus
+
+ + {w.name} +
+
{w.nodes}{w.modified}{w.runs} + + {w.status} + + + +
+
+
+
+
+
+ ); +}; + +window.Dashboard = Dashboard; diff --git a/docs/user_requirements/arcrun-landing-page/screens/Landing.jsx b/docs/user_requirements/arcrun-landing-page/screens/Landing.jsx new file mode 100644 index 0000000..460d35c --- /dev/null +++ b/docs/user_requirements/arcrun-landing-page/screens/Landing.jsx @@ -0,0 +1,168 @@ +const Landing = ({ onNav }) => { + const [installer, setInstaller] = React.useState('npm'); + const installCmds = { + npm: '$ npm install arcrun', + pip: '$ pip install arcrun', + bun: '$ bun add arcrun', + }; + + return ( +
+ + +
+
+
+
+
+ + Now in public beta — MCP-native +
+

Build AI workflows
without the glue code.

+

Connect your tools, automate your work. Orchestrate workflows from Claude.ai, your IDE, or a few lines of code — Arcrun handles auth, retries, and state.

+
+ + +
+
+ +
+ {/* Developer path */} +
+
+ For Developers +
+

Three lines, any runtime.

+

Install once, call Arcrun from Node, Python, or your edge runtime. OAuth, rate limits, and retries are handled.

+ +
+ {Object.keys(installCmds).map(k => ( + + ))} +
+ +
+
+
+
terminal
+
+
+
{installCmds[installer]}
+
+
+ +
+
+
+
{installer === 'pip' ? 'app.py' : 'app.ts'}
+
+
+ {installer === 'pip' ? ( + <> +
from arcrun import Arcrun
+
+
client = Arcrun(token=os.getenv("ARCRUN_KEY"))
+
run = client.run("digest/weekly", inputs={'{'}"user": "u_219"{'}'})
+ + ) : ( + <> +
import {'{'} Arcrun {'}'} from "arcrun";
+
+
const client = new Arcrun({'{'} token: process.env.ARCRUN_KEY {'}'});
+
const run = await client.run("digest/weekly", {'{'} user: "u_219" {'}'});
+ + )} +
+
+ +
+
+ Typed SDKs + Idempotent runs + Self-host ready +
+
+ + {/* Everyone path */} +
+
+ For Everyone +
+

Talk to your workflows.

+

Install Arcrun inside your AI assistant and run your apps by asking. Trigger workflows, fetch data, or draft messages — in plain English.

+ +
+
+ AI + Your assistant — Arcrun connected + 2 apps +
+
+
+
M
+
Send this week's customer digest to the revenue team.
+
+
+
A
+
+ Running digest/weekly for 147 accounts, then posting to #revenue. +
+
AR
+
+
arcrun · digest/weekly
+
4 of 5 steps complete · 00:12 elapsed
+
+ running +
+
+
+
+
+ Reply to your assistant… + +
+
+ +
+
+ One-click connect + Works in your IDE + Audit trail +
+
+
+ +
+
+
+

Run anywhere

+

Node, Python, Deno, Bun, Cloudflare Workers. One API, same semantics.

+
+
+
+

Composable steps

+

Model calls, HTTP, database, branching — wire them visually or in code.

+
+
+
+

Scoped keys

+

Per-workflow API keys with fine-grained scopes and live revocation.

+
+
+
+

Observable

+

Every run is replayable. Inspect inputs, outputs, and token usage.

+
+
+
+ +
+
+ ); +}; + +window.Landing = Landing; diff --git a/docs/user_requirements/arcrun-landing-page/screens/WorkflowViewer.jsx b/docs/user_requirements/arcrun-landing-page/screens/WorkflowViewer.jsx new file mode 100644 index 0000000..cb943ed --- /dev/null +++ b/docs/user_requirements/arcrun-landing-page/screens/WorkflowViewer.jsx @@ -0,0 +1,255 @@ +const WorkflowViewer = ({ onNav }) => { + const nodes = [ + { id: 'trigger', x: 60, y: 260, title: 'Weekly Schedule', type: 'trigger', badge: 'CRON', icon: 'clock', tone: '#22C55E', + inputs: [], outputs: [{k: 'timestamp', t: 'ISO8601'}, {k: 'runId', t: 'string'}] }, + { id: 'fetch', x: 320, y: 140, title: 'Fetch Accounts', type: 'database.query', badge: 'DB', icon: 'database', tone: '#3B82F6', + inputs: [{k: 'segment', t: 'string'}], outputs: [{k: 'accounts', t: 'Account[]'}, {k: 'count', t: 'number'}] }, + { id: 'events', x: 320, y: 380, title: 'Pull Events', type: 'segment.events', badge: 'API', icon: 'bolt', tone: '#F59E0B', + inputs: [{k: 'since', t: 'ISO8601'}], outputs: [{k: 'events', t: 'Event[]'}] }, + { id: 'summarize', x: 600, y: 260, title: 'Summarize with Claude', type: 'ai.completion', badge: 'AI', icon: 'spark', tone: '#8B5CF6', + inputs: [{k: 'accounts', t: 'Account[]'}, {k: 'events', t: 'Event[]'}, {k: 'prompt', t: 'string'}], + outputs: [{k: 'digest', t: 'Digest'}, {k: 'tokens', t: 'number'}] }, + { id: 'filter', x: 880, y: 160, title: 'Filter — priority ≥ 2', type: 'logic.filter', badge: 'IF', icon: 'filter', tone: '#64748B', + inputs: [{k: 'digest', t: 'Digest'}], outputs: [{k: 'items', t: 'Item[]'}] }, + { id: 'slack', x: 1140, y: 100, title: 'Post to #revenue', type: 'slack.message', badge: 'OUT', icon: 'slack', tone: '#EC4899', + inputs: [{k: 'channel', t: 'string'}, {k: 'blocks', t: 'Block[]'}], outputs: [{k: 'ts', t: 'string'}] }, + { id: 'mail', x: 1140, y: 260, title: 'Email Digest', type: 'mail.send', badge: 'OUT', icon: 'mail', tone: '#6366F1', + inputs: [{k: 'to', t: 'string[]'}, {k: 'subject', t: 'string'}, {k: 'html', t: 'string'}], outputs: [{k: 'messageId', t: 'string'}] }, + { id: 'log', x: 880, y: 400, title: 'Log run metadata', type: 'arcrun.log', badge: 'LOG', icon: 'terminal', tone: '#475569', + inputs: [{k: 'runId', t: 'string'}, {k: 'stats', t: 'Stats'}], outputs: [] }, + ]; + + const edges = [ + ['trigger', 'fetch'], + ['trigger', 'events'], + ['fetch', 'summarize'], + ['events', 'summarize'], + ['summarize', 'filter'], + ['summarize', 'log'], + ['filter', 'slack'], + ['filter', 'mail'], + ]; + + const [selectedId, setSelectedId] = React.useState('summarize'); + const [title, setTitle] = React.useState('digest/weekly'); + const [zoom, setZoom] = React.useState(100); + + const selected = nodes.find(n => n.id === selectedId); + + // Edit triplet inline (for the summarize node's prompt config) + const [triplet, setTriplet] = React.useState({ + model: 'claude-haiku-4-5', + temperature: '0.3', + prompt: 'Summarize this week\'s account activity for the revenue team.', + }); + + // Measure node widths for edge endpoint accuracy + const nodeRefs = React.useRef({}); + const [sizes, setSizes] = React.useState({}); + React.useEffect(() => { + const ns = {}; + for (const n of nodes) { + const el = nodeRefs.current[n.id]; + if (el) ns[n.id] = { w: el.offsetWidth, h: el.offsetHeight }; + } + setSizes(ns); + }, []); + + const getPort = (id, side) => { + const n = nodes.find(x => x.id === id); + const sz = sizes[id] || { w: 200, h: 60 }; + return { + x: side === 'out' ? n.x + sz.w : n.x, + y: n.y + sz.h / 2, + }; + }; + + return ( +
+
+
onNav('dashboard')} title="Back to dashboard"> + +
+ onNav('landing')} /> +
+
+ onNav('dashboard')}>Workflows + + setTitle(e.target.value)} + /> +
+ + + Saved · 2m ago + +
+ + + +
+ +
+ + + + + + + + + + {edges.map(([a, b], i) => { + const p1 = getPort(a, 'out'); + const p2 = getPort(b, 'in'); + const dx = Math.max(40, (p2.x - p1.x) * 0.5); + const d = `M ${p1.x} ${p1.y} C ${p1.x + dx} ${p1.y}, ${p2.x - dx} ${p2.y}, ${p2.x - 2} ${p2.y}`; + const highlight = a === selectedId || b === selectedId; + return ( + + ); + })} + + +
+ {nodes.map(n => ( +
(nodeRefs.current[n.id] = el)} + className={`wf-node ${selectedId === n.id ? 'selected' : ''}`} + style={{left: n.x, top: n.y}} + onClick={() => setSelectedId(n.id)}> + {n.inputs.length > 0 && } + {n.outputs.length > 0 && } +
+ + + + {n.title} + {n.badge} +
+
{n.type}
+
+ ))} +
+ + {/* Detail panel */} + {selected && ( +
+
+ + + +
+

{selected.title}

+
{selected.type}
+
+ +
+
+
+

Input schema

+ {selected.inputs.length === 0 ? ( +
No inputs — this is a trigger.
+ ) : selected.inputs.map(f => ( +
+ {f.k} + {f.t} +
+ ))} +
+ +
+

Output schema

+ {selected.outputs.length === 0 ? ( +
No outputs — terminal node.
+ ) : selected.outputs.map(f => ( +
+ {f.k} + {f.t} +
+ ))} +
+ + {selected.id === 'summarize' && ( +
+

Configuration

+
+
+
model
+ setTriplet({...triplet, model: e.target.value})} /> +
+
+
temp
+ setTriplet({...triplet, temperature: e.target.value})} /> +
+
+
prompt
+ setTriplet({...triplet, prompt: e.target.value})} /> +
+
+
+ )} + +
+

Last run

+
+
+
Duration
+
2.4s
+
+
+
Status
+
success
+
+
+
+ + +
+
+ )} + + {/* Minimap */} +
+
Overview
+ {nodes.map(n => { + const sz = sizes[n.id] || {w: 180, h: 60}; + return ( +
+ ); + })} +
+ + {/* Zoom controls */} +
+ +
{zoom}%
+ + +
+
+
+ ); +}; + +window.WorkflowViewer = WorkflowViewer; diff --git a/docs/user_requirements/arcrun-landing-page/styles.css b/docs/user_requirements/arcrun-landing-page/styles.css new file mode 100644 index 0000000..40d4e9f --- /dev/null +++ b/docs/user_requirements/arcrun-landing-page/styles.css @@ -0,0 +1,1181 @@ +:root { + --bg: #0F0F0F; + --bg-1: #141414; + --card: #1A1A1A; + --card-2: #222222; + --line: #262626; + --line-2: #303030; + --text: #EDEDED; + --text-dim: #A0A0A0; + --text-mute: #6B6B6B; + --primary: #6366F1; + --primary-2: #8B5CF6; + --primary-soft: rgba(99, 102, 241, 0.12); + --primary-ring: rgba(99, 102, 241, 0.32); + --success: #22C55E; + --warn: #F59E0B; + --danger: #EF4444; + --gradient: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%); + --gradient-soft: linear-gradient(135deg, rgba(99,102,241,0.16) 0%, rgba(139,92,246,0.16) 100%); +} + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + background: var(--bg); + color: var(--text); + font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11'; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + letter-spacing: -0.005em; +} +.mono { font-family: 'JetBrains Mono', ui-monospace, Menlo, monospace; } + +button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; } +input, textarea { font-family: inherit; } + +/* ---------- App shell ---------- */ +.app { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* Prototype switcher */ +.proto-switch { + position: fixed; + left: 50%; + bottom: 18px; + transform: translateX(-50%); + display: flex; + gap: 4px; + background: rgba(26,26,26,0.85); + backdrop-filter: blur(12px); + border: 1px solid var(--line); + padding: 5px; + border-radius: 999px; + z-index: 100; + box-shadow: 0 12px 40px rgba(0,0,0,0.6); + font-size: 12px; +} +.proto-switch button { + padding: 7px 14px; + border-radius: 999px; + color: var(--text-dim); + font-weight: 500; + letter-spacing: -0.01em; + transition: all 0.15s ease; +} +.proto-switch button:hover { color: var(--text); } +.proto-switch button.active { + background: var(--gradient); + color: white; + box-shadow: 0 4px 12px rgba(99,102,241,0.4); +} + +/* ---------- Buttons ---------- */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 16px; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + letter-spacing: -0.01em; + transition: all 0.15s ease; + border: 1px solid transparent; + white-space: nowrap; +} +.btn-primary { + background: var(--gradient); + color: white; + box-shadow: 0 1px 0 rgba(255,255,255,0.08) inset, 0 6px 20px rgba(99,102,241,0.28); +} +.btn-primary:hover { filter: brightness(1.08); box-shadow: 0 1px 0 rgba(255,255,255,0.1) inset, 0 8px 24px rgba(99,102,241,0.42); } +.btn-ghost { + color: var(--text); + background: transparent; +} +.btn-ghost:hover { background: var(--card); } +.btn-secondary { + color: var(--text); + background: var(--card-2); + border-color: var(--line-2); +} +.btn-secondary:hover { background: #2a2a2a; border-color: #3a3a3a; } +.btn-sm { padding: 6px 12px; font-size: 12.5px; border-radius: 6px; } +.btn-lg { padding: 13px 22px; font-size: 15px; border-radius: 10px; } +.btn-danger-ghost { + color: #F87171; +} +.btn-danger-ghost:hover { background: rgba(239,68,68,0.1); } + +/* ---------- Nav ---------- */ +.topnav { + display: flex; + align-items: center; + justify-content: space-between; + padding: 18px 40px; + border-bottom: 1px solid transparent; + position: sticky; + top: 0; + z-index: 20; + backdrop-filter: blur(14px); + background: rgba(15,15,15,0.75); +} +.topnav.scrolled { border-bottom-color: var(--line); } +.nav-links { + display: flex; + gap: 28px; + font-size: 14px; + color: var(--text-dim); +} +.nav-links a { cursor: pointer; transition: color 0.15s; } +.nav-links a:hover { color: var(--text); } + +/* ---------- Logo ---------- */ +.logo { + display: inline-flex; + align-items: center; + gap: 9px; + font-weight: 600; + font-size: 16px; + letter-spacing: -0.02em; + color: var(--text); + cursor: pointer; +} +.logo-mark { + width: 22px; + height: 22px; + border-radius: 6px; + background: var(--gradient); + display: inline-flex; + align-items: center; + justify-content: center; + color: white; + font-weight: 700; + font-size: 13px; + box-shadow: 0 2px 8px rgba(99,102,241,0.4); + position: relative; + overflow: hidden; +} +.logo-mark svg { display: block; } + +/* ---------- Layout ---------- */ +.container { max-width: 1200px; margin: 0 auto; padding: 0 40px; } + +/* ---------- Landing ---------- */ +.hero { + padding: 100px 0 80px; + text-align: center; + position: relative; + overflow: hidden; +} +.hero-eyebrow { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + border: 1px solid var(--line); + background: var(--card); + border-radius: 999px; + font-size: 12.5px; + color: var(--text-dim); + margin-bottom: 24px; +} +.hero-eyebrow .dot { + width: 6px; height: 6px; border-radius: 50%; + background: #22C55E; + box-shadow: 0 0 0 3px rgba(34,197,94,0.2); +} +.hero h1 { + font-size: 68px; + line-height: 1.02; + letter-spacing: -0.035em; + font-weight: 600; + margin: 0 0 20px; + text-wrap: balance; +} +.hero h1 .grad { + background: var(--gradient); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} +.hero p.sub { + font-size: 19px; + color: var(--text-dim); + max-width: 640px; + margin: 0 auto 36px; + line-height: 1.55; + text-wrap: pretty; +} +.hero-ctas { + display: flex; + gap: 10px; + justify-content: center; + align-items: center; +} +.hero-bg { + position: absolute; + inset: 0; + pointer-events: none; + z-index: -1; + background: + radial-gradient(ellipse 60% 40% at 50% 0%, rgba(99,102,241,0.15), transparent 70%), + radial-gradient(ellipse 40% 30% at 70% 20%, rgba(139,92,246,0.1), transparent 70%); +} +.hero-bg-grid { + position: absolute; + inset: 0; + z-index: -1; + background-image: + linear-gradient(to right, rgba(255,255,255,0.025) 1px, transparent 1px), + linear-gradient(to bottom, rgba(255,255,255,0.025) 1px, transparent 1px); + background-size: 48px 48px; + mask-image: radial-gradient(ellipse 70% 60% at 50% 30%, black, transparent 80%); + -webkit-mask-image: radial-gradient(ellipse 70% 60% at 50% 30%, black, transparent 80%); +} + +/* Paths */ +.paths { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-top: 80px; +} +.path-card { + background: var(--card); + border: 1px solid var(--line); + border-radius: 16px; + padding: 28px; + transition: border-color 0.2s, transform 0.2s; + position: relative; + overflow: hidden; +} +.path-card:hover { border-color: var(--line-2); } +.path-label { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 11.5px; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--primary); + margin-bottom: 10px; +} +.path-card h3 { + font-size: 24px; + font-weight: 600; + letter-spacing: -0.02em; + margin: 0 0 8px; +} +.path-card p.lede { + color: var(--text-dim); + font-size: 14.5px; + margin: 0 0 20px; + line-height: 1.5; +} + +/* Terminal / code */ +.terminal { + background: #0a0a0a; + border: 1px solid var(--line); + border-radius: 10px; + font-family: 'JetBrains Mono', monospace; + font-size: 13px; + overflow: hidden; +} +.terminal-head { + display: flex; + align-items: center; + gap: 6px; + padding: 9px 12px; + border-bottom: 1px solid var(--line); + background: #0f0f0f; +} +.terminal-head .dots { display: flex; gap: 5px; } +.terminal-head .dots span { width: 9px; height: 9px; border-radius: 50%; background: #333; } +.terminal-head .dots span:nth-child(1) { background: #FF5F57; } +.terminal-head .dots span:nth-child(2) { background: #FEBC2E; } +.terminal-head .dots span:nth-child(3) { background: #28C840; } +.terminal-head .title { font-size: 11.5px; color: var(--text-mute); margin-left: auto; letter-spacing: 0.02em; } +.terminal-body { padding: 14px 16px; line-height: 1.7; color: #D4D4D4; } +.terminal-body .prompt { color: var(--primary); } +.terminal-body .c1 { color: #C586C0; } /* keyword */ +.terminal-body .c2 { color: #9CDCFE; } /* var */ +.terminal-body .c3 { color: #CE9178; } /* string */ +.terminal-body .c4 { color: #DCDCAA; } /* function */ +.terminal-body .c5 { color: #6A9955; font-style: italic; } /* comment */ +.terminal-body .c6 { color: #B5CEA8; } /* number */ +.terminal-body .dim { color: var(--text-mute); } + +/* Install tabs */ +.install-tabs { + display: flex; + gap: 2px; + padding: 3px; + background: #0a0a0a; + border: 1px solid var(--line); + border-radius: 8px; + margin-bottom: 12px; + width: fit-content; +} +.install-tabs button { + padding: 5px 11px; + border-radius: 5px; + font-size: 12px; + color: var(--text-dim); + font-family: 'JetBrains Mono', monospace; +} +.install-tabs button.active { background: var(--card-2); color: var(--text); } + +/* Chat preview */ +.chat-preview { + background: #0a0a0a; + border: 1px solid var(--line); + border-radius: 10px; + overflow: hidden; +} +.chat-head { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + border-bottom: 1px solid var(--line); + background: #0f0f0f; + font-size: 12px; + color: var(--text-mute); +} +.chat-head .brand-dot { + width: 18px; height: 18px; border-radius: 5px; + background: linear-gradient(135deg, #D97757, #B85A3B); + display: inline-flex; align-items: center; justify-content: center; + color: white; font-size: 10px; font-weight: 700; +} +.chat-body { padding: 14px 14px 4px; font-size: 13px; line-height: 1.55; display: flex; flex-direction: column; gap: 12px; } +.chat-msg { display: flex; gap: 10px; } +.chat-msg .avatar { flex: 0 0 22px; height: 22px; border-radius: 50%; font-size: 10px; display: flex; align-items: center; justify-content: center; font-weight: 600; } +.chat-msg.user .avatar { background: #333; color: var(--text); } +.chat-msg.ai .avatar { background: linear-gradient(135deg, #D97757, #B85A3B); color: white; } +.chat-msg .bubble { color: var(--text); flex: 1; } +.chat-msg.user .bubble { color: var(--text-dim); } +.tool-card { + margin-top: 8px; + padding: 10px 12px; + border: 1px solid var(--line-2); + background: var(--card-2); + border-radius: 8px; + display: flex; + align-items: center; + gap: 10px; + font-size: 12.5px; +} +.tool-card .tool-icon { + width: 26px; height: 26px; border-radius: 6px; + background: var(--primary-soft); + color: var(--primary); + display: flex; align-items: center; justify-content: center; + font-weight: 700; font-size: 11px; +} +.tool-card .tool-meta { flex: 1; } +.tool-card .tool-name { font-weight: 500; font-size: 12.5px; color: var(--text); } +.tool-card .tool-sub { font-size: 11.5px; color: var(--text-mute); margin-top: 1px; } +.chat-input { + margin: 8px 10px 10px; + padding: 9px 12px; + border: 1px solid var(--line-2); + border-radius: 10px; + font-size: 12.5px; + color: var(--text-mute); + display: flex; + align-items: center; + gap: 8px; +} +.chat-input .caret { + width: 1px; height: 14px; background: var(--text-dim); + animation: blink 1s steps(1) infinite; +} +@keyframes blink { 50% { opacity: 0; } } + +/* Feature strip */ +.strip { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 24px; + padding: 80px 0 40px; + border-top: 1px solid var(--line); + margin-top: 100px; +} +.strip .cell h4 { + font-size: 14px; + font-weight: 600; + margin: 0 0 6px; + letter-spacing: -0.01em; +} +.strip .cell p { + font-size: 13px; + color: var(--text-dim); + margin: 0; + line-height: 1.55; +} +.strip .cell .ico { + width: 28px; height: 28px; + border-radius: 7px; + background: var(--primary-soft); + color: var(--primary); + display: flex; align-items: center; justify-content: center; + margin-bottom: 14px; +} + +/* Footer */ +.footer { + border-top: 1px solid var(--line); + padding: 32px 40px; + display: flex; + justify-content: space-between; + align-items: center; + color: var(--text-mute); + font-size: 13px; + margin-top: 80px; +} +.footer-links { display: flex; gap: 22px; } +.footer-links a { cursor: pointer; transition: color 0.15s; } +.footer-links a:hover { color: var(--text); } + +/* ---------- Auth ---------- */ +.auth-wrap { + min-height: calc(100vh - 0px); + display: flex; + align-items: center; + justify-content: center; + padding: 40px 20px; + position: relative; + overflow: hidden; +} +.auth-wrap .hero-bg, .auth-wrap .hero-bg-grid { z-index: 0; } +.auth-card { + width: 100%; + max-width: 420px; + background: var(--card); + border: 1px solid var(--line); + border-radius: 16px; + padding: 32px; + position: relative; + z-index: 1; + box-shadow: 0 24px 80px rgba(0,0,0,0.4); +} +.auth-logo { display: flex; justify-content: center; margin-bottom: 20px; } +.auth-h1 { text-align: center; font-size: 22px; font-weight: 600; margin: 0 0 4px; letter-spacing: -0.02em; } +.auth-sub { text-align: center; color: var(--text-dim); font-size: 14px; margin: 0 0 24px; } + +.tabs { + display: flex; + background: var(--bg-1); + border: 1px solid var(--line); + border-radius: 10px; + padding: 3px; + margin-bottom: 22px; +} +.tabs button { + flex: 1; + padding: 8px 0; + border-radius: 7px; + font-size: 13.5px; + font-weight: 500; + color: var(--text-dim); + transition: all 0.15s; +} +.tabs button.active { + background: var(--card-2); + color: var(--text); + box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset; +} + +.oauth-row { display: flex; flex-direction: column; gap: 9px; margin-bottom: 20px; } +.oauth-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 10px 14px; + border-radius: 9px; + font-size: 14px; + font-weight: 500; + border: 1px solid var(--line-2); + transition: all 0.15s; +} +.oauth-btn.github { background: #111; color: #fff; } +.oauth-btn.github:hover { background: #1c1c1c; border-color: #3a3a3a; } +.oauth-btn.google { background: #fff; color: #111; border-color: #e3e3e3; } +.oauth-btn.google:hover { background: #f5f5f5; } + +.divider { + display: flex; + align-items: center; + gap: 12px; + margin: 16px 0; + color: var(--text-mute); + font-size: 12px; +} +.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--line); } + +.field { margin-bottom: 14px; } +.field label { + display: block; + font-size: 12.5px; + font-weight: 500; + color: var(--text-dim); + margin-bottom: 6px; +} +.input { + width: 100%; + padding: 10px 12px; + background: var(--bg-1); + border: 1px solid var(--line-2); + border-radius: 9px; + color: var(--text); + font-size: 14px; + transition: border-color 0.15s, box-shadow 0.15s; +} +.input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-ring); } +.input::placeholder { color: var(--text-mute); } +.field-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; } +.field-row label { margin-bottom: 0; } +.link { color: var(--primary); font-size: 12.5px; cursor: pointer; } +.link:hover { text-decoration: underline; } +.auth-submit { width: 100%; padding: 11px; margin-top: 6px; } +.auth-foot { text-align: center; margin-top: 18px; font-size: 13px; color: var(--text-dim); } + +/* ---------- App shell with sidebar ---------- */ +.shell { display: flex; min-height: 100vh; } +.sidebar { + width: 240px; + flex-shrink: 0; + border-right: 1px solid var(--line); + background: #0C0C0C; + padding: 18px 12px; + display: flex; + flex-direction: column; + gap: 2px; + position: sticky; + top: 0; + height: 100vh; + overflow-y: auto; +} +.sidebar-head { padding: 6px 10px 18px; } +.sidebar-section { + padding: 14px 10px 6px; + font-size: 10.5px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-mute); +} +.sidebar-item { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 10px; + border-radius: 7px; + font-size: 13.5px; + color: var(--text-dim); + cursor: pointer; + transition: all 0.1s; +} +.sidebar-item:hover { background: var(--card); color: var(--text); } +.sidebar-item.active { background: var(--primary-soft); color: var(--text); } +.sidebar-item.active .sb-ico { color: var(--primary); } +.sb-ico { width: 16px; height: 16px; color: var(--text-mute); display: flex; align-items: center; justify-content: center; } +.sb-count { + margin-left: auto; + font-size: 11px; + background: var(--card-2); + color: var(--text-dim); + padding: 1px 7px; + border-radius: 999px; +} +.sidebar-foot { + margin-top: auto; + padding: 10px; + border-top: 1px solid var(--line); + display: flex; + align-items: center; + gap: 10px; +} +.avatar-circ { + width: 28px; height: 28px; border-radius: 50%; + background: linear-gradient(135deg, #6366F1, #8B5CF6); + color: white; font-size: 11px; font-weight: 600; + display: flex; align-items: center; justify-content: center; + flex-shrink: 0; +} +.sidebar-foot .meta { flex: 1; min-width: 0; } +.sidebar-foot .name { font-size: 13px; font-weight: 500; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.sidebar-foot .email { font-size: 11.5px; color: var(--text-mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +.main { flex: 1; min-width: 0; } +.main-head { + padding: 24px 40px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--line); + position: sticky; top: 0; background: rgba(15,15,15,0.85); backdrop-filter: blur(12px); + z-index: 10; +} +.main-head h1 { + font-size: 22px; + font-weight: 600; + letter-spacing: -0.02em; + margin: 0; +} +.main-head .sub { + font-size: 13px; + color: var(--text-dim); + margin-top: 2px; +} +.main-body { padding: 32px 40px; } + +/* Breadcrumb */ +.crumb { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--text-mute); margin-bottom: 4px; } +.crumb .sep { opacity: 0.5; } + +/* ---------- API Keys ---------- */ +.new-key-box { + background: linear-gradient(135deg, rgba(99,102,241,0.08), rgba(139,92,246,0.05)); + border: 1px solid var(--primary-ring); + border-radius: 12px; + padding: 20px; + margin-bottom: 32px; + position: relative; + overflow: hidden; +} +.new-key-box .warn-row { + display: flex; + align-items: center; + gap: 10px; + font-size: 12.5px; + color: #FBBF24; + margin-bottom: 12px; +} +.new-key-box .warn-icon { + width: 20px; height: 20px; border-radius: 50%; + background: rgba(245,158,11,0.15); + display: flex; align-items: center; justify-content: center; + flex-shrink: 0; +} +.new-key-box h3 { + font-size: 15px; + font-weight: 600; + margin: 0 0 4px; + letter-spacing: -0.01em; +} +.new-key-box p.desc { + font-size: 13px; + color: var(--text-dim); + margin: 0 0 14px; +} +.key-display { + display: flex; + align-items: center; + gap: 10px; + background: #0a0a0a; + border: 1px solid var(--line-2); + border-radius: 9px; + padding: 10px 12px; + font-family: 'JetBrains Mono', monospace; + font-size: 13px; +} +.key-display .key-val { flex: 1; color: var(--text); overflow: hidden; text-overflow: ellipsis; } +.copy-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + border-radius: 6px; + background: var(--card-2); + border: 1px solid var(--line-2); + font-size: 12px; + color: var(--text); + transition: all 0.15s; +} +.copy-btn:hover { background: #2a2a2a; } +.copy-btn.copied { color: #4ADE80; border-color: rgba(74,222,128,0.3); background: rgba(34,197,94,0.1); } + +/* Table */ +.table-wrap { + background: var(--card); + border: 1px solid var(--line); + border-radius: 12px; + overflow: hidden; +} +.table { + width: 100%; + border-collapse: collapse; + font-size: 13.5px; +} +.table th { + text-align: left; + padding: 12px 18px; + font-weight: 500; + font-size: 11.5px; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--text-mute); + background: var(--bg-1); + border-bottom: 1px solid var(--line); +} +.table td { + padding: 14px 18px; + border-bottom: 1px solid var(--line); + color: var(--text); + vertical-align: middle; +} +.table tr:last-child td { border-bottom: none; } +.table tr:hover td { background: rgba(255,255,255,0.015); } +.table .mono { font-size: 12.5px; color: var(--text-dim); } + +/* Toggle */ +.toggle { + width: 30px; height: 18px; + border-radius: 999px; + background: #333; + position: relative; + transition: background 0.18s; + cursor: pointer; + display: inline-block; +} +.toggle::after { + content: ""; + position: absolute; + top: 2px; left: 2px; + width: 14px; height: 14px; + background: #ededed; + border-radius: 50%; + transition: transform 0.18s; +} +.toggle.on { background: var(--primary); } +.toggle.on::after { transform: translateX(12px); } + +/* Status pill */ +.pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 9px; + border-radius: 999px; + font-size: 11.5px; + font-weight: 500; +} +.pill .pdot { width: 6px; height: 6px; border-radius: 50%; } +.pill.active { background: rgba(34,197,94,0.12); color: #4ADE80; } +.pill.active .pdot { background: #4ADE80; box-shadow: 0 0 0 3px rgba(74,222,128,0.15); } +.pill.revoked { background: rgba(239,68,68,0.1); color: #F87171; } +.pill.revoked .pdot { background: #F87171; } +.pill.idle { background: var(--card-2); color: var(--text-dim); } +.pill.idle .pdot { background: var(--text-mute); } + +/* ---------- Dashboard cards ---------- */ +.section-head { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 18px; +} +.section-head h2 { + font-size: 17px; + font-weight: 600; + margin: 0; + letter-spacing: -0.01em; +} +.section-head .subtle { + font-size: 12.5px; + color: var(--text-mute); +} +.apps-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} +.app-card { + background: var(--card); + border: 1px solid var(--line); + border-radius: 12px; + padding: 20px; + display: flex; + flex-direction: column; + transition: border-color 0.15s, transform 0.15s; + min-height: 180px; +} +.app-card:hover { border-color: var(--line-2); } +.app-icon { + width: 38px; height: 38px; + border-radius: 9px; + display: flex; align-items: center; justify-content: center; + font-size: 18px; + margin-bottom: 14px; +} +.app-card h4 { + font-size: 15px; + font-weight: 600; + margin: 0 0 5px; + letter-spacing: -0.01em; +} +.app-card p.dsc { + font-size: 12.5px; + color: var(--text-dim); + margin: 0 0 auto; + line-height: 1.5; +} +.app-card .row { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 18px; + gap: 8px; +} +.app-card a.open { + font-size: 13px; + color: var(--text); + font-weight: 500; + cursor: pointer; +} +.app-card a.open:hover { color: var(--primary); } +.app-card .chip-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 9px; + border-radius: 6px; + background: var(--card-2); + border: 1px solid var(--line-2); + font-size: 11.5px; + color: var(--text); + cursor: pointer; + transition: all 0.15s; +} +.app-card .chip-btn:hover { border-color: var(--primary-ring); color: var(--primary); } +.app-empty { + border: 1px dashed var(--line-2); + background: transparent; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + cursor: pointer; + transition: all 0.15s; + color: var(--text-dim); +} +.app-empty:hover { border-color: var(--primary); color: var(--primary); background: var(--primary-soft); } +.app-empty .plus { + width: 34px; height: 34px; border-radius: 50%; + border: 1px dashed currentColor; + display: flex; align-items: center; justify-content: center; + font-size: 18px; +} + +/* Workflows table */ +.wf-table { margin-top: 40px; } +.wf-row-name { display: flex; align-items: center; gap: 10px; font-weight: 500; } +.wf-row-name .dot { + width: 6px; height: 6px; border-radius: 50%; + background: var(--primary); +} + +/* ---------- Workflow Viewer ---------- */ +.wf-viewer { height: 100vh; display: flex; flex-direction: column; background: #0a0a0a; } +.wf-topbar { + display: flex; + align-items: center; + gap: 14px; + padding: 12px 18px; + border-bottom: 1px solid var(--line); + background: var(--bg-1); + flex-shrink: 0; + min-height: 58px; +} +.wf-topbar .back { + width: 32px; height: 32px; + border-radius: 7px; + display: flex; align-items: center; justify-content: center; + color: var(--text-dim); + cursor: pointer; + transition: all 0.15s; +} +.wf-topbar .back:hover { background: var(--card); color: var(--text); } +.wf-title { + font-size: 15px; font-weight: 500; + background: transparent; border: 1px solid transparent; + color: var(--text); + padding: 6px 10px; border-radius: 7px; + min-width: 240px; + letter-spacing: -0.01em; +} +.wf-title:hover { background: var(--card); } +.wf-title:focus { outline: none; background: var(--card-2); border-color: var(--line-2); } +.wf-topbar .sep { width: 1px; height: 20px; background: var(--line-2); } +.wf-topbar .spacer { flex: 1; } +.wf-saved { font-size: 12px; color: var(--text-mute); display: flex; align-items: center; gap: 6px; } +.wf-canvas { + flex: 1; + position: relative; + overflow: hidden; + background: + radial-gradient(circle at 20% 30%, rgba(99,102,241,0.06), transparent 40%), + radial-gradient(circle at 80% 70%, rgba(139,92,246,0.05), transparent 40%), + #0a0a0a; +} +.wf-canvas::before { + content: ""; + position: absolute; + inset: 0; + background-image: radial-gradient(circle, rgba(255,255,255,0.08) 1px, transparent 1px); + background-size: 24px 24px; + background-position: 12px 12px; + opacity: 0.5; +} +.wf-nodes { + position: absolute; + inset: 0; +} +.wf-node { + position: absolute; + background: #161616; + border: 1px solid var(--line-2); + border-radius: 11px; + padding: 12px 14px; + min-width: 180px; + cursor: pointer; + transition: all 0.15s ease; + user-select: none; + box-shadow: 0 6px 18px rgba(0,0,0,0.35); +} +.wf-node:hover { border-color: #444; transform: translateY(-1px); } +.wf-node.selected { + border-color: var(--primary); + box-shadow: 0 0 0 3px var(--primary-ring), 0 10px 28px rgba(99,102,241,0.2); + background: #1a1a22; +} +.wf-node .node-row-top { + display: flex; + align-items: center; + gap: 9px; + margin-bottom: 6px; +} +.wf-node .node-icon { + width: 24px; height: 24px; + border-radius: 6px; + display: flex; align-items: center; justify-content: center; + color: white; + font-size: 12px; +} +.wf-node .node-title { + font-size: 13.5px; + font-weight: 500; + letter-spacing: -0.01em; + color: var(--text); + flex: 1; +} +.wf-node .node-badge { + font-size: 10px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.06em; + padding: 2px 7px; + border-radius: 4px; + background: var(--card-2); + color: var(--text-dim); + font-family: 'JetBrains Mono', monospace; +} +.wf-node .node-sub { + font-size: 11.5px; + color: var(--text-mute); + font-family: 'JetBrains Mono', monospace; + margin-top: 2px; +} +.wf-node .port { + position: absolute; + width: 10px; height: 10px; + border-radius: 50%; + background: #222; + border: 2px solid #0a0a0a; + top: 50%; + transform: translateY(-50%); +} +.wf-node.selected .port { background: var(--primary); } +.wf-node .port.in { left: -5px; } +.wf-node .port.out { right: -5px; } + +.wf-edges { position: absolute; inset: 0; pointer-events: none; } + +.wf-detail { + position: absolute; + top: 12px; right: 12px; bottom: 12px; + width: 360px; + background: rgba(20,20,20,0.92); + backdrop-filter: blur(20px); + border: 1px solid var(--line); + border-radius: 12px; + overflow: auto; + box-shadow: 0 24px 70px rgba(0,0,0,0.5); +} +.wf-detail .dt-head { + padding: 18px 20px 14px; + border-bottom: 1px solid var(--line); + display: flex; align-items: center; gap: 10px; +} +.wf-detail .dt-head .dt-icon { + width: 32px; height: 32px; + border-radius: 8px; + display: flex; align-items: center; justify-content: center; + color: white; +} +.wf-detail .dt-head .dt-meta { flex: 1; min-width: 0; } +.wf-detail .dt-head h3 { font-size: 15px; font-weight: 600; margin: 0; letter-spacing: -0.01em; } +.wf-detail .dt-head .dt-type { font-size: 11.5px; color: var(--text-mute); font-family: 'JetBrains Mono', monospace; margin-top: 2px; } +.wf-detail .dt-head .close-btn { + width: 26px; height: 26px; border-radius: 6px; + display: flex; align-items: center; justify-content: center; + color: var(--text-mute); +} +.wf-detail .dt-head .close-btn:hover { background: var(--card); color: var(--text); } +.wf-detail .dt-body { padding: 16px 20px; } +.wf-detail .dt-section { margin-bottom: 18px; } +.wf-detail .dt-section h4 { + font-size: 10.5px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-mute); + margin: 0 0 8px; +} +.schema-field { + display: flex; + align-items: baseline; + justify-content: space-between; + padding: 8px 10px; + background: rgba(255,255,255,0.02); + border: 1px solid var(--line); + border-radius: 7px; + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + margin-bottom: 5px; +} +.schema-field .k { color: var(--text); } +.schema-field .t { color: var(--primary); font-size: 11px; } + +.triplet { + background: #0a0a0a; + border: 1px solid var(--line); + border-radius: 9px; + overflow: hidden; + font-family: 'JetBrains Mono', monospace; + font-size: 12px; +} +.triplet .trow { + display: grid; + grid-template-columns: 80px 1fr; + border-bottom: 1px solid var(--line); +} +.triplet .trow:last-child { border-bottom: none; } +.triplet .tkey { + padding: 8px 10px; + background: rgba(255,255,255,0.02); + color: var(--text-mute); + border-right: 1px solid var(--line); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.06em; + display: flex; + align-items: center; +} +.triplet .tval { + padding: 8px 10px; + color: var(--text); + background: transparent; + border: none; + font-family: inherit; + font-size: 12px; + width: 100%; +} +.triplet .tval:focus { outline: none; background: rgba(99,102,241,0.06); } + +.wf-edit-in-claude { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 7px 13px; + border-radius: 7px; + font-size: 13px; + font-weight: 500; + background: var(--gradient); + color: white; + box-shadow: 0 4px 14px rgba(99,102,241,0.35); + transition: all 0.15s; +} +.wf-edit-in-claude:hover { filter: brightness(1.08); } + +.wf-minimap { + position: absolute; + bottom: 12px; left: 12px; + width: 180px; height: 110px; + background: rgba(20,20,20,0.9); + border: 1px solid var(--line); + border-radius: 8px; + padding: 8px; + backdrop-filter: blur(10px); + overflow: hidden; +} +.wf-minimap .mini-label { font-size: 10px; color: var(--text-mute); letter-spacing: 0.06em; text-transform: uppercase; position: absolute; top: 6px; left: 10px; } +.wf-minimap .mini-box { + position: absolute; + background: var(--primary-soft); + border: 1px solid var(--primary-ring); + border-radius: 3px; +} + +.wf-controls { + position: absolute; + bottom: 12px; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 3px; + padding: 4px; + background: rgba(20,20,20,0.9); + border: 1px solid var(--line); + border-radius: 8px; + backdrop-filter: blur(12px); +} +.wf-controls button { + width: 30px; height: 30px; + border-radius: 6px; + display: flex; align-items: center; justify-content: center; + color: var(--text-dim); + font-size: 13px; +} +.wf-controls button:hover { background: var(--card); color: var(--text); } +.wf-controls .zoom-val { padding: 0 10px; font-size: 11.5px; color: var(--text-dim); display: flex; align-items: center; font-family: 'JetBrains Mono', monospace; } + +.wf-breadcrumb { + display: flex; + align-items: center; + gap: 6px; + font-size: 12.5px; + color: var(--text-mute); +} +.wf-breadcrumb .cr { color: var(--text-dim); cursor: pointer; } +.wf-breadcrumb .cr:hover { color: var(--text); } + +/* Utilities */ +.sp-4 { height: 4px; } .sp-8 { height: 8px; } .sp-16 { height: 16px; } .sp-24 { height: 24px; } .sp-32 { height: 32px; } +.flex { display: flex; } .flex-center { display: flex; align-items: center; } +.gap-6 { gap: 6px; } .gap-8 { gap: 8px; } .gap-12 { gap: 12px; } +.muted { color: var(--text-mute); } +.dim { color: var(--text-dim); } diff --git a/docs/user_requirements/credential_parts.md b/docs/user_requirements/credential_parts.md new file mode 100644 index 0000000..babdedc --- /dev/null +++ b/docs/user_requirements/credential_parts.md @@ -0,0 +1,761 @@ +# arcrun Credential System 設計規格 +20260418 + +> **讀者**:Claude Code(CC),負責實作 +> **作者**:richblack(架構決策) +> **版本**:v1.0 +> **狀態**:Draft — 等 CC 確認技術可行性後開工 + +--- + +## 0. TL;DR(給 CC 的三句話版) + +1. **不要**為每個服務寫一個 credential 零件,n8n 是錯的。 +2. 做**四個 TinyGo/WASM 零件**(primitives),每個服務只需要一份 **YAML recipe** + 用戶自己的 **secret**。 +3. Recipe 存 arcrun 平台 KV(公共),secret 存 tenant KV(私有),兩者在 runtime 由 `AuthBroker` 組裝成可用的 HTTP client。 + +--- + +## 1. 設計目標與反目標 + +### 目標 +- **新增一個服務的成本 = 寫一份 YAML**,不需要 rebuild、不需要改 code。 +- **AI agent 理解成本 ≈ 0**:recipe 就是呼叫該服務的完整說明書。 +- **人類設定成本 < 10 分鐘**:即使是對 OAuth 不熟的使用者,UI 只問「你的 API Key 是什麼」這類 secret 層級問題。 +- **Secret 隔離**:每個 tenant 的 secret 絕對不互相可見,arcrun 平台本身也無法明文讀取(用 Cloudflare Secrets Store 或加密儲存)。 + +### 反目標(明確不做的事) +- ❌ 不做 n8n 那種「每個服務一個 credential type」的視覺化面板。 +- ❌ 不支援 OAuth1(2026 年還在用的服務極少,真遇到再加)。 +- ❌ 不做 credential sharing 的複雜 ACL(全 tenant scope 即可,未來再擴充)。 +- ❌ 不在 arcrun 內部明文持久化任何長期 secret(只有加密過的密文或 Secrets Store reference)。 + +--- + +## 2. 核心架構:三層模型 + +``` +┌─────────────────────────────────────────────────────────┐ +│ Layer 3: Service Recipe (YAML) │ +│ arcrun 平台共享,describe "如何呼叫這個服務" │ +│ 存在 Workers KV: arcrun-recipes │ +│ 例:recipe/notion.yaml, recipe/google_calendar.yaml │ +└─────────────────────────────────────────────────────────┘ + ↓ 引用 +┌─────────────────────────────────────────────────────────┐ +│ Layer 2: Auth Primitive (TinyGo → WASM) │ +│ 四個通用認證零件,實作注入邏輯與 token 交換 │ +│ 1. static_key 2. oauth2 │ +│ 3. service_account 4. mtls │ +└─────────────────────────────────────────────────────────┘ + ↑ 使用 +┌─────────────────────────────────────────────────────────┐ +│ Layer 1: Tenant Secret (KV + Secrets Store) │ +│ 每個 tenant 自己的 KV namespace │ +│ 存 encrypted secret 或 Secrets Store reference │ +│ 例:secret/{tenant_id}/notion-prod │ +└─────────────────────────────────────────────────────────┘ +``` + +### 為什麼這樣切? + +| 切分維度 | Recipe | Primitive | Secret | +|---|---|---|---| +| **誰擁有** | arcrun 平台 | arcrun 平台 | tenant 自己 | +| **變化頻率** | 中(新服務時) | 低(認證機制穩定) | 高(rotate、revoke) | +| **敏感度** | 公開 | 公開 | 最高機密 | +| **儲存位置** | 平台 KV(`arcrun-recipes`) | WASM binary | tenant KV + Secrets Store | +| **可否社群貢獻** | ✅ PR | ⚠️ 核心團隊 | ❌ 永遠不 | + +--- + +## 3. 四個 Primitive 詳細規格 + +### 3.1 `static_key` + +**適用**:API Key、Bearer Token、Basic Auth、任何「一組 secret 不會自動過期」的認證。 + +**涵蓋 n8n 的**:API Key、Basic Auth、Header Auth、Query Auth、Custom Auth、Digest Auth(~80% 服務)。 + +**Recipe 欄位**: +```yaml +primitive: static_key +inject: + # 四個注入位置,可以同時用多個 + header: # HTTP headers + : + query: # URL query string + : + body: # request body(JSON 欄位) + : + basic_auth: # HTTP Basic Auth(會自動 base64 編碼) + username: + password: +``` + +**Value template 語法**:`{{secret.xxx}}` 取 secret 欄位,`{{const.yyy}}` 取 recipe 內定義的常數。 + +**Secret schema**:tenant 存 JSON,欄位由 recipe 的 `required_secrets` 宣告。 + +**範例(Notion)**: +```yaml +# arcrun-recipes KV: recipe/notion +service: notion +version: 1 +primitive: static_key +base_url: https://api.notion.com/v1 +required_secrets: + - key: token + label: "Internal Integration Token" + help_url: https://www.notion.so/my-integrations +inject: + header: + Authorization: "Bearer {{secret.token}}" + Notion-Version: "2022-06-28" +test: + method: GET + path: /users/me + expect_status: 200 +``` + +**Secret 範例**: +```json +// tenant KV: secret/tenant_123/notion-prod +{ + "token": "secret_abc123..." +} +``` + +--- + +### 3.2 `oauth2` + +**適用**:需要人類首次授權、之後用 refresh token 續命的場景。 + +**Grant types 支援**: +- `authorization_code`(最常見:GitHub、Slack、Google 用戶授權) +- `client_credentials`(機器對機器) +- `pkce`(SPA、行動應用) +- ❌ 不支援:password grant(2026 已被多數 OAuth 提供者棄用)、implicit(已棄用) + +**Recipe 欄位**: +```yaml +primitive: oauth2 +grant: authorization_code # or client_credentials, pkce +base_url: +oauth: + authorize_url: + token_url: + scopes: + - + - + client_auth: header # or body + # 是否使用 refresh token + refresh: true + # PKCE 時額外參數 + pkce_method: S256 # only for grant: pkce +required_secrets: + - key: client_id + label: "Client ID" + - key: client_secret + label: "Client Secret" + secret: true +inject: + header: + Authorization: "Bearer {{runtime.access_token}}" +``` + +**Runtime 欄位**(primitive 自動維護,存在 tenant KV 的 `oauth_state/{secret_id}` key): +- `access_token` +- `refresh_token` +- `expires_at` + +**首次授權流程**(人類要做的部分): +1. arcrun UI 呼叫 `AuthBroker.startAuth(recipe_id, tenant_id)` 回傳 authorize URL。 +2. 使用者瀏覽器跳轉到 IdP,同意授權。 +3. IdP redirect 回 arcrun callback endpoint(固定一個 URL,無論哪個服務)。 +4. `AuthBroker` 用 authorization code 換 token,寫入 tenant KV。 + +**之後 agent 呼叫時完全自動**:primitive 檢查 `expires_at`,過期自動用 refresh token 續,失敗再觸發重新授權通知。 + +--- + +### 3.3 `service_account` + +**適用**:Google Service Account、AWS IAM Role(assume role)、任何需要「私鑰簽 JWT 換短期 token」的機器身份。 + +**這個就是讓你 debug 兩天那個爆炸點。** 我們用 primitive 把地雷全部包起來。 + +**Recipe 欄位**: +```yaml +primitive: service_account +kind: google_jwt # or aws_sigv4, generic_jwt +base_url: +token_exchange: + # Google 的 JWT → OAuth access token 流程 + endpoint: https://oauth2.googleapis.com/token + audience: https://oauth2.googleapis.com/token + scopes: + - https://www.googleapis.com/auth/calendar + # JWT claims + issuer_from_secret: client_email + subject_from_secret: client_email # optional, for domain-wide delegation 改成其他 user + ttl_seconds: 3600 +required_secrets: + - key: service_account_json + label: "Service Account JSON" + type: json_blob # 特別型別,UI 可以接受貼整個 JSON + help: "到 GCP Console → IAM → Service Accounts → Keys → Add Key (JSON) 下載整份 JSON 貼上" +inject: + header: + Authorization: "Bearer {{runtime.access_token}}" +``` + +**為什麼不是每個服務一個 recipe?** +- Google Calendar、Gmail、Drive、Sheets 全部可以共用同一個 `service_account` primitive。 +- 差別只在 `scopes` 和 `base_url`。 +- Recipe 本身可以 import 共通片段(見 §5 recipe 繼承)。 + +**AWS SigV4(kind: aws_sigv4)**:這是特例,不是 JWT-based,但概念一樣——用 access_key_id + secret_access_key 在每次 request 上簽章。Primitive 內建處理,recipe 只要宣告 region 和 service name。 + +--- + +### 3.4 `mtls` + +**適用**:mTLS / client certificate。銀行 API、企業內部服務、醫療系統。 + +**Recipe 欄位**: +```yaml +primitive: mtls +base_url: +required_secrets: + - key: client_cert + label: "Client Certificate (PEM)" + type: pem_cert + - key: client_key + label: "Client Private Key (PEM)" + type: pem_key + secret: true + - key: ca_cert + label: "CA Certificate (PEM) — optional" + type: pem_cert + optional: true +# mtls 通常不需要額外 inject,憑證在 TLS 層 +inject: {} +``` + +**實作注意**:Cloudflare Workers 有原生 mTLS 支援(`mTLSCertificate` binding),primitive 只需要把 secret 轉成 Cloudflare mTLS binding 即可。 + +--- + +## 4. Recipe YAML Schema(完整版) + +```yaml +# 必填 +service: string # 唯一識別,snake_case,e.g. "notion", "google_calendar" +version: integer # recipe schema version,breaking change 要升版 +primitive: enum # static_key | oauth2 | service_account | mtls +base_url: string # service API base URL + +# primitive 相關(依 primitive 不同) +inject: object # 如何把 secret 注入 HTTP request +oauth: object # 僅 oauth2 primitive +token_exchange: object # 僅 service_account primitive + +# Secret 宣告(讓 UI 知道要問什麼) +required_secrets: + - key: string # secret 欄位名 + label: string # UI 顯示 + secret: boolean # 是否遮蔽顯示(default: true) + type: enum # text | json_blob | pem_cert | pem_key | url + optional: boolean # default: false + help: string # 給使用者的提示 + help_url: string # 導向服務文件 + +# 測試(驗證 credential 是否有效) +test: + method: GET | POST + path: string # 相對 base_url + expect_status: integer + expect_json: object # 選填,JSON path assertion + +# Metadata +display_name: string # UI 顯示名 +description: string +icon_url: string +docs_url: string +tags: + - communication + - crm + - ai +maintainers: + - github: username + +# 可選:共通片段繼承 +extends: string # recipe name,繼承其 schema 後覆寫 +``` + +--- + +## 5. Recipe 繼承(reduce 重複) + +Google 家族的 API 長得很像,重複寫 15 次太蠢。支援 `extends`: + +```yaml +# recipe/_google_base.yaml(底線開頭 = 抽象 recipe,不能直接用) +service: _google_base +version: 1 +primitive: service_account +token_exchange: + endpoint: https://oauth2.googleapis.com/token + audience: https://oauth2.googleapis.com/token + ttl_seconds: 3600 +required_secrets: + - key: service_account_json + type: json_blob +inject: + header: + Authorization: "Bearer {{runtime.access_token}}" +``` + +```yaml +# recipe/google_calendar.yaml +extends: _google_base +service: google_calendar +version: 1 +base_url: https://www.googleapis.com/calendar/v3 +token_exchange: + scopes: + - https://www.googleapis.com/auth/calendar +test: + method: GET + path: /users/me/calendarList + expect_status: 200 +``` + +繼承規則:scalar 覆寫,object 深度合併,array 預設覆寫(可用 `!append` 標記 append)。 + +--- + +## 6. TinyGo WASM Primitive 實作介面 + +### 6.1 統一介面(四個 primitive 都實作這個) + +```go +// primitive/interface.go +package primitive + +type AuthRequest struct { + Method string + URL string + Headers map[string]string + Body []byte +} + +type AuthContext struct { + Recipe Recipe // parsed YAML + Secret map[string]any // decrypted secret + Runtime RuntimeState // oauth token cache 等 + Now int64 // for testing +} + +type Primitive interface { + // 在 HTTP request 上注入認證資訊 + Authenticate(req *AuthRequest, ctx *AuthContext) error + + // 檢查是否需要 refresh(oauth2 / service_account 用) + NeedsRefresh(ctx *AuthContext) bool + + // 執行 refresh / token exchange,回傳新的 RuntimeState + Refresh(ctx *AuthContext) (RuntimeState, error) + + // 驗證 credential 是否有效(執行 recipe.test) + Test(ctx *AuthContext) error +} +``` + +### 6.2 編譯與部署 + +```bash +# 四個 primitive 各自編譯成獨立 WASM +tinygo build -o dist/static_key.wasm -target=wasi ./primitive/static_key +tinygo build -o dist/oauth2.wasm -target=wasi ./primitive/oauth2 +tinygo build -o dist/service_account.wasm -target=wasi ./primitive/service_account +tinygo build -o dist/mtls.wasm -target=wasi ./primitive/mtls + +# 部署時放到 Cloudflare Workers 的 Assets 或直接內嵌 +``` + +### 6.3 Runtime 載入(Worker 端) + +```typescript +// worker/src/auth-broker.ts +import staticKeyWasm from "../dist/static_key.wasm" +import oauth2Wasm from "../dist/oauth2.wasm" +// ... + +const primitives = { + static_key: await instantiate(staticKeyWasm), + oauth2: await instantiate(oauth2Wasm), + service_account: await instantiate(serviceAccountWasm), + mtls: await instantiate(mtlsWasm), +} +``` + +> **為什麼 WASM 而不是直接 TS?** +> 1. 跨 runtime 可攜性(未來若 arcrun 要跑在 Fly.io、local、或客戶自建環境,同一個 primitive 能用)。 +> 2. 配合 u6u/arcrun 既定的 WASM 架構方向,不破壞統一性。 +> 3. 沙箱化:primitive 只能透過明確的 host function 存取外部世界(網路、KV),降低惡意 recipe 攻擊面。 + +--- + +## 7. AuthBroker API(給 arcrun 其他部分調用) + +```typescript +interface AuthBroker { + // Agent 執行時用的主要 API + bind(serviceId: string, secretRef: string, tenantId: string): Promise + + // 首次授權(僅 oauth2 用) + startAuth(serviceId: string, tenantId: string): Promise<{ authorizeUrl: string, state: string }> + completeAuth(state: string, code: string): Promise<{ secretRef: string }> + + // 測試 credential + test(serviceId: string, secretRef: string, tenantId: string): Promise + + // 管理 + listRecipes(): Promise + getRecipe(serviceId: string): Promise +} + +interface AuthenticatedClient { + fetch(path: string, init?: RequestInit): Promise +} +``` + +**使用範例(agent 端)**: +```typescript +const notion = await authBroker.bind("notion", "notion-prod", ctx.tenantId) +const res = await notion.fetch("/databases/abc/query", { + method: "POST", + body: JSON.stringify({ filter: {...} }) +}) +``` + +**Agent 完全不需要知道是 API Key 還是 OAuth**——`authBroker.bind()` 回傳的 client 已經注入好認證,fetch 路徑用相對 base_url 的路徑即可。 + +--- + +## 8. Storage Layout + +### 8.1 Recipe 儲存(arcrun 平台共享) + +**Cloudflare KV namespace**:`arcrun-recipes` + +``` +key: recipe/{service_id} +value: + +key: recipe-list +value: [{ service_id, display_name, icon_url, tags }, ...] # 加速 UI 列表 +``` + +**更新流程**: +1. Recipe YAML 存在 arcrun 主 repo 的 `recipes/` 目錄下(version control + PR review)。 +2. CI 跑 schema validator,通過後上傳到 KV。 +3. UI 的 recipe 列表 5 分鐘 cache。 + +### 8.2 Secret 儲存(tenant 私有) + +**雙層策略**: +- **短期、低敏感** → tenant KV,用 AES-256-GCM 加密,key 從 Cloudflare Secrets Store 拿。 +- **高敏感(如 service account JSON、private key)** → 直接存 Cloudflare Secrets Store,tenant KV 只存 reference。 + +``` +# tenant KV namespace: arcrun-tenant-{tenant_id} +key: secret/{service_id}/{instance_name} +value: { + "recipe_version": 1, + "storage_mode": "kv_encrypted" | "secrets_store_ref", + "data": | { "ref": "secrets-store-id" }, + "created_at": "...", + "last_verified_at": "..." +} + +# oauth2 runtime state(primitive 自動管理) +key: oauth_state/{service_id}/{instance_name} +value: { + "access_token": "...", # encrypted + "refresh_token": "...", # encrypted + "expires_at": 1234567890 +} +``` + +**secretRef 格式**:`{service_id}/{instance_name}`,例如 `notion/prod`、`google_calendar/workspace-a`。 +一個 tenant 可以同一個服務存多個 instance(多帳號場景)。 + +### 8.3 KBDB 整合(可選,但建議) + +**按照 KBDB 架構,recipe metadata 可以用 Block + Template 表達**(不是 credential 本體,只是 metadata): + +建立一個 `service_recipe` Template: +```json +{ + "name": "service_recipe", + "display_name": "服務 Recipe Metadata", + "schema": { + "fields": [ + {"key": "service_id", "type": "text", "required": true, "description": "服務識別"}, + {"key": "primitive", "type": "text", "required": true, "description": "使用的 primitive"}, + {"key": "version", "type": "number", "required": true, "description": "Recipe 版本"}, + {"key": "display_name", "type": "text", "required": false, "description": "顯示名稱"}, + {"key": "docs_url", "type": "text", "required": false, "description": "文件 URL"}, + {"key": "kv_key", "type": "text", "required": true, "description": "KV 實際存取 key"} + ] + } +} +``` + +Secret **不**進 KBDB(KBDB 不該存敏感資料),只有 metadata 在 KBDB 裡方便搜尋和關聯。 + +--- + +## 9. 首次授權 UI Flow(給人類看的部分) + +這是「學員不知道該選哪個 credential 的痛點」的終結方案。 + +### 9.1 Static Key 的 UI + +``` +┌──────────────────────────────────────────┐ +│ 連接 Notion │ +├──────────────────────────────────────────┤ +│ │ +│ Internal Integration Token │ +│ ┌────────────────────────────────────┐ │ +│ │ secret_••••••••••••• │ │ +│ └────────────────────────────────────┘ │ +│ ↳ 如何取得?→ 開啟 Notion 整合設定頁 │ +│ │ +│ [ 測試連線 ] [ 儲存 ] │ +└──────────────────────────────────────────┘ +``` + +**零選項。** UI 從 recipe 的 `required_secrets` 動態生成。使用者不用選「這是 Header Auth 還是 Query Auth 還是 Custom Auth」——那是 recipe 的事,不是使用者的事。 + +### 9.2 OAuth2 的 UI + +``` +┌──────────────────────────────────────────┐ +│ 連接 GitHub │ +├──────────────────────────────────────────┤ +│ │ +│ [ 🔗 使用 GitHub 帳號登入 ] │ +│ │ +│ 將跳轉到 GitHub,授權後自動返回 │ +│ │ +└──────────────────────────────────────────┘ +``` + +**一個按鈕。** Client ID / Secret 由 arcrun 平台統一管理(OAuth App 註冊在 arcrun 這邊),使用者看不到也不用知道。 + +### 9.3 Service Account 的 UI + +``` +┌──────────────────────────────────────────┐ +│ 連接 Google Calendar │ +├──────────────────────────────────────────┤ +│ │ +│ Service Account JSON │ +│ ┌────────────────────────────────────┐ │ +│ │ 將整份 JSON 貼到這裡 │ │ +│ │ │ │ +│ │ { │ │ +│ │ "type": "service_account", │ │ +│ │ "project_id": "...", │ │ +│ │ ... │ │ +│ │ } │ │ +│ └────────────────────────────────────┘ │ +│ │ +│ 如何取得?→ 展開步驟說明 ▼ │ +│ 1. 打開 GCP Console │ +│ 2. IAM → Service Accounts │ +│ 3. 建立 Service Account │ +│ 4. Keys → Add Key → JSON │ +│ 5. 下載後整份貼到上方 │ +│ │ +│ [ 測試連線 ] [ 儲存 ] │ +└──────────────────────────────────────────┘ +``` + +**貼 JSON + 按鈕**。不用寫任何程式碼,不用 debug 兩天。 + +--- + +## 10. 實作任務分解(CC 的 TODO list) + +### Phase 1:核心骨架(1-2 週) + +- [ ] **T1.1** Recipe YAML schema 定義 + JSON Schema validator(放 `arcrun/schemas/recipe.schema.json`) +- [ ] **T1.2** Recipe loader:從 `recipes/` 目錄讀 YAML → validate → 轉 JSON 存入 KV namespace `arcrun-recipes` +- [ ] **T1.3** TinyGo WASM 專案骨架(`arcrun/primitives/`),四個子目錄,統一 interface +- [ ] **T1.4** Worker runtime 的 WASM loader + host function(網路、KV 讀寫) +- [ ] **T1.5** `AuthBroker` TypeScript 類別骨架 + unit test + +### Phase 2:Static Key(1 週) + +- [ ] **T2.1** `static_key.wasm` 實作(header/query/body/basic_auth 四種注入) +- [ ] **T2.2** 寫三個 recipe:`notion.yaml`, `openai.yaml`, `stripe.yaml` +- [ ] **T2.3** Tenant KV secret 加密寫入 + `AuthBroker.bind()` 整合 +- [ ] **T2.4** `recipe.test` 執行器(驗證 credential 有效性) +- [ ] **T2.5** E2E test:存 secret → bind → fetch Notion API → assert + +### Phase 3:OAuth2(1-2 週) + +- [ ] **T3.1** `oauth2.wasm` 實作(authorization_code + client_credentials + pkce) +- [ ] **T3.2** OAuth callback endpoint(統一 URL,用 state 路由到正確 tenant/recipe) +- [ ] **T3.3** Refresh token 自動續命邏輯(rate-limit 保護:同一 token 不能 1 秒內 refresh 多次) +- [ ] **T3.4** 寫三個 recipe:`github.yaml`, `slack.yaml`, `google_oauth_user.yaml` +- [ ] **T3.5** UI flow:startAuth → 跳轉 → callback → 寫 secret + +### Phase 4:Service Account(1 週) + +- [ ] **T4.1** `service_account.wasm` 實作(google_jwt) +- [ ] **T4.2** Google JWT signing(ES256 / RS256)— **這個 TinyGo 需要注意 crypto 支援** +- [ ] **T4.3** AWS SigV4 簽章實作(kind: aws_sigv4) +- [ ] **T4.4** Recipe 繼承機制(`extends` 支援) +- [ ] **T4.5** 寫 recipes:`_google_base`, `google_calendar`, `google_drive`, `gmail`, `aws_s3` + +### Phase 5:mTLS + 收尾(1 週) + +- [ ] **T5.1** `mtls.wasm` 實作(對接 Cloudflare `mTLSCertificate` binding) +- [ ] **T5.2** Cloudflare Secrets Store 整合(高敏感 secret 用) +- [ ] **T5.3** Recipe marketplace UI(列出可用 recipe,搜尋,一鍵設定) +- [ ] **T5.4** Observability:每次 bind / refresh / test 記錄到 KBDB(metadata,不含 secret) +- [ ] **T5.5** Docs:recipe 撰寫指南(讓社群能貢獻) + +### Phase 6:Recipe 生成器(選配,1 週) + +- [ ] **T6.1** 給 Claude 一份 API doc,自動產 recipe YAML 草稿 + 人類 review 介面 +- [ ] **T6.2** 從 OpenAPI spec 自動推論 recipe +- [ ] **T6.3** 從 n8n credential file 反向轉譯(擷取 400+ 現成整合) + +--- + +## 11. 關鍵技術風險與對策 + +| 風險 | 對策 | +|---|---| +| **TinyGo 的 crypto 支援不完整**(ES256 / RS256 JWT 簽章) | 先用 `crypto/rsa` + `crypto/ecdsa` 確認 TinyGo 版本支援;若不行,fallback 用 Worker runtime 的 `crypto.subtle` 實作這部分,WASM 透過 host function 呼叫 | +| **Recipe 被惡意提交**(如 inject 內含 `https://evil.com` 當 token_url) | Recipe 走 PR review + CI 自動檢查 URL 白名單;社群貢獻的 recipe 預設隔離在 `community/` 目錄,使用者明確選擇才啟用 | +| **OAuth state CSRF** | state 用 `crypto.randomUUID()` + 5 分鐘 TTL,存在 KV,callback 時比對 | +| **Secret 在 Worker log 外洩** | `AuthContext.Secret` 禁止 `toString` / `JSON.stringify`,用 Proxy 攔截;log 層強制 redact | +| **Token refresh 風暴**(100 個並發 request 同時發現過期) | 用 Durable Object 單執行緒化每個 secret 的 refresh,其他 request 等結果 | +| **TinyGo WASM bundle size** | 四個 primitive 分開編譯,最大 500KB/個;lazy load | +| **Recipe 版本升級破壞相容** | `version` 欄位 semver,tenant secret 記錄 `recipe_version`,primitive 內處理遷移 | + +--- + +## 12. 對比 n8n(給內部 review / 行銷用) + +| 維度 | n8n | arcrun | +|---|---|---| +| Credential types 數量 | 400+(一個服務一個) | 4(primitive) + N recipe | +| 新增一個服務 | 寫 TypeScript class + rebuild + npm publish | 寫一份 YAML + PR merge | +| AI agent 使用 | 需要讀 node 文件 + 猜參數 | 讀 recipe YAML 即可 | +| 使用者首次設定 | 從 400+ 選項選一個(常選錯) | 搜尋服務名,只問必要 secret | +| OAuth App 管理 | 使用者自己註冊 OAuth app | arcrun 平台統一管理(使用者只需點「授權」) | +| 社群貢獻成本 | 高(TS + 編譯 + 測試) | 低(YAML + 測試) | + +--- + +## 13. 接下來的決策點(需要 richblack 確認) + +- [ ] **Recipe 版本管理策略**:採用 semver?每個 recipe 獨立版本?還是整個 recipe set 一個版本? +- [ ] **OAuth App 註冊**:arcrun 平台要統一註冊幾個主流服務的 OAuth App(GitHub、Google、Slack、Microsoft)?還是讓 tenant 自己帶 client_id/secret? + - 建議:**雙模式**——平台模式(方便)+ BYO 模式(企業客戶用自己的 OAuth app 有稽核好處) +- [ ] **Recipe registry 的審核流程**:完全開放 PR 還是僅核心團隊維護? + - 建議:`recipes/official/`(核心維護)+ `recipes/community/`(PR 審核後 merge,使用者需明確啟用) +- [ ] **Secret rotation 政策**:要不要內建提醒 / 自動 rotate?(Phase 7+) + +--- + +## 14. 附錄:完整範例 + +### A. 最小可行 recipe(OpenAI) + +```yaml +service: openai +version: 1 +primitive: static_key +display_name: OpenAI +base_url: https://api.openai.com/v1 +required_secrets: + - key: api_key + label: API Key + help_url: https://platform.openai.com/api-keys +inject: + header: + Authorization: "Bearer {{secret.api_key}}" +test: + method: GET + path: /models + expect_status: 200 +tags: [ai] +``` + +### B. OAuth2 recipe(Slack) + +```yaml +service: slack +version: 1 +primitive: oauth2 +display_name: Slack +base_url: https://slack.com/api +grant: authorization_code +oauth: + authorize_url: https://slack.com/oauth/v2/authorize + token_url: https://slack.com/api/oauth.v2.access + scopes: + - chat:write + - channels:read + refresh: true + client_auth: header +inject: + header: + Authorization: "Bearer {{runtime.access_token}}" +test: + method: POST + path: /auth.test + expect_json: + ok: true +tags: [communication] +``` + +### C. Service Account recipe(Google Calendar) + +```yaml +extends: _google_base +service: google_calendar +version: 1 +display_name: Google Calendar +base_url: https://www.googleapis.com/calendar/v3 +token_exchange: + scopes: + - https://www.googleapis.com/auth/calendar +test: + method: GET + path: /users/me/calendarList + expect_status: 200 +tags: [calendar, google] +``` + +--- + +## 15. 給 CC 的行動指引 + +1. **先不要動既有 arcrun 的 credential 相關 code**,保持現狀到 Phase 2 完成再切換。 +2. **Phase 1 + 2 是 MVP**,做完可以接 80% 服務(API key 類)。 +3. **遇到 TinyGo 的技術阻礙(特別是 crypto),立刻回報**,不要自己 workaround 兩天。 +4. 每個 Phase 完成後寫一份 brief report(能跑什麼、不能跑什麼、下一步)。 +5. Recipe 撰寫先做 3-5 個手工範例,**確認 schema 夠用再開始批量生成**。 \ No newline at end of file diff --git a/docs/user_requirements/simplify_mvp/u6u-core-spec-v3.md b/docs/user_requirements/simplify_mvp/u6u-core-spec-v3.md new file mode 100644 index 0000000..78afedc --- /dev/null +++ b/docs/user_requirements/simplify_mvp/u6u-core-spec-v3.md @@ -0,0 +1,521 @@ +# u6u-core 獨立開源 Repo 需求規格 v3 + +## 背景與定位 + +### 為什麼開源 + +u6u-core 是 AI 工作流執行引擎,開源的護城河邏輯如下: + +``` +開源(u6u-core) 閉源(InkStone 付費服務) +──────────────────────── ────────────────────────────── +cypher-executor(執行引擎) KBDB 向量搜尋 +WASM 零件庫(Gmail / GSheets…) KBDB graph 查詢 +credentials Worker Persona SDK / Mini-me +CLI MatchGPT + → 需要訂閱,不需要 YAML / KV +``` + +用戶自架版:YAML + CF KV,完全免費。 +升級版:不需要 YAML,直接用自然語言查 KBDB 圖譜組 workflow,這是差異化。 + +### 目前 matrix repo 狀況 + +``` +matrix/ +├── cypher-executor/ ← 要搬進 u6u-core +├── u6u-core/ +│ ├── builtins/ +│ ├── credentials/ +│ └── registry/ +│ └── components/(21 個 WASM 零件) +└── ...(其他 InkStone 內部服務,不搬) +``` + +--- + +## 任務一:搬移 cypher-executor 進 u6u-core + +### 目標結構 + +``` +u6u-core/(新獨立 repo,開源) +├── README.md +├── cypher-executor/ ← 從 matrix/cypher-executor 搬入 +│ ├── src/ +│ ├── wrangler.toml ← 需要清理(移除 InkStone 內部 bindings) +│ └── ... +├── credentials/ ← 從 matrix/u6u-core/credentials 搬入 +├── builtins/ ← 從 matrix/u6u-core/builtins 搬入 +└── registry/ + └── components/ ← 從 matrix/u6u-core/registry/components 搬入 +``` + +### wrangler.toml 清理(重要) + +現有 `cypher-executor/wrangler.toml` 有大量 InkStone 內部 Service Bindings,開源版要移除: + +**移除(InkStone 專屬,不公開):** +```toml +# 移除這些 services bindings: +KBDB → inkstone-kbdb-api +REGISTRY → inkstone-component-registry +CLINIC_GDRIVE → clinic-gdrive +CLINIC_EXCEL → clinic-excel +CLINIC_ANALYSIS +CLINIC_RENDER +CLINIC_GSHEETS +AICEO → inkstone-aiceo-bot +MINI_ME → inkstone-mini-me +``` + +**保留(用戶自己部署需要的):** +```toml +[[kv_namespaces]] +binding = "EXEC_CONTEXT" # 執行上下文暫存 + +[[kv_namespaces]] +binding = "WEBHOOKS" # workflow YAML 儲存 + +[[r2_buckets]] +binding = "WASM_BUCKET" # WASM 零件二進位 + +[ai] +binding = "AI" # Workers AI(auto-publish 用) +``` + +**新增(開源版用戶需要的):** +```toml +[[kv_namespaces]] +binding = "CREDENTIALS_KV" # credential 加密存儲 +``` + +### 清理後的 component-loader + +現有 component-loader 可能有 InkStone 內部查詢邏輯(KBDB HTTP fetch), +開源版改為:**直接從 WASM_BUCKET R2 讀取 `.wasm` 檔案**,不依賴任何外部服務。 + +--- + +## 任務二:零件完成度審查與補充 + +### 完成度標準 + +每個零件的 `component.contract.yaml` 必須包含: + +```yaml +# 已有(現狀) +canonical_id: "gmail" +input_schema: ... +output_schema: ... +gherkin_tests: ... + +# 需要補充 +credentials_required: # 需要 token 的零件才需要此欄位 + - key: gmail_token # 對應 credentials.yaml 的 key 名稱慣例 + type: google_oauth # token 類型 + description: "Google OAuth access token(gmail.send scope)" + inject_as: access_token # 執行時自動注入到 input 的哪個欄位 + +config_example: | # scaffold 指令產出的範本,帶說明註解 + send_email: # 節點名稱(可自訂) + to: "" # 收件人 Email(必填) + subject: "" # 主旨(必填) + body: "" # 內文(必填) + # access_token 由 credentials.yaml 的 gmail_token 自動注入 +``` + +### 需要 credentials_required 的零件 + +| 零件 | 需要的 token | inject_as | +|------|-------------|-----------| +| gmail | google_oauth | access_token | +| google_sheets | google_oauth | access_token | +| telegram | telegram_bot_token | bot_token | +| line_notify | line_token | token | +| http_request | 不固定(用戶自訂) | 不適用 | + +### 不需要 credentials_required 的零件 + +set, filter, merge, switch, wait, if_control, foreach_control, +try_catch, validate_json, string_ops, number_ops, array_ops, +date_ops, cron, ai_transform_compile, ai_transform_run + +### 審查任務(給 CC) + +對 21 個零件逐一檢查,**只回報,不修改**: + +``` +路徑:u6u-core/registry/components/ + +檢查四項: +1. contract.yaml 存在? +2. 有 credentials_required?(需要 token 的才需要) +3. 有 config_example? +4. main.go required 欄位與 contract input_schema required[] 一致? + +回報格式:表格(✓ / ✗ / N/A)+ 每個零件缺少什麼 +不修改任何檔案。 +``` + +審查完成後,再逐一補充缺少的欄位。 + +--- + +## 任務三:workflow YAML 格式定義 + +### 格式設計原則 + +- `flow:` 用 `>>` 三元組描述資料流,人類直接看懂 +- 關係詞使用有語意的詞,**不使用 PIPE**(PIPE 等於什麼都沒說) +- `config:` 用零件名稱對應參數,欄位從 contract 的 config_example 來 +- credential 全部集中在 `credentials.yaml`,workflow 只寫 `{{creds.KEY}}` + +### 可用關係詞 + +| 關係詞 | 語意 | 使用時機 | +|--------|------|---------| +| `完成後` | 前一個成功後執行 | 最常用的串接 | +| `失敗時` | 前一個失敗後執行 | 錯誤處理 | +| `對每個` | 對陣列每個元素執行 | 迭代 | +| `條件滿足時` | 條件分支 | 判斷 | +| `ON_SUCCESS` | 同「完成後」 | 英文版 | +| `ON_FAIL` | 同「失敗時」 | 英文版 | +| `FOREACH` | 同「對每個」 | 英文版 | +| `IF` | 同「條件滿足時」 | 英文版 | +| `ON_CLICK` | 前端按鈕觸發 | UI 互動 | +| `CALLS_SUBFLOW` | 呼叫子工作流 | 模組化 | + +**禁止使用 PIPE** — 任何串接都應該用有語意的關係詞。 + +### workflow.yaml 範例 + +```yaml +name: newsletter_subscribe +description: 訂閱電子報,發感謝信並記錄到 GSheets + +flow: + - "input >> 完成後 >> send_thanks" + - "input >> 完成後 >> save_to_sheet" + - "send_thanks >> 完成後 >> output" + - "send_thanks >> 失敗時 >> notify_error" + - "save_to_sheet >> 完成後 >> output" + +config: + send_thanks: # componentId: gmail(由 cypher-executor 語意搜尋對應) + to: "{{input.email}}" + subject: "感謝訂閱!" + body: "歡迎加入!" + # access_token 由 credentials.yaml 的 gmail_token 自動注入 + + save_to_sheet: # componentId: google_sheets + action: write + spreadsheet_id: "{{creds.sheet_id}}" + range: "訂閱者!A:B" + values: [["{{input.email}}", "{{input.timestamp}}"]] + # access_token 由 credentials.yaml 的 google_oauth 自動注入 + + notify_error: # componentId: telegram + chat_id: "{{creds.telegram_chat_id}}" + text: "發信失敗:{{input.email}}" + # bot_token 由 credentials.yaml 的 telegram_bot_token 自動注入 +``` + +### credentials.yaml 範例 + +```yaml +# credentials.yaml — 類似 .env,加入 .gitignore,不進 git +# u6u creds push 時逐一加密上傳到 CREDENTIALS_KV + +gmail_token: "ya29.a0AfB_..." +google_oauth: "ya29.a0AfB_..." +sheet_id: "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms" +telegram_bot_token: "123456:ABC-..." +telegram_chat_id: "987654321" +``` + +### 執行時 credential 注入流程 + +``` +u6u run newsletter_subscribe + ↓ +cypher-executor 讀 workflow YAML + ↓ +遇到節點 send_thanks → 查 contract:credentials_required.inject_as = access_token + ↓ +去 CREDENTIALS_KV 讀 gmail_token → 解密 + ↓ +注入到 WASM input:{ to, subject, body, access_token: "ya29..." } + ↓ +WASM 執行,用戶的 config 裡完全不出現 token +``` + +--- + +## 任務四:CLI 開發 + +### 技術選型 + +- **語言**:Node.js(TypeScript) +- **安裝**:`npm i -g u6u` +- **依賴**:`commander`、`js-yaml`、`chalk`、`ora` + +### 指令規格 + +#### `u6u init` + +互動式初始化,產生 `~/.u6u/config.yaml` 和本機 `credentials.yaml`。 + +``` +$ u6u init +? Cloudflare Account ID: abc123 +? KV Namespace ID (WEBHOOKS): xyz789 +? KV Namespace ID (CREDENTIALS_KV): abc456 +? R2 Bucket name (WASM_BUCKET): u6u-wasm +? Cypher Executor Worker URL: https://cypher-executor.xxx.workers.dev +? Credentials Worker URL: https://u6u-credentials.xxx.workers.dev +? Cloudflare API Token: *** + +✓ 設定完成 → ~/.u6u/config.yaml +✓ 建立 credentials.yaml(已加入 .gitignore) +``` + +--- + +#### `u6u creds push [credentials.yaml]` + +讀取 credentials.yaml,逐一加密上傳到 CREDENTIALS_KV。 + +``` +$ u6u creds push +讀取 ./credentials.yaml... + ✓ gmail_token → 已加密上傳 + ✓ google_oauth → 已加密上傳 + ✓ sheet_id → 已上傳 + ✓ telegram_bot_token → 已加密上傳 + ✓ telegram_chat_id → 已上傳 +共上傳 5 個 credentials +``` + +--- + +#### `u6u push ` + +解析 `flow:` 三元組,轉換成 triplets 陣列,上傳到 WEBHOOKS KV。 + +``` +$ u6u push newsletter_subscribe.yaml +✓ 已上傳 newsletter_subscribe → WEBHOOKS KV + Webhook: https://cypher-executor.xxx.workers.dev/webhook/abc123 +``` + +轉換邏輯(CLI 負責): + +``` +flow[] 三元組 + ↓ +POST /cypher/search(取得 ExecutionGraph) + ↓ +連同 config 存入 WEBHOOKS KV +``` + +--- + +#### `u6u run [--input key=value...]` + +觸發執行,顯示結果。 + +``` +$ u6u run newsletter_subscribe --input email=test@example.com +⏳ 執行中... +✓ 完成(2.3s) + +結果: + send_thanks: { success: true, data: { message_id: "xxx" } } + save_to_sheet: { success: true, data: { range: "訂閱者!A2" } } +``` + +錯誤時給出具體修復步驟: + +``` +✗ 執行失敗:節點 send_thanks + 原因: access_token 無效(401 Unauthorized) + + 修復方式: + 1. 更新 credentials.yaml 的 gmail_token + 2. 執行 u6u creds push + 3. 重新執行 u6u run newsletter_subscribe + + 取得 Google OAuth token: + → https://developers.google.com/oauthplayground +``` + +--- + +#### `u6u validate ` + +執行前完整驗證,提前發現問題。 + +``` +$ u6u validate newsletter_subscribe.yaml +✓ YAML 格式正確 +✓ flow 三元組語法正確 +✓ 所有關係詞有效(無 PIPE) +✓ 所有節點名稱在 config 有對應 +✓ 所有零件存在於 WASM_BUCKET +✓ credentials 對應: + gmail_token ✓ 已上傳 + google_oauth ✓ 已上傳 + sheet_id ✓ 已上傳 + telegram_bot_token ✗ 缺少 + +⚠ 缺少 1 個 credential: + telegram_bot_token → 請加入 credentials.yaml 並執行 u6u creds push +``` + +--- + +#### `u6u parts` + +列出可用零件。 + +``` +$ u6u parts +可用零件(21): + + [整合] + • gmail Gmail 發信 + 需要: to, subject, body + credential: gmail_token(google_oauth) + • google_sheets 讀寫 Google 試算表 + 需要: spreadsheet_id, range, action + credential: google_oauth + • telegram Telegram Bot 發訊息 + 需要: chat_id, text + credential: telegram_bot_token + • line_notify LINE Notify + 需要: message + credential: line_token + • http_request 任意 HTTP 請求 + 需要: url + + [控制] + • if_control 條件分支 + • foreach_control 迭代執行 + • try_catch 錯誤處理 + • switch 多路路由 + • wait 等待 N 毫秒 + + [資料] + • set / filter / merge / string_ops / number_ops / array_ops / date_ops + + [AI] + • ai_transform_compile 自然語言 → JS 轉換函式 + • ai_transform_run 執行已編譯的轉換 +``` + +--- + +#### `u6u parts scaffold ` + +從 contract 的 config_example 產出可直接貼入 workflow 的 config 範本。 + +``` +$ u6u parts scaffold gmail + +貼入 workflow.yaml 的 config 區塊: + + send_email: # 節點名稱(可自訂) + to: "" # 收件人 Email(必填) + subject: "" # 主旨(必填) + body: "" # 內文(必填) + # access_token 由 credentials.yaml 的 gmail_token 自動注入 + +貼入 credentials.yaml: + + gmail_token: "" # Google OAuth token + # 取得方式:https://developers.google.com/oauthplayground +``` + +--- + +#### `u6u list` + +列出 WEBHOOKS KV 中所有 workflow。 + +``` +$ u6u list + • newsletter_subscribe (更新: 2026-04-16) + • daily_summary (更新: 2026-04-15) +``` + +--- + +#### `u6u logs ` + +查看最近執行記錄。 + +``` +$ u6u logs newsletter_subscribe + 2026-04-16 14:30 ✓ 成功 2.1s + 2026-04-16 09:00 ✗ 失敗 send_thanks: 401 Unauthorized + 2026-04-15 09:00 ✓ 成功 1.8s +``` + +--- + +## 開發順序 + +### Phase 1:搬移與清理(先做) + +``` +1. 建立新的獨立 repo:u6u-core +2. 從 matrix 搬入: + - cypher-executor/ + - u6u-core/credentials/ + - u6u-core/builtins/ + - u6u-core/registry/ +3. 清理 cypher-executor/wrangler.toml(移除 InkStone 內部 bindings) +4. 確認 component-loader 只依賴 WASM_BUCKET,不依賴 KBDB / REGISTRY +5. 本機部署測試 +``` + +### Phase 2:零件完成度(搬移後) + +``` +6. 審查 21 個零件的 contract.yaml +7. 補充 credentials_required(gmail, google_sheets, telegram, line_notify) +8. 補充 config_example(全部 21 個) +9. 驗證 main.go required 欄位與 contract 一致 +``` + +### Phase 3:CLI(完成度補充後) + +``` +10. u6u init +11. u6u creds push +12. u6u push +13. u6u run(含 credential 自動注入) +14. u6u parts / u6u parts scaffold +15. u6u validate +16. u6u list / u6u logs +``` + +### Phase 4:開源發布 + +``` +17. 撰寫 README.md(快速開始、零件列表、workflow 語法說明) +18. 撰寫 CONTRIBUTING.md(如何新增零件) +19. 發布到 GitHub +20. npm publish(u6u CLI) +``` + +--- + +## 不在此次範圍 + +- KBDB 整合(未來付費服務) +- 向量搜尋 / graph 查詢 +- 前端管理介面 +- Webhook trigger 設定(用戶自行設定 CF Cron) +- 新增 WASM 零件(現有 21 個先做完整,之後再擴充) diff --git a/docs/user_requirements/u6u-long-term/SourceDocs/u6u 智慧前端與工匠開發藍圖.md b/docs/user_requirements/u6u-long-term/SourceDocs/u6u 智慧前端與工匠開發藍圖.md new file mode 100644 index 0000000..826f97a --- /dev/null +++ b/docs/user_requirements/u6u-long-term/SourceDocs/u6u 智慧前端與工匠開發藍圖.md @@ -0,0 +1,89 @@ +# **u6u 智慧前端與工匠 AI 開發藍圖 (v4.0)** + +## **一、 核心設計理念:意圖導向的雙面畫布** + +u6u 的前端不是傳統的平面繪圖板,而是一個類似 Android Studio 或 Figma 的\*\*「結構化標籤編輯器」**。 畫布上的每一個元件(Web Component),本質上都是一個**「意圖發射器 (Intent Emitter)」\*\*。前端只負責「長得好看」與「收集人類動作」,後端全權負責「業務邏輯」。 + +### **人機協作的「雙向同步」** + +* **AI 詠唱修改:** 人類說「把按鈕改醒目一點」,CEO AI 在背景將 \ 修改為 \,畫面瞬間更新。 +* **人類手動覆寫:** 人類覺得 AI 調的紅色太暗,直接在右側屬性面板 (Properties Panel) 點選色碼器,底層 HTML 屬性隨之改變。**AI 也能「看見」這個改變,從中學習人類的審美偏好。** + +## **二、 畫布介面設計與運作機制** + +### **1\. 正反面翻轉機制 (The "Flip" Interface)** + +每個 UI 零件在畫布上都有「一體兩面」: + +* **正面 (UI 視圖):** 顯示 HTML 渲染的視覺結果(按鈕、溫度計、圖表)。人類可以在此調整 CSS 屬性、對齊方式與主題顏色。 +* **反面 (邏輯視圖):** 點擊「翻面」按鈕後,會進入底層的工作流設定。這裡使用 u6u 的自定義 Cypher 視覺化語法(例如 \>\> 符號)。 + * *範例:* \[ UI\_Button: "緊急停機" \] \>\> (Intent: emergency\_stop) \>\> \[ WASM: gsheets\_create \] + +### **2\. 智慧容器與區域感知 (Smart Zone Awareness)** + +為了消滅傳統 iPaaS(如 n8n)最痛苦的「手動變數綁定」,u6u 畫布具備「區域感知」能力。 + +* **底層邏輯(獨立元件):** 畫布上的 TextInput 與 Button 都是各自獨立的原子元件。 +* **麻瓜體驗(智慧表單):** 當使用者將這兩個元件拖入同一個排版容器(例如 \)時,系統會自動建立上下文關聯。當按下按鈕並觸發 Webhook 時,按鈕會**自動打包同容器內所有輸入框的值**一併送出: + { + "intent": "query\_attendance", + "payload": { "employee\_id": "A1234" } // 自動從旁邊的 TextInput 抓取 + } + + 使用者完全不需要理解「表單傳值」或「變數綁定」,拖拉組合即生效。 + +### **3\. 多重事件插槽與靜態屬性 (Multi-Event Slots)** + +一個前端元件可以具備多種觸發行為,系統透過介面將「視覺」與「後端邏輯」徹底分流: + +* **靜態視覺註釋:** 例如 mouseover 顯示提示。使用者只需在屬性面板輸入 Tooltip 文字,底層僅修改 HTML 屬性 \,不消耗任何伺服器資源或 Webhook。 +* **動態意圖綁定:** 在「反面」邏輯視圖中,使用者可以針對不同事件綁定不同的工作流: + * ⚡ When: 點擊 (onClick) ➡️ \[ 綁定至 Webhook A:送出查詢 \] + * ⚡ When: 獲得焦點 (onFocus) ➡️ \[ 綁定至 Webhook B:載入歷史紀錄 \] + +### **4\. 智慧上下文替換 (Smart Contextual Substitution)** + +當主管在畫布上對著一個已連接 Webhook 的「按鈕」點擊右鍵選擇「替換元件」時: + +* 系統讀取反面的 Cypher 連線,發現需要發射一個 trigger 意圖。 +* 系統過濾 KBDB 零件宇宙,**只顯示相容的 UI 零件**(如下拉選單、開關)。不具備 trigger 能力的元件(如純文字標籤)會被自動隱藏,確保替換後系統絕對不會報錯。 + +## **三、 原子化組裝與極致解耦:CEO AI 與工匠 AI 的分工** + +當企業主管提出需求:「我需要一個『輸入工號即可查詢員工打卡紀錄』的工具」時,這在 u6u 中**並不是一個單一零件**,而是一個由多個「原子零件」構成的**工作流 (Workflow)**。 + +### **1\. CEO AI 的動態組裝 (Macro Assembly)** + +面對需求,大腦 AI (CEO AI) 會快速從 KBDB 挑選現成積木進行組合: + +* **前端 Prototype 組合:** \ \+ \ \+ \。 +* **後端 Pseudo Code 組合:** webhook\_receiver \>\> check\_kbdb(template\_name, value)。 + AI 會自動用 Cypher 將前端的表單意圖連線到後端工作流。對不懂程式的主管來說,前端就是 Prototype,翻面的 Cypher 就是 Pseudo Code,整套系統瞬間組合完畢。 + +### **2\. 工匠 AI (Forge AI) 的原子生產線** + +只有當現有零件庫缺乏特定原子時,機甲才會喚醒工匠 AI 進行開發。 + +**解耦哲學:** 後端零件開發時,根本不需要管前端零件長什麼樣子(是按鈕還是輸入框)。只要前端送來的 JSON 它能吃,就是合法的候選零件。 + +* **Step 1: 規格定義 (Interface Contract)** + 工匠 AI 只在乎接收與回傳的 JSON 格式。 +* **Step 2: 打造純粹的邏輯黑箱 (後端 TinyGo WASM)** + 工匠 AI 撰寫 Go 程式,編譯成 .wasm。絕對純粹的後端邏輯,沒有任何介面程式碼。 +* **Step 3: 獨立的前端零件生產 (若需要)** + 獨立生成 Web Component(如 \),只負責接收特定 JSON 來渲染畫面。 +* **Step 4: 註冊與編目 (Cataloging into KBDB)** + 新積木註冊到圖資料庫,未來的 CEO AI 即可將其與任何既有的前端或後端元件進行無限的叉積組合。 + +## **四、 架構總結與終極產品體驗** + +這套前端架構讓 u6u 成為一個\*\*「表裡如一」**的系統,成功創造了**「麻瓜的 ERP 幻覺」\*\*: + +使用者不需要知道什麼是「前後端分離」、什麼是「API 串接」。他們只是覺得: + +1. 拖拉了一個溫度計。 +2. 翻面把「數值更新」連線到「機台感測器」。 +3. 拖拉了一個紅按鈕放在旁邊。 +4. 翻面把「點擊」連線到「發送 Line 警報」。 + +在十分鐘的「繪圖」過程中,沒有寫一行程式碼,也沒有設定任何變數。但透過**前端 Web Components** 的視覺封裝、**智慧容器**的自動資料打包,以及**後端 TinyGo WASM \+ Cypher** 的無縫承接,他們在不知不覺中,就搭建出了一套具備微服務架構、高擴展性、且可部署至極限邊緣的企業級系統。 \ No newline at end of file diff --git a/docs/user_requirements/u6u-long-term/SourceDocs/u6u 系統與零件宇宙全景規劃白皮書.md b/docs/user_requirements/u6u-long-term/SourceDocs/u6u 系統與零件宇宙全景規劃白皮書.md new file mode 100644 index 0000000..087ba71 --- /dev/null +++ b/docs/user_requirements/u6u-long-term/SourceDocs/u6u 系統與零件宇宙全景規劃白皮書.md @@ -0,0 +1,136 @@ +# **u6u 系統與零件宇宙全景規劃白皮書 (The u6u Ecosystem Blueprint)** + +## **1\. 核心理念與願景** + +u6u 旨在解決傳統 Workflow 軟體 (如 n8n) 存在的「單線程、沈重、複雜、難以組成系統」的痛點。 + +透過結合 Cloudflare Workers (輕量邊緣運算) 與 Cypher (圖形資料庫關係),u6u 提供一個由 AI 驅動的「意圖到系統」生成平台。所有的系統功能皆被拆解為可複用、可組合的「零件 (Components)」,並在一個會自然淘汰、自我修復的「零件宇宙 (Component Universe)」中演化。 + +## **2\. 四層架構拆解 (Four-Tier Architecture)** + +u6u 的工作模式採取由上到下 (Top-Down) 的 Break-down 機制: + +1. **Polaris (北極星層 / 意圖層):** + * 用戶以自然語言描述商業模式與想法(例如:「我要做一個 AI 客服表單系統」)。 + * 這是整個系統的起點,AI 會根據 Polaris 將意圖拆解為 Prototype。 +2. **Prototype (原型 / 前端層):** + * 定義前端的版型、頁面描述、UI 元件以及它們的屬性。 + * 作為使用者互動的入口,透過觸發事件 (Triggers) 連接到後端 Workflow。 +3. **Workflow (工作流層):** + * 系統的 Orchestrator (編排者),定義業務邏輯的走向。 + * 透過 Cypher 語法與三元組,定義每個節點 (Component) 的執行順序與條件分支。 +4. **Component (零件層 / 節點):** + * 最底層的執行單元,主要分為兩類: + * **功能型 (Logic):** 迴圈、條件判斷、資料轉換、統計等 (透過 CF Workers 執行 JS 邏輯)。 + * **介接型 (API):** 呼叫外部服務 (Webhook, HTTP Request)。 + +## **3\. 統一描述語言:擴展三元組與跨層級 YAML** + +為了解決跨 YAML 檔案串接的問題,u6u 採用易於人類閱讀與 AI 生成的 **「A \>\> 關係 \>\> B」** 三元組語法,結合自定義的 URI 協議 (workflow://, component://, ui://),實現跨層級的連結。 + +### **綜合 YAML 範例與三元組串接** + +*\# 1\. Prototype YAML (描述前端)* + +kind: Prototype + +id: ui\_dashboard + +triplets: + + *\# 結構與版型零件* + + \- "ui\_dashboard \>\> CONTAINS \>\> layout\_admin" + + \- "layout\_admin \>\> CONTAINS \>\> btn\_submit" + + *\# UI 零件與屬性零件 (CSS/行為)* + + \- "btn\_submit \>\> IS\_A \>\> ui://components/Button" + + \- "btn\_submit \>\> HAS\_STYLE \>\> style://tokens/GlowEffect" + + \- "btn\_submit \>\> HAS\_BEHAVIOR \>\> anim://motions/Pulse" + + *\# 跨層級串接:前端觸發 Workflow* + + \- "btn\_submit \>\> ON\_CLICK \>\> workflow://workflows/process\_data.yaml" + +*\# 2\. Workflow YAML (描述工作流編排)* + +kind: Workflow + +id: wf\_process\_data + +triplets: + + *\# 跨層級串接:Workflow 呼叫 Component* + + \- "START \>\> TRIGGERS \>\> step\_validate" + + \- "step\_validate \>\> IS\_A \>\> component://components/validate\_json" + + *\# Workflow 節點間的流轉 (轉譯為 Cypher 關係)* + + \- "step\_validate \>\> ON\_SUCCESS \>\> step\_call\_api" + + \- "step\_validate \>\> ON\_FAIL \>\> step\_notify\_error" + + *\# 跨 Workflow 串接* + + \- "step\_call\_api \>\> CALLS\_SUBFLOW \>\> workflow://workflows/save\_to\_db.yaml" + +## **4\. 零件宇宙 (Component Universe) 的審核與淘汰機制** + +在 u6u 中,所有的 UI、Style、Logic、API 都是「零件」。當 AI 發現缺乏所需零件時,會自動創造它。為了確保生態系的健康,必須建立嚴格的**審核標準**與**自然淘汰機制**。 + +### **4.1 零件的創建與審核標準 (Pass/Fail Criteria)** + +當 AI 或開發者提交一個新零件時,系統會啟動自動化沙盒測試。必須完全通過以下標準,零件才能進入「宇宙」供他人使用: + +1. **功能型零件 (Logic Components):** + * **Gherkin BDD 驗收:** 必須附帶 Feature/Scenario 測試規格,且執行結果 100% 通過 (例如:Given input JSON, When split, Then returns Array)。 + * **效能門檻:** 邊緣運算 (CF Workers) 執行時間需低於設定閾值 (例如 \< 50ms),無記憶體洩漏。 +2. **介接型零件 (API Components):** + * **連線驗證:** 端點 (Endpoint) 必須能 ping 通,或回傳正確的 2xx HTTP Status (提供 Mock Payload 測試)。 + * **Credential 安全:** 不可將 Token 或 Secret 寫死在代碼中,必須嚴格宣告所需的 Environment Variables 規格。 +3. **前端與屬性零件 (UI & Style Components):** + * **渲染驗證:** CSS / 組件代碼不能導致瀏覽器 Crash。 + * **相容性檢查:** 不可包含嚴格衝突的樣式 (例如寫死 \!important 破壞全域版型)。 + +### **4.2 零件宇宙的自然淘汰 (Natural Selection)** + +零件一旦上架,將面臨殘酷的達爾文機制: + +* **AI 偏好權重:** AI (透過 MCP 搜尋時) 會優先選擇「成功率高、執行速度快、被調用次數多」的零件。 +* **降級與墓地:** 連續 30 天無人/無 AI 使用,或錯誤率飆升的零件,會被降級 (Deprecated)。最終轉入「零件墓地」,從首選搜尋清單中剔除。 + +## **5\. 系統自癒與 AI 避坑機制 (Auto-Healing & Pitfall Avoidance)** + +這是 u6u 維持系統穩定運作的最核心機制。工作流不只要能跑,跑完後還必須經歷 **「強制 AI 評價 (Mandatory AI Evaluation)」**。 + +### **5.1 運行後的強制評價迴圈** + +每當一個 Workflow 在 CF Workers 上執行完畢 (或發生異常中斷),系統攔截日誌並強制啟動 AI 評價代理 (Evaluator Agent)。 + +* **評估維度:** + * **狀態:** 成功 / 失敗 (Crash) / 逾時 (Timeout)。 + * **效能:** 耗時是否合理 (例如 API 突然變得很慢)。 + * **警告訊息:** 資源消耗過大、API 回傳即將停用的 Warning。 + +### **5.2 自癒與避坑流程 (The Feedback Loop)** + +當 Evaluator Agent 發現問題時,會觸發以下流程: + +1. **回報與通知 (Notify):** 系統自動生成修復 Ticket,並通知當初建立該零件/工作流的製作人 (或系統管理員)。 +2. **AI 嘗試修復 (Auto-Fix):** 系統派遣「修復型 AI」嘗試讀取錯誤日誌並修復代碼 (例如:API 規格變更導致 JSON 解析錯誤,AI 自動修改解析邏輯)。 +3. **驗收與部署:** 修復後的代碼若通過 Gherkin 驗收,則無縫熱更新。 +4. **避坑標記 (Pitfall Marking):** \- 如果 AI 無法修復 (例如:外部第三方 API 永久倒閉,或邏輯存在根本性死結)。 + * 系統會在 Cypher 圖形資料庫中,將該零件或該特定的三元組關係標記為 \[HAS\_PITFALL\]。 + * **結果:** 下一個生成系統的 AI 在透過 MCP 搜尋時,會讀取到這個坑的紀錄,並**強制繞道**,改用其他方案或生成新的零件,實現「前人踩坑,後 AI 避坑」的群體智慧。 + +## **6\. 結論** + +u6u 不是一個單純的開發工具,它是一個**生物體積木系統**。 + +透過「三元組」統一語言打破系統壁壘,透過「零件審核」保證基因優良,再透過「強制評價與避坑機制」實現演化。當這套系統運轉起來,AI 就能在其中無止盡地為人類組裝出越來越強大、越來越穩定的商業應用。 \ No newline at end of file diff --git a/docs/user_requirements/u6u-long-term/SourceDocs/u6u 自動演化 ERP 架構藍圖.md b/docs/user_requirements/u6u-long-term/SourceDocs/u6u 自動演化 ERP 架構藍圖.md new file mode 100644 index 0000000..5904067 --- /dev/null +++ b/docs/user_requirements/u6u-long-term/SourceDocs/u6u 自動演化 ERP 架構藍圖.md @@ -0,0 +1,99 @@ +# **u6u 自動演化 ERP:全端統一架構規格書 (v3.0)** + +## **1\. 架構核心思想 (The Core Philosophy)** + +u6u ERP 是一套具備自我修復與功能擴充能力的「有機體」系統。 + +為確保系統在跨國雲端、機密地端與斷網邊緣皆能無縫運作,系統採用\*\*「向下相容的絕對標準化」\*\*:由最嚴苛的無人機環境來定義全域零件標準。 + +系統運作依賴三位一體的語言與載體: + +1. **大腦戰略層 (Markdown / Gherkin):** CEO AI 負責閱讀與撰寫,定義全域戰略、系統設計文件 (SDD) 與商業演算法則 (如 ROI 門檻)。 +2. **神經編排層 (Cypher):** u6u 引擎的核心。AI 透過撰寫 Cypher 語法來進行業務邏輯的動態編排、狀態流轉與意圖攔截。 +3. **肌肉執行層 (TinyGo WASM):** 系統中**唯一合法**的零件規格。負責所有具體的 I/O、資料轉換與運算,保證極小體積與極速冷啟動。 + +## **2\. 實戰演練:離岸風機巡檢的黑天鵝事件 (三層架構實踐)** + +為了具體理解這套系統如何運作,我們以一次「離岸風電場巡檢」的突發事件為例,展示雲、地、邊三層架構的完美協同。 + +### **第一階段:戰略下達與沙盤推演 (Tier 1 ➡️ Tier 2 ➡️ Tier 3\)** + +跨國能源集團的**雲端總部 (Tier 1\)** 收到年度檢修排程。雲端的 **CEO AI** 讀取了全局的 Markdown 戰略文件,向遠在海岸線的**地端指揮中心 (Tier 2\)** 下達指令。 + +地端指揮中心(配備強大伺服器與 workerd 叢集)的**部門主管 AI** 將任務拆解給 50 台即將出海的無人機。無人機 07 號 **(Tier 3\)** 的小腦 AI 透過本地的 Cypher 引擎進行沙盤推演,從地端資料庫下載了 rgb\_vision.wasm (光學影像)、lidar\_scan.wasm (光達) 等 60 個可能會用到的 TinyGo 零件,存入本地記憶體後隨船出航。 + +### **第二階段:邊緣的極限生存 (Tier 3 獨立運作)** + +無人機 07 號來到海上 50 公里處,完全失去對外網路。突然,海上濃霧降臨。 + +原本執行中的 Cypher 圖譜卡住了,因為 rgb\_vision.wasm 回報「無法獲取清晰影像」。07 號沒有驚慌,它內建的輕量級 Go \+ Wazero 引擎在 0.1 秒內動態重組了圖譜邏輯:剔除光學零件,瞬間載入並執行 lidar\_scan.wasm,不需人類介入,繼續在濃霧中精準貼行。 + +### **第三階段:游擊網與地端代工 (Tier 3 ↔️ Tier 2\)** + +巡檢中途,07 號發現風機葉片上有極罕見的「蜂巢狀熱應力微裂紋」,但它帶出來的 60 個零件中沒有對應的分析工具。 + +07 號飛昇至濃霧上方,短暫連上母船的微弱區域網路發起「短點射傳輸 (Burst)」:{"intent": "計算蜂巢狀熱應力微裂紋擴散率"},拿到任務單號後立刻斷網潛回霧中。 + +海岸線的**地端指揮中心 (Tier 2\)** 收到需求。強大的**工匠 AI** 瞬間啟動,生成了一段 TinyGo 程式碼,並在本地編譯與測試。三分鐘後,07 號再次探頭連網,下載了熱騰騰的 honeycomb\_analyzer.wasm,並將其編織進 Cypher 圖譜中完成測量。 + +### **第四階段:CEO AI 的全局戰略覆寫 (Tier 2 ➡️ Tier 1\)** + +同時,地端指揮中心匯整了無人機傳回的陣風數據,同步給**雲端總部 (Tier 1\)**。雲端的 CEO AI 呼叫 roi\_calculator.wasm 進行試算,發現風暴將造成設備重大損壞(ROI 極低)。 + +CEO AI 立刻修改總部的 Markdown 戰略文件,新增一條 BDD 規則:「風速大於 22m/s,立刻轉為陣列抗風模式」。新的最高指導 Cypher 範本瞬間下發至地端,再廣播給所有無人機。07 號收到新命令,掛起原任務,與機群組成抗風陣型,安全度過危機。 + +## **3\. 物理拓撲與技術棧 (The 3-Tier Tech Stack)** + +透過 **KBDB Adapter** 抽象層,AI 在任何環境中呼叫的 API 介面皆一致,但底層基礎設施依據物理環境的豐饒度進行適配。 + +### **Tier 1: 雲端總部 (Cloud \- The Global Brain)** + +* **場景:** 跨國集團資料整合、全域戰略備份、對外公開 API、跨國部門協調。 +* **AI 角色:** **CEO AI (大型語言模型)**。負責解析 Markdown、跨區資源調度、修改全域演算法參數。 +* **技術規格:** + * **調度引擎:** Cloudflare Workers (原生執行 TinyGo WASM)。 + * **圖資料庫 (狀態/關聯):** Cloudflare D1 \+ u6u Cypher 轉換層。 + * **零件與儲存:** Cloudflare R2 / KV。 + * **向量檢索 (意圖/型錄):** Cloudflare Vectorize。 +* **架構優勢:** 無限橫向擴展 (Serverless),無須維運硬體,扛載全球級別的 API 併發。 + +### **Tier 2: 企業地端/基地台 (On-Premise \- The Basecamp & Forge)** + +* **場景:** 高機密廠房內網、財務核心系統、無人機/機器人的母艦基地。 +* **AI 角色:** **部門主管 AI** (廠區派工);**工匠 AI** (專職接收規格,透過 TDD 閉環動態生成 TinyGo 程式碼)。 +* **技術規格 (企業級高可用架構):** + * **負載平衡:** Nginx 或 HAProxy (負責將請求分發給後端叢集)。 + * **調度引擎:** **workerd 叢集 (Cloudflare 開源執行環境)**。在本地實體伺服器或 VM 上平行部署多個 workerd 行程,完美相容雲端環境,提供極高的並發處理能力 (V8 JIT 極限算力)。 + * **圖資料庫 (狀態/關聯):** **Kùzu** (單機極速圖庫) 或 PostgreSQL \+ AGE (超高併發)。 + * **零件與儲存:** 企業本地 NVMe 硬碟叢集 / MinIO (S3 相容)。 + * **向量檢索 (意圖/型錄):** pgvector 或 Milvus。 +* **架構優勢:** 兼具資料不出網的「絕對資安」與雲端級別的「叢集擴展性」。內建「代工坊 (Forge)」,是推動企業系統自動演化的核心引擎。 + +### **Tier 3: 邊緣載具 (Extreme Edge \- The Operatives)** + +* **場景:** 無網環境的巡檢無人機、工廠無軌導引車 (AGV)、機械手臂。 +* **AI 角色:** **導航/執行 AI (極小參數 SLM)**。不具備寫程式能力,只負責解讀現場狀況、執行 Cypher 圖譜,並透過 DTN 呼叫地端請求新零件。 +* **技術規格 (極限微縮架構):** + * **調度引擎:** 輕量級 Go 排程引擎 \+ **內嵌 Wazero**。不依賴 V8 或 workerd,確保在極低 RAM 的晶片上流暢運行,實例化延遲僅需數微秒。 + * **圖資料庫 (狀態/關聯):** 嵌入式 Kùzu 或 SQLite。 + * **零件與儲存:** SD 卡 / eMMC 實體檔案系統。 + * **向量檢索 (意圖/型錄):** sqlite-vss (極輕量本地向量)。 +* **架構優勢:** 絕對的離線生存能力。只帶必要的 TinyGo WASM 零件出門,無任何編譯環境,體積最小化。 + +## **4\. 自動演化工作流 (The Auto-Evolution Loop)** + +當企業環境發生變化(例如:新增硬體規格、外部 API 變更),u6u 的演化路徑如下: + +1. **遭遇未知 (Anomaly Detection):** + 無人機 (Tier 3\) 或雲端服務 (Tier 1\) 在執行 Cypher 任務時,發現本地 KBDB 向量庫中缺乏對應的工具零件。 +2. **意圖攔截與 ROI 評估 (CEO/Manager AI):** + 機甲 (Harness) 攔截缺失意圖,呼叫 roi\_calculator.wasm 等評估零件。若認定具備開發價值,系統會生成一份標準的 Input/Output JSON Schema。 +3. **地端代工 (The Forge @ Tier 2):** + 規格需求透過網路或 DTN 送達 Tier 2 地端機房的「工匠 AI」。 + 工匠 AI 生成 TinyGo 程式碼 \-\> 在沙盒中執行 tinygo build \-target=wasi \-\> 通過測試迴圈 \-\> 輸出正式的 .wasm 檔案。 +4. **全域派發 (Distribution & Versioning):** + 新零件註冊進入企業的零件圖資料庫 (KBDB)。 + * **雲端:** 同步至 R2。 + * **邊緣:** 載具下次連網時,透過游擊網 (Burst Transmission) 下載更新檔。 +5. **動態編織 (Execution):** + 各端 AI 獲知新零件上線,瞬間將其編入新的 Cypher 圖譜中執行,完成企業能力的自動擴展。 \ No newline at end of file diff --git a/docs/user_requirements/u6u-long-term/u6u_system_spec.md b/docs/user_requirements/u6u-long-term/u6u_system_spec.md new file mode 100644 index 0000000..965a2a4 --- /dev/null +++ b/docs/user_requirements/u6u-long-term/u6u_system_spec.md @@ -0,0 +1,360 @@ +# u6u 系統規格書 v1.0 +## 給 AI 的架構思考指引 + +> 本文件用途:讓 AI 理解 u6u 的完整設計意圖、現況、與未來路徑, +> 在實作決策時能自行判斷方向正確性,而不只是執行單一任務。 + +--- + +## 一、系統本質(先理解再動手) + +u6u 不是 workflow 工具,不是 no-code 平台,不是 iPaaS。 + +u6u 是一個**「意圖到系統」的生物體積木平台**: + +- 人類說出意圖(自然語言) +- AI 從零件宇宙組裝出可運行的系統 +- 系統會自動評價、演化、淘汰舊零件 +- 累積的零件就是核心資產,越積越有價值 + +**設計的終極體驗:** 工廠主管拖拉十分鐘,組出具備微服務架構的企業系統,零程式碼,但底層是真正的分散式系統。 + +--- + +## 二、四層邏輯架構 + +``` +Polaris(意圖層) + ↓ 自然語言 → AI 拆解 +Prototype(前端層) + ↓ UI 元件 + 觸發事件 +Workflow(編排層) + ↓ Cypher 語法定義執行順序 +Component(零件層) + ↓ .wasm 實際執行 +``` + +每一層向下只透過標準介面溝通,層與層之間完全解耦。 + +--- + +## 三、物理三層部署 + +``` +Tier 1:雲端總部(Cloudflare Workers) + - CEO AI 讀取 Markdown 戰略文件 + - 全域零件同步至 R2 + - Cloudflare D1 + Vectorize(KBDB) + +Tier 2:企業地端(workerd 叢集) + - 部門主管 AI 派工 + - 工匠 AI 生成並測試新零件 + - Kùzu 或 PostgreSQL + AGE(圖資料庫) + - pgvector 或 Milvus(向量搜尋) + +Tier 3:邊緣載具(無人機、AGV、工廠設備) + - 極小參數 SLM + - Go 排程引擎 + 內嵌 Wazero(無 V8) + - SQLite + sqlite-vss + - 離線生存,DTN 短點射傳輸 +``` + +**關鍵約束:** Tier 3 沒有 V8,沒有 Node.js,沒有網路。 +所有零件必須在 Wazero 上跑,所有資料傳輸透過 stdin/stdout JSON。 + +--- + +## 四、零件規格(Component Contract) + +這是整個系統最核心的不變量。零件規格定錯,累積的資產會變成技術債。 + +### 4.1 零件的本質定義 + +**一個零件只做一件事。** + +``` +✅ gsheets_create_table +✅ gsheets_delete_table +✅ gsheets_get_entries +❌ gsheets_manager(做太多事,禁止) +``` + +### 4.2 零件合約格式(component.contract.yaml) + +每個零件必須附帶此合約,這是 AI 讀取零件的唯一介面描述: + +```yaml +id: "gsheets_get_entries" # 功能合約名稱(永久不變) +version: "v2" # 實作版本 +wasi_target: "preview1" # 明確標記 WASI 版本,未來升級用 +stability: "floating" # floating | stable | pinned + +runtime_compat: + - "cf-workers" + - "workerd" + - "wazero" + +constraints: + max_size_kb: 2048 # 超過視為打包了 runtime + max_cold_start_ms: 50 + no_network_syscall: true # 禁止零件自己發 HTTP + no_filesystem_syscall: true # 只能 stdin/stdout + io_model: "stdin_stdout_json" # 唯一合法的 I/O 模型 + +input_schema: + type: object + required: ["spreadsheet_id", "sheet_name"] + properties: + spreadsheet_id: { type: string } + sheet_name: { type: string } + limit: { type: integer, default: 100 } + +output_schema: + type: object + properties: + rows: { type: array } + total: { type: integer } + error: { type: string } + +gherkin_tests: + - scenario: "正常取得資料" + given: '{"spreadsheet_id":"abc","sheet_name":"Sheet1"}' + then_contains: '{"total":1}' + - scenario: "不存在的表格回傳錯誤" + given: '{"spreadsheet_id":"abc","sheet_name":"不存在"}' + then_contains: '{"error":' + +tags: ["google", "sheets", "data", "read"] +description: "從 Google Sheets 取得指定工作表的所有資料列" +``` + +### 4.3 語言無限制原則 + +**零件開發語言完全不限制**,只要輸出符合以上合約的 .wasm 即可。 + +可接受語言(非排他):TinyGo、Rust、AssemblyScript、C/C++ + +注意事項(不是禁止,是要求自行驗證): +- TypeScript via Extism:會打包 QuickJS,體積通常超過 2MB 限制 +- 標準 Go(非 TinyGo):runtime 過肥,通常超過體積限制 +- 任何語言:不可在 .wasm 內部呼叫網路或檔案系統 syscall + +**驗收標準只有一個:通過沙盒測試。** 語言是零件作者自己的事。 + +### 4.4 零件的前後端分類 + +| 類型 | 執行位置 | I/O | 範例 | +|------|----------|-----|------| +| 後端邏輯零件 | Workers/workerd/Wazero | JSON stdin/stdout | validate_json, http_request | +| 前端 UI 零件 | 瀏覽器 | HTML attributes / DOM events | u6u-btn, u6u-chart | +| **混合零件** | **禁止** | — | **強制拆成兩個** | + +--- + +## 五、零件版本控制策略 + +### 5.1 命名規則 + +``` +gsheets_get_entries ← 功能合約名稱(搜尋用,永遠存在) +gsheets_get_entries_v1 ← 第一個實作(慢但能用) +gsheets_get_entries_v2 ← 更快的實作(由另一個 AI/用戶提交) +``` + +### 5.2 穩定性標籤 + +Workflow 引用零件時可指定穩定性需求: + +``` +gsheets_get_entries → 預設 floating,AI 自動選最優版本 +gsheets_get_entries@stable → 有更好版本時提示,人工確認才換 +gsheets_get_entries@pinned:v1 → 版本凍結,宇宙怎麼演化都不影響 +``` + +| 標籤 | 適用情境 | 更新行為 | +|------|----------|----------| +| `floating` | 一般企業應用 | AI 自動換成最優版本 | +| `stable` | 重要業務流程 | 有更好版本時提示,人工確認 | +| `pinned` | 工廠控制器、嵌入式設備 | 永遠不動,即使進入墓地也保留 .wasm | + +### 5.3 淘汰機制 + +- AI 搜尋零件時,KBDB 依「成功率 × 速度 × 被調用次數」排序 +- 連續 30 天無使用且評價下降 → Deprecated +- Deprecated 後繼續 90 天無復活 → 進墓地(從搜尋清單移除) +- **墓地的 .wasm 永遠保留**,pinned 的 Workflow 永遠能拉到 + +--- + +## 六、零件製造指引書(給用戶 AI 的規範) + +u6u 不限制誰來造零件,任何 AI(用戶自己的 Claude、GPT、本地模型)都可以。 +但必須遵守此指引書,否則沙盒測試不過,無法上架。 + +### Step 1:理解介面合約 + +造零件前,先定義合約 YAML。 +**零件只在乎輸入 JSON 和輸出 JSON,完全不管前端長什麼樣子。** + +``` +人類:我要一個可以查 Google Sheets 的零件 +AI 的第一步:定義 input_schema 和 output_schema,不是寫程式 +``` + +### Step 2:選擇開發語言 + +選擇你最熟悉的、能產出 WASI preview1 相容 .wasm 的語言。 +建議: + +- 小型邏輯零件(轉換、計算)→ TinyGo 或 AssemblyScript(體積小) +- 效能敏感零件 → Rust(生態最成熟) +- 任何語言都可以,只要通過合約限制 + +### Step 3:實作規則 + +``` +✅ 只用 stdin 讀取輸入 JSON +✅ 只用 stdout 輸出結果 JSON +✅ 錯誤也用 stdout 輸出:{"error": "說明"},不要 panic/crash +✅ 無狀態:每次呼叫都是獨立的,不依賴上一次執行的結果 +✅ 需要打外部 API?透過 host function 注入,不在 .wasm 裡自己發 HTTP +❌ 禁止網路 syscall +❌ 禁止檔案系統 syscall +❌ 禁止打包 runtime(QuickJS、Node.js 等) +❌ 禁止超過 2MB +``` + +### Step 4:本地測試方式 + +```bash +# 用任何 WASI runtime 本地測試 +echo '{"spreadsheet_id":"abc","sheet_name":"Sheet1"}' | \ + wasmtime gsheets_get_entries.wasm + +# 預期輸出 +{"rows":[...],"total":5} +``` + +### Step 5:提交審核 + +提交 `.wasm` + `component.contract.yaml`,系統自動執行: + +1. 體積檢查(< 2MB) +2. 冷啟動時間(< 50ms) +3. Syscall 掃描(不能有網路/檔案系統呼叫) +4. Gherkin 測試(合約裡的所有 scenario 必須 100% 通過) +5. 多 runtime 相容測試(cf-workers / workerd / wazero) + +全部通過 → 上架進入零件宇宙,開始累積評價。 + +--- + +## 七、Cypher 編排語言 + +Workflow 使用擴展三元組語法描述執行邏輯: + +```yaml +kind: Workflow +id: wf_query_attendance + +triplets: + # 基本流程 + - "START >> TRIGGERS >> step_receive" + - "step_receive >> IS_A >> component://webhook_receiver_v1" + + # 條件分支 + - "step_receive >> ON_SUCCESS >> step_validate" + - "step_receive >> ON_FAIL >> step_notify_error" + + # 跨 Workflow 串接 + - "step_validate >> CALLS_SUBFLOW >> workflow://save_to_db" + + # 前端觸發後端 + - "btn_submit >> ON_CLICK >> workflow://wf_query_attendance" +``` + +**URI 協議規範:** +- `component://` → 引用零件 +- `workflow://` → 引用子 Workflow +- `ui://` → 引用前端零件 +- `style://` → 引用樣式零件 + +--- + +## 八、KBDB 在 u6u 的角色 + +u6u 的所有狀態都在 KBDB 裡: + +| KBDB Block 類型 | 存放內容 | +|-----------------|----------| +| Component Block | 零件合約、.wasm 位置、版本、評價指標 | +| Workflow Block | Cypher 三元組、依賴零件清單 | +| Prototype Block | 前端結構、UI 零件樹 | +| Pitfall Block | 避坑記錄,AI 搜尋時強制讀取 | +| Evaluation Block | 每次 Workflow 執行後的強制評價結果 | + +**KBDB 不變量:永遠只有三張表(blocks/templates/slots),不新增表。** +所有以上類型都用 Template + Slot 實現。 + +--- + +## 九、自動演化迴圈 + +``` +執行 Workflow + ↓ +強制 AI 評價(Evaluator Agent) + ↓ 發現問題 +生成修復 Ticket → 通知製作人 + ↓ AI 嘗試修復 +通過 Gherkin 驗收 → 熱更新 + ↓ 無法修復 +標記 [HAS_PITFALL] 到 Cypher 圖 + ↓ +下一個 AI 搜尋時讀到坑,強制繞道 +``` + +--- + +## 十、現況與未來路徑 + +### 現在已有 + +- KBDB(blocks/templates/slots + Vectorize) +- IS-Squad MCP(execute_cypher 等工具) +- Cloudflare Workers 環境 + +### 最小可 demo 路徑 + +1. **Cypher 執行引擎**:三元組 → 實際執行順序(確認 execute_cypher 邊界) +2. **首批核心零件**(5 個): + - `webhook_receiver` + - `json_transform` + - `http_request`(透過 host function) + - `notify_line` + - `validate_json` +3. **機甲最小版本**:意圖 → 零件搜尋 → 組裝 Workflow(先用硬編碼路由) +4. **前端畫布 MVP**:靜態 HTML 模擬雙面翻轉體驗 + +### 技術監控項目 + +- **WASI Component Model(preview2)**:目前用 preview1,未來 3-5 年會有遷移壓力。 + 合約裡已有 `wasi_target: "preview1"` 標記,升級時知道要改什麼。 +- **Kùzu 成熟度**:地端圖資料庫首選,持續觀察 v1.0 穩定性。 + +--- + +## 十一、實作決策原則(CC 行動準則) + +遇到不確定的實作決策時,依序問自己: + +1. **這個決策會影響零件合約嗎?** 如果是,停下來討論,不要自行決定。 +2. **這個實作是否限制了未來換 runtime 的自由?** 如果是,重新設計介面。 +3. **這個零件做超過一件事嗎?** 如果是,拆成兩個零件。 +4. **這個設計在 Tier 3 離線環境能跑嗎?** 如果不能,重新考慮。 +5. **有沒有現成零件可以組合?** 先搜尋 KBDB,不要重造輪子。 + +--- + +*本文件版本:v1.0* +*綜合自:u6u 系統與零件宇宙全景規劃白皮書、自動演化 ERP 架構藍圖、智慧前端與工匠開發藍圖,加入技術評論與補充建議。* diff --git a/docs/user_requirements/u6u-plan.md b/docs/user_requirements/u6u-plan.md new file mode 100644 index 0000000..8387f6e --- /dev/null +++ b/docs/user_requirements/u6u-plan.md @@ -0,0 +1,116 @@ +# u6u 系統與零件宇宙全景規劃白皮書 (The u6u Ecosystem Blueprint) + +## 1. 核心理念與願景 + +u6u 旨在解決傳統 Workflow 軟體 (如 n8n) 存在的「單線程、沈重、複雜、難以組成系統」的痛點。 +透過結合 Cloudflare Workers (輕量邊緣運算) 與 Cypher (圖形資料庫關係),u6u 提供一個由 AI 驅動的「意圖到系統」生成平台。所有的系統功能皆被拆解為可複用、可組合的「零件 (Components)」,並在一個會自然淘汰、自我修復的「零件宇宙 (Component Universe)」中演化。 + +## 2. 四層架構拆解 (Four-Tier Architecture) + +u6u 的工作模式採取由上到下 (Top-Down) 的 Break-down 機制: + +1. Polaris (北極星層 / 意圖層): + - 用戶以自然語言描述商業模式與想法(例如:「我要做一個 AI 客服表單系統」)。 + - 這是整個系統的起點,AI 會根據 Polaris 將意圖拆解為 Prototype。 +2. Prototype (原型 / 前端層): + - 定義前端的版型、頁面描述、UI 元件以及它們的屬性。 + - 作為使用者互動的入口,透過觸發事件 (Triggers) 連接到後端 Workflow。 +3. Workflow (工作流層): + - 系統的 Orchestrator (編排者),定義業務邏輯的走向。 + - 透過 Cypher 語法與三元組,定義每個節點 (Component) 的執行順序與條件分支。 +4. Component (零件層 / 節點): + - 最底層的執行單元,主要分為兩類: + - 功能型 (Logic): 迴圈、條件判斷、資料轉換、統計等 (透過 CF Workers 執行 JS 邏輯)。 + - 介接型 (API): 呼叫外部服務 (Webhook, HTTP Request)。 + +## 3. 統一描述語言:擴展三元組與跨層級 YAML + +為了解決跨 YAML 檔案串接的問題,u6u 採用易於人類閱讀與 AI 生成的 「A >> 關係 >> B」 三元組語法,結合自定義的 URI 協議 (workflow://, component://, ui://),實現跨層級的連結。 + +綜合 YAML 範例與三元組串接 + +```YAML +# 1. Prototype YAML (描述前端) +kind: Prototype +id: ui_dashboard +triplets: + # 結構與版型零件 + - "ui_dashboard >> CONTAINS >> layout_admin" + - "layout_admin >> CONTAINS >> btn_submit" + # UI 零件與屬性零件 (CSS/行為) + - "btn_submit >> IS_A >> ui://components/Button" + - "btn_submit >> HAS_STYLE >> style://tokens/GlowEffect" + - "btn_submit >> HAS_BEHAVIOR >> anim://motions/Pulse" + # 跨層級串接:前端觸發 Workflow + - "btn_submit >> ON_CLICK >> workflow://workflows/process_data.yaml" + + +# 2. Workflow YAML (描述工作流編排) +kind: Workflow +id: wf_process_data +triplets: + # 跨層級串接:Workflow 呼叫 Component + - "START >> TRIGGERS >> step_validate" + - "step_validate >> IS_A >> component://components/validate_json" + + # Workflow 節點間的流轉 (轉譯為 Cypher 關係) + - "step_validate >> ON_SUCCESS >> step_call_api" + - "step_validate >> ON_FAIL >> step_notify_error" + + # 跨 Workflow 串接 + - "step_call_api >> CALLS_SUBFLOW >> workflow://workflows/save_to_db.yaml" +``` + +## 4. 零件宇宙 (Component Universe) 的審核與淘汰機制 + +在 u6u 中,所有的 UI、Style、Logic、API 都是「零件」。當 AI 發現缺乏所需零件時,會自動創造它。為了確保生態系的健康,必須建立嚴格的審核標準與自然淘汰機制。 + +### 4.1 零件的創建與審核標準 (Pass/Fail Criteria) + +當 AI 或開發者提交一個新零件時,系統會啟動自動化沙盒測試。必須完全通過以下標準,零件才能進入「宇宙」供他人使用: + +1. 功能型零件 (Logic Components): + - Gherkin BDD 驗收: 必須附帶 Feature/Scenario 測試規格,且執行結果 100% 通過 (例如:Given input JSON, When split, Then returns Array)。 + - 效能門檻: 邊緣運算 (CF Workers) 執行時間需低於設定閾值 (例如 < 50ms),無記憶體洩漏。 +2. 介接型零件 (API Components): + - 連線驗證: 端點 (Endpoint) 必須能 ping 通,或回傳正確的 2xx HTTP Status (提供 Mock Payload 測試)。 + - Credential 安全: 不可將 Token 或 Secret 寫死在代碼中,必須嚴格宣告所需的 Environment Variables 規格。 +3. 前端與屬性零件 (UI & Style Components): + - 渲染驗證: CSS / 組件代碼不能導致瀏覽器 Crash。 + - 相容性檢查: 不可包含嚴格衝突的樣式 (例如寫死 !important 破壞全域版型)。 + +## 4.2 零件宇宙的自然淘汰 (Natural Selection) + +零件一旦上架,將面臨殘酷的達爾文機制: + +- AI 偏好權重: AI (透過 MCP 搜尋時) 會優先選擇「成功率高、執行速度快、被調用次數多」的零件。 +- 降級與墓地: 連續 30 天無人/無 AI 使用,或錯誤率飆升的零件,會被降級 (Deprecated)。最終轉入「零件墓地」,從首選搜尋清單中剔除。 + +## 5. 系統自癒與 AI 避坑機制 (Auto-Healing & Pitfall Avoidance) + +這是 u6u 維持系統穩定運作的最核心機制。工作流不只要能跑,跑完後還必須經歷 「強制 AI 評價 (Mandatory AI Evaluation)」。 + +### 5.1 運行後的強制評價迴圈 + +每當一個 Workflow 在 CF Workers 上執行完畢 (或發生異常中斷),系統攔截日誌並強制啟動 AI 評價代理 (Evaluator Agent)。 + +- 評估維度: + - 狀態: 成功 / 失敗 (Crash) / 逾時 (Timeout)。 + - 效能: 耗時是否合理 (例如 API 突然變得很慢)。 + - 警告訊息: 資源消耗過大、API 回傳即將停用的 Warning。 + +### 5.2 自癒與避坑流程 (The Feedback Loop) + +當 Evaluator Agent 發現問題時,會觸發以下流程: + +- 回報與通知 (Notify): 系統自動生成修復 Ticket,並通知當初建立該零件/工作流的製作人 (或系統管理員)。 +- AI 嘗試修復 (Auto-Fix): 系統派遣「修復型 AI」嘗試讀取錯誤日誌並修復代碼 (例如:API 規格變更導致 JSON 解析錯誤,AI 自動修改解析邏輯)。 +- 驗收與部署: 修復後的代碼若通過 Gherkin 驗收,則無縫熱更新。 +- 避坑標記 (Pitfall Marking): - 如果 AI 無法修復 (例如:外部第三方 API 永久倒閉,或邏輯存在根本性死結)。 + - 系統會在 Cypher 圖形資料庫中,將該零件或該特定的三元組關係標記為 [HAS_PITFALL]。 + - 結果: 下一個生成系統的 AI 在透過 MCP 搜尋時,會讀取到這個坑的紀錄,並強制繞道,改用其他方案或生成新的零件,實現「前人踩坑,後 AI 避坑」的群體智慧。 + +## 6. 結論 + +u6u 不是一個單純的開發工具,它是一個生物體積木系統。 +透過「三元組」統一語言打破系統壁壘,透過「零件審核」保證基因優良,再透過「強制評價與避坑機制」實現演化。當這套系統運轉起來,AI 就能在其中無止盡地為人類組裝出越來越強大、越來越穩定的商業應用。 \ No newline at end of file diff --git a/docs/user_requirements/u6u_design.md b/docs/user_requirements/u6u_design.md new file mode 100644 index 0000000..8d43604 --- /dev/null +++ b/docs/user_requirements/u6u_design.md @@ -0,0 +1,19 @@ +# u6u Design + +u6u 是一個 AI Friendly 的 n8n。 + +- 用 workers 天生比 n8n 速度快 +- 用 Cypher binding,不需 deploy 就可以隨時修改執行,不然原生 workers 的 binding 要 deploy +- 未來要有一個 GUI 可以解析 YAML 產生畫面,反之人拉的圖會產生 YAML +- 內建核心元件,http request, webhook, cron, if, switch, set, credential 等功能 +- 用戶自建功能多數是 http request 只是去 call 不同的 API,可以隨時建立,它的「配方」recipe 可以分享 +- 每個 API Call 獨立但搜尋會整合,例如有人實作 call google sheets create table API,它不用做完整的,因為另一人要 delete table 時發現沒有,AI 直接做一個,下次搜尋 google sheets 時,就提供了 create table, delete sheets 兩個端點,也就是哪些是大家需要的功能自然產生 +- marketplace 機制,但是是給 AI 的,強制 AI 使用後要回覆使用的評價,如果一個零件被幾次評為不佳,其他的 AI 就可以避開這個零件 +- 自動審核:如果是 call API,只要成功 Call 通就是通過,如果是功能性的,只要通過他設置的 Gherkin 就是通過,省去人工審核的麻煩 + +## 觀念想法 + +- u6u 通過前端網頁開發功能,每個元件是一個零件 +- 視覺優先的開發:要解釋什麼是 webhook 很難,但一般用戶做一個前端的按鈕、輸入框... 後面就會綁定某個 webhook,點擊這個前端界面就看到後端邏輯的工作流,這樣就不用解釋太多。 +- 系統功能:我是一個用戶,我建立不同的功能,例如我建立 CRM,又建立 ERP,這些系統有很多流程是共用的,但當我建立多個工作流時,zoom out 就會看到我的公司內不同流程間的關係,因為 cypher 放大就是 graph,但每個功能要可以摺疊成一個點,又可以 zoom in 展開來調整某一段工作流,再 zoom in 調整一個零件 +- 考慮讓他自己 OWN,就是企業版可以讓資料是獨立的 \ No newline at end of file diff --git a/docs/user_requirements/wishlist.md b/docs/user_requirements/wishlist.md new file mode 100644 index 0000000..fd31dff --- /dev/null +++ b/docs/user_requirements/wishlist.md @@ -0,0 +1,9 @@ +# Wishlist — 已移至 InkStoneCo 頂層(2026-06-15) + +> wishlist 是**跨專案的**(一個願望常牽動 arcrun + kbdb + gui + 多個 product), +> 不該埋在單一 repo。已移到頂層總計劃: +> +> **真身**:`github.com/uncle6me-web/InkStoneCo` 的 `docs/1-vision/product-wishlist.md` +> +> arcrun 相關的願望(code 零件、標準流程圖、CLI 多語系/logs)在該檔 C 區。 +> 動到 arcrun 願望前讀那邊,不要在此重記(單一真相源)。 diff --git a/landing/.gitignore b/landing/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/landing/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/landing/.npmrc b/landing/.npmrc new file mode 100644 index 0000000..521a9f7 --- /dev/null +++ b/landing/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/landing/AGENTS.md b/landing/AGENTS.md new file mode 100644 index 0000000..8bd0e39 --- /dev/null +++ b/landing/AGENTS.md @@ -0,0 +1,5 @@ + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. + diff --git a/landing/CLAUDE.md b/landing/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/landing/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/landing/README.md b/landing/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/landing/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/landing/app/api-docs/page.tsx b/landing/app/api-docs/page.tsx new file mode 100644 index 0000000..6019c29 --- /dev/null +++ b/landing/app/api-docs/page.tsx @@ -0,0 +1,100 @@ +'use client'; + +import { useEffect, useRef } from 'react'; +import Link from 'next/link'; +import SiteNav from '../components/SiteNav'; + +const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? 'https://cypher.arcrun.dev'; + +export default function ApiDocsPage() { + const containerRef = useRef(null); + const initialized = useRef(false); + + useEffect(() => { + if (initialized.current || !containerRef.current) return; + initialized.current = true; + + // Dynamically load Swagger UI from CDN + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'https://unpkg.com/swagger-ui-dist@5/swagger-ui.css'; + document.head.appendChild(link); + + const script = document.createElement('script'); + script.src = 'https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js'; + script.onload = () => { + const SwaggerUIBundle = (window as unknown as { SwaggerUIBundle: (opts: unknown) => void }).SwaggerUIBundle; + if (!SwaggerUIBundle || !containerRef.current) return; + SwaggerUIBundle({ + url: `${API_BASE}/openapi.json`, + dom_id: '#swagger-ui', + presets: [(window as unknown as { SwaggerUIBundle: { presets: { apis: unknown } } }).SwaggerUIBundle.presets.apis], + layout: 'BaseLayout', + defaultModelsExpandDepth: -1, + docExpansion: 'list', + filter: true, + tryItOutEnabled: true, + supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch'], + requestInterceptor: (request: { headers: Record }) => { + // Inject API key from localStorage if present + const key = localStorage.getItem('arcrun_api_key'); + if (key) request.headers['X-Arcrun-API-Key'] = key; + return request; + }, + }); + }; + document.head.appendChild(script); + + return () => { + // cleanup not strictly needed for page navigation + }; + }, []); + + return ( +
+ + + {/* Header */} +
+

API Reference

+

+ 這是 arcrun 的原始 API。Python / JS lib 是它的包裝,任何能發 HTTP request 的工具都能直接用。 +

+

+ Endpoint: {API_BASE} +

+ + {/* API Key hint */} +
+

+ 若要在此頁面試打 API,請先設定 API Key: +

+ +
+ + {/* Swagger UI */} +
+
+
+
+
+ ); +} + +function ApiKeyInput() { + return ( +
+ { + if (e.target.value.startsWith('ak_')) { + localStorage.setItem('arcrun_api_key', e.target.value); + } + }} + /> + 自動注入到 requests +
+ ); +} diff --git a/landing/app/components/AppLauncher.tsx b/landing/app/components/AppLauncher.tsx new file mode 100644 index 0000000..8f03f77 --- /dev/null +++ b/landing/app/components/AppLauncher.tsx @@ -0,0 +1,90 @@ +'use client'; + +// 九宮格 App Launcher(受 Google Apps menu 啟發) +// 規範:matrix/identity/.agents/specs/identity/design.md §2.5 +// 非白名單 user 看到 mira 等受限 app 顯示為灰色 + tooltip「即將開放」 + +import { useEffect, useRef, useState } from 'react'; +import { MATRIX_APPS, isAppAccessible, type AppEntry } from './apps'; + +export default function AppLauncher({ userEmail }: { userEmail: string | null }) { + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + if (!open) return; + const onClickOutside = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + }; + document.addEventListener('mousedown', onClickOutside); + return () => document.removeEventListener('mousedown', onClickOutside); + }, [open]); + + return ( +
+ + + {open && ( +
+
+ {MATRIX_APPS.map(app => ( + setOpen(false)} /> + ))} +
+
+ )} +
+ ); +} + +function AppTile({ + app, + userEmail, + onClose, +}: { + app: AppEntry; + userEmail: string | null; + onClose: () => void; +}) { + const accessible = isAppAccessible(app, userEmail); + const tooltip = !accessible ? (app.locked_tooltip ?? '即將開放') : (app.description ?? ''); + + if (!accessible) { + return ( +
+ {app.icon ?? '📦'} + {app.name} +
+ ); + } + + return ( + + {app.icon ?? '📦'} + {app.name} + + ); +} diff --git a/landing/app/components/SiteNav.tsx b/landing/app/components/SiteNav.tsx new file mode 100644 index 0000000..f93d3de --- /dev/null +++ b/landing/app/components/SiteNav.tsx @@ -0,0 +1,82 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import Link from 'next/link'; +import AppLauncher from './AppLauncher'; + +const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? 'https://cypher.arcrun.dev'; + +type NavUser = { + display_name: string; + email: string; + avatar_url?: string; +}; + +export default function SiteNav({ currentPath }: { currentPath?: string }) { + const [user, setUser] = useState(undefined); + + useEffect(() => { + fetch(`${API_BASE}/me`, { credentials: 'include' }) + .then(r => r.ok ? r.json() as Promise : null) + .then(u => setUser(u)) + .catch(() => setUser(null)); + }, []); + + const logout = async () => { + await fetch(`${API_BASE}/auth/logout`, { method: 'POST', credentials: 'include' }); + window.location.href = '/'; + }; + + const linkCls = (path: string) => + `transition-colors text-sm ${currentPath === path ? 'text-white' : 'text-[#666] hover:text-white'}`; + + return ( + + ); +} diff --git a/landing/app/components/apps.ts b/landing/app/components/apps.ts new file mode 100644 index 0000000..fec7a93 --- /dev/null +++ b/landing/app/components/apps.ts @@ -0,0 +1,54 @@ +// Matrix App Launcher 九宮格清單 +// 來源:matrix/identity/.agents/specs/identity/apps.json(v0 過渡複製,未來 v1 抽進 @matrix/identity-ui) +// 規範:matrix/identity/.agents/specs/identity/design.md §2.5 + +export type AppEntry = { + id: string; + name: string; + url: string; + icon?: string; + description?: string; + access?: 'public' | 'allowlist'; + allowlist_emails?: string[]; + locked_tooltip?: string; +}; + +export const MATRIX_APPS: AppEntry[] = [ + { + id: 'arcrun', + name: 'Arcrun', + url: 'https://arcrun.dev', + icon: '🔄', + description: '工作流引擎與零件平台', + }, + { + id: 'dashboard', + name: 'Dashboard', + url: 'https://arcrun.dev/dashboard', + icon: '🔑', + description: 'API Key 管理', + }, + { + id: 'integrations', + name: 'Integrations', + url: 'https://arcrun.dev/integrations', + icon: '🧩', + description: '服務目錄', + }, + { + id: 'mira', + name: 'Mira', + url: 'https://arcrun.dev/mira', + icon: '🌊', + description: '個人化 KM 河道', + access: 'allowlist', + allowlist_emails: ['leo21c@gmail.com'], + locked_tooltip: '即將開放', + }, +]; + +export function isAppAccessible(app: AppEntry, userEmail: string | null): boolean { + if (app.access !== 'allowlist') return true; + if (!userEmail) return false; + return (app.allowlist_emails ?? []).includes(userEmail); +} diff --git a/landing/app/dashboard/page.tsx b/landing/app/dashboard/page.tsx new file mode 100644 index 0000000..675647a --- /dev/null +++ b/landing/app/dashboard/page.tsx @@ -0,0 +1,215 @@ +'use client'; + +import { useEffect, useState, useCallback } from 'react'; +import Link from 'next/link'; +import SiteNav from '../components/SiteNav'; + +const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? 'https://cypher.arcrun.dev'; + +type User = { + email: string; + display_name: string; + avatar_url?: string; + api_key: string; + provider: string; + created_at: string; +}; + +export default function DashboardPage() { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [copied, setCopied] = useState(false); + const [rotating, setRotating] = useState(false); + const [revoking, setRevoking] = useState(false); + const [error, setError] = useState(''); + const [showKey, setShowKey] = useState(false); + + const fetchUser = useCallback(async () => { + try { + const res = await fetch(`${API_BASE}/me`, { credentials: 'include' }); + if (res.status === 401) { + window.location.href = '/login?redirect=/dashboard'; + return; + } + if (!res.ok) throw new Error('Failed to fetch user'); + const data = await res.json() as User; + setUser(data); + } catch { + setError('無法載入用戶資訊,請重新整理。'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchUser(); + }, [fetchUser]); + + const copyKey = async () => { + if (!user) return; + await navigator.clipboard.writeText(user.api_key); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const rotateKey = async () => { + if (!confirm('確定要 Rotate API Key 嗎?舊 Key 的 workflow credentials 不會自動遷移。')) return; + setRotating(true); + setError(''); + try { + const res = await fetch(`${API_BASE}/me/api-key/rotate`, { + method: 'PUT', + credentials: 'include', + }); + if (!res.ok) throw new Error('rotate failed'); + const data = await res.json() as { api_key: string; message: string }; + setUser(prev => prev ? { ...prev, api_key: data.api_key } : null); + setShowKey(true); + } catch { + setError('Rotate 失敗,請稍後重試。'); + } finally { + setRotating(false); + } + }; + + const revokeKey = async () => { + if (!confirm('確定要 Revoke API Key 嗎?所有使用此 Key 的服務將立即失效。')) return; + setRevoking(true); + setError(''); + try { + const res = await fetch(`${API_BASE}/me/api-key`, { + method: 'DELETE', + credentials: 'include', + }); + if (!res.ok) throw new Error('revoke failed'); + window.location.href = '/login?revoked=1'; + } catch { + setError('Revoke 失敗,請稍後重試。'); + } finally { + setRevoking(false); + } + }; + + if (loading) { + return ( +
+
載入中...
+
+ ); + } + + if (!user) { + return ( +
+

{error || '請先登入。'}

+ 前往登入 +
+ ); + } + + const maskedKey = showKey ? user.api_key : user.api_key.slice(0, 8) + '••••••••••••••••••••••••'; + + return ( +
+ + +
+

歡迎,{user.display_name}

+

+ 登入方式:{user.provider} · 帳號建立於 {new Date(user.created_at).toLocaleDateString('zh-TW')} +

+ + {error && ( +
+ {error} +
+ )} + + {/* API Key Card */} +
+

您的 API Key

+ +
+
+ {maskedKey} +
+ + +
+ +
+
# 使用方式
+
Authorization: Bearer {user.api_key.slice(0, 8)}...
+
# 或
+
X-Arcrun-API-Key: {user.api_key.slice(0, 8)}...
+
+ +
+ + +
+
+ + {/* Quick Start */} +
+

快速開始

+
+
+ 1. +
+
安裝 CLI
+
npm install -g arcrun
+
+
+
+ 2. +
+
初始化(已有 API Key 可直接輸入)
+
acr init
+
+
+
+ 3. +
+
設定服務認證
+
acr auth-recipe scaffold notion
+
+
+
+
+ +
+ + 查看 20 個支援服務 → + + + API 文件 → + +
+
+
+ ); +} diff --git a/landing/app/favicon.ico b/landing/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/landing/app/favicon.ico differ diff --git a/landing/app/globals.css b/landing/app/globals.css new file mode 100644 index 0000000..80ff86f --- /dev/null +++ b/landing/app/globals.css @@ -0,0 +1,22 @@ +@import "tailwindcss"; + +:root { + --background: #0a0a0a; + --foreground: #ededed; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); +} + +body { + background: var(--background); + color: var(--foreground); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + -webkit-font-smoothing: antialiased; +} + +pre, code { + font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace; +} diff --git a/landing/app/integrations/page.tsx b/landing/app/integrations/page.tsx new file mode 100644 index 0000000..be986fa --- /dev/null +++ b/landing/app/integrations/page.tsx @@ -0,0 +1,164 @@ +export const runtime = 'edge'; + +import Link from 'next/link'; +import SiteNav from '../components/SiteNav'; + +type Recipe = { + id: string; + name: string; + primitive: 'static_key' | 'service_account'; + category: string; + secrets: string[]; + badge?: 'official'; +}; + +const RECIPES: Recipe[] = [ + // AI / LLM + { id: 'openai', name: 'OpenAI', primitive: 'static_key', category: 'AI', secrets: ['OPENAI_API_KEY'], badge: 'official' }, + { id: 'anthropic', name: 'Anthropic', primitive: 'static_key', category: 'AI', secrets: ['ANTHROPIC_API_KEY'], badge: 'official' }, + // Productivity + { id: 'notion', name: 'Notion', primitive: 'static_key', category: 'Productivity', secrets: ['NOTION_TOKEN'], badge: 'official' }, + { id: 'airtable', name: 'Airtable', primitive: 'static_key', category: 'Productivity', secrets: ['AIRTABLE_TOKEN'], badge: 'official' }, + { id: 'typeform', name: 'Typeform', primitive: 'static_key', category: 'Productivity', secrets: ['TYPEFORM_TOKEN'], badge: 'official' }, + { id: 'jira', name: 'Jira', primitive: 'static_key', category: 'Productivity', secrets: ['JIRA_DOMAIN', 'JIRA_EMAIL', 'JIRA_API_TOKEN'], badge: 'official' }, + // Communication + { id: 'slack', name: 'Slack', primitive: 'static_key', category: 'Communication', secrets: ['SLACK_TOKEN'], badge: 'official' }, + { id: 'discord', name: 'Discord', primitive: 'static_key', category: 'Communication', secrets: ['DISCORD_BOT_TOKEN'], badge: 'official' }, + { id: 'twilio', name: 'Twilio', primitive: 'static_key', category: 'Communication', secrets: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN'], badge: 'official' }, + { id: 'sendgrid', name: 'SendGrid', primitive: 'static_key', category: 'Communication', secrets: ['SENDGRID_API_KEY'], badge: 'official' }, + { id: 'resend', name: 'Resend', primitive: 'static_key', category: 'Communication', secrets: ['RESEND_API_KEY'], badge: 'official' }, + // Dev / Code + { id: 'github', name: 'GitHub', primitive: 'static_key', category: 'Dev', secrets: ['GITHUB_TOKEN'], badge: 'official' }, + { id: 'linear', name: 'Linear', primitive: 'static_key', category: 'Dev', secrets: ['LINEAR_API_KEY'], badge: 'official' }, + { id: 'supabase', name: 'Supabase', primitive: 'static_key', category: 'Dev', secrets: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'], badge: 'official' }, + // Commerce + { id: 'stripe', name: 'Stripe', primitive: 'static_key', category: 'Commerce', secrets: ['STRIPE_SECRET_KEY'], badge: 'official' }, + { id: 'shopify', name: 'Shopify', primitive: 'static_key', category: 'Commerce', secrets: ['SHOPIFY_STORE_DOMAIN', 'SHOPIFY_ACCESS_TOKEN'], badge: 'official' }, + { id: 'hubspot', name: 'HubSpot', primitive: 'static_key', category: 'Commerce', secrets: ['HUBSPOT_ACCESS_TOKEN'], badge: 'official' }, + // Google Service Account + { id: 'google_drive_sa', name: 'Google Drive', primitive: 'service_account', category: 'Google', secrets: ['GOOGLE_SERVICE_ACCOUNT_JSON'], badge: 'official' }, + { id: 'google_gmail_sa', name: 'Gmail', primitive: 'service_account', category: 'Google', secrets: ['GOOGLE_SERVICE_ACCOUNT_JSON'], badge: 'official' }, + { id: 'google_sheets_sa', name: 'Google Sheets', primitive: 'service_account', category: 'Google', secrets: ['GOOGLE_SERVICE_ACCOUNT_JSON'], badge: 'official' }, +]; + +const CATEGORIES = ['All', 'AI', 'Productivity', 'Communication', 'Dev', 'Commerce', 'Google']; + +export default function IntegrationsPage({ + searchParams, +}: { + searchParams: Promise<{ cat?: string }>; +}) { + return ; +} + +async function IntegrationsContent({ + searchParamsPromise, +}: { + searchParamsPromise: Promise<{ cat?: string }>; +}) { + const params = await searchParamsPromise; + const cat = params.cat ?? 'All'; + const filtered = cat === 'All' ? RECIPES : RECIPES.filter(r => r.category === cat); + + const staticCount = RECIPES.filter(r => r.primitive === 'static_key').length; + const saCount = RECIPES.filter(r => r.primitive === 'service_account').length; + + return ( +
+ + +
+ {/* Header */} +

+ {RECIPES.length} 個已驗證的認證服務 +

+

+ 由 arcrun 團隊維護,每個 recipe 都通過整合測試。 +

+
+ {staticCount} API Key 類 + · + {saCount} Service Account 類 +
+ + {/* Category filter */} +
+ {CATEGORIES.map(c => ( + + {c} + + ))} +
+ + {/* Recipe grid */} +
+ {filtered.map(recipe => ( + + ))} +
+ + {/* Contribute CTA */} +
+

找不到你要的服務?

+

+ 大部分 API Key 類的服務,填一份 YAML 就能加進來。 + 把 API 文件丟給 AI,五分鐘生成,開 PR 送出。 +

+ +
+
+
+ ); +} + +function RecipeCard({ recipe }: { recipe: Recipe }) { + const primitiveLabel = recipe.primitive === 'static_key' ? 'API Key' : 'Service Account'; + const primitiveColor = recipe.primitive === 'static_key' ? 'text-blue-400' : 'text-orange-400'; + + return ( +
+
+
+

{recipe.name}

+ {primitiveLabel} +
+ {recipe.badge === 'official' && ( + + ★ 官方 + + )} +
+ +
+ {recipe.secrets.map(s => ( +
+ {s} +
+ ))} +
+ +
+ + acr auth-recipe scaffold {recipe.id} + +
+
+ ); +} diff --git a/landing/app/layout.tsx b/landing/app/layout.tsx new file mode 100644 index 0000000..7f92779 --- /dev/null +++ b/landing/app/layout.tsx @@ -0,0 +1,27 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "arcrun — Stop fighting OAuth", + description: "One API key. Every service. Works anywhere. arcrun handles Google, Notion, GitHub, Slack authentication so your code doesn't have to.", + openGraph: { + title: "arcrun — Stop fighting OAuth", + description: "One API key. Every service. Works anywhere.", + url: "https://arcrun.dev", + siteName: "arcrun", + }, +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/landing/app/login/page.tsx b/landing/app/login/page.tsx new file mode 100644 index 0000000..e3f1e3e --- /dev/null +++ b/landing/app/login/page.tsx @@ -0,0 +1,102 @@ +export const runtime = 'edge'; + +import Link from 'next/link'; + +const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? 'https://cypher.arcrun.dev'; + +export default function LoginPage({ + searchParams, +}: { + searchParams: Promise<{ error?: string; redirect?: string }>; +}) { + return ( + + ); +} + +async function LoginContent({ + searchParamsPromise, +}: { + searchParamsPromise: Promise<{ error?: string; redirect?: string }>; +}) { + const params = await searchParamsPromise; + const error = params.error; + const redirect = params.redirect ?? '/dashboard'; + + const googleUrl = `${API_BASE}/auth/google/start?redirect=${encodeURIComponent(redirect)}`; + const githubUrl = `${API_BASE}/auth/github/start?redirect=${encodeURIComponent(redirect)}`; + + const errorMessages: Record = { + cancelled: '登入已取消。', + invalid_state: '安全性驗證失敗,請重試。', + server_error: '伺服器錯誤,請稍後重試。', + github_email_required: 'GitHub 帳號需要設定公開 Email 才能登入。', + }; + + return ( +
+ {/* Logo */} +
+ + arcrun + +
+ + {/* Card */} +
+

登入或建立帳號

+

取得您的 API Key,立即開始使用

+ + {error && ( +
+ {errorMessages[error] ?? '登入時發生錯誤,請重試。'} +
+ )} + +
+ {/* Google */} + + + Continue with Google + + + {/* GitHub */} + + + Continue with GitHub + +
+ +

+ 登入即表示您同意我們的服務條款。 + 不需要信用卡。 +

+
+ + + ← 返回首頁 + +
+ ); +} + +function GoogleIcon() { + return ( + + + + + + + ); +} + +function GitHubIcon() { + return ( + + + + ); +} diff --git a/landing/app/mira/_shared/MiraChat.tsx b/landing/app/mira/_shared/MiraChat.tsx new file mode 100644 index 0000000..7c8ebff --- /dev/null +++ b/landing/app/mira/_shared/MiraChat.tsx @@ -0,0 +1,199 @@ +'use client'; + +// Mira 對話核心元件(河道右側 dock 與 /mira/chat 單頁共用) +// SDD: polaris/mira/.agents/specs/mira-app/design.md §3.6.5 +// RAG:提問先語義搜尋 KBDB(wiki + 河道)取 context → claude-api daemon。 +// 重要:context 空時 prompt 明確要求「只說沒有相關筆記」,避免 daemon 自由發揮(曾幻想 OpenWebUI) + +import { useEffect, useRef, useState } from 'react'; +import Link from 'next/link'; +import { MarkdownView } from './markdown'; + +const KBDB_BASE = 'https://kbdb.finally.click'; +const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? 'https://cypher.arcrun.dev'; +const CLAUDE_API = 'https://claude-api.arcrun.dev'; + +type Source = { label: string; href: string }; +type Msg = { role: 'user' | 'mira'; text: string; sources?: Source[]; pending?: boolean }; + +type SearchMatch = { + score: number; + type: 'block' | 'triplet'; + block: { id: string; page_name: string | null; content: string | null; type: string } | null; + triplet: { subject?: string; predicate?: string; object?: string } | null; +}; + +// 繁體異體字正規化(臺→台 等),讓 query 跟 KB 內容(多用「台」)對得上 +function normalizeQuery(q: string): string { + return q.replace(/臺/g, '台'); +} + +async function fetchContext(apiKey: string, query: string): Promise<{ context: string; sources: Source[] }> { + try { + const res = await fetch(`${KBDB_BASE}/search`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, + body: JSON.stringify({ query: normalizeQuery(query), type: 'semantic', topK: 8 }), + }); + if (!res.ok) return { context: '', sources: [] }; + const data = (await res.json()) as { matches?: SearchMatch[] }; + const parts: string[] = []; + const sources: Source[] = []; + for (const m of data.matches ?? []) { + if (m.block?.content) { + const b = m.block; + parts.push(`### ${b.type}:${b.page_name ?? b.id}\n${b.content}`); + if (b.type === 'wiki-page' && b.page_name) { + sources.push({ label: `📚 ${(b.content || b.page_name).slice(0, 20)}`, href: `/mira/wiki/${encodeURIComponent(b.page_name)}` }); + } else if (b.page_name) { + sources.push({ label: `🌊 ${(b.content || '').slice(0, 20) || b.page_name}`, href: `/mira/feed#page=${encodeURIComponent(b.page_name)}` }); + } + } else if (m.triplet) { + const t = m.triplet; + parts.push(`關係:${t.subject} >> ${t.predicate} >> ${t.object}`); + } + } + const seen = new Set(); + const uniq = sources.filter(s => (seen.has(s.href) ? false : (seen.add(s.href), true))); + return { context: parts.join('\n\n'), sources: uniq.slice(0, 5) }; + } catch { + return { context: '', sources: [] }; + } +} + +function buildPrompt(history: Msg[], context: string, question: string): string { + const convo = history + .filter(m => !m.pending) + .map(m => `${m.role === 'user' ? 'leo' : 'Mira'}:${m.text}`) + .join('\n'); + const persona = + `你是 Mira,leo 的個人知識庫副駕 AI。你只能根據下方「知識庫」與「對話脈絡」回答,` + + `沒有任何外部系統存取權(沒有 OpenWebUI、沒有檔案系統、沒有別的工具)。\n\n`; + const kb = context + ? `## 知識庫(跟本次提問相關的 wiki / 河道內容)\n\n${context}\n\n` + : `## 知識庫\n(這次在 leo 的筆記裡找不到相關內容。)\n\n`; + const rules = context + ? `規則:繁體中文(台灣用語)、務實不客套、優先引用上方知識庫並說「你之前寫過⋯」、簡短切題。` + : `規則:繁體中文。**明確告訴 leo「你的筆記裡目前沒有關於這個的內容」**,` + + `可以再用常識補一兩句(要標明那不是來自他的筆記),不要假裝有資料、不要編造系統或工具。`; + return ( + persona + + kb + + (convo ? `## 對話脈絡\n${convo}\n\n` : '') + + `---\n\nleo 現在問:「${question}」\n\n${rules}` + ); +} + +export default function MiraChat({ compact = false }: { compact?: boolean }) { + const [apiKey, setApiKey] = useState(null); + const [msgs, setMsgs] = useState([]); + const [input, setInput] = useState(''); + const [sending, setSending] = useState(false); + const logRef = useRef(null); + + useEffect(() => { + fetch(`${API_BASE}/me`, { credentials: 'include' }) + .then(r => (r.ok ? r.json() : null)) + .then((me: { api_key: string } | null) => { if (me?.api_key) setApiKey(me.api_key); }) + .catch(() => {}); + }, []); + + useEffect(() => { + logRef.current?.scrollTo({ top: logRef.current.scrollHeight, behavior: 'smooth' }); + }, [msgs]); + + const send = async () => { + const question = input.trim(); + if (!question || sending || !apiKey) return; + setInput(''); + setSending(true); + const history = msgs; + setMsgs(m => [...m, { role: 'user', text: question }, { role: 'mira', text: '', pending: true }]); + try { + const { context, sources } = await fetchContext(apiKey, question); + const prompt = buildPrompt(history, context, question); + const res = await fetch(CLAUDE_API, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ prompt, timeout_ms: 60000 }), + }); + const data = (await res.json()) as { success?: boolean; pending?: boolean; data?: { text?: string }; error?: string }; + let text: string; + if (!res.ok || !data.success) text = `(回答失敗:${data.error ?? res.status})`; + else if (data.pending) text = '(Mira 還在想,daemon 切到背景模式,請稍後再問一次)'; + else text = data.data?.text ?? '(沒有內容)'; + setMsgs(m => { + const next = [...m]; + next[next.length - 1] = { role: 'mira', text, sources: sources.length ? sources : undefined }; + return next; + }); + } catch (e) { + setMsgs(m => { + const next = [...m]; + next[next.length - 1] = { role: 'mira', text: `(錯誤:${e instanceof Error ? e.message : String(e)})` }; + return next; + }); + } finally { + setSending(false); + } + }; + + return ( +
+
+ {msgs.length === 0 && ( +
+
💬
+

問 Mira 任何事 — 它會先翻你的知識庫。

+
+ )} + {msgs.map((m, i) => ( +
+
+ {m.pending ? ( + Mira 思考中 + ) : m.role === 'mira' ? ( + + ) : ( + m.text + )} + {m.sources && m.sources.length > 0 && ( +
+ 來源: + {m.sources.map((s, j) => ( + + {j > 0 && ' · '} + {s.label} + + ))} +
+ )} +
+
+ ))} +
+ +
+