@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.
- package/node_modules/@vellumai/service-contracts/package.json +0 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +0 -2
- package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +3 -5
- package/node_modules/@vellumai/service-contracts/src/index.ts +2 -4
- package/node_modules/@vellumai/service-contracts/src/rpc.ts +4 -447
- package/package.json +1 -1
- package/src/__tests__/managed-integration.test.ts +108 -87
- package/src/index.ts +5 -85
- package/src/main.ts +79 -549
- package/src/paths.ts +0 -15
- package/src/server.ts +52 -469
- package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +0 -686
- package/node_modules/@vellumai/service-contracts/src/grants.ts +0 -184
- package/node_modules/@vellumai/service-contracts/src/rendering.ts +0 -135
- package/src/__tests__/command-executor.test.ts +0 -1879
- package/src/__tests__/command-validator.test.ts +0 -1405
- package/src/__tests__/command-workspace.test.ts +0 -1050
- package/src/__tests__/grant-store.test.ts +0 -689
- package/src/__tests__/http-executor.test.ts +0 -1336
- package/src/__tests__/http-policy.test.ts +0 -1069
- package/src/__tests__/local-materializers.test.ts +0 -860
- package/src/__tests__/local-token-refresh.test.ts +0 -361
- package/src/__tests__/manage-secure-command-tool.test.ts +0 -134
- package/src/__tests__/managed-lazy-getters.test.ts +0 -359
- package/src/__tests__/managed-materializers.test.ts +0 -1028
- package/src/__tests__/managed-rejection.test.ts +0 -43
- package/src/__tests__/toolstore.test.ts +0 -773
- package/src/audit/store.ts +0 -188
- package/src/commands/auth-adapters.ts +0 -169
- package/src/commands/egress-hooks.ts +0 -203
- package/src/commands/executor.ts +0 -1155
- package/src/commands/output-scan.ts +0 -157
- package/src/commands/profiles.ts +0 -286
- package/src/commands/validator.ts +0 -702
- package/src/commands/workspace.ts +0 -550
- package/src/grants/index.ts +0 -17
- package/src/grants/persistent-store.ts +0 -309
- package/src/grants/rpc-handlers.ts +0 -293
- package/src/grants/temporary-store.ts +0 -289
- package/src/http/audit.ts +0 -84
- package/src/http/executor.ts +0 -684
- package/src/http/path-template.ts +0 -245
- package/src/http/policy.ts +0 -238
- package/src/http/response-filter.ts +0 -233
- package/src/managed-errors.ts +0 -9
- package/src/managed-lazy-getters.ts +0 -106
- package/src/materializers/local-oauth-lookup.ts +0 -98
- package/src/materializers/local-token-refresh.ts +0 -287
- package/src/materializers/local.ts +0 -316
- package/src/materializers/managed-platform.ts +0 -295
- package/src/subjects/local.ts +0 -177
- package/src/subjects/managed.ts +0 -311
- package/src/subjects/policy.ts +0 -79
- package/src/toolstore/integrity.ts +0 -94
- package/src/toolstore/manifest.ts +0 -154
- 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
|
|
11
|
-
* server (`serveStandaloneSocket`). Each connection gets its
|
|
12
|
-
* over a shared, process-scoped handler registry. The server
|
|
13
|
-
* across connections, so an assistant that disconnects (crash,
|
|
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
|
-
|
|
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
|
-
//
|
|
67
|
+
// Credential CRUD handlers
|
|
126
68
|
// ---------------------------------------------------------------------------
|
|
127
69
|
|
|
128
70
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
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
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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?: (
|
|
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 {
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
646
|
-
|
|
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(
|
|
699
|
-
|
|
700
|
-
|
|
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(
|
|
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
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
|
|
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
|
|
827
|
-
//
|
|
828
|
-
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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) => {
|