昨日回顧
Day 16 將核心決策留在 SKILL.md,詳細規格放進 references/,可重複的機械步驟放進 scripts/。檔案拆開後,下一個問題不是「能不能壓縮成 ZIP」,而是別人拿到這一包,能否確認內容完整、安裝方式清楚,而且安裝後的行為與測試時相同。
今天以 ticket-source-guard 為例,做一份可重現的發行物。它只用中立的購票來源查核情境示範,不帶入私人資料或特定粉絲社群。
先定義發行物的邊界
一份 Skill 發行包至少需要入口、規格、執行檔、測試與授權說明。發行前先把「哪些檔案應該存在」列成清單,而不是把整個工作目錄直接打包。
ticket-source-guard/
├── SKILL.md
├── VERSION
├── MANIFEST.sha256
├── README.md
├── LICENSE
├── references/
│ ├── source-registry.md
│ └── eval-cases.md
├── scripts/
│ ├── normalize_host.py
│ └── classify_source.py
└── tests/
├── test_classifier.py
└── fixtures/
└── registry.example.json
這裡的檔名是示意,不是任何平台強制的封裝標準。若實際專案有其他必要檔案,應先補進明確的允許清單,再發行。反過來說,草稿、憑證、快取、測試輸出及本機設定,不應因為它們剛好在目錄裡就進包。
版本標示要對應行為變更
把 VERSION 寫成單一、可讀取的版本號,例如 0.3.0。不要只靠 ZIP 檔名或 Git 分支稱呼版本。版本規則可以很簡單,但必須對使用者有意義:
這是本專案的發版約定,不是保證所有 Skill 都使用同一套規則。尤其 Day 13 的 OFFICIAL、UNCONFIRMED、INSUFFICIENT_INPUT、CONFIGURATION_ERROR、CONFLICT,以及 Day 14 的 freshness 判斷,若語義改變,就不能偽裝成純文件修訂。README 應列出從上一版升級時需要改動的輸入、schema 或測試。
建立允許清單與雜湊清單
打包程式從明確的檔名清單讀取內容,先拒絕缺檔、符號連結、絕對路徑與 .. 路徑,再建立壓縮檔。下列範例只展示清單與雜湊的核心,不宣稱涵蓋完整的 ZIP 安全實作。
from hashlib import sha256
from pathlib import Path
root = Path("ticket-source-guard").resolve()
allowed = [
"SKILL.md", "VERSION", "README.md", "LICENSE",
"references/source-registry.md", "references/eval-cases.md",
"scripts/normalize_host.py", "scripts/classify_source.py",
"tests/test_classifier.py", "tests/fixtures/registry.example.json",
]
lines = []
for rel in sorted(allowed):
path = root / rel
if path.is_symlink() or not path.is_file() or path.resolve().is_relative_to(root) is False:
raise ValueError(f"unsafe or missing file: {rel}")
digest = sha256(path.read_bytes()).hexdigest()
lines.append(f"{digest} {rel}")
(root / "MANIFEST.sha256").write_text("\n".join(lines) + "\n", encoding="utf-8")
這段示意碼需要 Python 3.9 以上的 Path.is_relative_to。雜湊清單幫我們發現檔案被改過,卻不能單獨證明發行者身分:攻擊者若能同時改檔案與清單,兩者仍會一致。發行時還需要由可信管道公布版本、壓縮檔雜湊或簽章;驗證者必須先信任那個管道,不能從包內自行證明包的可信性。
可重現不只是一樣的原始檔
若要兩次建置產出相同的壓縮檔位元組,還要固定排序、壓縮參數、檔案權限與封存中的時間戳。否則 MANIFEST 相同,ZIP 雜湊仍可能不同。最基本的 CI 驗收是從同一個 commit 在乾淨環境建置兩次,比對兩個壓縮檔的 SHA-256;若不同,先檢查時間戳與工具版本,而不是把差異忽略掉。
不要把真實 registry 的私人備註、登入權杖或本機憑證放入 fixtures。示例來源紀錄應使用公開、可分享或完全虛構的資料,並在 README 標明它不代表即時官方來源判斷。
安裝後跑 smoke test
建置成功不等於安裝成功。把封包解壓到乾淨的暫存目錄,從那裡執行三組最小測試:
1. 結構:SKILL.md、references/、scripts/、VERSION 均存在;指針不越界。
2. 機械判斷:示例 registry 的有效、過期、衝突案例各回傳預期狀態。
3. 載入路徑:驗證網址的任務會讀來源規格與 classifier;
修改 schema 的任務會讀 schema 規格與測試,不會靠記憶猜欄位。
測試時要用封包內的檔案,不要偷偷引用開發機上的同名模組。如果 classifier 找不到 registry 或日期,應顯示明確錯誤,不可降級成 OFFICIAL。如果 smoke test 需要網路,請把它標成另一組整合測試;離線的基本測試應能在沒有帳號、cookie 或即時頁面的環境執行。
驗收與回退
發版前把結果記到一筆 release record:版本、來源 commit、建置工具版本、壓縮檔雜湊、MANIFEST 驗證結果、兩次建置比對結果、smoke test 結果和驗證時間。任一項失敗就停止發行,回到上個已驗證版本;不要用「大致可用」替代可追查的驗收。發行後若發現狀態判斷錯誤,保留舊版識別資訊,發布修正版與變更說明,而非悄悄替換同版本的檔案。
今天完成的驗收標準
[ ] 允許清單只包含要發行的檔案,無憑證、快取與私人資料
[ ] VERSION 與變更說明對應到實際行為
[ ] MANIFEST.sha256 覆蓋發行內容且驗證通過
[ ] 同一 commit 的兩次乾淨建置產出相同封包雜湊
[ ] 從安裝後目錄跑結構、判斷與載入路徑 smoke test
[ ] 缺資料、過期、衝突與 script failure 不會變成 OFFICIAL
[ ] release record 記下版本、來源、雜湊、測試與時間
明天預告
Day 18 把發行物交給陌生環境:設計安裝步驟、相容性檢查與失敗時的安全回退,避免「我這台可以跑」成為唯一驗收證據。