薄殼 SessionStart 跑在 bootstrap.sh 之前,InkStoneCo/ 還沒 clone、家目錄空 ⇒ 兩份都不在 ⇒ countdown.py 退到「期限最近的 milestone」⇒ 第一則 prompt 印逾期 341 小時的舊線。機制沒壞,是順序。 - hooks/lib/mainline.py:第三個來源 fetch_gitea()(本檔唯一會打網路的函式,只有 SessionStart 的 `mainline refresh` 叫它;load()/line() 照舊零網路)。優先序寫進檔頭:家目錄 > repo 那份 > Gitea 那份。 匿名優先,匿名被拒(401/403/404)且拿得到 token 才帶 token 再問一次;讀不到回 None,逾時有上限。 - scripts/mainline refresh:兩份都不在 ⇒ fetch_gitea 一次、只寫家目錄(不 mirror);token 拿不到不炸、不抱怨。 - hooks/tests/mainline-gitea-fallback.test.sh:31 條,假 Gitea 有 public/private/500/garbage/notitle/hang 六種回法。 假設(票上紅線寫「一律匿名不帶 token」,其前提「Gitea 匿名讀得到」實測為假): inkstone/InkStoneCo 在 Gitea 是 private——匿名打 raw/contents/repo API 都 404,ISEP 同端點 200。 ⇒ 照字面做這張票在真環境永遠讀不到。改成匿名優先、被拒才帶 token(同一條 SessionStart 鏈上 countdown-milestone-refresh.sh 本來就帶 GITEA_TOKEN_CLAUDE_CODE 打 Gitea;D20 管的是 GitHub)。 真跑:帶 token 2.06s 讀到 inkstone/Arcrun#48;拔 token rc 0、零檔案、「現在沒有主線」。 版本:待總管定版。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0178ef1fGw3XeZtpN7LaZrm4
15 KiB
雲端 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 原生支援、且經官方文件證實可行的路徑:
- ISEP 本身已經是一個合法 plugin(
.claude-plugin/plugin.json+.claude-plugin/marketplace.json,另一張票的產物),hook 一律用${CLAUDE_PLUGIN_ROOT},不寫死路徑。 - 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 userclaude plugin install是同一條路徑,裝的內容 100% 來自 ISEP 這個 repo 的 HEAD,沒有第二份、沒有產生器、沒有「子集」。 - 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 重寫:
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:<token>@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:<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 pluginCLI」,但 Cloud environment 的 Setup script 欄位、Environment variables 欄位是 claude.ai 帳號層級的設定, 我沒有去改——那是 leo 的 dashboard,不是這台機器上的檔案,我也判斷這件事 不屬於「可以自己裁」的範圍(不是 GitHub push,但同樣是帳號層級設定, 比照 D20 的精神交給 leo 動手)。 - ❌ 沒有驗到「hook 真的攔下第一個工具呼叫」這一步的完整 runtime 行為——
只驗到「plugin 在磁碟上正確就位、
claude plugin list回報 enabled」。 完整跑一個已登入的claude -psession需要這台機器的 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'sknown_marketplaces.json... This works in both interactive mode and non-interactive mode with the-pflag.」),不是我自己重現的 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):
- 選 InkStoneCo 這條線在用的 environment(或建一個新的),
Environment variables 欄位加一行:
GITEA_TOKEN_CLAUDE_CODE=<既有那把值>(名字沿用InkStoneCo#14已建立的那把,不要新造;值只有 leo 知道要填什麼, 我這邊沒有也不該有)。 - Setup script 欄位貼
docs/cloud-setup-script.sh的內容。 - 開一個新 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 兩份一起寫。兩份都不在(SessionStart 跑在 bootstrap.sh 之前,InkStoneCo/ 還沒 clone)⇒ mainline refresh 去 Gitea 讀 InkStoneCo main 那份一次寫進家目錄(inkstone/ISEP#140)——第一則 prompt 的 ⏱/🎯 就是現在這條,不再退到逾期的舊線 |
總管 set 之後要把 repo 那份 commit/併 main;Cloud environment 要有 GITEA_TOKEN_CLAUDE_CODE(InkStoneCo 是 private,匿名讀不到) |
正門工具被分類器擋(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 層硬規則,誰都不能代 |
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)。