pg-admin-mcp 0.0.0-stage → 0.1.0
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/README.md +608 -2
- package/dist/config/env.js +67 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/profiles.js +257 -0
- package/dist/config/profiles.js.map +1 -0
- package/dist/core/audit-context.js +21 -0
- package/dist/core/audit-context.js.map +1 -0
- package/dist/core/audit.js +91 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/brands.js +24 -0
- package/dist/core/brands.js.map +1 -0
- package/dist/core/classifier.js +279 -0
- package/dist/core/classifier.js.map +1 -0
- package/dist/core/errors.js +197 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/format.js +258 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/guards.js +513 -0
- package/dist/core/guards.js.map +1 -0
- package/dist/core/identifiers.js +431 -0
- package/dist/core/identifiers.js.map +1 -0
- package/dist/core/limits.js +79 -0
- package/dist/core/limits.js.map +1 -0
- package/dist/core/logger.js +34 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/paginate.js +195 -0
- package/dist/core/paginate.js.map +1 -0
- package/dist/core/paths.js +392 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/policy.js +98 -0
- package/dist/core/policy.js.map +1 -0
- package/dist/core/shutdown.js +10 -0
- package/dist/core/shutdown.js.map +1 -0
- package/dist/core/stdout-guard.js +25 -0
- package/dist/core/stdout-guard.js.map +1 -0
- package/dist/db/catalog.js +1006 -0
- package/dist/db/catalog.js.map +1 -0
- package/dist/db/pool.js +158 -0
- package/dist/db/pool.js.map +1 -0
- package/dist/db/session.js +258 -0
- package/dist/db/session.js.map +1 -0
- package/dist/db/types.js +2 -0
- package/dist/db/types.js.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/index.js +88 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/runtime/binaries.js +356 -0
- package/dist/runtime/binaries.js.map +1 -0
- package/dist/runtime/env-file.js +62 -0
- package/dist/runtime/env-file.js.map +1 -0
- package/dist/runtime/environment.js +34 -0
- package/dist/runtime/environment.js.map +1 -0
- package/dist/server.js +106 -0
- package/dist/server.js.map +1 -0
- package/dist/snapshot/capture.js +189 -0
- package/dist/snapshot/capture.js.map +1 -0
- package/dist/snapshot/diff.js +254 -0
- package/dist/snapshot/diff.js.map +1 -0
- package/dist/snapshot/migrate.js +1596 -0
- package/dist/snapshot/migrate.js.map +1 -0
- package/dist/snapshot/model.js +80 -0
- package/dist/snapshot/model.js.map +1 -0
- package/dist/tools/_archive_meta.js +138 -0
- package/dist/tools/_archive_meta.js.map +1 -0
- package/dist/tools/_batch_shared.js +73 -0
- package/dist/tools/_batch_shared.js.map +1 -0
- package/dist/tools/_export_format.js +107 -0
- package/dist/tools/_export_format.js.map +1 -0
- package/dist/tools/_object_kinds.js +97 -0
- package/dist/tools/_object_kinds.js.map +1 -0
- package/dist/tools/_relation_detail.js +55 -0
- package/dist/tools/_relation_detail.js.map +1 -0
- package/dist/tools/_shared.js +17 -0
- package/dist/tools/_shared.js.map +1 -0
- package/dist/tools/_snapshot_io.js +238 -0
- package/dist/tools/_snapshot_io.js.map +1 -0
- package/dist/tools/admin/_shared.js +18 -0
- package/dist/tools/admin/_shared.js.map +1 -0
- package/dist/tools/admin/extensions.js +80 -0
- package/dist/tools/admin/extensions.js.map +1 -0
- package/dist/tools/admin/grants.js +207 -0
- package/dist/tools/admin/grants.js.map +1 -0
- package/dist/tools/admin/index.js +138 -0
- package/dist/tools/admin/index.js.map +1 -0
- package/dist/tools/admin/maintenance.js +106 -0
- package/dist/tools/admin/maintenance.js.map +1 -0
- package/dist/tools/admin/roles.js +144 -0
- package/dist/tools/admin/roles.js.map +1 -0
- package/dist/tools/admin/sessions.js +104 -0
- package/dist/tools/admin/sessions.js.map +1 -0
- package/dist/tools/admin/stats.js +63 -0
- package/dist/tools/admin/stats.js.map +1 -0
- package/dist/tools/backup.js +288 -0
- package/dist/tools/backup.js.map +1 -0
- package/dist/tools/connection_status.js +138 -0
- package/dist/tools/connection_status.js.map +1 -0
- package/dist/tools/copy.js +355 -0
- package/dist/tools/copy.js.map +1 -0
- package/dist/tools/delete_batch.js +73 -0
- package/dist/tools/delete_batch.js.map +1 -0
- package/dist/tools/describe_object.js +87 -0
- package/dist/tools/describe_object.js.map +1 -0
- package/dist/tools/execute_sql.js +395 -0
- package/dist/tools/execute_sql.js.map +1 -0
- package/dist/tools/explain.js +134 -0
- package/dist/tools/explain.js.map +1 -0
- package/dist/tools/export.js +215 -0
- package/dist/tools/export.js.map +1 -0
- package/dist/tools/get_schema.js +148 -0
- package/dist/tools/get_schema.js.map +1 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/insert_batch.js +212 -0
- package/dist/tools/insert_batch.js.map +1 -0
- package/dist/tools/list_objects.js +160 -0
- package/dist/tools/list_objects.js.map +1 -0
- package/dist/tools/prepare_migration.js +112 -0
- package/dist/tools/prepare_migration.js.map +1 -0
- package/dist/tools/restore.js +301 -0
- package/dist/tools/restore.js.map +1 -0
- package/dist/tools/schema_diff.js +79 -0
- package/dist/tools/schema_diff.js.map +1 -0
- package/dist/tools/schema_snapshot.js +57 -0
- package/dist/tools/schema_snapshot.js.map +1 -0
- package/dist/tools/select.js +162 -0
- package/dist/tools/select.js.map +1 -0
- package/dist/tools/update_batch.js +154 -0
- package/dist/tools/update_batch.js.map +1 -0
- package/package.json +47 -5
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { withTx } from '../db/session.js';
|
|
3
|
+
import { noteAudit } from './audit-context.js';
|
|
4
|
+
import { scanSegments } from './classifier.js';
|
|
5
|
+
import { PgMcpError } from './errors.js';
|
|
6
|
+
import { confirmPolicyFor, effectiveRowDrift, requireCapability } from './policy.js';
|
|
7
|
+
function missingWhere(message, hint, ctx, details = {}) {
|
|
8
|
+
throw new PgMcpError({
|
|
9
|
+
code: 'GUARD_MISSING_WHERE',
|
|
10
|
+
message,
|
|
11
|
+
hint,
|
|
12
|
+
details: {
|
|
13
|
+
profile: ctx.profile,
|
|
14
|
+
operation: ctx.operation,
|
|
15
|
+
...(ctx.target === undefined ? {} : { target: ctx.target }),
|
|
16
|
+
...details,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** A structured `where` must carry at least one predicate — an empty array is an unfiltered statement written the polite way. */
|
|
21
|
+
export function assertStructuredWhere(predicateCount, ctx) {
|
|
22
|
+
if (predicateCount > 0)
|
|
23
|
+
return;
|
|
24
|
+
missingWhere(`${ctx.operation} was called with no filter, which would affect every row of ${ctx.target ?? 'the target relation'}`, 'add at least one predicate to `where`; to empty a table deliberately, use postgres_execute_sql with TRUNCATE ' +
|
|
25
|
+
'on a profile that allows DDL', ctx);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Words that carry no row-dependence. A WHERE clause built **only** out of
|
|
29
|
+
* these plus literals and operators matches every row, so it is refused.
|
|
30
|
+
* Anything else — a column, a function call, a subquery — counts as a real
|
|
31
|
+
* reference and passes.
|
|
32
|
+
*/
|
|
33
|
+
const CONSTANT_WORDS = new Set([
|
|
34
|
+
'and',
|
|
35
|
+
'or',
|
|
36
|
+
'not',
|
|
37
|
+
'is',
|
|
38
|
+
'isnull',
|
|
39
|
+
'notnull',
|
|
40
|
+
'true',
|
|
41
|
+
'false',
|
|
42
|
+
'null',
|
|
43
|
+
'unknown',
|
|
44
|
+
'between',
|
|
45
|
+
'symmetric',
|
|
46
|
+
'asymmetric',
|
|
47
|
+
'in',
|
|
48
|
+
'like',
|
|
49
|
+
'ilike',
|
|
50
|
+
'similar',
|
|
51
|
+
'to',
|
|
52
|
+
'escape',
|
|
53
|
+
'distinct',
|
|
54
|
+
'from',
|
|
55
|
+
'as',
|
|
56
|
+
'collate',
|
|
57
|
+
'cast',
|
|
58
|
+
'uescape',
|
|
59
|
+
]);
|
|
60
|
+
/**
|
|
61
|
+
* The letters PostgreSQL allows in front of a string literal (`E'…'`, `B'…'`,
|
|
62
|
+
* `X'…'`, `N'…'`, `U&'…'`). The scanner hands the literal back as a literal but
|
|
63
|
+
* the prefix back as *code*, where a bare `E` reads exactly like a column name —
|
|
64
|
+
* which is how `WHERE E'a' = E'a'` used to pass the check below.
|
|
65
|
+
*/
|
|
66
|
+
const LITERAL_PREFIX_RE = /(?:[EeBbXxNn]|[Uu]&)$/;
|
|
67
|
+
/**
|
|
68
|
+
* A word that could be a column or a function name. The non-ASCII range is
|
|
69
|
+
* spelled with escapes so this file stays pure ASCII, same rule as
|
|
70
|
+
* `core/classifier.ts` and for the same reason: an invisible character in a
|
|
71
|
+
* security-relevant scanner is what nobody spots in review.
|
|
72
|
+
*/
|
|
73
|
+
const WORD_RE = /[A-Za-z_\u0080-\uFFFF][A-Za-z0-9_$\u0080-\uFFFF]*/g;
|
|
74
|
+
/**
|
|
75
|
+
* A cast's type name (`'a'::text`, `id::bigint[]`) is code, but it is no more a
|
|
76
|
+
* column reference than the `text` in `CAST('a' AS text)` is. Struck out before
|
|
77
|
+
* the word scan so `WHERE 'a'::text = 'a'` is refused like `WHERE 'a' = 'a'`.
|
|
78
|
+
* Built from {@link WORD_RE} so the two agree on what a word is.
|
|
79
|
+
*/
|
|
80
|
+
const CAST_TYPE_RE = new RegExp(`::\\s*${WORD_RE.source}(?:\\s*\\[\\s*\\])*`, 'g');
|
|
81
|
+
/**
|
|
82
|
+
* Blanks every literal, comment and terminator, preserving offsets, so a
|
|
83
|
+
* keyword search sees only real SQL code. `WHERE` inside `'…'`, `"…"`,
|
|
84
|
+
* `$tag$…$tag$`, `-- …` or a nested `/* … *\/` is invisible here, exactly as it
|
|
85
|
+
* is to PostgreSQL. The scanner is `core/classifier.ts`'s — the same one that
|
|
86
|
+
* backs multi-statement rejection, so the two cannot disagree about where a
|
|
87
|
+
* literal ends.
|
|
88
|
+
*/
|
|
89
|
+
function codeMask(sql) {
|
|
90
|
+
let out = '';
|
|
91
|
+
for (const seg of scanSegments(sql)) {
|
|
92
|
+
const text = sql.slice(seg.start, seg.end);
|
|
93
|
+
if (seg.kind === 'code') {
|
|
94
|
+
out += text;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
// The `E`/`B`/`X`/`N`/`U&` in front of a literal is part of the literal's
|
|
98
|
+
// syntax, not a reference to anything — blank it with the literal it opens.
|
|
99
|
+
// Length-preserving, so offsets still line up.
|
|
100
|
+
out = out.replace(LITERAL_PREFIX_RE, (prefix) => ' '.repeat(prefix.length));
|
|
101
|
+
out += ' '.repeat(text.length);
|
|
102
|
+
}
|
|
103
|
+
// `segments` partitions the whole input, so offsets are preserved exactly.
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/** Index just past a depth-0 occurrence of `keyword`, or -1. Parenthesised occurrences (a subquery's own WHERE) are skipped. */
|
|
107
|
+
function findTopLevelKeyword(mask, keyword) {
|
|
108
|
+
const re = new RegExp(`\\b${keyword}\\b`, 'gi');
|
|
109
|
+
let depth = 0;
|
|
110
|
+
let scanned = 0;
|
|
111
|
+
for (let match = re.exec(mask); match !== null; match = re.exec(mask)) {
|
|
112
|
+
for (; scanned < match.index; scanned++) {
|
|
113
|
+
const ch = mask.charAt(scanned);
|
|
114
|
+
if (ch === '(')
|
|
115
|
+
depth++;
|
|
116
|
+
else if (ch === ')')
|
|
117
|
+
depth--;
|
|
118
|
+
}
|
|
119
|
+
if (depth === 0)
|
|
120
|
+
return match.index + match[0].length;
|
|
121
|
+
}
|
|
122
|
+
return -1;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Rejects a raw `UPDATE`/`DELETE` whose WHERE clause is absent or matches
|
|
126
|
+
* everything.
|
|
127
|
+
*
|
|
128
|
+
* **What this cannot do, stated plainly.** It is a lexical check, not a
|
|
129
|
+
* planner. `WHERE id = id`, `WHERE x IS NOT NULL` on a `NOT NULL` column,
|
|
130
|
+
* `WHERE coalesce(x, x) = x`, `WHERE id > -1` — every one of those references a
|
|
131
|
+
* column, passes here, and still matches every row. Enumerating tautologies is
|
|
132
|
+
* a losing game, and a check that appeared to win it would be trusted as a
|
|
133
|
+
* guarantee it cannot provide.
|
|
134
|
+
*
|
|
135
|
+
* So this guard catches exactly one class: the clause that is *obviously*
|
|
136
|
+
* unconditional because it mentions no column at all — no WHERE, `WHERE true`,
|
|
137
|
+
* `WHERE 1=1`, `WHERE 'a' = 'a'`, `WHERE E'a' = E'a'`, `WHERE 'a'::text = 'a'`,
|
|
138
|
+
* `WHERE (1=1) AND true`, `WHERE NOT false`. It is the cheap guard, and its
|
|
139
|
+
* value is a fast, readable refusal.
|
|
140
|
+
*
|
|
141
|
+
* Even inside that one class it is lexical, so a spelling that hides a constant
|
|
142
|
+
* behind something word-shaped still slips past: `CAST('a' AS text) = 'a'` (the
|
|
143
|
+
* type name after `AS` is indistinguishable from a column here),
|
|
144
|
+
* `'a'::pg_catalog.text = 'a'`, `length('a') > 0`. Those are refusals not made,
|
|
145
|
+
* never wrong commits — and the row count below is what actually stops one.
|
|
146
|
+
*
|
|
147
|
+
* **The row count is the real guard.** The dry run runs the actual mutation and
|
|
148
|
+
* reports the actual number of rows it touched; an agent that asked to delete
|
|
149
|
+
* one row and is shown 4,000,000 has the information the WHERE check could
|
|
150
|
+
* never have derived. Do not oversell this function.
|
|
151
|
+
*/
|
|
152
|
+
export function assertRawWhere(sql, ctx) {
|
|
153
|
+
const mask = codeMask(sql);
|
|
154
|
+
const whereEnd = findTopLevelKeyword(mask, 'WHERE');
|
|
155
|
+
if (whereEnd === -1) {
|
|
156
|
+
missingWhere(`${ctx.operation} received a statement with no top-level WHERE clause, which would affect every row`, 'add a WHERE clause that selects only the rows you mean; to empty a table deliberately, use TRUNCATE on a ' +
|
|
157
|
+
'profile that allows DDL', ctx);
|
|
158
|
+
}
|
|
159
|
+
// `DELETE … WHERE … RETURNING *` — the RETURNING list is not part of the filter.
|
|
160
|
+
const returningStart = findTopLevelKeyword(mask.slice(whereEnd), 'RETURNING');
|
|
161
|
+
const clause = returningStart === -1 ? mask.slice(whereEnd) : mask.slice(whereEnd, whereEnd + returningStart - 'RETURNING'.length);
|
|
162
|
+
const words = clause.replace(CAST_TYPE_RE, '').match(WORD_RE) ?? [];
|
|
163
|
+
if (words.some((word) => !CONSTANT_WORDS.has(word.toLowerCase())))
|
|
164
|
+
return;
|
|
165
|
+
missingWhere(`${ctx.operation} received a WHERE clause that references no column (${clause.trim() === '' ? 'empty' : clause.trim()}), so it matches every row`, 'add a predicate on a real column; to empty a table deliberately, use TRUNCATE on a profile that allows DDL', ctx, { clause: clause.trim() });
|
|
166
|
+
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// The hash
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/**
|
|
171
|
+
* Canonicalizes SQL text for hashing. **Exactly two things change, and nothing
|
|
172
|
+
* else:**
|
|
173
|
+
*
|
|
174
|
+
* 1. **Whitespace runs outside literals and comments collapse.** A run
|
|
175
|
+
* containing a newline becomes a single `\n`; any other run becomes a single
|
|
176
|
+
* space. Respacing within a line is free; **introducing or removing a line
|
|
177
|
+
* break is not** and costs one extra dry run.
|
|
178
|
+
*
|
|
179
|
+
* That asymmetry is the point, not an oversight. A line comment is
|
|
180
|
+
* terminated by a newline, so collapsing newlines into spaces would give
|
|
181
|
+
* `SELECT 1 -- c\nFROM t` and `SELECT 1 -- c FROM t` — two statements that
|
|
182
|
+
* mean entirely different things — the same digest, and a confirming call
|
|
183
|
+
* could then execute something the dry run never previewed. Every
|
|
184
|
+
* normalization decision in this function fails in the *mismatch* direction:
|
|
185
|
+
* the worst case is a re-run of the dry run, never a wrong commit.
|
|
186
|
+
* Whitespace inside a string, a quoted identifier or a dollar-quoted body is
|
|
187
|
+
* part of the value and is left alone.
|
|
188
|
+
* 2. **Trailing `;` and trailing whitespace are dropped**, and internal
|
|
189
|
+
* terminators normalize to a bare `;`. `DELETE …` and `DELETE …;` are the
|
|
190
|
+
* same statement.
|
|
191
|
+
*
|
|
192
|
+
* Deliberately **not** normalized:
|
|
193
|
+
*
|
|
194
|
+
* - **Case.** `"Col"` and `"col"` are different columns; `$Tag$` and `$tag$`
|
|
195
|
+
* are different dollar-quote tags. Folding buys nothing (an agent replaying a
|
|
196
|
+
* statement does not re-case it) and could merge two statements that differ.
|
|
197
|
+
* - **Comments.** Stripping them would buy full reformat-insensitivity, and it
|
|
198
|
+
* was rejected: it would make a *security* decision depend on the scanner
|
|
199
|
+
* agreeing with PostgreSQL about where a comment ends. Keeping them means a
|
|
200
|
+
* scanner disagreement can only produce a false mismatch (a re-run), never a
|
|
201
|
+
* false match (a wrong commit). The hash is the state; it covers what was
|
|
202
|
+
* actually sent.
|
|
203
|
+
* - **Parameter values.** They are not in the SQL at all — they are hashed
|
|
204
|
+
* separately as `JSON(params)`, so `$1 = 7` and `$1 = 8` differ.
|
|
205
|
+
*/
|
|
206
|
+
export function normalizeSql(sql) {
|
|
207
|
+
let out = '';
|
|
208
|
+
for (const seg of scanSegments(sql)) {
|
|
209
|
+
const text = sql.slice(seg.start, seg.end);
|
|
210
|
+
if (seg.kind === 'code')
|
|
211
|
+
out += text.replace(/\s+/g, (ws) => (ws.includes('\n') ? '\n' : ' '));
|
|
212
|
+
else if (seg.kind === 'terminator')
|
|
213
|
+
out += ';';
|
|
214
|
+
else
|
|
215
|
+
out += text;
|
|
216
|
+
}
|
|
217
|
+
return out.replace(/[\s;]+$/, '').trim();
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* `sha256(profile ∥ database ∥ normalizedSql ∥ JSON(params))`, per design.md.
|
|
221
|
+
* The four parts go through `JSON.stringify` of a tuple rather than string
|
|
222
|
+
* concatenation, so the separators are unambiguous and no combination of a
|
|
223
|
+
* profile alias and a statement can be re-spelled as a different pair.
|
|
224
|
+
*
|
|
225
|
+
* **Deliberately not the cursor shape hash.** `core/paginate.ts#computeShapeHash`
|
|
226
|
+
* is 64 bits of sha256 and exists to stop a cursor being replayed against a
|
|
227
|
+
* differently-shaped query — a *correctness* construct where a collision costs
|
|
228
|
+
* a rejected page. This one is a *security* construct: it is what stands
|
|
229
|
+
* between "the agent previewed X" and "the agent committed Y", so it keeps the
|
|
230
|
+
* full 256-bit digest and lives in a different module. Do not merge them.
|
|
231
|
+
*/
|
|
232
|
+
export function computeSqlHash(input) {
|
|
233
|
+
return createHash('sha256')
|
|
234
|
+
.update(JSON.stringify([input.profile, input.database, normalizeSql(input.sql), input.params]))
|
|
235
|
+
.digest('hex');
|
|
236
|
+
}
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// The two-call protocol
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
/** Named rather than random so it is greppable in a server log. */
|
|
241
|
+
const TARGET_CTE = 'pg_admin_mcp_target';
|
|
242
|
+
const KEYS_CTE = 'pg_admin_mcp_keys';
|
|
243
|
+
/** design.md: "SELECT up to 20 affected primary keys for the preview". */
|
|
244
|
+
export const PREVIEW_LIMIT = 20;
|
|
245
|
+
/**
|
|
246
|
+
* Wraps the mutation so PostgreSQL returns the row count, the key checksum and
|
|
247
|
+
* the preview in **one** round trip.
|
|
248
|
+
*
|
|
249
|
+
* A data-modifying CTE executes exactly once, so this is the same single
|
|
250
|
+
* mutation either way — but the affected keys never cross the wire as
|
|
251
|
+
* individual rows, which matters when the statement touches millions of them.
|
|
252
|
+
*
|
|
253
|
+
* Computing the checksum from the mutation's own `RETURNING` rather than from a
|
|
254
|
+
* `SELECT` issued just before it is a strengthening, not a shortcut: there is
|
|
255
|
+
* no window at all between "the rows I measured" and "the rows I changed".
|
|
256
|
+
*
|
|
257
|
+
* `COLLATE "C"` pins the sort to byte order, so the checksum does not depend on
|
|
258
|
+
* the database's collation or on any lc_collate change between the two calls.
|
|
259
|
+
*/
|
|
260
|
+
function buildProbedSql(sql, keyColumns) {
|
|
261
|
+
return [
|
|
262
|
+
`WITH ${TARGET_CTE} AS (`,
|
|
263
|
+
sql,
|
|
264
|
+
// `::text` on every key column, deliberately: `to_jsonb` renders a bigint
|
|
265
|
+
// as a JSON *number*, and a bigint past 2^53 loses precision the moment the
|
|
266
|
+
// driver parses it — the preview would then show a key that does not exist.
|
|
267
|
+
// Text also makes the sort, and therefore the checksum, independent of the
|
|
268
|
+
// column's type.
|
|
269
|
+
`RETURNING ${keyColumns.map((column) => `${column}::text AS ${column}`).join(', ')}`,
|
|
270
|
+
`), ${KEYS_CTE} AS (`,
|
|
271
|
+
`SELECT ${TARGET_CTE}::text AS key_text, to_jsonb(${TARGET_CTE}) AS key_json FROM ${TARGET_CTE}`,
|
|
272
|
+
')',
|
|
273
|
+
'SELECT count(*)::text AS row_count,',
|
|
274
|
+
`md5(coalesce(string_agg(key_text, E'\\n' ORDER BY key_text COLLATE "C"), '')) AS key_checksum,`,
|
|
275
|
+
`to_jsonb((array_agg(key_json ORDER BY key_text COLLATE "C"))[1:${PREVIEW_LIMIT}]) AS preview`,
|
|
276
|
+
`FROM ${KEYS_CTE}`,
|
|
277
|
+
].join('\n');
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Runs the mutation. `queryMode: 'extended'` is not decoration — see
|
|
281
|
+
* `tools/execute_sql.ts`'s header: without it, `pg` uses simple-query mode
|
|
282
|
+
* whenever `params` is empty, which would run a multi-statement string as one
|
|
283
|
+
* implicit transaction. That is the structural fence behind
|
|
284
|
+
* `planStatements`.
|
|
285
|
+
*/
|
|
286
|
+
async function runMutation(client, text, params, probed) {
|
|
287
|
+
if (!probed) {
|
|
288
|
+
const res = await client.query({ text, values: [...params], queryMode: 'extended' });
|
|
289
|
+
return { rowCount: res.rowCount ?? 0, keyChecksum: undefined, preview: [] };
|
|
290
|
+
}
|
|
291
|
+
const res = await client.query({ text, values: [...params], queryMode: 'extended' });
|
|
292
|
+
const row = res.rows[0];
|
|
293
|
+
// `to_jsonb(NULL::jsonb[])` is the JSON scalar `null`, which is what a
|
|
294
|
+
// zero-row mutation produces — hence the shape check rather than `?? []`.
|
|
295
|
+
const preview = row !== undefined && Array.isArray(row.preview) ? row.preview : [];
|
|
296
|
+
return {
|
|
297
|
+
rowCount: Number(row?.row_count ?? '0'),
|
|
298
|
+
keyChecksum: row?.key_checksum,
|
|
299
|
+
preview,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Paste into every guarded tool's `description` so the honest limitation
|
|
304
|
+
* reaches the agent, not just this file.
|
|
305
|
+
*
|
|
306
|
+
* The limitation is stated as it actually behaves, which is **not** what
|
|
307
|
+
* design.md's one-line summary claims. `keyChecksum` is `md5` over the sorted
|
|
308
|
+
* list of affected **primary keys**, so it pins *which* rows a statement
|
|
309
|
+
* touches — it says nothing about their contents. A row whose non-key columns
|
|
310
|
+
* changed between the two calls, leaving the primary-key set and the row count
|
|
311
|
+
* identical, is invisible to both checks. What `keyChecksum` does close, and
|
|
312
|
+
* `rowCount` alone does not, is the substitution: one matching row disappearing
|
|
313
|
+
* while a different one starts matching keeps the count identical and changes
|
|
314
|
+
* the key set. That is why production requires it.
|
|
315
|
+
*/
|
|
316
|
+
export const CONFIRM_PROTOCOL_DESCRIPTION = 'Two calls. Call it once WITHOUT confirm to get a dry run: the real mutation runs inside a transaction that is ' +
|
|
317
|
+
'always rolled back, and you get back the true row count, up to 20 affected primary keys, and an `expect` token. ' +
|
|
318
|
+
'Read the row count — it is the guard that matters. Then call again with confirm:true and `expect` passed back ' +
|
|
319
|
+
'verbatim; the server recomputes the hash from your arguments (nothing is stored between calls), re-verifies the ' +
|
|
320
|
+
'row count and key checksum INSIDE the committing transaction, and rolls back instead of committing if anything ' +
|
|
321
|
+
'moved. LIMITATION: keyChecksum is md5 over the sorted primary keys of the affected rows, so it detects a ' +
|
|
322
|
+
'different SET of rows (including a substitution that keeps the count identical) but NOT a change to a row\'s ' +
|
|
323
|
+
'contents when its primary key and the total row count both stayed the same. envClass=production requires ' +
|
|
324
|
+
'`expect` including keyChecksum and forces allowRowDrift to 0 — the one exception is INSERT, which creates the rows ' +
|
|
325
|
+
'it affects instead of selecting pre-existing ones, so there production verifies the row count alone (still with ' +
|
|
326
|
+
'`expect` and the hash). development accepts a bare confirm:true.';
|
|
327
|
+
function confirmRequired(message, hint, profile, details = {}) {
|
|
328
|
+
throw new PgMcpError({
|
|
329
|
+
code: 'GUARD_CONFIRM_REQUIRED',
|
|
330
|
+
message,
|
|
331
|
+
hint,
|
|
332
|
+
details: { profile: profile.alias, envClass: profile.envClass, ...details },
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* The dry-run / confirm protocol, end to end.
|
|
337
|
+
*
|
|
338
|
+
* Call 1 (`confirm` absent or false) runs the mutation under
|
|
339
|
+
* `withTx({ rollbackAlways: true })` and returns a `dryRun` outcome carrying the
|
|
340
|
+
* `expect` token. Call 2 (`confirm: true`) recomputes the hash from the
|
|
341
|
+
* incoming arguments, then re-runs the mutation in a real transaction and
|
|
342
|
+
* commits only if the checksum and row count still hold.
|
|
343
|
+
*
|
|
344
|
+
* Callers must have resolved identifiers **before** calling this: it opens a
|
|
345
|
+
* transaction and takes a pool checkout, and taking a second checkout inside
|
|
346
|
+
* one is the deadlock the Phase 2 review found (`resolveRelation` opens its own
|
|
347
|
+
* session). Nothing inside this function acquires a client.
|
|
348
|
+
*/
|
|
349
|
+
export async function runGuardedMutation(req) {
|
|
350
|
+
requireCapability(req.capability, req.profile);
|
|
351
|
+
const policy = confirmPolicyFor(req.profile.envClass);
|
|
352
|
+
const executor = req.executor;
|
|
353
|
+
const wrapped = executor === undefined && req.keyColumns.length > 0;
|
|
354
|
+
const probed = executor === undefined ? wrapped : executor.providesKeyChecksum;
|
|
355
|
+
// See `noPreexistingRows`: an INSERT's affected rows are its own input, so
|
|
356
|
+
// `sqlHash` + `rowCount` is the whole receipt and production accepts it.
|
|
357
|
+
const checksumRequired = policy.requireKeyChecksum && req.noPreexistingRows !== true;
|
|
358
|
+
if (checksumRequired && !probed) {
|
|
359
|
+
// Refused here rather than after a dry run the agent could never confirm.
|
|
360
|
+
// The two ways to get here are different situations and the caller can act
|
|
361
|
+
// on exactly one of them, so they do not share a message.
|
|
362
|
+
if (executor !== undefined) {
|
|
363
|
+
confirmRequired(`profile "${req.profile.alias}" is envClass ${req.profile.envClass}, which requires expect.keyChecksum, but ` +
|
|
364
|
+
`${req.operation} issues its statement through an executor that reports no checksum`, 'use a profile whose PG_<ALIAS>_ENV is development or staging — this operation cannot pin which rows it ' +
|
|
365
|
+
'wrote, so there is nothing a production confirm could verify', req.profile, { operation: req.operation });
|
|
366
|
+
}
|
|
367
|
+
confirmRequired(`profile "${req.profile.alias}" is envClass ${req.profile.envClass}, which requires expect.keyChecksum, but ` +
|
|
368
|
+
`${req.operation} has no primary key to compute one from`, 'run this against a relation with a primary key, or use a profile whose PG_<ALIAS>_ENV is development or staging', req.profile, { operation: req.operation });
|
|
369
|
+
}
|
|
370
|
+
const text = wrapped ? buildProbedSql(req.sql, req.keyColumns) : req.sql;
|
|
371
|
+
// The hash covers the statement that actually executes, wrapper included —
|
|
372
|
+
// so the key columns are folded in for free and the digest is provably the
|
|
373
|
+
// text PostgreSQL received, not an approximation of it.
|
|
374
|
+
const sqlHash = computeSqlHash({
|
|
375
|
+
profile: req.profile.alias,
|
|
376
|
+
database: req.profile.database,
|
|
377
|
+
sql: text,
|
|
378
|
+
params: req.params,
|
|
379
|
+
});
|
|
380
|
+
const execute = async (client) => {
|
|
381
|
+
if (executor === undefined)
|
|
382
|
+
return runMutation(client, text, req.params, wrapped);
|
|
383
|
+
const done = await executor.run(client);
|
|
384
|
+
// The presence half of `providesKeyChecksum` — the only half this module can
|
|
385
|
+
// check. Without it the claim is what unlocked a production profile up
|
|
386
|
+
// front, and the missing checksum surfaces two calls later as a
|
|
387
|
+
// GUARD_CONFIRM_REQUIRED aimed at the caller for a server-side bug.
|
|
388
|
+
if (executor.providesKeyChecksum && done.keyChecksum === undefined) {
|
|
389
|
+
throw new PgMcpError({
|
|
390
|
+
code: 'PG_ERROR',
|
|
391
|
+
message: `${req.operation} declares providesKeyChecksum but returned no keyChecksum — that claim is what let this ` +
|
|
392
|
+
'call past the envClass check, so the guard refuses rather than hand out a token it cannot verify',
|
|
393
|
+
hint: 'this is a bug in the tool, not in your call — report it; nothing was committed',
|
|
394
|
+
details: { profile: req.profile.alias, operation: req.operation },
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return { rowCount: done.rowCount, keyChecksum: done.keyChecksum, preview: done.preview ?? [] };
|
|
398
|
+
};
|
|
399
|
+
const txOpts = {
|
|
400
|
+
operation: req.operation,
|
|
401
|
+
...(req.statementTimeoutMs === undefined ? {} : { statementTimeoutMs: req.statementTimeoutMs }),
|
|
402
|
+
...(req.lockTimeoutMs === undefined ? {} : { lockTimeoutMs: req.lockTimeoutMs }),
|
|
403
|
+
};
|
|
404
|
+
// Both branches below produce a `GuardedOutcome` and fall through to the
|
|
405
|
+
// single `noteAudit` call at the very end of this function instead of
|
|
406
|
+
// returning directly, so there is exactly one call site regardless of
|
|
407
|
+
// which branch ran.
|
|
408
|
+
let outcome;
|
|
409
|
+
if (req.confirm !== true) {
|
|
410
|
+
const probe = await withTx(req.profile, execute, {
|
|
411
|
+
...txOpts,
|
|
412
|
+
rollbackAlways: true,
|
|
413
|
+
});
|
|
414
|
+
outcome = {
|
|
415
|
+
kind: 'dryRun',
|
|
416
|
+
rowCount: probe.rowCount,
|
|
417
|
+
preview: probe.preview,
|
|
418
|
+
expect: {
|
|
419
|
+
sqlHash,
|
|
420
|
+
rowCount: probe.rowCount,
|
|
421
|
+
...(probe.keyChecksum === undefined ? {} : { keyChecksum: probe.keyChecksum }),
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
const expect = req.expect;
|
|
427
|
+
if (policy.requireExpect && expect === undefined) {
|
|
428
|
+
confirmRequired(`profile "${req.profile.alias}" is envClass ${req.profile.envClass}, which does not accept a bare confirm:true`, 'call this tool once without confirm to get a dry run, then re-run with confirm:true and the returned expect ' +
|
|
429
|
+
'object passed back verbatim', req.profile, { operation: req.operation });
|
|
430
|
+
}
|
|
431
|
+
if (expect !== undefined && expect.sqlHash !== sqlHash) {
|
|
432
|
+
throw new PgMcpError({
|
|
433
|
+
code: 'GUARD_HASH_MISMATCH',
|
|
434
|
+
message: `the expect.sqlHash you passed back does not match the statement this call would run — the arguments changed ` +
|
|
435
|
+
'between the dry run and the confirm, so confirming would commit something you never previewed',
|
|
436
|
+
hint: 're-run the dry run with the arguments you actually want, then confirm with the expect object it returns',
|
|
437
|
+
details: {
|
|
438
|
+
profile: req.profile.alias,
|
|
439
|
+
operation: req.operation,
|
|
440
|
+
expected: sqlHash,
|
|
441
|
+
received: expect.sqlHash,
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
// `Math.abs(NaN - n) > drift` is `false`, so a non-numeric `expect.rowCount`
|
|
446
|
+
// would sail through the drift check below and COMMIT — measured: `NaN`,
|
|
447
|
+
// `undefined` and `{}` all committed, only `'2'` and `null` were refused. No
|
|
448
|
+
// tool can send those today (`tools/_batch_shared.ts#expectTokenSchema` is
|
|
449
|
+
// `z.number().int().min(0)`), but this module's whole claim is that it never
|
|
450
|
+
// fails open, and it is the seam Phase 5's destructive sub-ops reuse. The
|
|
451
|
+
// comparison below is *also* inverted to `!(… <= …)` so a value that somehow
|
|
452
|
+
// reaches it fails closed rather than silently passing.
|
|
453
|
+
if (expect !== undefined && !Number.isInteger(expect.rowCount)) {
|
|
454
|
+
confirmRequired(`expect.rowCount is ${typeof expect.rowCount === 'number' ? String(expect.rowCount) : typeof expect.rowCount}, ` +
|
|
455
|
+
'not a whole number, so there is nothing to compare the committing statement against', 'pass back the whole expect object the dry run returned, verbatim — do not hand-build it', req.profile, { operation: req.operation });
|
|
456
|
+
}
|
|
457
|
+
if (checksumRequired && expect?.keyChecksum === undefined) {
|
|
458
|
+
confirmRequired(`profile "${req.profile.alias}" is envClass ${req.profile.envClass}, which requires expect.keyChecksum`, 'pass back the whole expect object the dry run returned, including keyChecksum — do not hand-build it', req.profile, { operation: req.operation });
|
|
459
|
+
}
|
|
460
|
+
const allowedDrift = effectiveRowDrift(policy, req.allowRowDrift, req.profile.alias);
|
|
461
|
+
// Everything below runs INSIDE the committing transaction. A throw here makes
|
|
462
|
+
// `withTx` ROLLBACK, so a concurrent change between the two calls aborts the
|
|
463
|
+
// commit rather than silently widening the blast radius.
|
|
464
|
+
const applied = await withTx(req.profile, async (client) => {
|
|
465
|
+
const result = await execute(client);
|
|
466
|
+
if (expect?.keyChecksum !== undefined && result.keyChecksum !== expect.keyChecksum) {
|
|
467
|
+
throw new PgMcpError({
|
|
468
|
+
code: 'GUARD_ROW_DRIFT',
|
|
469
|
+
message: `the set of rows this statement affects changed since the dry run: expected key checksum ` +
|
|
470
|
+
`${expect.keyChecksum}, got ${result.keyChecksum ?? '(none)'} — the row count alone would not have ` +
|
|
471
|
+
'caught this, so a different set of rows was about to be committed',
|
|
472
|
+
hint: 're-run the dry run to see the current target rows, then confirm with the new expect object',
|
|
473
|
+
details: {
|
|
474
|
+
profile: req.profile.alias,
|
|
475
|
+
operation: req.operation,
|
|
476
|
+
expectedKeyChecksum: expect.keyChecksum,
|
|
477
|
+
actualKeyChecksum: result.keyChecksum ?? null,
|
|
478
|
+
expectedRowCount: expect.rowCount,
|
|
479
|
+
actualRowCount: result.rowCount,
|
|
480
|
+
// Reported even though this check ignores it: an agent that passed
|
|
481
|
+
// allowRowDrift needs to see the value that actually applied, or a
|
|
482
|
+
// refusal on a production profile looks like the tolerance was lost.
|
|
483
|
+
allowRowDrift: allowedDrift,
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
// `!(… <= …)`, never `… > …`: the negated form is the one that refuses a
|
|
488
|
+
// NaN instead of passing it. `expect.rowCount` is already validated above;
|
|
489
|
+
// this is the second lock on the same door.
|
|
490
|
+
if (expect !== undefined && !(Math.abs(result.rowCount - expect.rowCount) <= allowedDrift)) {
|
|
491
|
+
throw new PgMcpError({
|
|
492
|
+
code: 'GUARD_ROW_DRIFT',
|
|
493
|
+
message: `this statement affected ${result.rowCount} row(s) but the dry run previewed ${expect.rowCount}` +
|
|
494
|
+
`${allowedDrift > 0 ? ` (allowRowDrift ${allowedDrift})` : ''} — the data changed between the two calls, ` +
|
|
495
|
+
'so the transaction was rolled back instead of committed',
|
|
496
|
+
hint: 're-run the dry run to see the current row count, then confirm with the new expect object',
|
|
497
|
+
details: {
|
|
498
|
+
profile: req.profile.alias,
|
|
499
|
+
operation: req.operation,
|
|
500
|
+
expectedRowCount: expect.rowCount,
|
|
501
|
+
actualRowCount: result.rowCount,
|
|
502
|
+
allowRowDrift: allowedDrift,
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
return result;
|
|
507
|
+
}, txOpts);
|
|
508
|
+
outcome = { kind: 'mutation', rowCount: applied.rowCount, committed: true };
|
|
509
|
+
}
|
|
510
|
+
noteAudit({ sql: text, sqlHash, rowCount: outcome.rowCount });
|
|
511
|
+
return outcome;
|
|
512
|
+
}
|
|
513
|
+
//# sourceMappingURL=guards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../../src/core/guards.ts"],"names":[],"mappings":"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"}
|