@negusdev/dbask 0.0.0-stage → 0.1.8

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.
Files changed (161) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +15 -0
  3. package/README.md +422 -2
  4. package/dist/ai/factory.d.ts +4 -0
  5. package/dist/ai/factory.d.ts.map +1 -0
  6. package/dist/ai/factory.js +55 -0
  7. package/dist/ai/factory.js.map +1 -0
  8. package/dist/ai/provider.d.ts +13 -0
  9. package/dist/ai/provider.d.ts.map +1 -0
  10. package/dist/ai/provider.js +2 -0
  11. package/dist/ai/provider.js.map +1 -0
  12. package/dist/ai/sdk-provider.d.ts +13 -0
  13. package/dist/ai/sdk-provider.d.ts.map +1 -0
  14. package/dist/ai/sdk-provider.js +36 -0
  15. package/dist/ai/sdk-provider.js.map +1 -0
  16. package/dist/cli-args.d.ts +26 -0
  17. package/dist/cli-args.d.ts.map +1 -0
  18. package/dist/cli-args.js +72 -0
  19. package/dist/cli-args.js.map +1 -0
  20. package/dist/cli-observer.d.ts +23 -0
  21. package/dist/cli-observer.d.ts.map +1 -0
  22. package/dist/cli-observer.js +224 -0
  23. package/dist/cli-observer.js.map +1 -0
  24. package/dist/cli.d.ts +3 -0
  25. package/dist/cli.d.ts.map +1 -0
  26. package/dist/cli.js +127 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/commands/init.d.ts +2 -0
  29. package/dist/commands/init.d.ts.map +1 -0
  30. package/dist/commands/init.js +247 -0
  31. package/dist/commands/init.js.map +1 -0
  32. package/dist/commands/validate.d.ts +31 -0
  33. package/dist/commands/validate.d.ts.map +1 -0
  34. package/dist/commands/validate.js +205 -0
  35. package/dist/commands/validate.js.map +1 -0
  36. package/dist/config/config.d.ts +42 -0
  37. package/dist/config/config.d.ts.map +1 -0
  38. package/dist/config/config.js +94 -0
  39. package/dist/config/config.js.map +1 -0
  40. package/dist/context/db-context.d.ts +12 -0
  41. package/dist/context/db-context.d.ts.map +1 -0
  42. package/dist/context/db-context.js +29 -0
  43. package/dist/context/db-context.js.map +1 -0
  44. package/dist/core/agent/agent.d.ts +57 -0
  45. package/dist/core/agent/agent.d.ts.map +1 -0
  46. package/dist/core/agent/agent.js +518 -0
  47. package/dist/core/agent/agent.js.map +1 -0
  48. package/dist/core/agent/index.d.ts +4 -0
  49. package/dist/core/agent/index.d.ts.map +1 -0
  50. package/dist/core/agent/index.js +2 -0
  51. package/dist/core/agent/index.js.map +1 -0
  52. package/dist/core/agent/observability.d.ts +19 -0
  53. package/dist/core/agent/observability.d.ts.map +1 -0
  54. package/dist/core/agent/observability.js +4 -0
  55. package/dist/core/agent/observability.js.map +1 -0
  56. package/dist/core/agent/query-plan.d.ts +68 -0
  57. package/dist/core/agent/query-plan.d.ts.map +1 -0
  58. package/dist/core/agent/query-plan.js +282 -0
  59. package/dist/core/agent/query-plan.js.map +1 -0
  60. package/dist/core/tools/describe-table.d.ts +6 -0
  61. package/dist/core/tools/describe-table.d.ts.map +1 -0
  62. package/dist/core/tools/describe-table.js +25 -0
  63. package/dist/core/tools/describe-table.js.map +1 -0
  64. package/dist/core/tools/describe-tables.d.ts +6 -0
  65. package/dist/core/tools/describe-tables.d.ts.map +1 -0
  66. package/dist/core/tools/describe-tables.js +22 -0
  67. package/dist/core/tools/describe-tables.js.map +1 -0
  68. package/dist/core/tools/execute-query.d.ts +6 -0
  69. package/dist/core/tools/execute-query.d.ts.map +1 -0
  70. package/dist/core/tools/execute-query.js +31 -0
  71. package/dist/core/tools/execute-query.js.map +1 -0
  72. package/dist/core/tools/explain-query.d.ts +10 -0
  73. package/dist/core/tools/explain-query.d.ts.map +1 -0
  74. package/dist/core/tools/explain-query.js +34 -0
  75. package/dist/core/tools/explain-query.js.map +1 -0
  76. package/dist/core/tools/get-relationships.d.ts +6 -0
  77. package/dist/core/tools/get-relationships.d.ts.map +1 -0
  78. package/dist/core/tools/get-relationships.js +23 -0
  79. package/dist/core/tools/get-relationships.js.map +1 -0
  80. package/dist/core/tools/index.d.ts +9 -0
  81. package/dist/core/tools/index.d.ts.map +1 -0
  82. package/dist/core/tools/index.js +8 -0
  83. package/dist/core/tools/index.js.map +1 -0
  84. package/dist/core/tools/list-tables.d.ts +3 -0
  85. package/dist/core/tools/list-tables.d.ts.map +1 -0
  86. package/dist/core/tools/list-tables.js +18 -0
  87. package/dist/core/tools/list-tables.js.map +1 -0
  88. package/dist/core/tools/registry.d.ts +11 -0
  89. package/dist/core/tools/registry.d.ts.map +1 -0
  90. package/dist/core/tools/registry.js +48 -0
  91. package/dist/core/tools/registry.js.map +1 -0
  92. package/dist/core/types/index.d.ts +102 -0
  93. package/dist/core/types/index.d.ts.map +1 -0
  94. package/dist/core/types/index.js +6 -0
  95. package/dist/core/types/index.js.map +1 -0
  96. package/dist/database/adapter.d.ts +3 -0
  97. package/dist/database/adapter.d.ts.map +1 -0
  98. package/dist/database/adapter.js +2 -0
  99. package/dist/database/adapter.js.map +1 -0
  100. package/dist/database/factory.d.ts +16 -0
  101. package/dist/database/factory.d.ts.map +1 -0
  102. package/dist/database/factory.js +27 -0
  103. package/dist/database/factory.js.map +1 -0
  104. package/dist/database/index.d.ts +8 -0
  105. package/dist/database/index.d.ts.map +1 -0
  106. package/dist/database/index.js +4 -0
  107. package/dist/database/index.js.map +1 -0
  108. package/dist/database/mysql/adapter.d.ts +26 -0
  109. package/dist/database/mysql/adapter.d.ts.map +1 -0
  110. package/dist/database/mysql/adapter.js +131 -0
  111. package/dist/database/mysql/adapter.js.map +1 -0
  112. package/dist/database/postgres/adapter.d.ts +60 -0
  113. package/dist/database/postgres/adapter.d.ts.map +1 -0
  114. package/dist/database/postgres/adapter.js +307 -0
  115. package/dist/database/postgres/adapter.js.map +1 -0
  116. package/dist/database/result-limits.d.ts +17 -0
  117. package/dist/database/result-limits.d.ts.map +1 -0
  118. package/dist/database/result-limits.js +26 -0
  119. package/dist/database/result-limits.js.map +1 -0
  120. package/dist/index.d.ts +20 -0
  121. package/dist/index.d.ts.map +1 -0
  122. package/dist/index.js +13 -0
  123. package/dist/index.js.map +1 -0
  124. package/dist/safety/limits.d.ts +7 -0
  125. package/dist/safety/limits.d.ts.map +1 -0
  126. package/dist/safety/limits.js +15 -0
  127. package/dist/safety/limits.js.map +1 -0
  128. package/dist/safety/policy.d.ts +8 -0
  129. package/dist/safety/policy.d.ts.map +1 -0
  130. package/dist/safety/policy.js +11 -0
  131. package/dist/safety/policy.js.map +1 -0
  132. package/dist/safety/sql-tokenizer.d.ts +49 -0
  133. package/dist/safety/sql-tokenizer.d.ts.map +1 -0
  134. package/dist/safety/sql-tokenizer.js +240 -0
  135. package/dist/safety/sql-tokenizer.js.map +1 -0
  136. package/dist/safety/sql-validator.d.ts +34 -0
  137. package/dist/safety/sql-validator.d.ts.map +1 -0
  138. package/dist/safety/sql-validator.js +270 -0
  139. package/dist/safety/sql-validator.js.map +1 -0
  140. package/dist/schema/cache.d.ts +24 -0
  141. package/dist/schema/cache.d.ts.map +1 -0
  142. package/dist/schema/cache.js +57 -0
  143. package/dist/schema/cache.js.map +1 -0
  144. package/dist/ui/markdown.d.ts +8 -0
  145. package/dist/ui/markdown.d.ts.map +1 -0
  146. package/dist/ui/markdown.js +205 -0
  147. package/dist/ui/markdown.js.map +1 -0
  148. package/dist/ui/spinner.d.ts +32 -0
  149. package/dist/ui/spinner.d.ts.map +1 -0
  150. package/dist/ui/spinner.js +74 -0
  151. package/dist/ui/spinner.js.map +1 -0
  152. package/dist/ui/sql-highlight.d.ts +4 -0
  153. package/dist/ui/sql-highlight.d.ts.map +1 -0
  154. package/dist/ui/sql-highlight.js +28 -0
  155. package/dist/ui/sql-highlight.js.map +1 -0
  156. package/dist/ui/style.d.ts +38 -0
  157. package/dist/ui/style.d.ts.map +1 -0
  158. package/dist/ui/style.js +69 -0
  159. package/dist/ui/style.js.map +1 -0
  160. package/package.json +91 -4
  161. package/sql-ai.example.md +32 -0
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Minimal SQL tokenizer used by the safety validator. It understands the
3
+ * lexical rules of MySQL/MariaDB and PostgreSQL.
4
+ *
5
+ * It is not a full SQL parser. Its only job is to separate the parts of a
6
+ * statement that carry meaning (keywords, identifiers, punctuation) from the
7
+ * parts that do not (string literals, quoted identifiers, comments), so safety
8
+ * rules are applied to real SQL tokens instead of raw text. Getting the lexical
9
+ * rules of the *target* database exactly right matters: if this tokenizer and
10
+ * the server disagree about where a string or comment ends, a second statement
11
+ * can hide inside what the validator thinks is a literal.
12
+ *
13
+ * MySQL/MariaDB (defaults assumed):
14
+ * - `-- ` and `#` line comments, non-nested block comments
15
+ * - backslash escapes inside '...' and "..." strings (no NO_BACKSLASH_ESCAPES)
16
+ * - double quotes delimit strings, not identifiers (no ANSI_QUOTES)
17
+ * - executable comments (`/*! ... *\/`, `/*M! ... *\/`) are rejected outright
18
+ * because the server executes their contents
19
+ *
20
+ * PostgreSQL:
21
+ * - `--` line comments (no whitespace needed); `#` is an operator, not a comment
22
+ * - nested block comments
23
+ * - '...' strings only escape quotes by doubling; backslash escapes apply only
24
+ * to E'...' strings (the adapter pins standard_conforming_strings = on)
25
+ * - "..." delimits identifiers
26
+ * - dollar-quoted strings ($$...$$ and $tag$...$tag$)
27
+ * - U&'...' and U&"..." are rejected: their escapes can spell any name
28
+ */
29
+ const WORD_CHAR = /[A-Za-z0-9_$\u0080-￿]/;
30
+ const DOLLAR_TAG = /\$(?:[A-Za-z_\u0080-￿][A-Za-z0-9_\u0080-￿]*)?\$/y;
31
+ /**
32
+ * Tokenizes SQL using the lexical rules of `dialect`. Dialects other than
33
+ * PostgreSQL use the MySQL rules (no other adapter exists yet).
34
+ */
35
+ export function tokenizeSql(sql, dialect = 'mysql') {
36
+ const pg = dialect === 'postgres';
37
+ const tokens = [];
38
+ const n = sql.length;
39
+ let i = 0;
40
+ while (i < n) {
41
+ const c = sql[i];
42
+ const next = sql[i + 1];
43
+ // Whitespace.
44
+ if (/\s/.test(c)) {
45
+ i += 1;
46
+ continue;
47
+ }
48
+ // `--` line comment. PostgreSQL always treats `--` as a comment. MySQL
49
+ // requires whitespace or a control character after the two dashes; `--x`
50
+ // is two minus signs there, not a comment.
51
+ if (c === '-' && next === '-') {
52
+ const after = sql[i + 2];
53
+ if (pg || after === undefined || /[\s\u0000-\u001f]/.test(after)) {
54
+ const eol = sql.indexOf('\n', i + 2);
55
+ i = eol === -1 ? n : eol + 1;
56
+ continue;
57
+ }
58
+ }
59
+ // `#` line comment (MySQL only; in PostgreSQL `#` starts operators like #>).
60
+ if (c === '#' && !pg) {
61
+ const eol = sql.indexOf('\n', i + 1);
62
+ i = eol === -1 ? n : eol + 1;
63
+ continue;
64
+ }
65
+ // Block comment.
66
+ if (c === '/' && next === '*') {
67
+ if (!pg && (sql[i + 2] === '!' || (sql[i + 2] === 'M' && sql[i + 3] === '!'))) {
68
+ return {
69
+ tokens,
70
+ error: 'Executable comments (/*! ... */) are not allowed.',
71
+ };
72
+ }
73
+ if (pg) {
74
+ // PostgreSQL block comments nest.
75
+ let depth = 1;
76
+ let j = i + 2;
77
+ while (j < n && depth > 0) {
78
+ if (sql[j] === '/' && sql[j + 1] === '*') {
79
+ depth += 1;
80
+ j += 2;
81
+ }
82
+ else if (sql[j] === '*' && sql[j + 1] === '/') {
83
+ depth -= 1;
84
+ j += 2;
85
+ }
86
+ else {
87
+ j += 1;
88
+ }
89
+ }
90
+ if (depth > 0) {
91
+ return { tokens, error: 'The query contains an unterminated comment.' };
92
+ }
93
+ i = j;
94
+ continue;
95
+ }
96
+ const close = sql.indexOf('*/', i + 2);
97
+ if (close === -1) {
98
+ return { tokens, error: 'The query contains an unterminated comment.' };
99
+ }
100
+ i = close + 2;
101
+ continue;
102
+ }
103
+ // PostgreSQL dollar-quoted strings: $$...$$ or $tag$...$tag$.
104
+ // `$1` is a parameter placeholder and falls through to the word rule.
105
+ if (pg && c === '$') {
106
+ DOLLAR_TAG.lastIndex = i;
107
+ const match = DOLLAR_TAG.exec(sql);
108
+ if (match) {
109
+ const tag = match[0];
110
+ const close = sql.indexOf(tag, i + tag.length);
111
+ if (close === -1) {
112
+ return { tokens, error: 'The query contains an unterminated dollar-quoted string.' };
113
+ }
114
+ const end = close + tag.length;
115
+ tokens.push({ type: 'string', value: sql.slice(i, end), start: i, end });
116
+ i = end;
117
+ continue;
118
+ }
119
+ }
120
+ // Quoted strings ('...', and "..." in MySQL) and, in PostgreSQL,
121
+ // double-quoted identifiers.
122
+ if (c === "'" || c === '"') {
123
+ const start = i;
124
+ if (pg) {
125
+ const previous = tokens[tokens.length - 1];
126
+ const beforePrevious = tokens[tokens.length - 2];
127
+ // U&'...' / U&"..." (Unicode escapes can spell any identifier).
128
+ if (previous?.type === 'punct' &&
129
+ previous.value === '&' &&
130
+ previous.end === i &&
131
+ beforePrevious?.type === 'word' &&
132
+ beforePrevious.value === 'U' &&
133
+ beforePrevious.end === previous.start) {
134
+ return {
135
+ tokens,
136
+ error: 'Unicode escape strings and identifiers (U&"...") are not allowed.',
137
+ };
138
+ }
139
+ }
140
+ // E'...' strings treat backslash as an escape character.
141
+ let backslashEscapes = !pg;
142
+ let tokenStart = start;
143
+ if (pg && c === "'") {
144
+ const previous = tokens[tokens.length - 1];
145
+ if (previous?.type === 'word' && previous.value === 'E' && previous.end === i) {
146
+ backslashEscapes = true;
147
+ tokenStart = previous.start;
148
+ tokens.pop();
149
+ }
150
+ }
151
+ let j = i + 1;
152
+ let closed = false;
153
+ while (j < n) {
154
+ const d = sql[j];
155
+ if (backslashEscapes && d === '\\') {
156
+ j += 2;
157
+ continue;
158
+ }
159
+ if (d === c) {
160
+ if (sql[j + 1] === c) {
161
+ j += 2;
162
+ continue;
163
+ }
164
+ closed = true;
165
+ j += 1;
166
+ break;
167
+ }
168
+ j += 1;
169
+ }
170
+ if (!closed) {
171
+ return {
172
+ tokens,
173
+ error: pg && c === '"'
174
+ ? 'The query contains an unterminated quoted identifier.'
175
+ : 'The query contains an unterminated string.',
176
+ };
177
+ }
178
+ tokens.push({
179
+ type: pg && c === '"' ? 'quoted-identifier' : 'string',
180
+ value: sql.slice(tokenStart, j),
181
+ start: tokenStart,
182
+ end: j,
183
+ });
184
+ i = j;
185
+ continue;
186
+ }
187
+ // Backtick-quoted identifiers (MySQL only).
188
+ if (c === '`' && !pg) {
189
+ const start = i;
190
+ let j = i + 1;
191
+ let closed = false;
192
+ while (j < n) {
193
+ if (sql[j] === '`') {
194
+ if (sql[j + 1] === '`') {
195
+ j += 2;
196
+ continue;
197
+ }
198
+ closed = true;
199
+ j += 1;
200
+ break;
201
+ }
202
+ j += 1;
203
+ }
204
+ if (!closed) {
205
+ return {
206
+ tokens,
207
+ error: 'The query contains an unterminated quoted identifier.',
208
+ };
209
+ }
210
+ tokens.push({
211
+ type: 'quoted-identifier',
212
+ value: sql.slice(start, j),
213
+ start,
214
+ end: j,
215
+ });
216
+ i = j;
217
+ continue;
218
+ }
219
+ // Words: keywords, identifiers, and numbers.
220
+ if (WORD_CHAR.test(c)) {
221
+ const start = i;
222
+ let j = i + 1;
223
+ while (j < n && WORD_CHAR.test(sql[j]))
224
+ j += 1;
225
+ tokens.push({
226
+ type: 'word',
227
+ value: sql.slice(start, j).toUpperCase(),
228
+ start,
229
+ end: j,
230
+ });
231
+ i = j;
232
+ continue;
233
+ }
234
+ // Everything else is single-character punctuation.
235
+ tokens.push({ type: 'punct', value: c, start: i, end: i + 1 });
236
+ i += 1;
237
+ }
238
+ return { tokens };
239
+ }
240
+ //# sourceMappingURL=sql-tokenizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql-tokenizer.js","sourceRoot":"","sources":["../../src/safety/sql-tokenizer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import type { SqlDialect, SqlSafetyPolicy } from '../core/types/index.js';
2
+ export interface SqlValidationResult {
3
+ valid: boolean;
4
+ sql?: string;
5
+ reason?: string;
6
+ }
7
+ /**
8
+ * First-stage SQL safety boundary.
9
+ *
10
+ * The SQL is tokenized with the target dialect's lexical rules so string
11
+ * literals, quoted identifiers, and comments are never mistaken for SQL, then
12
+ * these rules are applied:
13
+ *
14
+ * - exactly one statement (a single trailing semicolon is accepted)
15
+ * - the statement must start with a read-oriented keyword
16
+ * - executable comments (MySQL) and Unicode-escaped names (PostgreSQL) are
17
+ * rejected
18
+ * - when `policy.readOnly` is set: write/DDL/locking keywords, `INTO`, and
19
+ * locking reads (`FOR SHARE` / `FOR UPDATE`, ...) are rejected
20
+ * - functions that read files, block the connection, take locks, or run
21
+ * dynamic SQL are rejected wherever they appear, including schema-qualified
22
+ * (`pg_catalog.pg_sleep(...)`) and quoted (`"pg_sleep"(...)`) forms
23
+ *
24
+ * This is not a complete SQL parser. MySQL rules assume the default sql_mode
25
+ * (backslash escapes on, ANSI_QUOTES off); PostgreSQL rules assume
26
+ * standard_conforming_strings = on, which the PostgreSQL adapter enforces.
27
+ * User-defined functions cannot be recognized as side-effecting. The database
28
+ * account must still be read-only; the PostgreSQL adapter additionally runs
29
+ * every statement in a READ ONLY transaction.
30
+ *
31
+ * @param dialect SQL dialect of the target database. Defaults to MySQL.
32
+ */
33
+ export declare function validateSql(sql: string, policy: SqlSafetyPolicy, dialect?: SqlDialect): SqlValidationResult;
34
+ //# sourceMappingURL=sql-validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql-validator.d.ts","sourceRoot":"","sources":["../../src/safety/sql-validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AA4J1E,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA8BD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,eAAe,EACvB,OAAO,GAAE,UAAoB,GAC5B,mBAAmB,CAgHrB"}
@@ -0,0 +1,270 @@
1
+ import { tokenizeSql } from './sql-tokenizer.js';
2
+ const MYSQL_RULES = {
3
+ allowedFirstKeywords: new Set(['SELECT', 'WITH', 'EXPLAIN', 'SHOW', 'DESCRIBE', 'DESC']),
4
+ blockedKeywords: new Set([
5
+ 'INSERT',
6
+ 'UPDATE',
7
+ 'DELETE',
8
+ 'DROP',
9
+ 'ALTER',
10
+ 'TRUNCATE',
11
+ 'CREATE',
12
+ 'REPLACE',
13
+ 'MERGE',
14
+ 'GRANT',
15
+ 'REVOKE',
16
+ 'CALL',
17
+ 'SET',
18
+ 'USE',
19
+ 'LOAD',
20
+ 'HANDLER',
21
+ 'LOCK',
22
+ 'UNLOCK',
23
+ 'PREPARE',
24
+ 'EXECUTE',
25
+ 'DEALLOCATE',
26
+ 'RENAME',
27
+ 'KILL',
28
+ 'FLUSH',
29
+ // Writes query output to the server filesystem or into variables.
30
+ 'INTO',
31
+ ]),
32
+ // INSERT(), REPLACE() and TRUNCATE() are also ordinary string/math functions.
33
+ functionLikeKeywords: new Set(['INSERT', 'REPLACE', 'TRUNCATE']),
34
+ // Functions that read server files, block, take locks, or are commonly used
35
+ // to tie up a connection.
36
+ blockedFunctions: new Set([
37
+ 'LOAD_FILE',
38
+ 'SLEEP',
39
+ 'BENCHMARK',
40
+ 'GET_LOCK',
41
+ 'RELEASE_LOCK',
42
+ 'RELEASE_ALL_LOCKS',
43
+ 'IS_FREE_LOCK',
44
+ 'IS_USED_LOCK',
45
+ 'MASTER_POS_WAIT',
46
+ 'SOURCE_POS_WAIT',
47
+ 'WAIT_FOR_EXECUTED_GTID_SET',
48
+ 'WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS',
49
+ ]),
50
+ blockedFunctionPrefixes: [],
51
+ lockingWords: new Set(['SHARE', 'UPDATE']),
52
+ };
53
+ /**
54
+ * PostgreSQL. Only words that can occur *inside* a single SELECT/WITH/EXPLAIN/
55
+ * SHOW statement matter here: the statement-starting rule already rejects
56
+ * everything else (COPY, DO, VACUUM, LOCK, ...). Words that are common column
57
+ * names (LOAD, LOCK, USE, KILL, ...) are deliberately not blocked.
58
+ */
59
+ const POSTGRES_RULES = {
60
+ allowedFirstKeywords: new Set(['SELECT', 'WITH', 'EXPLAIN', 'SHOW']),
61
+ blockedKeywords: new Set([
62
+ // Data-modifying CTEs and statements nested in EXPLAIN.
63
+ 'INSERT',
64
+ 'UPDATE',
65
+ 'DELETE',
66
+ 'MERGE',
67
+ 'CREATE',
68
+ 'DROP',
69
+ 'ALTER',
70
+ 'TRUNCATE',
71
+ 'GRANT',
72
+ 'REVOKE',
73
+ 'CALL',
74
+ 'SET',
75
+ 'COPY',
76
+ 'DO',
77
+ 'PREPARE',
78
+ 'EXECUTE',
79
+ 'DEALLOCATE',
80
+ // SELECT ... INTO creates a table.
81
+ 'INTO',
82
+ ]),
83
+ functionLikeKeywords: new Set(),
84
+ blockedFunctions: new Set([
85
+ // Block the connection or change the session's safety settings.
86
+ 'PG_SLEEP',
87
+ 'PG_SLEEP_FOR',
88
+ 'PG_SLEEP_UNTIL',
89
+ 'SET_CONFIG',
90
+ // Sequences are state.
91
+ 'NEXTVAL',
92
+ 'SETVAL',
93
+ // Notifications and backend control.
94
+ 'PG_NOTIFY',
95
+ 'PG_TERMINATE_BACKEND',
96
+ 'PG_CANCEL_BACKEND',
97
+ 'PG_RELOAD_CONF',
98
+ 'PG_ROTATE_LOGFILE',
99
+ 'PG_SWITCH_WAL',
100
+ 'PG_PROMOTE',
101
+ 'PG_IMPORT_SYSTEM_COLLATIONS',
102
+ // Read server files.
103
+ 'PG_STAT_FILE',
104
+ // Server-side dynamic SQL: runs query text that the validator never sees.
105
+ 'QUERY_TO_XML',
106
+ 'QUERY_TO_XML_AND_XMLSCHEMA',
107
+ 'QUERY_TO_XMLSCHEMA',
108
+ 'CURSOR_TO_XML',
109
+ 'CURSOR_TO_XMLSCHEMA',
110
+ ]),
111
+ blockedFunctionPrefixes: [
112
+ 'PG_LS_', // pg_ls_dir, pg_ls_logdir, ...
113
+ 'PG_READ_', // pg_read_file, pg_read_binary_file
114
+ 'PG_FILE_',
115
+ 'PG_ADVISORY_', // pg_advisory_lock*, pg_advisory_unlock*, pg_advisory_xact_lock*
116
+ 'PG_TRY_ADVISORY_',
117
+ 'PG_CREATE_', // replication slots, restore points
118
+ 'PG_DROP_',
119
+ 'PG_REPLICATION_',
120
+ 'PG_LOGICAL_',
121
+ 'PG_COPY_',
122
+ 'PG_BACKUP_',
123
+ 'PG_WAL_REPLAY_',
124
+ 'LO_', // large objects: lo_import / lo_export touch the server filesystem
125
+ 'DBLINK', // other databases, arbitrary remote SQL
126
+ ],
127
+ lockingWords: new Set(['SHARE', 'UPDATE', 'NO', 'KEY']),
128
+ };
129
+ function rulesFor(dialect) {
130
+ // Dialects without an adapter yet fall back to the MySQL rules.
131
+ return dialect === 'postgres' ? POSTGRES_RULES : MYSQL_RULES;
132
+ }
133
+ function isPunct(token, value) {
134
+ return token?.type === 'punct' && token.value === value;
135
+ }
136
+ /**
137
+ * Upper-cased function name when `token` is a (possibly quoted) identifier,
138
+ * otherwise undefined. Quoted identifiers are unquoted first so that
139
+ * "pg_sleep"(10) cannot hide a blocked function.
140
+ */
141
+ function identifierName(token) {
142
+ if (token.type === 'word')
143
+ return token.value;
144
+ if (token.type === 'quoted-identifier') {
145
+ const quote = token.value[0];
146
+ const inner = token.value.slice(1, -1).split(quote + quote).join(quote);
147
+ return inner.toUpperCase();
148
+ }
149
+ return undefined;
150
+ }
151
+ function isBlockedFunction(name, rules) {
152
+ return (rules.blockedFunctions.has(name) ||
153
+ rules.blockedFunctionPrefixes.some((prefix) => name.startsWith(prefix)));
154
+ }
155
+ /**
156
+ * First-stage SQL safety boundary.
157
+ *
158
+ * The SQL is tokenized with the target dialect's lexical rules so string
159
+ * literals, quoted identifiers, and comments are never mistaken for SQL, then
160
+ * these rules are applied:
161
+ *
162
+ * - exactly one statement (a single trailing semicolon is accepted)
163
+ * - the statement must start with a read-oriented keyword
164
+ * - executable comments (MySQL) and Unicode-escaped names (PostgreSQL) are
165
+ * rejected
166
+ * - when `policy.readOnly` is set: write/DDL/locking keywords, `INTO`, and
167
+ * locking reads (`FOR SHARE` / `FOR UPDATE`, ...) are rejected
168
+ * - functions that read files, block the connection, take locks, or run
169
+ * dynamic SQL are rejected wherever they appear, including schema-qualified
170
+ * (`pg_catalog.pg_sleep(...)`) and quoted (`"pg_sleep"(...)`) forms
171
+ *
172
+ * This is not a complete SQL parser. MySQL rules assume the default sql_mode
173
+ * (backslash escapes on, ANSI_QUOTES off); PostgreSQL rules assume
174
+ * standard_conforming_strings = on, which the PostgreSQL adapter enforces.
175
+ * User-defined functions cannot be recognized as side-effecting. The database
176
+ * account must still be read-only; the PostgreSQL adapter additionally runs
177
+ * every statement in a READ ONLY transaction.
178
+ *
179
+ * @param dialect SQL dialect of the target database. Defaults to MySQL.
180
+ */
181
+ export function validateSql(sql, policy, dialect = 'mysql') {
182
+ const rules = rulesFor(dialect);
183
+ const normalized = sql.trim();
184
+ if (!normalized) {
185
+ return { valid: false, reason: 'SQL query is empty.' };
186
+ }
187
+ const { tokens: allTokens, error } = tokenizeSql(normalized, dialect);
188
+ if (error) {
189
+ return { valid: false, reason: error };
190
+ }
191
+ // Split on the first semicolon outside strings, quoted identifiers, and
192
+ // comments. Anything after it, other than comments, is a second statement.
193
+ let tokens = allTokens;
194
+ let output = normalized;
195
+ const semicolon = allTokens.findIndex((token) => isPunct(token, ';'));
196
+ if (semicolon !== -1) {
197
+ if (semicolon < allTokens.length - 1) {
198
+ return { valid: false, reason: 'Multiple SQL statements are not allowed.' };
199
+ }
200
+ tokens = allTokens.slice(0, semicolon);
201
+ output = normalized.slice(0, allTokens[semicolon].start).trimEnd();
202
+ }
203
+ if (tokens.length === 0) {
204
+ return { valid: false, reason: 'SQL query is empty.' };
205
+ }
206
+ // Allow leading parentheses, e.g. (SELECT ...) UNION (SELECT ...).
207
+ let first = 0;
208
+ while (isPunct(tokens[first], '('))
209
+ first += 1;
210
+ const head = tokens[first];
211
+ if (!head ||
212
+ head.type !== 'word' ||
213
+ !rules.allowedFirstKeywords.has(head.value)) {
214
+ return { valid: false, reason: 'Only read-oriented SQL statements are allowed.' };
215
+ }
216
+ for (let index = 0; index < tokens.length; index += 1) {
217
+ const token = tokens[index];
218
+ const following = tokens[index + 1];
219
+ const isCall = isPunct(following, '(');
220
+ // Blocked functions are matched wherever they appear: a name after a dot
221
+ // (pg_catalog.pg_sleep) or in quotes ("pg_sleep") is still that function.
222
+ if (isCall) {
223
+ const name = identifierName(token);
224
+ if (name !== undefined && isBlockedFunction(name, rules)) {
225
+ return {
226
+ valid: false,
227
+ reason: `The function ${name.toLowerCase()}() is not allowed.`,
228
+ };
229
+ }
230
+ }
231
+ if (token.type !== 'word')
232
+ continue;
233
+ // A word after a dot is part of a qualified name (t.update), not a keyword.
234
+ if (isPunct(tokens[index - 1], '.'))
235
+ continue;
236
+ if (!policy.readOnly)
237
+ continue;
238
+ if (rules.blockedKeywords.has(token.value)) {
239
+ if (rules.functionLikeKeywords.has(token.value) && isCall)
240
+ continue;
241
+ // CHARACTER SET utf8 (in CAST/CONVERT, SHOW CHARACTER SET).
242
+ if (dialect !== 'postgres' &&
243
+ token.value === 'SET' &&
244
+ tokens[index - 1]?.value === 'CHARACTER') {
245
+ continue;
246
+ }
247
+ // Index hints: USE INDEX (...) / USE KEY (...).
248
+ if (dialect !== 'postgres' &&
249
+ token.value === 'USE' &&
250
+ following?.type === 'word' &&
251
+ (following.value === 'INDEX' || following.value === 'KEY')) {
252
+ continue;
253
+ }
254
+ return {
255
+ valid: false,
256
+ reason: `The query contains a blocked SQL operation (${token.value}).`,
257
+ };
258
+ }
259
+ if (token.value === 'FOR' &&
260
+ following?.type === 'word' &&
261
+ rules.lockingWords.has(following.value)) {
262
+ return {
263
+ valid: false,
264
+ reason: 'Locking reads (FOR SHARE / FOR UPDATE) are not allowed.',
265
+ };
266
+ }
267
+ }
268
+ return { valid: true, sql: output };
269
+ }
270
+ //# sourceMappingURL=sql-validator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql-validator.js","sourceRoot":"","sources":["../../src/safety/sql-validator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { DatabaseAdapter, TableInfo, TableSchema, RelationshipInfo } from '../core/types/index.js';
2
+ export interface SchemaCacheOptions {
3
+ ttlMs?: number;
4
+ }
5
+ /**
6
+ * Process-local cache for database metadata. It never caches query results.
7
+ * A short TTL keeps repeated agent schema discovery from repeatedly hitting
8
+ * information_schema while allowing schema changes to become visible.
9
+ */
10
+ export declare class SchemaCache {
11
+ private readonly ttlMs;
12
+ private tables?;
13
+ private relationships?;
14
+ private readonly tableSchemas;
15
+ constructor(options?: SchemaCacheOptions);
16
+ listTables(database: DatabaseAdapter): Promise<TableInfo[]>;
17
+ describeTable(database: DatabaseAdapter, table: string): Promise<TableSchema>;
18
+ describeTables(database: DatabaseAdapter, tables: string[]): Promise<TableSchema[]>;
19
+ getRelationships(database: DatabaseAdapter): Promise<RelationshipInfo[]>;
20
+ clear(): void;
21
+ private entry;
22
+ private get;
23
+ }
24
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/schema/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAExG,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD;;;;GAIG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,MAAM,CAAC,CAA0B;IACzC,OAAO,CAAC,aAAa,CAAC,CAAiC;IACvD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA8C;IAE3E,YAAY,OAAO,GAAE,kBAAuB,EAE3C;IAEK,UAAU,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAOhE;IAEK,aAAa,CAAC,QAAQ,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CASlF;IAEK,cAAc,CAAC,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAExF;IAEK,gBAAgB,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAO7E;IAED,KAAK,IAAI,IAAI,CAIZ;IAED,OAAO,CAAC,KAAK;IAIb,OAAO,CAAC,GAAG;CAIZ"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Process-local cache for database metadata. It never caches query results.
3
+ * A short TTL keeps repeated agent schema discovery from repeatedly hitting
4
+ * information_schema while allowing schema changes to become visible.
5
+ */
6
+ export class SchemaCache {
7
+ ttlMs;
8
+ tables;
9
+ relationships;
10
+ tableSchemas = new Map();
11
+ constructor(options = {}) {
12
+ this.ttlMs = options.ttlMs ?? 300_000;
13
+ }
14
+ async listTables(database) {
15
+ const cached = this.get(this.tables);
16
+ if (cached)
17
+ return cached;
18
+ const value = await database.listTables();
19
+ this.tables = this.entry(value);
20
+ return value;
21
+ }
22
+ async describeTable(database, table) {
23
+ // PostgreSQL names are case-sensitive when quoted; MySQL table names are not portable anyway.
24
+ const key = database.dialect === 'postgres' ? table : table.toLowerCase();
25
+ const cached = this.get(this.tableSchemas.get(key));
26
+ if (cached)
27
+ return cached;
28
+ const value = await database.describeTable(table);
29
+ this.tableSchemas.set(key, this.entry(value));
30
+ return value;
31
+ }
32
+ async describeTables(database, tables) {
33
+ return Promise.all(tables.map((table) => this.describeTable(database, table)));
34
+ }
35
+ async getRelationships(database) {
36
+ const cached = this.get(this.relationships);
37
+ if (cached)
38
+ return cached;
39
+ const value = await database.getRelationships();
40
+ this.relationships = this.entry(value);
41
+ return value;
42
+ }
43
+ clear() {
44
+ this.tables = undefined;
45
+ this.relationships = undefined;
46
+ this.tableSchemas.clear();
47
+ }
48
+ entry(value) {
49
+ return { value, expiresAt: Date.now() + this.ttlMs };
50
+ }
51
+ get(entry) {
52
+ if (!entry || entry.expiresAt <= Date.now())
53
+ return undefined;
54
+ return entry.value;
55
+ }
56
+ }
57
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/schema/cache.ts"],"names":[],"mappings":"AAWA;;;;GAIG;AACH,MAAM,OAAO,WAAW;IACL,KAAK,CAAS;IACvB,MAAM,CAA2B;IACjC,aAAa,CAAkC;IACtC,YAAY,GAAG,IAAI,GAAG,EAAmC,CAAC;IAE3E,YAAY,OAAO,GAAuB,EAAE;QAC1C,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,QAAyB;QACxC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACrC,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAE1B,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,CAAC;QAC1C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAChC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,QAAyB,EAAE,KAAa;QAC1D,8FAA8F;QAC9F,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;QAC1E,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QACpD,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAE1B,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAClD,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,QAAyB,EAAE,MAAgB;QAC9D,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IACjF,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,QAAyB;QAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAC5C,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAE1B,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,gBAAgB,EAAE,CAAC;QAChD,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACvC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK;QACH,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;QACxB,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC;QAC/B,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;IAC5B,CAAC;IAEO,KAAK,CAAI,KAAQ;QACvB,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;IACvD,CAAC;IAEO,GAAG,CAAI,KAAgC;QAC7C,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE;YAAE,OAAO,SAAS,CAAC;QAC9D,OAAO,KAAK,CAAC,KAAK,CAAC;IACrB,CAAC;CACF"}