Files
ISEP/docs/governance/cloud-wiring.md
claude-code 4322deb23a 補上 stat -f 診斷的最後一層:連離開碼都不可靠(inkstone/ISEP#90 ④)
總管自己驗這一格量到 exit=0,我量到 exit=1。**兩個都是真的**,
而分歧本身就是最後一塊拼圖:

**GNU 的 `-f` 是 `--file-system`,布林旗標、不接格式字串**
⇒ `%m` 不是格式,它被當成**另一個檔名運算元**
⇒ 離開碼取決於「cwd 裡有沒有一個叫 `%m` 的檔」:

    A. 沒有(一般情況)  → exit 1 ⇒ `||` 會跑   ⇒ 正確的秒數接在垃圾後面
    B. 剛好有            → exit 0 ⇒ `||` 不會跑 ⇒ 整包連一個數字都沒有

兩種情況閘的結果一樣:MT 都不是純數字、戳記都作廢。(實測 GNU coreutils 9.4,兩種都重現過)

🔴 教訓比原本寫的更尖銳:舊寫法的 `||` fallback 救不了,
**不是因為它沒跑,而是因為「跑不跑」根本不由這支腳本決定**——
它由「cwd 裡有沒有某個檔名」決定。
一個行為取決於 cwd 有沒有某個檔的判斷式,不管跑不跑都是壞的。
⇒ 所以修法不能只是「把順序反過來」,**每一步都要驗它是不是純數字**
(lib/mtime.sh 本來就是這樣寫的,現在把理由寫進去了)。

gate-ok 測試補第 ⑰ 條守這一層:cwd 裡有一個叫 `%m` 的檔時,file_mtime 仍要回純數字。
16 → 17 條,TESTING.md 的 A16 一併更新。
2026-08-28 00:27:51 +00:00

196 lines
9.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 雲端 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)
```
macOSBSD 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)上,那三支閘的戳記永遠不會被接受。**
### 再往下一層:`-f` 根本不吃格式參數,所以連離開碼都不可靠
總管 2026-08-28 自己驗這一格時量到 `exit=0`,而我量到 `exit=1`
**兩個都是真的**,而分歧本身就是這個 bug 最後一塊拼圖:
**GNU 的 `-f` 是 `--file-system`,它是布林旗標、不接格式字串**
`%m` 不是格式,它被當成**另一個檔名運算元** ⇒ 離開碼取決於
「當前目錄裡有沒有一個叫 `%m` 的檔」:
```
$ cd /tmp/statprobe && touch .mt
# A. 沒有名為 %m 的檔(一般情況)
$ stat -f %m .mt ; echo "exit=$?"
stat: cannot read file system information for '%m': No such file or directory
File: ".mt" … ← 檔案系統資訊照樣印到 stdout
exit=1 ← `||` **會**跑 ⇒ 正確的秒數接在那堆垃圾後面
# B. 剛好有一個叫 %m 的檔
$ touch '%m' && stat -f %m .mt ; echo "exit=$?"
exit=0 ← `||` **不會**跑 ⇒ 整包連一個數字都沒有
```
(實測環境:`stat (GNU coreutils) 9.4`
**兩種情況下閘的結果一模一樣**——`MT` 都不是純數字,
`case "$NOW$MT" in *[!0-9]*) return 1` 都必然命中,戳記都作廢:
```
情況 A:非數字 ⇒ return 1 ⇒ 戳記作廢
情況 B:非數字 ⇒ return 1 ⇒ 戳記作廢
```
🔴 **這一層才是真正該記住的教訓**:舊寫法的 `||` fallback 之所以救不了,
**不是因為它沒跑,而是因為「跑不跑」根本不由這支腳本決定**
——它由「當前目錄裡有沒有一個叫 `%m` 的檔」決定。
一個**行為取決於 cwd 裡有沒有某個檔名**的判斷式,不管跑不跑都是壞的。
⇒ 所以 `lib/mtime.sh` 的修法不是「把順序反過來」而已,是
**每一步都驗它是不是純數字**:這個 bug 的成因正是「命令失敗了卻還是印了東西」,
**只看離開碼會再被騙一次**
📌 這一格的實害(總管 2026-08-28 原話):「我今天為了發一則通知,
用了兩種方式蓋 `prod-write-ok` 都無效,一度以為是權限問題。」
**閘不會告訴你門是壞的**,所以人會往錯的方向查(權限、classifier、設定),
而根因在閘自己身上。
🔴 **後果**:那三支閘在雲端**等於純擋**。總管照著閘自己印的指示做,
做幾次都打不開,**而閘不會告訴他門是壞的**。
**為什麼活這麼久**:既有的三支測試(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 開頭被信標點名。