# 雲端 session 怎麼載到 ISEP —— inkstone/ISEP#5 對照 `docs/governance/sdd-gitea-governance.md` §11.3「載入契約」三條硬規則 (L11.3.1/L11.3.2/L11.3.3)與其驗收標準(L11.3.4:能貼出雲端實際載到的清單, 逐條對上 ISEP 的註冊條數)。本檔記錄機制、實測結果、還缺什麼。 ## 舊模型死在哪(不要重蹈) `InkStoneCo/.claude/cloud-shell/`(`generate-shell-payload.py` + 薄殼)的模型是: 真身 hook/command/skill → 產生器跑出「薄殼該長的樣子」→ 人推一份**複製本**進 GitHub 私 repo `youlinhsieh/inkstoneco`。這條鏈上兩個環節都「要有人記得」: - 忘了重跑產生器 → 產物落後真身 - 產物沒推 → 薄殼落後產物 `inkstone/InkStoneCo#57` 的實測:薄殼比真身少 7 支閘,其中兩支才立一天。 `#14` 更早:雲端 33 支閘一支都沒生效。**兩次同一個病**:任何「複製一份」的設計, 新鮮度只能靠人記得,而人會忘。 ## 新機制:讓 Claude Code 自己的 plugin marketplace 去裝真身 不做複製,改用 Claude Code 原生支援、且經官方文件證實可行的路徑: 1. **ISEP 本身已經是一個合法 plugin**(`.claude-plugin/plugin.json` + `.claude-plugin/marketplace.json`,另一張票的產物),hook 一律用 `${CLAUDE_PLUGIN_ROOT}`,不寫死路徑。 2. Cloud environment 的 **Setup script**(code-on-web 原生功能, 在 Claude Code 啟動**之前**跑,跑在同一台會被拍成快照的 VM 上)裡跑: ``` claude plugin marketplace add https://git.uncle6.me/inkstone/ISEP.git --scope user claude plugin install isep@inkstone --scope user ``` 這兩行**不是複製**——跟本機 `claude plugin install` 是同一條路徑,裝的內容 100% 來自 ISEP 這個 repo 的 HEAD,沒有第二份、沒有產生器、沒有「子集」。 3. Setup script 跑完,Anthropic 把整個檔案系統(含 `~/.claude/plugins/`)拍成快照, 之後每個新 session 直接沿用快照,**在 Claude Code 啟動當下**(不是「clone 完才補」) plugin 就已經在磁碟上——`command`/`skill` 啟動時的目錄掃描掃得到,不再是 `#14` 那個「hook 可以晚到、skill/command 不行」的破口(見 L11.3.2)。 私有 repo 的認證:不把 token 寫進任何檔案,改用官方文件建議的 CI/CD 寫法—— 用 `GITEA_TOKEN_CLAUDE_CODE`(既有機器帳號 token,`InkStoneCo#14` 已建立的同一把, 沒有新造)在 Setup script 裡做一次 git URL 重寫: ```sh git config --global url."https://x-access-token:${GITEA_TOKEN_CLAUDE_CODE}@git.uncle6.me/".insteadOf \ "https://git.uncle6.me/" ``` `marketplace add` 用乾淨網址(不帶 token),認證完全交給上面那條重寫, 所以 `known_marketplaces.json` 裡存的來源網址也不帶 token (本機實測驗過,見下面「已驗」第 2 輪)。 完整腳本:`docs/cloud-setup-script.sh`(貼進 code-on-web 的 Setup script 欄位用)。 ## 已驗(本機,隔離環境,不影響本機正在跑的任何 session) 🔴 **怎麼保證沒有干擾**:全程把 `$HOME` 指到 scratchpad 底下的隔離目錄 (`isep-test-home`/`isep-test-home2`),從未寫到真正的 `~/.claude/`, 也沒有動到 `InkStoneCo/.claude/` 那份舊設定。兩者互不相干, 本機目前跑著的其他 session/agent 全程沒受影響。 **第 1 輪**(token 直接嵌在 marketplace URL 裡,沿用 ISEP 這個 git checkout 本來就有的、已解析好的 origin 憑證——不是我另外造的憑證,是既有機制解析出來的那份): ``` $ claude plugin marketplace add "https://claude-code:@git.uncle6.me/inkstone/ISEP.git" --scope user Adding marketplace…Refreshing marketplace cache (timeout: 120s)… Cloning repository (timeout: 120s): https://***:***@git.uncle6.me/inkstone/ISEP.git Clone complete, validating marketplace… ✔ Successfully added marketplace: inkstone (declared in user settings) $ claude plugin install isep@inkstone --scope user Installing plugin "isep@inkstone"...✔ Successfully installed plugin: isep@inkstone (scope: user) $ claude plugin list Installed plugins: ❯ isep@inkstone Version: 0.0.0 Scope: user Status: ✔ enabled ``` **第 2 輪**(重跑一次,改用實際要交付的「乾淨 URL + git config url.insteadOf 重寫」 寫法,驗證 §建議腳本 那段真的可行,而不是理論上可行): ``` $ git config --global url."https://x-access-token:@git.uncle6.me/".insteadOf "https://git.uncle6.me/" $ claude plugin marketplace add https://git.uncle6.me/inkstone/ISEP.git --scope user ✔ Successfully added marketplace: inkstone (declared in user settings) $ claude plugin install isep@inkstone --scope user ✔ Successfully installed plugin: isep@inkstone (scope: user) $ cat ~/.claude/plugins/known_marketplaces.json { "inkstone": { "source": { "source": "git", "url": "https://git.uncle6.me/inkstone/ISEP.git" }, ... } } ``` ⇒ 存在磁碟上的 marketplace 來源紀錄**不帶 token**——符合 L11.3.3「憑證只准取名字」。 **逐條對照**(`claude plugin details isep@inkstone` 的輸出 + 直接數快取目錄裡的檔案, 對照 ISEP 這次測試當下的 Gitea `main` HEAD): | | 裝到本機隔離環境的 | ISEP `main` 當下的來源 | 對上了嗎 | |---|---|---|---| | Skills | 9(`cp-write`/`deep-recall`/`issue-handle`/`sdd-check`/`ship-check`/`wiki-capture`/`wiki-init`/`wiki-recall`/`wiki-update`) | `commands/` 7 支 + `skills/` 2 支 = 9 | ✅ 逐支比對名稱一致 | | Commands 目錄 | 7 個 `.md` | 7 個 `.md` | ✅ `diff` 兩邊檔名清單完全一致 | | Skills 目錄 | `deep-recall`/`ship-check` 2 個 | 同 | ✅ | | Hook 腳本(`hooks/*.sh` 實體檔) | 42 支 | 42 支 | ✅ `ls` 兩邊都是 42 | | `hooks.json` 裡註冊的 hook 腳本路徑(去重) | 39 支唯一路徑 | 39 支 | ✅ `diff` 兩邊 grep 結果完全一致 | (42 支實體檔 vs 39 支被 `hooks.json` 引用:差的 3 支是 `hooks.json` 自己 + `pre-write-guard.sh`/`pre-write-guard.template.sh` 這類非直接掛註冊的輔助檔, 兩邊都一樣,不是漏裝。) **意外的額外證據**:測試途中 ISEP 的 `main` 因為別的票(`#6`/`#9` 等)合併而往前推進 (多出 `hooks/release-tag-guard.sh`、`docs/governance/`…),**下一次 `claude plugin marketplace update` / 重裝立刻拿到新內容**——證明這條路徑讀的是 Gitea 當下的 HEAD, 不是任何時間點的快照複製本。 ## 沒驗到的(明講,不含糊) - ❌ **沒有在真正的 code-on-web 雲端 session 裡跑過。** 本機能做到的最接近測試是 「隔離 `$HOME` + 真的私有 repo + 真的 `claude plugin` CLI」,但 Cloud environment 的 Setup script 欄位、Environment variables 欄位是 claude.ai 帳號層級的設定, 我沒有去改——那是 leo 的 dashboard,不是這台機器上的檔案,我也判斷這件事 不屬於「可以自己裁」的範圍(不是 GitHub push,但同樣是帳號層級設定, 比照 D20 的精神交給 leo 動手)。 - ❌ **沒有驗到「hook 真的攔下第一個工具呼叫」這一步的完整 runtime 行為**—— 只驗到「plugin 在磁碟上正確就位、`claude plugin list` 回報 enabled」。 完整跑一個已登入的 `claude -p` session需要這台機器的 Claude Code 登入憑證 (存在 macOS Keychain,不是可複製的檔案),我判斷把它匯出到隔離測試環境 超出這張票該做的事,沒有做。 Q5(plugin 是否在 SessionStart 前同步就位、保證第一個工具呼叫就有效) 這格的證據來自官方文件(`plugin-marketplaces.md` §Pre-populate plugins for containers:「At startup, Claude Code registers marketplaces found in the seed's `known_marketplaces.json`... This works in both interactive mode and non-interactive mode with the `-p` flag.」),**不是我自己重現的 runtime 實測**。 - ❌ **Environment caching 的 ~7 天新鮮度窗口沒有解**——setup script 只在 「這個 environment 第一次開 session」跑一次,之後沿用快照,直到快照過期 (約 7 天)或 leo 改了 setup script/allowed network hosts 才重跑。 這代表 ISEP 若在窗口期內更新,雲端會暫時停在舊版本,直到快照重建。 這不是本票要解的「載不載得到」問題,而是另一種新鮮度問題,**留給 leo 決定 要不要另開票**(例如:leo 定期手動點一下「rebuild environment」,或接受 7 天週期)。 ## 需要 leo 做的(帳號層級設定,非 GitHub push,但同樣是我不該自己動的地方) 去 code-on-web 的 **Cloud environments** 設定(`claude.ai` 帳號設定,不是 GitHub、不是 Gitea): 1. 選 InkStoneCo 這條線在用的 environment(或建一個新的), **Environment variables** 欄位加一行:`GITEA_TOKEN_CLAUDE_CODE=<既有那把值>` (名字沿用 `InkStoneCo#14` 已建立的那把,不要新造;值只有 leo 知道要填什麼, 我這邊沒有也不該有)。 2. **Setup script** 欄位貼 `docs/cloud-setup-script.sh` 的內容。 3. 開一個新 session(或用 dashboard 的「rebuild environment」逼快照重建), 驗 `claude plugin list` 顯示 `isep@inkstone enabled`,且照 §逐條對照 那張表 再核一次數字。 ## 雲端要全權處理的三樣:票、主線、通知(inkstone/ISEP#130,2026-09-07) leo 09-07:「雲端回覆的問題你首要解決⋯⋯僅有需要依賴 Mac 環境的東西要求地端處理,其他全部需要可以從雲端全權處理。」 09-04 兩支 Routine 的 run log 實查出四件雲端拿不到的東西,ISEP 這一半的解法: | 拿不到什麼 | 為什麼 | ISEP 這一半 | 還缺誰 | |---|---|---|---| | **主線是哪條** | 主線檔只在本機 `~/.claude` | `hooks/lib/mainline.py`:家目錄沒有就讀 repo 的 `InkStoneCo/system-dev/mainline.json`(雲端形狀:`$CLAUDE_PROJECT_DIR` 是薄殼根、真身在 `InkStoneCo/`);`mainline set`/`adopt`/`clear` 兩份一起寫 | 總管 `set` 之後要把 repo 那份 commit/併 main | | **正門工具被分類器擋**(`permission_denials=6`) | leo 09-07 親手加的四條只在本機 settings | `docs/permissions-allow.json` 一份清單;`scripts/settings-allow-sync` 寫進那台的 `~/.claude/settings.json`(setup script 裝完 plugin 跑一次,SessionStart 每次再對一次) | 雲端一趟 run 看 `permission_denials`=0 才算驗到 | | **讀 leo21c 也被擋** | `leo21c-write-guard.sh` 把 `tr -d` 的 `-d` 當 body | 唯讀 `-d` 先剪掉再判;`notify_leo` trigger 放行(同 `prod-write-guard` 白名單);改法段改印現役 youlin 子網域 | — | | **收工摘要到不了手機** | 雲端沒 `TELEGRAM_*`;leo21c 的 `notify_leo` 404;youlin 被 egress proxy 擋 | `scripts/isep-notify` 有 `TELEGRAM_BOT_TOKEN`/`TELEGRAM_CHAT_ID` 就先走 Bot API 直送(`api.telegram.org` 從雲端是通的) | **leo**:在 Cloud environment 加那兩個變數 | ### 雲端連不到 youlin 不是 000,是 proxy 403(2026-09-07 實查) ``` $ curl -sS -o /dev/null -w '%{http_code}' https://arcrun-cypher-executor.arcrun-yuga3bse.workers.dev/health curl: (56) CONNECT tunnel failed, response 403 ← egress policy denial $ curl -sS "$HTTPS_PROXY/__agentproxy/status" | jq .recentRelayFailures { "kind": "connect_rejected", "host": "arcrun-cypher-executor.arcrun-yuga3bse.workers.dev:443", "detail": "gateway answered 403 to CONNECT (policy denial or upstream failure)" } $ curl … https://arcrun-cypher-executor.leo21c.workers.dev/health → 200 $ curl … https://api.telegram.org/ → 302 $ curl … https://git.uncle6.me/api/v1/version → 200 ``` ⇒ 09-04 log 寫的「youlin 從雲端 HTTP 000」有兩層:舊子網域 `youlin-hsieh-dev` DNS 已死(總管 09-07 查到), **而新子網域從雲端是被 policy 擋**——`/root/.ccr/README.md` 明寫「403/407 = 你組織的 egress policy 不允許這個主機,不要重試、不要繞路,回報主機名」。leo21c 通、youlin 不通,差別只在主機有沒有被放行。 **這格是 leo 的**:Cloud environment 的 allowed network hosts 加 `*.arcrun-yuga3bse.workers.dev`; 在那之前雲端做不了 stage 驗證,所有要打 youlin 的驗證一律交回地端或標 `Human`。 ### 只有「要 Mac」的事才交回地端(給 `cloud-worker.md` 的清單草稿) 判準一句話:**這件事依賴的東西隨 repo clone 走得到嗎?走得到就雲端做。** | 交回地端 | 為什麼雲端做不了 | |---|---| | 打包 DMG/簽章/MS Store 送件 | 要 macOS 工具鏈與簽章身分(Keychain),不隨 clone 走 | | 本機 daemon(`~/.arcrun-rag`、`~/.arcrun/config.yaml`)的改與驗 | 住在 leo 的家目錄 | | 用 `~/.wrangler` OAuth 登入態出貨到 uncle6 | 登入態不隨 clone 走(`make-cloud-env.sh` 檔頭記過) | | 要 leo 親手輸入的互動閘(`acr recipe push` 的暴露同意、密碼打進登入欄) | surface 層硬規則,誰都不能代 | | ~~打 youlin stage 的驗證~~ | 09-07 已回到雲端:`*.arcrun-yuga3bse.workers.dev` 從雲端 curl 200。**寫入走 `scripts/stage`**(secret/api,inkstone/ISEP#137),不再交回地端 | 其餘(讀票、認領、留言、handback、寫 code、跑測試、開 PR、對 leo21c 唯讀查詢、發 Telegram、**種 secret/直推成品/POST 到 stage 實例**——走 `scripts/stage` 與 Arcrun 的 `stage-deploy-artifacts.py`)雲端全做。 📌 這份清單的家是 `inkstone/InkStoneCo` 的 `cloud-worker.md`(票上驗收第 4 條),ISEP 只放草稿;併進去由總管做。 ## 這張票沒動、也不會動的東西 - 沒有動 `youlinhsieh/inkstoneco`(GitHub 薄殼 repo)——這個新機制**完全不需要 改它**:安裝目標是 `--scope user`(VM 家目錄),跟 session 從哪個 cwd 啟動無關。 舊模型需要在薄殼裡放 `.claude/settings.json` 指標,新模型不需要。 - 沒有 push 到 GitHub、沒有 push 到本 repo的 `main`。全部改動只在 `leaf/5-cloud` 這條分支。 - 沒有把任何 token 值寫進這個 repo 的任何檔案(`docs/cloud-setup-script.sh` 只引用環境變數名字 `GITEA_TOKEN_CLAUDE_CODE`)。