把四個雲端接線缺陷的診斷寫下來(inkstone/ISEP#90)
docs/governance/cloud-wiring.md:四個缺陷分成兩種病—— ①④ 是「閘的判準寫的是地端才成立的假設」,②③ 是「同一個東西有兩份,雲端跑到舊的」。 每一格都附實測證據與修法,並標明還沒關掉的兩格住在哪張票(ISEP#67/真身 repo)。 TESTING.md 補 A14/A15/A16 三格:怎麼跑、該看到什麼、什麼算失敗。 三支閘被擋下的訊息也一併指向 scripts/gate-ok(原本的手打法保留當等價寫法)。
This commit is contained in:
@@ -91,6 +91,63 @@ bash scripts/test-baton-handback-guard.sh
|
||||
- 「不該報」4 條任一紅 ⇒ 每條線收工都被念一次,警報會被學會忽略
|
||||
- 「票已關」那條紅 ⇒ 棒子已經到終點還在催,那是最典型的假警報
|
||||
|
||||
### A14 — 「總管可以放行」那道門真的打得開:16 條
|
||||
```
|
||||
bash hooks/tests/gate-ok.test.sh
|
||||
```
|
||||
**該看到**:`通過 16 條,失敗 0 條`。
|
||||
|
||||
**它在守什麼**(inkstone/ISEP#90 ④,2026-08-28 實查):三支閘
|
||||
(`prod-write-guard`/`main-and-prod-push-guard`/`stage-before-prod-guard`)
|
||||
都是「擋下來、但**總管看過就能放行**」的設計,而**既有測試只驗了擋得住,
|
||||
一條都沒驗過放得開**。於是這個 bug 活了很久:`stat -f %m` 在 GNU coreutils 上
|
||||
是「檔案系統資訊」,它一邊回非零一邊吐一整段文字 ⇒ 秒數被污染 ⇒
|
||||
**在 Linux(=每一個雲端 session)上戳記永遠不被接受** ⇒ 那三支閘在雲端等於純擋,
|
||||
而閘不會告訴你門是壞的。
|
||||
|
||||
**失敗**:
|
||||
- ②⑤(門打得開)紅 ⇒ 逃生口又焊死了,那三支閘在雲端變回純擋
|
||||
- ⑥⑦⑧(門沒變寬)任一紅 ⇒ **更嚴重**:綁 repo/有效期/空戳記那三條性質是
|
||||
08-11、08-12 兩次真的被穿透之後才補上的,不准為了「好放行」而鬆掉
|
||||
- ⑫ 紅 ⇒ `gate-ok` 在解不出 repo 時留下了一枚註定打不開的空戳記,
|
||||
那會讓人以為門開了(而空戳記本身就是 08-12 那把萬用鑰匙的形狀)
|
||||
|
||||
⚠️ 這支會真的寫 `/tmp` 的戳記檔(那是閘寫死的路徑)。**不要在「已經蓋好戳記正要推東西」
|
||||
的當下跑它**——它會把那枚戳記洗掉。
|
||||
|
||||
### A15 — 未推警察不會對雲端的工作分支亂叫:10 條
|
||||
```
|
||||
bash hooks/tests/unpushed-police.test.sh
|
||||
```
|
||||
**該看到**:`通過 10 條,失敗 0 條`。全離線(用本機 bare repo 當「遠端」)。
|
||||
|
||||
**它在守什麼**(inkstone/ISEP#90 ①):雲端 session 開出來的工作分支天生沒有 upstream,
|
||||
內容卻等於遠端 main ⇒ 舊判準「沒 upstream=從沒推過」讓**每個雲端 session、每次收工
|
||||
都被攔一次**(08-27 一個 session 五次全是誤報)。
|
||||
|
||||
**失敗**:
|
||||
- A 群(①—⑥,不該報)任一紅 ⇒ **誤攔**,比漏擋嚴重:永遠在響的警報=訓練人忽略它,
|
||||
下一條真的失蹤的分支會混在雜訊裡
|
||||
- B 群(⑦—⑩,該報)任一紅 ⇒ 為了不吵而改成放行了,那是把閘關掉不是修好
|
||||
- 特別看 ④:問不到遠端(離線)**不准當成「你沒推」**
|
||||
|
||||
### A16 — 信標會講出雲端會壞掉的那三件事:14 條
|
||||
```
|
||||
bash hooks/tests/isep-presence-beacon.test.sh
|
||||
```
|
||||
**該看到**:`通過 14 條,失敗 0 條`。全離線。
|
||||
|
||||
**它在守什麼**(inkstone/ISEP#90 ②③):信標原本只證明「有一份 plugin 載入了」,
|
||||
不證明「載入的是哪一份」——08-27 雲端載 0.3.9、main 0.9.0,差 7 個 release,
|
||||
而它照樣是綠的。
|
||||
|
||||
**失敗**:
|
||||
- ⑫⑬⑭(信標永遠在)任一紅 ⇒ **最嚴重**:那一行消失=leo 會判定這個 session 零閘
|
||||
- ③ 紅 ⇒ 版本落差報不出來,`ISEP#67` 那個病又變回看不見
|
||||
- ⑤⑦ 紅 ⇒ 舊複本遮蔽正門抓不到(`InkStoneCo/scripts/ticket` 那件)
|
||||
- ⑥ 紅 ⇒ **誤攔**:同步過的複本被念,人就學會忽略它
|
||||
- ⑪ 紅 ⇒ 內層迴圈變數撞名的回歸(同一個 `.claude` 下第二個殘骸會被靜靜跳過)
|
||||
|
||||
### A5 — 開票前的搜尋是跨 repo 的
|
||||
```
|
||||
python3 scripts/ticket where 標籤 模組化
|
||||
@@ -290,6 +347,9 @@ B2(信標那行)/B3(setup 輸出)/B4(閘的訊息)三個畫面
|
||||
| **A10 人閘警察準度** | 總管 | ✅ 9/9,連跑三次(2026-08-26),A 群誤攔 0 |
|
||||
| **A11 派工單只剩票號** | 總管 | ✅ 19/19(2026-08-27) |
|
||||
| **A12 留言身份欄(兩道門)** | 總管 | ✅ 11/11(2026-08-27) |
|
||||
| **A14 放行的門真的打得開** | 總管 | ✅ 16/16(2026-08-28,inkstone/ISEP#90) |
|
||||
| **A15 未推警察不誤攔雲端分支** | 總管 | ✅ 10/10(2026-08-28,inkstone/ISEP#90) |
|
||||
| **A16 信標會報雲端接線缺陷** | 總管 | ✅ 14/14(2026-08-28,inkstone/ISEP#90) |
|
||||
| A7 plugin 裝得起來 | 總管 | ✅ |
|
||||
| **A8 新 session 閘會觸發** | 總管 | 見本版 release note |
|
||||
| **B1–B5 雲端** | **leo** | 還沒跑(機器碰不到 Cloud environment) |
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# 雲端 session 的閘為什麼跟地端不一樣(inkstone/ISEP#90)
|
||||
|
||||
> leo 的目標一句話:**讓雲端 session 跟地端拿到同一組閘、同一組能用的工具。**
|
||||
|
||||
雲端不是「地端少幾支閘」,是**同一批閘在雲端的行為不一樣**。四個實例,
|
||||
每一個都穩定重現,而且每一個都**不會自己喊痛**——這才是它們活這麼久的原因。
|
||||
|
||||
---
|
||||
|
||||
## 四個缺陷,兩種病
|
||||
|
||||
| | 缺陷 | 病 |
|
||||
|---|---|---|
|
||||
| ① | 未推警察每個雲端 session 都誤攔 | 判準在雲端**不成立** |
|
||||
| ② | `scripts/ticket` 在雲端拿不到 token | 跑到的是**另一份**(舊複本) |
|
||||
| ③ | 已刪除的機制在雲端重生 | 載到的是**另一版**(0.3.9 vs 0.9.0) |
|
||||
| ④ | 總管的「我確認過了」出口打不開 | 判準在雲端**不成立**(平台差異) |
|
||||
|
||||
①④ 是同一種病:**閘的判準寫的是地端才成立的假設**。
|
||||
②③ 是同一種病:**同一個東西有兩份,而雲端跑到的是舊的那份**。
|
||||
|
||||
---
|
||||
|
||||
## ① 未推警察:判準是「有沒有 upstream」,而雲端的分支天生沒有
|
||||
|
||||
雲端 session 開出來的工作分支沒有 upstream,**內容卻等於遠端 main**:
|
||||
|
||||
```
|
||||
薄殼 HEAD = 081c547d90dc2d80692485af083ca1f71f2094f2
|
||||
GitHub 遠端 main = 081c547d90dc2d80692485af083ca1f71f2094f2 ← 同一顆
|
||||
```
|
||||
|
||||
⇒ 每個雲端 session、每次收工都被攔一次(2026-08-27 一個 session 五次全是誤報)。
|
||||
|
||||
**修法**:判準改成「**遠端有沒有這顆 commit**」(`git ls-remote`)。
|
||||
本機的 remote-tracking ref 只答得準「有」——08-28 實測那份 `origin/main` 落後遠端 4 天
|
||||
——所以答「沒有」的時候才打網路。三態:有/沒有/**問不到**;問不到一律不報。
|
||||
|
||||
**順手補的**:`$CLAUDE_PROJECT_DIR` 在雲端是**薄殼根**,真身在 `$TOP/InkStoneCo/`。
|
||||
舊的掃描清單四個路徑在雲端一個都不存在 ⇒ **真身有東西沒推,這支閘一輩子不會知道。**
|
||||
同一支閘在雲端既亂叫、又看不到該看的地方。
|
||||
|
||||
---
|
||||
|
||||
## ② `scripts/ticket`:跑到的是舊複本,不是 plugin 那一份
|
||||
|
||||
- ISEP 的 `scripts/ticket` **早在 2026-08-20 就修好了**(掃所有 remote + 環境變數 fallback)
|
||||
- 但雲端 cwd 是真身,那裡有一份 `InkStoneCo/scripts/ticket` 的**舊複本**,
|
||||
取 token 邏輯還停在「只認名叫 `gitea` 的 remote」,而 `bootstrap.sh` 把 Gitea 設成 `origin`
|
||||
- ⇒ 雲端一律死在「拿不到 gitea token」
|
||||
|
||||
🔴 **後果比「一支腳本壞了」嚴重**:`scripts/ticket` 是開票/留言的**正門**,
|
||||
它一壞,人就繞過去直接打 Gitea API——**而那正是 `ticket-api-bypass-guard.sh` 在防的事**。
|
||||
**一道閘把人逼去走它自己禁止的那條路,那道閘就是在製造違規。**
|
||||
|
||||
**ISEP 這半的修法**:信標在 session 開頭就點名「專案裡有 ISEP 腳本的舊複本,
|
||||
而且**內容不同**」。判準不是檔名一樣,是**檔名一樣而內容不同**——同步過的複本不吵。
|
||||
**真身那半(把那份複本同步或刪掉)不在 ISEP,要在 `inkstone/InkStoneCo` 修。**
|
||||
|
||||
---
|
||||
|
||||
## ③ 已刪除的機制在雲端重生
|
||||
|
||||
```
|
||||
ISEP main .claude-plugin/plugin.json → 0.9.0
|
||||
雲端實際載入 → 0.3.9 ← 差 7 個 release
|
||||
```
|
||||
|
||||
0.3.9 裡還活著兩支在 v0.9.0 已整支刪除的 hook ⇒ `.claude/pending-verification/`
|
||||
被清掉之後又長回來。**一個看不見的版本落差,會讓已經刪掉的機制在別人的工作區裡復活。**
|
||||
|
||||
**傳輸那半是 `inkstone/ISEP#67`**(新版到不到得了手上),本票不重複那件。
|
||||
**ISEP 這半能做的是讓它不再看不見**:信標匿名讀 ISEP main 的 `plugin.json`
|
||||
(D20 判準下屬於「讀」,不需開閘),落後就講清楚差幾版、怎麼重拍快照;
|
||||
同時掃 `.claude/` 底下**這一份 plugin 的 hooks/scripts 一個字都沒提到**的目錄,
|
||||
點名它們是殘骸。判準是「plugin 現在還認不認得它」,**不是關鍵字黑名單**
|
||||
(leo 2026-08-17 已證明那條路 8 次誤攔、0 次正確攔截)。
|
||||
|
||||
---
|
||||
|
||||
## ④ 「總管可以放行」的門,在雲端是焊死的
|
||||
|
||||
三支閘(`prod-write-guard`/`main-and-prod-push-guard`/`stage-before-prod-guard`)
|
||||
都是「擋下來、但**總管看過就能放行**」。它們判斷戳記新不新都用同一行:
|
||||
|
||||
```sh
|
||||
MT=$(stat -f %m "$STAMP" 2>/dev/null || stat -c %Y "$STAMP" 2>/dev/null || echo 0)
|
||||
```
|
||||
|
||||
macOS(BSD stat)上 `-f %m` 就是 mtime,對的。
|
||||
**GNU coreutils 的 `-f` 是「顯示檔案系統資訊」**,而且它**一邊回非零、一邊往 stdout
|
||||
吐一整段區塊**:
|
||||
|
||||
```
|
||||
$ stat -f %m /tmp/.probe
|
||||
stat: cannot read file system information for '%m': No such file or directory ← stderr
|
||||
File: "/tmp/.probe" ← stdout
|
||||
ID: 0 Namelen: 255 Type: ext2/ext3
|
||||
```
|
||||
|
||||
⇒ `2>/dev/null` 吃掉錯誤訊息、`||` 把正確的秒數接在那堆垃圾**後面**
|
||||
⇒ `case "$NOW$MT" in *[!0-9]*) return 1` 必然命中
|
||||
⇒ **在 Linux(=每一個雲端 session)上,那三支閘的戳記永遠不會被接受。**
|
||||
|
||||
🔴 **後果**:那三支閘在雲端**等於純擋**。總管照著閘自己印的指示做,
|
||||
做幾次都打不開,**而閘不會告訴他門是壞的**。
|
||||
|
||||
**為什麼活這麼久**:既有的三支測試(29/16/10 條)**只驗了「擋得住」,
|
||||
一條都沒驗過「放得開」**。⇒ 這正是「閘的另外一半從來沒被測過」的代價。
|
||||
|
||||
**修法**:`hooks/lib/mtime.sh` —— 先 `-c %Y`(GNU)再 `-f %m`(BSD),
|
||||
**每一步都驗它是不是純數字**(這個 bug 的成因正是「命令失敗了卻還是印了東西」,
|
||||
只看離開碼會再被騙一次)。
|
||||
|
||||
### 附帶:逃生口收斂成一個入口 `scripts/gate-ok`
|
||||
|
||||
原本每支閘的門長得都不一樣,而且藏在被擋下的那則訊息裡:
|
||||
|
||||
```
|
||||
touch /tmp/.prod-write-ok
|
||||
git rev-parse --show-toplevel > /tmp/.main-push-ok
|
||||
touch /tmp/.solo-ok-<session_id>
|
||||
…
|
||||
```
|
||||
|
||||
兩個後果:**記不住**(抄錯一個字門就打不開),以及**沒有穩定形狀可以事先放行**
|
||||
——`.claude/settings.json` 的 allow 只能逐條完全比對(現場真的寫著
|
||||
`Bash(touch /tmp/.prod-write-ok)` 這種一行),多一個 `&&`、換一個 session id
|
||||
就落在規則之外,然後由權限層自己判斷。
|
||||
|
||||
⇒ `bash "$CLAUDE_PLUGIN_ROOT/scripts/gate-ok" <閘名> [參數]`:**一個名字、一種形狀**,
|
||||
一條前綴規則涵蓋全部,以後新增閘不必再動一次設定。
|
||||
|
||||
🔴 **它沒有弱化任何一道閘**:蓋的是同一個檔、同一種語意——單次用完即丟、綁 repo、
|
||||
綁 session、有效期全部沒動。換掉的只有「怎麼蓋」。
|
||||
`hooks/tests/gate-ok.test.sh` 的 ⑥⑦⑧ 三條就是在守這件事(那三條性質是 08-11、08-12
|
||||
兩次真的被穿透之後才補上的)。
|
||||
|
||||
---
|
||||
|
||||
## 還沒關掉的那兩格(不屬於 ISEP)
|
||||
|
||||
| 缺口 | 住在哪 |
|
||||
|---|---|
|
||||
| 雲端載到的版本追上 ISEP main | `inkstone/ISEP#67` |
|
||||
| `InkStoneCo/scripts/ticket` 這份舊複本 | `inkstone/InkStoneCo`(真身) |
|
||||
|
||||
ISEP 這一側能做的是**讓它們不再是看不見的**:兩件現在都會在 session 開頭被信標點名。
|
||||
@@ -273,7 +273,9 @@ leo 原話:
|
||||
逐筆看過那些 commit(`git log --oneline gitea/main..<branch>`、`git diff --stat`),
|
||||
確定它們該進 main,再:
|
||||
|
||||
git rev-parse --show-toplevel > /tmp/.main-push-ok && <你的 git push 指令>
|
||||
bash "$CLAUDE_PLUGIN_ROOT/scripts/gate-ok" main-push <那個 repo 的路徑>
|
||||
<你的 git push 指令>
|
||||
(等價的手打法:git rev-parse --show-toplevel > /tmp/.main-push-ok)
|
||||
|
||||
戳記 **綁這個 repo、只能用一次、15 分鐘失效**——它代表「**這一次、這個 repo,我看過了**」。
|
||||
|
||||
|
||||
@@ -104,7 +104,9 @@ leo 原話:
|
||||
━━━ 你是總管 ━━━
|
||||
看清楚它會覆蓋什麼(`arcrun_get_workflow` 先撈一份現況存起來,好回滾),確定要推,再:
|
||||
|
||||
touch /tmp/.prod-write-ok # ⚠️ 只放行「下一個」動作,用完即消失
|
||||
bash "$CLAUDE_PLUGIN_ROOT/scripts/gate-ok" prod-write # ⚠️ 只放行「下一個」動作,用完即消失
|
||||
(等價的手打法:touch /tmp/.prod-write-ok。gate-ok 是所有閘共用的同一個入口,
|
||||
好處是它的形狀固定 ⇒ 一條規則就能事先放行,不會每次被權限層擋在門外)
|
||||
|
||||
戳記 15 分鐘有效。**若這一推會讓封測者/用戶當場拿到東西,那就不是你的層級——要 leo 解保險。**
|
||||
MSG
|
||||
@@ -268,7 +270,9 @@ leo 原話:
|
||||
━━━ 你是總管 ━━━
|
||||
確認過要推什麼、會蓋掉什麼之後:
|
||||
|
||||
touch /tmp/.prod-write-ok # ⚠️ 只放行「下一個」動作,用完即消失
|
||||
bash "$CLAUDE_PLUGIN_ROOT/scripts/gate-ok" prod-write # ⚠️ 只放行「下一個」動作,用完即消失
|
||||
(等價的手打法:touch /tmp/.prod-write-ok。gate-ok 是所有閘共用的同一個入口,
|
||||
好處是它的形狀固定 ⇒ 一條規則就能事先放行,不會每次被權限層擋在門外)
|
||||
|
||||
⚠️ `acr recipe push` 另有一道**互動式暴露同意閘**(終端機裡要人親手輸入資源名)。
|
||||
那道閘擋的是「把資源變成可被外部呼叫」,**本閘的戳記蓋不過它**——那是 leo 的手。
|
||||
|
||||
@@ -208,7 +208,8 @@ cat >&2 <<'EOF'
|
||||
⚠️ 身分:leo 2026-07-25 令「測試一律用 youlin,別拿 leo21c 當探針(會製造假信號)」
|
||||
⇒ 部署前先 `acr whoami` 確認身分。
|
||||
|
||||
驗過了 ⇒ `touch /tmp/.stage-verified` 後重送,並在回覆裡**貼 stage 的實測輸出**
|
||||
驗過了 ⇒ `bash "$CLAUDE_PLUGIN_ROOT/scripts/gate-ok" stage-verified` 後重送
|
||||
(等價:`touch /tmp/.stage-verified`),並在回覆裡**貼 stage 的實測輸出**
|
||||
(「我測過了」不算——貼指令與它吐出來的東西)。
|
||||
EOF
|
||||
exit 2
|
||||
|
||||
Reference in New Issue
Block a user