@vellumai/credential-executor 0.5.7 → 0.5.9

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.
@@ -3,7 +3,7 @@
3
3
  * dependencies between index.ts and rpc.ts.
4
4
  */
5
5
 
6
- import { z } from "zod/v4";
6
+ import { z } from "zod";
7
7
 
8
8
  export const RpcErrorSchema = z.object({
9
9
  code: z.string(),
@@ -9,7 +9,7 @@
9
9
  * - Audit record summaries (materialization events)
10
10
  */
11
11
 
12
- import { z } from "zod/v4";
12
+ import { z } from "zod";
13
13
 
14
14
  // ---------------------------------------------------------------------------
15
15
  // Grant proposal types
@@ -20,7 +20,7 @@
20
20
  * is the platform-assigned connection identifier.
21
21
  */
22
22
 
23
- import { z } from "zod/v4";
23
+ import { z } from "zod";
24
24
 
25
25
  // ---------------------------------------------------------------------------
26
26
  // Handle type discriminator
@@ -7,7 +7,7 @@
7
7
  * module so that both sides can depend on it without circular references.
8
8
  */
9
9
 
10
- import { z } from "zod/v4";
10
+ import { z } from "zod";
11
11
  import { RpcErrorSchema } from "./error.js";
12
12
 
13
13
  // ---------------------------------------------------------------------------
@@ -33,7 +33,7 @@
33
33
  * - `list_credentials` — List all credential account names
34
34
  */
35
35
 
36
- import { z } from "zod/v4";
36
+ import { z } from "zod";
37
37
  import {
38
38
  AuditRecordSummarySchema,
39
39
  GrantProposalSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/credential-executor",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -29,6 +29,10 @@ import {
29
29
  CesRpcMethod,
30
30
  type HandshakeAck,
31
31
  type ListGrantsResponse,
32
+ type GetCredentialResponse,
33
+ type SetCredentialResponse,
34
+ type DeleteCredentialResponse,
35
+ type ListCredentialsResponse,
32
36
  type RpcEnvelope,
33
37
  } from "@vellumai/ces-contracts";
34
38
 
@@ -40,6 +44,7 @@ import {
40
44
  createListAuditRecordsHandler,
41
45
  } from "../grants/rpc-handlers.js";
42
46
  import { CesRpcServer, type RpcHandlerRegistry, type SessionIdRef } from "../server.js";
47
+ import { createLocalSecureKeyBackend } from "../materializers/local-secure-key-backend.js";
43
48
 
44
49
  // ---------------------------------------------------------------------------
45
50
  // Environment setup
@@ -52,6 +57,7 @@ const SAVED_ENV_KEYS = [
52
57
  "CES_BOOTSTRAP_SOCKET",
53
58
  "CES_HEALTH_PORT",
54
59
  "CES_MODE",
60
+ "CREDENTIAL_SECURITY_DIR",
55
61
  ] as const;
56
62
 
57
63
  type SavedEnv = Record<string, string | undefined>;
@@ -107,6 +113,39 @@ function buildMinimalHandlers(dataDir: string): RpcHandlerRegistry {
107
113
  return handlers;
108
114
  }
109
115
 
116
+ /**
117
+ * Build an RPC handler registry with credential CRUD handlers backed by
118
+ * a real SecureKeyBackend using a temp directory for credential storage.
119
+ *
120
+ * Mirrors the handler registration in managed-main.ts.
121
+ */
122
+ function buildCredentialHandlers(vellumRoot: string): RpcHandlerRegistry {
123
+ const secureKeyBackend = createLocalSecureKeyBackend(vellumRoot);
124
+ const handlers: RpcHandlerRegistry = {};
125
+
126
+ handlers[CesRpcMethod.GetCredential] = (async (req: { account: string }) => {
127
+ const value = await secureKeyBackend.get(req.account);
128
+ return { found: value !== undefined, value };
129
+ }) as typeof handlers[string];
130
+
131
+ handlers[CesRpcMethod.SetCredential] = (async (req: { account: string; value: string }) => {
132
+ const ok = await secureKeyBackend.set(req.account, req.value);
133
+ return { ok };
134
+ }) as typeof handlers[string];
135
+
136
+ handlers[CesRpcMethod.DeleteCredential] = (async (req: { account: string }) => {
137
+ const result = await secureKeyBackend.delete(req.account);
138
+ return { result };
139
+ }) as typeof handlers[string];
140
+
141
+ handlers[CesRpcMethod.ListCredentials] = (async () => {
142
+ const accounts = await secureKeyBackend.list();
143
+ return { accounts };
144
+ }) as typeof handlers[string];
145
+
146
+ return handlers;
147
+ }
148
+
110
149
  /**
111
150
  * Accept a single connection on a Unix socket and return
112
151
  * readable/writable streams plus cleanup helpers.
@@ -629,3 +668,277 @@ describe("managed CES integration (real Unix socket)", () => {
629
668
  controller.abort();
630
669
  });
631
670
  });
671
+
672
+ // ---------------------------------------------------------------------------
673
+ // Credential CRUD RPC tests
674
+ // ---------------------------------------------------------------------------
675
+
676
+ describe("credential CRUD RPC", () => {
677
+ /**
678
+ * Helper: set up a Unix socket server with credential CRUD handlers,
679
+ * connect a client, and complete the handshake. Returns the client
680
+ * socket and a serve promise for cleanup.
681
+ */
682
+ async function setupCredentialRpc(): Promise<{
683
+ clientSock: Socket;
684
+ servePromise: Promise<void>;
685
+ }> {
686
+ savedEnv = saveEnv();
687
+ tmpDir = mkdtempSync(join(tmpdir(), "ces-cred-integ-"));
688
+ const dataDir = join(tmpDir, "ces-data");
689
+ const securityDir = join(tmpDir, "security");
690
+ const socketDir = join(tmpDir, "bootstrap");
691
+ const socketPath = join(socketDir, "ces.sock");
692
+ mkdirSync(dataDir, { recursive: true });
693
+ mkdirSync(securityDir, { recursive: true });
694
+ mkdirSync(socketDir, { recursive: true });
695
+
696
+ process.env["CES_DATA_DIR"] = dataDir;
697
+ process.env["CES_MODE"] = "managed";
698
+ process.env["CREDENTIAL_SECURITY_DIR"] = securityDir;
699
+
700
+ controller = new AbortController();
701
+
702
+ const connectionPromise = acceptOneConnection(socketPath, controller.signal);
703
+ clientSocket = await connectToSocket(socketPath);
704
+ const conn = await connectionPromise;
705
+
706
+ // vellumRoot is unused when CREDENTIAL_SECURITY_DIR is set,
707
+ // but we pass dataDir for consistency with the backend API.
708
+ const handlers = buildCredentialHandlers(dataDir);
709
+
710
+ serverRpcServer = new CesRpcServer({
711
+ input: conn.readable,
712
+ output: conn.writable,
713
+ handlers,
714
+ logger: { log: () => {}, warn: () => {}, error: () => {} },
715
+ signal: controller.signal,
716
+ });
717
+
718
+ const servePromise = serverRpcServer.serve();
719
+
720
+ // Complete the handshake
721
+ sendMessage(clientSocket, {
722
+ type: "handshake_request",
723
+ protocolVersion: CES_PROTOCOL_VERSION,
724
+ sessionId: `cred-integ-${Date.now()}`,
725
+ });
726
+ const hsMessages = await readMessages(clientSocket, 1);
727
+ const ack = hsMessages[0] as HandshakeAck;
728
+ expect(ack.type).toBe("handshake_ack");
729
+ expect(ack.accepted).toBe(true);
730
+
731
+ return { clientSock: clientSocket, servePromise };
732
+ }
733
+
734
+ test("set + get round-trip", async () => {
735
+ const { clientSock, servePromise } = await setupCredentialRpc();
736
+
737
+ // Set credential
738
+ sendMessage(clientSock, {
739
+ type: "rpc",
740
+ id: "cred-set-1",
741
+ kind: "request",
742
+ method: CesRpcMethod.SetCredential,
743
+ payload: { account: "test-key", value: "secret-value" },
744
+ timestamp: new Date().toISOString(),
745
+ });
746
+
747
+ const setMessages = await readMessages(clientSock, 1);
748
+ expect(setMessages.length).toBe(1);
749
+ const setResp = setMessages[0] as RpcEnvelope & { type: "rpc" };
750
+ expect(setResp.id).toBe("cred-set-1");
751
+ expect(setResp.kind).toBe("response");
752
+ expect(setResp.method).toBe(CesRpcMethod.SetCredential);
753
+ const setPayload = setResp.payload as SetCredentialResponse;
754
+ expect(setPayload.ok).toBe(true);
755
+
756
+ // Get credential
757
+ sendMessage(clientSock, {
758
+ type: "rpc",
759
+ id: "cred-get-1",
760
+ kind: "request",
761
+ method: CesRpcMethod.GetCredential,
762
+ payload: { account: "test-key" },
763
+ timestamp: new Date().toISOString(),
764
+ });
765
+
766
+ const getMessages = await readMessages(clientSock, 1);
767
+ expect(getMessages.length).toBe(1);
768
+ const getResp = getMessages[0] as RpcEnvelope & { type: "rpc" };
769
+ expect(getResp.id).toBe("cred-get-1");
770
+ expect(getResp.kind).toBe("response");
771
+ expect(getResp.method).toBe(CesRpcMethod.GetCredential);
772
+ const getPayload = getResp.payload as GetCredentialResponse;
773
+ expect(getPayload).toEqual({ found: true, value: "secret-value" });
774
+
775
+ clientSock.end();
776
+ controller.abort();
777
+ await servePromise;
778
+ });
779
+
780
+ test("list includes set key", async () => {
781
+ const { clientSock, servePromise } = await setupCredentialRpc();
782
+
783
+ // Set a credential first
784
+ sendMessage(clientSock, {
785
+ type: "rpc",
786
+ id: "cred-set-2",
787
+ kind: "request",
788
+ method: CesRpcMethod.SetCredential,
789
+ payload: { account: "test-key", value: "secret-value" },
790
+ timestamp: new Date().toISOString(),
791
+ });
792
+ await readMessages(clientSock, 1);
793
+
794
+ // List credentials
795
+ sendMessage(clientSock, {
796
+ type: "rpc",
797
+ id: "cred-list-1",
798
+ kind: "request",
799
+ method: CesRpcMethod.ListCredentials,
800
+ payload: {},
801
+ timestamp: new Date().toISOString(),
802
+ });
803
+
804
+ const listMessages = await readMessages(clientSock, 1);
805
+ expect(listMessages.length).toBe(1);
806
+ const listResp = listMessages[0] as RpcEnvelope & { type: "rpc" };
807
+ expect(listResp.id).toBe("cred-list-1");
808
+ expect(listResp.kind).toBe("response");
809
+ expect(listResp.method).toBe(CesRpcMethod.ListCredentials);
810
+ const listPayload = listResp.payload as ListCredentialsResponse;
811
+ expect(listPayload.accounts).toContain("test-key");
812
+
813
+ clientSock.end();
814
+ controller.abort();
815
+ await servePromise;
816
+ });
817
+
818
+ test("delete credential", async () => {
819
+ const { clientSock, servePromise } = await setupCredentialRpc();
820
+
821
+ // Set a credential first
822
+ sendMessage(clientSock, {
823
+ type: "rpc",
824
+ id: "cred-set-3",
825
+ kind: "request",
826
+ method: CesRpcMethod.SetCredential,
827
+ payload: { account: "test-key", value: "secret-value" },
828
+ timestamp: new Date().toISOString(),
829
+ });
830
+ await readMessages(clientSock, 1);
831
+
832
+ // Delete credential
833
+ sendMessage(clientSock, {
834
+ type: "rpc",
835
+ id: "cred-del-1",
836
+ kind: "request",
837
+ method: CesRpcMethod.DeleteCredential,
838
+ payload: { account: "test-key" },
839
+ timestamp: new Date().toISOString(),
840
+ });
841
+
842
+ const delMessages = await readMessages(clientSock, 1);
843
+ expect(delMessages.length).toBe(1);
844
+ const delResp = delMessages[0] as RpcEnvelope & { type: "rpc" };
845
+ expect(delResp.id).toBe("cred-del-1");
846
+ expect(delResp.kind).toBe("response");
847
+ expect(delResp.method).toBe(CesRpcMethod.DeleteCredential);
848
+ const delPayload = delResp.payload as DeleteCredentialResponse;
849
+ expect(delPayload).toEqual({ result: "deleted" });
850
+
851
+ clientSock.end();
852
+ controller.abort();
853
+ await servePromise;
854
+ });
855
+
856
+ test("get after delete returns not found", async () => {
857
+ const { clientSock, servePromise } = await setupCredentialRpc();
858
+
859
+ // Set a credential
860
+ sendMessage(clientSock, {
861
+ type: "rpc",
862
+ id: "cred-set-4",
863
+ kind: "request",
864
+ method: CesRpcMethod.SetCredential,
865
+ payload: { account: "test-key", value: "secret-value" },
866
+ timestamp: new Date().toISOString(),
867
+ });
868
+ await readMessages(clientSock, 1);
869
+
870
+ // Delete it
871
+ sendMessage(clientSock, {
872
+ type: "rpc",
873
+ id: "cred-del-2",
874
+ kind: "request",
875
+ method: CesRpcMethod.DeleteCredential,
876
+ payload: { account: "test-key" },
877
+ timestamp: new Date().toISOString(),
878
+ });
879
+ await readMessages(clientSock, 1);
880
+
881
+ // Get after delete
882
+ sendMessage(clientSock, {
883
+ type: "rpc",
884
+ id: "cred-get-2",
885
+ kind: "request",
886
+ method: CesRpcMethod.GetCredential,
887
+ payload: { account: "test-key" },
888
+ timestamp: new Date().toISOString(),
889
+ });
890
+
891
+ const getMessages = await readMessages(clientSock, 1);
892
+ expect(getMessages.length).toBe(1);
893
+ const getResp = getMessages[0] as RpcEnvelope & { type: "rpc" };
894
+ expect(getResp.id).toBe("cred-get-2");
895
+ expect(getResp.kind).toBe("response");
896
+ expect(getResp.method).toBe(CesRpcMethod.GetCredential);
897
+ const getPayload = getResp.payload as GetCredentialResponse;
898
+ expect(getPayload).toEqual({ found: false });
899
+
900
+ clientSock.end();
901
+ controller.abort();
902
+ await servePromise;
903
+ });
904
+
905
+ test("delete non-existent credential returns not-found", async () => {
906
+ const { clientSock, servePromise } = await setupCredentialRpc();
907
+
908
+ // Set a credential first to initialize the store file on disk.
909
+ // Without a store file, delete returns "error" (no store) rather
910
+ // than "not-found" (store exists, key absent).
911
+ sendMessage(clientSock, {
912
+ type: "rpc",
913
+ id: "cred-set-init",
914
+ kind: "request",
915
+ method: CesRpcMethod.SetCredential,
916
+ payload: { account: "init-key", value: "init-value" },
917
+ timestamp: new Date().toISOString(),
918
+ });
919
+ await readMessages(clientSock, 1);
920
+
921
+ // Delete a credential that was never set
922
+ sendMessage(clientSock, {
923
+ type: "rpc",
924
+ id: "cred-del-3",
925
+ kind: "request",
926
+ method: CesRpcMethod.DeleteCredential,
927
+ payload: { account: "nonexistent" },
928
+ timestamp: new Date().toISOString(),
929
+ });
930
+
931
+ const delMessages = await readMessages(clientSock, 1);
932
+ expect(delMessages.length).toBe(1);
933
+ const delResp = delMessages[0] as RpcEnvelope & { type: "rpc" };
934
+ expect(delResp.id).toBe("cred-del-3");
935
+ expect(delResp.kind).toBe("response");
936
+ expect(delResp.method).toBe(CesRpcMethod.DeleteCredential);
937
+ const delPayload = delResp.payload as DeleteCredentialResponse;
938
+ expect(delPayload).toEqual({ result: "not-found" });
939
+
940
+ clientSock.end();
941
+ controller.abort();
942
+ await servePromise;
943
+ });
944
+ });
@@ -57,6 +57,7 @@ import { materializeManagedToken } from "./materializers/managed-platform.js";
57
57
  import { HandleType, parseHandle } from "@vellumai/ces-contracts";
58
58
  import { buildLazyGetters, type ApiKeyRef } from "./managed-lazy-getters.js";
59
59
  import { MANAGED_LOCAL_STATIC_REJECTION_ERROR } from "./managed-errors.js";
60
+ import type { SecureKeyBackend } from "@vellumai/credential-storage";
60
61
  import { createLocalSecureKeyBackend } from "./materializers/local-secure-key-backend.js";
61
62
  import { handleCredentialRoute, type CredentialRouteDeps } from "./http/credential-routes.js";
62
63
 
@@ -90,7 +91,7 @@ function ensureDataDirs(): void {
90
91
  // Build RPC handler registry (managed mode)
91
92
  // ---------------------------------------------------------------------------
92
93
 
93
- function buildHandlers(sessionIdRef: SessionIdRef, apiKeyRef: ApiKeyRef): RpcHandlerRegistry {
94
+ function buildHandlers(sessionIdRef: SessionIdRef, apiKeyRef: ApiKeyRef, secureKeyBackend: SecureKeyBackend): RpcHandlerRegistry {
94
95
  // -- Grant stores ----------------------------------------------------------
95
96
  const persistentGrantStore = new PersistentGrantStore(
96
97
  getCesGrantsDir("managed"),
@@ -129,10 +130,9 @@ function buildHandlers(sessionIdRef: SessionIdRef, apiKeyRef: ApiKeyRef): RpcHan
129
130
  }
130
131
 
131
132
  // -- Workspace root for command execution cwd ------------------------------
132
- // Prefer WORKSPACE_DIR when set (new Docker layout mounts workspace at
133
- // /workspace). Fall back to the legacy path derived from the assistant
134
- // data mount for backwards compatibility.
135
- const defaultWorkspaceDir = process.env["WORKSPACE_DIR"] ?? (() => {
133
+ // Use VELLUM_WORKSPACE_DIR when set, otherwise fall back to the legacy
134
+ // path derived from the assistant data mount.
135
+ const defaultWorkspaceDir = process.env["VELLUM_WORKSPACE_DIR"] ?? (() => {
136
136
  const assistantDataMount =
137
137
  process.env["CES_ASSISTANT_DATA_MOUNT"] ?? "/assistant-data-ro";
138
138
  return join(join(assistantDataMount, ".vellum"), "workspace");
@@ -322,6 +322,27 @@ function buildHandlers(sessionIdRef: SessionIdRef, apiKeyRef: ApiKeyRef): RpcHan
322
322
  auditStore,
323
323
  }) as typeof handlers[string];
324
324
 
325
+ // Register credential CRUD handlers
326
+ handlers[CesRpcMethod.GetCredential] = (async (req: { account: string }) => {
327
+ const value = await secureKeyBackend.get(req.account);
328
+ return { found: value !== undefined, value };
329
+ }) as typeof handlers[string];
330
+
331
+ handlers[CesRpcMethod.SetCredential] = (async (req: { account: string; value: string }) => {
332
+ const ok = await secureKeyBackend.set(req.account, req.value);
333
+ return { ok };
334
+ }) as typeof handlers[string];
335
+
336
+ handlers[CesRpcMethod.DeleteCredential] = (async (req: { account: string }) => {
337
+ const result = await secureKeyBackend.delete(req.account);
338
+ return { result };
339
+ }) as typeof handlers[string];
340
+
341
+ handlers[CesRpcMethod.ListCredentials] = (async () => {
342
+ const accounts = await secureKeyBackend.list();
343
+ return { accounts };
344
+ }) as typeof handlers[string];
345
+
325
346
  return handlers;
326
347
  }
327
348
 
@@ -498,6 +519,14 @@ async function main(): Promise<void> {
498
519
  process.on("SIGTERM", shutdown);
499
520
  process.on("SIGINT", shutdown);
500
521
 
522
+ // Create the secure key backend unconditionally — it's needed by both
523
+ // HTTP credential routes (when CES_SERVICE_TOKEN is set) and RPC
524
+ // credential CRUD handlers (always available).
525
+ const assistantDataMount =
526
+ process.env["CES_ASSISTANT_DATA_MOUNT"] ?? "/assistant-data-ro";
527
+ const vellumRoot = join(assistantDataMount, ".vellum");
528
+ const secureKeyBackend = createLocalSecureKeyBackend(vellumRoot);
529
+
501
530
  // Set up credential CRUD routes if a service token is configured.
502
531
  // The assistant and gateway use CES_SERVICE_TOKEN to authenticate
503
532
  // credential management requests over HTTP.
@@ -505,11 +534,7 @@ async function main(): Promise<void> {
505
534
  let credentialDeps: CredentialRouteDeps | null = null;
506
535
 
507
536
  if (serviceToken) {
508
- const assistantDataMount =
509
- process.env["CES_ASSISTANT_DATA_MOUNT"] ?? "/assistant-data-ro";
510
- const vellumRoot = join(assistantDataMount, ".vellum");
511
- const backend = createLocalSecureKeyBackend(vellumRoot);
512
- credentialDeps = { backend, serviceToken };
537
+ credentialDeps = { backend: secureKeyBackend, serviceToken };
513
538
  log("Credential CRUD routes enabled (CES_SERVICE_TOKEN configured)");
514
539
  } else {
515
540
  warn(
@@ -545,7 +570,7 @@ async function main(): Promise<void> {
545
570
  // are available to handlers at call time (after the handshake completes).
546
571
  const sessionIdRef: SessionIdRef = { current: `ces-managed-${Date.now()}` };
547
572
  const apiKeyRef: ApiKeyRef = { current: "" };
548
- const handlers = buildHandlers(sessionIdRef, apiKeyRef);
573
+ const handlers = buildHandlers(sessionIdRef, apiKeyRef, secureKeyBackend);
549
574
 
550
575
  const server = new CesRpcServer({
551
576
  input: connection.readable,