@vellumai/credential-executor 0.10.7-dev.202607102035.64f07ea → 0.10.7-staging.1
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 +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +686 -0
- package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +5 -3
- package/node_modules/@vellumai/service-contracts/src/grants.ts +184 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +4 -2
- package/node_modules/@vellumai/service-contracts/src/rendering.ts +135 -0
- package/node_modules/@vellumai/service-contracts/src/rpc.ts +447 -4
- package/package.json +3 -2
- package/src/__tests__/bulk-set-credentials.test.ts +1 -1
- package/src/__tests__/command-executor.test.ts +1879 -0
- package/src/__tests__/command-validator.test.ts +1405 -0
- package/src/__tests__/command-workspace.test.ts +1050 -0
- package/src/__tests__/grant-store.test.ts +689 -0
- package/src/__tests__/http-executor.test.ts +1336 -0
- package/src/__tests__/http-policy.test.ts +1069 -0
- package/src/__tests__/local-materializers.test.ts +860 -0
- package/src/__tests__/local-standalone.test.ts +36 -5
- package/src/__tests__/local-token-refresh.test.ts +361 -0
- package/src/__tests__/manage-secure-command-tool.test.ts +134 -0
- package/src/__tests__/managed-integration.test.ts +91 -112
- package/src/__tests__/managed-lazy-getters.test.ts +359 -0
- package/src/__tests__/managed-materializers.test.ts +1028 -0
- package/src/__tests__/managed-reconnect.test.ts +2 -2
- package/src/__tests__/managed-rejection.test.ts +43 -0
- package/src/__tests__/toolstore.test.ts +773 -0
- package/src/__tests__/transport.test.ts +27 -23
- package/src/audit/store.ts +188 -0
- package/src/cli.ts +1 -1
- package/src/commands/auth-adapters.ts +169 -0
- package/src/commands/egress-hooks.ts +203 -0
- package/src/commands/executor.ts +1155 -0
- package/src/commands/output-scan.ts +157 -0
- package/src/commands/profiles.ts +286 -0
- package/src/commands/validator.ts +702 -0
- package/src/commands/workspace.ts +550 -0
- package/src/grants/index.ts +17 -0
- package/src/grants/persistent-store.ts +309 -0
- package/src/grants/rpc-handlers.ts +293 -0
- package/src/grants/temporary-store.ts +289 -0
- package/src/http/audit.ts +84 -0
- package/src/http/executor.ts +684 -0
- package/src/http/path-template.ts +245 -0
- package/src/http/policy.ts +238 -0
- package/src/http/response-filter.ts +233 -0
- package/src/index.ts +88 -8
- package/src/main.ts +340 -228
- package/src/managed-errors.ts +9 -0
- package/src/managed-lazy-getters.ts +106 -0
- package/src/managed-main.ts +822 -0
- package/src/materializers/local-oauth-lookup.ts +98 -0
- package/src/materializers/local-token-refresh.ts +287 -0
- package/src/materializers/local.ts +316 -0
- package/src/materializers/managed-platform.ts +295 -0
- package/src/paths.ts +20 -4
- package/src/server.ts +469 -52
- package/src/subjects/local.ts +177 -0
- package/src/subjects/managed.ts +311 -0
- package/src/subjects/policy.ts +79 -0
- package/src/toolstore/integrity.ts +94 -0
- package/src/toolstore/manifest.ts +154 -0
- package/src/toolstore/publish.ts +571 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +0 -104
- package/node_modules/@vellumai/service-contracts/src/attachment-naming.ts +0 -118
package/src/main.ts
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* CES
|
|
4
|
-
* local (bare-metal sibling) and managed (Kubernetes sidecar) modes.
|
|
3
|
+
* Local CES entrypoint. Two run modes:
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* - **stdio child (default):** the assistant spawns CES as a child process and
|
|
6
|
+
* communicates over stdin/stdout. CES shuts down when stdin closes (parent
|
|
7
|
+
* exit) or on SIGTERM. This is today's behavior.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* - **standalone sibling (`CES_STANDALONE=1`):** CES is launched independently
|
|
10
|
+
* by the CLI (the opt-in), serves RPC over a
|
|
11
|
+
* Unix socket (`getLocalSocketPath()`), and runs until SIGTERM — no stdio.
|
|
12
|
+
* This is the direction local CES is converging on; the socket-serving here
|
|
13
|
+
* is temporary scaffolding to be folded into a single unified CES entrypoint.
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Local mode never opens a TCP listener. Neither the stdio transport nor the
|
|
16
|
+
* Unix socket's listening fd is inherited by shell subprocesses spawned by CES
|
|
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.
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import { mkdirSync, unlinkSync } from "node:fs";
|
|
21
22
|
import { createServer as createNetServer, type Socket } from "node:net";
|
|
23
|
+
import { homedir } from "node:os";
|
|
22
24
|
import { dirname, join } from "node:path";
|
|
23
25
|
import { Readable, Writable } from "node:stream";
|
|
24
26
|
|
|
@@ -26,60 +28,285 @@ import {
|
|
|
26
28
|
CES_PROTOCOL_VERSION,
|
|
27
29
|
CesRpcMethod,
|
|
28
30
|
} from "@vellumai/service-contracts/credential-rpc";
|
|
29
|
-
import
|
|
31
|
+
import { StaticCredentialMetadataStore } from "@vellumai/credential-storage";
|
|
30
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
|
+
import type { SecureKeyBackend } from "@vellumai/credential-storage";
|
|
31
44
|
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";
|
|
32
49
|
import { initLogger, getLogger } from "./logger.js";
|
|
33
50
|
import {
|
|
34
|
-
|
|
51
|
+
getCesAuditDir,
|
|
35
52
|
getCesDataRoot,
|
|
53
|
+
getCesGrantsDir,
|
|
36
54
|
getCesLogDir,
|
|
37
|
-
|
|
38
|
-
getHealthPort,
|
|
55
|
+
getCesToolStoreDir,
|
|
39
56
|
getLocalSocketPath,
|
|
40
|
-
getSecurityDir,
|
|
41
|
-
type CesMode,
|
|
42
57
|
} from "./paths.js";
|
|
43
|
-
import { CesRpcServer, type RpcHandlerRegistry } from "./server.js";
|
|
44
58
|
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
buildHandlersWithHttp,
|
|
60
|
+
CesRpcServer,
|
|
61
|
+
registerCommandExecutionHandler,
|
|
62
|
+
registerManageSecureCommandToolHandler,
|
|
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";
|
|
49
71
|
import { CES_MIGRATIONS } from "./migrations/registry.js";
|
|
50
72
|
import { runCesMigrations } from "./migrations/runner.js";
|
|
51
73
|
|
|
52
74
|
// ---------------------------------------------------------------------------
|
|
53
|
-
//
|
|
75
|
+
// Data directory bootstrap
|
|
54
76
|
// ---------------------------------------------------------------------------
|
|
55
77
|
|
|
56
|
-
|
|
78
|
+
function ensureDataDirs(): void {
|
|
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
|
+
}
|
|
57
89
|
|
|
58
90
|
// ---------------------------------------------------------------------------
|
|
59
|
-
//
|
|
91
|
+
// Path resolution
|
|
60
92
|
// ---------------------------------------------------------------------------
|
|
61
93
|
|
|
62
|
-
|
|
63
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the workspace directory.
|
|
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
|
+
);
|
|
64
121
|
}
|
|
65
122
|
|
|
66
123
|
// ---------------------------------------------------------------------------
|
|
67
|
-
//
|
|
124
|
+
// Build RPC handler registry
|
|
68
125
|
// ---------------------------------------------------------------------------
|
|
69
126
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
127
|
+
function buildHandlers(secureKeyBackend: SecureKeyBackend): RpcHandlerRegistry {
|
|
128
|
+
// -- Grant stores ----------------------------------------------------------
|
|
129
|
+
const persistentGrantStore = new PersistentGrantStore(
|
|
130
|
+
getCesGrantsDir("local"),
|
|
131
|
+
);
|
|
132
|
+
persistentGrantStore.init();
|
|
133
|
+
|
|
134
|
+
const temporaryGrantStore = new TemporaryGrantStore();
|
|
135
|
+
|
|
136
|
+
// -- Audit store -----------------------------------------------------------
|
|
137
|
+
const auditStore = new AuditStore(getCesAuditDir("local"));
|
|
138
|
+
auditStore.init();
|
|
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];
|
|
82
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];
|
|
308
|
+
|
|
309
|
+
// Register credential CRUD handlers
|
|
83
310
|
handlers[CesRpcMethod.GetCredential] = (async (req: { account: string }) => {
|
|
84
311
|
const value = await secureKeyBackend.get(req.account);
|
|
85
312
|
return { found: value !== undefined, value };
|
|
@@ -120,16 +347,16 @@ function buildCrudHandlers(
|
|
|
120
347
|
}
|
|
121
348
|
|
|
122
349
|
// ---------------------------------------------------------------------------
|
|
123
|
-
//
|
|
350
|
+
// Main
|
|
124
351
|
// ---------------------------------------------------------------------------
|
|
125
352
|
|
|
126
353
|
/**
|
|
127
|
-
* Serve RPC over a Unix socket
|
|
354
|
+
* Serve RPC over a Unix socket for standalone-sibling mode.
|
|
128
355
|
*
|
|
129
356
|
* Binds the socket, accepts connections concurrently (each served by its own
|
|
130
357
|
* CesRpcServer over the shared handler registry), and unlinks the socket when
|
|
131
|
-
* the signal aborts.
|
|
132
|
-
*
|
|
358
|
+
* the signal aborts. Temporary scaffolding — this serving path will be folded
|
|
359
|
+
* into a single unified CES entrypoint shared with the managed sidecar.
|
|
133
360
|
*/
|
|
134
361
|
function serveStandaloneSocket(opts: {
|
|
135
362
|
socketPath: string;
|
|
@@ -137,22 +364,8 @@ function serveStandaloneSocket(opts: {
|
|
|
137
364
|
signal: AbortSignal;
|
|
138
365
|
logger: Pick<Console, "log" | "warn" | "error">;
|
|
139
366
|
log: ReturnType<typeof getLogger>;
|
|
140
|
-
onHandshakeComplete?: (
|
|
141
|
-
sessionId: string,
|
|
142
|
-
assistantApiKey?: string,
|
|
143
|
-
assistantId?: string,
|
|
144
|
-
) => void;
|
|
145
|
-
onApiKeyUpdate?: (assistantApiKey: string, assistantId?: string) => void;
|
|
146
367
|
}): void {
|
|
147
|
-
const {
|
|
148
|
-
socketPath,
|
|
149
|
-
handlers,
|
|
150
|
-
signal,
|
|
151
|
-
logger,
|
|
152
|
-
log,
|
|
153
|
-
onHandshakeComplete,
|
|
154
|
-
onApiKeyUpdate,
|
|
155
|
-
} = opts;
|
|
368
|
+
const { socketPath, handlers, signal, logger, log } = opts;
|
|
156
369
|
|
|
157
370
|
mkdirSync(dirname(socketPath), { recursive: true });
|
|
158
371
|
try {
|
|
@@ -164,11 +377,10 @@ function serveStandaloneSocket(opts: {
|
|
|
164
377
|
const netServer = createNetServer();
|
|
165
378
|
|
|
166
379
|
netServer.on("error", (err) => {
|
|
167
|
-
log.warn({ err }, "CES socket server error");
|
|
380
|
+
log.warn({ err }, "CES standalone socket server error");
|
|
168
381
|
});
|
|
169
382
|
|
|
170
383
|
netServer.on("connection", (socket: Socket) => {
|
|
171
|
-
connectionCount++;
|
|
172
384
|
const readable = new Readable({ read() {} });
|
|
173
385
|
const writable = new Writable({
|
|
174
386
|
write(chunk, _encoding, callback) {
|
|
@@ -192,24 +404,19 @@ function serveStandaloneSocket(opts: {
|
|
|
192
404
|
handlers,
|
|
193
405
|
logger,
|
|
194
406
|
signal,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
407
|
+
onApiKeyUpdate: () => {},
|
|
408
|
+
});
|
|
409
|
+
void server.serve().catch((err) => {
|
|
410
|
+
server.close();
|
|
411
|
+
log.warn(
|
|
412
|
+
{ err },
|
|
413
|
+
"CES standalone connection ended with a transport error",
|
|
414
|
+
);
|
|
199
415
|
});
|
|
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
|
-
});
|
|
209
416
|
});
|
|
210
417
|
|
|
211
418
|
netServer.listen(socketPath, () => {
|
|
212
|
-
log.info(`CES socket listening at ${socketPath}`);
|
|
419
|
+
log.info(`CES standalone socket listening at ${socketPath}`);
|
|
213
420
|
});
|
|
214
421
|
|
|
215
422
|
signal.addEventListener(
|
|
@@ -226,89 +433,20 @@ function serveStandaloneSocket(opts: {
|
|
|
226
433
|
);
|
|
227
434
|
}
|
|
228
435
|
|
|
229
|
-
// ---------------------------------------------------------------------------
|
|
230
|
-
// Health server (managed mode only)
|
|
231
|
-
// ---------------------------------------------------------------------------
|
|
232
|
-
|
|
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;
|
|
284
|
-
|
|
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
436
|
async function main(): Promise<void> {
|
|
305
|
-
|
|
306
|
-
ensureDataDirs(mode);
|
|
437
|
+
ensureDataDirs();
|
|
307
438
|
|
|
308
|
-
initLogger({ dir: getCesLogDir(
|
|
439
|
+
initLogger({ dir: getCesLogDir(), retentionDays: 30 });
|
|
440
|
+
const log = getLogger("main");
|
|
441
|
+
|
|
442
|
+
// `CES_STANDALONE=1` runs CES as an independent, CLI-launched sibling over a
|
|
443
|
+
// Unix socket; otherwise CES is the assistant's stdio child, as today.
|
|
444
|
+
const standalone = process.env["CES_STANDALONE"] === "1";
|
|
309
445
|
|
|
310
446
|
log.info(
|
|
311
|
-
`Starting CES v${CES_PROTOCOL_VERSION} (
|
|
447
|
+
`Starting CES v${CES_PROTOCOL_VERSION} (local mode, ${
|
|
448
|
+
standalone ? "standalone socket" : "stdio"
|
|
449
|
+
} transport)`,
|
|
312
450
|
);
|
|
313
451
|
|
|
314
452
|
const controller = new AbortController();
|
|
@@ -321,57 +459,24 @@ async function main(): Promise<void> {
|
|
|
321
459
|
process.on("SIGTERM", shutdown);
|
|
322
460
|
process.on("SIGINT", shutdown);
|
|
323
461
|
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
? createLocalSecureKeyBackend(
|
|
332
|
-
join(
|
|
333
|
-
process.env["CES_ASSISTANT_DATA_MOUNT"] ?? "/assistant-data-ro",
|
|
334
|
-
".vellum",
|
|
335
|
-
),
|
|
336
|
-
)
|
|
337
|
-
: createLocalSecureKeyBackend(dirname(getSecurityDir()));
|
|
338
|
-
|
|
462
|
+
// Build the credential backend and run one-time migrations before starting
|
|
463
|
+
// the RPC server. Migrations complete synchronously before any connection
|
|
464
|
+
// is accepted — the backend is then passed to buildHandlers so it is not
|
|
465
|
+
// re-instantiated.
|
|
466
|
+
const secureKeyBackend = createLocalSecureKeyBackend(
|
|
467
|
+
dirname(getSecurityDir()),
|
|
468
|
+
);
|
|
339
469
|
await runCesMigrations(
|
|
340
|
-
getCesDataRoot(
|
|
470
|
+
getCesDataRoot("local"),
|
|
341
471
|
secureKeyBackend,
|
|
342
472
|
CES_MIGRATIONS,
|
|
343
473
|
);
|
|
344
|
-
log.info(
|
|
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
|
-
}
|
|
474
|
+
log.info("CES local startup: migrations complete");
|
|
366
475
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
// -- Socket server ---------------------------------------------------------
|
|
373
|
-
const socketPath =
|
|
374
|
-
mode === "managed" ? getBootstrapSocketPath() : getLocalSocketPath();
|
|
476
|
+
// Build the handler registry with all available RPC implementations.
|
|
477
|
+
// The handshake session ID is captured per connection in the server's
|
|
478
|
+
// SessionContext; handlers read it at call time for audit records.
|
|
479
|
+
const handlers = buildHandlers(secureKeyBackend);
|
|
375
480
|
|
|
376
481
|
const rpcLog = getLogger("rpc");
|
|
377
482
|
const rpcLogger = {
|
|
@@ -380,36 +485,43 @@ async function main(): Promise<void> {
|
|
|
380
485
|
error: (msg: string, ...args: unknown[]) => rpcLog.error({ args }, msg),
|
|
381
486
|
};
|
|
382
487
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
488
|
+
if (standalone) {
|
|
489
|
+
// Serve over a Unix socket and run until a shutdown signal — no stdio
|
|
490
|
+
// parent to anchor the lifecycle.
|
|
491
|
+
serveStandaloneSocket({
|
|
492
|
+
socketPath: getLocalSocketPath(),
|
|
493
|
+
handlers,
|
|
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,
|
|
389
516
|
handlers,
|
|
390
|
-
signal: controller.signal,
|
|
391
517
|
logger: rpcLogger,
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
log.info("Assistant API key updated via RPC");
|
|
397
|
-
if (newAssistantId) {
|
|
398
|
-
log.info("Assistant ID updated via RPC");
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
: undefined,
|
|
518
|
+
signal: controller.signal,
|
|
519
|
+
// Local mode reads API keys from env/store directly — no-op handler so
|
|
520
|
+
// update_managed_credential is still registered and returns success.
|
|
521
|
+
onApiKeyUpdate: () => {},
|
|
402
522
|
});
|
|
403
523
|
|
|
404
|
-
await
|
|
405
|
-
if (controller.signal.aborted) {
|
|
406
|
-
resolve();
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
controller.signal.addEventListener("abort", () => resolve(), {
|
|
410
|
-
once: true,
|
|
411
|
-
});
|
|
412
|
-
});
|
|
524
|
+
await server.serve();
|
|
413
525
|
log.info("Server stopped.");
|
|
414
526
|
}
|
|
415
527
|
|
|
@@ -417,7 +529,7 @@ main().catch((err) => {
|
|
|
417
529
|
try {
|
|
418
530
|
getLogger("main").fatal({ err }, "Fatal error");
|
|
419
531
|
} catch {
|
|
420
|
-
process.stderr.write(`[ces
|
|
532
|
+
process.stderr.write(`[ces-local] Fatal: ${err}\n`);
|
|
421
533
|
}
|
|
422
534
|
process.exit(1);
|
|
423
535
|
});
|