@vellumai/credential-executor 0.10.7 → 0.10.8-dev.202607102228.5945895
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/Dockerfile +1 -1
- package/node_modules/@vellumai/service-contracts/package.json +1 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +104 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +0 -2
- package/node_modules/@vellumai/service-contracts/src/attachment-naming.ts +118 -0
- 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 +2 -3
- package/src/__tests__/bulk-set-credentials.test.ts +1 -1
- package/src/__tests__/local-standalone.test.ts +5 -36
- package/src/__tests__/managed-integration.test.ts +112 -91
- package/src/__tests__/managed-reconnect.test.ts +2 -2
- package/src/__tests__/transport.test.ts +23 -27
- package/src/cli.ts +1 -1
- package/src/index.ts +8 -88
- package/src/main.ts +228 -340
- package/src/paths.ts +4 -20
- 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/managed-main.ts +0 -822
- 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
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* CES (Credential Execution Service) entrypoint — unified for both
|
|
4
|
+
* local (bare-metal sibling) and managed (Kubernetes sidecar) modes.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Mode is determined by `getCesMode()` from `paths.ts`:
|
|
7
|
+
* - `CES_MODE=managed` (set in the Dockerfile / K8s statefulset) → managed mode
|
|
8
|
+
* - absent or any other value → local mode (bare-metal sibling)
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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.
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* (e.g. for `run_authenticated_command`): Bun's `Bun.spawn` defaults to "pipe"
|
|
18
|
-
* for stdio, and the listening socket is not passed to those subprocesses.
|
|
16
|
+
* Managed mode additionally starts a health HTTP server (`/healthz`, `/readyz`,
|
|
17
|
+
* optional credential CRUD routes) for Kubernetes liveness/readiness probes.
|
|
19
18
|
*/
|
|
20
19
|
|
|
21
20
|
import { mkdirSync, unlinkSync } from "node:fs";
|
|
22
21
|
import { createServer as createNetServer, type Socket } from "node:net";
|
|
23
|
-
import { homedir } from "node:os";
|
|
24
22
|
import { dirname, join } from "node:path";
|
|
25
23
|
import { Readable, Writable } from "node:stream";
|
|
26
24
|
|
|
@@ -28,285 +26,60 @@ import {
|
|
|
28
26
|
CES_PROTOCOL_VERSION,
|
|
29
27
|
CesRpcMethod,
|
|
30
28
|
} from "@vellumai/service-contracts/credential-rpc";
|
|
31
|
-
import { StaticCredentialMetadataStore } from "@vellumai/credential-storage";
|
|
32
|
-
|
|
33
|
-
import { AuditStore } from "./audit/store.js";
|
|
34
|
-
import { PersistentGrantStore } from "./grants/persistent-store.js";
|
|
35
|
-
import {
|
|
36
|
-
createListAuditRecordsHandler,
|
|
37
|
-
createListGrantsHandler,
|
|
38
|
-
createRecordGrantHandler,
|
|
39
|
-
createRevokeGrantHandler,
|
|
40
|
-
} from "./grants/rpc-handlers.js";
|
|
41
|
-
import { TemporaryGrantStore } from "./grants/temporary-store.js";
|
|
42
|
-
import { LocalMaterialiser } from "./materializers/local.js";
|
|
43
29
|
import type { SecureKeyBackend } from "@vellumai/credential-storage";
|
|
30
|
+
|
|
44
31
|
import { createLocalSecureKeyBackend } from "./materializers/local-secure-key-backend.js";
|
|
45
|
-
import { createLocalOAuthLookup } from "./materializers/local-oauth-lookup.js";
|
|
46
|
-
import { createLocalTokenRefreshFn } from "./materializers/local-token-refresh.js";
|
|
47
|
-
import { resolveLocalSubject } from "./subjects/local.js";
|
|
48
|
-
import { checkCredentialPolicy } from "./subjects/policy.js";
|
|
49
32
|
import { initLogger, getLogger } from "./logger.js";
|
|
50
33
|
import {
|
|
51
|
-
|
|
34
|
+
getBootstrapSocketPath,
|
|
52
35
|
getCesDataRoot,
|
|
53
|
-
getCesGrantsDir,
|
|
54
36
|
getCesLogDir,
|
|
55
|
-
|
|
37
|
+
getCesMode,
|
|
38
|
+
getHealthPort,
|
|
56
39
|
getLocalSocketPath,
|
|
40
|
+
getSecurityDir,
|
|
41
|
+
type CesMode,
|
|
57
42
|
} from "./paths.js";
|
|
43
|
+
import { CesRpcServer, type RpcHandlerRegistry } from "./server.js";
|
|
58
44
|
import {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
type RpcHandlerRegistry,
|
|
64
|
-
} from "./server.js";
|
|
65
|
-
import {
|
|
66
|
-
deleteBundleFromToolstore,
|
|
67
|
-
publishBundle,
|
|
68
|
-
} from "./toolstore/publish.js";
|
|
69
|
-
import { validateSourceUrl } from "./toolstore/manifest.js";
|
|
70
|
-
import { buildCesEgressHooks } from "./commands/egress-hooks.js";
|
|
45
|
+
handleCredentialRoute,
|
|
46
|
+
type CredentialRouteDeps,
|
|
47
|
+
} from "./http/credential-routes.js";
|
|
48
|
+
import { handleLogExportRoute } from "./http/log-export-routes.js";
|
|
71
49
|
import { CES_MIGRATIONS } from "./migrations/registry.js";
|
|
72
50
|
import { runCesMigrations } from "./migrations/runner.js";
|
|
73
51
|
|
|
74
52
|
// ---------------------------------------------------------------------------
|
|
75
|
-
//
|
|
53
|
+
// Logging (module-level for early bootstrap + structured logging post-init)
|
|
76
54
|
// ---------------------------------------------------------------------------
|
|
77
55
|
|
|
78
|
-
|
|
79
|
-
const dirs = [
|
|
80
|
-
getCesDataRoot("local"),
|
|
81
|
-
getCesGrantsDir("local"),
|
|
82
|
-
getCesAuditDir("local"),
|
|
83
|
-
getCesToolStoreDir("local"),
|
|
84
|
-
];
|
|
85
|
-
for (const dir of dirs) {
|
|
86
|
-
mkdirSync(dir, { recursive: true });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
56
|
+
const log = getLogger("main");
|
|
89
57
|
|
|
90
58
|
// ---------------------------------------------------------------------------
|
|
91
|
-
//
|
|
59
|
+
// Data directory bootstrap
|
|
92
60
|
// ---------------------------------------------------------------------------
|
|
93
61
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
*
|
|
97
|
-
* Priority:
|
|
98
|
-
* 1. `VELLUM_WORKSPACE_DIR` env var (set by the platform template)
|
|
99
|
-
* 2. Default: `~/.vellum/workspace`
|
|
100
|
-
*/
|
|
101
|
-
function getWorkspaceDir(): string {
|
|
102
|
-
return (
|
|
103
|
-
process.env["VELLUM_WORKSPACE_DIR"]?.trim() ||
|
|
104
|
-
join(homedir(), ".vellum", "workspace")
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Resolve the CES security directory (contains key stores, encryption data).
|
|
110
|
-
*
|
|
111
|
-
* Priority:
|
|
112
|
-
* 1. `CREDENTIAL_SECURITY_DIR` env var (set by the platform template for
|
|
113
|
-
* the CES container — `/ces-security` in managed mode)
|
|
114
|
-
* 2. Default: `~/.vellum/protected` (local mode)
|
|
115
|
-
*/
|
|
116
|
-
function getSecurityDir(): string {
|
|
117
|
-
return (
|
|
118
|
-
process.env["CREDENTIAL_SECURITY_DIR"]?.trim() ||
|
|
119
|
-
join(homedir(), ".vellum", "protected")
|
|
120
|
-
);
|
|
62
|
+
function ensureDataDirs(mode: CesMode): void {
|
|
63
|
+
mkdirSync(getCesDataRoot(mode), { recursive: true });
|
|
121
64
|
}
|
|
122
65
|
|
|
123
66
|
// ---------------------------------------------------------------------------
|
|
124
|
-
//
|
|
67
|
+
// Credential CRUD handlers
|
|
125
68
|
// ---------------------------------------------------------------------------
|
|
126
69
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// -- Credential backend (local) --------------------------------------------
|
|
141
|
-
// In local mode CES shares the filesystem with the assistant and can access
|
|
142
|
-
// the same credential metadata and secure-key stores.
|
|
143
|
-
const workspaceDir = getWorkspaceDir();
|
|
144
|
-
const credentialMetadataPath = join(
|
|
145
|
-
workspaceDir,
|
|
146
|
-
"data",
|
|
147
|
-
"credentials",
|
|
148
|
-
"metadata.json",
|
|
149
|
-
);
|
|
150
|
-
const metadataStore = new StaticCredentialMetadataStore(
|
|
151
|
-
credentialMetadataPath,
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
// Read-only OAuth connection lookup backed by the assistant's SQLite
|
|
155
|
-
// database. CES opens the database in read-only mode.
|
|
156
|
-
const oauthConnections = createLocalOAuthLookup(workspaceDir);
|
|
157
|
-
|
|
158
|
-
const localMaterialiser = new LocalMaterialiser({
|
|
159
|
-
secureKeyBackend,
|
|
160
|
-
tokenRefreshFn: createLocalTokenRefreshFn(workspaceDir, secureKeyBackend),
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// -- Build handler registry ------------------------------------------------
|
|
164
|
-
|
|
165
|
-
// Start with the HTTP handler (make_authenticated_request)
|
|
166
|
-
const handlers = buildHandlersWithHttp({
|
|
167
|
-
persistentGrantStore,
|
|
168
|
-
temporaryGrantStore,
|
|
169
|
-
localMaterialiser,
|
|
170
|
-
localSubjectDeps: {
|
|
171
|
-
metadataStore,
|
|
172
|
-
oauthConnections,
|
|
173
|
-
},
|
|
174
|
-
auditStore,
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// Register run_authenticated_command handler
|
|
178
|
-
registerCommandExecutionHandler(handlers, {
|
|
179
|
-
executorDeps: {
|
|
180
|
-
persistentStore: persistentGrantStore,
|
|
181
|
-
temporaryStore: temporaryGrantStore,
|
|
182
|
-
materializeCredential: async (handle) => {
|
|
183
|
-
// Resolve the subject first, then materialise through the local backend
|
|
184
|
-
const subjectResult = resolveLocalSubject(handle, {
|
|
185
|
-
metadataStore,
|
|
186
|
-
oauthConnections,
|
|
187
|
-
});
|
|
188
|
-
if (!subjectResult.ok) {
|
|
189
|
-
return { ok: false as const, error: subjectResult.error };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Enforce credential-level policies for local static handles
|
|
193
|
-
if (subjectResult.subject.type === "local_static") {
|
|
194
|
-
const policyCheck = checkCredentialPolicy(
|
|
195
|
-
subjectResult.subject.metadata,
|
|
196
|
-
"run_authenticated_command",
|
|
197
|
-
);
|
|
198
|
-
if (!policyCheck.ok) {
|
|
199
|
-
return { ok: false as const, error: policyCheck.error! };
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const matResult = await localMaterialiser.materialise(
|
|
204
|
-
subjectResult.subject,
|
|
205
|
-
);
|
|
206
|
-
if (!matResult.ok) {
|
|
207
|
-
return { ok: false as const, error: matResult.error };
|
|
208
|
-
}
|
|
209
|
-
return {
|
|
210
|
-
ok: true as const,
|
|
211
|
-
value: matResult.credential.value,
|
|
212
|
-
handleType: matResult.credential.handleType,
|
|
213
|
-
};
|
|
214
|
-
},
|
|
215
|
-
auditStore,
|
|
216
|
-
cesMode: "local",
|
|
217
|
-
egressHooks: buildCesEgressHooks(),
|
|
218
|
-
},
|
|
219
|
-
defaultWorkspaceDir: workspaceDir,
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// Register manage_secure_command_tool handler
|
|
223
|
-
const toolRegistry = new Map<
|
|
224
|
-
string,
|
|
225
|
-
{
|
|
226
|
-
toolName: string;
|
|
227
|
-
credentialHandle: string;
|
|
228
|
-
description: string;
|
|
229
|
-
bundleDigest: string;
|
|
230
|
-
}
|
|
231
|
-
>();
|
|
232
|
-
|
|
233
|
-
registerManageSecureCommandToolHandler(handlers, {
|
|
234
|
-
downloadBundle: async (sourceUrl: string) => {
|
|
235
|
-
const urlError = validateSourceUrl(sourceUrl);
|
|
236
|
-
if (urlError) {
|
|
237
|
-
throw new Error(urlError);
|
|
238
|
-
}
|
|
239
|
-
const MAX_BUNDLE_SIZE = 100 * 1024 * 1024; // 100 MB
|
|
240
|
-
const resp = await fetch(sourceUrl, {
|
|
241
|
-
signal: AbortSignal.timeout(60_000),
|
|
242
|
-
});
|
|
243
|
-
if (!resp.ok) {
|
|
244
|
-
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
|
|
245
|
-
}
|
|
246
|
-
const contentLength = resp.headers.get("content-length");
|
|
247
|
-
if (contentLength && parseInt(contentLength, 10) > MAX_BUNDLE_SIZE) {
|
|
248
|
-
throw new Error(
|
|
249
|
-
`Bundle too large: ${contentLength} bytes (max ${MAX_BUNDLE_SIZE})`,
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
// Stream the body and enforce the size limit on actual bytes received,
|
|
253
|
-
// since Content-Length can be absent (chunked encoding) or lie.
|
|
254
|
-
const body = resp.body;
|
|
255
|
-
if (!body) {
|
|
256
|
-
throw new Error("Response body is null");
|
|
257
|
-
}
|
|
258
|
-
const chunks: Uint8Array[] = [];
|
|
259
|
-
let totalBytes = 0;
|
|
260
|
-
for await (const chunk of body) {
|
|
261
|
-
totalBytes += chunk.byteLength;
|
|
262
|
-
if (totalBytes > MAX_BUNDLE_SIZE) {
|
|
263
|
-
throw new Error(
|
|
264
|
-
`Bundle too large: received >${MAX_BUNDLE_SIZE} bytes (max ${MAX_BUNDLE_SIZE})`,
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
chunks.push(chunk);
|
|
268
|
-
}
|
|
269
|
-
return Buffer.concat(chunks);
|
|
270
|
-
},
|
|
271
|
-
publishBundle: (request) => publishBundle({ ...request, cesMode: "local" }),
|
|
272
|
-
unregisterTool: (toolName: string) => {
|
|
273
|
-
const entry = toolRegistry.get(toolName);
|
|
274
|
-
const removed = toolRegistry.delete(toolName);
|
|
275
|
-
if (removed && entry?.bundleDigest) {
|
|
276
|
-
const stillInUse = Array.from(toolRegistry.values()).some(
|
|
277
|
-
(t) => t.bundleDigest === entry.bundleDigest,
|
|
278
|
-
);
|
|
279
|
-
if (!stillInUse) {
|
|
280
|
-
deleteBundleFromToolstore(entry.bundleDigest, "local");
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
return removed;
|
|
284
|
-
},
|
|
285
|
-
registerTool: (entry) => {
|
|
286
|
-
toolRegistry.set(entry.toolName, entry);
|
|
287
|
-
},
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
// Register grant management handlers
|
|
291
|
-
handlers[CesRpcMethod.RecordGrant] = createRecordGrantHandler({
|
|
292
|
-
persistentGrantStore,
|
|
293
|
-
temporaryGrantStore,
|
|
294
|
-
}) as (typeof handlers)[string];
|
|
295
|
-
|
|
296
|
-
handlers[CesRpcMethod.ListGrants] = createListGrantsHandler({
|
|
297
|
-
persistentGrantStore,
|
|
298
|
-
}) as (typeof handlers)[string];
|
|
299
|
-
|
|
300
|
-
handlers[CesRpcMethod.RevokeGrant] = createRevokeGrantHandler({
|
|
301
|
-
persistentGrantStore,
|
|
302
|
-
}) as (typeof handlers)[string];
|
|
303
|
-
|
|
304
|
-
// Register audit record handler
|
|
305
|
-
handlers[CesRpcMethod.ListAuditRecords] = createListAuditRecordsHandler({
|
|
306
|
-
auditStore,
|
|
307
|
-
}) as (typeof handlers)[string];
|
|
70
|
+
/**
|
|
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.
|
|
77
|
+
*/
|
|
78
|
+
function buildCrudHandlers(
|
|
79
|
+
secureKeyBackend: SecureKeyBackend,
|
|
80
|
+
): RpcHandlerRegistry {
|
|
81
|
+
const handlers: RpcHandlerRegistry = {};
|
|
308
82
|
|
|
309
|
-
// Register credential CRUD handlers
|
|
310
83
|
handlers[CesRpcMethod.GetCredential] = (async (req: { account: string }) => {
|
|
311
84
|
const value = await secureKeyBackend.get(req.account);
|
|
312
85
|
return { found: value !== undefined, value };
|
|
@@ -347,16 +120,16 @@ function buildHandlers(secureKeyBackend: SecureKeyBackend): RpcHandlerRegistry {
|
|
|
347
120
|
}
|
|
348
121
|
|
|
349
122
|
// ---------------------------------------------------------------------------
|
|
350
|
-
//
|
|
123
|
+
// Socket server (unified multi-connection model)
|
|
351
124
|
// ---------------------------------------------------------------------------
|
|
352
125
|
|
|
353
126
|
/**
|
|
354
|
-
* Serve RPC over a Unix socket
|
|
127
|
+
* Serve RPC over a Unix socket using a concurrent multi-connection model.
|
|
355
128
|
*
|
|
356
129
|
* Binds the socket, accepts connections concurrently (each served by its own
|
|
357
130
|
* CesRpcServer over the shared handler registry), and unlinks the socket when
|
|
358
|
-
* the signal aborts.
|
|
359
|
-
*
|
|
131
|
+
* the signal aborts. The listener stays open across connections so a client
|
|
132
|
+
* that disconnects can reconnect without CES re-binding.
|
|
360
133
|
*/
|
|
361
134
|
function serveStandaloneSocket(opts: {
|
|
362
135
|
socketPath: string;
|
|
@@ -364,8 +137,22 @@ function serveStandaloneSocket(opts: {
|
|
|
364
137
|
signal: AbortSignal;
|
|
365
138
|
logger: Pick<Console, "log" | "warn" | "error">;
|
|
366
139
|
log: ReturnType<typeof getLogger>;
|
|
140
|
+
onHandshakeComplete?: (
|
|
141
|
+
sessionId: string,
|
|
142
|
+
assistantApiKey?: string,
|
|
143
|
+
assistantId?: string,
|
|
144
|
+
) => void;
|
|
145
|
+
onApiKeyUpdate?: (assistantApiKey: string, assistantId?: string) => void;
|
|
367
146
|
}): void {
|
|
368
|
-
const {
|
|
147
|
+
const {
|
|
148
|
+
socketPath,
|
|
149
|
+
handlers,
|
|
150
|
+
signal,
|
|
151
|
+
logger,
|
|
152
|
+
log,
|
|
153
|
+
onHandshakeComplete,
|
|
154
|
+
onApiKeyUpdate,
|
|
155
|
+
} = opts;
|
|
369
156
|
|
|
370
157
|
mkdirSync(dirname(socketPath), { recursive: true });
|
|
371
158
|
try {
|
|
@@ -377,10 +164,11 @@ function serveStandaloneSocket(opts: {
|
|
|
377
164
|
const netServer = createNetServer();
|
|
378
165
|
|
|
379
166
|
netServer.on("error", (err) => {
|
|
380
|
-
log.warn({ err }, "CES
|
|
167
|
+
log.warn({ err }, "CES socket server error");
|
|
381
168
|
});
|
|
382
169
|
|
|
383
170
|
netServer.on("connection", (socket: Socket) => {
|
|
171
|
+
connectionCount++;
|
|
384
172
|
const readable = new Readable({ read() {} });
|
|
385
173
|
const writable = new Writable({
|
|
386
174
|
write(chunk, _encoding, callback) {
|
|
@@ -404,19 +192,24 @@ function serveStandaloneSocket(opts: {
|
|
|
404
192
|
handlers,
|
|
405
193
|
logger,
|
|
406
194
|
signal,
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
log.warn(
|
|
412
|
-
{ err },
|
|
413
|
-
"CES standalone connection ended with a transport error",
|
|
414
|
-
);
|
|
195
|
+
onHandshakeComplete: (sessionId, apiKey, assistantId) => {
|
|
196
|
+
onHandshakeComplete?.(sessionId, apiKey, assistantId);
|
|
197
|
+
},
|
|
198
|
+
onApiKeyUpdate: onApiKeyUpdate ?? (() => {}),
|
|
415
199
|
});
|
|
200
|
+
void server
|
|
201
|
+
.serve()
|
|
202
|
+
.catch((err) => {
|
|
203
|
+
server.close();
|
|
204
|
+
log.warn({ err }, "CES connection ended with a transport error");
|
|
205
|
+
})
|
|
206
|
+
.then(() => {
|
|
207
|
+
connectionCount = Math.max(0, connectionCount - 1);
|
|
208
|
+
});
|
|
416
209
|
});
|
|
417
210
|
|
|
418
211
|
netServer.listen(socketPath, () => {
|
|
419
|
-
log.info(`CES
|
|
212
|
+
log.info(`CES socket listening at ${socketPath}`);
|
|
420
213
|
});
|
|
421
214
|
|
|
422
215
|
signal.addEventListener(
|
|
@@ -433,20 +226,89 @@ function serveStandaloneSocket(opts: {
|
|
|
433
226
|
);
|
|
434
227
|
}
|
|
435
228
|
|
|
436
|
-
|
|
437
|
-
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Health server (managed mode only)
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
438
232
|
|
|
439
|
-
|
|
440
|
-
|
|
233
|
+
let connectionCount = 0;
|
|
234
|
+
|
|
235
|
+
function startHealthServer(
|
|
236
|
+
port: number,
|
|
237
|
+
signal: AbortSignal,
|
|
238
|
+
credentialDeps: CredentialRouteDeps | null,
|
|
239
|
+
): ReturnType<typeof Bun.serve> {
|
|
240
|
+
const server = Bun.serve({
|
|
241
|
+
port,
|
|
242
|
+
async fetch(req) {
|
|
243
|
+
const url = new URL(req.url);
|
|
244
|
+
if (url.pathname === "/healthz") {
|
|
245
|
+
return new Response(JSON.stringify({ status: "ok" }), {
|
|
246
|
+
headers: { "Content-Type": "application/json" },
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (url.pathname === "/readyz") {
|
|
250
|
+
// Always return 200 — pod readiness must not depend on whether the
|
|
251
|
+
// assistant has connected. When the CES feature flag is off the
|
|
252
|
+
// assistant never connects, and a 503 here would block pod
|
|
253
|
+
// scheduling during dark-launch. The sidecar can't do useful work
|
|
254
|
+
// without a connection anyway, so readiness is purely about the
|
|
255
|
+
// process being up and able to accept a future connection.
|
|
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
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Credential CRUD routes (only if service token is configured)
|
|
270
|
+
if (credentialDeps) {
|
|
271
|
+
const credentialResponse = await handleCredentialRoute(
|
|
272
|
+
req,
|
|
273
|
+
credentialDeps,
|
|
274
|
+
);
|
|
275
|
+
if (credentialResponse) return credentialResponse;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Log export route
|
|
279
|
+
const logExportResponse = await handleLogExportRoute(
|
|
280
|
+
req,
|
|
281
|
+
getCesLogDir("managed"),
|
|
282
|
+
);
|
|
283
|
+
if (logExportResponse) return logExportResponse;
|
|
441
284
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
285
|
+
return new Response("Not Found", { status: 404 });
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
signal.addEventListener(
|
|
290
|
+
"abort",
|
|
291
|
+
() => {
|
|
292
|
+
server.stop(true);
|
|
293
|
+
},
|
|
294
|
+
{ once: true },
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
return server;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// Main
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
async function main(): Promise<void> {
|
|
305
|
+
const mode = getCesMode();
|
|
306
|
+
ensureDataDirs(mode);
|
|
307
|
+
|
|
308
|
+
initLogger({ dir: getCesLogDir(mode), retentionDays: 30 });
|
|
445
309
|
|
|
446
310
|
log.info(
|
|
447
|
-
`Starting CES v${CES_PROTOCOL_VERSION} (
|
|
448
|
-
standalone ? "standalone socket" : "stdio"
|
|
449
|
-
} transport)`,
|
|
311
|
+
`Starting CES v${CES_PROTOCOL_VERSION} (${mode} mode, socket transport)`,
|
|
450
312
|
);
|
|
451
313
|
|
|
452
314
|
const controller = new AbortController();
|
|
@@ -459,24 +321,57 @@ async function main(): Promise<void> {
|
|
|
459
321
|
process.on("SIGTERM", shutdown);
|
|
460
322
|
process.on("SIGINT", shutdown);
|
|
461
323
|
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
324
|
+
// -- Secure key backend + migrations --------------------------------------
|
|
325
|
+
// The secure key backend path differs by mode:
|
|
326
|
+
// - Local: dirname(getSecurityDir()) — the parent of ~/.vellum/protected
|
|
327
|
+
// (or CREDENTIAL_SECURITY_DIR when set by the CLI).
|
|
328
|
+
// - Managed: join(assistantDataMount, ".vellum") — the assistant data mount.
|
|
329
|
+
const secureKeyBackend =
|
|
330
|
+
mode === "managed"
|
|
331
|
+
? createLocalSecureKeyBackend(
|
|
332
|
+
join(
|
|
333
|
+
process.env["CES_ASSISTANT_DATA_MOUNT"] ?? "/assistant-data-ro",
|
|
334
|
+
".vellum",
|
|
335
|
+
),
|
|
336
|
+
)
|
|
337
|
+
: createLocalSecureKeyBackend(dirname(getSecurityDir()));
|
|
338
|
+
|
|
469
339
|
await runCesMigrations(
|
|
470
|
-
getCesDataRoot(
|
|
340
|
+
getCesDataRoot(mode),
|
|
471
341
|
secureKeyBackend,
|
|
472
342
|
CES_MIGRATIONS,
|
|
473
343
|
);
|
|
474
|
-
log.info(
|
|
344
|
+
log.info(`CES ${mode} startup: migrations complete`);
|
|
345
|
+
|
|
346
|
+
// -- Build handlers --------------------------------------------------------
|
|
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);
|
|
351
|
+
|
|
352
|
+
// -- Health server (managed only) -----------------------------------------
|
|
353
|
+
if (mode === "managed") {
|
|
354
|
+
const serviceToken = process.env["CES_SERVICE_TOKEN"] ?? "";
|
|
355
|
+
let credentialDeps: CredentialRouteDeps | null = null;
|
|
356
|
+
|
|
357
|
+
if (serviceToken) {
|
|
358
|
+
credentialDeps = { backend: secureKeyBackend, serviceToken };
|
|
359
|
+
log.info("Credential CRUD routes enabled (CES_SERVICE_TOKEN configured)");
|
|
360
|
+
} else {
|
|
361
|
+
log.warn(
|
|
362
|
+
"CES_SERVICE_TOKEN not set — credential CRUD HTTP routes are disabled. " +
|
|
363
|
+
"Set CES_SERVICE_TOKEN to enable credential management over HTTP.",
|
|
364
|
+
);
|
|
365
|
+
}
|
|
475
366
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
367
|
+
const healthPort = getHealthPort();
|
|
368
|
+
startHealthServer(healthPort, controller.signal, credentialDeps);
|
|
369
|
+
log.info(`Health server listening on port ${healthPort}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// -- Socket server ---------------------------------------------------------
|
|
373
|
+
const socketPath =
|
|
374
|
+
mode === "managed" ? getBootstrapSocketPath() : getLocalSocketPath();
|
|
480
375
|
|
|
481
376
|
const rpcLog = getLogger("rpc");
|
|
482
377
|
const rpcLogger = {
|
|
@@ -485,43 +380,36 @@ async function main(): Promise<void> {
|
|
|
485
380
|
error: (msg: string, ...args: unknown[]) => rpcLog.error({ args }, msg),
|
|
486
381
|
};
|
|
487
382
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
signal: controller.signal,
|
|
495
|
-
logger: rpcLogger,
|
|
496
|
-
log,
|
|
497
|
-
});
|
|
498
|
-
await new Promise<void>((resolve) => {
|
|
499
|
-
if (controller.signal.aborted) {
|
|
500
|
-
resolve();
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
controller.signal.addEventListener("abort", () => resolve(), {
|
|
504
|
-
once: true,
|
|
505
|
-
});
|
|
506
|
-
});
|
|
507
|
-
log.info("Server stopped.");
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Default: serve the spawning assistant over stdio. stdin closing (parent
|
|
512
|
-
// exit) or SIGTERM shuts CES down.
|
|
513
|
-
const server = new CesRpcServer({
|
|
514
|
-
input: process.stdin,
|
|
515
|
-
output: process.stdout,
|
|
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.
|
|
387
|
+
serveStandaloneSocket({
|
|
388
|
+
socketPath,
|
|
516
389
|
handlers,
|
|
517
|
-
logger: rpcLogger,
|
|
518
390
|
signal: controller.signal,
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
onApiKeyUpdate:
|
|
391
|
+
logger: rpcLogger,
|
|
392
|
+
log,
|
|
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
|
+
}
|
|
400
|
+
}
|
|
401
|
+
: undefined,
|
|
522
402
|
});
|
|
523
403
|
|
|
524
|
-
await
|
|
404
|
+
await new Promise<void>((resolve) => {
|
|
405
|
+
if (controller.signal.aborted) {
|
|
406
|
+
resolve();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
controller.signal.addEventListener("abort", () => resolve(), {
|
|
410
|
+
once: true,
|
|
411
|
+
});
|
|
412
|
+
});
|
|
525
413
|
log.info("Server stopped.");
|
|
526
414
|
}
|
|
527
415
|
|
|
@@ -529,7 +417,7 @@ main().catch((err) => {
|
|
|
529
417
|
try {
|
|
530
418
|
getLogger("main").fatal({ err }, "Fatal error");
|
|
531
419
|
} catch {
|
|
532
|
-
process.stderr.write(`[ces
|
|
420
|
+
process.stderr.write(`[ces-${getCesMode()}] Fatal: ${err}\n`);
|
|
533
421
|
}
|
|
534
422
|
process.exit(1);
|
|
535
423
|
});
|