@vellumai/credential-executor 0.10.7-dev.202607102035.64f07ea → 0.10.7-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/Dockerfile +1 -1
  2. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  3. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +2 -0
  4. package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +686 -0
  5. package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +5 -3
  6. package/node_modules/@vellumai/service-contracts/src/grants.ts +184 -0
  7. package/node_modules/@vellumai/service-contracts/src/index.ts +4 -2
  8. package/node_modules/@vellumai/service-contracts/src/rendering.ts +135 -0
  9. package/node_modules/@vellumai/service-contracts/src/rpc.ts +447 -4
  10. package/package.json +3 -2
  11. package/src/__tests__/bulk-set-credentials.test.ts +1 -1
  12. package/src/__tests__/command-executor.test.ts +1879 -0
  13. package/src/__tests__/command-validator.test.ts +1405 -0
  14. package/src/__tests__/command-workspace.test.ts +1050 -0
  15. package/src/__tests__/grant-store.test.ts +689 -0
  16. package/src/__tests__/http-executor.test.ts +1336 -0
  17. package/src/__tests__/http-policy.test.ts +1069 -0
  18. package/src/__tests__/local-materializers.test.ts +860 -0
  19. package/src/__tests__/local-standalone.test.ts +36 -5
  20. package/src/__tests__/local-token-refresh.test.ts +361 -0
  21. package/src/__tests__/manage-secure-command-tool.test.ts +134 -0
  22. package/src/__tests__/managed-integration.test.ts +91 -112
  23. package/src/__tests__/managed-lazy-getters.test.ts +359 -0
  24. package/src/__tests__/managed-materializers.test.ts +1028 -0
  25. package/src/__tests__/managed-reconnect.test.ts +2 -2
  26. package/src/__tests__/managed-rejection.test.ts +43 -0
  27. package/src/__tests__/toolstore.test.ts +773 -0
  28. package/src/__tests__/transport.test.ts +27 -23
  29. package/src/audit/store.ts +188 -0
  30. package/src/cli.ts +1 -1
  31. package/src/commands/auth-adapters.ts +169 -0
  32. package/src/commands/egress-hooks.ts +203 -0
  33. package/src/commands/executor.ts +1155 -0
  34. package/src/commands/output-scan.ts +157 -0
  35. package/src/commands/profiles.ts +286 -0
  36. package/src/commands/validator.ts +702 -0
  37. package/src/commands/workspace.ts +550 -0
  38. package/src/grants/index.ts +17 -0
  39. package/src/grants/persistent-store.ts +309 -0
  40. package/src/grants/rpc-handlers.ts +293 -0
  41. package/src/grants/temporary-store.ts +289 -0
  42. package/src/http/audit.ts +84 -0
  43. package/src/http/executor.ts +684 -0
  44. package/src/http/path-template.ts +245 -0
  45. package/src/http/policy.ts +238 -0
  46. package/src/http/response-filter.ts +233 -0
  47. package/src/index.ts +88 -8
  48. package/src/main.ts +340 -228
  49. package/src/managed-errors.ts +9 -0
  50. package/src/managed-lazy-getters.ts +106 -0
  51. package/src/managed-main.ts +822 -0
  52. package/src/materializers/local-oauth-lookup.ts +98 -0
  53. package/src/materializers/local-token-refresh.ts +287 -0
  54. package/src/materializers/local.ts +316 -0
  55. package/src/materializers/managed-platform.ts +295 -0
  56. package/src/paths.ts +20 -4
  57. package/src/server.ts +469 -52
  58. package/src/subjects/local.ts +177 -0
  59. package/src/subjects/managed.ts +311 -0
  60. package/src/subjects/policy.ts +79 -0
  61. package/src/toolstore/integrity.ts +94 -0
  62. package/src/toolstore/manifest.ts +154 -0
  63. package/src/toolstore/publish.ts +571 -0
  64. package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +0 -104
  65. package/node_modules/@vellumai/service-contracts/src/attachment-naming.ts +0 -118
@@ -21,17 +21,14 @@ import { afterEach, describe, expect, test } from "bun:test";
21
21
  import { mkdirSync, mkdtempSync, rmSync, unlinkSync } from "node:fs";
22
22
  import { tmpdir } from "node:os";
23
23
  import { join } from "node:path";
24
- import {
25
- createConnection,
26
- createServer as createNetServer,
27
- type Socket,
28
- } from "node:net";
24
+ import { createConnection, createServer as createNetServer, type Socket } from "node:net";
29
25
  import { Readable, Writable } from "node:stream";
30
26
 
31
27
  import {
32
28
  CES_PROTOCOL_VERSION,
33
29
  CesRpcMethod,
34
30
  type HandshakeAck,
31
+ type ListGrantsResponse,
35
32
  type GetCredentialResponse,
36
33
  type SetCredentialResponse,
37
34
  type DeleteCredentialResponse,
@@ -39,11 +36,13 @@ import {
39
36
  type RpcEnvelope,
40
37
  } from "@vellumai/service-contracts/credential-rpc";
41
38
 
39
+ import { PersistentGrantStore } from "../grants/persistent-store.js";
40
+ import { AuditStore } from "../audit/store.js";
42
41
  import {
43
- CesRpcServer,
44
- type RpcHandlerRegistry,
45
- type ServeEndReason,
46
- } from "../server.js";
42
+ createListGrantsHandler,
43
+ createListAuditRecordsHandler,
44
+ } from "../grants/rpc-handlers.js";
45
+ import { CesRpcServer, type RpcHandlerRegistry, type ServeEndReason } from "../server.js";
47
46
  import { createLocalSecureKeyBackend } from "../materializers/local-secure-key-backend.js";
48
47
 
49
48
  // ---------------------------------------------------------------------------
@@ -84,11 +83,40 @@ function restoreEnv(saved: SavedEnv): void {
84
83
  // Helpers
85
84
  // ---------------------------------------------------------------------------
86
85
 
86
+ /**
87
+ * Build a minimal RPC handler registry with `list_grants` and
88
+ * `list_audit_records` backed by real stores in a temp directory.
89
+ */
90
+ function buildMinimalHandlers(dataDir: string): RpcHandlerRegistry {
91
+ const grantsDir = join(dataDir, "grants");
92
+ const auditDir = join(dataDir, "audit");
93
+ mkdirSync(grantsDir, { recursive: true });
94
+ mkdirSync(auditDir, { recursive: true });
95
+
96
+ const persistentGrantStore = new PersistentGrantStore(grantsDir);
97
+ persistentGrantStore.init();
98
+
99
+ const auditStore = new AuditStore(auditDir);
100
+ auditStore.init();
101
+
102
+ const handlers: RpcHandlerRegistry = {};
103
+
104
+ handlers[CesRpcMethod.ListGrants] = createListGrantsHandler({
105
+ persistentGrantStore,
106
+ }) as typeof handlers[string];
107
+
108
+ handlers[CesRpcMethod.ListAuditRecords] = createListAuditRecordsHandler({
109
+ auditStore,
110
+ }) as typeof handlers[string];
111
+
112
+ return handlers;
113
+ }
114
+
87
115
  /**
88
116
  * Build an RPC handler registry with credential CRUD handlers backed by
89
117
  * a real SecureKeyBackend using a temp directory for credential storage.
90
118
  *
91
- * Mirrors the handler registration in main.ts.
119
+ * Mirrors the handler registration in managed-main.ts.
92
120
  */
93
121
  function buildCredentialHandlers(vellumRoot: string): RpcHandlerRegistry {
94
122
  const secureKeyBackend = createLocalSecureKeyBackend(vellumRoot);
@@ -97,27 +125,22 @@ function buildCredentialHandlers(vellumRoot: string): RpcHandlerRegistry {
97
125
  handlers[CesRpcMethod.GetCredential] = (async (req: { account: string }) => {
98
126
  const value = await secureKeyBackend.get(req.account);
99
127
  return { found: value !== undefined, value };
100
- }) as (typeof handlers)[string];
128
+ }) as typeof handlers[string];
101
129
 
102
- handlers[CesRpcMethod.SetCredential] = (async (req: {
103
- account: string;
104
- value: string;
105
- }) => {
130
+ handlers[CesRpcMethod.SetCredential] = (async (req: { account: string; value: string }) => {
106
131
  const ok = await secureKeyBackend.set(req.account, req.value);
107
132
  return { ok };
108
- }) as (typeof handlers)[string];
133
+ }) as typeof handlers[string];
109
134
 
110
- handlers[CesRpcMethod.DeleteCredential] = (async (req: {
111
- account: string;
112
- }) => {
135
+ handlers[CesRpcMethod.DeleteCredential] = (async (req: { account: string }) => {
113
136
  const result = await secureKeyBackend.delete(req.account);
114
137
  return { result };
115
- }) as (typeof handlers)[string];
138
+ }) as typeof handlers[string];
116
139
 
117
140
  handlers[CesRpcMethod.ListCredentials] = (async () => {
118
141
  const accounts = await secureKeyBackend.list();
119
142
  return { accounts };
120
- }) as (typeof handlers)[string];
143
+ }) as typeof handlers[string];
121
144
 
122
145
  return handlers;
123
146
  }
@@ -126,13 +149,10 @@ function buildCredentialHandlers(vellumRoot: string): RpcHandlerRegistry {
126
149
  * Accept a single connection on a Unix socket and return
127
150
  * readable/writable streams plus cleanup helpers.
128
151
  *
129
- * Replicates the same accept-one-connection pattern from main.ts
152
+ * Replicates the same accept-one-connection pattern from managed-main.ts
130
153
  * but in a test-friendly form.
131
154
  */
132
- function acceptOneConnection(
133
- socketPath: string,
134
- signal: AbortSignal,
135
- ): Promise<{
155
+ function acceptOneConnection(socketPath: string, signal: AbortSignal): Promise<{
136
156
  readable: Readable;
137
157
  writable: Writable;
138
158
  socket: Socket;
@@ -142,11 +162,7 @@ function acceptOneConnection(
142
162
 
143
163
  const cleanup = () => {
144
164
  netServer.close();
145
- try {
146
- unlinkSync(socketPath);
147
- } catch {
148
- /* ok */
149
- }
165
+ try { unlinkSync(socketPath); } catch { /* ok */ }
150
166
  };
151
167
 
152
168
  if (signal.aborted) {
@@ -154,14 +170,10 @@ function acceptOneConnection(
154
170
  return;
155
171
  }
156
172
 
157
- signal.addEventListener(
158
- "abort",
159
- () => {
160
- cleanup();
161
- reject(new Error("Aborted while waiting for connection"));
162
- },
163
- { once: true },
164
- );
173
+ signal.addEventListener("abort", () => {
174
+ cleanup();
175
+ reject(new Error("Aborted while waiting for connection"));
176
+ }, { once: true });
165
177
 
166
178
  netServer.on("error", (err: Error) => {
167
179
  cleanup();
@@ -174,19 +186,11 @@ function acceptOneConnection(
174
186
 
175
187
  netServer.on("connection", (sock: Socket) => {
176
188
  netServer.close();
177
- try {
178
- unlinkSync(socketPath);
179
- } catch {
180
- /* ok */
181
- }
189
+ try { unlinkSync(socketPath); } catch { /* ok */ }
182
190
 
183
191
  const readable = new Readable({ read() {} });
184
192
  const writable = new Writable({
185
- write(
186
- chunk: Buffer,
187
- _encoding: string,
188
- callback: (err?: Error | null) => void,
189
- ) {
193
+ write(chunk: Buffer, _encoding: string, callback: (err?: Error | null) => void) {
190
194
  if (sock.writable) {
191
195
  sock.write(chunk, callback);
192
196
  } else {
@@ -252,11 +256,7 @@ function connectToSocket(
252
256
  * Read newline-delimited JSON messages from a socket, collecting them
253
257
  * until the expected count is reached or a timeout fires.
254
258
  */
255
- function readMessages(
256
- sock: Socket,
257
- expectedCount: number,
258
- timeoutMs = 3000,
259
- ): Promise<unknown[]> {
259
+ function readMessages(sock: Socket, expectedCount: number, timeoutMs = 3000): Promise<unknown[]> {
260
260
  return new Promise((resolve, reject) => {
261
261
  const messages: unknown[] = [];
262
262
  let buffer = "";
@@ -329,9 +329,7 @@ afterEach(async () => {
329
329
  if (tmpDir) {
330
330
  try {
331
331
  rmSync(tmpDir, { recursive: true, force: true });
332
- } catch {
333
- /* ok */
334
- }
332
+ } catch { /* ok */ }
335
333
  }
336
334
  });
337
335
 
@@ -369,7 +367,7 @@ describe("managed CES integration (real Unix socket)", () => {
369
367
 
370
368
  // -- Start health server ---------------------------------------------------
371
369
  // NOTE: This uses a local Bun.serve mock rather than the production
372
- // `startHealthServer()` from main.ts. The production function is
370
+ // `startHealthServer()` from managed-main.ts. The production function is
373
371
  // not exported and depends on module-level mutable state (the
374
372
  // `rpcConnected` flag) that cannot be controlled from tests.
375
373
  //
@@ -379,7 +377,7 @@ describe("managed CES integration (real Unix socket)", () => {
379
377
  // - End-to-end socket + RPC + health plumbing in one integration flow.
380
378
  //
381
379
  // What this does NOT cover:
382
- // - The actual `startHealthServer()` implementation in main.ts,
380
+ // - The actual `startHealthServer()` implementation in managed-main.ts,
383
381
  // including the `rpcConnected` field in the /readyz response. If the
384
382
  // production health handler drifts (changes routes, status codes, or
385
383
  // response shape), this test will not catch it. To close that gap,
@@ -390,9 +388,10 @@ describe("managed CES integration (real Unix socket)", () => {
390
388
  fetch(req) {
391
389
  const url = new URL(req.url);
392
390
  if (url.pathname === "/healthz") {
393
- return new Response(JSON.stringify({ status: "ok" }), {
394
- headers: { "Content-Type": "application/json" },
395
- });
391
+ return new Response(
392
+ JSON.stringify({ status: "ok" }),
393
+ { headers: { "Content-Type": "application/json" } },
394
+ );
396
395
  }
397
396
  if (url.pathname === "/readyz") {
398
397
  return new Response(
@@ -405,10 +404,7 @@ describe("managed CES integration (real Unix socket)", () => {
405
404
  });
406
405
 
407
406
  // -- Start accept-one-connection on the Unix socket ------------------------
408
- const connectionPromise = acceptOneConnection(
409
- socketPath,
410
- controller.signal,
411
- );
407
+ const connectionPromise = acceptOneConnection(socketPath, controller.signal);
412
408
 
413
409
  // -- Client connects -------------------------------------------------------
414
410
  clientSocket = await connectToSocket(socketPath);
@@ -417,7 +413,7 @@ describe("managed CES integration (real Unix socket)", () => {
417
413
  const conn = await connectionPromise;
418
414
 
419
415
  let observedSessionId = "";
420
- const handlers = buildCredentialHandlers(dataDir);
416
+ const handlers = buildMinimalHandlers(dataDir);
421
417
 
422
418
  serverRpcServer = new CesRpcServer({
423
419
  input: conn.readable,
@@ -458,13 +454,13 @@ describe("managed CES integration (real Unix socket)", () => {
458
454
  expect(observedSessionId).toBe(handshakeSessionId);
459
455
  expect(serverRpcServer.currentSessionId).toBe(handshakeSessionId);
460
456
 
461
- // -- Step 2: RPC dispatch (list_credentials) -------------------------------
457
+ // -- Step 2: RPC dispatch (list_grants) ------------------------------------
462
458
  const rpcId = "rpc-1";
463
459
  sendMessage(clientSocket, {
464
460
  type: "rpc",
465
461
  id: rpcId,
466
462
  kind: "request",
467
- method: CesRpcMethod.ListCredentials,
463
+ method: CesRpcMethod.ListGrants,
468
464
  payload: {},
469
465
  timestamp: new Date().toISOString(),
470
466
  });
@@ -476,33 +472,34 @@ describe("managed CES integration (real Unix socket)", () => {
476
472
  expect(rpcResp.type).toBe("rpc");
477
473
  expect(rpcResp.id).toBe(rpcId);
478
474
  expect(rpcResp.kind).toBe("response");
479
- expect(rpcResp.method).toBe(CesRpcMethod.ListCredentials);
475
+ expect(rpcResp.method).toBe(CesRpcMethod.ListGrants);
480
476
 
481
- const listPayload = rpcResp.payload as { accounts: string[] };
482
- expect(listPayload.accounts).toEqual([]);
477
+ const grantsPayload = rpcResp.payload as ListGrantsResponse;
478
+ expect(grantsPayload.grants).toEqual([]);
483
479
 
484
- // -- Step 3: RPC dispatch (get_credential, not found) ----------------------
485
- const getRpcId = "rpc-2";
480
+ // -- Step 3: RPC dispatch (list_audit_records) -----------------------------
481
+ const auditRpcId = "rpc-2";
486
482
  sendMessage(clientSocket, {
487
483
  type: "rpc",
488
- id: getRpcId,
484
+ id: auditRpcId,
489
485
  kind: "request",
490
- method: CesRpcMethod.GetCredential,
491
- payload: { account: "does-not-exist" },
486
+ method: CesRpcMethod.ListAuditRecords,
487
+ payload: {},
492
488
  timestamp: new Date().toISOString(),
493
489
  });
494
490
 
495
- const getMessages = await readMessages(clientSocket, 1);
496
- expect(getMessages.length).toBe(1);
491
+ const auditMessages = await readMessages(clientSocket, 1);
492
+ expect(auditMessages.length).toBe(1);
497
493
 
498
- const getResp = getMessages[0] as RpcEnvelope & { type: "rpc" };
499
- expect(getResp.type).toBe("rpc");
500
- expect(getResp.id).toBe(getRpcId);
501
- expect(getResp.kind).toBe("response");
502
- expect(getResp.method).toBe(CesRpcMethod.GetCredential);
494
+ const auditResp = auditMessages[0] as RpcEnvelope & { type: "rpc" };
495
+ expect(auditResp.type).toBe("rpc");
496
+ expect(auditResp.id).toBe(auditRpcId);
497
+ expect(auditResp.kind).toBe("response");
498
+ expect(auditResp.method).toBe(CesRpcMethod.ListAuditRecords);
503
499
 
504
- const getPayload = getResp.payload as { found: boolean };
505
- expect(getPayload.found).toBe(false);
500
+ const auditPayload = auditResp.payload as { records: unknown[]; nextCursor: string | null };
501
+ expect(auditPayload.records).toEqual([]);
502
+ expect(auditPayload.nextCursor).toBeNull();
506
503
 
507
504
  // -- Step 4: Health endpoint -----------------------------------------------
508
505
  const healthzResp = await fetch(`http://localhost:${healthPort}/healthz`);
@@ -532,10 +529,7 @@ describe("managed CES integration (real Unix socket)", () => {
532
529
  const unknownResp = unknownMessages[0] as RpcEnvelope & { type: "rpc" };
533
530
  expect(unknownResp.id).toBe(unknownRpcId);
534
531
  expect(unknownResp.kind).toBe("response");
535
- const unknownPayload = unknownResp.payload as {
536
- success: boolean;
537
- error: { code: string };
538
- };
532
+ const unknownPayload = unknownResp.payload as { success: boolean; error: { code: string } };
539
533
  expect(unknownPayload.error.code).toBe("METHOD_NOT_FOUND");
540
534
 
541
535
  // -- Cleanup ---------------------------------------------------------------
@@ -558,10 +552,7 @@ describe("managed CES integration (real Unix socket)", () => {
558
552
 
559
553
  controller = new AbortController();
560
554
 
561
- const connectionPromise = acceptOneConnection(
562
- socketPath,
563
- controller.signal,
564
- );
555
+ const connectionPromise = acceptOneConnection(socketPath, controller.signal);
565
556
  clientSocket = await connectToSocket(socketPath);
566
557
  const conn = await connectionPromise;
567
558
 
@@ -610,14 +601,11 @@ describe("managed CES integration (real Unix socket)", () => {
610
601
 
611
602
  controller = new AbortController();
612
603
 
613
- const connectionPromise = acceptOneConnection(
614
- socketPath,
615
- controller.signal,
616
- );
604
+ const connectionPromise = acceptOneConnection(socketPath, controller.signal);
617
605
  clientSocket = await connectToSocket(socketPath);
618
606
  const conn = await connectionPromise;
619
607
 
620
- const handlers = buildCredentialHandlers(dataDir);
608
+ const handlers = buildMinimalHandlers(dataDir);
621
609
  serverRpcServer = new CesRpcServer({
622
610
  input: conn.readable,
623
611
  output: conn.writable,
@@ -633,7 +621,7 @@ describe("managed CES integration (real Unix socket)", () => {
633
621
  type: "rpc",
634
622
  id: "pre-hs-1",
635
623
  kind: "request",
636
- method: CesRpcMethod.ListCredentials,
624
+ method: CesRpcMethod.ListGrants,
637
625
  payload: {},
638
626
  timestamp: new Date().toISOString(),
639
627
  });
@@ -644,10 +632,7 @@ describe("managed CES integration (real Unix socket)", () => {
644
632
  const resp = messages[0] as RpcEnvelope & { type: "rpc" };
645
633
  expect(resp.id).toBe("pre-hs-1");
646
634
  expect(resp.kind).toBe("response");
647
- const payload = resp.payload as {
648
- success: boolean;
649
- error: { code: string };
650
- };
635
+ const payload = resp.payload as { success: boolean; error: { code: string } };
651
636
  expect(payload.error.code).toBe("HANDSHAKE_REQUIRED");
652
637
 
653
638
  clientSocket.end();
@@ -664,10 +649,7 @@ describe("managed CES integration (real Unix socket)", () => {
664
649
 
665
650
  controller = new AbortController();
666
651
 
667
- const connectionPromise = acceptOneConnection(
668
- socketPath,
669
- controller.signal,
670
- );
652
+ const connectionPromise = acceptOneConnection(socketPath, controller.signal);
671
653
 
672
654
  // First connection succeeds
673
655
  clientSocket = await connectToSocket(socketPath);
@@ -718,10 +700,7 @@ describe("credential CRUD RPC", () => {
718
700
 
719
701
  controller = new AbortController();
720
702
 
721
- const connectionPromise = acceptOneConnection(
722
- socketPath,
723
- controller.signal,
724
- );
703
+ const connectionPromise = acceptOneConnection(socketPath, controller.signal);
725
704
  clientSocket = await connectToSocket(socketPath);
726
705
  const conn = await connectionPromise;
727
706