@vellumai/credential-executor 0.4.55

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 (42) hide show
  1. package/Dockerfile +55 -0
  2. package/bun.lock +37 -0
  3. package/package.json +32 -0
  4. package/src/__tests__/command-executor.test.ts +1333 -0
  5. package/src/__tests__/command-validator.test.ts +708 -0
  6. package/src/__tests__/command-workspace.test.ts +997 -0
  7. package/src/__tests__/grant-store.test.ts +467 -0
  8. package/src/__tests__/http-executor.test.ts +1251 -0
  9. package/src/__tests__/http-policy.test.ts +970 -0
  10. package/src/__tests__/local-materializers.test.ts +826 -0
  11. package/src/__tests__/managed-materializers.test.ts +961 -0
  12. package/src/__tests__/toolstore.test.ts +539 -0
  13. package/src/__tests__/transport.test.ts +388 -0
  14. package/src/audit/store.ts +188 -0
  15. package/src/commands/auth-adapters.ts +169 -0
  16. package/src/commands/executor.ts +840 -0
  17. package/src/commands/output-scan.ts +157 -0
  18. package/src/commands/profiles.ts +282 -0
  19. package/src/commands/validator.ts +438 -0
  20. package/src/commands/workspace.ts +512 -0
  21. package/src/grants/index.ts +17 -0
  22. package/src/grants/persistent-store.ts +247 -0
  23. package/src/grants/rpc-handlers.ts +269 -0
  24. package/src/grants/temporary-store.ts +219 -0
  25. package/src/http/audit.ts +84 -0
  26. package/src/http/executor.ts +540 -0
  27. package/src/http/path-template.ts +179 -0
  28. package/src/http/policy.ts +256 -0
  29. package/src/http/response-filter.ts +233 -0
  30. package/src/index.ts +106 -0
  31. package/src/main.ts +263 -0
  32. package/src/managed-main.ts +420 -0
  33. package/src/materializers/local.ts +300 -0
  34. package/src/materializers/managed-platform.ts +270 -0
  35. package/src/paths.ts +137 -0
  36. package/src/server.ts +636 -0
  37. package/src/subjects/local.ts +177 -0
  38. package/src/subjects/managed.ts +290 -0
  39. package/src/toolstore/integrity.ts +94 -0
  40. package/src/toolstore/manifest.ts +154 -0
  41. package/src/toolstore/publish.ts +342 -0
  42. package/tsconfig.json +20 -0
@@ -0,0 +1,420 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Managed CES entrypoint.
4
+ *
5
+ * In managed (sidecar) mode the CES container:
6
+ *
7
+ * 1. Ensures the CES-private data directories exist.
8
+ * 2. Binds a bootstrap Unix socket on the shared bootstrap volume.
9
+ * 3. Accepts exactly **one** assistant runtime connection.
10
+ * 4. Unlinks the socket path immediately after the connection is accepted,
11
+ * preventing any second process from connecting.
12
+ * 5. Serves RPC on the accepted stream only.
13
+ * 6. Simultaneously serves health probes (`/healthz`, `/readyz`) on a
14
+ * dedicated HTTP port for Kubernetes liveness/readiness checks.
15
+ *
16
+ * The managed entrypoint never opens a generic TCP or HTTP command API.
17
+ * All RPC traffic flows exclusively over the accepted Unix socket stream.
18
+ */
19
+
20
+ import { mkdirSync, unlinkSync } from "node:fs";
21
+ import { createServer as createNetServer, type Socket } from "node:net";
22
+ import { dirname } from "node:path";
23
+ import { Readable, Writable } from "node:stream";
24
+
25
+ import { CES_PROTOCOL_VERSION, CesRpcMethod } from "@vellumai/ces-contracts";
26
+
27
+ import { AuditStore } from "./audit/store.js";
28
+ import { PersistentGrantStore } from "./grants/persistent-store.js";
29
+ import {
30
+ createListAuditRecordsHandler,
31
+ createListGrantsHandler,
32
+ createRecordGrantHandler,
33
+ createRevokeGrantHandler,
34
+ } from "./grants/rpc-handlers.js";
35
+ import { TemporaryGrantStore } from "./grants/temporary-store.js";
36
+ import {
37
+ getBootstrapSocketPath,
38
+ getCesAuditDir,
39
+ getCesDataRoot,
40
+ getCesGrantsDir,
41
+ getCesToolStoreDir,
42
+ getHealthPort,
43
+ } from "./paths.js";
44
+ import {
45
+ buildHandlersWithHttp,
46
+ CesRpcServer,
47
+ registerCommandExecutionHandler,
48
+ registerManageSecureCommandToolHandler,
49
+ type RpcHandlerRegistry,
50
+ } from "./server.js";
51
+ import { publishBundle } from "./toolstore/publish.js";
52
+ import type { ManagedSubjectResolverOptions } from "./subjects/managed.js";
53
+ import type { ManagedMaterializerOptions } from "./materializers/managed-platform.js";
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Logging (managed always logs to stderr)
57
+ // ---------------------------------------------------------------------------
58
+
59
+ const log = (msg: string) =>
60
+ process.stderr.write(`[ces-managed] ${msg}\n`);
61
+
62
+ const warn = (msg: string) =>
63
+ process.stderr.write(`[ces-managed] WARN: ${msg}\n`);
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Data directory bootstrap
67
+ // ---------------------------------------------------------------------------
68
+
69
+ function ensureDataDirs(): void {
70
+ const dirs = [
71
+ getCesDataRoot("managed"),
72
+ getCesGrantsDir("managed"),
73
+ getCesAuditDir("managed"),
74
+ getCesToolStoreDir("managed"),
75
+ ];
76
+ for (const dir of dirs) {
77
+ mkdirSync(dir, { recursive: true });
78
+ }
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Build RPC handler registry (managed mode)
83
+ // ---------------------------------------------------------------------------
84
+
85
+ function buildHandlers(sessionId: string): RpcHandlerRegistry {
86
+ // -- Grant stores ----------------------------------------------------------
87
+ const persistentGrantStore = new PersistentGrantStore(
88
+ getCesGrantsDir("managed"),
89
+ );
90
+ persistentGrantStore.init();
91
+
92
+ const temporaryGrantStore = new TemporaryGrantStore();
93
+
94
+ // -- Audit store -----------------------------------------------------------
95
+ const auditStore = new AuditStore(getCesAuditDir("managed"));
96
+ auditStore.init();
97
+
98
+ // -- Managed credential options --------------------------------------------
99
+ // In managed mode, credentials are obtained from the platform via its
100
+ // token-materialization endpoint. The platform URL and API key are provided
101
+ // through environment variables set by the orchestration layer.
102
+ const platformBaseUrl = process.env["PLATFORM_BASE_URL"] ?? "";
103
+ const assistantApiKey = process.env["ASSISTANT_API_KEY"] ?? "";
104
+
105
+ const managedSubjectOptions: ManagedSubjectResolverOptions | undefined =
106
+ platformBaseUrl && assistantApiKey
107
+ ? { platformBaseUrl, assistantApiKey }
108
+ : undefined;
109
+
110
+ const managedMaterializerOptions: ManagedMaterializerOptions | undefined =
111
+ platformBaseUrl && assistantApiKey
112
+ ? { platformBaseUrl, assistantApiKey }
113
+ : undefined;
114
+
115
+ if (!managedSubjectOptions) {
116
+ warn(
117
+ "PLATFORM_BASE_URL and/or ASSISTANT_API_KEY not set. " +
118
+ "Managed credential materialisation will not be available.",
119
+ );
120
+ }
121
+
122
+ // -- Build handler registry ------------------------------------------------
123
+
124
+ // In managed mode there is no local secure-key backend. The HTTP handler
125
+ // uses managed subject resolution and managed materialisation instead.
126
+ // The localMaterialiser and localSubjectDeps are required by HttpExecutorDeps
127
+ // but will only be reached for local_static/local_oauth handles (which are
128
+ // not expected in managed deployments). We stub them to fail closed.
129
+ const stubLocalMaterialiser = {
130
+ async materialise() {
131
+ return {
132
+ ok: false as const,
133
+ error: "Local credential materialisation is not available in managed mode.",
134
+ };
135
+ },
136
+ reset() {},
137
+ };
138
+
139
+ const handlers = buildHandlersWithHttp(
140
+ {
141
+ persistentGrantStore,
142
+ temporaryGrantStore,
143
+ localMaterialiser: stubLocalMaterialiser as any,
144
+ localSubjectDeps: {
145
+ metadataStore: { getByServiceField: () => undefined } as any,
146
+ oauthConnections: { getById: () => undefined },
147
+ },
148
+ managedSubjectOptions,
149
+ managedMaterializerOptions,
150
+ sessionId,
151
+ },
152
+ );
153
+
154
+ // Register run_authenticated_command handler
155
+ registerCommandExecutionHandler(handlers, {
156
+ executorDeps: {
157
+ persistentStore: persistentGrantStore,
158
+ temporaryStore: temporaryGrantStore,
159
+ materializeCredential: async (_handle) => ({
160
+ ok: false as const,
161
+ error:
162
+ "Command credential materialisation in managed mode is not yet available.",
163
+ }),
164
+ cesMode: "managed",
165
+ },
166
+ defaultWorkspaceDir: "/workspace",
167
+ });
168
+
169
+ // Register manage_secure_command_tool handler
170
+ const toolRegistry = new Map<string, { toolName: string; credentialHandle: string; description: string; bundleDigest: string }>();
171
+
172
+ registerManageSecureCommandToolHandler(handlers, {
173
+ downloadBundle: async (sourceUrl: string) => {
174
+ const resp = await fetch(sourceUrl);
175
+ if (!resp.ok) {
176
+ throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
177
+ }
178
+ return Buffer.from(await resp.arrayBuffer());
179
+ },
180
+ publishBundle: (request) => publishBundle({ ...request, cesMode: "managed" }),
181
+ unregisterTool: (toolName: string) => {
182
+ return toolRegistry.delete(toolName);
183
+ },
184
+ registerTool: (entry) => {
185
+ toolRegistry.set(entry.toolName, entry);
186
+ },
187
+ });
188
+
189
+ // Register grant management handlers
190
+ handlers[CesRpcMethod.RecordGrant] = createRecordGrantHandler({
191
+ persistentGrantStore,
192
+ temporaryGrantStore,
193
+ }) as typeof handlers[string];
194
+
195
+ handlers[CesRpcMethod.ListGrants] = createListGrantsHandler({
196
+ persistentGrantStore,
197
+ sessionId,
198
+ }) as typeof handlers[string];
199
+
200
+ handlers[CesRpcMethod.RevokeGrant] = createRevokeGrantHandler({
201
+ persistentGrantStore,
202
+ }) as typeof handlers[string];
203
+
204
+ // Register audit record handler
205
+ handlers[CesRpcMethod.ListAuditRecords] = createListAuditRecordsHandler({
206
+ auditStore,
207
+ }) as typeof handlers[string];
208
+
209
+ return handlers;
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Health server
214
+ // ---------------------------------------------------------------------------
215
+
216
+ let rpcConnected = false;
217
+
218
+ function startHealthServer(port: number, signal: AbortSignal): ReturnType<typeof Bun.serve> {
219
+ const server = Bun.serve({
220
+ port,
221
+ fetch(req) {
222
+ const url = new URL(req.url);
223
+ if (url.pathname === "/healthz") {
224
+ return new Response(
225
+ JSON.stringify({ status: "ok", version: CES_PROTOCOL_VERSION }),
226
+ { headers: { "Content-Type": "application/json" } },
227
+ );
228
+ }
229
+ if (url.pathname === "/readyz") {
230
+ const ready = rpcConnected;
231
+ return new Response(
232
+ JSON.stringify({ ready, version: CES_PROTOCOL_VERSION }),
233
+ {
234
+ status: ready ? 200 : 503,
235
+ headers: { "Content-Type": "application/json" },
236
+ },
237
+ );
238
+ }
239
+ return new Response("Not Found", { status: 404 });
240
+ },
241
+ });
242
+
243
+ signal.addEventListener("abort", () => {
244
+ server.stop(true);
245
+ }, { once: true });
246
+
247
+ return server;
248
+ }
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // Bootstrap socket server (accepts exactly one connection)
252
+ // ---------------------------------------------------------------------------
253
+
254
+ /**
255
+ * Listen on a Unix socket, accept exactly one connection, unlink the
256
+ * socket path, and return readable/writable streams for the accepted
257
+ * connection.
258
+ */
259
+ function acceptOneConnection(
260
+ socketPath: string,
261
+ signal: AbortSignal,
262
+ ): Promise<{ readable: Readable; writable: Writable; socket: Socket }> {
263
+ return new Promise((resolve, reject) => {
264
+ // Ensure the socket directory exists
265
+ mkdirSync(dirname(socketPath), { recursive: true });
266
+
267
+ // Clean up any stale socket file
268
+ try {
269
+ unlinkSync(socketPath);
270
+ } catch {
271
+ // Ignore — file may not exist
272
+ }
273
+
274
+ const netServer = createNetServer();
275
+
276
+ const cleanup = () => {
277
+ netServer.close();
278
+ try {
279
+ unlinkSync(socketPath);
280
+ } catch {
281
+ // Already unlinked or never created
282
+ }
283
+ };
284
+
285
+ if (signal.aborted) {
286
+ reject(new Error("Aborted before listening"));
287
+ return;
288
+ }
289
+
290
+ signal.addEventListener("abort", () => {
291
+ cleanup();
292
+ reject(new Error("Aborted while waiting for connection"));
293
+ }, { once: true });
294
+
295
+ netServer.on("error", (err) => {
296
+ cleanup();
297
+ reject(err);
298
+ });
299
+
300
+ netServer.listen(socketPath, () => {
301
+ log(`Bootstrap socket listening at ${socketPath}`);
302
+ });
303
+
304
+ netServer.on("connection", (socket: Socket) => {
305
+ // Accept exactly one connection, then close the listener and
306
+ // unlink the socket path so no other process can connect.
307
+ log("Assistant connected via bootstrap socket");
308
+ netServer.close();
309
+ try {
310
+ unlinkSync(socketPath);
311
+ } catch {
312
+ // Already unlinked
313
+ }
314
+ log("Bootstrap socket unlinked (single-connection enforced)");
315
+
316
+ const readable = new Readable({
317
+ read() {
318
+ // Data is pushed externally
319
+ },
320
+ });
321
+
322
+ const writable = new Writable({
323
+ write(chunk, _encoding, callback) {
324
+ if (socket.writable) {
325
+ socket.write(chunk, callback);
326
+ } else {
327
+ callback(new Error("Socket no longer writable"));
328
+ }
329
+ },
330
+ });
331
+
332
+ socket.on("data", (chunk) => {
333
+ readable.push(chunk);
334
+ });
335
+
336
+ socket.on("end", () => {
337
+ readable.push(null);
338
+ });
339
+
340
+ socket.on("error", (err) => {
341
+ readable.destroy(err);
342
+ writable.destroy(err);
343
+ });
344
+
345
+ resolve({ readable, writable, socket });
346
+ });
347
+ });
348
+ }
349
+
350
+ // ---------------------------------------------------------------------------
351
+ // Main
352
+ // ---------------------------------------------------------------------------
353
+
354
+ async function main(): Promise<void> {
355
+ ensureDataDirs();
356
+
357
+ log(`Starting CES v${CES_PROTOCOL_VERSION} (managed mode)`);
358
+
359
+ const controller = new AbortController();
360
+
361
+ // Graceful shutdown
362
+ const shutdown = () => {
363
+ log("Shutting down...");
364
+ controller.abort();
365
+ };
366
+ process.on("SIGTERM", shutdown);
367
+ process.on("SIGINT", shutdown);
368
+
369
+ // Start health server on dedicated port
370
+ const healthPort = getHealthPort();
371
+ const healthServer = startHealthServer(healthPort, controller.signal);
372
+ log(`Health server listening on port ${healthPort}`);
373
+
374
+ // Wait for exactly one assistant connection on the bootstrap socket
375
+ const socketPath = getBootstrapSocketPath();
376
+ log(`Waiting for assistant connection on ${socketPath}...`);
377
+
378
+ let connection: Awaited<ReturnType<typeof acceptOneConnection>>;
379
+ try {
380
+ connection = await acceptOneConnection(socketPath, controller.signal);
381
+ } catch (err) {
382
+ if (controller.signal.aborted) {
383
+ log("Shutdown before assistant connected.");
384
+ return;
385
+ }
386
+ throw err;
387
+ }
388
+
389
+ rpcConnected = true;
390
+
391
+ // Build the handler registry with all available RPC implementations
392
+ const sessionId = `ces-managed-${Date.now()}`;
393
+ const handlers = buildHandlers(sessionId);
394
+
395
+ const server = new CesRpcServer({
396
+ input: connection.readable,
397
+ output: connection.writable,
398
+ handlers,
399
+ logger: {
400
+ log: (msg: string, ...args: unknown[]) =>
401
+ process.stderr.write(`[ces-managed] ${msg} ${args.map(String).join(" ")}\n`),
402
+ warn: (msg: string, ...args: unknown[]) =>
403
+ process.stderr.write(`[ces-managed] WARN: ${msg} ${args.map(String).join(" ")}\n`),
404
+ error: (msg: string, ...args: unknown[]) =>
405
+ process.stderr.write(`[ces-managed] ERROR: ${msg} ${args.map(String).join(" ")}\n`),
406
+ },
407
+ signal: controller.signal,
408
+ });
409
+
410
+ await server.serve();
411
+
412
+ rpcConnected = false;
413
+ log("RPC session ended. Shutting down...");
414
+ controller.abort();
415
+ }
416
+
417
+ main().catch((err) => {
418
+ process.stderr.write(`[ces-managed] Fatal: ${err}\n`);
419
+ process.exit(1);
420
+ });