@vellumai/vellum-gateway 0.8.11 → 0.8.12-staging.2

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 (28) hide show
  1. package/Dockerfile +2 -0
  2. package/bun.lock +9 -0
  3. package/knip.json +1 -0
  4. package/node_modules/@vellumai/gateway-client/bun.lock +40 -0
  5. package/node_modules/@vellumai/gateway-client/package.json +25 -0
  6. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +343 -0
  7. package/node_modules/@vellumai/gateway-client/src/__tests__/package-boundary.test.ts +140 -0
  8. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
  9. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +422 -0
  10. package/node_modules/@vellumai/gateway-client/src/index.ts +34 -0
  11. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +378 -0
  12. package/node_modules/@vellumai/gateway-client/src/types.ts +144 -0
  13. package/node_modules/@vellumai/gateway-client/tsconfig.json +20 -0
  14. package/package.json +3 -1
  15. package/src/feature-flag-registry.json +8 -16
  16. package/src/http/routes/log-tail.ts +80 -50
  17. package/src/http/routes/trust-rules.ts +24 -10
  18. package/src/index.ts +5 -3
  19. package/src/ipc/assistant-client.ts +2 -3
  20. package/src/ipc/log-tail-handlers.test.ts +78 -0
  21. package/src/ipc/log-tail-handlers.ts +23 -0
  22. package/src/ipc/trust-rules-handlers.test.ts +72 -0
  23. package/src/ipc/trust-rules-handlers.ts +24 -0
  24. package/src/risk/bash-risk-classifier.test.ts +3 -20
  25. package/src/risk/bash-risk-classifier.ts +1 -1
  26. package/src/risk/command-registry/commands/assistant.ts +7 -0
  27. package/src/risk/command-registry.test.ts +5 -0
  28. package/src/risk/risk-classifier-parity.test.ts +0 -1
package/Dockerfile CHANGED
@@ -11,6 +11,7 @@ COPY --from=bun /usr/local/bin/bun /usr/local/bin/bun
11
11
  # Copy shared packages needed by gateway's repo-local dependencies
12
12
  COPY packages/assistant-client ./packages/assistant-client
13
13
  COPY packages/ces-client ./packages/ces-client
14
+ COPY packages/gateway-client ./packages/gateway-client
14
15
  COPY packages/ipc-server-utils ./packages/ipc-server-utils
15
16
  COPY packages/service-contracts ./packages/service-contracts
16
17
  COPY packages/slack-text ./packages/slack-text
@@ -18,6 +19,7 @@ COPY packages/twilio-client ./packages/twilio-client
18
19
 
19
20
  # Install deps for shared packages whose source is loaded at runtime.
20
21
  RUN cd /app/packages/ces-client && bun install --frozen-lockfile
22
+ RUN cd /app/packages/gateway-client && bun install --frozen-lockfile
21
23
  RUN cd /app/packages/service-contracts && bun install --frozen-lockfile
22
24
 
23
25
  # Install gateway dependencies first for cache reuse
package/bun.lock CHANGED
@@ -7,6 +7,7 @@
7
7
  "dependencies": {
8
8
  "@vellumai/assistant-client": "file:../packages/assistant-client",
9
9
  "@vellumai/ces-client": "file:../packages/ces-client",
10
+ "@vellumai/gateway-client": "file:../packages/gateway-client",
10
11
  "@vellumai/ipc-server-utils": "file:../packages/ipc-server-utils",
11
12
  "@vellumai/service-contracts": "file:../packages/service-contracts",
12
13
  "@vellumai/slack-text": "file:../packages/slack-text",
@@ -210,6 +211,8 @@
210
211
 
211
212
  "@vellumai/ces-client": ["@vellumai/ces-client@file:../packages/ces-client", { "dependencies": { "@vellumai/service-contracts": "file:../service-contracts" }, "devDependencies": { "@types/bun": "1.2.4", "typescript": "5.7.3" } }],
212
213
 
214
+ "@vellumai/gateway-client": ["@vellumai/gateway-client@file:../packages/gateway-client", { "dependencies": { "@vellumai/service-contracts": "file:../service-contracts", "zod": "4.3.6" }, "devDependencies": { "@types/bun": "1.3.10", "typescript": "5.9.3" } }],
215
+
213
216
  "@vellumai/ipc-server-utils": ["@vellumai/ipc-server-utils@file:../packages/ipc-server-utils", { "devDependencies": { "@types/bun": "1.3.10", "typescript": "5.9.3" } }],
214
217
 
215
218
  "@vellumai/service-contracts": ["@vellumai/service-contracts@file:../packages/service-contracts", { "dependencies": { "zod": "4.3.6" }, "devDependencies": { "@types/bun": "1.2.4", "typescript": "5.7.3" } }],
@@ -504,6 +507,10 @@
504
507
 
505
508
  "@vellumai/ces-client/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
506
509
 
510
+ "@vellumai/gateway-client/@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
511
+
512
+ "@vellumai/gateway-client/@vellumai/service-contracts": ["@vellumai/service-contracts@file:../packages/service-contracts", {}],
513
+
507
514
  "@vellumai/ipc-server-utils/@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
508
515
 
509
516
  "@vellumai/service-contracts/@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="],
@@ -572,6 +579,8 @@
572
579
 
573
580
  "@vellumai/ces-client/@types/bun/bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="],
574
581
 
582
+ "@vellumai/gateway-client/@types/bun/bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
583
+
575
584
  "@vellumai/ipc-server-utils/@types/bun/bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
576
585
 
577
586
  "@vellumai/service-contracts/@types/bun/bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="],
package/knip.json CHANGED
@@ -4,6 +4,7 @@
4
4
  "ignoreDependencies": [
5
5
  "@vellumai/assistant-client",
6
6
  "@vellumai/ces-client",
7
+ "@vellumai/gateway-client",
7
8
  "@vellumai/ipc-server-utils",
8
9
  "@vellumai/service-contracts",
9
10
  "@vellumai/slack-text",
@@ -0,0 +1,40 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "@vellumai/gateway-client",
7
+ "dependencies": {
8
+ "@vellumai/service-contracts": "file:../service-contracts",
9
+ "zod": "4.3.6",
10
+ },
11
+ "devDependencies": {
12
+ "@types/bun": "1.3.10",
13
+ "typescript": "5.9.3",
14
+ },
15
+ },
16
+ },
17
+ "packages": {
18
+ "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
19
+
20
+ "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
21
+
22
+ "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
23
+
24
+ "@vellumai/service-contracts": ["@vellumai/service-contracts@file:../service-contracts", { "dependencies": { "zod": "4.3.6" }, "devDependencies": { "@types/bun": "1.2.4", "typescript": "5.7.3" } }],
25
+
26
+ "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
27
+
28
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
29
+
30
+ "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
31
+
32
+ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
33
+
34
+ "@vellumai/service-contracts/@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="],
35
+
36
+ "@vellumai/service-contracts/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
37
+
38
+ "@vellumai/service-contracts/@types/bun/bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="],
39
+ }
40
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@vellumai/gateway-client",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./gateway-ipc-contracts": "./src/gateway-ipc-contracts.ts",
10
+ "./http-delivery": "./src/http-delivery.ts",
11
+ "./ipc-client": "./src/ipc-client.ts"
12
+ },
13
+ "scripts": {
14
+ "typecheck": "bunx tsc --noEmit",
15
+ "test": "bun test src/"
16
+ },
17
+ "dependencies": {
18
+ "@vellumai/service-contracts": "file:../service-contracts",
19
+ "zod": "4.3.6"
20
+ },
21
+ "devDependencies": {
22
+ "@types/bun": "1.3.10",
23
+ "typescript": "5.9.3"
24
+ }
25
+ }
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Tests for @vellumai/gateway-client
3
+ *
4
+ * Covers:
5
+ * 1. Package independence — no imports from assistant/ or gateway/.
6
+ * 2. IPC NDJSON framing and timeout behavior.
7
+ * 3. HTTP delivery auth headers and error handling.
8
+ */
9
+
10
+ import { createServer, type Server } from "node:net";
11
+ import { join } from "node:path";
12
+ import { tmpdir } from "node:os";
13
+ import { randomUUID } from "node:crypto";
14
+ import { unlinkSync } from "node:fs";
15
+
16
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
17
+
18
+ import { ipcCall, PersistentIpcClient } from "../ipc-client.js";
19
+ import { ChannelDeliveryError, deliverChannelReply } from "../http-delivery.js";
20
+ import type { Logger } from "../types.js";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Independence guard — package must not pull in assistant or gateway modules.
24
+ // ---------------------------------------------------------------------------
25
+
26
+ describe("package independence", () => {
27
+ const sourceFiles = [
28
+ "../index.ts",
29
+ "../types.ts",
30
+ "../http-delivery.ts",
31
+ "../ipc-client.ts",
32
+ "../gateway-ipc-contracts.ts",
33
+ ];
34
+
35
+ for (const file of sourceFiles) {
36
+ test(`${file} does not import from assistant/ or gateway/`, () => {
37
+ const src = require("node:fs").readFileSync(
38
+ require("node:path").resolve(__dirname, file),
39
+ "utf-8",
40
+ );
41
+ expect(src).not.toMatch(/from\s+['"].*assistant\//);
42
+ expect(src).not.toMatch(/from\s+['"].*gateway\//);
43
+ expect(src).not.toMatch(/require\(['"].*assistant\//);
44
+ expect(src).not.toMatch(/require\(['"].*gateway\//);
45
+ });
46
+ }
47
+ });
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Test helpers
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /** Create a no-op logger that collects messages for assertions. */
54
+ function createTestLogger(): Logger & {
55
+ messages: Array<{ level: string; msg: string }>;
56
+ } {
57
+ const messages: Array<{ level: string; msg: string }> = [];
58
+ return {
59
+ messages,
60
+ debug(_obj: Record<string, unknown>, msg: string) {
61
+ messages.push({ level: "debug", msg });
62
+ },
63
+ info(_obj: Record<string, unknown>, msg: string) {
64
+ messages.push({ level: "info", msg });
65
+ },
66
+ warn(_obj: Record<string, unknown>, msg: string) {
67
+ messages.push({ level: "warn", msg });
68
+ },
69
+ error(_obj: Record<string, unknown>, msg: string) {
70
+ messages.push({ level: "error", msg });
71
+ },
72
+ };
73
+ }
74
+
75
+ /** Create a temporary Unix socket path for tests. */
76
+ function tmpSocketPath(): string {
77
+ return join(tmpdir(), `gw-client-test-${randomUUID()}.sock`);
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // IPC: NDJSON framing
82
+ // ---------------------------------------------------------------------------
83
+
84
+ describe("ipc-client", () => {
85
+ describe("ipcCall — one-shot", () => {
86
+ let server: Server;
87
+ let socketPath: string;
88
+
89
+ beforeEach(() => {
90
+ socketPath = tmpSocketPath();
91
+ });
92
+
93
+ afterEach(() => {
94
+ server?.close();
95
+ try {
96
+ unlinkSync(socketPath);
97
+ } catch {
98
+ // Already cleaned up
99
+ }
100
+ });
101
+
102
+ test("sends NDJSON request and parses NDJSON response", async () => {
103
+ server = createServer((conn) => {
104
+ let buf = "";
105
+ conn.on("data", (chunk) => {
106
+ buf += chunk.toString();
107
+ const idx = buf.indexOf("\n");
108
+ if (idx !== -1) {
109
+ const line = buf.slice(0, idx);
110
+ const req = JSON.parse(line);
111
+ const resp = JSON.stringify({
112
+ id: req.id,
113
+ result: { flags: { browser: true } },
114
+ });
115
+ conn.write(resp + "\n");
116
+ }
117
+ });
118
+ });
119
+
120
+ await new Promise<void>((resolve) => {
121
+ server.listen(socketPath, () => resolve());
122
+ });
123
+
124
+ const result = await ipcCall(socketPath, "get_feature_flags");
125
+ expect(result).toEqual({ flags: { browser: true } });
126
+ });
127
+
128
+ test("returns undefined when server sends error response", async () => {
129
+ const log = createTestLogger();
130
+ server = createServer((conn) => {
131
+ let buf = "";
132
+ conn.on("data", (chunk) => {
133
+ buf += chunk.toString();
134
+ const idx = buf.indexOf("\n");
135
+ if (idx !== -1) {
136
+ const req = JSON.parse(buf.slice(0, idx));
137
+ conn.write(
138
+ JSON.stringify({ id: req.id, error: "method not found" }) + "\n",
139
+ );
140
+ }
141
+ });
142
+ });
143
+
144
+ await new Promise<void>((resolve) => {
145
+ server.listen(socketPath, () => resolve());
146
+ });
147
+
148
+ const result = await ipcCall(
149
+ socketPath,
150
+ "unknown_method",
151
+ undefined,
152
+ log,
153
+ );
154
+ expect(result).toBeUndefined();
155
+ expect(
156
+ log.messages.some((m) => m.msg === "IPC call returned error"),
157
+ ).toBe(true);
158
+ });
159
+
160
+ test("returns undefined when socket does not exist", async () => {
161
+ const log = createTestLogger();
162
+ const result = await ipcCall(
163
+ "/tmp/nonexistent-socket.sock",
164
+ "test_method",
165
+ undefined,
166
+ log,
167
+ );
168
+ expect(result).toBeUndefined();
169
+ });
170
+
171
+ test("forwards params in the request", async () => {
172
+ let receivedParams: Record<string, unknown> | undefined;
173
+ server = createServer((conn) => {
174
+ let buf = "";
175
+ conn.on("data", (chunk) => {
176
+ buf += chunk.toString();
177
+ const idx = buf.indexOf("\n");
178
+ if (idx !== -1) {
179
+ const req = JSON.parse(buf.slice(0, idx));
180
+ receivedParams = req.params;
181
+ conn.write(JSON.stringify({ id: req.id, result: "ok" }) + "\n");
182
+ }
183
+ });
184
+ });
185
+
186
+ await new Promise<void>((resolve) => {
187
+ server.listen(socketPath, () => resolve());
188
+ });
189
+
190
+ await ipcCall(socketPath, "test", { key: "value" });
191
+ expect(receivedParams).toEqual({ key: "value" });
192
+ });
193
+
194
+ test("handles fragmented NDJSON across multiple data chunks", async () => {
195
+ server = createServer((conn) => {
196
+ let buf = "";
197
+ conn.on("data", (chunk) => {
198
+ buf += chunk.toString();
199
+ const idx = buf.indexOf("\n");
200
+ if (idx !== -1) {
201
+ const req = JSON.parse(buf.slice(0, idx));
202
+ const resp = JSON.stringify({ id: req.id, result: 42 });
203
+ // Send the response in two separate chunks
204
+ const mid = Math.floor(resp.length / 2);
205
+ conn.write(resp.slice(0, mid));
206
+ setTimeout(() => {
207
+ conn.write(resp.slice(mid) + "\n");
208
+ }, 10);
209
+ }
210
+ });
211
+ });
212
+
213
+ await new Promise<void>((resolve) => {
214
+ server.listen(socketPath, () => resolve());
215
+ });
216
+
217
+ const result = await ipcCall(socketPath, "fragmented");
218
+ expect(result).toBe(42);
219
+ });
220
+ });
221
+
222
+ describe("PersistentIpcClient", () => {
223
+ let server: Server;
224
+ let socketPath: string;
225
+
226
+ beforeEach(() => {
227
+ socketPath = tmpSocketPath();
228
+ });
229
+
230
+ afterEach(() => {
231
+ server?.close();
232
+ try {
233
+ unlinkSync(socketPath);
234
+ } catch {
235
+ // Already cleaned up
236
+ }
237
+ });
238
+
239
+ test("multiplexes concurrent calls over a single connection", async () => {
240
+ server = createServer((conn) => {
241
+ let buf = "";
242
+ conn.on("data", (chunk) => {
243
+ buf += chunk.toString();
244
+ let idx: number;
245
+ while ((idx = buf.indexOf("\n")) !== -1) {
246
+ const line = buf.slice(0, idx);
247
+ buf = buf.slice(idx + 1);
248
+ const req = JSON.parse(line);
249
+ // Echo the method as the result
250
+ conn.write(
251
+ JSON.stringify({ id: req.id, result: req.method }) + "\n",
252
+ );
253
+ }
254
+ });
255
+ });
256
+
257
+ await new Promise<void>((resolve) => {
258
+ server.listen(socketPath, () => resolve());
259
+ });
260
+
261
+ const client = new PersistentIpcClient(socketPath);
262
+ try {
263
+ const [r1, r2, r3] = await Promise.all([
264
+ client.call("method_a"),
265
+ client.call("method_b"),
266
+ client.call("method_c"),
267
+ ]);
268
+ expect(r1).toBe("method_a");
269
+ expect(r2).toBe("method_b");
270
+ expect(r3).toBe("method_c");
271
+ } finally {
272
+ client.destroy();
273
+ }
274
+ });
275
+
276
+ test("rejects pending calls on destroy", async () => {
277
+ server = createServer(() => {
278
+ // Server accepts but never responds
279
+ });
280
+
281
+ await new Promise<void>((resolve) => {
282
+ server.listen(socketPath, () => resolve());
283
+ });
284
+
285
+ const client = new PersistentIpcClient(socketPath, 30_000);
286
+ const callPromise = client.call("hanging_method");
287
+ // Give the connection time to establish
288
+ await new Promise((r) => setTimeout(r, 50));
289
+ client.destroy();
290
+
291
+ await expect(callPromise).rejects.toThrow(
292
+ "PersistentIpcClient destroyed",
293
+ );
294
+ });
295
+
296
+ test("rejects on server error response", async () => {
297
+ server = createServer((conn) => {
298
+ let buf = "";
299
+ conn.on("data", (chunk) => {
300
+ buf += chunk.toString();
301
+ const idx = buf.indexOf("\n");
302
+ if (idx !== -1) {
303
+ const req = JSON.parse(buf.slice(0, idx));
304
+ conn.write(
305
+ JSON.stringify({ id: req.id, error: "something broke" }) + "\n",
306
+ );
307
+ }
308
+ });
309
+ });
310
+
311
+ await new Promise<void>((resolve) => {
312
+ server.listen(socketPath, () => resolve());
313
+ });
314
+
315
+ const client = new PersistentIpcClient(socketPath);
316
+ try {
317
+ await expect(client.call("broken")).rejects.toThrow("something broke");
318
+ } finally {
319
+ client.destroy();
320
+ }
321
+ });
322
+
323
+ test("times out when server does not respond", async () => {
324
+ server = createServer(() => {
325
+ // Accepts connections but never responds
326
+ });
327
+
328
+ await new Promise<void>((resolve) => {
329
+ server.listen(socketPath, () => resolve());
330
+ });
331
+
332
+ const client = new PersistentIpcClient(socketPath, 100);
333
+ try {
334
+ await expect(client.call("slow_method")).rejects.toThrow("timed out");
335
+ } finally {
336
+ client.destroy();
337
+ }
338
+ });
339
+ });
340
+ });
341
+
342
+ // ---------------------------------------------------------------------------
343
+ // HTTP delivery: auth headers and error handling
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Package boundary tests for @vellumai/gateway-client.
3
+ *
4
+ * Ensures the package:
5
+ * 1. Does NOT import from assistant, gateway, or credential-executor service
6
+ * runtime modules.
7
+ * 2. Does NOT import from runtime shared packages (@vellumai/credential-storage,
8
+ * @vellumai/egress-proxy).
9
+ * 3. Remains a lightweight client package with no runtime service dependencies.
10
+ *
11
+ * @vellumai/gateway-client is a typed HTTP client for assistant-to-gateway
12
+ * calls (trust API, feature flags, log export, deliver). It may depend on
13
+ * @vellumai/service-contracts for shared type definitions, but must not pull
14
+ * in runtime service internals.
15
+ */
16
+
17
+ import { describe, expect, test } from "bun:test";
18
+ import { readFileSync, readdirSync, statSync } from "node:fs";
19
+ import { join, resolve } from "node:path";
20
+
21
+ const PACKAGE_ROOT = resolve(import.meta.dirname, "../..");
22
+ const SRC_DIR = join(PACKAGE_ROOT, "src");
23
+
24
+ /**
25
+ * Recursively collect all .ts source files, excluding test and declaration files.
26
+ */
27
+ function collectSourceFiles(dir: string): string[] {
28
+ const files: string[] = [];
29
+ for (const entry of readdirSync(dir)) {
30
+ const full = join(dir, entry);
31
+ const stat = statSync(full);
32
+ if (stat.isDirectory()) {
33
+ if (entry === "node_modules" || entry === "__tests__") continue;
34
+ files.push(...collectSourceFiles(full));
35
+ } else if (
36
+ entry.endsWith(".ts") &&
37
+ !entry.endsWith(".test.ts") &&
38
+ !entry.endsWith(".d.ts")
39
+ ) {
40
+ files.push(full);
41
+ }
42
+ }
43
+ return files;
44
+ }
45
+
46
+ /**
47
+ * Patterns that must NOT appear in any import/require statement within
48
+ * source files.
49
+ */
50
+ const FORBIDDEN_IMPORT_PATTERNS = [
51
+ // Assistant runtime internals
52
+ /from\s+["'](?:\.\.\/)+assistant\/src/,
53
+ /require\s*\(\s*["'](?:\.\.\/)+assistant\/src/,
54
+ /from\s+["']@vellumai\/assistant(?:\/|["'])/,
55
+ /require\s*\(\s*["']@vellumai\/assistant(?:\/|["'])/,
56
+
57
+ // Gateway runtime internals (not the gateway-client package itself)
58
+ /from\s+["'](?:\.\.\/)+gateway\/src/,
59
+ /require\s*\(\s*["'](?:\.\.\/)+gateway\/src/,
60
+ /from\s+["']@vellumai\/(?:vellum-)?gateway(?:\/|["'])/,
61
+ /require\s*\(\s*["']@vellumai\/(?:vellum-)?gateway(?:\/|["'])/,
62
+
63
+ // Credential executor runtime internals
64
+ /from\s+["'](?:\.\.\/)+credential-executor\/src/,
65
+ /require\s*\(\s*["'](?:\.\.\/)+credential-executor\/src/,
66
+ /from\s+["']@vellumai\/credential-executor(?:\/|["'])/,
67
+ /require\s*\(\s*["']@vellumai\/credential-executor(?:\/|["'])/,
68
+
69
+ // Runtime shared packages
70
+ /from\s+["']@vellumai\/credential-storage(?:\/|["'])/,
71
+ /require\s*\(\s*["']@vellumai\/credential-storage(?:\/|["'])/,
72
+ /from\s+["']@vellumai\/egress-proxy(?:\/|["'])/,
73
+ /require\s*\(\s*["']@vellumai\/egress-proxy(?:\/|["'])/,
74
+ ];
75
+
76
+ describe("package boundary", () => {
77
+ const sourceFiles = collectSourceFiles(SRC_DIR);
78
+
79
+ test("has source files to validate", () => {
80
+ expect(sourceFiles.length).toBeGreaterThan(0);
81
+ });
82
+
83
+ test("does not import from service runtime modules or runtime shared packages", () => {
84
+ const violations: string[] = [];
85
+
86
+ for (const file of sourceFiles) {
87
+ const content = readFileSync(file, "utf-8");
88
+ const lines = content.split("\n");
89
+
90
+ for (let i = 0; i < lines.length; i++) {
91
+ const line = lines[i];
92
+ for (const pattern of FORBIDDEN_IMPORT_PATTERNS) {
93
+ if (pattern.test(line)) {
94
+ const relative = file.replace(PACKAGE_ROOT + "/", "");
95
+ violations.push(`${relative}:${i + 1}: ${line.trim()}`);
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ if (violations.length > 0) {
102
+ throw new Error(
103
+ `Found ${violations.length} forbidden import(s) in gateway-client package:\n` +
104
+ violations.map((v) => ` - ${v}`).join("\n") +
105
+ "\n\n" +
106
+ "@vellumai/gateway-client must not import from service runtime modules\n" +
107
+ "or runtime shared packages (credential-storage, egress-proxy).",
108
+ );
109
+ }
110
+ });
111
+
112
+ test("package.json declares it as private", () => {
113
+ const pkg = JSON.parse(
114
+ readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8"),
115
+ );
116
+ expect(pkg.private).toBe(true);
117
+ });
118
+
119
+ test("package.json does not depend on service runtime or runtime shared packages", () => {
120
+ const pkg = JSON.parse(
121
+ readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8"),
122
+ );
123
+ const allDeps = {
124
+ ...pkg.dependencies,
125
+ ...pkg.devDependencies,
126
+ ...pkg.peerDependencies,
127
+ };
128
+
129
+ const forbidden = Object.keys(allDeps).filter((dep) =>
130
+ [
131
+ "@vellumai/assistant",
132
+ "@vellumai/vellum-gateway",
133
+ "@vellumai/credential-storage",
134
+ "@vellumai/egress-proxy",
135
+ ].includes(dep),
136
+ );
137
+
138
+ expect(forbidden).toEqual([]);
139
+ });
140
+ });