@vellumai/credential-executor 0.10.5-dev.202607042025.8a04d0b → 0.10.5-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.
@@ -5,8 +5,10 @@ import {
5
5
  normalizePublicBaseUrl,
6
6
  } from "../ingress.js";
7
7
  import {
8
+ buildTwilioConnectActionUrl,
8
9
  buildTwilioMediaStreamUrl,
9
10
  buildTwilioPhoneNumberWebhookUrls,
11
+ buildTwilioRelayUrl,
10
12
  buildTwilioVoiceWebhookUrl,
11
13
  resolveTwilioPublicBaseUrl,
12
14
  } from "../twilio-ingress.js";
@@ -88,6 +90,12 @@ describe("Twilio ingress helpers", () => {
88
90
  expect(buildTwilioVoiceWebhookUrl("https://example.test", "call-123")).toBe(
89
91
  "https://example.test/webhooks/twilio/voice?callSessionId=call-123",
90
92
  );
93
+ expect(buildTwilioConnectActionUrl("https://example.test")).toBe(
94
+ "https://example.test/webhooks/twilio/connect-action",
95
+ );
96
+ expect(buildTwilioRelayUrl("https://example.test")).toBe(
97
+ "wss://example.test/webhooks/twilio/relay",
98
+ );
91
99
  expect(buildTwilioMediaStreamUrl("http://example.test")).toBe(
92
100
  "ws://example.test/webhooks/twilio/media-stream",
93
101
  );
@@ -2,6 +2,9 @@ import { normalizePublicBaseUrl } from "./ingress.js";
2
2
 
3
3
  export const TWILIO_VOICE_WEBHOOK_PATH = "/webhooks/twilio/voice";
4
4
  export const TWILIO_STATUS_WEBHOOK_PATH = "/webhooks/twilio/status";
5
+ export const TWILIO_CONNECT_ACTION_WEBHOOK_PATH =
6
+ "/webhooks/twilio/connect-action";
7
+ export const TWILIO_RELAY_WEBHOOK_PATH = "/webhooks/twilio/relay";
5
8
  export const TWILIO_MEDIA_STREAM_WEBHOOK_PATH = "/webhooks/twilio/media-stream";
6
9
 
7
10
  /**
@@ -10,9 +13,9 @@ export const TWILIO_MEDIA_STREAM_WEBHOOK_PATH = "/webhooks/twilio/media-stream";
10
13
  * with the actual public URL (from Velay registration, config, or the
11
14
  * `X-Vellum-Ingress-URL` header) before returning TwiML to Twilio.
12
15
  *
13
- * The placeholder uses `https://` so that `buildTwilioMediaStreamUrl` can
14
- * apply the standard `http→ws` scheme conversion, producing
15
- * `wss://__VELLUM_PUBLIC_BASE_URL__/…` in the output.
16
+ * The placeholder uses `https://` so that `buildTwilioRelayUrl` /
17
+ * `buildTwilioMediaStreamUrl` can apply the standard `http→ws` scheme
18
+ * conversion, producing `wss://__VELLUM_PUBLIC_BASE_URL__/…` in the output.
16
19
  */
17
20
  export const TWILIO_PUBLIC_BASE_URL_PLACEHOLDER =
18
21
  "https://__VELLUM_PUBLIC_BASE_URL__";
@@ -55,6 +58,14 @@ export function buildTwilioStatusWebhookUrl(baseUrl: string): string {
55
58
  return `${baseUrl}${TWILIO_STATUS_WEBHOOK_PATH}`;
56
59
  }
57
60
 
61
+ export function buildTwilioConnectActionUrl(baseUrl: string): string {
62
+ return `${baseUrl}${TWILIO_CONNECT_ACTION_WEBHOOK_PATH}`;
63
+ }
64
+
65
+ export function buildTwilioRelayUrl(baseUrl: string): string {
66
+ return `${toTwilioWebSocketBaseUrl(baseUrl)}${TWILIO_RELAY_WEBHOOK_PATH}`;
67
+ }
68
+
58
69
  export function buildTwilioMediaStreamUrl(baseUrl: string): string {
59
70
  return `${toTwilioWebSocketBaseUrl(baseUrl)}${TWILIO_MEDIA_STREAM_WEBHOOK_PATH}`;
60
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/credential-executor",
3
- "version": "0.10.5-dev.202607042025.8a04d0b",
3
+ "version": "0.10.5-staging.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -23,11 +23,7 @@ import {
23
23
  type RpcEnvelope,
24
24
  } from "@vellumai/service-contracts/credential-rpc";
25
25
 
26
- import {
27
- getCesDataRoot,
28
- getBootstrapSocketPath,
29
- getHealthPort,
30
- } from "../paths.js";
26
+ import { getCesDataRoot, getBootstrapSocketPath, getHealthPort } from "../paths.js";
31
27
  import { CesRpcServer, type RpcHandlerRegistry } from "../server.js";
32
28
 
33
29
  // ---------------------------------------------------------------------------
@@ -108,16 +104,7 @@ function createTestServer(handlers: RpcHandlerRegistry = {}) {
108
104
  return JSON.parse(lines[lines.length - 1]) as RpcEnvelope;
109
105
  }
110
106
 
111
- return {
112
- server,
113
- input,
114
- output,
115
- send,
116
- collectOutputLines,
117
- handshake,
118
- rpc,
119
- logs,
120
- };
107
+ return { server, input, output, send, collectOutputLines, handshake, rpc, logs };
121
108
  }
122
109
 
123
110
  // ---------------------------------------------------------------------------
@@ -207,15 +194,15 @@ describe("health probes", () => {
207
194
  // ---------------------------------------------------------------------------
208
195
 
209
196
  describe("local entrypoint transport isolation", () => {
210
- test("main.ts serves over stdio or a Unix socket, never a TCP listener", () => {
197
+ test("main.ts uses process.stdin/stdout, not TCP listeners", () => {
211
198
  const src = readFileSync(resolve(__dirname, "..", "main.ts"), "utf-8");
212
- // Serves the stdio-child transport (default mode).
199
+ // Uses stdin/stdout for transport
213
200
  expect(src).toMatch(/process\.stdin/);
214
201
  expect(src).toMatch(/process\.stdout/);
215
- // Standalone mode (CES_STANDALONE=1) listens on a Unix socket path only —
216
- // never a numeric TCP port — and never opens an HTTP server.
202
+ // Does not open any TCP or Unix socket listener
217
203
  expect(src).not.toMatch(/Bun\.serve\(/);
218
- expect(src).not.toMatch(/\.listen\(\d+/);
204
+ expect(src).not.toMatch(/createServer\(/);
205
+ expect(src).not.toMatch(/\.listen\(/);
219
206
  });
220
207
 
221
208
  test("main.ts logs to stderr, not stdout (avoids polluting transport)", () => {
@@ -323,10 +310,9 @@ describe("CesRpcServer", () => {
323
310
 
324
311
  const resp = await rpc("nonexistent_method", {});
325
312
  expect(resp.kind).toBe("response");
326
- expect(
327
- (resp.payload as { success: boolean; error: { code: string } }).error
328
- .code,
329
- ).toBe("METHOD_NOT_FOUND");
313
+ expect((resp.payload as { success: boolean; error: { code: string } }).error.code).toBe(
314
+ "METHOD_NOT_FOUND",
315
+ );
330
316
 
331
317
  server.close();
332
318
  input.end();
@@ -346,10 +332,7 @@ describe("CesRpcServer", () => {
346
332
 
347
333
  const resp = await rpc("fail_method", {});
348
334
  expect(resp.kind).toBe("response");
349
- const payload = resp.payload as {
350
- success: boolean;
351
- error: { code: string; message: string };
352
- };
335
+ const payload = resp.payload as { success: boolean; error: { code: string; message: string } };
353
336
  expect(payload.error.code).toBe("HANDLER_ERROR");
354
337
  expect(payload.error.message).toMatch(/Intentional test failure/);
355
338
 
package/src/main.ts CHANGED
@@ -1,28 +1,27 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Local CES entrypoint. Two run modes:
3
+ * Local CES entrypoint.
4
4
  *
5
- * - **stdio child (default):** the assistant spawns CES as a child process and
6
- * communicates over stdin/stdout. CES shuts down when stdin closes (parent
7
- * exit) or on SIGTERM. This is today's behavior.
5
+ * In local mode the assistant spawns CES as a child process and communicates
6
+ * over stdin/stdout using newline-delimited JSON. This entrypoint:
8
7
  *
9
- * - **standalone sibling (`CES_STANDALONE=1`):** CES is launched independently
10
- * by the CLI (the opt-in), serves RPC over a
11
- * Unix socket (`getLocalSocketPath()`), and runs until SIGTERM no stdio.
12
- * This is the direction local CES is converging on; the socket-serving here
13
- * is temporary scaffolding to be folded into a single unified CES entrypoint.
8
+ * 1. Ensures the CES-private data directories exist.
9
+ * 2. Starts the RPC server on process.stdin / process.stdout.
10
+ * 3. Shuts down cleanly when stdin closes (parent exit) or SIGTERM arrives.
14
11
  *
15
- * Local mode never opens a TCP listener. Neither the stdio transport nor the
16
- * Unix socket's listening fd is inherited by shell subprocesses spawned by CES
17
- * (e.g. for `run_authenticated_command`): Bun's `Bun.spawn` defaults to "pipe"
18
- * for stdio, and the listening socket is not passed to those subprocesses.
12
+ * Local mode never opens a TCP listener or Unix socket. All communication
13
+ * flows through the inherited stdio file descriptors, which are automatically
14
+ * closed when the parent process exits.
15
+ *
16
+ * The stdio transport ensures that shell subprocesses spawned by CES
17
+ * (e.g. for `run_authenticated_command`) do not accidentally inherit the
18
+ * command channel — Bun's `Bun.spawn` defaults to "pipe" for stdio on
19
+ * child processes, so CES's own stdin/stdout are not leaked to subprocesses.
19
20
  */
20
21
 
21
- import { mkdirSync, unlinkSync } from "node:fs";
22
- import { createServer as createNetServer, type Socket } from "node:net";
22
+ import { mkdirSync } from "node:fs";
23
23
  import { homedir } from "node:os";
24
24
  import { dirname, join } from "node:path";
25
- import { Readable, Writable } from "node:stream";
26
25
 
27
26
  import {
28
27
  CES_PROTOCOL_VERSION,
@@ -53,7 +52,6 @@ import {
53
52
  getCesGrantsDir,
54
53
  getCesLogDir,
55
54
  getCesToolStoreDir,
56
- getLocalSocketPath,
57
55
  } from "./paths.js";
58
56
  import {
59
57
  buildHandlersWithHttp,
@@ -124,7 +122,9 @@ function getSecurityDir(): string {
124
122
  // Build RPC handler registry
125
123
  // ---------------------------------------------------------------------------
126
124
 
127
- function buildHandlers(secureKeyBackend: SecureKeyBackend): RpcHandlerRegistry {
125
+ function buildHandlers(
126
+ secureKeyBackend: SecureKeyBackend,
127
+ ): RpcHandlerRegistry {
128
128
  // -- Grant stores ----------------------------------------------------------
129
129
  const persistentGrantStore = new PersistentGrantStore(
130
130
  getCesGrantsDir("local"),
@@ -350,103 +350,14 @@ function buildHandlers(secureKeyBackend: SecureKeyBackend): RpcHandlerRegistry {
350
350
  // Main
351
351
  // ---------------------------------------------------------------------------
352
352
 
353
- /**
354
- * Serve RPC over a Unix socket for standalone-sibling mode.
355
- *
356
- * Binds the socket, accepts connections concurrently (each served by its own
357
- * CesRpcServer over the shared handler registry), and unlinks the socket when
358
- * the signal aborts. Temporary scaffolding — this serving path will be folded
359
- * into a single unified CES entrypoint shared with the managed sidecar.
360
- */
361
- function serveStandaloneSocket(opts: {
362
- socketPath: string;
363
- handlers: RpcHandlerRegistry;
364
- signal: AbortSignal;
365
- logger: Pick<Console, "log" | "warn" | "error">;
366
- log: ReturnType<typeof getLogger>;
367
- }): void {
368
- const { socketPath, handlers, signal, logger, log } = opts;
369
-
370
- mkdirSync(dirname(socketPath), { recursive: true });
371
- try {
372
- unlinkSync(socketPath);
373
- } catch {
374
- // stale or absent — fine
375
- }
376
-
377
- const netServer = createNetServer();
378
-
379
- netServer.on("error", (err) => {
380
- log.warn({ err }, "CES standalone socket server error");
381
- });
382
-
383
- netServer.on("connection", (socket: Socket) => {
384
- const readable = new Readable({ read() {} });
385
- const writable = new Writable({
386
- write(chunk, _encoding, callback) {
387
- if (socket.writable) {
388
- socket.write(chunk, callback);
389
- } else {
390
- callback(new Error("Socket no longer writable"));
391
- }
392
- },
393
- });
394
- socket.on("data", (chunk) => readable.push(chunk));
395
- socket.on("end", () => readable.push(null));
396
- socket.on("error", (err) => {
397
- readable.destroy(err);
398
- writable.destroy(err);
399
- });
400
-
401
- const server = new CesRpcServer({
402
- input: readable,
403
- output: writable,
404
- handlers,
405
- logger,
406
- signal,
407
- onApiKeyUpdate: () => {},
408
- });
409
- void server.serve().catch((err) => {
410
- server.close();
411
- log.warn(
412
- { err },
413
- "CES standalone connection ended with a transport error",
414
- );
415
- });
416
- });
417
-
418
- netServer.listen(socketPath, () => {
419
- log.info(`CES standalone socket listening at ${socketPath}`);
420
- });
421
-
422
- signal.addEventListener(
423
- "abort",
424
- () => {
425
- netServer.close();
426
- try {
427
- unlinkSync(socketPath);
428
- } catch {
429
- // already removed
430
- }
431
- },
432
- { once: true },
433
- );
434
- }
435
-
436
353
  async function main(): Promise<void> {
437
354
  ensureDataDirs();
438
355
 
439
356
  initLogger({ dir: getCesLogDir(), retentionDays: 30 });
440
357
  const log = getLogger("main");
441
358
 
442
- // `CES_STANDALONE=1` runs CES as an independent, CLI-launched sibling over a
443
- // Unix socket; otherwise CES is the assistant's stdio child, as today.
444
- const standalone = process.env["CES_STANDALONE"] === "1";
445
-
446
359
  log.info(
447
- `Starting CES v${CES_PROTOCOL_VERSION} (local mode, ${
448
- standalone ? "standalone socket" : "stdio"
449
- } transport)`,
360
+ `Starting CES v${CES_PROTOCOL_VERSION} (local mode, stdio transport)`,
450
361
  );
451
362
 
452
363
  const controller = new AbortController();
@@ -479,42 +390,15 @@ async function main(): Promise<void> {
479
390
  const handlers = buildHandlers(secureKeyBackend);
480
391
 
481
392
  const rpcLog = getLogger("rpc");
482
- const rpcLogger = {
483
- log: (msg: string, ...args: unknown[]) => rpcLog.info({ args }, msg),
484
- warn: (msg: string, ...args: unknown[]) => rpcLog.warn({ args }, msg),
485
- error: (msg: string, ...args: unknown[]) => rpcLog.error({ args }, msg),
486
- };
487
-
488
- if (standalone) {
489
- // Serve over a Unix socket and run until a shutdown signal — no stdio
490
- // parent to anchor the lifecycle.
491
- serveStandaloneSocket({
492
- socketPath: getLocalSocketPath(),
493
- handlers,
494
- signal: controller.signal,
495
- logger: rpcLogger,
496
- log,
497
- });
498
- await new Promise<void>((resolve) => {
499
- if (controller.signal.aborted) {
500
- resolve();
501
- return;
502
- }
503
- controller.signal.addEventListener("abort", () => resolve(), {
504
- once: true,
505
- });
506
- });
507
- log.info("Server stopped.");
508
- return;
509
- }
510
-
511
- // Default: serve the spawning assistant over stdio. stdin closing (parent
512
- // exit) or SIGTERM shuts CES down.
513
393
  const server = new CesRpcServer({
514
394
  input: process.stdin,
515
395
  output: process.stdout,
516
396
  handlers,
517
- logger: rpcLogger,
397
+ logger: {
398
+ log: (msg: string, ...args: unknown[]) => rpcLog.info({ args }, msg),
399
+ warn: (msg: string, ...args: unknown[]) => rpcLog.warn({ args }, msg),
400
+ error: (msg: string, ...args: unknown[]) => rpcLog.error({ args }, msg),
401
+ },
518
402
  signal: controller.signal,
519
403
  // Local mode reads API keys from env/store directly — no-op handler so
520
404
  // update_managed_credential is still registered and returns success.
package/src/paths.ts CHANGED
@@ -140,33 +140,6 @@ export function getBootstrapSocketPath(): string {
140
140
  );
141
141
  }
142
142
 
143
- // ---------------------------------------------------------------------------
144
- // Local-mode standalone socket (temporary — CES_STANDALONE)
145
- // ---------------------------------------------------------------------------
146
-
147
- /** Default local-mode CES socket filename (under the local data root). */
148
- const LOCAL_SOCKET_NAME = "ces.sock";
149
-
150
- /**
151
- * Return the path to the local-mode CES Unix socket.
152
- *
153
- * Used when local CES runs as a standalone sibling (`CES_STANDALONE=1`, the
154
- * CLI-launched opt-in) rather than as the assistant's stdio
155
- * child. The socket lives under the CES-private local data root, whose
156
- * directory permissions are the access boundary.
157
- *
158
- * Priority:
159
- * 1. `CES_LOCAL_SOCKET` env var (full file path override; the CLI sets this
160
- * when launching the sibling).
161
- * 2. Default: `<localDataRoot>/ces.sock`.
162
- */
163
- export function getLocalSocketPath(): string {
164
- return (
165
- process.env["CES_LOCAL_SOCKET"] ??
166
- join(getCesDataRoot("local"), LOCAL_SOCKET_NAME)
167
- );
168
- }
169
-
170
143
  // ---------------------------------------------------------------------------
171
144
  // Health port (managed mode only)
172
145
  // ---------------------------------------------------------------------------
@@ -1,233 +0,0 @@
1
- /**
2
- * Local CES standalone-sibling test (real entrypoint subprocess).
3
- *
4
- * Spawns the actual `main.ts` entrypoint with `CES_STANDALONE=1` and **stdin
5
- * closed** — the way the CLI launches the sibling (the `CES_STANDALONE`
6
- * opt-in) — and verifies that CES:
7
- *
8
- * 1. binds its Unix socket and serves RPC despite having no stdio parent
9
- * (lifecycle anchored to SIGTERM, not stdin), and
10
- * 2. survives a client disconnecting, and
11
- * 3. shuts down on SIGTERM.
12
- *
13
- * Local mode has no TCP health server, so this runs without binding a TCP port.
14
- */
15
-
16
- import { afterEach, describe, expect, test } from "bun:test";
17
- import { createConnection, type Socket } from "node:net";
18
- import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
19
- import { tmpdir } from "node:os";
20
- import { join, resolve } from "node:path";
21
-
22
- import {
23
- CES_PROTOCOL_VERSION,
24
- CesRpcMethod,
25
- type HandshakeAck,
26
- type RpcEnvelope,
27
- type ListCredentialsResponse,
28
- } from "@vellumai/service-contracts/credential-rpc";
29
-
30
- import type { Subprocess } from "bun";
31
-
32
- function delay(ms: number): Promise<void> {
33
- return new Promise((r) => setTimeout(r, ms));
34
- }
35
-
36
- async function waitForSocket(
37
- socketPath: string,
38
- timeoutMs = 10_000,
39
- ): Promise<void> {
40
- const deadline = Date.now() + timeoutMs;
41
- while (Date.now() < deadline) {
42
- if (existsSync(socketPath)) return;
43
- await delay(50);
44
- }
45
- throw new Error(`Standalone CES socket did not appear within ${timeoutMs}ms`);
46
- }
47
-
48
- function connectToSocket(
49
- socketPath: string,
50
- { maxRetries = 40, baseDelayMs = 25 } = {},
51
- ): Promise<Socket> {
52
- return new Promise((resolveConn, reject) => {
53
- let attempt = 0;
54
- const tryConnect = () => {
55
- const sock = createConnection(socketPath, () => {
56
- sock.removeAllListeners("error");
57
- resolveConn(sock);
58
- });
59
- sock.on("error", (err: NodeJS.ErrnoException) => {
60
- sock.destroy();
61
- attempt++;
62
- if (
63
- attempt < maxRetries &&
64
- (err.code === "ENOENT" || err.code === "ECONNREFUSED")
65
- ) {
66
- setTimeout(tryConnect, baseDelayMs);
67
- } else {
68
- reject(err);
69
- }
70
- });
71
- };
72
- tryConnect();
73
- });
74
- }
75
-
76
- /** Read one newline-delimited JSON message from the socket. */
77
- function readOne<T>(sock: Socket, timeoutMs = 5_000): Promise<T> {
78
- return new Promise((resolveMsg, reject) => {
79
- let buffer = "";
80
- const timer = setTimeout(() => {
81
- sock.removeAllListeners("data");
82
- reject(new Error("Timed out waiting for a message"));
83
- }, timeoutMs);
84
- const onData = (chunk: Buffer) => {
85
- buffer += chunk.toString("utf-8");
86
- const idx = buffer.indexOf("\n");
87
- if (idx === -1) return;
88
- clearTimeout(timer);
89
- sock.removeListener("data", onData);
90
- try {
91
- resolveMsg(JSON.parse(buffer.slice(0, idx).trim()) as T);
92
- } catch (err) {
93
- reject(err as Error);
94
- }
95
- };
96
- sock.on("data", onData);
97
- sock.on("error", (err) => {
98
- clearTimeout(timer);
99
- reject(err);
100
- });
101
- });
102
- }
103
-
104
- async function handshake(
105
- sock: Socket,
106
- sessionId: string,
107
- ): Promise<HandshakeAck> {
108
- sock.write(
109
- JSON.stringify({
110
- type: "handshake_request",
111
- protocolVersion: CES_PROTOCOL_VERSION,
112
- sessionId,
113
- }) + "\n",
114
- );
115
- return readOne<HandshakeAck>(sock);
116
- }
117
-
118
- let tmpDir: string | undefined;
119
- let proc: Subprocess | undefined;
120
-
121
- afterEach(async () => {
122
- if (proc) {
123
- proc.kill("SIGTERM");
124
- await Promise.race([proc.exited, delay(3_000)]);
125
- proc = undefined;
126
- }
127
- if (tmpDir) {
128
- try {
129
- rmSync(tmpDir, { recursive: true, force: true });
130
- } catch {
131
- /* ok */
132
- }
133
- tmpDir = undefined;
134
- }
135
- });
136
-
137
- describe("local CES standalone sibling (real entrypoint)", () => {
138
- test("serves over a socket with no stdio parent, survives disconnect, exits on SIGTERM", async () => {
139
- tmpDir = mkdtempSync(join(tmpdir(), "ces-standalone-"));
140
- const socketPath = join(tmpDir, "ces.sock");
141
- const securityDir = join(tmpDir, "protected");
142
- const workspaceDir = join(tmpDir, "workspace");
143
- mkdirSync(securityDir, { recursive: true });
144
- mkdirSync(workspaceDir, { recursive: true });
145
-
146
- const localMain = resolve(__dirname, "..", "main.ts");
147
-
148
- // CES_STANDALONE=1 + stdin closed is how the CLI launches the sibling.
149
- proc = Bun.spawn({
150
- cmd: [process.execPath, localMain],
151
- env: {
152
- ...process.env,
153
- CES_STANDALONE: "1",
154
- CES_LOCAL_SOCKET: socketPath,
155
- CREDENTIAL_SECURITY_DIR: securityDir,
156
- VELLUM_WORKSPACE_DIR: workspaceDir,
157
- },
158
- stdin: "ignore",
159
- stdout: "ignore",
160
- stderr: "ignore",
161
- });
162
-
163
- await waitForSocket(socketPath);
164
- expect(proc.killed).toBe(false);
165
-
166
- // First client: handshake + a real RPC.
167
- const first = await connectToSocket(socketPath);
168
- const ack1 = await handshake(first, "sibling-1");
169
- expect(ack1.accepted).toBe(true);
170
-
171
- first.write(
172
- JSON.stringify({
173
- type: "rpc",
174
- id: "rpc-1",
175
- kind: "request",
176
- method: CesRpcMethod.ListCredentials,
177
- payload: {},
178
- timestamp: new Date().toISOString(),
179
- }) + "\n",
180
- );
181
- const rpcResp = await readOne<RpcEnvelope & { type: "rpc" }>(first);
182
- expect((rpcResp.payload as ListCredentialsResponse).accounts).toEqual([]);
183
-
184
- // Disconnect — CES must stay up.
185
- first.destroy();
186
- await delay(200);
187
- expect(proc.killed).toBe(false);
188
-
189
- // Reconnect proves it survived.
190
- const second = await connectToSocket(socketPath);
191
- const ack2 = await handshake(second, "sibling-2");
192
- expect(ack2.accepted).toBe(true);
193
- second.destroy();
194
-
195
- // SIGTERM shuts it down.
196
- proc.kill("SIGTERM");
197
- const exited = await Promise.race([
198
- proc.exited.then(() => true),
199
- delay(5_000).then(() => false),
200
- ]);
201
- expect(exited).toBe(true);
202
- }, 30_000);
203
-
204
- test("without CES_STANDALONE, exits when stdin closes (stdio child)", async () => {
205
- tmpDir = mkdtempSync(join(tmpdir(), "ces-stdio-"));
206
- const securityDir = join(tmpDir, "protected");
207
- const workspaceDir = join(tmpDir, "workspace");
208
- mkdirSync(securityDir, { recursive: true });
209
- mkdirSync(workspaceDir, { recursive: true });
210
-
211
- const localMain = resolve(__dirname, "..", "main.ts");
212
-
213
- // Default mode: stdin "ignore" EOFs immediately, so the stdio server's
214
- // input stream ends and CES shuts down on its own — the today behavior.
215
- proc = Bun.spawn({
216
- cmd: [process.execPath, localMain],
217
- env: {
218
- ...process.env,
219
- CREDENTIAL_SECURITY_DIR: securityDir,
220
- VELLUM_WORKSPACE_DIR: workspaceDir,
221
- },
222
- stdin: "ignore",
223
- stdout: "ignore",
224
- stderr: "ignore",
225
- });
226
-
227
- const exited = await Promise.race([
228
- proc.exited.then(() => true),
229
- delay(10_000).then(() => false),
230
- ]);
231
- expect(exited).toBe(true);
232
- }, 20_000);
233
- });