Skip to content

v8.0.0 - agent 平台契約、證據鏈與安全邊界完成

Choose a tag to compare

@CarlLee1983 CarlLee1983 released this 05 Sep 15:40
· 207 commits to main since this release
d1f1173

ForgeFlow Story DBCLI-001 到 DBCLI-012 全數在 v7.0.1(224ee59d)之後交付;該 tag
早於 Story 的合併點 04a88a44,因此以下每一條都在 7.0.1 之外。DBCLI-013 補上
採用漂移的檢查與這份紀錄本身。

版本為 8.0.0。理由不是變更量,而是三處會拒絕先前被接受的輸入、以及一處會
改變 JSON 判決值——SemVer 對這四項的答案都是 MAJOR,詳見下方 Breaking。

Added

  • dbcli capabilities 與 dbcli capabilities check:外部 Skill 現在問得到「這個
    工具能做什麼」。
    Catalog 是靜態、有自己的 schemaVersion、依 id 排序,同一個
    build 兩次輸出 byte-identical;check --require <ids> 只讀本機設定的 engine 與
    permission。兩者都不建立資料庫連線,也不動檔案系統,這一點由 import graph 的
    contract test 結構性地保證,不是靠註解宣稱。純型別與純函式從
    @carllee1983/dbcli/core 匯出。

    Capability 描述的是工具的原子能力(schema.read、data.delete),不是職務或
    方法——後者屬於組合 dbcli 的 Role Skill 與 Method Skill。每一項 engine 與 risk
    宣稱都由既有的 ENGINE_CAPABILITIES 推導,不另立第二張表;contract test 雙向
    檢查 catalog 與實際 Commander tree 不會漂移。

    Fail closed 是預設:不認得的 id 回 unknown 而不會被解析成長得像的那一個,缺少
    設定回 unavailable + context-unavailable 而不是拿 DEFAULT_CONFIG 的
    localhost PostgreSQL 充數。Exit code 0 / 1 / 2。

    DBCLI_AGENT_MODE=1 是 context 的一部分,不是免責聲明能蓋過去的東西。它無條件
    拒絕所有設定、權限與憑證變更,而且不連線就完全可知——差別在拒絕是何時決定的:
    blacklist 與人類同意在執行當下決定,契約無從代言;agent mode 在這裡就決定了。
    connection.select、connection.init、blacklist.manage 因此在 agent mode 下
    回 unavailable + agent-mode,不管權限寫的是不是 admin。

    「沒有設定」與「有設定但解不開」是兩件事。{"$env":...} 密碼指向未設定變數的
    設定檔是存在且可讀的,回報「找不到設定」是對使用者機器說假話——context-unresolvable
    因此獨立成一個原因,且不外洩錯誤訊息裡的檔案路徑。

    v1 涵蓋 engine capability matrix 管轄的 34 個 command key;另外 16 個指令
    (explain、plan、impact、assert、verify、evidence 等)不在其中,
    因為替它們寫 engine 支援度等於憑讀碼捏造未經稽核的宣稱。它們回 unknown,
    擴充 matrix 是後續 Story。(DBCLI-PLAT-001,ADR-0022)

  • Agent output 成為可組合的穩定契約。 --agent-output 提供嚴格、限界且遮蔽過的
    Operation Envelope;--correlation-id 能把跨命令結果與 audit metadata 串在一起,
    同時保留原有 human 與 JSON 輸出。(DBCLI-PLAT-004、005、006)

  • 外部 agent workflow 能在執行前驗證能力並留下證據。 inspect、report、
    schema、plan、lint、explain 與 impact assess 可選擇寫出 bounded receipt;
    Task Pack 的 safety.requires 會對 catalog 與本機 context 做 fail-closed preflight。
    (DBCLI-PLAT-007、008)

  • 套件新增 Skill Author Integration Kit 與外部 consumer contracts。 可複製的
    Bun/TypeScript fixture 只使用公開 package surface,示範 discovery、schema pin、
    envelope、correlation 與 evidence;CRUD、CQRS、DBA contract tests 覆蓋成功 preflight
    與 query-only permission refusal。(DBCLI-PLAT-009、010)

Added

  • Capability catalog 涵蓋所有公開指令,從 34 條增為 53 條。 PLAT-001 刻意把
    十六個指令留在 catalog 之外——explain、plan、impact、assert、snapshot、
    verify、verification、evidence、contract、semantic、design、proxy、
    recovery、backfill、password,以及 capabilities 自己——理由是替沒有稽核過
    的指令寫 engine 支援度,正是這份契約要防的那種捏造。

    這次每一條宣稱都是從實作讀出來的,證據(file:line)逐條寫在 Story 裡。程式碼
    會直接拒絕非 SQL 連線的六個指令標成 unsupported,並指名那道 gate;只有某一個
    模式 需要 SQL 的(impact assess 與 design 的 --against-cache、semantic
    的 draft validate)標成 limited,因為指令其餘部分照跑;完全不碰資料庫的標成
    not-applicable。判定不了的一律 unsupported,不新增第三種狀態——一個「我們沒查」
    的值,呼叫端拿到也不能做任何事,而且會永遠留在那裡沒人查。

    那三條 limited 是第一版寫錯、被第二個讀者抓回來的:grep 到 engine check 就停手,
    少讀了一層 caller——那道 check 擋的是模式,不是指令。寫成 unsupported 會對著
    MongoDB 上的 agent 說一個它其實跑得動的指令不能用;fail-closed,所以永遠不會看起來
    像錯的,只會讓契約悄悄比工具本身沒用。

    capabilities 自己也在 catalog 裡(capability.discover、capability.check),
    決定寫在 ADR-0023:live 讀的時候它確實是個常數,但 catalog 本來就是設計成可以被
    pin 住的——一個帶著舊 catalog 的 Skill,對著沒有這個指令的 dbcli 問
    --require capability.check 會拿到 unknown-capability,那正是它需要的訊號。
    (DBCLI-PLAT-011)

  • supportsEvidence 不再全部是 false。 它原本是空集合,因為會寫 receipt 的
    assert、verify、evidence 都不在 v1 catalog 裡。現在由 contract test 從指令層
    的實際 writer 呼叫雙向推導——「import graph 碰得到 evidence 模組」不等於「會寫」,
    insert、query 等十幾個指令只是遞移拉到型別而已。順帶補上 recover:它一直在
    catalog 裡,也一直能用 --write-verification-artifact 寫出 artifact。
    (DBCLI-PLAT-011)

Changed

  • ORM artifact 的讀取搬進 src/core/orm-drift/input.ts。 這段程式碼的註解本來就
    寫著「never opens a database connection or reads dbcli configuration」,但它住在
    src/commands/diff.ts 裡,於是 impact assess 與 design 只要 import 它,就會
    把 diff 的 adapter import 拖進自己的 static graph——contract test 因此拒絕它們
    requiresConnection: false 的宣稱。行為一個字都沒改,diff 原樣 re-export,
    差別是那兩個指令的離線宣稱現在證明得出來,而不是被豁免。(DBCLI-PLAT-011)

  • contract test 不再假設指令名等於檔名。 commands/<name>.ts 對 password
    (credential.ts)與 contract(contracts.ts)都找不到檔案,而找不到時它是
    「跳過」——於是「這個指令不會寫設定」對著那個專門用來改憑證的指令回了 true。
    對照表改從 program-lazy.ts 的 lazy loader 讀,找不到模組是失敗而不是跳過。
    (DBCLI-PLAT-011)

Fixed

  • 要求 receipt 的失敗命令現在也會留下 outcome: failed。 inspect、report、
    schema、plan、lint 與 explain 原本會在 Commander postAction 前 exit 或 throw,
    因而跳過 receipt;schema 的既有快取/未加 --force 成功路徑也會提早 exit。
    這些路徑現在保留原本的輸出與 exit code,再完成 bounded receipt;任何 receipt 寫入
    失敗仍只回報 Failed to write evidence receipt。(DBCLI-PLAT-007、#157)

  • Schema cache 的寫入不再是一次完整設定的重新發布。 dbcli schema 原本透過
    configModule.write(v1)與 writeV2Config(v2)存快取,兩者都在
    assertConfigMutationApproved() 後面,所以 DBCLI_AGENT_MODE=1 下
    capabilities check 回報 schema.read 可用、指令本身卻 exit 1,訊息還說的是
    「configuration, permission, and credential changes」——只讀 exit code 的 agent
    只能推論成資料庫讀取失敗,而讀取其實成功了。

    移動時量到一個沒有人記錄過的缺陷:v1 路徑會把 connection.password 從
    config.json 刪掉、並整份覆寫 .env.local。這件事在 agent mode 之外也會發生,
    也就是每一次 dbcli schema 都在搬憑證,而那裡根本沒有任何 guard 會攔。

    新的 src/core/schema-cache-persistence.ts 只寫快取欄位,而且寫不出別的東西:
    它的參數只有 schema、connection slot 與兩個時間戳,設定是它自己從磁碟讀的,
    沒有任何參數能夠承載憑證或權限。窄化在簽章,不在旗標——一個「這次寫入沒問題」的
    布林參數等於把邊界交給呼叫端的誠實。assertOnlyCacheFieldsChanged 把同一句保證
    講出來,任何擴大寫入範圍的修改會在單元測試指名欄位而失敗。

    assertConfigMutationApproved() 一個字都沒改,六個 guarded writer 一個都沒少,
    並由 tests/contract/config-mutation-boundary.test.ts 以名冊釘住。agent mode 下
    init、use、blacklist add 與憑證指令照樣被拒。(DBCLI-PLAT-012)

  • 快取寫入失敗現在說自己是快取寫入失敗。 訊息明講 schema 已成功讀出、失敗的是
    存進本機快取那一步,並且是 bounded 的:只有 SchemaCacheWriteError 會原樣輸出
    (它的訊息在建構上就不含路徑),其他成因一律分類而不引用,因為完整性拒絕會帶上
    它檢查的設定檔路徑。(DBCLI-PLAT-012)

Breaking

  • query-only 的最後一道界線改由資料庫提供。 PostgreSQL / MySQL / MariaDB 上
    每一句 caller-controlled SQL 都在該實體連線上開一個原生唯讀交易後才送出,涵蓋
    query、export、saved query 及其 verification、report 診斷、SQL shell、
    analyzed explain,以及會把較高權限收斂成 query-only 的多連線 fan-out。語句分類
    只看得到語句形狀,看不到 SELECT mutate_accounts() 背後的函式會不會寫資料——
    這類先前成功的呼叫現在會被資料庫本身擋下。邊界建立不起來就在送出目標語句前失敗;
    目標語句跑完但交易收尾失敗時回報結果不確定並丟棄該連線。(DBCLI-002)

  • slow-endpoint-investigation task pack 新增必填的 table 參數。 計畫順序變成
    blacklist → proxy analyze → schema <table> → explain → guide
    missing-index-for。先前不帶 table 的呼叫現在會在產出任何計畫之前失敗。在沒確認
    資料表形狀的情況下讀執行計畫或索引候選,那是猜測,不是佐證。(DBCLI-012)

  • 儀表板內嵌 payload 改為 allowlist。 SavedQueryMeta 先前是整包序列化進獨立
    HTML 的,把參數預設值與列舉、目標 index / collection、verification 的 query 與
    expects 一起帶進一個從未渲染它們的檔案。現在只有已顯示的資料列、applied-limit
    與安全通知、provenance,以及顯示用的 name / description 與引用已顯示欄位的圖表
    /KPI 定義會被序列化。讀取那些欄位的下游消費者會發現它們不再存在。(DBCLI-006)

  • 語意契約中形式錯誤的 subject 判為 invalid 而非 stale。 table:orders
    這類形式本身就不合法的 subject 先前與「model 被改名」得到同一個判決,
    contract drift --format json 的消費者因此無法區分兩者。形式檢查移到 parse
    階段。(DBCLI-011)

  • init --no-interactive 從無效變成有效。 它先前讀的是
    options.noInteractive,而 Commander 存的是 interactive,所以那個旗標從來沒
    擋掉任何提問。依賴舊行為(帶著 --no-interactive 卻仍期待被提問)的腳本會改變
    行為。(DBCLI-004)

  • 互動式 init 的遮蔽輸入沒有純文字退路。 沒有 TTY 或載入不到遮蔽實作時,
    直接在寫出任何設定之前失敗,並指出當下模式支援的替代輸入(--password、
    .env / 環境變數、--use-env-refs,MongoDB 則是 --uri)。先前這些情境會以
    可見提示收下密碼。(DBCLI-004)

  • assert 在 receipt 寫入失敗時的 stdout 形狀改變。 先前是一行 stderr 且沒有
    envelope,現在 --format json 的呼叫端拿得到完整 envelope,exit code 仍為 1。
    解析舊有失敗輸出的呼叫端需要跟著改。(DBCLI-009)

Added

  • skill context --context-version 2:給外部 agent 的穩定契約。 離線、不開
    連線、不掃 Redis key、不讀文件、不讀專案原始碼。輸出引擎、設定權限、能力、可見
    資源、snippet metadata、semantic 與 contracts、truncation 與 gaps;不輸出憑證、
    實際值、預設值與計數、原始 ES mapping、Redis key/value 或專案路徑。Elasticsearch
    只以快取的扁平欄位呈現,Redis 只接受 repository 自己宣告的
    dbcli.redis-context.json。缺證據時明確回報 gap,不推測名稱與語意。不帶旗標時
    維持 byte-compatible 的 version 1 輸出。(DBCLI-003)

  • saved-query 儀表板帶上可攜、封閉的執行溯源。 version 1 的 provenance 物件與
    對應的 Execution Traceability 區塊:邏輯連線名稱與引擎、snippet key 與來源分類、
    實際生效的權限,以及實際生效的筆數上限——沒有套上限時明講 not-applied,而不是
    留白讓人分不出完整結果與被截斷的結果。applied-limit 與畫面上的截斷警示不一致就
    在寫檔前拒絕。直接查詢的儀表板行為不變,也不帶溯源區塊。(DBCLI-006)

  • impact assess 的不完整性契約寫進兩份 index.html。 coverage 永遠是
    declared 或 partial、v1 不宣稱 complete、--fail-on 只在報告寫出後改變
    exit code。(DBCLI-008)

Security

  • 互動式 init 的憑證改走遮蔽輸入。 PostgreSQL / MySQL / MariaDB / Redis /
    Elasticsearch 的密碼、MongoDB 逐欄模式的密碼,以及貼上的 MongoDB 連線字串(它
    本身就帶著帳密),先前都停在終端機捲動紀錄、session 錄影與螢幕分享畫面裡。連線
    測試失敗時 driver 常把密碼或整條 URI 原樣寫回錯誤訊息,現在先經過共用的
    redactSecretsForDisplay 遮蔽並截斷再輸出。(DBCLI-004)

  • safe-backfill 的佐證文物不再外洩敏感字串。 寫進佐證文物的失敗原因改用固定的
    安全字串而非 driver 的錯誤訊息;使用者自訂的 subject / summary 與 SQL 佐證會先把
    憑證、檔案系統路徑與 SQL 註解遮掉再落檔。(DBCLI-005)

  • evidence pack 的失敗輸出不再帶出絕對路徑與原始錯誤。
    evidence compose --receipt <不存在> 先前會原樣印出
    ENOENT: ... lstat '/private/var/.../x.json'。receipt 的 realpath 失敗改拋有界
    訊息;safeMessage 只放行兩個 validation error 類別;validateFormat 換成自己的
    有界檢查——它先前會把被拒的值原樣引用,而那個位置可以是路徑。(DBCLI-010)

  • 語意契約的診斷不再複述產物輸入。 rejectUnknownKeys 先前把被拒絕的 JSON
    property key 內插進診斷路徑,而那個 key 由產物作者控制:contract validate 的
    stderr 與 contract drift --format json 會原樣印出帶憑證與絕對路徑的 key。改為
    列出允許的 property 名稱。fail() 與 collectContractEvidence 的邊界一併收
    緊。(DBCLI-011)

Fixed

  • safe-backfill 的四項防護全數執行。 先前一失敗就中斷,操作者只看得到第一個
    擋下來的原因。blacklist、schema、plan、verify-query-readonly 現在都會執行並各自
    回報結果;after-write 仍只在四項全過時才跑回讀斷言。(DBCLI-005)

  • assert 的 receipt 寫入失敗不再吞掉斷言判決。 先前直接 process.exit(1),
    --format json 的呼叫端拿到的不是 envelope 而是一行 stderr,--no-fail 的語意
    也被推翻。改成錯誤記在結果旁邊、判決照常輸出、exit code 仍為 1——與同一個檔案裡
    相鄰的 artifact 寫入分支一致。(DBCLI-009)

  • JSON 形態的 ORM 產物改為指名檔案的 fail closed。 Drizzle snapshot 與
    normalized JSON 先前會把 JSON.parse 與 Zod 的原始錯誤直接丟出去,都沒有指出是
    哪一個檔案壞掉;agent 一次審多份產物時無從得知該修哪一個。(DBCLI-007)

  • impact assess 的輸出目標衝突看得出原因。 impact output already exists
    不在 safeMessage 的清單裡而被壓成泛用句子,審查者無從得知是報告檔已存在。
    同時把 prefix 比對改成完全比對——這是雙向的:design dialect 這類帶插值後綴的
    訊息反而不再放行、收斂成泛用句。prefix 的寫法在某個字串日後長出插值後綴時會默默
    漏資訊,而那正是這個函式存在的理由。(DBCLI-008)

Documentation

  • --context-version 2、儀表板溯源、遮蔽輸入、safe-backfill、slow-endpoint pack
    的說明同步到四份使用者文件與相關 Pages guide
    ,中英雙語、Markdown 與 HTML 兩種
    格式皆同步。(DBCLI-002 到 DBCLI-012)

  • 三份長期落在檢查外的 guide 納入涵蓋範圍。 evidence-packs、
    offline-impact-assessment、semantic-contracts 與 verification-evidence
    併入 guideSlugs;該清單本身改由讀目錄的檢查守住,因為手維護的清單擋不住第四
    份。(DBCLI-008、DBCLI-010、DBCLI-011)

  • receipt 旗標表格不再被一句話切成兩張,「Flag trio」/「旗標三件組」列了四個
    旗標,四份文件一併改為「Flags」/「旗標」。(DBCLI-009)

  • capabilities check --require 的順序語意現在只有一種說法。 PLAT-001 的
    Story 與 acceptance 宣告輸出「與 --require 參數順序無關」,實作從第一天起就在
    required 與 results 保留 first-seen 輸入順序,而且它自己的單元測試逐字斷言
    這件事。正式語意改為三條分開的話:相同輸入 byte-identical、兩個清單保留
    first-seen 輸入順序、換順序不改變任何 capability 的判決與整體 ok。實作與輸出
    一個位元都沒有動;tests/docs/capability-ordering-parity.test.ts 把八個表面
    綁在一起,並拒絕被撤回的那句話重新長出來。(DBCLI-PLAT-013)

Process

  • ForgeFlow 採用版本現在只有一個可檢查的答案。 specs/.forgeflow-adoption 是
    唯一權威,bun run forgeflow:check 新增的
    scripts/check-forgeflow-adoption.ts 把 specs/stories/README.md、
    specs/handoff.md、Story template 與本地 story-development Skill 全部對回它。
    這條 gate 的起因是它已經失效過一次:marker 與 README 推進到 0.3.2 之後,handoff
    跨兩個已合併的 PR 仍寫著 0.3.1,沒有任何東西比對過。(DBCLI-013)

  • ForgeFlow delivery gate 不再認 Story ID 的形狀。
    scripts/check-forgeflow-handoff.ts 原本用 /^(DBCLI-\d+).*$/ 從目錄名推 ID,
    DBCLI-PLAT-001-capability-contract 不匹配就被鍵在自己的完整目錄名底下——把
    DBCLI-PLAT-001 加進 completed_stories 會得到「找不到 Story directory」,
    指著一個明明存在的目錄。現在 ID 從每份 story.md 的 # Story: <ID> 標題讀,
    gate 不認任何形狀,新的 ID family 不需要改它;仍然比對宣告的 ID 是不是目錄名的
    前綴。規則搬進 scripts/lib/forgeflow-handoff.ts,對 fixture 測,而且不 import
    任何東西——「這個 gate 不碰網路」因此是檔案的性質,不是註解裡的承諾。同時新增
    一條規則:同一個 Story 不能同時是 current_story 與 completed_stories。
    (DBCLI-PLAT-013)