@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.
Files changed (118) hide show
  1. package/README.md +22 -22
  2. package/dist/a2a/server.d.ts +2 -2
  3. package/dist/a2a/server.js +13 -2
  4. package/dist/agent/thread-data-builder.d.ts +4 -0
  5. package/dist/agent/thread-data-builder.js +8 -0
  6. package/dist/agent-sql/analysis.d.ts +31 -0
  7. package/dist/agent-sql/analysis.js +169 -0
  8. package/dist/agent-sql/index.d.ts +4 -0
  9. package/dist/agent-sql/index.js +4 -0
  10. package/dist/agent-sql/lexer.d.ts +37 -0
  11. package/dist/agent-sql/lexer.js +357 -0
  12. package/dist/agent-sql/postgres.d.ts +51 -0
  13. package/dist/agent-sql/postgres.js +197 -0
  14. package/dist/agent-sql/query.d.ts +30 -0
  15. package/dist/agent-sql/query.js +514 -0
  16. package/dist/catalog.json +24 -24
  17. package/dist/client/analytics/index.d.ts +1 -1
  18. package/dist/client/analytics/index.js +1 -1
  19. package/dist/client/analytics.d.ts +8 -0
  20. package/dist/client/analytics.js +17 -2
  21. package/dist/client/session-replay.js +3 -2
  22. package/dist/identity/offboard.js +31 -3
  23. package/dist/identity/rekey.d.ts +12 -1
  24. package/dist/identity/rekey.js +77 -6
  25. package/dist/identity/retired-emails.d.ts +23 -0
  26. package/dist/identity/retired-emails.js +55 -0
  27. package/dist/mcp/actions/create-org-service-token.js +20 -8
  28. package/dist/mcp/actions/service-token-access.d.ts +1 -0
  29. package/dist/mcp/actions/service-token-access.js +41 -24
  30. package/dist/mcp/build-server.d.ts +20 -6
  31. package/dist/mcp/build-server.js +131 -35
  32. package/dist/mcp/connect-route.d.ts +4 -3
  33. package/dist/mcp/connect-route.js +140 -82
  34. package/dist/mcp/connect-store.d.ts +17 -11
  35. package/dist/mcp/connect-store.js +88 -60
  36. package/dist/mcp/credential-issuance.d.ts +23 -0
  37. package/dist/mcp/credential-issuance.js +77 -0
  38. package/dist/mcp/credential-membership.d.ts +26 -0
  39. package/dist/mcp/credential-membership.js +67 -0
  40. package/dist/mcp/oauth-route.js +133 -45
  41. package/dist/mcp/oauth-store.d.ts +15 -5
  42. package/dist/mcp/oauth-store.js +119 -34
  43. package/dist/mcp/oauth-token.d.ts +4 -0
  44. package/dist/mcp/oauth-token.js +5 -0
  45. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  46. package/dist/mcp/server.js +14 -0
  47. package/dist/onboarding/default-steps.js +1 -1
  48. package/dist/org/handlers.d.ts +1 -1
  49. package/dist/org/index.d.ts +2 -2
  50. package/dist/org/membership.d.ts +3 -1
  51. package/dist/org/membership.js +5 -2
  52. package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
  53. package/dist/provider-api/actions/provider-api.d.ts +10 -10
  54. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  55. package/dist/resource-changes/store.d.ts +2 -0
  56. package/dist/resource-changes/store.js +4 -0
  57. package/dist/resources/handlers.d.ts +1 -1
  58. package/dist/scripts/db/exec.js +28 -101
  59. package/dist/scripts/db/patch.js +14 -34
  60. package/dist/scripts/db/postgres-client.d.ts +8 -1
  61. package/dist/scripts/db/postgres-client.js +20 -6
  62. package/dist/scripts/db/query.js +31 -33
  63. package/dist/scripts/db/safety.d.ts +47 -2
  64. package/dist/scripts/db/safety.js +383 -137
  65. package/dist/scripts/db/scoping.d.ts +1 -0
  66. package/dist/scripts/db/scoping.js +3 -2
  67. package/dist/search/index-store.d.ts +21 -0
  68. package/dist/search/index-store.js +47 -0
  69. package/dist/search/indexer.js +35 -32
  70. package/dist/search/registry.d.ts +6 -3
  71. package/dist/search/registry.js +30 -8
  72. package/dist/secrets/routes.d.ts +6 -6
  73. package/dist/server/agent-chat-plugin.js +18 -6
  74. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  75. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  76. package/dist/server/agent-run-context.js +9 -0
  77. package/dist/server/attribution.d.ts +2 -0
  78. package/dist/server/attribution.js +7 -1
  79. package/dist/server/auth.js +9 -0
  80. package/dist/server/credential-membership-unavailable.d.ts +13 -0
  81. package/dist/server/credential-membership-unavailable.js +22 -0
  82. package/dist/server/index.d.ts +1 -0
  83. package/dist/server/index.js +1 -0
  84. package/dist/server/onboarding-html.js +1 -1
  85. package/dist/server/realtime-token.d.ts +1 -1
  86. package/dist/server/recap-image-route.js +4 -0
  87. package/dist/server/release-schema.js +4 -0
  88. package/dist/shared/docs-url.d.ts +3 -1
  89. package/dist/shared/docs-url.js +6 -3
  90. package/dist/shared/environment-lanes.d.ts +8 -0
  91. package/dist/shared/environment-lanes.js +26 -0
  92. package/dist/templates/chat/.agents/skills/security/SKILL.md +9 -2
  93. package/dist/templates/chat/.agents/skills/storing-data/SKILL.md +12 -7
  94. package/dist/templates/chat/README.md +2 -2
  95. package/dist/templates/default/.agents/skills/security/SKILL.md +9 -2
  96. package/dist/templates/default/.agents/skills/storing-data/SKILL.md +12 -7
  97. package/dist/templates/default/app/routes/home.tsx +5 -1
  98. package/dist/templates/headless/.agents/skills/security/SKILL.md +9 -2
  99. package/dist/templates/headless/.agents/skills/storing-data/SKILL.md +12 -7
  100. package/dist/templates/workspace-core/.agents/skills/security/SKILL.md +9 -2
  101. package/dist/templates/workspace-core/.agents/skills/storing-data/SKILL.md +12 -7
  102. package/dist/tracking/providers.js +13 -1
  103. package/docs/content/authentication.mdx +5 -1
  104. package/docs/content/durable-background-runs.mdx +17 -8
  105. package/docs/content/external-agents.mdx +3 -1
  106. package/docs/content/mcp-protocol.mdx +6 -2
  107. package/docs/content/security.mdx +1 -1
  108. package/package.json +5 -4
  109. package/src/templates/chat/.agents/skills/security/SKILL.md +9 -2
  110. package/src/templates/chat/.agents/skills/storing-data/SKILL.md +12 -7
  111. package/src/templates/chat/README.md +2 -2
  112. package/src/templates/default/.agents/skills/security/SKILL.md +9 -2
  113. package/src/templates/default/.agents/skills/storing-data/SKILL.md +12 -7
  114. package/src/templates/default/app/routes/home.tsx +5 -1
  115. package/src/templates/headless/.agents/skills/security/SKILL.md +9 -2
  116. package/src/templates/headless/.agents/skills/storing-data/SKILL.md +12 -7
  117. package/src/templates/workspace-core/.agents/skills/security/SKILL.md +9 -2
  118. 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;