@vellumai/credential-executor 0.10.7-dev.202607092332.9deedf4 → 0.10.7-dev.202607100313.fffdfea

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 (56) hide show
  1. package/node_modules/@vellumai/service-contracts/package.json +0 -2
  2. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +0 -2
  3. package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +3 -5
  4. package/node_modules/@vellumai/service-contracts/src/index.ts +2 -4
  5. package/node_modules/@vellumai/service-contracts/src/rpc.ts +4 -447
  6. package/package.json +1 -1
  7. package/src/__tests__/managed-integration.test.ts +108 -87
  8. package/src/index.ts +5 -85
  9. package/src/main.ts +79 -549
  10. package/src/paths.ts +0 -15
  11. package/src/server.ts +52 -469
  12. package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +0 -686
  13. package/node_modules/@vellumai/service-contracts/src/grants.ts +0 -184
  14. package/node_modules/@vellumai/service-contracts/src/rendering.ts +0 -135
  15. package/src/__tests__/command-executor.test.ts +0 -1879
  16. package/src/__tests__/command-validator.test.ts +0 -1405
  17. package/src/__tests__/command-workspace.test.ts +0 -1050
  18. package/src/__tests__/grant-store.test.ts +0 -689
  19. package/src/__tests__/http-executor.test.ts +0 -1336
  20. package/src/__tests__/http-policy.test.ts +0 -1069
  21. package/src/__tests__/local-materializers.test.ts +0 -860
  22. package/src/__tests__/local-token-refresh.test.ts +0 -361
  23. package/src/__tests__/manage-secure-command-tool.test.ts +0 -134
  24. package/src/__tests__/managed-lazy-getters.test.ts +0 -359
  25. package/src/__tests__/managed-materializers.test.ts +0 -1028
  26. package/src/__tests__/managed-rejection.test.ts +0 -43
  27. package/src/__tests__/toolstore.test.ts +0 -773
  28. package/src/audit/store.ts +0 -188
  29. package/src/commands/auth-adapters.ts +0 -169
  30. package/src/commands/egress-hooks.ts +0 -203
  31. package/src/commands/executor.ts +0 -1155
  32. package/src/commands/output-scan.ts +0 -157
  33. package/src/commands/profiles.ts +0 -286
  34. package/src/commands/validator.ts +0 -702
  35. package/src/commands/workspace.ts +0 -550
  36. package/src/grants/index.ts +0 -17
  37. package/src/grants/persistent-store.ts +0 -309
  38. package/src/grants/rpc-handlers.ts +0 -293
  39. package/src/grants/temporary-store.ts +0 -289
  40. package/src/http/audit.ts +0 -84
  41. package/src/http/executor.ts +0 -684
  42. package/src/http/path-template.ts +0 -245
  43. package/src/http/policy.ts +0 -238
  44. package/src/http/response-filter.ts +0 -233
  45. package/src/managed-errors.ts +0 -9
  46. package/src/managed-lazy-getters.ts +0 -106
  47. package/src/materializers/local-oauth-lookup.ts +0 -98
  48. package/src/materializers/local-token-refresh.ts +0 -287
  49. package/src/materializers/local.ts +0 -316
  50. package/src/materializers/managed-platform.ts +0 -295
  51. package/src/subjects/local.ts +0 -177
  52. package/src/subjects/managed.ts +0 -311
  53. package/src/subjects/policy.ts +0 -79
  54. package/src/toolstore/integrity.ts +0 -94
  55. package/src/toolstore/manifest.ts +0 -154
  56. package/src/toolstore/publish.ts +0 -571
package/src/main.ts CHANGED
@@ -7,90 +7,40 @@
7
7
  * - `CES_MODE=managed` (set in the Dockerfile / K8s statefulset) → managed mode
8
8
  * - absent or any other value → local mode (bare-metal sibling)
9
9
  *
10
- * Both modes serve RPC over a Unix socket using a concurrent multi-connection
11
- * server (`serveStandaloneSocket`). Each connection gets its own `CesRpcServer`
12
- * over a shared, process-scoped handler registry. The server stays listening
13
- * across connections, so an assistant that disconnects (crash, restart) can
14
- * reconnect without CES re-binding.
10
+ * Both modes serve credential CRUD RPC over a Unix socket using a concurrent
11
+ * multi-connection server (`serveStandaloneSocket`). Each connection gets its
12
+ * own `CesRpcServer` over a shared, process-scoped handler registry. The server
13
+ * stays listening across connections, so an assistant that disconnects (crash,
14
+ * restart) can reconnect without CES re-binding.
15
15
  *
16
16
  * Managed mode additionally starts a health HTTP server (`/healthz`, `/readyz`,
17
- * optional credential CRUD routes) for Kubernetes liveness/readiness probes,
18
- * and uses handshake-provided credential refs (API key, assistant ID) resolved
19
- * lazily via `buildLazyGetters`.
20
- *
21
- * Local mode never opens a TCP listener. The Unix socket's listening fd is not
22
- * inherited by shell subprocesses spawned by CES (e.g. for
23
- * `run_authenticated_command`): Bun's `Bun.spawn` defaults to "pipe" for
24
- * stdio, and the listening socket is not passed to those subprocesses.
17
+ * optional credential CRUD routes) for Kubernetes liveness/readiness probes.
25
18
  */
26
19
 
27
20
  import { mkdirSync, unlinkSync } from "node:fs";
28
21
  import { createServer as createNetServer, type Socket } from "node:net";
29
- import { homedir } from "node:os";
30
22
  import { dirname, join } from "node:path";
31
23
  import { Readable, Writable } from "node:stream";
32
24
 
33
25
  import {
34
26
  CES_PROTOCOL_VERSION,
35
27
  CesRpcMethod,
36
- HandleType,
37
- parseHandle,
38
28
  } from "@vellumai/service-contracts/credential-rpc";
39
- import { StaticCredentialMetadataStore } from "@vellumai/credential-storage";
40
29
  import type { SecureKeyBackend } from "@vellumai/credential-storage";
41
30
 
42
- import { AuditStore } from "./audit/store.js";
43
- import { PersistentGrantStore } from "./grants/persistent-store.js";
44
- import {
45
- createListAuditRecordsHandler,
46
- createListGrantsHandler,
47
- createRecordGrantHandler,
48
- createRevokeGrantHandler,
49
- } from "./grants/rpc-handlers.js";
50
- import { TemporaryGrantStore } from "./grants/temporary-store.js";
51
- import { LocalMaterialiser } from "./materializers/local.js";
52
31
  import { createLocalSecureKeyBackend } from "./materializers/local-secure-key-backend.js";
53
- import { createLocalOAuthLookup } from "./materializers/local-oauth-lookup.js";
54
- import { createLocalTokenRefreshFn } from "./materializers/local-token-refresh.js";
55
- import { resolveLocalSubject } from "./subjects/local.js";
56
- import type { LocalSubjectResolverDeps } from "./subjects/local.js";
57
- import { checkCredentialPolicy } from "./subjects/policy.js";
58
- import { resolveManagedSubject } from "./subjects/managed.js";
59
- import { materializeManagedToken } from "./materializers/managed-platform.js";
60
- import {
61
- applyManagedCredentialRefs,
62
- buildLazyGetters,
63
- type ApiKeyRef,
64
- type AssistantIdRef,
65
- } from "./managed-lazy-getters.js";
66
- import { MANAGED_LOCAL_STATIC_REJECTION_ERROR } from "./managed-errors.js";
67
32
  import { initLogger, getLogger } from "./logger.js";
68
33
  import {
69
34
  getBootstrapSocketPath,
70
- getCesAuditDir,
71
35
  getCesDataRoot,
72
- getCesGrantsDir,
73
36
  getCesLogDir,
74
37
  getCesMode,
75
- getCesToolStoreDir,
76
38
  getHealthPort,
77
39
  getLocalSocketPath,
78
40
  getSecurityDir,
79
41
  type CesMode,
80
42
  } from "./paths.js";
81
- import {
82
- buildHandlersWithHttp,
83
- CesRpcServer,
84
- registerCommandExecutionHandler,
85
- registerManageSecureCommandToolHandler,
86
- type RpcHandlerRegistry,
87
- } from "./server.js";
88
- import {
89
- deleteBundleFromToolstore,
90
- publishBundle,
91
- } from "./toolstore/publish.js";
92
- import { validateSourceUrl } from "./toolstore/manifest.js";
93
- import { buildCesEgressHooks } from "./commands/egress-hooks.js";
43
+ import { CesRpcServer, type RpcHandlerRegistry } from "./server.js";
94
44
  import {
95
45
  handleCredentialRoute,
96
46
  type CredentialRouteDeps,
@@ -110,152 +60,26 @@ const log = getLogger("main");
110
60
  // ---------------------------------------------------------------------------
111
61
 
112
62
  function ensureDataDirs(mode: CesMode): void {
113
- const dirs = [
114
- getCesDataRoot(mode),
115
- getCesGrantsDir(mode),
116
- getCesAuditDir(mode),
117
- getCesToolStoreDir(mode),
118
- ];
119
- for (const dir of dirs) {
120
- mkdirSync(dir, { recursive: true });
121
- }
63
+ mkdirSync(getCesDataRoot(mode), { recursive: true });
122
64
  }
123
65
 
124
66
  // ---------------------------------------------------------------------------
125
- // Path resolution (local-mode helpers)
67
+ // Credential CRUD handlers
126
68
  // ---------------------------------------------------------------------------
127
69
 
128
70
  /**
129
- * Resolve the workspace directory (local mode only).
130
- *
131
- * Priority:
132
- * 1. `VELLUM_WORKSPACE_DIR` env var (set by the CLI when launching the sibling)
133
- * 2. Default: `~/.vellum/workspace`
134
- */
135
- function getWorkspaceDir(): string {
136
- return (
137
- process.env["VELLUM_WORKSPACE_DIR"]?.trim() ||
138
- join(homedir(), ".vellum", "workspace")
139
- );
140
- }
141
-
142
- // ---------------------------------------------------------------------------
143
- // Shared handler registration
144
- // ---------------------------------------------------------------------------
145
-
146
- /**
147
- * Download a secure-command tool bundle from a source URL, enforcing a 100 MB
148
- * size limit. Used by the manage_secure_command_tool handler in both modes.
149
- */
150
- async function downloadBundle(sourceUrl: string): Promise<Buffer> {
151
- const urlError = validateSourceUrl(sourceUrl);
152
- if (urlError) {
153
- throw new Error(urlError);
154
- }
155
- const MAX_BUNDLE_SIZE = 100 * 1024 * 1024; // 100 MB
156
- const resp = await fetch(sourceUrl, {
157
- signal: AbortSignal.timeout(60_000),
158
- });
159
- if (!resp.ok) {
160
- throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
161
- }
162
- const contentLength = resp.headers.get("content-length");
163
- if (contentLength && parseInt(contentLength, 10) > MAX_BUNDLE_SIZE) {
164
- throw new Error(
165
- `Bundle too large: ${contentLength} bytes (max ${MAX_BUNDLE_SIZE})`,
166
- );
167
- }
168
- // Stream the body and enforce the size limit on actual bytes received,
169
- // since Content-Length can be absent (chunked encoding) or lie.
170
- const body = resp.body;
171
- if (!body) {
172
- throw new Error("Response body is null");
173
- }
174
- const chunks: Uint8Array[] = [];
175
- let totalBytes = 0;
176
- for await (const chunk of body) {
177
- totalBytes += chunk.byteLength;
178
- if (totalBytes > MAX_BUNDLE_SIZE) {
179
- throw new Error(
180
- `Bundle too large: received >${MAX_BUNDLE_SIZE} bytes (max ${MAX_BUNDLE_SIZE})`,
181
- );
182
- }
183
- chunks.push(chunk);
184
- }
185
- return Buffer.concat(chunks);
186
- }
187
-
188
- /**
189
- * Register handlers shared between local and managed modes:
190
- * manage_secure_command_tool, grant management, audit records, and
191
- * credential CRUD. These are identical across modes except for the cesMode
192
- * argument passed to publishBundle / deleteBundleFromToolstore.
71
+ * Build the RPC handler registry. CES serves credential CRUD (get / set /
72
+ * delete / list / bulk-set) backed by the secure key store. The
73
+ * `update_managed_credential` handler is registered separately by the RPC
74
+ * server when an `onApiKeyUpdate` callback is supplied. Local and managed modes
75
+ * share the same registry — they differ only in where the secure key backend
76
+ * reads from and whether the health server is started.
193
77
  */
194
- function registerSharedHandlers(
195
- handlers: RpcHandlerRegistry,
196
- opts: {
197
- secureKeyBackend: SecureKeyBackend;
198
- persistentGrantStore: PersistentGrantStore;
199
- temporaryGrantStore: TemporaryGrantStore;
200
- auditStore: AuditStore;
201
- cesMode: CesMode;
202
- },
203
- ): void {
204
- const { secureKeyBackend, persistentGrantStore, temporaryGrantStore, auditStore, cesMode } = opts;
205
-
206
- // -- manage_secure_command_tool -------------------------------------------
207
- const toolRegistry = new Map<
208
- string,
209
- {
210
- toolName: string;
211
- credentialHandle: string;
212
- description: string;
213
- bundleDigest: string;
214
- }
215
- >();
216
-
217
- registerManageSecureCommandToolHandler(handlers, {
218
- downloadBundle,
219
- publishBundle: (request) =>
220
- publishBundle({ ...request, cesMode }),
221
- unregisterTool: (toolName: string) => {
222
- const entry = toolRegistry.get(toolName);
223
- const removed = toolRegistry.delete(toolName);
224
- if (removed && entry?.bundleDigest) {
225
- const stillInUse = Array.from(toolRegistry.values()).some(
226
- (t) => t.bundleDigest === entry.bundleDigest,
227
- );
228
- if (!stillInUse) {
229
- deleteBundleFromToolstore(entry.bundleDigest, cesMode);
230
- }
231
- }
232
- return removed;
233
- },
234
- registerTool: (entry) => {
235
- toolRegistry.set(entry.toolName, entry);
236
- },
237
- });
238
-
239
- // -- Grant management handlers ---------------------------------------------
240
- handlers[CesRpcMethod.RecordGrant] = createRecordGrantHandler({
241
- persistentGrantStore,
242
- temporaryGrantStore,
243
- }) as (typeof handlers)[string];
244
-
245
- handlers[CesRpcMethod.ListGrants] = createListGrantsHandler({
246
- persistentGrantStore,
247
- }) as (typeof handlers)[string];
248
-
249
- handlers[CesRpcMethod.RevokeGrant] = createRevokeGrantHandler({
250
- persistentGrantStore,
251
- }) as (typeof handlers)[string];
252
-
253
- // -- Audit record handler --------------------------------------------------
254
- handlers[CesRpcMethod.ListAuditRecords] = createListAuditRecordsHandler({
255
- auditStore,
256
- }) as (typeof handlers)[string];
78
+ function buildCrudHandlers(
79
+ secureKeyBackend: SecureKeyBackend,
80
+ ): RpcHandlerRegistry {
81
+ const handlers: RpcHandlerRegistry = {};
257
82
 
258
- // -- Credential CRUD handlers ----------------------------------------------
259
83
  handlers[CesRpcMethod.GetCredential] = (async (req: { account: string }) => {
260
84
  const value = await secureKeyBackend.get(req.account);
261
85
  return { found: value !== undefined, value };
@@ -291,284 +115,6 @@ function registerSharedHandlers(
291
115
  }
292
116
  return { results };
293
117
  }) as (typeof handlers)[string];
294
- }
295
-
296
- // ---------------------------------------------------------------------------
297
- // Local-mode handler builder
298
- // ---------------------------------------------------------------------------
299
-
300
- function buildLocalHandlers(secureKeyBackend: SecureKeyBackend): RpcHandlerRegistry {
301
- // -- Grant stores ----------------------------------------------------------
302
- const persistentGrantStore = new PersistentGrantStore(
303
- getCesGrantsDir("local"),
304
- );
305
- persistentGrantStore.init();
306
-
307
- const temporaryGrantStore = new TemporaryGrantStore();
308
-
309
- // -- Audit store -----------------------------------------------------------
310
- const auditStore = new AuditStore(getCesAuditDir("local"));
311
- auditStore.init();
312
-
313
- // -- Credential backend (local) --------------------------------------------
314
- // In local mode CES shares the filesystem with the assistant and can access
315
- // the same credential metadata and secure-key stores.
316
- const workspaceDir = getWorkspaceDir();
317
- const credentialMetadataPath = join(
318
- workspaceDir,
319
- "data",
320
- "credentials",
321
- "metadata.json",
322
- );
323
- const metadataStore = new StaticCredentialMetadataStore(
324
- credentialMetadataPath,
325
- );
326
-
327
- // Read-only OAuth connection lookup backed by the assistant's SQLite
328
- // database. CES opens the database in read-only mode.
329
- const oauthConnections = createLocalOAuthLookup(workspaceDir);
330
-
331
- const localMaterialiser = new LocalMaterialiser({
332
- secureKeyBackend,
333
- tokenRefreshFn: createLocalTokenRefreshFn(workspaceDir, secureKeyBackend),
334
- });
335
-
336
- // -- Build handler registry ------------------------------------------------
337
- const handlers = buildHandlersWithHttp({
338
- persistentGrantStore,
339
- temporaryGrantStore,
340
- localMaterialiser,
341
- localSubjectDeps: {
342
- metadataStore,
343
- oauthConnections,
344
- },
345
- auditStore,
346
- });
347
-
348
- // -- run_authenticated_command (local) ------------------------------------
349
- registerCommandExecutionHandler(handlers, {
350
- executorDeps: {
351
- persistentStore: persistentGrantStore,
352
- temporaryStore: temporaryGrantStore,
353
- materializeCredential: async (handle) => {
354
- const subjectResult = resolveLocalSubject(handle, {
355
- metadataStore,
356
- oauthConnections,
357
- });
358
- if (!subjectResult.ok) {
359
- return { ok: false as const, error: subjectResult.error };
360
- }
361
-
362
- if (subjectResult.subject.type === "local_static") {
363
- const policyCheck = checkCredentialPolicy(
364
- subjectResult.subject.metadata,
365
- "run_authenticated_command",
366
- );
367
- if (!policyCheck.ok) {
368
- return { ok: false as const, error: policyCheck.error! };
369
- }
370
- }
371
-
372
- const matResult = await localMaterialiser.materialise(
373
- subjectResult.subject,
374
- );
375
- if (!matResult.ok) {
376
- return { ok: false as const, error: matResult.error };
377
- }
378
- return {
379
- ok: true as const,
380
- value: matResult.credential.value,
381
- handleType: matResult.credential.handleType,
382
- };
383
- },
384
- auditStore,
385
- cesMode: "local",
386
- egressHooks: buildCesEgressHooks(),
387
- },
388
- defaultWorkspaceDir: workspaceDir,
389
- });
390
-
391
- // -- Shared handlers (grants, audit, CRUD, toolstore) ---------------------
392
- registerSharedHandlers(handlers, {
393
- secureKeyBackend,
394
- persistentGrantStore,
395
- temporaryGrantStore,
396
- auditStore,
397
- cesMode: "local",
398
- });
399
-
400
- return handlers;
401
- }
402
-
403
- // ---------------------------------------------------------------------------
404
- // Managed-mode handler builder
405
- // ---------------------------------------------------------------------------
406
-
407
- function buildManagedHandlers(
408
- apiKeyRef: ApiKeyRef,
409
- assistantIdRef: AssistantIdRef,
410
- secureKeyBackend: SecureKeyBackend,
411
- ): RpcHandlerRegistry {
412
- // -- Grant stores ----------------------------------------------------------
413
- const persistentGrantStore = new PersistentGrantStore(
414
- getCesGrantsDir("managed"),
415
- );
416
- persistentGrantStore.init();
417
-
418
- const temporaryGrantStore = new TemporaryGrantStore();
419
-
420
- // -- Audit store -----------------------------------------------------------
421
- const auditStore = new AuditStore(getCesAuditDir("managed"));
422
- auditStore.init();
423
-
424
- // -- Managed credential options --------------------------------------------
425
- // In managed mode, credentials are obtained from the platform via its
426
- // token-materialization endpoint. The platform URL and assistant ID come
427
- // from environment variables. The API key may come from the env var OR
428
- // from the bootstrap handshake (the assistant forwards it after hatch).
429
- // We use a lazy getter so the handshake-provided key takes effect even
430
- // though handlers are built before the handshake completes.
431
- const platformBaseUrl = process.env["VELLUM_PLATFORM_URL"] ?? "";
432
-
433
- const { getManagedSubjectOptions, getManagedMaterializerOptions } =
434
- buildLazyGetters({
435
- platformBaseUrl,
436
- assistantIdRef,
437
- apiKeyRef,
438
- envApiKey: process.env["ASSISTANT_API_KEY"] || "",
439
- });
440
-
441
- if (!platformBaseUrl) {
442
- log.warn(
443
- "VELLUM_PLATFORM_URL not set. " +
444
- "Managed credential materialisation will depend on the handshake-provided values.",
445
- );
446
- }
447
-
448
- // -- Workspace root for command execution cwd ------------------------------
449
- const defaultWorkspaceDir =
450
- process.env["VELLUM_WORKSPACE_DIR"] ??
451
- (() => {
452
- const assistantDataMount =
453
- process.env["CES_ASSISTANT_DATA_MOUNT"] ?? "/assistant-data-ro";
454
- return join(join(assistantDataMount, ".vellum"), "workspace");
455
- })();
456
-
457
- // -- local_static stubs (not supported in managed mode) -------------------
458
- // v2 stores use a UID-independent `store.key` file that removes the
459
- // technical barrier (legacy v1 stores relied on PBKDF2 key derivation
460
- // from user identity, which broke across container users). The managed-
461
- // mode restriction is now a policy choice: managed deployments use
462
- // platform_oauth handles exclusively for simpler lifecycle and
463
- // centralized token management.
464
- const localMaterialiserStub = {
465
- materialise: async () => ({
466
- ok: false as const,
467
- error: MANAGED_LOCAL_STATIC_REJECTION_ERROR,
468
- }),
469
- };
470
-
471
- const localSubjectDepsStub: LocalSubjectResolverDeps = {
472
- metadataStore: {
473
- getById: () => undefined,
474
- list: () => [],
475
- } as unknown as LocalSubjectResolverDeps["metadataStore"],
476
- oauthConnections: { getById: () => undefined },
477
- };
478
-
479
- // Use a deps obj with getters so the handshake-provided API key
480
- // is resolved lazily at RPC call time (after the handshake completes).
481
- const httpDeps = {
482
- persistentGrantStore,
483
- temporaryGrantStore,
484
- localMaterialiser: localMaterialiserStub as unknown as LocalMaterialiser,
485
- localSubjectDeps: localSubjectDepsStub,
486
- get managedSubjectOptions() {
487
- return getManagedSubjectOptions();
488
- },
489
- get managedMaterializerOptions() {
490
- return getManagedMaterializerOptions();
491
- },
492
- auditStore,
493
- };
494
-
495
- const handlers = buildHandlersWithHttp(httpDeps);
496
-
497
- // -- run_authenticated_command (managed) ----------------------------------
498
- registerCommandExecutionHandler(handlers, {
499
- executorDeps: {
500
- persistentStore: persistentGrantStore,
501
- temporaryStore: temporaryGrantStore,
502
- materializeCredential: async (handle) => {
503
- const parseResult = parseHandle(handle);
504
- if (!parseResult.ok) {
505
- return { ok: false as const, error: parseResult.error };
506
- }
507
-
508
- switch (parseResult.handle.type) {
509
- case HandleType.LocalStatic: {
510
- return {
511
- ok: false as const,
512
- error: MANAGED_LOCAL_STATIC_REJECTION_ERROR,
513
- };
514
- }
515
-
516
- case HandleType.PlatformOAuth: {
517
- const matOpts = getManagedMaterializerOptions();
518
- const subOpts = getManagedSubjectOptions();
519
- if (!matOpts || !subOpts) {
520
- return {
521
- ok: false as const,
522
- error:
523
- "VELLUM_PLATFORM_URL and/or ASSISTANT_API_KEY not set. " +
524
- "Managed credential materialisation is not available.",
525
- };
526
- }
527
-
528
- const subjectResult = await resolveManagedSubject(handle, subOpts);
529
- if (!subjectResult.ok) {
530
- return { ok: false as const, error: subjectResult.error.message };
531
- }
532
-
533
- const matResult = await materializeManagedToken(
534
- subjectResult.subject,
535
- matOpts,
536
- );
537
- if (!matResult.ok) {
538
- return { ok: false as const, error: matResult.error.message };
539
- }
540
-
541
- return {
542
- ok: true as const,
543
- value: matResult.token.accessToken,
544
- handleType: HandleType.PlatformOAuth,
545
- };
546
- }
547
-
548
- default:
549
- return {
550
- ok: false as const,
551
- error:
552
- `Handle type "${parseResult.handle.type}" is not supported in managed mode. ` +
553
- `Supported types: platform_oauth.`,
554
- };
555
- }
556
- },
557
- auditStore,
558
- cesMode: "managed",
559
- egressHooks: buildCesEgressHooks(),
560
- },
561
- defaultWorkspaceDir,
562
- });
563
-
564
- // -- Shared handlers (grants, audit, CRUD, toolstore) ---------------------
565
- registerSharedHandlers(handlers, {
566
- secureKeyBackend,
567
- persistentGrantStore,
568
- temporaryGrantStore,
569
- auditStore,
570
- cesMode: "managed",
571
- });
572
118
 
573
119
  return handlers;
574
120
  }
@@ -591,10 +137,22 @@ function serveStandaloneSocket(opts: {
591
137
  signal: AbortSignal;
592
138
  logger: Pick<Console, "log" | "warn" | "error">;
593
139
  log: ReturnType<typeof getLogger>;
594
- onHandshakeComplete?: (sessionId: string, assistantApiKey?: string, assistantId?: string) => void;
140
+ onHandshakeComplete?: (
141
+ sessionId: string,
142
+ assistantApiKey?: string,
143
+ assistantId?: string,
144
+ ) => void;
595
145
  onApiKeyUpdate?: (assistantApiKey: string, assistantId?: string) => void;
596
146
  }): void {
597
- const { socketPath, handlers, signal, logger, log, onHandshakeComplete, onApiKeyUpdate } = opts;
147
+ const {
148
+ socketPath,
149
+ handlers,
150
+ signal,
151
+ logger,
152
+ log,
153
+ onHandshakeComplete,
154
+ onApiKeyUpdate,
155
+ } = opts;
598
156
 
599
157
  mkdirSync(dirname(socketPath), { recursive: true });
600
158
  try {
@@ -629,23 +187,23 @@ function serveStandaloneSocket(opts: {
629
187
  });
630
188
 
631
189
  const server = new CesRpcServer({
632
- input: readable,
633
- output: writable,
634
- handlers,
635
- logger,
636
- signal,
637
- onHandshakeComplete: (sessionId, apiKey, assistantId) => {
638
- onHandshakeComplete?.(sessionId, apiKey, assistantId);
639
- },
640
- onApiKeyUpdate: onApiKeyUpdate ?? (() => {}),
641
- });
642
- void server.serve().catch((err) => {
190
+ input: readable,
191
+ output: writable,
192
+ handlers,
193
+ logger,
194
+ signal,
195
+ onHandshakeComplete: (sessionId, apiKey, assistantId) => {
196
+ onHandshakeComplete?.(sessionId, apiKey, assistantId);
197
+ },
198
+ onApiKeyUpdate: onApiKeyUpdate ?? (() => {}),
199
+ });
200
+ void server
201
+ .serve()
202
+ .catch((err) => {
643
203
  server.close();
644
- log.warn(
645
- { err },
646
- "CES connection ended with a transport error",
647
- );
648
- }).then(() => {
204
+ log.warn({ err }, "CES connection ended with a transport error");
205
+ })
206
+ .then(() => {
649
207
  connectionCount = Math.max(0, connectionCount - 1);
650
208
  });
651
209
  });
@@ -695,10 +253,17 @@ function startHealthServer(
695
253
  // scheduling during dark-launch. The sidecar can't do useful work
696
254
  // without a connection anyway, so readiness is purely about the
697
255
  // process being up and able to accept a future connection.
698
- return new Response(JSON.stringify({ status: "ok", connections: connectionCount, rpcConnected: connectionCount > 0 }), {
699
- status: 200,
700
- headers: { "Content-Type": "application/json" },
701
- });
256
+ return new Response(
257
+ JSON.stringify({
258
+ status: "ok",
259
+ connections: connectionCount,
260
+ rpcConnected: connectionCount > 0,
261
+ }),
262
+ {
263
+ status: 200,
264
+ headers: { "Content-Type": "application/json" },
265
+ },
266
+ );
702
267
  }
703
268
 
704
269
  // Credential CRUD routes (only if service token is configured)
@@ -742,7 +307,9 @@ async function main(): Promise<void> {
742
307
 
743
308
  initLogger({ dir: getCesLogDir(mode), retentionDays: 30 });
744
309
 
745
- log.info(`Starting CES v${CES_PROTOCOL_VERSION} (${mode} mode, socket transport)`);
310
+ log.info(
311
+ `Starting CES v${CES_PROTOCOL_VERSION} (${mode} mode, socket transport)`,
312
+ );
746
313
 
747
314
  const controller = new AbortController();
748
315
 
@@ -777,20 +344,10 @@ async function main(): Promise<void> {
777
344
  log.info(`CES ${mode} startup: migrations complete`);
778
345
 
779
346
  // -- Build handlers --------------------------------------------------------
780
- // Managed mode needs credential refs for handshake callbacks; local mode
781
- // does not. The refs are process-scoped and persist across reconnects.
782
- // The per-connection session ID lives in each CesRpcServer's
783
- // SessionContext; handlers read it at call time for audit records.
784
- let managedRefs: { apiKeyRef: ApiKeyRef; assistantIdRef: AssistantIdRef } | undefined;
785
- const handlers =
786
- mode === "managed"
787
- ? (() => {
788
- const apiKeyRef: ApiKeyRef = { current: "" };
789
- const assistantIdRef: AssistantIdRef = { current: "" };
790
- managedRefs = { apiKeyRef, assistantIdRef };
791
- return buildManagedHandlers(apiKeyRef, assistantIdRef, secureKeyBackend);
792
- })()
793
- : buildLocalHandlers(secureKeyBackend);
347
+ // The per-connection session ID lives in each CesRpcServer's SessionContext;
348
+ // handlers read it at call time. The registry is shared across connections
349
+ // and identical in both modes.
350
+ const handlers = buildCrudHandlers(secureKeyBackend);
794
351
 
795
352
  // -- Health server (managed only) -----------------------------------------
796
353
  if (mode === "managed") {
@@ -823,52 +380,25 @@ async function main(): Promise<void> {
823
380
  error: (msg: string, ...args: unknown[]) => rpcLog.error({ args }, msg),
824
381
  };
825
382
 
826
- // Managed mode: wire handshake callbacks to update credential refs.
827
- // Capture in a const so TS narrows the type inside the closures.
828
- const refs = managedRefs;
383
+ // Managed mode registers the `update_managed_credential` handler (via the
384
+ // `onApiKeyUpdate` hook) so the assistant can push its API key/ID after hatch.
385
+ // The push is acknowledged and logged; CES stores credentials via the CRUD
386
+ // handlers and no longer materializes platform tokens itself.
829
387
  serveStandaloneSocket({
830
388
  socketPath,
831
389
  handlers,
832
390
  signal: controller.signal,
833
391
  logger: rpcLogger,
834
392
  log,
835
- onHandshakeComplete: refs
836
- ? (_hsSessionId, hsApiKey, hsAssistantId) => {
837
- // Update the process-scoped credential refs on every handshake.
838
- // With multi-client CES, multiple connections from the same
839
- // assistant instance share these refs. All connections belong to
840
- // the same guardian, so they carry the same API key - last
841
- // handshake wins is correct. A connection that omits the key
842
- // fails closed (falling back to the env key, or no key) rather
843
- // than reusing a previous session's credentials.
844
- applyManagedCredentialRefs(
845
- refs.apiKeyRef,
846
- refs.assistantIdRef,
847
- hsApiKey,
848
- hsAssistantId,
849
- );
850
- if (hsApiKey) {
851
- log.info("Received assistant API key via handshake");
852
- }
853
- if (hsAssistantId) {
854
- log.info("Received assistant ID via handshake");
855
- }
856
- }
857
- : undefined,
858
- onApiKeyUpdate: refs
859
- ? (newKey: string, newAssistantId?: string) => {
860
- applyManagedCredentialRefs(
861
- refs.apiKeyRef,
862
- refs.assistantIdRef,
863
- newKey,
864
- newAssistantId,
865
- );
866
- log.info("Assistant API key updated via RPC");
867
- if (newAssistantId) {
868
- log.info("Assistant ID updated via RPC");
393
+ onApiKeyUpdate:
394
+ mode === "managed"
395
+ ? (_newKey: string, newAssistantId?: string) => {
396
+ log.info("Assistant API key updated via RPC");
397
+ if (newAssistantId) {
398
+ log.info("Assistant ID updated via RPC");
399
+ }
869
400
  }
870
- }
871
- : undefined,
401
+ : undefined,
872
402
  });
873
403
 
874
404
  await new Promise<void>((resolve) => {