@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.
- package/Dockerfile +2 -0
- package/bun.lock +9 -0
- package/knip.json +1 -0
- package/node_modules/@vellumai/gateway-client/bun.lock +40 -0
- package/node_modules/@vellumai/gateway-client/package.json +25 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +343 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/package-boundary.test.ts +140 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
- package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +422 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +378 -0
- package/node_modules/@vellumai/gateway-client/src/types.ts +144 -0
- package/node_modules/@vellumai/gateway-client/tsconfig.json +20 -0
- package/package.json +3 -1
- package/src/feature-flag-registry.json +8 -16
- package/src/http/routes/log-tail.ts +80 -50
- package/src/http/routes/trust-rules.ts +24 -10
- package/src/index.ts +5 -3
- package/src/ipc/assistant-client.ts +2 -3
- package/src/ipc/log-tail-handlers.test.ts +78 -0
- package/src/ipc/log-tail-handlers.ts +23 -0
- package/src/ipc/trust-rules-handlers.test.ts +72 -0
- package/src/ipc/trust-rules-handlers.ts +24 -0
- package/src/risk/bash-risk-classifier.test.ts +3 -20
- package/src/risk/bash-risk-classifier.ts +1 -1
- package/src/risk/command-registry/commands/assistant.ts +7 -0
- package/src/risk/command-registry.test.ts +5 -0
- 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
|
@@ -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
|
+
});
|