@carllee1983/dbcli 9.0.0 → 9.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/skills/dbcli/reference.md +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/skills/dbcli/reference.md +1 -1
- package/CHANGELOG.md +147 -14
- package/README.dev.md +1 -1
- package/README.md +77 -0
- package/assets/reference.md +1 -1
- package/dist/cli-runtime.mjs +818 -175
- package/dist/cli.mjs +3 -2
- package/dist/core.d.ts +452 -6
- package/dist/core.mjs +223 -22
- package/gemini-extension.json +1 -1
- package/package.json +3 -2
- package/plugins/dbcli-agent/.codex-plugin/plugin.json +1 -1
- package/plugins/dbcli-agent/skills/dbcli/reference.md +1 -1
- package/skills/dbcli/reference.md +1 -1
|
@@ -3872,7 +3872,7 @@ GET /orders/_search
|
|
|
3872
3872
|
|
|
3873
3873
|
- Enter a request line `<METHOD> /<path>`, then an optional multi-line JSON body; a **blank line** submits the block. Responses render as pretty-printed JSON.
|
|
3874
3874
|
- **The connection's permission tier applies, as it does to `dbcli query`.** The request is classified through the same classifier the query path uses, on the path the server will actually route — the query string is discarded and percent-encoding and dot segments are resolved first, so `?filter_path=...` cannot change the verdict.
|
|
3875
|
-
- **Reads are an allowlist; anything else needs `admin`.** Permitted below `admin`: `_search` and `_count` on an index or unscoped, `_doc` / `_source` reads, `_mapping` / `_settings` / `_alias` reads, `_cat/*` (except `_cat/aliases` and `_cat/tasks`), `_cluster/health`, and `GET` / `HEAD` of a bare index name. Writes to one document are the usual tiers. **Everything else — including any endpoint not listed here — requires `admin`**, so `DELETE /<index>`, `DELETE /_all`, `_delete_by_query`, `_update_by_query`, `_reindex`, `_aliases`, `PUT /<index>/_mapping`, `PUT /<index>/_settings`, `_sql`, `_scripts`, `_security`, `_snapshot`, `_nodes` and `_cluster/state` are all refused below it. The list is a floor, not a claim that everything absent from it is dangerous: a missing entry costs you an unnecessary `admin` requirement rather than a bypass. See [ADR-0014](https://github.com/CarlLee1983/dbcli/blob/main/docs/adr/0014-elasticsearch-reads-are-an-allowlist.md).
|
|
3875
|
+
- **Reads are an allowlist; anything else needs `admin`.** Permitted below `admin`: `_search` and `_count` on an index or unscoped, `_doc` / `_source` reads, `_mapping` / `_settings` / `_alias` reads, `_cat/*` (except `_cat/aliases` and `_cat/tasks`), `_cluster/health`, and `GET` / `HEAD` of a bare index name. Writes to one document are the usual tiers. **Everything else — including any endpoint not listed here — requires `admin`**, so `DELETE /<index>`, `DELETE /_all`, `_delete_by_query`, `_update_by_query`, `_reindex`, `_aliases`, `PUT /<index>/_mapping`, `PUT /<index>/_settings`, `_sql`, `_scripts`, `_security`, `_snapshot`, `_nodes` and `_cluster/state` are all refused below it. The list is a floor, not a claim that everything absent from it is dangerous: a missing entry costs you an unnecessary `admin` requirement rather than a bypass. See [ADR-0014](https://github.com/CarlLee1983/dbcli/blob/main/docs/adr/ADR-0014-elasticsearch-reads-are-an-allowlist.md).
|
|
3876
3876
|
- A path is refused unless it is **byte-identical** to what the URL parser produces — the same parser the request goes through — and the refusal tells you the canonical spelling to write instead. This is what stops a path that reads one way here and routes another way at the server: `#` truncates a path in transit (`POST /_reindex#/_count` reaches Elasticsearch as `POST /_reindex`), and tab, newline and `\` are rewritten too. A document id containing a space or a non-ASCII character must be written percent-encoded; the error message gives you the exact string.
|
|
3877
3877
|
- A quoted string request body is refused; write the body as JSON. So is a `source=` query parameter — Elasticsearch accepts it in place of a body, where none of the body checks can see it. `_source`, `_source_includes` and `_source_excludes` are unaffected.
|
|
3878
3878
|
- Blacklisted field names are refused in the query string as well as the body, because the URI-search form names fields directly (`?q=…`, `?sort=…`, `?docvalue_fields=…`). A name matches on any dot component, so `password.keyword` — the multi-field every `text` field gets by default — and `params._source.password` inside a script are both refused.
|
|
@@ -3872,7 +3872,7 @@ GET /orders/_search
|
|
|
3872
3872
|
|
|
3873
3873
|
- Enter a request line `<METHOD> /<path>`, then an optional multi-line JSON body; a **blank line** submits the block. Responses render as pretty-printed JSON.
|
|
3874
3874
|
- **The connection's permission tier applies, as it does to `dbcli query`.** The request is classified through the same classifier the query path uses, on the path the server will actually route — the query string is discarded and percent-encoding and dot segments are resolved first, so `?filter_path=...` cannot change the verdict.
|
|
3875
|
-
- **Reads are an allowlist; anything else needs `admin`.** Permitted below `admin`: `_search` and `_count` on an index or unscoped, `_doc` / `_source` reads, `_mapping` / `_settings` / `_alias` reads, `_cat/*` (except `_cat/aliases` and `_cat/tasks`), `_cluster/health`, and `GET` / `HEAD` of a bare index name. Writes to one document are the usual tiers. **Everything else — including any endpoint not listed here — requires `admin`**, so `DELETE /<index>`, `DELETE /_all`, `_delete_by_query`, `_update_by_query`, `_reindex`, `_aliases`, `PUT /<index>/_mapping`, `PUT /<index>/_settings`, `_sql`, `_scripts`, `_security`, `_snapshot`, `_nodes` and `_cluster/state` are all refused below it. The list is a floor, not a claim that everything absent from it is dangerous: a missing entry costs you an unnecessary `admin` requirement rather than a bypass. See [ADR-0014](https://github.com/CarlLee1983/dbcli/blob/main/docs/adr/0014-elasticsearch-reads-are-an-allowlist.md).
|
|
3875
|
+
- **Reads are an allowlist; anything else needs `admin`.** Permitted below `admin`: `_search` and `_count` on an index or unscoped, `_doc` / `_source` reads, `_mapping` / `_settings` / `_alias` reads, `_cat/*` (except `_cat/aliases` and `_cat/tasks`), `_cluster/health`, and `GET` / `HEAD` of a bare index name. Writes to one document are the usual tiers. **Everything else — including any endpoint not listed here — requires `admin`**, so `DELETE /<index>`, `DELETE /_all`, `_delete_by_query`, `_update_by_query`, `_reindex`, `_aliases`, `PUT /<index>/_mapping`, `PUT /<index>/_settings`, `_sql`, `_scripts`, `_security`, `_snapshot`, `_nodes` and `_cluster/state` are all refused below it. The list is a floor, not a claim that everything absent from it is dangerous: a missing entry costs you an unnecessary `admin` requirement rather than a bypass. See [ADR-0014](https://github.com/CarlLee1983/dbcli/blob/main/docs/adr/ADR-0014-elasticsearch-reads-are-an-allowlist.md).
|
|
3876
3876
|
- A path is refused unless it is **byte-identical** to what the URL parser produces — the same parser the request goes through — and the refusal tells you the canonical spelling to write instead. This is what stops a path that reads one way here and routes another way at the server: `#` truncates a path in transit (`POST /_reindex#/_count` reaches Elasticsearch as `POST /_reindex`), and tab, newline and `\` are rewritten too. A document id containing a space or a non-ASCII character must be written percent-encoded; the error message gives you the exact string.
|
|
3877
3877
|
- A quoted string request body is refused; write the body as JSON. So is a `source=` query parameter — Elasticsearch accepts it in place of a body, where none of the body checks can see it. `_source`, `_source_includes` and `_source_excludes` are unaffected.
|
|
3878
3878
|
- Blacklisted field names are refused in the query string as well as the body, because the URI-search form names fields directly (`?q=…`, `?sort=…`, `?docvalue_fields=…`). A name matches on any dot component, so `password.keyword` — the multi-field every `text` field gets by default — and `params._source.password` inside a script are both refused.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,139 @@ All notable changes to dbcli are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [9.1.1] - 2026-09-15
|
|
9
|
+
|
|
10
|
+
一個使用者回報的查詢缺陷(#197),一條會把人導向無效權限階的拒絕訊息,以及兩個
|
|
11
|
+
會對同一個 commit 給出兩種答案、或只會叫「東西變了」的測試門檻。
|
|
12
|
+
|
|
13
|
+
版本為 **9.1.1**。沒有新增指令、旗標或 engine;沒有任何一處拒絕先前被接受的輸入。
|
|
14
|
+
唯一改變輸出的是拒絕訊息裡的 `required:` 欄位與它的句子——那個值先前是錯的,照它
|
|
15
|
+
去要權限會被再拒一次,所以修正它不是相容性變更,是讓它開始為真。
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **snippet 裡重複使用同一個具名參數,在 MySQL / MariaDB / SQLite 上不再炸。**
|
|
20
|
+
`rewriteToBind()` 對每一次 `:name` 出現輸出一個 placeholder,但 bind values 是由
|
|
21
|
+
去重後的名稱清單映射出來的。PostgreSQL 重用 `$1`,一個值對一個索引本來就對;`?`
|
|
22
|
+
沒有索引可以重用,所以 placeholder 數量跟著出現次數走、values 數量跟著相異名稱數
|
|
23
|
+
走,兩者在有重複參數時必然對不上,MariaDB 以 `ER_WRONG_ARGUMENTS` 拒絕整條
|
|
24
|
+
statement。
|
|
25
|
+
|
|
26
|
+
實務影響是「參數為 0 就查全部、否則查單一」這類條件在 snippet 裡寫不出來,只能
|
|
27
|
+
繞道 CTE 讓參數只出現一次。`--dry-run` 看得到 SQL 與 bind values 不匹配,但不會
|
|
28
|
+
報錯,所以問題要到真的執行才炸。非 PostgreSQL 引擎現在每一次出現送一個值,
|
|
29
|
+
PostgreSQL 的去重行為不變。(#197)
|
|
30
|
+
|
|
31
|
+
- **拒絕訊息指向的權限階,現在是真的會通過的那一階。** 無法分類的語句(`REPLACE
|
|
32
|
+
INTO`、`PRAGMA`、`VACUUM`、`REINDEX` 等)在 `query-only` 下被拒時宣稱需要
|
|
33
|
+
`read-write`,但沒有任何一階授予未知語句,取得 read-write 之後會再被拒一次。訊息
|
|
34
|
+
與 `required:` 欄位現在都說 `admin`,en 與 zh-TW 同步。判決完全沒變:本來拒絕的仍
|
|
35
|
+
然拒絕,本來允許的仍然允許。(DBCLI-037)
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **capability catalog 的輸出改由契約守住,不再由 sha256 守住。** 三份渲染
|
|
40
|
+
(JSON/text/Markdown)的凍結雜湊換成結構性斷言:文件在釘住的 schema 版本下
|
|
41
|
+
parse 得過、三份渲染列出同一組 capability、每個 capability 指的 command 是 CLI 真的
|
|
42
|
+
帶得動的。catalog 是從 engine 矩陣推導出來的(ADR-0022),所以每次矩陣變動都會
|
|
43
|
+
移動那三個雜湊,而修法永遠是把現在的值抄回去——連續三次之後,它報告的是 diff
|
|
44
|
+
本來就說了的事。決定與被否決的選項記在 ADR-0039。使用者可見的輸出沒有變。
|
|
45
|
+
(DBCLI-038)
|
|
46
|
+
|
|
47
|
+
- **驗證 artifact 的秘密洩漏檢查改成逐欄檢查。** 原本在整份文件裡搜尋連線 port 的
|
|
48
|
+
四位數字,而 artifact 帶一個隨機後綴的 id,後綴湊出那四位數字時測試會紅——什麼都
|
|
49
|
+
沒洩漏,重跑就過。改為解析後逐欄走訪並指名帶著 port 的欄位;判準仍是包含而非
|
|
50
|
+
相等,藏在連線字串裡的 port 照樣抓得到。(DBCLI-039)
|
|
51
|
+
|
|
52
|
+
## [9.1.0] - 2026-09-11
|
|
53
|
+
|
|
54
|
+
ForgeFlow Story DBCLI-014 到 DBCLI-036 在 `v9.0.0` 之後交付。其中只有最後三張
|
|
55
|
+
(DBCLI-034、035、036)改動使用者看得到的行為——SQLite 成為第六種 engine;其餘
|
|
56
|
+
二十張全部落在 ForgeFlow/ForgePilot 的工作控制面、`make verify` 的證據與效能門檻上,
|
|
57
|
+
照 8.0.0 處理 DBCLI-013 的方式,收在 Changed 底下。
|
|
58
|
+
|
|
59
|
+
版本為 **9.1.0**。新增一種 engine 是 MINOR;本區間沒有任何一處拒絕先前被接受的輸入、
|
|
60
|
+
改變既有 JSON 欄位的意義,或移除旗標與指令。三張 SQLite Story 各自帶一條回歸驗收
|
|
61
|
+
(DBCLI-034 AC-012、DBCLI-035 AC-010、DBCLI-036 AC-012/013),要求既有 fixture 的判決
|
|
62
|
+
一字不變,而不是「大致相容」。
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
- **SQLite 成為第六種 engine:一個檔案就是一條連線。** `list`、`schema`、`query`、`q`、
|
|
67
|
+
`export`、`queries` 走的是其他 SQL engine 同一條權限階梯、同一份 blacklist 與同一套
|
|
68
|
+
audit 規則,`--use a,b` 扇出同樣可用。連線設定只有 `file` 一個欄位有意義——沒有 host、
|
|
69
|
+
port、user、password,因為存取控制來自作業系統的檔案權限,而不是網路上的一次認證。
|
|
70
|
+
|
|
71
|
+
`query-only` 由 engine 執行,不是由 dbcli 代為保證。PostgreSQL 與 MySQL 把語句放進
|
|
72
|
+
read-only transaction,SQLite 沒有這種 transaction,所以 dbcli 直接以
|
|
73
|
+
`SQLITE_OPEN_READONLY` 開檔——一個根本沒有寫入通道的 handle。代價講明:帶著未重放
|
|
74
|
+
write-ahead log 的資料庫開不成唯讀,因為重放本身就是寫入;dbcli 會指名 WAL,而不是把
|
|
75
|
+
`SQLITE_CANTOPEN` 原樣丟出來。(DBCLI-034、ADR-0037)
|
|
76
|
+
|
|
77
|
+
dbcli 不會替你建立資料庫檔。路徑不存在、路徑存在但不是 SQLite 資料庫,都在連線時
|
|
78
|
+
失敗並指名原因;`:memory:`、`file::memory:` 與任何帶 `mode=memory` 的路徑則更早,
|
|
79
|
+
在設定解析時就被拒絕——連線是 blacklist、audit log 與 schema cache 共同綁定的身分,
|
|
80
|
+
而一個記憶體資料庫每次呼叫都是不同的資料庫,這種身分撐不住。(ADR-0038)
|
|
81
|
+
|
|
82
|
+
- **SQLite 專屬語句有了判決,寫入路徑打通。** `insert`、`update`、`delete` 在與其他 SQL
|
|
83
|
+
engine 相同的階梯與 `--dry-run` 下對 SQLite 檔案運作,產生的 SQL 使用 SQLite 標準的
|
|
84
|
+
雙引號識別字與 `?` 佔位符。`REPLACE INTO` 要求 `data-admin` 而不是 `read-write`——它
|
|
85
|
+
會先移除衝突的那一列再寫入替代值,那正是它與 INSERT 的差別;`PRAGMA`、`VACUUM`、
|
|
86
|
+
`REINDEX` 要求 `admin`。MySQL 的 `REPLACE INTO` 仍維持先前的分類,這條映射只在 SQLite
|
|
87
|
+
方言上生效。
|
|
88
|
+
|
|
89
|
+
`ATTACH` 與 `DETACH` 在任何權限層級都被拒絕,`admin` 也不例外。它們不是階梯問題:
|
|
90
|
+
提高權限不會讓「碰到連線沒有指名的第二個資料庫檔」變得可以接受。拒絕走的是新的
|
|
91
|
+
`ConnectionBoundaryError` 而不是 `PermissionError`,因為後者會印出 `required: admin`,
|
|
92
|
+
等於邀請一次幫不上忙的提權。(DBCLI-035、ADR-0037)
|
|
93
|
+
|
|
94
|
+
- **SQLite 連線不必手寫。** `dbcli init --system sqlite` 只問兩件事——檔案路徑與權限——
|
|
95
|
+
不問 host、port、user、password 或資料庫名稱;非互動時用 `--file <path>`。路徑在任何
|
|
96
|
+
東西被寫進 `config.json` 之前就檢查,不存在、不是 SQLite 資料庫、或任何記憶體形式
|
|
97
|
+
一律拒絕並保持設定檔原封不動。
|
|
98
|
+
|
|
99
|
+
`use` 與 `status` 在其他 engine 顯示 host 的位置顯示檔案路徑,所以 SQLite 連線不會
|
|
100
|
+
render 成看起來像解析失敗的 `:0/`;`status --format json` 與 `use` 的機器輸出各多一個
|
|
101
|
+
只在 SQLite 連線上出現的 `file` 欄位。`doctor` 檢查的是檔案而非網路端點:存在、可讀,
|
|
102
|
+
以及只在權限允許寫入時才檢查可寫——`query-only` 下的唯讀檔案是設定正確,會被如此
|
|
103
|
+
回報而不是判為失敗。v1 設定無法升級成 SQLite(v1 格式早於這個 engine),訊息改為
|
|
104
|
+
指向 `dbcli init --system sqlite`。(DBCLI-036)
|
|
105
|
+
|
|
106
|
+
- **`docs/feature-matrix.md` 多了 SQLite 一欄與限制摘要。** 表格與
|
|
107
|
+
`src/adapters/capabilities.ts` 的 `sqlite` 登錄同步。
|
|
108
|
+
|
|
109
|
+
### Changed
|
|
110
|
+
|
|
111
|
+
- **ForgePilot 成為生命週期的唯一權威,`specs/handoff.md` 只留下沒有別的家的東西。**
|
|
112
|
+
handoff 從工作佇列變成交付紀錄,並由 gate 比對倉庫自己的歷史雙向檢查:宣告了交付卻
|
|
113
|
+
沒有東西背書會紅,歷史已經宣稱交付卻沒登進清單也會紅;「誰被允許 commit 或 push」
|
|
114
|
+
是交付回答不了的另一個問題,gate 停止回答它。ForgeFlow 升級到 0.6.0,Story 的
|
|
115
|
+
`## Authority`、風險宣告與逐條驗收證據改由上游 checker 執行而非在本倉庫重寫,Story
|
|
116
|
+
對 ADR 的依賴從散文連結變成解析得開的一行。(DBCLI-014、016、018、021、027、028、
|
|
117
|
+
029、033;ADR-0025、0027、0029、0030、0031、0032、0036)
|
|
118
|
+
|
|
119
|
+
- **`make verify` 自帶依賴安裝,寫出綁定 revision 的 Verification Attestation,並少
|
|
120
|
+
build 一次。** FAIL 會指名它停在哪一步,log 邊跑邊說自己在第幾步、每步花了多久;
|
|
121
|
+
attestation 不是 evidence receipt,兩個階段都不准決定 verdict。`test:perf` 的判決不再
|
|
122
|
+
取決於機器忙不忙:十九條讀時鐘的門換成計算實際做了多少工作,blacklist 遮蔽成本改由
|
|
123
|
+
確定性計數表達。(DBCLI-015、017、019、020、030、031、032;ADR-0026、0028、0033、
|
|
124
|
+
0034、0035)
|
|
125
|
+
|
|
126
|
+
- **五條被承認的 contract 豁免全部歸零。** DBCLI-PLAT-004 到 007、012 的信任邊界與
|
|
127
|
+
fixture 矩陣改成指名真正的欄位與真的存在的測試;先前沒有任何斷言的三列
|
|
128
|
+
correlation-id fixture 補上測試。純規格與測試修正,程式行為未變。
|
|
129
|
+
(DBCLI-022、023、024、025、026)
|
|
130
|
+
|
|
131
|
+
- **ADR 檔名統一為 `ADR-00NN-*.md`。** `docs/adr/0001-*.md` 這類舊檔名全部改名,讓
|
|
132
|
+
Story 的決策記錄連結可以被 checker 解析;指向舊路徑的外部書籤需要調整。
|
|
133
|
+
(DBCLI-021、ADR-0029)
|
|
134
|
+
|
|
135
|
+
### Fixed
|
|
136
|
+
|
|
137
|
+
- **Windows 上三個 SQLite 測試依賴 POSIX 權限語意而假紅。** `chmod` 在 Windows 拿不掉
|
|
138
|
+
讀取權限、也擋不住目錄內建檔,這兩個情境改為 `skipIf(win32)`;「可讀但不是資料庫」
|
|
139
|
+
的 fixture 改用臨時目錄內寫出的文字檔,不再依賴 `/etc/passwd`。
|
|
140
|
+
|
|
8
141
|
## [9.0.0] - 2026-09-06
|
|
9
142
|
|
|
10
143
|
### Changed
|
|
@@ -323,8 +456,8 @@ ForgeFlow Story DBCLI-001 到 DBCLI-012 全數在 `v7.0.1`(`224ee59d`)之後
|
|
|
323
456
|
ADR-0019 在自己的 Consequences 裡寫下這一則:一份設定的大小寫折疊仍然是三套規則。
|
|
324
457
|
這一版把它收成一套,並在收的過程中發現同一份設定還有一條完全繞過它的路——規則從來
|
|
325
458
|
沒有抵達 `dbcli q` 與 `dbcli report` 用的那個 adapter。設計決策記在
|
|
326
|
-
`docs/adr/0020-one-fold-rule-for-every-blacklist-comparison.md` 與
|
|
327
|
-
`docs/adr/0021-connection-only-adapters-say-without-rules.md`。
|
|
459
|
+
`docs/adr/ADR-0020-one-fold-rule-for-every-blacklist-comparison.md` 與
|
|
460
|
+
`docs/adr/ADR-0021-connection-only-adapters-say-without-rules.md`。
|
|
328
461
|
|
|
329
462
|
2026-09-01 直接呼叫四個比對器量到的起點——除了最後一列,每一列都是「寫入被拒、
|
|
330
463
|
讀取原文回傳」的設定,而操作者確認規則有效的方式,通常就是看寫入被擋下來:
|
|
@@ -548,8 +681,8 @@ ADR-0019 在自己的 Consequences 裡寫下這一則:一份設定的大小寫
|
|
|
548
681
|
## [6.0.0] - 2026-09-01 - 一份黑名單設定,四個互不相同的比對器
|
|
549
682
|
|
|
550
683
|
規格 SQL 第 7、8、9 則與 MongoDB 第 3–6 則。設計決策記在
|
|
551
|
-
`docs/adr/0018-a-blacklist-rule-that-does-not-match-fails-loudly.md` 與
|
|
552
|
-
`docs/adr/0019-one-blacklist-rule-one-matcher.md`。
|
|
684
|
+
`docs/adr/ADR-0018-a-blacklist-rule-that-does-not-match-fails-loudly.md` 與
|
|
685
|
+
`docs/adr/ADR-0019-one-blacklist-rule-one-matcher.md`。
|
|
553
686
|
|
|
554
687
|
SQL 那半:本機 MariaDB、表 `probe_users (id, Password, note)`、值 `s3cret`,八種設定裡七種洩漏,而全部都被設定載入器無聲接受——操作者「規則有效」的唯一證據是 dbcli 沒有抱怨。
|
|
555
688
|
|
|
@@ -589,7 +722,7 @@ MongoDB 那半(本機容器 MongoDB 7.0.31 實測):同一組規則被四
|
|
|
589
722
|
|
|
590
723
|
## [5.1.0] - 2026-08-31 - 一次被黑名單擋下的查詢,紀錄裡指向的是沒被擋的那張表
|
|
591
724
|
|
|
592
|
-
`docs/specs/2026-08-30-cross-engine-blacklist-gaps.md` 的 audit 第 10 則。設計決策記在 `docs/adr/0017-the-audit-target-stays-wrong-and-the-record-stops-depending-on-it.md`。
|
|
725
|
+
`docs/specs/2026-08-30-cross-engine-blacklist-gaps.md` 的 audit 第 10 則。設計決策記在 `docs/adr/ADR-0017-the-audit-target-stays-wrong-and-the-record-stops-depending-on-it.md`。
|
|
593
726
|
|
|
594
727
|
### Added
|
|
595
728
|
|
|
@@ -603,7 +736,7 @@ MongoDB 那半(本機容器 MongoDB 7.0.31 實測):同一組規則被四
|
|
|
603
736
|
|
|
604
737
|
## [5.0.0] - 2026-08-31 - shell 裡一句真的改了資料的 UPDATE,稽核裡沒有任何一列
|
|
605
738
|
|
|
606
|
-
`docs/specs/2026-08-30-cross-engine-blacklist-gaps.md` 的 audit 第 11 則自己註明是純讀碼結論、值得先端到端確認。確認了,結論成立,而且比讀碼看到的更嚴重。本機 MariaDB、`read-write` 連線、每次先清空 audit:`dbcli query "SELECT …"` 寫一列,而在 `dbcli>` 提示符打的同一句寫零列;一句帶 WHERE 的 `UPDATE` 走完全程、改掉了資料,同樣零列。設計決策記在 `docs/adr/0016-the-sql-shell-audits-every-statement-in-two-rows.md`。
|
|
739
|
+
`docs/specs/2026-08-30-cross-engine-blacklist-gaps.md` 的 audit 第 11 則自己註明是純讀碼結論、值得先端到端確認。確認了,結論成立,而且比讀碼看到的更嚴重。本機 MariaDB、`read-write` 連線、每次先清空 audit:`dbcli query "SELECT …"` 寫一列,而在 `dbcli>` 提示符打的同一句寫零列;一句帶 WHERE 的 `UPDATE` 走完全程、改掉了資料,同樣零列。設計決策記在 `docs/adr/ADR-0016-the-sql-shell-audits-every-statement-in-two-rows.md`。
|
|
607
740
|
|
|
608
741
|
### Changed
|
|
609
742
|
|
|
@@ -783,7 +916,7 @@ Three numbers in this repository disagreed with reality and nothing checked any
|
|
|
783
916
|
|
|
784
917
|
- **`_update_by_query` 從 `read-write` 收緊為 `admin`**:它是獨立的區段,精確比對之下落到破壞性預設,而它確實會改寫索引裡的每一份文件。
|
|
785
918
|
|
|
786
|
-
- **BREAKING (artifact format): evidence packs and receipts are `version: 2`, and the format version is no longer tied to the package version.** v3.0.0 changed the pack digest input, the id derivation, the `coverage` field and the receipt `observation` while leaving both constants at `1` — ADR-0012 decided to amend the schema in place, which was right about the repairs and wrong about the number. A reader now classifies an artifact by version *and* structure before computing anything: `v1-coverage` (2.1.0 and earlier), `v1-untagged-v3` (3.0.0), current, or unsupported. Both legacy formats stay readable and integrity-checkable through frozen reimplementations of their own digests, and neither is ever `trust: "current-valid"`. There is no migration and cannot be one — a pack's id derives from its digest, so rewriting an old pack would mint a new artifact wearing an old one's provenance, and a v2.1.0 receipt's hashed `observation` can only be "recovered" by inverting it, which is a guess and not a record. Recorded in `docs/adr/0013-evidence-artifact-format-versions-are-independent-of-the-package-version.md`, amending ADR-0012.
|
|
919
|
+
- **BREAKING (artifact format): evidence packs and receipts are `version: 2`, and the format version is no longer tied to the package version.** v3.0.0 changed the pack digest input, the id derivation, the `coverage` field and the receipt `observation` while leaving both constants at `1` — ADR-0012 decided to amend the schema in place, which was right about the repairs and wrong about the number. A reader now classifies an artifact by version *and* structure before computing anything: `v1-coverage` (2.1.0 and earlier), `v1-untagged-v3` (3.0.0), current, or unsupported. Both legacy formats stay readable and integrity-checkable through frozen reimplementations of their own digests, and neither is ever `trust: "current-valid"`. There is no migration and cannot be one — a pack's id derives from its digest, so rewriting an old pack would mint a new artifact wearing an old one's provenance, and a v2.1.0 receipt's hashed `observation` can only be "recovered" by inverting it, which is a guess and not a record. Recorded in `docs/adr/ADR-0013-evidence-artifact-format-versions-are-independent-of-the-package-version.md`, amending ADR-0012.
|
|
787
920
|
|
|
788
921
|
- **`evidence validate --format json` distinguishes three answers where it used to give one.** `status` is `current-valid`, `current-references-expired`, `recognized-legacy`, or `unsupported`, with `trust` stating plainly whether the pack may be relied on; `recognized-legacy` also carries `legacyFormat`, `producedBy`, and that format's own `integrity`. An unknown version fails closed. Previously every one of these arrived as `evidence pack digest mismatch`, which tells the holder of a two-week-old file that someone tampered with it.
|
|
789
922
|
|
|
@@ -793,7 +926,7 @@ Three numbers in this repository disagreed with reality and nothing checked any
|
|
|
793
926
|
|
|
794
927
|
## [3.0.0] - 2026-08-16 - Evidence that could not reproduce itself, and a hash that hid nothing
|
|
795
928
|
|
|
796
|
-
The evidence subsystem shipped in v1.53.0 and, until this week, nobody had composed a pack outside its own tests. The first real use — a `verify safe-backfill --after-write` against a live PostgreSQL — came back `not_verified` on data that was correct, and the audit that followed found three more defects of the same kind: an evidence pack whose digest covered a random UUID, so the same claims never produced the same pack twice; a receipt "fingerprint" that was an unsalted SHA-256 over eight possible values; and a blacklist comparison with no identifier boundaries, so a protected column named `id` refused any claim containing the word "identifier". Fixing them changes both published formats, which is what makes this a major release: **packs written by 2.x will fail validation under 3.0.0, and `observation.fingerprint` no longer exists.** The reversal that authorized the repairs — known defects get fixed whether or not anyone is using the code — is recorded in `docs/adr/0012-known-defects-get-fixed-whether-or-not-anyone-is-using-the-code.md`, superseding ADR 0011.
|
|
929
|
+
The evidence subsystem shipped in v1.53.0 and, until this week, nobody had composed a pack outside its own tests. The first real use — a `verify safe-backfill --after-write` against a live PostgreSQL — came back `not_verified` on data that was correct, and the audit that followed found three more defects of the same kind: an evidence pack whose digest covered a random UUID, so the same claims never produced the same pack twice; a receipt "fingerprint" that was an unsalted SHA-256 over eight possible values; and a blacklist comparison with no identifier boundaries, so a protected column named `id` refused any claim containing the word "identifier". Fixing them changes both published formats, which is what makes this a major release: **packs written by 2.x will fail validation under 3.0.0, and `observation.fingerprint` no longer exists.** The reversal that authorized the repairs — known defects get fixed whether or not anyone is using the code — is recorded in `docs/adr/ADR-0012-known-defects-get-fixed-whether-or-not-anyone-is-using-the-code.md`, superseding ADR 0011.
|
|
797
930
|
|
|
798
931
|
### Changed
|
|
799
932
|
|
|
@@ -877,7 +1010,7 @@ Three shapes that ran unattended before now exit `1`, all of them full-table wri
|
|
|
877
1010
|
|
|
878
1011
|
### Changed
|
|
879
1012
|
|
|
880
|
-
- **BREAKING: a statement that is not limited to particular rows is refused when nobody is watching.** `dbcli query "UPDATE users SET banned = 1"` used to execute against any read-write connection without asking anything, and the caller most likely to produce an unqualified `UPDATE` is the agent this product exists to serve. Raw SQL now passes through a two-tier gate before the connection is opened. Tier one is any write — an `INSERT`, an `UPDATE` or `DELETE` that has a `WHERE` or `LIMIT`, a `CREATE`, an `ALTER` — and behaves as it always has for a non-interactive caller; at a terminal it shows what dbcli understood the statement to do and asks, which `--yes` skips. Tier two is `UPDATE` / `DELETE` with no `WHERE`, `DROP`, `TRUNCATE`, several statements in one string — one statement to a classifier reading the leading keyword, two to a driver — and any statement the SQL parser cannot read: at a terminal the operator types the target table name, and **no flag skips it** — not `--yes`, not `--force`. Away from a terminal, or under `--format json`, tier two is refused: exit `1`, a `reason=` a caller can branch on (`no_where`, `ddl_destruction`, `unparseable`, `multiple_statements`), and the statement never sent, because the gate runs before the adapter is built rather than after. A parse failure resolves to tier two rather than tier one; the cost of being wrong is a needlessly typed table name against a needlessly emptied table. The reasoning, the alternatives, and the condition that would falsify it are in `docs/adr/0010-unattended-callers-are-refused-full-table-writes.md` (#70).
|
|
1013
|
+
- **BREAKING: a statement that is not limited to particular rows is refused when nobody is watching.** `dbcli query "UPDATE users SET banned = 1"` used to execute against any read-write connection without asking anything, and the caller most likely to produce an unqualified `UPDATE` is the agent this product exists to serve. Raw SQL now passes through a two-tier gate before the connection is opened. Tier one is any write — an `INSERT`, an `UPDATE` or `DELETE` that has a `WHERE` or `LIMIT`, a `CREATE`, an `ALTER` — and behaves as it always has for a non-interactive caller; at a terminal it shows what dbcli understood the statement to do and asks, which `--yes` skips. Tier two is `UPDATE` / `DELETE` with no `WHERE`, `DROP`, `TRUNCATE`, several statements in one string — one statement to a classifier reading the leading keyword, two to a driver — and any statement the SQL parser cannot read: at a terminal the operator types the target table name, and **no flag skips it** — not `--yes`, not `--force`. Away from a terminal, or under `--format json`, tier two is refused: exit `1`, a `reason=` a caller can branch on (`no_where`, `ddl_destruction`, `unparseable`, `multiple_statements`), and the statement never sent, because the gate runs before the adapter is built rather than after. A parse failure resolves to tier two rather than tier one; the cost of being wrong is a needlessly typed table name against a needlessly emptied table. The reasoning, the alternatives, and the condition that would falsify it are in `docs/adr/ADR-0010-unattended-callers-are-refused-full-table-writes.md` (#70).
|
|
881
1014
|
|
|
882
1015
|
- **BREAKING: `dbcli update` / `dbcli delete` refuse a `--where` that matches on nothing unique.** Their `WHERE` is mandatory, so "no `WHERE`" cannot happen — but `--where "status=active"` reads like a filter and writes like a full-table statement. When the conditions cover neither the primary key nor any unique index, the same tier-two treatment applies: type the table name, or be refused with `reason=non_unique_where`. The schema needed to tell the two apart is already in hand at that point, so this costs no extra round trip. `--force` is unaffected in what it always did — skip the ordinary confirmation — and does not open this gate (#70).
|
|
883
1016
|
|
|
@@ -924,7 +1057,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
|
|
|
924
1057
|
|
|
925
1058
|
- **A write that fails in a terminal now says so in prose too, on stderr.** Prose covered the endings a person cares least about — a successful write, a cancellation, a dry run — while the two that actually stop you got a raw JSON envelope on stdout: a blacklist refusal and a validation failure (a malformed `--set`, a missing `--where`). Both now go through `printMutationFailure`, which prints the reason as a sentence when somebody is watching and the same envelope, byte for byte, when nobody is. Human-mode failures — including the executor's own `status: "error"` — write to **stderr**, joining the `PermissionError` and `ConnectionError` branches that always did, so every human-facing failure is on the error stream while routine progress stays on stdout. A blacklist refusal gets a hint naming `dbcli blacklist list`; nothing else gets the `--recovery` line, because `--recovery` writes a plan for a statement that failed against the database and a refusal at this stage never reached one (#70).
|
|
926
1059
|
|
|
927
|
-
- **Core modules can no longer write to stdout, and CI fails if one starts.** `DataExecutor` printed the generated SQL and blocked on `promptUser.confirm` from inside `src/core`, on the same stdout that carries the JSON envelope agents parse — and `dist/core.mjs` shipped a real `import("@inquirer/prompts")` so that a library consumer could be handed an interactive prompt it never asked for. Core now describes the pending mutation and asks the caller through a `confirm` callback; `src/commands/mutation-confirm.ts` holds the CLI's implementation. `scripts/check-core-no-stdout.ts` enforces the boundary in CI, with the 16 modules that predate the rule in a ratchet list that can only shrink. The reasoning, its alternatives, and the condition that would falsify it are recorded in `docs/adr/0009-core-does-not-write-to-stdout.md` (#70).
|
|
1060
|
+
- **Core modules can no longer write to stdout, and CI fails if one starts.** `DataExecutor` printed the generated SQL and blocked on `promptUser.confirm` from inside `src/core`, on the same stdout that carries the JSON envelope agents parse — and `dist/core.mjs` shipped a real `import("@inquirer/prompts")` so that a library consumer could be handed an interactive prompt it never asked for. Core now describes the pending mutation and asks the caller through a `confirm` callback; `src/commands/mutation-confirm.ts` holds the CLI's implementation. `scripts/check-core-no-stdout.ts` enforces the boundary in CI, with the 16 modules that predate the rule in a ratchet list that can only shrink. The reasoning, its alternatives, and the condition that would falsify it are recorded in `docs/adr/ADR-0009-core-does-not-write-to-stdout.md` (#70).
|
|
928
1061
|
|
|
929
1062
|
### Fixed
|
|
930
1063
|
|
|
@@ -976,7 +1109,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
|
|
|
976
1109
|
|
|
977
1110
|
### Removed
|
|
978
1111
|
|
|
979
|
-
- **`engines.node` — the runtime declaration now matches the artifact.** `package.json` claimed `node: ">=18.0.0"` since the first release, but measured on Node v22.17.1 against the `v1.54.1` `dist/`, only one published entry point actually loaded. `node dist/cli.mjs` threw `ERR_MODULE_NOT_FOUND` as soon as the launcher reached its dynamic import: `src/cli.ts` keeps the runtime path as a non-literal `'./cli-runtime'` so the `--version` launcher does not inline the heavy runtime, and Bun's resolver appends `.mjs` where Node's ESM resolver requires the extension. `import('dist/core.mjs')` — the `./core` subpath export — threw `Bun is not defined`, the bundle holding 30 Bun global references against `dist/cli-runtime.mjs`'s 143. The `bin` shebang is `#!/usr/bin/env bun` regardless, and the `--version` fast path branches on `import.meta.main`, which is `undefined` before Node 24. `engines` now declares `bun >= 1.3.3` alone. `dist/agent-core.mjs` was and remains Node-importable, and is documented as the one entry point that is. npm and npx stay supported as distribution channels, with the docs stating plainly that the installed executable still runs under Bun. `tests/integration/runtime-contract.test.ts` holds both ends: re-declaring `engines.node` fails unless `dist/cli.mjs` and `dist/core.mjs` really import in a bare Node process, and `dist/agent-core.mjs` must keep importing there whatever else changes. The alternative — `--target node` plus ~169 Bun API replacements in `src/` and a Node runtime CI suite — is rejected and its cost recorded in `docs/adr/0008-dbcli-is-a-bun-program-and-engines-says-so.md` (#65).
|
|
1112
|
+
- **`engines.node` — the runtime declaration now matches the artifact.** `package.json` claimed `node: ">=18.0.0"` since the first release, but measured on Node v22.17.1 against the `v1.54.1` `dist/`, only one published entry point actually loaded. `node dist/cli.mjs` threw `ERR_MODULE_NOT_FOUND` as soon as the launcher reached its dynamic import: `src/cli.ts` keeps the runtime path as a non-literal `'./cli-runtime'` so the `--version` launcher does not inline the heavy runtime, and Bun's resolver appends `.mjs` where Node's ESM resolver requires the extension. `import('dist/core.mjs')` — the `./core` subpath export — threw `Bun is not defined`, the bundle holding 30 Bun global references against `dist/cli-runtime.mjs`'s 143. The `bin` shebang is `#!/usr/bin/env bun` regardless, and the `--version` fast path branches on `import.meta.main`, which is `undefined` before Node 24. `engines` now declares `bun >= 1.3.3` alone. `dist/agent-core.mjs` was and remains Node-importable, and is documented as the one entry point that is. npm and npx stay supported as distribution channels, with the docs stating plainly that the installed executable still runs under Bun. `tests/integration/runtime-contract.test.ts` holds both ends: re-declaring `engines.node` fails unless `dist/cli.mjs` and `dist/core.mjs` really import in a bare Node process, and `dist/agent-core.mjs` must keep importing there whatever else changes. The alternative — `--target node` plus ~169 Bun API replacements in `src/` and a Node runtime CI suite — is rejected and its cost recorded in `docs/adr/ADR-0008-dbcli-is-a-bun-program-and-engines-says-so.md` (#65).
|
|
980
1113
|
|
|
981
1114
|
### Added
|
|
982
1115
|
|
|
@@ -1258,7 +1391,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
|
|
|
1258
1391
|
|
|
1259
1392
|
## [1.47.1] - 2026-08-05 - 唯讀保證涵蓋所有執行路徑(安全性修復)
|
|
1260
1393
|
|
|
1261
|
-
決策記錄:`docs/adr/0004-database-access-stays-a-cli-surface.md`。
|
|
1394
|
+
決策記錄:`docs/adr/ADR-0004-database-access-stays-a-cli-surface.md`。
|
|
1262
1395
|
|
|
1263
1396
|
### Security
|
|
1264
1397
|
|
|
@@ -1298,7 +1431,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
|
|
|
1298
1431
|
|
|
1299
1432
|
## [1.47.0] - 2026-08-05 - 連線逾時可設定
|
|
1300
1433
|
|
|
1301
|
-
決策記錄:`docs/adr/0003-connection-timeout-override-resolved-at-adapter-construction.md`。
|
|
1434
|
+
決策記錄:`docs/adr/ADR-0003-connection-timeout-override-resolved-at-adapter-construction.md`。
|
|
1302
1435
|
|
|
1303
1436
|
### Added
|
|
1304
1437
|
|
|
@@ -1314,7 +1447,7 @@ Automation that performs unqualified full-table writes stops working. Three shap
|
|
|
1314
1447
|
|
|
1315
1448
|
## [1.46.0] - 2026-08-04 - MongoDB 逐欄連線設定
|
|
1316
1449
|
|
|
1317
|
-
決策記錄:`docs/adr/0002-mongodb-connection-field-first-config.md`;規格:`docs/specs/2026-08-04-mongodb-field-first-connection.md`。
|
|
1450
|
+
決策記錄:`docs/adr/ADR-0002-mongodb-connection-field-first-config.md`;規格:`docs/specs/2026-08-04-mongodb-field-first-connection.md`。
|
|
1318
1451
|
|
|
1319
1452
|
### Changed
|
|
1320
1453
|
|
package/README.dev.md
CHANGED
|
@@ -106,7 +106,7 @@ Then ship a patch version with the fix. Prefer **deprecate** over **unpublish**
|
|
|
106
106
|
|
|
107
107
|
- **`files` (in `package.json`):** Publishes `dist/`, `assets/`, `README.md`, `CHANGELOG.md`, `LICENSE`. The `assets/` tree is required for `dbcli skill` to copy bundled `SKILL.md` / `reference.md` from the installed package.
|
|
108
108
|
- **`prepublishOnly`:** `bun run build` so `dist/cli.mjs` matches current source.
|
|
109
|
-
- **`engines`:** Declares `bun >= 1.3.3` only. `node` was removed because the published bundles cannot run on Node: `dist/cli.mjs` dynamic-imports the extensionless `./cli-runtime` (Bun's resolver appends `.mjs`, Node's does not) and `dist/core.mjs` calls Bun globals. `tests/integration/runtime-contract.test.ts` fails if `engines.node` comes back without the bundles being fixed to match, and if `dist/agent-core.mjs` — the one entry point that is Node-importable — regresses. See `docs/adr/0008-dbcli-is-a-bun-program-and-engines-says-so.md`.
|
|
109
|
+
- **`engines`:** Declares `bun >= 1.3.3` only. `node` was removed because the published bundles cannot run on Node: `dist/cli.mjs` dynamic-imports the extensionless `./cli-runtime` (Bun's resolver appends `.mjs`, Node's does not) and `dist/core.mjs` calls Bun globals. `tests/integration/runtime-contract.test.ts` fails if `engines.node` comes back without the bundles being fixed to match, and if `dist/agent-core.mjs` — the one entry point that is Node-importable — regresses. See `docs/adr/ADR-0008-dbcli-is-a-bun-program-and-engines-says-so.md`.
|
|
110
110
|
- **Shebang:** `scripts/build.ts` prepends `#!/usr/bin/env bun` to `dist/cli.mjs`; the `bin` field in `package.json` points at that file.
|
|
111
111
|
- **`exports`:** `./core` and `./agent-core` only — there is deliberately no `.` entry. It used to point at `dist/cli.mjs`, and `src/cli-runtime.ts` calls `outputHelp()` and `parseAsync(process.argv)` at module top level, so importing the package ran the CLI against the host's argv and never returned. `bin` does not resolve through `exports`, so the executable is unaffected. `tests/integration/runtime-contract.test.ts` pins both the absence of `.` and the side effect that justifies it.
|
|
112
112
|
- **`postinstall`:** `scripts/postinstall-check-bun.mjs` reports a missing Bun — npm ignores `engines.bun`, so this is the only signal an npm-only machine gets before `dbcli` refuses to start. The command is `bun … || node …` so it runs under whichever runtime is present: Bun-only machines have no `node` to invoke it with, and a hard `node` dependency there would break the primary install path. It must **not** end in `|| exit 0`; v1.55.0 shipped with that mask and npm, which hides lifecycle output unless a script fails, therefore showed nothing at all. On a global install (`npm_config_global === 'true'`) a missing Bun exits 1, so the reason is printed and npm rolls the `bin` back; a dependency install only warns, because that is the `./agent-core` consumer. Both outcomes are pinned in `tests/integration/runtime-contract.test.ts`.
|
package/README.md
CHANGED
|
@@ -1715,6 +1715,83 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for full setup, testing, and release pr
|
|
|
1715
1715
|
|
|
1716
1716
|
---
|
|
1717
1717
|
|
|
1718
|
+
## Governance
|
|
1719
|
+
|
|
1720
|
+
For people changing this repository. dbcli itself depends on none of the tools
|
|
1721
|
+
below: `package.json` and `src/` never mention them, `.forgepilot/` is
|
|
1722
|
+
gitignored local state, and a clone without any of them runs `make verify`
|
|
1723
|
+
normally.
|
|
1724
|
+
|
|
1725
|
+
### How work is specified and reviewed: ForgeFlow
|
|
1726
|
+
|
|
1727
|
+
Each unit of work is a Story under `specs/stories/`, made of a `story.md` (the
|
|
1728
|
+
intent boundary) and an `acceptance.md` (the criteria). That shape is not a
|
|
1729
|
+
local convention — it is upstream ForgeFlowV2's contract. The adopted version
|
|
1730
|
+
and revision are pinned in `specs/.forgeflow-adoption`, and the CI
|
|
1731
|
+
`forgeflow-contract` job clones **that exact revision** and runs upstream's own
|
|
1732
|
+
checkers against this repository (`bun run forgeflow:contract`, which needs
|
|
1733
|
+
`FORGEFLOW_ROOT`). The contract is executed by upstream's code rather than
|
|
1734
|
+
reimplemented here, because two implementations of one contract diverge on a
|
|
1735
|
+
schedule nobody controls (ADR-0027).
|
|
1736
|
+
|
|
1737
|
+
A Story declaring itself security sensitive must enumerate its trust-boundary
|
|
1738
|
+
fields and a security fixture matrix, and **every cell must be an exact value**
|
|
1739
|
+
rather than prose. That rule catches more than wording: clearing the last of it
|
|
1740
|
+
turned up three fixture rows that asserted nothing at all, and one row
|
|
1741
|
+
describing a mechanism that does not exist.
|
|
1742
|
+
|
|
1743
|
+
The exemption list in `scripts/lib/forgeflow-contract.ts` is a ratchet that may
|
|
1744
|
+
shrink and never grow. It is currently empty, so any finding at all fails the
|
|
1745
|
+
gate; adding an entry back requires lowering — never raising — the two counts
|
|
1746
|
+
beside it, which a test enforces.
|
|
1747
|
+
|
|
1748
|
+
### When work is done: ForgePilot and `make verify`
|
|
1749
|
+
|
|
1750
|
+
`make verify` is the automated completion authority. Passing it makes work
|
|
1751
|
+
*eligible* for human review; it does not approve or merge anything. Do not
|
|
1752
|
+
bypass verification or delete failing tests to obtain a pass.
|
|
1753
|
+
|
|
1754
|
+
ForgePilot is an operator tool that holds the work queue and the verification
|
|
1755
|
+
evidence. It runs this repository's own `make verify` inside a detached worktree
|
|
1756
|
+
at an exact commit, so a recorded pass is bound to that revision rather than
|
|
1757
|
+
meaning "it passes on a machine that already has everything installed". It has
|
|
1758
|
+
no completion command — only a human can approve.
|
|
1759
|
+
|
|
1760
|
+
### Where decisions live: `docs/adr/`
|
|
1761
|
+
|
|
1762
|
+
A decision that is hard to reverse, surprising without context, or the result of
|
|
1763
|
+
a real trade-off gets a record in `docs/adr/`, named `ADR-<digits>-<slug>.md`
|
|
1764
|
+
with its status as a `* Status:` bullet rather than front matter. That shape
|
|
1765
|
+
exists so a Story's `Decision:` field resolves to it mechanically (ADR-0029).
|
|
1766
|
+
Open questions live there as `status: proposed`; undecided is a state that has
|
|
1767
|
+
to be storable.
|
|
1768
|
+
|
|
1769
|
+
Prefer closing a record with a falsification condition — one sentence saying
|
|
1770
|
+
when the decision would no longer hold — naming the files it depends on in
|
|
1771
|
+
backticks. Tooling reads the boundary list from those backticks, so the marker
|
|
1772
|
+
is load-bearing rather than decorative.
|
|
1773
|
+
|
|
1774
|
+
### The principle behind all of it
|
|
1775
|
+
|
|
1776
|
+
Every gate here is a variation on one lesson: **a declaration nothing compares
|
|
1777
|
+
to anything is not a control.** Delivery claims in the handoff are reconciled
|
|
1778
|
+
against commit trailers; the adopted ForgeFlow version is reconciled against
|
|
1779
|
+
every place that restates it; the ForgeFlow contract itself is run against a
|
|
1780
|
+
pinned upstream revision.
|
|
1781
|
+
|
|
1782
|
+
The lesson has a limit, and finding it cost a decision. A Story's `## Authority`
|
|
1783
|
+
was briefly reconciled against the fact of its delivery, on the reasoning that
|
|
1784
|
+
reaching `main` requires commits on a pushed branch (ADR-0030). It does — but
|
|
1785
|
+
`completed_stories` records that a Story was delivered, not **who** performed
|
|
1786
|
+
each operation, and the normal flow here is an agent that is granted `modify` and
|
|
1787
|
+
a local `commit` handing its branch to a human who pushes and merges. The gate
|
|
1788
|
+
refused that flow by name, so the rule was removed (ADR-0031). Whether an
|
|
1789
|
+
Authority declaration is true is a question this repository cannot observe and
|
|
1790
|
+
Human Review can; a gate that guesses at it is worse than an unchecked
|
|
1791
|
+
declaration, because its verdict looks like evidence.
|
|
1792
|
+
|
|
1793
|
+
---
|
|
1794
|
+
|
|
1718
1795
|
## License
|
|
1719
1796
|
|
|
1720
1797
|
See LICENSE file for details.
|
package/assets/reference.md
CHANGED
|
@@ -3872,7 +3872,7 @@ GET /orders/_search
|
|
|
3872
3872
|
|
|
3873
3873
|
- Enter a request line `<METHOD> /<path>`, then an optional multi-line JSON body; a **blank line** submits the block. Responses render as pretty-printed JSON.
|
|
3874
3874
|
- **The connection's permission tier applies, as it does to `dbcli query`.** The request is classified through the same classifier the query path uses, on the path the server will actually route — the query string is discarded and percent-encoding and dot segments are resolved first, so `?filter_path=...` cannot change the verdict.
|
|
3875
|
-
- **Reads are an allowlist; anything else needs `admin`.** Permitted below `admin`: `_search` and `_count` on an index or unscoped, `_doc` / `_source` reads, `_mapping` / `_settings` / `_alias` reads, `_cat/*` (except `_cat/aliases` and `_cat/tasks`), `_cluster/health`, and `GET` / `HEAD` of a bare index name. Writes to one document are the usual tiers. **Everything else — including any endpoint not listed here — requires `admin`**, so `DELETE /<index>`, `DELETE /_all`, `_delete_by_query`, `_update_by_query`, `_reindex`, `_aliases`, `PUT /<index>/_mapping`, `PUT /<index>/_settings`, `_sql`, `_scripts`, `_security`, `_snapshot`, `_nodes` and `_cluster/state` are all refused below it. The list is a floor, not a claim that everything absent from it is dangerous: a missing entry costs you an unnecessary `admin` requirement rather than a bypass. See [ADR-0014](https://github.com/CarlLee1983/dbcli/blob/main/docs/adr/0014-elasticsearch-reads-are-an-allowlist.md).
|
|
3875
|
+
- **Reads are an allowlist; anything else needs `admin`.** Permitted below `admin`: `_search` and `_count` on an index or unscoped, `_doc` / `_source` reads, `_mapping` / `_settings` / `_alias` reads, `_cat/*` (except `_cat/aliases` and `_cat/tasks`), `_cluster/health`, and `GET` / `HEAD` of a bare index name. Writes to one document are the usual tiers. **Everything else — including any endpoint not listed here — requires `admin`**, so `DELETE /<index>`, `DELETE /_all`, `_delete_by_query`, `_update_by_query`, `_reindex`, `_aliases`, `PUT /<index>/_mapping`, `PUT /<index>/_settings`, `_sql`, `_scripts`, `_security`, `_snapshot`, `_nodes` and `_cluster/state` are all refused below it. The list is a floor, not a claim that everything absent from it is dangerous: a missing entry costs you an unnecessary `admin` requirement rather than a bypass. See [ADR-0014](https://github.com/CarlLee1983/dbcli/blob/main/docs/adr/ADR-0014-elasticsearch-reads-are-an-allowlist.md).
|
|
3876
3876
|
- A path is refused unless it is **byte-identical** to what the URL parser produces — the same parser the request goes through — and the refusal tells you the canonical spelling to write instead. This is what stops a path that reads one way here and routes another way at the server: `#` truncates a path in transit (`POST /_reindex#/_count` reaches Elasticsearch as `POST /_reindex`), and tab, newline and `\` are rewritten too. A document id containing a space or a non-ASCII character must be written percent-encoded; the error message gives you the exact string.
|
|
3877
3877
|
- A quoted string request body is refused; write the body as JSON. So is a `source=` query parameter — Elasticsearch accepts it in place of a body, where none of the body checks can see it. `_source`, `_source_includes` and `_source_excludes` are unaffected.
|
|
3878
3878
|
- Blacklisted field names are refused in the query string as well as the body, because the URI-search form names fields directly (`?q=…`, `?sort=…`, `?docvalue_fields=…`). A name matches on any dot component, so `password.keyword` — the multi-field every `text` field gets by default — and `params._source.password` inside a script are both refused.
|