diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 3b9fb83..09b2084 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "isep", - "description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:61 支機械閘(85 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、7 位有名字的工人(agents/,見 docs/governance/worker-roster.md)、54 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。", + "description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:61 支機械閘(85 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、7 位有名字的工人(agents/,見 docs/governance/worker-roster.md)、56 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。", "version": "0.23.0", "keywords": [ "inkstone", diff --git a/README.md b/README.md index d3fc695..77f5cf9 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ | `agents/` | 7 位 | **工人名單**(`inkstone/ISEP#86`)——派工時指名派給誰,規約見 `docs/governance/worker-roster.md` | | `commands/` | 7 支 | `/wiki-recall` `/wiki-capture` `/cp-write` `/issue-handle` … | | `skills/` | 2 支 | `ship-check`(東西要出去之前)/`deep-recall`(把散落的枝葉還原成一棵樹)。🔴 **這兩支的內容不一定是在這裡寫的**——誰是真相源查 `docs/file-ownership.tsv` | -| `scripts/` | 54 支 | `ticket`/`roster`/`isep-nag`/`wiki-compress`/`github-arm.sh` …(頂層檔案,不含 `lib/` 等子目錄) | +| `scripts/` | 56 支 | `ticket`/`roster`/`isep-nag`/`wiki-compress`/`github-arm.sh` …(頂層檔案,不含 `lib/` 等子目錄) | > 🔴 這五個數字**每次都要在自己的樹上實數**,不准沿用上一版、也不准用加減推 > (`system-dev/wiki/mistakes.md`:那是 leo 的驗收介面,多報就是假綠): diff --git a/docs/TESTING.md b/docs/TESTING.md index c885633..81846bd 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -822,6 +822,43 @@ api.telegram.org → 302,通 **失敗**:A1 紅 ⇒ 沒憑證時不講缺什麼,leo 不知道要在 Cloud environment 加哪兩個; B3 紅 ⇒ 送到了還去打實例;C1 紅 ⇒ 401 被講成送到;D1 紅 ⇒ 舊版閘 block 時直送也被連坐。 +### A36 — 雲端對 stage 的寫入有正門,而且只寫得進 stage:45 條 +``` +bash scripts/test-stage.sh +``` +**該看到**:`45/45 通過`。CF API 與 stage 實例都指到本機一個假伺服器——不打真 Cloudflare、不碰任何 worker。 +假伺服器對 secret PUT 回 **201**(真 CF 就是 201)。 + +**它在守什麼**(inkstone/ISEP#137,2026-09-07 三條主線工人在雲端撞的同一面牆): +``` +printf … | npx wrangler secret put … ← 分類器擋(複合指令對不上白名單任何一條前綴) +python3 scripts/stage-deploy-artifacts.py all --confirm ← 分類器擋 +curl -X POST …arcrun-yuga3bse.workers.dev/records ← 分類器擋;0.21.0 的 prod-write-guard 又當它是 prod +``` +⇒ `scripts/stage`:`secret put/list/delete`(直接打 CF API,不經 npx)與 `api /` +(curl 打實例,標頭與 body 走 stdin,不走 argv)。**只寫 stage**:secret 先 `GET /accounts` 看這把 token +打得到誰、URL 永遠只帶 `1129efd7…`;api 的主機只會長在 `*.arcrun-yuga3bse.workers.dev`。 +值只從環境變數名讀(`--from-env`/`--bearer-env`/`--header-env`/`--data-env`),`--value`/`--bearer`/`--header` 一律拒收。 +Arcrun 的 `stage-deploy-artifacts.py` 自己已寫死只認 youlin 帳號 ID、不收 `--account`/`--token`, +白名單直接放它從 Arcrun 根目錄跑的相對形狀,不重造一支。 + +**失敗**: +- A1/B1/B2/D1/D2 紅 ⇒ **非 stage 也寫得進去**——這是本工具存在的唯一理由 +- A4/D3 紅 ⇒ 值可以從指令列進來(會進 shell 歷史與 session log) +- C1 紅 ⇒ 又把 201 講成失敗(第一次實跑就是這樣:CF 種好了、工具說失敗) +- C7/C9 紅 ⇒ 只信 PUT 的回應不去列 +- **E2 紅 ⇒ ISEP 自己的閘誤攔正門**——誤攔比漏擋嚴重,被擋的人會去走側門 +- E4 紅 ⇒ 又長出第二支平行的工具 + +**真實跑(2026-09-07,雲端 session,isep-hand)**: +`stage secret put` 把隨機值的 `ISEP_137_PROBE` 種進 youlin `arcrun-cypher-executor` → 不經工具用 CF API 列到 +`["ISEP_137_PROBE","KBDB_INTERNAL_TOKEN"]` → `stage secret delete` → 再列剩 `["KBDB_INTERNAL_TOKEN"]`(stage 恢復原狀)。 +`stage api GET arcrun-cypher-executor/health` → HTTP 200;`stage api POST arcrun-kbdb/map/recompute…` 不帶 Bearer → HTTP 401、離開碼 1(打到了、沒寫); +完整網址指到 leo21c → 拒絕、離開碼 2、零請求。 +⚠️ **帶 Bearer 真的寫進 stage KBDB** 這格沒跑——雲端 Cloud environment 沒有 `KBDB_INTERNAL_TOKEN`(它只住在安裝器中心側 KV,Arcrun#176 comment 6487 步驟 0); +`CF_SECRETS_API_TOKEN` 的真值是主線 B-3(Arcrun#86)的東西,由總管用 `stage secret put` 種,本票不編一個假的塞進去。 +⚠️ 「分類器真的不擋」與 A33 同款:要下一版裝進雲端之後,看一趟 run 的 `permission_denials`。 + ### A5 — 開票前的搜尋是跨 repo 的 ``` python3 scripts/ticket where 標籤 模組化 @@ -1104,6 +1141,7 @@ B2(信標那行)/B3(setup 輸出)/B4(閘的訊息)三個畫面 | **A33 權限白名單住 ISEP 一份** | isep-hand | ✅ 19/19(2026-09-07,inkstone/ISEP#130);「分類器真的不擋」要雲端 run 的 `permission_denials` 才驗得到 | | **A34 收件 repo 寫 owner/repo 不 404** | isep-hand | ✅ 7/7(2026-09-07,inkstone/ISEP#130) | | **A35 Bot API 直送** | isep-hand | ✅ 10/10(2026-09-07);**真的到 leo 手機**要 Cloud environment 先有 `TELEGRAM_BOT_TOKEN`/`TELEGRAM_CHAT_ID`(leo) | +| **A36 雲端對 stage 的寫入有正門、只寫得進 stage** | isep-hand | ✅ 45/45(2026-09-07,inkstone/ISEP#137)+真實種進 youlin 一把探針再刪掉、真打 stage GET 200/無 Bearer POST 401;「帶 Bearer 真寫 KBDB」缺 `KBDB_INTERNAL_TOKEN` 沒跑 | | A7 plugin 裝得起來 | 總管 | ✅ | | **A8 新 session 閘會觸發** | 總管 | 見本版 release note | | **B1–B5 雲端** | **leo** | 還沒跑(機器碰不到 Cloud environment) | diff --git a/docs/cloud-session-bootstrap.md b/docs/cloud-session-bootstrap.md index 49c8e65..9d63824 100644 --- a/docs/cloud-session-bootstrap.md +++ b/docs/cloud-session-bootstrap.md @@ -201,9 +201,9 @@ $ curl … https://git.uncle6.me/api/v1/version → 200 | 本機 daemon(`~/.arcrun-rag`、`~/.arcrun/config.yaml`)的改與驗 | 住在 leo 的家目錄 | | 用 `~/.wrangler` OAuth 登入態出貨到 uncle6 | 登入態不隨 clone 走(`make-cloud-env.sh` 檔頭記過) | | 要 leo 親手輸入的互動閘(`acr recipe push` 的暴露同意、密碼打進登入欄) | surface 層硬規則,誰都不能代 | -| 打 youlin stage 的驗證 | **暫時**:egress proxy 擋(上一段)。leo 放行主機後這條就回到雲端 | +| ~~打 youlin stage 的驗證~~ | 09-07 已回到雲端:`*.arcrun-yuga3bse.workers.dev` 從雲端 curl 200。**寫入走 `scripts/stage`**(secret/api,inkstone/ISEP#137),不再交回地端 | -其餘(讀票、認領、留言、handback、寫 code、跑測試、開 PR、對 leo21c 唯讀查詢、發 Telegram)雲端全做。 +其餘(讀票、認領、留言、handback、寫 code、跑測試、開 PR、對 leo21c 唯讀查詢、發 Telegram、**種 secret/直推成品/POST 到 stage 實例**——走 `scripts/stage` 與 Arcrun 的 `stage-deploy-artifacts.py`)雲端全做。 📌 這份清單的家是 `inkstone/InkStoneCo` 的 `cloud-worker.md`(票上驗收第 4 條),ISEP 只放草稿;併進去由總管做。 ## 這張票沒動、也不會動的東西 diff --git a/docs/hooks-inventory.md b/docs/hooks-inventory.md index 714db61..ac422d8 100644 --- a/docs/hooks-inventory.md +++ b/docs/hooks-inventory.md @@ -212,7 +212,7 @@ |---|---|---| | `isep-presence-beacon.sh` | 對話一開始印一行 `🟢 ISEP vX.Y.Z 已載入(N 支閘|來源:…)`。**這行不是裝飾,是唯一能證明「這個 session 真的有閘」的東西**——它自己就住在 plugin 裡,看得到它就表示 plugin 載入了;某個 session 從頭到尾沒有這行,那個 session 是零閘狀態,先修 plugin 再做事。同一台機器可能同時有兩份 ISEP(marketplace 裝的、repo 裡 vendor 的),所以那行會講出這次是哪一份在說話。它另外會報四件「這一份是不是還有效」:①這一份落後 ISEP main 幾版 ②專案裡有沒有內容不同的同名**腳本**②b 專案裡有沒有內容不同的同名**skill/command/agent**——這些是**自動載入**的,載到舊的那份不會有任何症狀,只會安靜地教錯的東西(`docs/file-ownership.tsv` 說得出每一組的真相源是哪一份、往哪個方向修)②c 本身的內容跟那張歸屬表對不對得上(雲端沒有專案那一份可比時,這是唯一還作數的檢查)③工作區有沒有已退役機制留下的產物。**全部只講不擋。** | 📝 記錄(context 注入) | | `session-start-recall.sh` | 對話一開始就自動把「全局現況」(Gitea 各 repo 的票、KBDB 的藏書地圖)推到 AI 眼前,不必等它自己想到要查。 | 📝 記錄(context 注入) | -| `scripts/settings-allow-sync --quiet` | (不是閘,是腳本)對話一開始把 ISEP 的權限白名單(`docs/permissions-allow.json`:`ticket`/`mainline`/`gate-ok`/`gitea-pr-merge` 四個正門工具)寫進**這台機器**的 `~/.claude/settings.json`。只加不減、冪等;目標檔壞掉就不碰它、講一聲。**為什麼**:leo 09-07 親手加的四條只在本機,雲端沒有 ⇒ 同樣的動作被分類器擋(09-04 run log `permission_denials=6`);薄殼 repo 要 D20 開閘才推得動,所以清單住 ISEP 一份、兩台各自寫進自己的家目錄(inkstone/ISEP#130)。 | 📝 記錄(寫 settings) | +| `scripts/settings-allow-sync --quiet` | (不是閘,是腳本)對話一開始把 ISEP 的權限白名單(`docs/permissions-allow.json`:`ticket`/`mainline`/`gate-ok`/`gitea-pr-merge`/`stage`(inkstone/ISEP#137 起,雲端對 stage 寫入的正門)五個正門工具,外加 Arcrun 的 `stage-deploy-artifacts.py`)寫進**這台機器**的 `~/.claude/settings.json`。只加不減、冪等;目標檔壞掉就不碰它、講一聲。**為什麼**:leo 09-07 親手加的四條只在本機,雲端沒有 ⇒ 同樣的動作被分類器擋(09-04 run log `permission_denials=6`);薄殼 repo 要 D20 開閘才推得動,所以清單住 ISEP 一份、兩台各自寫進自己的家目錄(inkstone/ISEP#130)。 | 📝 記錄(寫 settings) | | `scripts/mainline refresh` | (不是閘,是腳本)對話一開始把**現在的主線**那條 milestone 的進度與期限更新一次,好讓每回合眼前那一行講的是今天的數字。拿不到 Gitea 就原封不動——**寧可資料舊,不要把主線弄丟**。跑一次就結束,不輪詢。 | 📝 記錄 | | `overdue-nag-guard.sh` | 對話一開始就去 Gitea 撈**沒有人會叫的事**:逾期的 milestone、掛著等你的票(標「等了幾天」)、標著「有人在做」卻好幾天沒動的票;撈完用白話講出來,**有事就發 Telegram 給你**。沒東西可報時它會說「查過了,沒有」——**安靜跟壞掉長得一模一樣**。發不出去時不會靜默:它會先問這個 session 的 `prod-write-guard` 會不會擋(舊版把「發通知」誤認成「部署」),擋就改貼回票上並把原文印在眼前。只在開 session 時跑一次,**不輪詢、不掛排程**。 | 📝 記錄(context 注入 + Telegram) | | `wiki-size-guard.sh` | 對話一開始講出「哪幾個 wiki 檔已經沒有人讀得完了」(預設超過 1200 行就點名)。**讀不完的必讀檔,跟沒有那個檔的差別只在於它讓人以為有。** 另一半掛在寫檔上,見 B 組。 | 📝 記錄(context 注入) | diff --git a/docs/permissions-allow.json b/docs/permissions-allow.json index 433272a..2c3acc9 100644 --- a/docs/permissions-allow.json +++ b/docs/permissions-allow.json @@ -4,12 +4,14 @@ "leo 09-07 親手把四個 Gitea 正門工具(ticket/mainline/gate-ok/gitea-pr-merge)加進本機 InkStoneCo/.claude/settings.json,auto mode 的分類器才不擋。", "雲端 session 沒有那四條,同樣的動作照樣被擋(09-04 progress-guard run log:permission_denials=6)。", "薄殼 repo(GitHub)要 D20 開閘才推得動 ⇒ 白名單住 ISEP,`scripts/settings-allow-sync` 開工時寫進那台機器的 ~/.claude/settings.json。", - "以後加規則只改這個檔、出一版,雲端下一趟就有。" + "以後加規則只改這個檔、出一版,雲端下一趟就有。", + "inkstone/ISEP#137:09-07 雲端對 stage 的三種寫入(`npx wrangler secret put`/`stage-deploy-artifacts.py`/curl POST 到 arcrun-yuga3bse)全被分類器擋——複合指令與任意 curl 對不上任何一條前綴。收成 `scripts/stage`(secret/api 兩個子命令,只寫 stage、值只從環境變數名讀)四種形狀,外加 Arcrun 自己那支 `scripts/stage-deploy-artifacts.py`(它已寫死只認 youlin 帳號 ID、不收 --account/--token)從 Arcrun repo 根目錄的相對形狀。不放 `Bash(npx *)`/`Bash(wrangler *)`/`Bash(curl *)`。" ], "_rules": [ "開頭的 `~` 由 settings-allow-sync 換成那台機器的家目錄(本機 /Users/…、雲端 /root),其餘一字不改。", "只放「正門工具」:每一條都要說得出它是哪支腳本的哪種呼叫形狀。不放 Bash(python3 *) 這種整類放行。", - "同一支工具幾種呼叫形狀就幾條(快取路徑/$CLAUDE_PLUGIN_ROOT/相對路徑)——權限規則是逐條完全比對,形狀差一個字就落在規則之外(scripts/gate-ok 檔頭記過這一課)。" + "同一支工具幾種呼叫形狀就幾條(快取路徑/$CLAUDE_PLUGIN_ROOT/相對路徑)——權限規則是逐條完全比對,形狀差一個字就落在規則之外(scripts/gate-ok 檔頭記過這一課)。", + "別的 repo 的正門工具也可以進來(例 Arcrun 的 `scripts/stage-deploy-artifacts.py`),條件一樣:那支腳本自己把「只打哪台」寫死在程式裡,本清單放的是它從那個 repo 根目錄跑的相對形狀。" ], "allow": [ "Bash(python3 ~/.claude/plugins/cache/inkstone/isep/*/scripts/ticket *)", @@ -24,6 +26,11 @@ "Bash(bash \"$CLAUDE_PLUGIN_ROOT/scripts/gate-ok\" *)", "Bash(bash \"$CLAUDE_PLUGIN_ROOT\"/scripts/gate-ok *)", "Bash(bash scripts/gate-ok *)", - "Bash(bash scripts/gitea-pr-merge.sh *)" + "Bash(bash scripts/gitea-pr-merge.sh *)", + "Bash(python3 ~/.claude/plugins/cache/inkstone/isep/*/scripts/stage *)", + "Bash(python3 \"$CLAUDE_PLUGIN_ROOT/scripts/stage\" *)", + "Bash(python3 \"$CLAUDE_PLUGIN_ROOT\"/scripts/stage *)", + "Bash(python3 scripts/stage *)", + "Bash(python3 scripts/stage-deploy-artifacts.py *)" ] } diff --git a/scripts/stage b/scripts/stage new file mode 100755 index 0000000..412e887 --- /dev/null +++ b/scripts/stage @@ -0,0 +1,410 @@ +#!/usr/bin/env python3 +"""stage — 雲端對 **stage(youlin)** 寫入的正門(inkstone/ISEP#137) + +━━ 為什麼有這支 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +09-07 三條主線工人在雲端對 stage 的寫入全被 Claude Code auto mode 的分類器擋(不是 ISEP 的閘): + ① `printf … | npx wrangler secret put CF_SECRETS_API_TOKEN --name arcrun-cypher-executor` + ② `python3 scripts/stage-deploy-artifacts.py all --confirm`(Arcrun 的成品直推,CF API 上傳) + ③ `curl -X POST …arcrun-yuga3bse.workers.dev/records`/`/graph/canonicalize-entities`/`/map/recompute` +權限白名單(docs/permissions-allow.json)是**逐條完全比對前綴**:複合指令(`cd … && printf … | VAR=… npx …`) +與任意寫法的 curl 永遠對不上任何一條;放整類 `Bash(npx *)`/`Bash(curl *)` 又是紅線。 +leo 09-07:「為什麼會擋你,這就是要修復的問題」「本機環境跟你不同應該不行」。 + +⇒ 每一種寫入收成**一支固定形狀**,白名單放它的四種呼叫形狀: + `stage secret …` = ①(打 api.cloudflare.com,不經 npx——雲端每次要重抓 wrangler, + 而且 wrangler 會讀 ~/.wrangler 的登入態:本機是 uncle6、雲端沒有,兩台行為不同) + `stage api …` = ③(用 curl 打 stage 實例;youlin 邊緣對 python-urllib UA 回 403 code 1010, + Arcrun#176 comment 6487——所以這一段**不用 urllib**) + ② = Arcrun 自己那支已經寫死只認 youlin 帳號 ID、憑證只從 + CLOUDFLARE_API_TOKEN_YOULIN_CC_USE 讀、不收 --account/--token; + 白名單直接放它的形狀(`python3 scripts/stage-deploy-artifacts.py *`),不重造一支。 + +━━ 只寫 stage,判準是唯一識別碼往放行方向比對,不是名字 ━━━━━━━━━━━━━━━━━━━━━━ +stage 隨便炸;出貨機(geek6688/uncle6)與 leo 的實例(leo21c)不是。 + secret:這把 token 打得到的帳號(GET /accounts 現查)裡要有 1129efd7df2e8899d537e9c8fbabb6cb, + 而且寫入的 URL 永遠只帶這個 ID。打不到 ⇒ 拒絕、印出它打得到誰、離開碼 2。 + api :URL 只會長在 `.arcrun-yuga3bse.workers.dev`(09-02 重裝後的 stage 子網域)。 + 給完整網址時主機必須等於它,否則拒絕、離開碼 2——不管那個主機叫 leo21c 還是別的什麼。 +不看變數名字裡有沒有 `leo21c`/`geek6688`——名字改了黑名單就漏,ID 與主機改不了。 + +━━ 值不進指令列 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +secret 的值、Bearer、任何標頭值都只從**環境變數名**讀(--from-env/--bearer-env/--header-env); +不落磁碟、不 echo、不進 git、不進 shell 歷史。curl 的標頭與 body 走 stdin 設定檔+管線,不走 argv。 + +━━ 用法 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + stage secret put --worker