agent-sql 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +13 -15
  2. package/dist/index.mjs +17 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,20 +4,20 @@ Sanitise agent-written SQL for multi-tenant DBs.
4
4
 
5
5
  You provide a tenant ID, and the agent supplies the query.
6
6
 
7
- agent-sql works by fully parsing the supplied SQL query into an AST.
8
- The grammar ONLY accepts `SELECT` statements. Anything else is an error.
9
- CTEs and other complex things that we aren't confident of securing: error.
10
-
11
- It ensures that that the needed tenant table is somewhere in the query,
12
- and adds a `WHERE` clause ensuring that only values from the supplied ID are returned.
13
- Then it checks that the tables and `JOIN`s follow the schema, preventing sneaky joins.
7
+ Apparently this is how [Trigger.dev does it](https://x.com/mattaitken/status/2033928542975639785).
8
+ And [Cloudflare](https://x.com/thomas_ankcorn/status/2033931057133748330).
14
9
 
15
- Function calls also go through a whitelist (configurable).
10
+ ## How it works
16
11
 
17
- Finally, we throw in a `LIMIT` clause (configurable) to prevent accidental LLM denial-of-service.
12
+ agent-sql works by fully parsing the supplied SQL query into an AST and transforming it:
18
13
 
19
- Apparently this is how [Trigger.dev does it](https://x.com/mattaitken/status/2033928542975639785).
20
- And [Cloudflare](https://x.com/thomas_ankcorn/status/2033931057133748330).
14
+ - **Only `SELECT`:** it's impossible to insert, drop or anything else.
15
+ - **Reduced subset:** CTEs, subqueries and other tricky things are rejected.
16
+ - **Limited functions:** passed through a (configurable) whitelist.
17
+ - **No DoS:** a default `LIMIT` is applied, but can be adjusted.
18
+ - **`WHERE` guards:** insert multiple tenant/ownership conditions to be inserted.
19
+ - **`JOIN`s added:** if needed to reach the guard tenant tables (save on tokens).
20
+ - **No sneaky joins:** no `join secrets on true`. We have your back.
21
21
 
22
22
  ## Quickstart
23
23
 
@@ -28,17 +28,15 @@ npm install agent-sql
28
28
  ```ts
29
29
  import { agentSql } from "agent-sql";
30
30
 
31
- const sql = agentSql(`SELECT * FROM msg`, "msg.user_id", 123);
31
+ const sql = agentSql(`SELECT * FROM msg`, "msg.tenant_id", 123);
32
32
 
33
33
  console.log(sql);
34
34
  // SELECT *
35
35
  // FROM msg
36
- // WHERE msg.user_id = 123
36
+ // WHERE msg.tenant_id = 123
37
37
  // LIMIT 10000
38
38
  ```
39
39
 
40
- `agent-sql` parses the SQL, enforces a mandatory equality filter on the given column as the outermost `AND` condition (so it cannot be short-circuited by agent-supplied `OR` clauses), and returns the sanitised SQL string.
41
-
42
40
  ## Usage
43
41
 
44
42
  ### Define once, use many times
package/dist/index.mjs CHANGED
@@ -822,6 +822,7 @@ function resolveGraphForJoins(ast, schema, guards) {
822
822
  const haveTables = /* @__PURE__ */ new Set();
823
823
  haveTables.add(ast.from.table.name);
824
824
  for (const join of ast.joins) haveTables.add(join.table.name);
825
+ const originalTables = new Set(haveTables);
825
826
  const adj = buildAdjacency(schema);
826
827
  const newJoins = [];
827
828
  for (const guard of guards) {
@@ -839,8 +840,10 @@ function resolveGraphForJoins(ast, schema, guards) {
839
840
  }
840
841
  }
841
842
  if (newJoins.length === 0) return Ok(ast);
843
+ const columns = qualifyWildcards(ast.columns, originalTables);
842
844
  return Ok({
843
845
  ...ast,
846
+ columns,
844
847
  joins: [...ast.joins, ...newJoins]
845
848
  });
846
849
  }
@@ -927,6 +930,20 @@ function edgeToJoin(edge, fromTable) {
927
930
  }
928
931
  };
929
932
  }
933
+ function qualifyWildcards(columns, tables) {
934
+ if (!columns.some((c) => c.expr.kind === "wildcard")) return columns;
935
+ const qualified = [];
936
+ for (const col of columns) if (col.expr.kind === "wildcard") for (const table of tables) qualified.push({
937
+ type: "column",
938
+ expr: {
939
+ type: "column_expr",
940
+ kind: "qualified_wildcard",
941
+ table
942
+ }
943
+ });
944
+ else qualified.push(col);
945
+ return qualified;
946
+ }
930
947
  //#endregion
931
948
  //#region src/utils.ts
932
949
  function unreachable(x) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sql",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "A starter for creating a TypeScript package.",
5
5
  "keywords": [
6
6
  "agent",