@agent-native/core 0.201.0-nightly-20261005153421 → 0.201.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 +22 -22
- package/dist/a2a/server.d.ts +2 -2
- package/dist/a2a/server.js +13 -2
- package/dist/agent/thread-data-builder.d.ts +4 -0
- package/dist/agent/thread-data-builder.js +8 -0
- package/dist/agent-sql/analysis.d.ts +31 -0
- package/dist/agent-sql/analysis.js +169 -0
- package/dist/agent-sql/index.d.ts +4 -0
- package/dist/agent-sql/index.js +4 -0
- package/dist/agent-sql/lexer.d.ts +37 -0
- package/dist/agent-sql/lexer.js +357 -0
- package/dist/agent-sql/postgres.d.ts +51 -0
- package/dist/agent-sql/postgres.js +197 -0
- package/dist/agent-sql/query.d.ts +30 -0
- package/dist/agent-sql/query.js +514 -0
- package/dist/catalog.json +24 -24
- package/dist/client/analytics/index.d.ts +1 -1
- package/dist/client/analytics/index.js +1 -1
- package/dist/client/analytics.d.ts +8 -0
- package/dist/client/analytics.js +17 -2
- package/dist/client/session-replay.js +3 -2
- package/dist/identity/offboard.js +31 -3
- package/dist/identity/rekey.d.ts +12 -1
- package/dist/identity/rekey.js +77 -6
- package/dist/identity/retired-emails.d.ts +23 -0
- package/dist/identity/retired-emails.js +55 -0
- package/dist/mcp/actions/create-org-service-token.js +20 -8
- package/dist/mcp/actions/service-token-access.d.ts +1 -0
- package/dist/mcp/actions/service-token-access.js +41 -24
- package/dist/mcp/build-server.d.ts +20 -6
- package/dist/mcp/build-server.js +131 -35
- package/dist/mcp/connect-route.d.ts +4 -3
- package/dist/mcp/connect-route.js +140 -82
- package/dist/mcp/connect-store.d.ts +17 -11
- package/dist/mcp/connect-store.js +88 -60
- package/dist/mcp/credential-issuance.d.ts +23 -0
- package/dist/mcp/credential-issuance.js +77 -0
- package/dist/mcp/credential-membership.d.ts +26 -0
- package/dist/mcp/credential-membership.js +67 -0
- package/dist/mcp/oauth-route.js +133 -45
- package/dist/mcp/oauth-store.d.ts +15 -5
- package/dist/mcp/oauth-store.js +119 -34
- package/dist/mcp/oauth-token.d.ts +4 -0
- package/dist/mcp/oauth-token.js +5 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/mcp/server.js +14 -0
- package/dist/onboarding/default-steps.js +1 -1
- package/dist/org/handlers.d.ts +1 -1
- package/dist/org/index.d.ts +2 -2
- package/dist/org/membership.d.ts +3 -1
- package/dist/org/membership.js +5 -2
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +10 -10
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resource-changes/store.d.ts +2 -0
- package/dist/resource-changes/store.js +4 -0
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/db/exec.js +28 -101
- package/dist/scripts/db/patch.js +14 -34
- package/dist/scripts/db/postgres-client.d.ts +8 -1
- package/dist/scripts/db/postgres-client.js +20 -6
- package/dist/scripts/db/query.js +31 -33
- package/dist/scripts/db/safety.d.ts +47 -2
- package/dist/scripts/db/safety.js +383 -137
- package/dist/scripts/db/scoping.d.ts +1 -0
- package/dist/scripts/db/scoping.js +3 -2
- package/dist/search/index-store.d.ts +21 -0
- package/dist/search/index-store.js +47 -0
- package/dist/search/indexer.js +35 -32
- package/dist/search/registry.d.ts +6 -3
- package/dist/search/registry.js +30 -8
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-chat-plugin.js +18 -6
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/agent-run-context.js +9 -0
- package/dist/server/attribution.d.ts +2 -0
- package/dist/server/attribution.js +7 -1
- package/dist/server/auth.js +9 -0
- package/dist/server/credential-membership-unavailable.d.ts +13 -0
- package/dist/server/credential-membership-unavailable.js +22 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/onboarding-html.js +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/recap-image-route.js +4 -0
- package/dist/server/release-schema.js +4 -0
- package/dist/shared/docs-url.d.ts +3 -1
- package/dist/shared/docs-url.js +6 -3
- package/dist/shared/environment-lanes.d.ts +8 -0
- package/dist/shared/environment-lanes.js +26 -0
- package/dist/templates/chat/.agents/skills/security/SKILL.md +9 -2
- package/dist/templates/chat/.agents/skills/storing-data/SKILL.md +12 -7
- package/dist/templates/chat/README.md +2 -2
- package/dist/templates/default/.agents/skills/security/SKILL.md +9 -2
- package/dist/templates/default/.agents/skills/storing-data/SKILL.md +12 -7
- package/dist/templates/default/app/routes/home.tsx +5 -1
- package/dist/templates/headless/.agents/skills/security/SKILL.md +9 -2
- package/dist/templates/headless/.agents/skills/storing-data/SKILL.md +12 -7
- package/dist/templates/workspace-core/.agents/skills/security/SKILL.md +9 -2
- package/dist/templates/workspace-core/.agents/skills/storing-data/SKILL.md +12 -7
- package/dist/tracking/providers.js +13 -1
- package/docs/content/authentication.mdx +5 -1
- package/docs/content/durable-background-runs.mdx +17 -8
- package/docs/content/external-agents.mdx +3 -1
- package/docs/content/mcp-protocol.mdx +6 -2
- package/docs/content/security.mdx +1 -1
- package/package.json +5 -4
- package/src/templates/chat/.agents/skills/security/SKILL.md +9 -2
- package/src/templates/chat/.agents/skills/storing-data/SKILL.md +12 -7
- package/src/templates/chat/README.md +2 -2
- package/src/templates/default/.agents/skills/security/SKILL.md +9 -2
- package/src/templates/default/.agents/skills/storing-data/SKILL.md +12 -7
- package/src/templates/default/app/routes/home.tsx +5 -1
- package/src/templates/headless/.agents/skills/security/SKILL.md +9 -2
- package/src/templates/headless/.agents/skills/storing-data/SKILL.md +12 -7
- package/src/templates/workspace-core/.agents/skills/security/SKILL.md +9 -2
- package/src/templates/workspace-core/.agents/skills/storing-data/SKILL.md +12 -7
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lexer could not read the SQL the way the database would. Callers must
|
|
3
|
+
* refuse to run the statement: a guard that skipped what it could not read
|
|
4
|
+
* would be checking different text from what executes.
|
|
5
|
+
*/
|
|
6
|
+
export class AgentSqlSyntaxError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
position;
|
|
9
|
+
constructor(code, message, position) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "AgentSqlSyntaxError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.position = position;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// Postgres also allows a backtick in operator names; agent SQL rejects it
|
|
17
|
+
// because it reads as identifier quoting in other dialects.
|
|
18
|
+
const POSTGRES_OPERATOR_CHARS = new Set("+-*/<>=~!@#%^&|?");
|
|
19
|
+
const BIGQUERY_OPERATOR_CHARS = new Set("+-*/<>=~!%^&|");
|
|
20
|
+
const PUNCTUATION = new Set(["(", ")", "[", "]", ",", ";", ".", ":"]);
|
|
21
|
+
function isIdentStart(ch, dialect) {
|
|
22
|
+
if (/[A-Za-z_]/.test(ch))
|
|
23
|
+
return true;
|
|
24
|
+
// Postgres accepts any non-ASCII letter in an unquoted identifier.
|
|
25
|
+
return dialect === "postgres" && ch.charCodeAt(0) >= 0x80;
|
|
26
|
+
}
|
|
27
|
+
function isIdentPart(ch, dialect) {
|
|
28
|
+
if (/[A-Za-z0-9_]/.test(ch))
|
|
29
|
+
return true;
|
|
30
|
+
if (dialect === "postgres")
|
|
31
|
+
return ch === "$" || ch.charCodeAt(0) >= 0x80;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
// Exactly the whitespace Postgres skips. JavaScript's `\s` also matches
|
|
35
|
+
// Unicode spaces, which Postgres reads as identifier characters; treating them
|
|
36
|
+
// as separators would split one database identifier into several tokens.
|
|
37
|
+
const WHITESPACE = new Set([" ", "\t", "\n", "\r", "\f", "\v"]);
|
|
38
|
+
/** Postgres folds only the ASCII letters of unquoted identifiers. */
|
|
39
|
+
function foldAscii(text) {
|
|
40
|
+
return text.replace(/[A-Z]/g, (ch) => ch.toLowerCase());
|
|
41
|
+
}
|
|
42
|
+
class Scanner {
|
|
43
|
+
sql;
|
|
44
|
+
dialect;
|
|
45
|
+
tokens = [];
|
|
46
|
+
pos = 0;
|
|
47
|
+
constructor(sql, dialect) {
|
|
48
|
+
this.sql = sql;
|
|
49
|
+
this.dialect = dialect;
|
|
50
|
+
}
|
|
51
|
+
peek(offset = 0) {
|
|
52
|
+
return this.sql[this.pos + offset] ?? "";
|
|
53
|
+
}
|
|
54
|
+
push(kind, start, value) {
|
|
55
|
+
const text = this.sql.slice(start, this.pos);
|
|
56
|
+
this.tokens.push({
|
|
57
|
+
kind,
|
|
58
|
+
text,
|
|
59
|
+
value: value ?? text,
|
|
60
|
+
start,
|
|
61
|
+
end: this.pos,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
fail(code, message, at = this.pos) {
|
|
65
|
+
throw new AgentSqlSyntaxError(code, message, at);
|
|
66
|
+
}
|
|
67
|
+
/** A line comment ends at a line feed or a carriage return, as in Postgres. */
|
|
68
|
+
skipLineComment() {
|
|
69
|
+
while (this.pos < this.sql.length &&
|
|
70
|
+
this.sql[this.pos] !== "\n" &&
|
|
71
|
+
this.sql[this.pos] !== "\r") {
|
|
72
|
+
this.pos++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
skipBlockComment() {
|
|
76
|
+
const start = this.pos;
|
|
77
|
+
let depth = 0;
|
|
78
|
+
while (this.pos < this.sql.length) {
|
|
79
|
+
if (this.peek() === "/" && this.peek(1) === "*") {
|
|
80
|
+
// Postgres block comments nest; BigQuery's do not.
|
|
81
|
+
depth = this.dialect === "postgres" ? depth + 1 : 1;
|
|
82
|
+
this.pos += 2;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (this.peek() === "*" && this.peek(1) === "/") {
|
|
86
|
+
depth--;
|
|
87
|
+
this.pos += 2;
|
|
88
|
+
if (depth === 0)
|
|
89
|
+
return;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
this.pos++;
|
|
93
|
+
}
|
|
94
|
+
this.fail("unterminated", "Unterminated block comment.", start);
|
|
95
|
+
}
|
|
96
|
+
/** Consumes a quoted run starting at `this.pos` and returns its body. */
|
|
97
|
+
readQuoted(quote, options, label) {
|
|
98
|
+
const start = this.pos;
|
|
99
|
+
this.pos += quote.length;
|
|
100
|
+
let body = "";
|
|
101
|
+
while (this.pos < this.sql.length) {
|
|
102
|
+
const ch = this.sql[this.pos];
|
|
103
|
+
if (options.backslashEscapes && ch === "\\") {
|
|
104
|
+
if (this.pos + 1 >= this.sql.length)
|
|
105
|
+
break;
|
|
106
|
+
body += this.sql.slice(this.pos, this.pos + 2);
|
|
107
|
+
this.pos += 2;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (this.sql.startsWith(quote, this.pos)) {
|
|
111
|
+
if (options.doubledQuoteEscapes &&
|
|
112
|
+
this.sql.startsWith(quote, this.pos + quote.length)) {
|
|
113
|
+
body += quote;
|
|
114
|
+
this.pos += quote.length * 2;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
this.pos += quote.length;
|
|
118
|
+
return body;
|
|
119
|
+
}
|
|
120
|
+
body += ch;
|
|
121
|
+
this.pos++;
|
|
122
|
+
}
|
|
123
|
+
this.fail("unterminated", `Unterminated ${label}.`, start);
|
|
124
|
+
}
|
|
125
|
+
readWord(start) {
|
|
126
|
+
while (this.pos < this.sql.length &&
|
|
127
|
+
isIdentPart(this.peek(), this.dialect)) {
|
|
128
|
+
this.pos++;
|
|
129
|
+
}
|
|
130
|
+
this.push("word", start, foldAscii(this.sql.slice(start, this.pos)));
|
|
131
|
+
}
|
|
132
|
+
readNumber(start) {
|
|
133
|
+
const rest = this.sql.slice(this.pos);
|
|
134
|
+
const match = /^0[xXoObB][0-9A-Fa-f_]+/.exec(rest) ??
|
|
135
|
+
/^(?:\d[\d_]*(?:\.[\d_]*)?|\.\d[\d_]*)(?:[eE][+-]?\d[\d_]*)?/.exec(rest);
|
|
136
|
+
if (!match)
|
|
137
|
+
this.fail("unexpected_character", "Malformed number.");
|
|
138
|
+
this.pos += match[0].length;
|
|
139
|
+
// `1abc` is a lexing error on current Postgres and ambiguous elsewhere.
|
|
140
|
+
if (isIdentStart(this.peek(), this.dialect)) {
|
|
141
|
+
this.fail("unsupported_syntax", "A number must not run directly into an identifier.");
|
|
142
|
+
}
|
|
143
|
+
this.push("number", start);
|
|
144
|
+
}
|
|
145
|
+
readOperator(start, chars) {
|
|
146
|
+
while (this.pos < this.sql.length && chars.has(this.peek())) {
|
|
147
|
+
// An operator never contains the start of a comment.
|
|
148
|
+
const pair = this.peek() + this.peek(1);
|
|
149
|
+
if (this.pos > start && (pair === "--" || pair === "/*"))
|
|
150
|
+
break;
|
|
151
|
+
this.pos++;
|
|
152
|
+
}
|
|
153
|
+
if (this.dialect === "postgres") {
|
|
154
|
+
// Postgres drops a trailing + or - from a multi-character operator
|
|
155
|
+
// unless the operator contains one of these characters, so `a=-1`
|
|
156
|
+
// reads as `a = -1`.
|
|
157
|
+
const text = this.sql.slice(start, this.pos);
|
|
158
|
+
if (!/[~!@#%^&|`?]/.test(text)) {
|
|
159
|
+
while (this.pos - start > 1 && /[+-]/.test(this.sql[this.pos - 1])) {
|
|
160
|
+
this.pos--;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
this.push("operator", start);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function lexPostgres(s) {
|
|
168
|
+
while (s.pos < s.sql.length) {
|
|
169
|
+
const ch = s.peek();
|
|
170
|
+
const next = s.peek(1);
|
|
171
|
+
const start = s.pos;
|
|
172
|
+
if (WHITESPACE.has(ch)) {
|
|
173
|
+
s.pos++;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (ch === "-" && next === "-") {
|
|
177
|
+
s.skipLineComment();
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (ch === "/" && next === "*") {
|
|
181
|
+
s.skipBlockComment();
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// U&'...' and U&"..." spell characters as escape codes, so their text
|
|
185
|
+
// is not the name or value the database sees.
|
|
186
|
+
if ((ch === "u" || ch === "U") && next === "&") {
|
|
187
|
+
const quote = s.peek(2);
|
|
188
|
+
if (quote === "'" || quote === '"') {
|
|
189
|
+
s.fail("unsupported_syntax", "Unicode-escaped strings and identifiers (U&) are not supported in agent SQL.");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if ((ch === "e" || ch === "E") && next === "'") {
|
|
193
|
+
s.pos++;
|
|
194
|
+
s.readQuoted("'", { backslashEscapes: true, doubledQuoteEscapes: true }, "escape string");
|
|
195
|
+
s.push("string", start);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (/[bBxXnN]/.test(ch) && next === "'") {
|
|
199
|
+
s.pos++;
|
|
200
|
+
s.readQuoted("'", { backslashEscapes: false, doubledQuoteEscapes: true }, "string");
|
|
201
|
+
s.push("string", start);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (ch === "'") {
|
|
205
|
+
const previous = s.tokens.at(-1);
|
|
206
|
+
if (previous?.kind === "string" && /^[eE]'/.test(previous.text)) {
|
|
207
|
+
// PostgreSQL continuation inherits the E prefix across comments and
|
|
208
|
+
// newlines; reading this as an ordinary string would hide SQL tokens.
|
|
209
|
+
s.fail("unsupported_syntax", "Continuation after an escape string is not supported in agent SQL.");
|
|
210
|
+
}
|
|
211
|
+
s.readQuoted("'", { backslashEscapes: false, doubledQuoteEscapes: true }, "string");
|
|
212
|
+
s.push("string", start);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (ch === '"') {
|
|
216
|
+
const name = s.readQuoted('"', { backslashEscapes: false, doubledQuoteEscapes: true }, "quoted identifier");
|
|
217
|
+
if (name.length === 0) {
|
|
218
|
+
s.fail("unsupported_syntax", "Empty quoted identifier.", start);
|
|
219
|
+
}
|
|
220
|
+
s.push("quoted-identifier", start, name);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (ch === "$") {
|
|
224
|
+
if (/\d/.test(next)) {
|
|
225
|
+
s.pos++;
|
|
226
|
+
while (/\d/.test(s.peek()))
|
|
227
|
+
s.pos++;
|
|
228
|
+
s.push("parameter", start);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const tag = /^\$(?:[A-Za-z_\u0080-][A-Za-z0-9_\u0080-]*)?\$/.exec(s.sql.slice(s.pos));
|
|
232
|
+
if (!tag) {
|
|
233
|
+
s.fail("unexpected_character", 'Unexpected "$".');
|
|
234
|
+
}
|
|
235
|
+
const end = s.sql.indexOf(tag[0], s.pos + tag[0].length);
|
|
236
|
+
if (end === -1) {
|
|
237
|
+
s.fail("unterminated", "Unterminated dollar-quoted string.", start);
|
|
238
|
+
}
|
|
239
|
+
s.pos = end + tag[0].length;
|
|
240
|
+
s.push("string", start);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (isIdentStart(ch, "postgres")) {
|
|
244
|
+
s.readWord(start);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (/\d/.test(ch) || (ch === "." && /\d/.test(next))) {
|
|
248
|
+
s.readNumber(start);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (ch === ":" && next === ":") {
|
|
252
|
+
s.pos += 2;
|
|
253
|
+
s.push("punctuation", start);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (PUNCTUATION.has(ch)) {
|
|
257
|
+
s.pos++;
|
|
258
|
+
s.push("punctuation", start);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (ch === "?" && !POSTGRES_OPERATOR_CHARS.has(next)) {
|
|
262
|
+
s.pos++;
|
|
263
|
+
s.push("parameter", start);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (POSTGRES_OPERATOR_CHARS.has(ch)) {
|
|
267
|
+
s.readOperator(start, POSTGRES_OPERATOR_CHARS);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
s.fail("unexpected_character", `Unexpected character "${ch}".`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function lexBigQuery(s) {
|
|
274
|
+
while (s.pos < s.sql.length) {
|
|
275
|
+
const ch = s.peek();
|
|
276
|
+
const next = s.peek(1);
|
|
277
|
+
const start = s.pos;
|
|
278
|
+
if (WHITESPACE.has(ch)) {
|
|
279
|
+
s.pos++;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (ch === "#" || (ch === "-" && next === "-")) {
|
|
283
|
+
s.skipLineComment();
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (ch === "/" && next === "*") {
|
|
287
|
+
s.skipBlockComment();
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const literal = /^([rRbB]{0,2})('''|"""|'|")/.exec(s.sql.slice(s.pos));
|
|
291
|
+
if (literal &&
|
|
292
|
+
/^(?:|[rR]|[bB]|[rR][bB]|[bB][rR])$/.test(literal[1]) &&
|
|
293
|
+
// A bare prefix letter is an identifier unless a quote follows it.
|
|
294
|
+
(literal[1].length > 0 || ch === "'" || ch === '"')) {
|
|
295
|
+
const raw = /[rR]/.test(literal[1]);
|
|
296
|
+
s.pos += literal[1].length;
|
|
297
|
+
s.readQuoted(literal[2], { backslashEscapes: !raw, doubledQuoteEscapes: false }, "string");
|
|
298
|
+
s.push("string", start);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (ch === "`") {
|
|
302
|
+
const name = s.readQuoted("`", { backslashEscapes: true, doubledQuoteEscapes: false }, "quoted identifier");
|
|
303
|
+
if (name.length === 0 || name.includes("\\")) {
|
|
304
|
+
s.fail("unsupported_syntax", "Empty or escaped backtick identifiers are not supported in agent SQL.", start);
|
|
305
|
+
}
|
|
306
|
+
s.push("quoted-identifier", start, name);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (ch === "@") {
|
|
310
|
+
s.pos += next === "@" ? 2 : 1;
|
|
311
|
+
if (!isIdentStart(s.peek(), "bigquery")) {
|
|
312
|
+
s.fail("unexpected_character", 'Expected a parameter name after "@".');
|
|
313
|
+
}
|
|
314
|
+
while (isIdentPart(s.peek(), "bigquery"))
|
|
315
|
+
s.pos++;
|
|
316
|
+
s.push("parameter", start);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (ch === "?") {
|
|
320
|
+
s.pos++;
|
|
321
|
+
s.push("parameter", start);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (isIdentStart(ch, "bigquery")) {
|
|
325
|
+
s.readWord(start);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (/\d/.test(ch) || (ch === "." && /\d/.test(next))) {
|
|
329
|
+
s.readNumber(start);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (PUNCTUATION.has(ch)) {
|
|
333
|
+
s.pos++;
|
|
334
|
+
s.push("punctuation", start);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (BIGQUERY_OPERATOR_CHARS.has(ch)) {
|
|
338
|
+
s.readOperator(start, BIGQUERY_OPERATOR_CHARS);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
s.fail("unexpected_character", `Unexpected character "${ch}".`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Splits agent-authored SQL into the tokens the target database will see.
|
|
346
|
+
* Comments and whitespace are dropped; everything else is kept with its
|
|
347
|
+
* source offsets. Throws {@link AgentSqlSyntaxError} for anything the lexer
|
|
348
|
+
* cannot read the same way the database does.
|
|
349
|
+
*/
|
|
350
|
+
export function lexAgentSql(sql, options) {
|
|
351
|
+
const scanner = new Scanner(sql, options.dialect);
|
|
352
|
+
if (options.dialect === "postgres")
|
|
353
|
+
lexPostgres(scanner);
|
|
354
|
+
else
|
|
355
|
+
lexBigQuery(scanner);
|
|
356
|
+
return scanner.tokens;
|
|
357
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type AgentSqlToken } from "./lexer.js";
|
|
2
|
+
export type AgentSqlPolicyErrorCode = "unreadable" | "empty" | "multiple_statements" | "refused";
|
|
3
|
+
/**
|
|
4
|
+
* Agent SQL was refused by policy. The message is written for the agent: it
|
|
5
|
+
* says what was refused and what to do instead. `code` lets a caller word the
|
|
6
|
+
* structural cases in its own terms.
|
|
7
|
+
*/
|
|
8
|
+
export declare class AgentSqlPolicyError extends Error {
|
|
9
|
+
readonly code: AgentSqlPolicyErrorCode;
|
|
10
|
+
constructor(message: string, code?: AgentSqlPolicyErrorCode);
|
|
11
|
+
}
|
|
12
|
+
export interface AgentPostgresStatement {
|
|
13
|
+
/**
|
|
14
|
+
* The statement text from its first token to its last, so a trailing
|
|
15
|
+
* semicolon or comment is gone and appended text cannot be swallowed.
|
|
16
|
+
*/
|
|
17
|
+
sql: string;
|
|
18
|
+
tokens: AgentSqlToken[];
|
|
19
|
+
/** Lower-case leading keyword, ignoring leading parentheses. */
|
|
20
|
+
keyword: string | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reads exactly one Postgres statement the way the database will. Throws
|
|
24
|
+
* {@link AgentSqlPolicyError} for text the lexer cannot read, an empty input,
|
|
25
|
+
* or more than one statement.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readAgentPostgresStatement(sql: string): AgentPostgresStatement;
|
|
28
|
+
/**
|
|
29
|
+
* Checks that need only the statement's tokens. Run them on every statement
|
|
30
|
+
* before it reaches the database, and again on the final executed text.
|
|
31
|
+
*/
|
|
32
|
+
export declare function assertAgentPostgresExpressionTokenPolicy(statement: AgentPostgresStatement): void;
|
|
33
|
+
/** Includes the schema-qualification restrictions needed by raw DB tools. */
|
|
34
|
+
export declare function assertAgentPostgresTokenPolicy(statement: AgentPostgresStatement): void;
|
|
35
|
+
export interface AgentSqlQueryRunner {
|
|
36
|
+
unsafe(sql: string, args?: unknown[]): Promise<unknown[]>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Checks expression resolution inside the executing transaction. Callers
|
|
40
|
+
* must separately scope relation reads; this check does not require temp
|
|
41
|
+
* views. Conversions can invoke a type's input routines or domain checks,
|
|
42
|
+
* including those nested inside a temporary view's composite row type.
|
|
43
|
+
*/
|
|
44
|
+
export declare function verifyAgentPostgresExpressions(db: AgentSqlQueryRunner, statement: AgentPostgresStatement): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Checks expression and relation resolution in the executing transaction,
|
|
47
|
+
* after the per-user temporary views exist. Every resolved relation must
|
|
48
|
+
* belong to this session's temp schema; qualifiers cannot name a schema or
|
|
49
|
+
* this database.
|
|
50
|
+
*/
|
|
51
|
+
export declare function verifyAgentPostgresResolution(db: AgentSqlQueryRunner, statement: AgentPostgresStatement): Promise<void>;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { agentSqlIdentifierNames, agentSqlQualifiedReferences, agentSqlTypeNames, leadingAgentSqlKeyword, splitAgentSqlStatements, } from "./analysis.js";
|
|
2
|
+
import { AgentSqlSyntaxError, lexAgentSql, } from "./lexer.js";
|
|
3
|
+
/**
|
|
4
|
+
* Agent SQL was refused by policy. The message is written for the agent: it
|
|
5
|
+
* says what was refused and what to do instead. `code` lets a caller word the
|
|
6
|
+
* structural cases in its own terms.
|
|
7
|
+
*/
|
|
8
|
+
export class AgentSqlPolicyError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
constructor(message, code = "refused") {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "AgentSqlPolicyError";
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function refuse(message, code = "refused") {
|
|
17
|
+
throw new AgentSqlPolicyError(message, code);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Reads exactly one Postgres statement the way the database will. Throws
|
|
21
|
+
* {@link AgentSqlPolicyError} for text the lexer cannot read, an empty input,
|
|
22
|
+
* or more than one statement.
|
|
23
|
+
*/
|
|
24
|
+
export function readAgentPostgresStatement(sql) {
|
|
25
|
+
let tokens;
|
|
26
|
+
try {
|
|
27
|
+
tokens = lexAgentSql(sql, { dialect: "postgres" });
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error instanceof AgentSqlSyntaxError) {
|
|
31
|
+
refuse(`The SQL could not be read safely: ${error.message}`, "unreadable");
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
const statements = splitAgentSqlStatements(tokens);
|
|
36
|
+
if (statements.length === 0)
|
|
37
|
+
refuse("The SQL is empty.", "empty");
|
|
38
|
+
if (statements.length > 1) {
|
|
39
|
+
refuse("The SQL contains more than one statement. Send one statement at a time.", "multiple_statements");
|
|
40
|
+
}
|
|
41
|
+
const [statementTokens] = statements;
|
|
42
|
+
return {
|
|
43
|
+
sql: sql.slice(statementTokens[0].start, statementTokens[statementTokens.length - 1].end),
|
|
44
|
+
tokens: statementTokens,
|
|
45
|
+
keyword: leadingAgentSqlKeyword(statementTokens),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// Postgres truncates longer identifiers to this many bytes, so a longer name
|
|
49
|
+
// could resolve to an object whose name does not match it textually.
|
|
50
|
+
const MAX_IDENTIFIER_BYTES = 63;
|
|
51
|
+
// Schemas that hold base tables or catalogs. A name qualified with one of them
|
|
52
|
+
// skips the per-user views, which only shadow unqualified names. The
|
|
53
|
+
// in-transaction check below also catches every other schema in the database.
|
|
54
|
+
// `main` is SQLite's schema name, kept so ported SQL gets the same answer.
|
|
55
|
+
const KNOWN_SCHEMA_RE = /^(?:public|main|pg_catalog|information_schema|pg_toast|pg_temp|pg_temp_\d+|pg_toast_temp_\d+)$/i;
|
|
56
|
+
// Built-in functions that run SQL text, read or change server or session
|
|
57
|
+
// state, or reach outside the database. Matched against every identifier, not
|
|
58
|
+
// only called names, because Postgres also resolves `row.fn` as `fn(row)`.
|
|
59
|
+
const DENIED_IDENTIFIER_RES = [
|
|
60
|
+
// Catalog functions and relations. A short allowlist below keeps the
|
|
61
|
+
// harmless type and size helpers usable.
|
|
62
|
+
/^pg_/i,
|
|
63
|
+
// Large-object functions read and write server-side files and objects.
|
|
64
|
+
/^lo_/i,
|
|
65
|
+
// dblink opens a new connection that runs arbitrary SQL.
|
|
66
|
+
/^dblink/i,
|
|
67
|
+
// query_to_xml, table_to_xml, schema_to_xml, ... run or dump SQL by name.
|
|
68
|
+
/_to_xml/i,
|
|
69
|
+
/^(?:set_config|current_setting|nextval|currval|lastval|setval|ts_stat|ts_rewrite)$/i,
|
|
70
|
+
// These helpers convert fields using a supplied row type or type OID,
|
|
71
|
+
// which can invoke app-defined input routines and domain checks.
|
|
72
|
+
/^jsonb?_(?:populate_record(?:set|_valid)?|to_record(?:set)?)$/i,
|
|
73
|
+
/^(?:array|domain|record|range|multirange)_(?:in|recv)$/i,
|
|
74
|
+
];
|
|
75
|
+
const ALLOWED_PG_IDENTIFIERS = new Set([
|
|
76
|
+
"pg_typeof",
|
|
77
|
+
"pg_column_size",
|
|
78
|
+
"pg_size_pretty",
|
|
79
|
+
]);
|
|
80
|
+
/**
|
|
81
|
+
* Checks that need only the statement's tokens. Run them on every statement
|
|
82
|
+
* before it reaches the database, and again on the final executed text.
|
|
83
|
+
*/
|
|
84
|
+
export function assertAgentPostgresExpressionTokenPolicy(statement) {
|
|
85
|
+
for (const [index, token] of statement.tokens.entries()) {
|
|
86
|
+
if (token.kind !== "word" && token.kind !== "quoted-identifier")
|
|
87
|
+
continue;
|
|
88
|
+
if (Buffer.byteLength(token.value, "utf8") > MAX_IDENTIFIER_BYTES) {
|
|
89
|
+
refuse(`Identifier "${token.value.slice(0, 20)}…" is longer than ${MAX_IDENTIFIER_BYTES} bytes.`);
|
|
90
|
+
}
|
|
91
|
+
if (token.value === "pg_catalog" &&
|
|
92
|
+
statement.tokens[index + 1]?.text === ".") {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const value = token.value.toLowerCase();
|
|
96
|
+
if (ALLOWED_PG_IDENTIFIERS.has(value))
|
|
97
|
+
continue;
|
|
98
|
+
if (DENIED_IDENTIFIER_RES.some((re) => re.test(value))) {
|
|
99
|
+
refuse(`"${token.value}" is not available in agent SQL. Query the app's tables by their bare names.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Includes the schema-qualification restrictions needed by raw DB tools. */
|
|
104
|
+
export function assertAgentPostgresTokenPolicy(statement) {
|
|
105
|
+
assertAgentPostgresExpressionTokenPolicy(statement);
|
|
106
|
+
for (const reference of agentSqlQualifiedReferences(statement.tokens)) {
|
|
107
|
+
if (reference.qualifiers.length > 1) {
|
|
108
|
+
refuse(`"${[...reference.qualifiers, reference.name].join(".")}" names a database or schema. Use bare table names; the current user's scoping is applied automatically.`);
|
|
109
|
+
}
|
|
110
|
+
if (KNOWN_SCHEMA_RE.test(reference.qualifiers[0])) {
|
|
111
|
+
refuse(`Schema-qualified names such as "${reference.qualifiers[0]}.${reference.name}" bypass the per-user data scoping. Use the bare table name; the current user's scoping is applied automatically.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Checks expression resolution inside the executing transaction. Callers
|
|
117
|
+
* must separately scope relation reads; this check does not require temp
|
|
118
|
+
* views. Conversions can invoke a type's input routines or domain checks,
|
|
119
|
+
* including those nested inside a temporary view's composite row type.
|
|
120
|
+
*/
|
|
121
|
+
export async function verifyAgentPostgresExpressions(db, statement) {
|
|
122
|
+
assertAgentPostgresExpressionTokenPolicy(statement);
|
|
123
|
+
const [setting] = (await db.unsafe("SELECT pg_catalog.current_setting('standard_conforming_strings') AS value"));
|
|
124
|
+
if (setting?.value !== "on") {
|
|
125
|
+
refuse("Agent SQL requires standard_conforming_strings to be on for this database connection.");
|
|
126
|
+
}
|
|
127
|
+
const names = [...agentSqlIdentifierNames(statement.tokens)];
|
|
128
|
+
const typeNames = [...agentSqlTypeNames(statement.tokens)];
|
|
129
|
+
if (typeNames.length > 0) {
|
|
130
|
+
const types = (await db.unsafe(`SELECT t.typname AS name
|
|
131
|
+
FROM pg_catalog.pg_type t
|
|
132
|
+
JOIN pg_catalog.pg_namespace ns ON ns.oid = t.typnamespace
|
|
133
|
+
WHERE t.typname = ANY($1::text[])
|
|
134
|
+
AND ns.nspname <> 'pg_catalog'
|
|
135
|
+
LIMIT 1`, [typeNames]));
|
|
136
|
+
if (types.length > 0) {
|
|
137
|
+
refuse(`"${types[0].name}" is not a built-in type, so agent SQL cannot convert values to it.`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const routines = (await db.unsafe(`SELECT p.proname AS name
|
|
141
|
+
FROM pg_catalog.pg_proc p
|
|
142
|
+
JOIN pg_catalog.pg_namespace ns ON ns.oid = p.pronamespace
|
|
143
|
+
WHERE p.proname = ANY($1::text[])
|
|
144
|
+
AND ns.nspname <> 'pg_catalog'
|
|
145
|
+
LIMIT 1`, [names]));
|
|
146
|
+
if (routines.length > 0) {
|
|
147
|
+
refuse(`"${routines[0].name}" matches an app-defined database function, which agent SQL cannot call.`);
|
|
148
|
+
}
|
|
149
|
+
const operators = [
|
|
150
|
+
...new Set(statement.tokens
|
|
151
|
+
.filter((token) => token.kind === "operator")
|
|
152
|
+
.map((token) => token.text)),
|
|
153
|
+
];
|
|
154
|
+
if (operators.length > 0) {
|
|
155
|
+
const appOperators = (await db.unsafe(`SELECT o.oprname AS name
|
|
156
|
+
FROM pg_catalog.pg_operator o
|
|
157
|
+
JOIN pg_catalog.pg_namespace ns ON ns.oid = o.oprnamespace
|
|
158
|
+
WHERE o.oprname = ANY($1::text[])
|
|
159
|
+
AND ns.nspname <> 'pg_catalog'
|
|
160
|
+
LIMIT 1`, [operators]));
|
|
161
|
+
if (appOperators.length > 0) {
|
|
162
|
+
refuse(`Operator "${appOperators[0].name}" is backed by an app-defined database function, which agent SQL cannot call.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Checks expression and relation resolution in the executing transaction,
|
|
168
|
+
* after the per-user temporary views exist. Every resolved relation must
|
|
169
|
+
* belong to this session's temp schema; qualifiers cannot name a schema or
|
|
170
|
+
* this database.
|
|
171
|
+
*/
|
|
172
|
+
export async function verifyAgentPostgresResolution(db, statement) {
|
|
173
|
+
assertAgentPostgresTokenPolicy(statement);
|
|
174
|
+
await verifyAgentPostgresExpressions(db, statement);
|
|
175
|
+
const names = [...agentSqlIdentifierNames(statement.tokens)];
|
|
176
|
+
if (names.length === 0)
|
|
177
|
+
return;
|
|
178
|
+
const qualifiers = [
|
|
179
|
+
...new Set(agentSqlQualifiedReferences(statement.tokens).flatMap((reference) => reference.qualifiers)),
|
|
180
|
+
];
|
|
181
|
+
if (qualifiers.length > 0) {
|
|
182
|
+
const schemas = (await db.unsafe(`SELECT nspname AS name FROM pg_catalog.pg_namespace WHERE nspname = ANY($1::text[])
|
|
183
|
+
UNION ALL
|
|
184
|
+
SELECT pg_catalog.current_database() WHERE pg_catalog.current_database() = ANY($1::text[])`, [qualifiers]));
|
|
185
|
+
if (schemas.length > 0) {
|
|
186
|
+
refuse(`"${schemas[0].name}" names a schema or database. Use bare table names; the current user's scoping is applied automatically.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const relations = (await db.unsafe(`SELECT n.name
|
|
190
|
+
FROM pg_catalog.unnest($1::text[]) AS n(name)
|
|
191
|
+
JOIN pg_catalog.pg_class c
|
|
192
|
+
ON c.oid = pg_catalog.to_regclass(pg_catalog.quote_ident(n.name))
|
|
193
|
+
WHERE c.relnamespace IS DISTINCT FROM pg_catalog.pg_my_temp_schema()`, [names]));
|
|
194
|
+
if (relations.length > 0) {
|
|
195
|
+
refuse(`"${relations[0].name}" is not one of the app's tables scoped to the current user, so agent SQL cannot read or write it.`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type AgentSqlDialect, type AgentSqlToken } from "./lexer.js";
|
|
2
|
+
export interface AgentSqlQueryCte {
|
|
3
|
+
name: string;
|
|
4
|
+
quoted: boolean;
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
}
|
|
8
|
+
export interface AgentSqlQuerySource extends AgentSqlQueryCte {
|
|
9
|
+
qualifiers: string[];
|
|
10
|
+
cte: boolean;
|
|
11
|
+
commaSeparated: boolean;
|
|
12
|
+
modifiers: ("only" | "lateral")[];
|
|
13
|
+
alias?: {
|
|
14
|
+
name: string;
|
|
15
|
+
quoted: boolean;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface AgentSqlQuery {
|
|
19
|
+
sql: string;
|
|
20
|
+
dialect: AgentSqlDialect;
|
|
21
|
+
tokens: AgentSqlToken[];
|
|
22
|
+
sources: AgentSqlQuerySource[];
|
|
23
|
+
ctes: AgentSqlQueryCte[];
|
|
24
|
+
}
|
|
25
|
+
/** Reads sources in a bounded SELECT grammar; unsupported source syntax throws. */
|
|
26
|
+
export declare function readAgentSqlQuery(sql: string, options: {
|
|
27
|
+
dialect: AgentSqlDialect;
|
|
28
|
+
}): AgentSqlQuery;
|
|
29
|
+
/** Replaces only analyzed source-name spans, preserving all other source text. */
|
|
30
|
+
export declare function rewriteAgentSqlQuerySources(query: AgentSqlQuery, replacer: (source: AgentSqlQuerySource) => string): string;
|