ffp-sql-sandbox 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/INTEGRATION.md +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +144 -0
  4. package/dist/docker-executor.d.ts +6 -0
  5. package/dist/docker-executor.d.ts.map +1 -0
  6. package/dist/docker-executor.js +192 -0
  7. package/dist/docker-executor.js.map +1 -0
  8. package/dist/docker-types.d.ts +37 -0
  9. package/dist/docker-types.d.ts.map +1 -0
  10. package/dist/docker-types.js +50 -0
  11. package/dist/docker-types.js.map +1 -0
  12. package/dist/execute-sql.d.ts +9 -0
  13. package/dist/execute-sql.d.ts.map +1 -0
  14. package/dist/execute-sql.js +89 -0
  15. package/dist/execute-sql.js.map +1 -0
  16. package/dist/host-allowlist.d.ts +2 -0
  17. package/dist/host-allowlist.d.ts.map +1 -0
  18. package/dist/host-allowlist.js +9 -0
  19. package/dist/host-allowlist.js.map +1 -0
  20. package/dist/image.d.ts +16 -0
  21. package/dist/image.d.ts.map +1 -0
  22. package/dist/image.js +19 -0
  23. package/dist/image.js.map +1 -0
  24. package/dist/index.d.ts +8 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +9 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/limits.d.ts +11 -0
  29. package/dist/limits.d.ts.map +1 -0
  30. package/dist/limits.js +25 -0
  31. package/dist/limits.js.map +1 -0
  32. package/dist/password-tar.d.ts +6 -0
  33. package/dist/password-tar.d.ts.map +1 -0
  34. package/dist/password-tar.js +29 -0
  35. package/dist/password-tar.js.map +1 -0
  36. package/dist/rewrite-host.d.ts +6 -0
  37. package/dist/rewrite-host.d.ts.map +1 -0
  38. package/dist/rewrite-host.js +16 -0
  39. package/dist/rewrite-host.js.map +1 -0
  40. package/dist/stream-consumer.d.ts +31 -0
  41. package/dist/stream-consumer.d.ts.map +1 -0
  42. package/dist/stream-consumer.js +139 -0
  43. package/dist/stream-consumer.js.map +1 -0
  44. package/dist/types.d.ts +50 -0
  45. package/dist/types.d.ts.map +1 -0
  46. package/dist/types.js +8 -0
  47. package/dist/types.js.map +1 -0
  48. package/dist/validate-sql.d.ts +8 -0
  49. package/dist/validate-sql.d.ts.map +1 -0
  50. package/dist/validate-sql.js +30 -0
  51. package/dist/validate-sql.js.map +1 -0
  52. package/package.json +45 -0
  53. package/sandbox/Dockerfile +13 -0
  54. package/sandbox/events.d.ts +12 -0
  55. package/sandbox/events.js +14 -0
  56. package/sandbox/execute.js +235 -0
  57. package/sandbox/package.json +10 -0
  58. package/sandbox/secrets.d.ts +5 -0
  59. package/sandbox/secrets.js +16 -0
  60. package/sandbox/session-setup.d.ts +4 -0
  61. package/sandbox/session-setup.js +20 -0
  62. package/sandbox/stream-limit.d.ts +16 -0
  63. package/sandbox/stream-limit.js +35 -0
package/INTEGRATION.md ADDED
@@ -0,0 +1,37 @@
1
+ # Integrating ffp-sql-sandbox with ChuTingzj/ai-bi
2
+
3
+ This repository does **not** wire the library into [ChuTingzj/ai-bi](https://github.com/ChuTingzj/ai-bi). Keep that as a follow-up PR in ai-bi after this package is a real dependency, tests are green, and there are no P0s.
4
+
5
+ ## Adapter shape
6
+
7
+ Keep NestJS, `CryptoService`, and `DataSource` inside ai-bi. Do not leak those types into this library.
8
+
9
+ Suggested adapter (ai-bi `SandboxService`):
10
+
11
+ 1. Decrypt `dataSource.password` with the existing `CryptoService`.
12
+ 2. Call `validateSql` / `executeSql` from `ffp-sql-sandbox`.
13
+ 3. Map `{ ok: true, data }` → the current `SandboxResult` / `QueryResult` shape (`success` / `columns`+`rows`).
14
+ 4. Map `{ ok: false, code, error }` → `{ success: false, error }`.
15
+
16
+ ## Host rewrite stays in ai-bi
17
+
18
+ `resolveSandboxDbHost` is **not** a public export of this package (on purpose).
19
+
20
+ ai-bi may keep its private loopback → `host.docker.internal` helper for its own tests and Docker-on-Linux setups. If the adapter rewrites the host *before* calling `executeSql`, put the rewritten name on `hostAllowlist` as well.
21
+
22
+ Alternatively, pass the original host (`localhost`, `db.internal`, …) in `connection.host`, allowlist **that** name, and let this library apply a private rewrite after the allowlist check. Either way, user-controlled hosts must not bypass `hostAllowlist`.
23
+
24
+ ## What not to copy from the old sandbox
25
+
26
+ | Old ai-bi behavior | v1 library |
27
+ | --- | --- |
28
+ | `DB_PASS` / password in container `Env` | Forbidden. Password goes to tmpfs `/run/secrets/db_password`. |
29
+ | `Cmd: [sql]` | SQL on stdin JSON. |
30
+ | Floating `SANDBOX_IMAGE=ai-bi-sandbox:latest` | Default image digest-pinned; custom `image` is untrusted. |
31
+ | Configurable validator prefixes/patterns | Not exposed. `validateSql(sql)` only. |
32
+ | Wait for container + `logs()` then parse/truncate | Streaming attach; `maxRows` / `maxBytes` applied as data arrives. |
33
+ | Regex as the security proof | Documented as fail-fast only. Proof is RO role + limits + allowlist. |
34
+
35
+ ## Image
36
+
37
+ Build `sandbox/Dockerfile` from this repo (or pull the published digest) and configure ai-bi to use `DEFAULT_SANDBOX_IMAGE` or an explicit digest-pinned ref. Do not default to `:latest`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FFP-Tech-Lab (From First Principle)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # ffp-sql-sandbox
2
+
3
+ Read-only SQL sandbox primitives for Node.js: fail-fast `validateSql` plus one-shot Docker `executeSql` with hard resource, row, byte, timeout, and host-allowlist limits.
4
+
5
+ This is the v1 library extracted from the sandbox ideas in [ChuTingzj/ai-bi](https://github.com/ChuTingzj/ai-bi) (`validateSql` + dockerode one-shot container) and redesigned around a stricter security model. It is **not** wired into ai-bi in this repository — see [INTEGRATION.md](./INTEGRATION.md).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add ffp-sql-sandbox
11
+ ```
12
+
13
+ Docker is a **hard dependency** of `executeSql`. If the engine cannot be pinged, the call fails with `DOCKER_UNAVAILABLE` rather than falling back to in-process SQL.
14
+
15
+ Build the runner image from `sandbox/Dockerfile` (digest-pinned `FROM`) and either publish it at the default ref below or pass `image` (untrusted override).
16
+
17
+ ```bash
18
+ docker build -t ghcr.io/ffp-tech-lab/ffp-sql-sandbox-runner:v1 ./sandbox
19
+ ```
20
+
21
+ ## Public API
22
+
23
+ ```ts
24
+ import {
25
+ validateSql,
26
+ executeSql,
27
+ DEFAULT_SANDBOX_LIMITS,
28
+ DEFAULT_SANDBOX_IMAGE,
29
+ } from 'ffp-sql-sandbox'
30
+
31
+ validateSql(sql: string): { ok: true } | { ok: false; code: string; reason: string }
32
+
33
+ executeSql(input: {
34
+ sql: string
35
+ connection: {
36
+ type: 'postgres' | 'mysql'
37
+ host: string
38
+ port: number
39
+ user: string
40
+ password: string // never placed in container Env
41
+ database: string
42
+ }
43
+ hostAllowlist: string[] // required
44
+ limits?: Partial<SandboxLimits>
45
+ image?: string // untrusted override; default image is digest-pinned
46
+ }): Promise<
47
+ | { ok: true; data: { columns: string[]; rows: unknown[][]; truncated?: boolean } }
48
+ | { ok: false; code: string; error: string }
49
+ >
50
+ ```
51
+
52
+ `validateSql` is fail-fast only. There are no `allowPrefixes` / `forbidPatterns` options.
53
+
54
+ `resolveSandboxDbHost` is **not** exported. Loopback rewrite for container DNS, if needed, stays private (and, for ai-bi, in the adapter — see INTEGRATION.md).
55
+
56
+ ## Non-goals
57
+
58
+ - Natural language → SQL, query guidance, schema sync, or an AST parser as a required v1 component
59
+ - A multi-tenant auth / connection-pool product
60
+ - Claiming absolute network isolation (the runner uses Docker `bridge` so it can reach the allowlisted database)
61
+ - Exporting `resolveSandboxDbHost` as public API
62
+ - Making `validateSql` a proof of safety (it is a cheap reject, not a guarantee)
63
+
64
+ ## Threat model
65
+
66
+ Proof that a query cannot mutate data or exfiltrate unbounded results does **not** come from regex. v1 proof is the combination of:
67
+
68
+ | Guard | What it actually does |
69
+ | --- | --- |
70
+ | 1. Read-only DB role | **Caller must connect as a read-only role** (and/or a `READ ONLY` transaction). The library also sets `statement_timeout` and *prefers* a read-only session (`SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY` on Postgres, `SET SESSION TRANSACTION READ ONLY` on MySQL) when the dialect allows it. If that `SET` is ignored or the role can override it, the database role is still the real write barrier. |
71
+ | 2. Container limits + dual timeout | Memory, nano-CPUs, PID cap, dropped capabilities. **Dual timeout**: the runner sets DB `statement_timeout` / `MAX_EXECUTION_TIME` to `timeoutMs`, and the host kills the container after `timeoutMs + 2000ms` if it is still running. |
72
+ | 3. Streaming `maxRows` / `maxBytes` | The runner applies limits **as rows arrive** and stops/cancels instead of buffering the full result. The host consumer is a backstop on the live Docker attach stream. **Demux-then-truncate of a completed log buffer is not the limits implementation.** |
73
+ | 4. `connection.host` allowlist | SSRF defense. `hostAllowlist` is required. Hosts not on the list are rejected **before** any container is created. No arbitrary hosts, no glob, no CIDR in v1 — exact match after trim + case-insensitive compare. |
74
+
75
+ Passwords are written to a **tmpfs** file at `/run/secrets/db_password` inside the container (mode/uid for the `node` user). They are **never** placed in container `Env` (no `DB_PASS`, `PGPASSWORD`, or `MYSQL_PWD`). SQL is sent on stdin JSON, not `Cmd`, so `docker inspect` does not show the password.
76
+
77
+ ### Default image vs custom image
78
+
79
+ | Image | Trust |
80
+ | --- | --- |
81
+ | `DEFAULT_SANDBOX_IMAGE` (`…@sha256:…`) | Digest-pinned default. Treat as the supported runner. |
82
+ | `image` override | **Untrusted.** Allowed so callers can build locally, but a custom image can ignore streaming caps, log secrets, or exfiltrate. Pin and review your own image if you override. |
83
+
84
+ The Dockerfile `FROM` line is also digest-pinned (`node:22-bookworm-slim@sha256:83f487e0a63425e5b4d146fb5e5be574bcbe1b7b843d3ebafdd95eaf7767a7e5`).
85
+
86
+ Until GHCR publish, `DEFAULT_SANDBOX_IMAGE` is a **content digest** of the v1 runner files, not a registry-pullable image. `executeSql` without `image` will return `IMAGE_UNAVAILABLE` if that digest is not present locally. Build `sandbox/Dockerfile` and pass `image` (untrusted override), or `docker load` / retag the digest-pinned image. After the first registry push, replace the constant with `docker buildx imagetools inspect` output.
87
+
88
+ ## Default limits
89
+
90
+ | Limit | Default | Role |
91
+ | --- | --- | --- |
92
+ | `timeoutMs` | `10000` | DB `statement_timeout` / `MAX_EXECUTION_TIME`, plus container kill at `timeoutMs + 2000` |
93
+ | `memoryMb` | `128` | Container memory (and memory-swap) cap |
94
+ | `nanoCpus` | `500000000` (0.5 CPU) | Container CFS quota |
95
+ | `maxRows` | `1000` | Streaming row cap; result may set `truncated: true` |
96
+ | `maxBytes` | `1000000` | Streaming serialized-row / stdout byte cap; result may set `truncated: true` |
97
+
98
+ ## `hostAllowlist` contract
99
+
100
+ - Required on every `executeSql` call.
101
+ - Empty list → `HOST_NOT_ALLOWED` (deny all).
102
+ - Compared against the **caller-supplied** `connection.host` (trimmed, case-insensitive). The library may rewrite loopback (`localhost`, `127.0.0.1`, `::1`, `0.0.0.0`) to `host.docker.internal` *after* the allowlist check so the container can reach a DB on the Docker host. That rewrite is private and not a public API.
103
+ - Put the host you actually pass in on the list (e.g. `db.internal` or `localhost`). Do not accept user-controlled hosts without your own allowlist.
104
+
105
+ ## `validateSql` is fail-fast only (known bypasses)
106
+
107
+ `validateSql` is a cheap prefix + keyword + multi-statement reject. **Do not treat a `{ ok: true }` as authorization to run SQL outside this sandbox.** Known classes:
108
+
109
+ | Class | Example | Notes |
110
+ | --- | --- | --- |
111
+ | Writes that still look like `SELECT` | `SELECT … INTO …`, `SELECT pg_file_write(...)` | Prefix allowlist does not model Postgres side effects. **Read-only role** is the guard. |
112
+ | False positives | `SELECT * FROM t WHERE action = 'UPDATE'` | Keyword scan is not an AST. Fail-fast, not completeness. |
113
+ | Comment / encoding tricks | Leading `--` comments, Unicode homoglyphs | Rejected or missed; not a parser. |
114
+ | Expensive reads | `SELECT * FROM huge_table` | Allowed by regex; stopped by timeout + `maxRows`/`maxBytes` + role. |
115
+ | Multi-statement via protocol | Driver-level stacked queries if the runner used a naive exec | Runner sends a single query text; DB role + `READ ONLY` still apply. |
116
+ | Network | `dblink`, `http` FDW, `LOAD_FILE` | Allowlist is on `connection.host`, not on SQL-level network functions. Use a locked-down role. |
117
+
118
+ ## Error codes
119
+
120
+ | Code | Where |
121
+ | --- | --- |
122
+ | `NOT_READ_ONLY_PREFIX` | `validateSql` / `executeSql` |
123
+ | `FORBIDDEN_KEYWORD` | `validateSql` / `executeSql` |
124
+ | `MULTI_STATEMENT` | `validateSql` / `executeSql` |
125
+ | `HOST_NOT_ALLOWED` | `executeSql` |
126
+ | `DOCKER_UNAVAILABLE` | `executeSql` |
127
+ | `TIMEOUT` | `executeSql` |
128
+ | `INVALID_LIMITS` | `executeSql` |
129
+ | `IMAGE_UNPINNED` | `executeSql` (default image missing digest) |
130
+ | `IMAGE_UNAVAILABLE` | `executeSql` (image missing locally / not pullable; default digest is unpublished until GHCR) |
131
+ | `UNSUPPORTED_DIALECT` | `executeSql` |
132
+ | `EXECUTION_FAILED` | `executeSql` |
133
+
134
+ ## Scripts
135
+
136
+ ```bash
137
+ pnpm test # node:test via tsx
138
+ pnpm typecheck
139
+ pnpm build
140
+ ```
141
+
142
+ ## License
143
+
144
+ MIT. The GitHub repository may stay private until the org publishes it; that does not change the source license.
@@ -0,0 +1,6 @@
1
+ import { type DockerLike, type DockerRunRequest } from './docker-types.js';
2
+ import type { ExecuteSqlResult } from './types.js';
3
+ export type { DockerLike, DockerRunRequest } from './docker-types.js';
4
+ export declare function createDefaultDocker(): DockerLike;
5
+ export declare function runInSandbox(docker: DockerLike, request: DockerRunRequest): Promise<ExecuteSqlResult>;
6
+ //# sourceMappingURL=docker-executor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docker-executor.d.ts","sourceRoot":"","sources":["../src/docker-executor.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,gBAAgB,EACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAKtE,wBAAgB,mBAAmB,IAAI,UAAU,CAEhD;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,gBAAgB,CAAC,CA0K3B"}
@@ -0,0 +1,192 @@
1
+ import Docker from 'dockerode';
2
+ import { createPasswordTar } from './password-tar.js';
3
+ import { consumeNdjsonResult } from './stream-consumer.js';
4
+ import { demuxStdout, } from './docker-types.js';
5
+ const CONTAINER_KILL_GRACE_MS = 2_000;
6
+ const SECRET_MOUNT = '/run/secrets';
7
+ export function createDefaultDocker() {
8
+ return new Docker();
9
+ }
10
+ export async function runInSandbox(docker, request) {
11
+ let container = null;
12
+ let timedOut = false;
13
+ let killedForLimit = false;
14
+ let attachStream = null;
15
+ const containerTimeoutMs = request.timeoutMs + CONTAINER_KILL_GRACE_MS;
16
+ let killTimer;
17
+ try {
18
+ container = await docker.createContainer({
19
+ Image: request.image,
20
+ AttachStdin: true,
21
+ AttachStdout: true,
22
+ AttachStderr: true,
23
+ OpenStdin: true,
24
+ StdinOnce: true,
25
+ Tty: false,
26
+ Env: request.env,
27
+ HostConfig: {
28
+ Memory: request.memoryBytes,
29
+ MemorySwap: request.memoryBytes,
30
+ NanoCpus: request.nanoCpus,
31
+ NetworkMode: 'bridge',
32
+ ExtraHosts: ['host.docker.internal:host-gateway'],
33
+ AutoRemove: false,
34
+ CapDrop: ['ALL'],
35
+ SecurityOpt: ['no-new-privileges:true'],
36
+ PidsLimit: 256,
37
+ Tmpfs: {
38
+ [SECRET_MOUNT]: 'rw,noexec,nosuid,size=1m,mode=0700,uid=1000,gid=1000',
39
+ },
40
+ },
41
+ Labels: {
42
+ 'ffp.sql-sandbox': 'v1',
43
+ },
44
+ });
45
+ await container.start();
46
+ killTimer = setTimeout(() => {
47
+ timedOut = true;
48
+ void container?.kill().catch(() => undefined);
49
+ destroyStream(attachStream);
50
+ }, containerTimeoutMs);
51
+ await container.putArchive(createPasswordTar(request.password), {
52
+ path: SECRET_MOUNT,
53
+ });
54
+ attachStream = await container.attach({
55
+ stream: true,
56
+ stdin: true,
57
+ stdout: true,
58
+ stderr: true,
59
+ hijack: true,
60
+ });
61
+ const writable = attachStream;
62
+ if (typeof writable.write === 'function') {
63
+ writable.write(request.stdinJson);
64
+ if (typeof writable.end === 'function') {
65
+ writable.end();
66
+ }
67
+ }
68
+ const stdout = demuxStdout(attachStream);
69
+ const consumed = consumeNdjsonResult(stdout, {
70
+ maxRows: request.maxRows,
71
+ maxBytes: request.maxBytes,
72
+ abort: () => {
73
+ killedForLimit = true;
74
+ destroyStream(attachStream);
75
+ void container?.kill().catch(() => undefined);
76
+ },
77
+ });
78
+ const wait = container.wait();
79
+ let hangTimer;
80
+ const hang = new Promise((_, reject) => {
81
+ hangTimer = setTimeout(() => {
82
+ timedOut = true;
83
+ reject(Object.assign(new Error('timeout'), { code: 'TIMEOUT' }));
84
+ }, containerTimeoutMs + 50);
85
+ });
86
+ let result;
87
+ try {
88
+ result = await Promise.race([
89
+ Promise.all([consumed, wait]).then(([data, status]) => ({
90
+ data,
91
+ status,
92
+ })),
93
+ hang,
94
+ ]);
95
+ }
96
+ finally {
97
+ if (hangTimer !== undefined) {
98
+ clearTimeout(hangTimer);
99
+ }
100
+ }
101
+ if (timedOut) {
102
+ return {
103
+ ok: false,
104
+ code: 'TIMEOUT',
105
+ error: `SQL execution timed out after ${request.timeoutMs}ms (container kill + statement_timeout)`,
106
+ };
107
+ }
108
+ if (result.data.error) {
109
+ return { ok: false, code: 'EXECUTION_FAILED', error: result.data.error };
110
+ }
111
+ if (result.status.StatusCode !== 0 && !result.data.truncated && !killedForLimit) {
112
+ return {
113
+ ok: false,
114
+ code: 'EXECUTION_FAILED',
115
+ error: result.data.error ?? 'Sandbox runner exited with a non-zero status',
116
+ };
117
+ }
118
+ const data = {
119
+ columns: result.data.columns,
120
+ rows: result.data.rows,
121
+ };
122
+ if (result.data.truncated) {
123
+ data.truncated = true;
124
+ }
125
+ return { ok: true, data };
126
+ }
127
+ catch (err) {
128
+ if (timedOut) {
129
+ return {
130
+ ok: false,
131
+ code: 'TIMEOUT',
132
+ error: `SQL execution timed out after ${request.timeoutMs}ms (container kill + statement_timeout)`,
133
+ };
134
+ }
135
+ if (isImageUnavailableError(err)) {
136
+ return {
137
+ ok: false,
138
+ code: 'IMAGE_UNAVAILABLE',
139
+ error: imageUnavailableMessage(request),
140
+ };
141
+ }
142
+ return {
143
+ ok: false,
144
+ code: 'EXECUTION_FAILED',
145
+ error: err instanceof Error ? err.message : String(err),
146
+ };
147
+ }
148
+ finally {
149
+ if (killTimer !== undefined) {
150
+ clearTimeout(killTimer);
151
+ }
152
+ destroyStream(attachStream);
153
+ if (container) {
154
+ try {
155
+ await container.kill();
156
+ }
157
+ catch {
158
+ // already exited
159
+ }
160
+ try {
161
+ await container.remove({ force: true });
162
+ }
163
+ catch {
164
+ // already removed
165
+ }
166
+ }
167
+ }
168
+ }
169
+ function destroyStream(stream) {
170
+ if (!stream) {
171
+ return;
172
+ }
173
+ if ('destroy' in stream && typeof stream.destroy === 'function') {
174
+ stream.destroy();
175
+ }
176
+ }
177
+ function isImageUnavailableError(err) {
178
+ const status = typeof err === 'object' && err !== null && 'statusCode' in err
179
+ ? Number(err.statusCode)
180
+ : undefined;
181
+ const msg = err instanceof Error ? err.message : String(err);
182
+ return (status === 404 ||
183
+ /no such image/i.test(msg) ||
184
+ /\(HTTP code 404\).*image/i.test(msg));
185
+ }
186
+ function imageUnavailableMessage(request) {
187
+ if (request.usingDefaultImage) {
188
+ return `Default sandbox image is not pullable (${request.image}). Until GHCR publish, build sandbox/Dockerfile locally and pass image: '<tag-or-id>' (untrusted override), or docker load/tag the digest-pinned image.`;
189
+ }
190
+ return `Sandbox image not found: ${request.image}. Build or pull it before calling executeSql.`;
191
+ }
192
+ //# sourceMappingURL=docker-executor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docker-executor.js","sourceRoot":"","sources":["../src/docker-executor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import { PassThrough, type Readable } from 'node:stream';
2
+ export type ContainerLike = {
3
+ start: () => Promise<unknown>;
4
+ putArchive: (tar: Buffer | NodeJS.ReadableStream, opts: {
5
+ path: string;
6
+ }) => Promise<unknown>;
7
+ attach: (opts: unknown) => Promise<NodeJS.ReadWriteStream | Readable>;
8
+ wait: () => Promise<{
9
+ StatusCode: number;
10
+ }>;
11
+ kill: () => Promise<unknown>;
12
+ remove: (opts?: unknown) => Promise<unknown>;
13
+ };
14
+ export type DockerLike = {
15
+ ping: () => Promise<unknown>;
16
+ createContainer: (opts: Record<string, unknown>) => Promise<ContainerLike>;
17
+ };
18
+ export type DockerRunRequest = {
19
+ image: string;
20
+ usingDefaultImage: boolean;
21
+ env: string[];
22
+ password: string;
23
+ stdinJson: string;
24
+ memoryBytes: number;
25
+ nanoCpus: number;
26
+ timeoutMs: number;
27
+ maxRows: number;
28
+ maxBytes: number;
29
+ };
30
+ /**
31
+ * Incrementally demux Docker's 8-byte multiplexed attach stream.
32
+ * Tty:false attach is always multiplexed — buffer until a full 8-byte header.
33
+ * Type-1 (stdout) and type-2 (stderr) payloads are forwarded to the NDJSON
34
+ * consumer so runner `{type:"error"}` events are not dropped.
35
+ */
36
+ export declare function demuxStdout(stream: AsyncIterable<Buffer | string>): PassThrough;
37
+ //# sourceMappingURL=docker-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docker-types.d.ts","sourceRoot":"","sources":["../src/docker-types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9B,UAAU,EAAE,CACV,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,cAAc,EACnC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KACnB,OAAO,CAAC,OAAO,CAAC,CAAC;IACtB,MAAM,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,eAAe,GAAG,QAAQ,CAAC,CAAC;IACtE,IAAI,EAAE,MAAM,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5C,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9C,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;CAC5E,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,EAAE,OAAO,CAAC;IAC3B,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,WAAW,CAsB/E"}
@@ -0,0 +1,50 @@
1
+ import { PassThrough } from 'node:stream';
2
+ /**
3
+ * Incrementally demux Docker's 8-byte multiplexed attach stream.
4
+ * Tty:false attach is always multiplexed — buffer until a full 8-byte header.
5
+ * Type-1 (stdout) and type-2 (stderr) payloads are forwarded to the NDJSON
6
+ * consumer so runner `{type:"error"}` events are not dropped.
7
+ */
8
+ export function demuxStdout(stream) {
9
+ const stdout = new PassThrough();
10
+ let carry = Buffer.alloc(0);
11
+ void (async () => {
12
+ try {
13
+ for await (const chunk of stream) {
14
+ const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
15
+ const joined = Buffer.concat([carry, incoming]);
16
+ const extracted = extractMuxed(joined);
17
+ carry = Buffer.from(extracted.rest);
18
+ if (extracted.stdout.length > 0) {
19
+ stdout.write(extracted.stdout);
20
+ }
21
+ }
22
+ stdout.end();
23
+ }
24
+ catch (err) {
25
+ stdout.destroy(err instanceof Error ? err : new Error(String(err)));
26
+ }
27
+ })();
28
+ return stdout;
29
+ }
30
+ function extractMuxed(buffer) {
31
+ const stdoutChunks = [];
32
+ let offset = 0;
33
+ while (offset + 8 <= buffer.length) {
34
+ const size = buffer.readUInt32BE(offset + 4);
35
+ if (offset + 8 + size > buffer.length) {
36
+ break;
37
+ }
38
+ const streamType = buffer[offset];
39
+ const payload = buffer.subarray(offset + 8, offset + 8 + size);
40
+ if (streamType === 1 || streamType === 2) {
41
+ stdoutChunks.push(payload);
42
+ }
43
+ offset += 8 + size;
44
+ }
45
+ return {
46
+ stdout: Buffer.concat(stdoutChunks),
47
+ rest: buffer.subarray(offset),
48
+ };
49
+ }
50
+ //# sourceMappingURL=docker-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docker-types.js","sourceRoot":"","sources":["../src/docker-types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAiB,MAAM,aAAa,CAAC;AAgCzD;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,MAAsC;IAChE,MAAM,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;IACjC,IAAI,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAE5B,KAAK,CAAC,KAAK,IAAI,EAAE;QACf,IAAI,CAAC;YACH,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBACjC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACrE,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;gBAChD,MAAM,SAAS,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;gBACvC,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;gBACpC,IAAI,SAAS,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAChC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;gBACjC,CAAC;YACH,CAAC;YACD,MAAM,CAAC,GAAG,EAAE,CAAC;QACf,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,OAAO,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACtE,CAAC;IACH,CAAC,CAAC,EAAE,CAAC;IAEL,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,YAAY,CAAC,MAAc;IAClC,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,OAAO,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC7C,IAAI,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;YACtC,MAAM;QACR,CAAC;QACD,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;QAClC,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;QAC/D,IAAI,UAAU,KAAK,CAAC,IAAI,UAAU,KAAK,CAAC,EAAE,CAAC;YACzC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC7B,CAAC;QACD,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;IACrB,CAAC;IACD,OAAO;QACL,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC;QACnC,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;KAC9B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { DockerLike } from './docker-types.js';
2
+ import type { ExecuteSqlInput, ExecuteSqlResult } from './types.js';
3
+ export type { ExecuteSqlInput, ExecuteSqlResult } from './types.js';
4
+ export type { DockerLike } from './docker-types.js';
5
+ export type ExecuteSqlDeps = {
6
+ docker?: DockerLike;
7
+ };
8
+ export declare function executeSql(input: ExecuteSqlInput, deps?: ExecuteSqlDeps): Promise<ExecuteSqlResult>;
9
+ //# sourceMappingURL=execute-sql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execute-sql.d.ts","sourceRoot":"","sources":["../src/execute-sql.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAEjB,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACpE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB,CAAC;AAEF,wBAAsB,UAAU,CAC9B,KAAK,EAAE,eAAe,EACtB,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,gBAAgB,CAAC,CAwE3B"}
@@ -0,0 +1,89 @@
1
+ import { validateSql } from './validate-sql.js';
2
+ import { hostIsAllowlisted } from './host-allowlist.js';
3
+ import { mergeLimits } from './limits.js';
4
+ import { DEFAULT_SANDBOX_IMAGE, isDigestPinnedImage } from './image.js';
5
+ import { rewriteLoopbackHost } from './rewrite-host.js';
6
+ import { createDefaultDocker, runInSandbox, } from './docker-executor.js';
7
+ export async function executeSql(input, deps = {}) {
8
+ const validation = validateSql(input.sql);
9
+ if (!validation.ok) {
10
+ return {
11
+ ok: false,
12
+ code: validation.code,
13
+ error: validation.reason,
14
+ };
15
+ }
16
+ if (!hostIsAllowlisted(input.connection.host, input.hostAllowlist)) {
17
+ return {
18
+ ok: false,
19
+ code: 'HOST_NOT_ALLOWED',
20
+ error: `connection.host ${JSON.stringify(input.connection.host)} is not on hostAllowlist`,
21
+ };
22
+ }
23
+ const dialect = parseDialect(input.connection.type);
24
+ if (!dialect.ok) {
25
+ return dialect;
26
+ }
27
+ const merged = mergeLimits(input.limits);
28
+ if (!merged.ok) {
29
+ return { ok: false, code: 'INVALID_LIMITS', error: merged.error };
30
+ }
31
+ const image = input.image ?? DEFAULT_SANDBOX_IMAGE;
32
+ if (input.image === undefined && !isDigestPinnedImage(image)) {
33
+ return {
34
+ ok: false,
35
+ code: 'IMAGE_UNPINNED',
36
+ error: 'Default sandbox image must be pinned by sha256 digest',
37
+ };
38
+ }
39
+ const docker = deps.docker ?? createDefaultDocker();
40
+ try {
41
+ await docker.ping();
42
+ }
43
+ catch (err) {
44
+ return {
45
+ ok: false,
46
+ code: 'DOCKER_UNAVAILABLE',
47
+ error: `Docker is required to execute SQL. Could not ping the Docker engine: ${err instanceof Error ? err.message : String(err)}`,
48
+ };
49
+ }
50
+ const dbHost = rewriteLoopbackHost(input.connection.host);
51
+ const env = [
52
+ `DB_TYPE=${dialect.type}`,
53
+ `DB_HOST=${dbHost}`,
54
+ `DB_PORT=${input.connection.port}`,
55
+ `DB_USER=${input.connection.user}`,
56
+ `DB_NAME=${input.connection.database}`,
57
+ `QUERY_TIMEOUT=${merged.limits.timeoutMs}`,
58
+ `MAX_ROWS=${merged.limits.maxRows}`,
59
+ `MAX_BYTES=${merged.limits.maxBytes}`,
60
+ ];
61
+ return runInSandbox(docker, {
62
+ image,
63
+ usingDefaultImage: input.image === undefined,
64
+ env,
65
+ password: input.connection.password,
66
+ stdinJson: JSON.stringify({ sql: input.sql }) + '\n',
67
+ memoryBytes: merged.limits.memoryMb * 1024 * 1024,
68
+ nanoCpus: merged.limits.nanoCpus,
69
+ timeoutMs: merged.limits.timeoutMs,
70
+ maxRows: merged.limits.maxRows,
71
+ maxBytes: merged.limits.maxBytes,
72
+ });
73
+ }
74
+ function parseDialect(type) {
75
+ switch (type) {
76
+ case 'postgres':
77
+ case 'mysql':
78
+ return { ok: true, type };
79
+ default: {
80
+ const unexpected = type;
81
+ return {
82
+ ok: false,
83
+ code: 'UNSUPPORTED_DIALECT',
84
+ error: `Unsupported dialect: ${String(unexpected)}`,
85
+ };
86
+ }
87
+ }
88
+ }
89
+ //# sourceMappingURL=execute-sql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execute-sql.js","sourceRoot":"","sources":["../src/execute-sql.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function hostIsAllowlisted(host: string, hostAllowlist: string[]): boolean;
2
+ //# sourceMappingURL=host-allowlist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-allowlist.d.ts","sourceRoot":"","sources":["../src/host-allowlist.ts"],"names":[],"mappings":"AAAA,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,MAAM,EAAE,GACtB,OAAO,CAST"}
@@ -0,0 +1,9 @@
1
+ export function hostIsAllowlisted(host, hostAllowlist) {
2
+ const normalized = host.trim().toLowerCase();
3
+ if (normalized.length === 0) {
4
+ return false;
5
+ }
6
+ const allowed = new Set(hostAllowlist.map((entry) => entry.trim().toLowerCase()).filter(Boolean));
7
+ return allowed.has(normalized);
8
+ }
9
+ //# sourceMappingURL=host-allowlist.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-allowlist.js","sourceRoot":"","sources":["../src/host-allowlist.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,iBAAiB,CAC/B,IAAY,EACZ,aAAuB;IAEvB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC7C,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,GAAG,CACrB,aAAa,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CACzE,CAAC;IACF,OAAO,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;AACjC,CAAC"}