@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/paths.ts
CHANGED
|
@@ -84,21 +84,6 @@ export function getCesDataRoot(mode?: CesMode): string {
|
|
|
84
84
|
// Subdirectory layout
|
|
85
85
|
// ---------------------------------------------------------------------------
|
|
86
86
|
|
|
87
|
-
/** Directory for CES grant persistence. */
|
|
88
|
-
export function getCesGrantsDir(mode?: CesMode): string {
|
|
89
|
-
return join(getCesDataRoot(mode), "grants");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Directory for CES audit log persistence. */
|
|
93
|
-
export function getCesAuditDir(mode?: CesMode): string {
|
|
94
|
-
return join(getCesDataRoot(mode), "audit");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Directory for CES secure tool store (registered secure command tools). */
|
|
98
|
-
export function getCesToolStoreDir(mode?: CesMode): string {
|
|
99
|
-
return join(getCesDataRoot(mode), "toolstore");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
87
|
/** Directory for CES log files. */
|
|
103
88
|
export function getCesLogDir(mode?: CesMode): string {
|
|
104
89
|
return join(getCesDataRoot(mode), "logs");
|
|
@@ -141,7 +126,7 @@ export function getBootstrapSocketPath(): string {
|
|
|
141
126
|
}
|
|
142
127
|
|
|
143
128
|
// ---------------------------------------------------------------------------
|
|
144
|
-
// Local-mode
|
|
129
|
+
// Local-mode CES socket
|
|
145
130
|
// ---------------------------------------------------------------------------
|
|
146
131
|
|
|
147
132
|
/** Default local-mode CES socket filename (under the local data root). */
|
|
@@ -150,10 +135,9 @@ const LOCAL_SOCKET_NAME = "ces.sock";
|
|
|
150
135
|
/**
|
|
151
136
|
* Return the path to the local-mode CES Unix socket.
|
|
152
137
|
*
|
|
153
|
-
* Used when local CES runs as a
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* directory permissions are the access boundary.
|
|
138
|
+
* Used when local CES runs as a CLI-launched sibling process. The socket
|
|
139
|
+
* lives under the CES-private local data root, whose directory permissions
|
|
140
|
+
* are the access boundary.
|
|
157
141
|
*
|
|
158
142
|
* Priority:
|
|
159
143
|
* 1. `CES_LOCAL_SOCKET` env var (full file path override; the CLI sets this
|
package/src/server.ts
CHANGED
|
@@ -20,35 +20,13 @@ import {
|
|
|
20
20
|
CES_PROTOCOL_VERSION,
|
|
21
21
|
CesRpcMethod,
|
|
22
22
|
CesRpcSchemas,
|
|
23
|
-
hashProposal,
|
|
24
|
-
type CommandGrantProposal,
|
|
25
23
|
type HandshakeAck,
|
|
26
24
|
type HandshakeRequest,
|
|
27
|
-
type MakeAuthenticatedRequest,
|
|
28
|
-
type ManageSecureCommandTool,
|
|
29
|
-
type ManageSecureCommandToolResponse,
|
|
30
25
|
type RpcEnvelope,
|
|
31
|
-
type RunAuthenticatedCommand,
|
|
32
|
-
type RunAuthenticatedCommandResponse,
|
|
33
26
|
type TransportMessage,
|
|
34
27
|
TransportMessageSchema,
|
|
35
28
|
} from "@vellumai/service-contracts/credential-rpc";
|
|
36
29
|
|
|
37
|
-
import { resolve } from "node:path";
|
|
38
|
-
|
|
39
|
-
import {
|
|
40
|
-
executeAuthenticatedCommand,
|
|
41
|
-
type CommandExecutorDeps,
|
|
42
|
-
type ExecuteCommandRequest,
|
|
43
|
-
} from "./commands/executor.js";
|
|
44
|
-
|
|
45
|
-
import { validateContainedPath } from "./commands/workspace.js";
|
|
46
|
-
|
|
47
|
-
import {
|
|
48
|
-
executeAuthenticatedHttpRequest,
|
|
49
|
-
type HttpExecutorDeps,
|
|
50
|
-
} from "./http/executor.js";
|
|
51
|
-
|
|
52
30
|
// ---------------------------------------------------------------------------
|
|
53
31
|
// Types
|
|
54
32
|
// ---------------------------------------------------------------------------
|
|
@@ -59,13 +37,8 @@ import {
|
|
|
59
37
|
* Each accepted connection owns one `SessionContext`, created when the server
|
|
60
38
|
* is constructed and populated with the negotiated session ID at handshake.
|
|
61
39
|
* Handlers receive it as their second argument and read the session ID at call
|
|
62
|
-
* time
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* Identity that does not vary across a daemon's connections — the assistant
|
|
66
|
-
* API key and assistant ID — deliberately lives outside this context
|
|
67
|
-
* (process-global, see `managed-lazy-getters.ts`); only the per-connection
|
|
68
|
-
* session ID belongs here.
|
|
40
|
+
* time, so a handler registry shared across connections can attribute each call
|
|
41
|
+
* to the originating connection.
|
|
69
42
|
*/
|
|
70
43
|
export interface SessionContext {
|
|
71
44
|
/** The RPC session ID negotiated at handshake. */
|
|
@@ -86,9 +59,7 @@ export type RpcMethodHandler<TReq = unknown, TRes = unknown> = (
|
|
|
86
59
|
/**
|
|
87
60
|
* Registry of method name to handler function.
|
|
88
61
|
*/
|
|
89
|
-
export type RpcHandlerRegistry = Partial<
|
|
90
|
-
Record<string, RpcMethodHandler>
|
|
91
|
-
>;
|
|
62
|
+
export type RpcHandlerRegistry = Partial<Record<string, RpcMethodHandler>>;
|
|
92
63
|
|
|
93
64
|
export interface CesServerOptions {
|
|
94
65
|
/** Readable stream to consume messages from. */
|
|
@@ -102,7 +73,11 @@ export interface CesServerOptions {
|
|
|
102
73
|
/** Optional abort signal to shut down the server. */
|
|
103
74
|
signal?: AbortSignal;
|
|
104
75
|
/** Callback invoked when the handshake completes with the negotiated session ID and optional API key / assistant ID. */
|
|
105
|
-
onHandshakeComplete?: (
|
|
76
|
+
onHandshakeComplete?: (
|
|
77
|
+
sessionId: string,
|
|
78
|
+
assistantApiKey?: string,
|
|
79
|
+
assistantId?: string,
|
|
80
|
+
) => void;
|
|
106
81
|
/** Callback invoked when the assistant pushes an updated API key (and optionally assistant ID) after hatch. */
|
|
107
82
|
onApiKeyUpdate?: (assistantApiKey: string, assistantId?: string) => void;
|
|
108
83
|
}
|
|
@@ -122,7 +97,11 @@ export class CesRpcServer {
|
|
|
122
97
|
private readonly handlers: RpcHandlerRegistry;
|
|
123
98
|
private readonly logger: Pick<Console, "log" | "warn" | "error">;
|
|
124
99
|
private readonly signal?: AbortSignal;
|
|
125
|
-
private readonly onHandshakeComplete?: (
|
|
100
|
+
private readonly onHandshakeComplete?: (
|
|
101
|
+
sessionId: string,
|
|
102
|
+
assistantApiKey?: string,
|
|
103
|
+
assistantId?: string,
|
|
104
|
+
) => void;
|
|
126
105
|
|
|
127
106
|
private handshakeComplete = false;
|
|
128
107
|
/**
|
|
@@ -145,8 +124,13 @@ export class CesRpcServer {
|
|
|
145
124
|
// Auto-register the update_managed_credential handler if a callback is provided.
|
|
146
125
|
if (options.onApiKeyUpdate) {
|
|
147
126
|
const onUpdate = options.onApiKeyUpdate;
|
|
148
|
-
this.handlers[CesRpcMethod.UpdateManagedCredential] = (
|
|
149
|
-
|
|
127
|
+
this.handlers[CesRpcMethod.UpdateManagedCredential] = (
|
|
128
|
+
request: unknown,
|
|
129
|
+
) => {
|
|
130
|
+
const { assistantApiKey, assistantId } = request as {
|
|
131
|
+
assistantApiKey: string;
|
|
132
|
+
assistantId?: string;
|
|
133
|
+
};
|
|
150
134
|
onUpdate(assistantApiKey, assistantId);
|
|
151
135
|
return { updated: true };
|
|
152
136
|
};
|
|
@@ -176,7 +160,8 @@ export class CesRpcServer {
|
|
|
176
160
|
|
|
177
161
|
this.input.on("data", (chunk: Buffer | string) => {
|
|
178
162
|
if (this.closed) return;
|
|
179
|
-
this.buffer +=
|
|
163
|
+
this.buffer +=
|
|
164
|
+
typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
180
165
|
this.processBuffer();
|
|
181
166
|
});
|
|
182
167
|
|
|
@@ -213,7 +198,9 @@ export class CesRpcServer {
|
|
|
213
198
|
if (this.closed) return;
|
|
214
199
|
this.closed = true;
|
|
215
200
|
this.input.destroy();
|
|
216
|
-
if (
|
|
201
|
+
if (
|
|
202
|
+
typeof (this.output as { destroy?: () => void }).destroy === "function"
|
|
203
|
+
) {
|
|
217
204
|
this.output.destroy();
|
|
218
205
|
}
|
|
219
206
|
}
|
|
@@ -255,7 +242,9 @@ export class CesRpcServer {
|
|
|
255
242
|
|
|
256
243
|
if (msg.type === "handshake_request") {
|
|
257
244
|
if (this.handshakeComplete) {
|
|
258
|
-
this.logger.warn(
|
|
245
|
+
this.logger.warn(
|
|
246
|
+
"[ces-server] Duplicate handshake_request after session established; rejecting",
|
|
247
|
+
);
|
|
259
248
|
const ack: HandshakeAck = {
|
|
260
249
|
type: "handshake_ack",
|
|
261
250
|
protocolVersion: CES_PROTOCOL_VERSION,
|
|
@@ -269,7 +258,9 @@ export class CesRpcServer {
|
|
|
269
258
|
this.handleHandshake(msg as HandshakeRequest);
|
|
270
259
|
} else if (msg.type === "rpc") {
|
|
271
260
|
this.handleRpcEnvelope(msg as unknown as RpcEnvelope).catch((err) => {
|
|
272
|
-
this.logger.error(
|
|
261
|
+
this.logger.error(
|
|
262
|
+
`[ces-server] Unhandled error in RPC handler: ${err}`,
|
|
263
|
+
);
|
|
273
264
|
});
|
|
274
265
|
} else {
|
|
275
266
|
this.logger.warn("[ces-server] Unexpected message type:", msg.type);
|
|
@@ -283,14 +274,22 @@ export class CesRpcServer {
|
|
|
283
274
|
protocolVersion: CES_PROTOCOL_VERSION,
|
|
284
275
|
sessionId: req.sessionId,
|
|
285
276
|
accepted,
|
|
286
|
-
...(accepted
|
|
277
|
+
...(accepted
|
|
278
|
+
? {}
|
|
279
|
+
: { reason: `Unsupported protocol version: ${req.protocolVersion}` }),
|
|
287
280
|
};
|
|
288
281
|
|
|
289
282
|
if (accepted) {
|
|
290
283
|
this.handshakeComplete = true;
|
|
291
284
|
this.sessionContext.sessionId = req.sessionId;
|
|
292
|
-
this.logger.log(
|
|
293
|
-
|
|
285
|
+
this.logger.log(
|
|
286
|
+
`[ces-server] Handshake accepted for session ${req.sessionId}`,
|
|
287
|
+
);
|
|
288
|
+
this.onHandshakeComplete?.(
|
|
289
|
+
req.sessionId,
|
|
290
|
+
req.assistantApiKey,
|
|
291
|
+
req.assistantId,
|
|
292
|
+
);
|
|
294
293
|
} else {
|
|
295
294
|
this.logger.warn(
|
|
296
295
|
`[ces-server] Handshake rejected: version mismatch (got ${req.protocolVersion}, expected ${CES_PROTOCOL_VERSION})`,
|
|
@@ -303,7 +302,11 @@ export class CesRpcServer {
|
|
|
303
302
|
private async handleRpcEnvelope(envelope: RpcEnvelope): Promise<void> {
|
|
304
303
|
if (!this.handshakeComplete) {
|
|
305
304
|
this.logger.warn("[ces-server] RPC received before handshake; ignoring");
|
|
306
|
-
this.sendRpcError(
|
|
305
|
+
this.sendRpcError(
|
|
306
|
+
envelope,
|
|
307
|
+
"HANDSHAKE_REQUIRED",
|
|
308
|
+
"Handshake not completed",
|
|
309
|
+
);
|
|
307
310
|
return;
|
|
308
311
|
}
|
|
309
312
|
|
|
@@ -316,7 +319,11 @@ export class CesRpcServer {
|
|
|
316
319
|
const handler = this.handlers[method];
|
|
317
320
|
|
|
318
321
|
if (!handler) {
|
|
319
|
-
this.sendRpcError(
|
|
322
|
+
this.sendRpcError(
|
|
323
|
+
envelope,
|
|
324
|
+
"METHOD_NOT_FOUND",
|
|
325
|
+
`Unknown method: ${method}`,
|
|
326
|
+
);
|
|
320
327
|
return;
|
|
321
328
|
}
|
|
322
329
|
|
|
@@ -384,47 +391,6 @@ export class CesRpcServer {
|
|
|
384
391
|
}
|
|
385
392
|
}
|
|
386
393
|
|
|
387
|
-
// ---------------------------------------------------------------------------
|
|
388
|
-
// Handler factory: make_authenticated_request
|
|
389
|
-
// ---------------------------------------------------------------------------
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Create a handler function for the `make_authenticated_request` RPC method.
|
|
393
|
-
*
|
|
394
|
-
* Binds the executor to the provided dependencies so it can be registered in
|
|
395
|
-
* the RPC handler registry. The per-connection session ID is merged in from
|
|
396
|
-
* the SessionContext at call time (for audit attribution); all other deps —
|
|
397
|
-
* including the managed subject/materializer options — are taken as supplied.
|
|
398
|
-
*/
|
|
399
|
-
export function createMakeAuthenticatedRequestHandler(
|
|
400
|
-
deps: HttpExecutorDeps,
|
|
401
|
-
): RpcMethodHandler {
|
|
402
|
-
return async (request: unknown, ctx: SessionContext) => {
|
|
403
|
-
return executeAuthenticatedHttpRequest(request as MakeAuthenticatedRequest, {
|
|
404
|
-
...deps,
|
|
405
|
-
sessionId: ctx.sessionId,
|
|
406
|
-
});
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Build an RPC handler registry that includes the `make_authenticated_request`
|
|
412
|
-
* handler alongside any additional handlers.
|
|
413
|
-
*
|
|
414
|
-
* This is a convenience helper for callers that want to wire up the HTTP
|
|
415
|
-
* executor without manually constructing the registry.
|
|
416
|
-
*/
|
|
417
|
-
export function buildHandlersWithHttp(
|
|
418
|
-
httpDeps: HttpExecutorDeps,
|
|
419
|
-
additionalHandlers?: RpcHandlerRegistry,
|
|
420
|
-
): RpcHandlerRegistry {
|
|
421
|
-
return {
|
|
422
|
-
...additionalHandlers,
|
|
423
|
-
[CesRpcMethod.MakeAuthenticatedRequest]:
|
|
424
|
-
createMakeAuthenticatedRequestHandler(httpDeps),
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
|
|
428
394
|
// ---------------------------------------------------------------------------
|
|
429
395
|
// Factory helper
|
|
430
396
|
// ---------------------------------------------------------------------------
|
|
@@ -438,386 +404,3 @@ export function buildHandlersWithHttp(
|
|
|
438
404
|
export function createCesServer(options: CesServerOptions): CesRpcServer {
|
|
439
405
|
return new CesRpcServer(options);
|
|
440
406
|
}
|
|
441
|
-
|
|
442
|
-
// ---------------------------------------------------------------------------
|
|
443
|
-
// run_authenticated_command handler factory
|
|
444
|
-
// ---------------------------------------------------------------------------
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* Options for creating the `run_authenticated_command` RPC handler.
|
|
448
|
-
*/
|
|
449
|
-
export interface RunAuthenticatedCommandHandlerOptions {
|
|
450
|
-
/** Dependencies for the command executor. */
|
|
451
|
-
executorDeps: CommandExecutorDeps;
|
|
452
|
-
/**
|
|
453
|
-
* Default workspace directory for commands that don't specify one.
|
|
454
|
-
* Typically the assistant's workspace root.
|
|
455
|
-
*/
|
|
456
|
-
defaultWorkspaceDir: string;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Create an RPC handler for the `run_authenticated_command` method.
|
|
461
|
-
*
|
|
462
|
-
* This handler:
|
|
463
|
-
* 1. Parses the `command` string into a bundleDigest, profileName, and argv.
|
|
464
|
-
* The expected format is: `<bundleDigest>/<profileName> <argv...>`
|
|
465
|
-
* 2. Delegates to `executeAuthenticatedCommand` for the full security pipeline.
|
|
466
|
-
* 3. Returns a `RunAuthenticatedCommandResponse` with the execution result.
|
|
467
|
-
*
|
|
468
|
-
* If the command string doesn't match the expected format (i.e. it's a
|
|
469
|
-
* plain shell command), the handler returns a structured error since only
|
|
470
|
-
* manifest-driven secure commands are supported.
|
|
471
|
-
*/
|
|
472
|
-
export function createRunAuthenticatedCommandHandler(
|
|
473
|
-
options: RunAuthenticatedCommandHandlerOptions,
|
|
474
|
-
): RpcMethodHandler<RunAuthenticatedCommand, RunAuthenticatedCommandResponse> {
|
|
475
|
-
return async (request, ctx) => {
|
|
476
|
-
// Parse the command string into bundle-digest/profile and argv
|
|
477
|
-
const parseResult = parseCommandString(request.command);
|
|
478
|
-
if (!parseResult.ok) {
|
|
479
|
-
return {
|
|
480
|
-
success: false,
|
|
481
|
-
error: { code: "INVALID_COMMAND", message: parseResult.error },
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Validate cwd when inputs or outputs are present — the workspace
|
|
486
|
-
// staging/copyback pipeline resolves paths relative to workspaceDir,
|
|
487
|
-
// so an unvalidated cwd could let a caller read/write outside the
|
|
488
|
-
// assistant workspace.
|
|
489
|
-
const workspaceDir = request.cwd ?? options.defaultWorkspaceDir;
|
|
490
|
-
const hasWorkspaceIO =
|
|
491
|
-
(request.inputs && request.inputs.length > 0) ||
|
|
492
|
-
(request.outputs && request.outputs.length > 0);
|
|
493
|
-
|
|
494
|
-
if (hasWorkspaceIO && request.cwd) {
|
|
495
|
-
const resolvedCwd = resolve(request.cwd);
|
|
496
|
-
const cwdError = validateContainedPath(
|
|
497
|
-
resolvedCwd,
|
|
498
|
-
options.defaultWorkspaceDir,
|
|
499
|
-
"Command cwd",
|
|
500
|
-
);
|
|
501
|
-
if (cwdError) {
|
|
502
|
-
return {
|
|
503
|
-
success: false,
|
|
504
|
-
error: {
|
|
505
|
-
code: "INVALID_CWD",
|
|
506
|
-
message:
|
|
507
|
-
`cwd cannot be used with inputs/outputs when it resolves outside ` +
|
|
508
|
-
`the workspace directory: ${cwdError}`,
|
|
509
|
-
},
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const execRequest: ExecuteCommandRequest = {
|
|
515
|
-
bundleDigest: parseResult.bundleDigest,
|
|
516
|
-
profileName: parseResult.profileName,
|
|
517
|
-
credentialHandle: request.credentialHandle,
|
|
518
|
-
argv: parseResult.argv,
|
|
519
|
-
workspaceDir,
|
|
520
|
-
inputs: request.inputs,
|
|
521
|
-
outputs: request.outputs,
|
|
522
|
-
purpose: request.purpose,
|
|
523
|
-
grantId: request.grantId,
|
|
524
|
-
conversationId: request.conversationId,
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
// Bind the per-connection session ID (for audit attribution) into the
|
|
528
|
-
// executor deps for this call.
|
|
529
|
-
const result = await executeAuthenticatedCommand(execRequest, {
|
|
530
|
-
...options.executorDeps,
|
|
531
|
-
sessionId: ctx.sessionId,
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
// If the failure was due to a missing grant, return a structured
|
|
535
|
-
// APPROVAL_REQUIRED response with the proposal so the approval
|
|
536
|
-
// bridge can activate.
|
|
537
|
-
if (!result.success && result.approvalRequired) {
|
|
538
|
-
const { credentialHandle, bundleDigest, profileName, command, purpose } =
|
|
539
|
-
result.approvalRequired;
|
|
540
|
-
|
|
541
|
-
const proposal: CommandGrantProposal = {
|
|
542
|
-
type: "command",
|
|
543
|
-
credentialHandle,
|
|
544
|
-
command,
|
|
545
|
-
purpose,
|
|
546
|
-
allowedCommandPatterns: [`${credentialHandle}:${bundleDigest}:${profileName}`],
|
|
547
|
-
};
|
|
548
|
-
|
|
549
|
-
return {
|
|
550
|
-
success: false,
|
|
551
|
-
error: {
|
|
552
|
-
code: "APPROVAL_REQUIRED",
|
|
553
|
-
message: `No active grant covers this command. Approval is required.`,
|
|
554
|
-
details: {
|
|
555
|
-
proposal,
|
|
556
|
-
proposalHash: hashProposal(proposal),
|
|
557
|
-
},
|
|
558
|
-
},
|
|
559
|
-
auditId: result.auditId,
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
return {
|
|
564
|
-
success: result.success,
|
|
565
|
-
exitCode: result.exitCode,
|
|
566
|
-
stdout: result.stdout,
|
|
567
|
-
stderr: result.stderr,
|
|
568
|
-
error: result.error
|
|
569
|
-
? { code: "EXECUTION_ERROR", message: result.error }
|
|
570
|
-
: undefined,
|
|
571
|
-
auditId: result.auditId,
|
|
572
|
-
};
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
/**
|
|
577
|
-
* Parse a CES command string into bundle digest, profile name, and argv.
|
|
578
|
-
*
|
|
579
|
-
* Expected format: `<bundleDigest>/<profileName> [argv...]`
|
|
580
|
-
*
|
|
581
|
-
* Examples:
|
|
582
|
-
* - `abc123def.../api-read api /repos/owner/repo --method GET`
|
|
583
|
-
* - `abc123def.../list-repos`
|
|
584
|
-
*/
|
|
585
|
-
function parseCommandString(
|
|
586
|
-
command: string,
|
|
587
|
-
): { ok: true; bundleDigest: string; profileName: string; argv: string[] }
|
|
588
|
-
| { ok: false; error: string } {
|
|
589
|
-
const trimmed = command.trim();
|
|
590
|
-
if (!trimmed) {
|
|
591
|
-
return { ok: false, error: "Command string is empty" };
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// Split on first space to separate the bundle/profile reference from argv
|
|
595
|
-
const firstSpaceIdx = trimmed.indexOf(" ");
|
|
596
|
-
const ref = firstSpaceIdx === -1 ? trimmed : trimmed.slice(0, firstSpaceIdx);
|
|
597
|
-
const argvStr = firstSpaceIdx === -1 ? "" : trimmed.slice(firstSpaceIdx + 1).trim();
|
|
598
|
-
|
|
599
|
-
// Parse the reference: <bundleDigest>/<profileName>
|
|
600
|
-
const slashIdx = ref.indexOf("/");
|
|
601
|
-
if (slashIdx === -1 || slashIdx === 0 || slashIdx === ref.length - 1) {
|
|
602
|
-
return {
|
|
603
|
-
ok: false,
|
|
604
|
-
error: `Invalid command reference "${ref}". Expected format: "<bundleDigest>/<profileName> [argv...]"`,
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const bundleDigest = ref.slice(0, slashIdx);
|
|
609
|
-
const profileName = ref.slice(slashIdx + 1);
|
|
610
|
-
|
|
611
|
-
// Parse argv — split on whitespace (simple tokenization)
|
|
612
|
-
const argv = argvStr ? argvStr.split(/\s+/).filter((s) => s.length > 0) : [];
|
|
613
|
-
|
|
614
|
-
return { ok: true, bundleDigest, profileName, argv };
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
/**
|
|
618
|
-
* Convenience helper to register the `run_authenticated_command` handler
|
|
619
|
-
* into an RPC handler registry.
|
|
620
|
-
*/
|
|
621
|
-
export function registerCommandExecutionHandler(
|
|
622
|
-
registry: RpcHandlerRegistry,
|
|
623
|
-
options: RunAuthenticatedCommandHandlerOptions,
|
|
624
|
-
): void {
|
|
625
|
-
registry[CesRpcMethod.RunAuthenticatedCommand] =
|
|
626
|
-
createRunAuthenticatedCommandHandler(options) as RpcMethodHandler;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// ---------------------------------------------------------------------------
|
|
630
|
-
// manage_secure_command_tool handler factory
|
|
631
|
-
// ---------------------------------------------------------------------------
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
* Dependencies for the `manage_secure_command_tool` handler.
|
|
635
|
-
*/
|
|
636
|
-
export interface ManageSecureCommandToolHandlerDeps {
|
|
637
|
-
/**
|
|
638
|
-
* Download bundle bytes from the given HTTPS URL.
|
|
639
|
-
* Implementations should enforce size limits and timeouts.
|
|
640
|
-
*/
|
|
641
|
-
downloadBundle: (sourceUrl: string) => Promise<Buffer | Uint8Array>;
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Publish a bundle into the CES-private toolstore.
|
|
645
|
-
* Typically delegates to `publishBundle()` from `./toolstore/publish.js`.
|
|
646
|
-
*/
|
|
647
|
-
publishBundle: (request: import("./toolstore/publish.js").PublishRequest) => import("./toolstore/publish.js").PublishResult;
|
|
648
|
-
|
|
649
|
-
/**
|
|
650
|
-
* Unregister/remove a tool from the tool registry by name.
|
|
651
|
-
* Returns true if the tool was found and removed.
|
|
652
|
-
*/
|
|
653
|
-
unregisterTool: (toolName: string) => boolean;
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
* Register a tool in the tool registry after successful publication.
|
|
657
|
-
* Called with the tool name, credential handle, description, and the
|
|
658
|
-
* bundle digest for runtime lookup.
|
|
659
|
-
*/
|
|
660
|
-
registerTool: (entry: {
|
|
661
|
-
toolName: string;
|
|
662
|
-
credentialHandle: string;
|
|
663
|
-
description: string;
|
|
664
|
-
bundleDigest: string;
|
|
665
|
-
}) => void;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
/**
|
|
669
|
-
* Create an RPC handler for the `manage_secure_command_tool` method.
|
|
670
|
-
*
|
|
671
|
-
* This handler:
|
|
672
|
-
* 1. For "register" actions: validates required bundle metadata fields,
|
|
673
|
-
* downloads the bundle from `sourceUrl`, publishes it into the
|
|
674
|
-
* immutable toolstore with digest verification, and registers
|
|
675
|
-
* the tool entry.
|
|
676
|
-
* 2. For "unregister" actions: removes the tool from the registry.
|
|
677
|
-
*/
|
|
678
|
-
export function createManageSecureCommandToolHandler(
|
|
679
|
-
deps: ManageSecureCommandToolHandlerDeps,
|
|
680
|
-
): RpcMethodHandler<ManageSecureCommandTool, ManageSecureCommandToolResponse> {
|
|
681
|
-
// Serialize all manage_secure_command_tool operations. The register path
|
|
682
|
-
// awaits a bundle download mid-handler; during that await a concurrent
|
|
683
|
-
// unregister would run its "still in use?" check + bundle delete against a
|
|
684
|
-
// registry that doesn't yet reflect the in-flight registration — transiently
|
|
685
|
-
// deleting a bundle another caller is publishing or executing. Running these
|
|
686
|
-
// operations one-at-a-time closes that window. The registry and toolstore are
|
|
687
|
-
// process-global, so this single chain serializes tool management across
|
|
688
|
-
// every connection.
|
|
689
|
-
let tail: Promise<unknown> = Promise.resolve();
|
|
690
|
-
|
|
691
|
-
const handle = async (
|
|
692
|
-
request: ManageSecureCommandTool,
|
|
693
|
-
): Promise<ManageSecureCommandToolResponse> => {
|
|
694
|
-
if (request.action === "unregister") {
|
|
695
|
-
const removed = deps.unregisterTool(request.toolName);
|
|
696
|
-
if (!removed) {
|
|
697
|
-
return {
|
|
698
|
-
success: false,
|
|
699
|
-
error: {
|
|
700
|
-
code: "TOOL_NOT_FOUND",
|
|
701
|
-
message: `Tool "${request.toolName}" is not registered.`,
|
|
702
|
-
},
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
return { success: true };
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// action === "register"
|
|
709
|
-
const missing: string[] = [];
|
|
710
|
-
if (!request.bundleId) missing.push("bundleId");
|
|
711
|
-
if (!request.version) missing.push("version");
|
|
712
|
-
if (!request.sourceUrl) missing.push("sourceUrl");
|
|
713
|
-
if (!request.sha256) missing.push("sha256");
|
|
714
|
-
if (!request.credentialHandle) missing.push("credentialHandle");
|
|
715
|
-
if (!request.description) missing.push("description");
|
|
716
|
-
if (!request.secureCommandManifest) missing.push("secureCommandManifest");
|
|
717
|
-
if (missing.length > 0) {
|
|
718
|
-
return {
|
|
719
|
-
success: false,
|
|
720
|
-
error: {
|
|
721
|
-
code: "MISSING_FIELDS",
|
|
722
|
-
message: `Register action requires: ${missing.join(", ")}`,
|
|
723
|
-
},
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// Validate HTTPS before downloading — CES is the security boundary
|
|
728
|
-
// and must not rely on the caller for URL scheme validation.
|
|
729
|
-
try {
|
|
730
|
-
const parsed = new URL(request.sourceUrl!);
|
|
731
|
-
if (parsed.protocol !== "https:") {
|
|
732
|
-
return {
|
|
733
|
-
success: false,
|
|
734
|
-
error: {
|
|
735
|
-
code: "INVALID_SOURCE_URL",
|
|
736
|
-
message: "sourceUrl must use HTTPS for secure bundle downloads.",
|
|
737
|
-
},
|
|
738
|
-
};
|
|
739
|
-
}
|
|
740
|
-
} catch {
|
|
741
|
-
return {
|
|
742
|
-
success: false,
|
|
743
|
-
error: {
|
|
744
|
-
code: "INVALID_SOURCE_URL",
|
|
745
|
-
message: "sourceUrl is not a valid URL.",
|
|
746
|
-
},
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// Download the bundle
|
|
751
|
-
let bundleBytes: Buffer | Uint8Array;
|
|
752
|
-
try {
|
|
753
|
-
bundleBytes = await deps.downloadBundle(request.sourceUrl!);
|
|
754
|
-
} catch (err) {
|
|
755
|
-
return {
|
|
756
|
-
success: false,
|
|
757
|
-
error: {
|
|
758
|
-
code: "DOWNLOAD_FAILED",
|
|
759
|
-
message: `Failed to download bundle from ${request.sourceUrl}: ${err instanceof Error ? err.message : String(err)}`,
|
|
760
|
-
},
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// The caller provides the full secure command manifest via the RPC
|
|
765
|
-
// payload. Cast to the internal type — publishBundle() validates it.
|
|
766
|
-
const secureCommandManifest =
|
|
767
|
-
request.secureCommandManifest as unknown as import("./commands/profiles.js").SecureCommandManifest;
|
|
768
|
-
|
|
769
|
-
// Publish into the immutable toolstore (includes digest verification)
|
|
770
|
-
const publishResult = deps.publishBundle({
|
|
771
|
-
bundleBytes,
|
|
772
|
-
expectedDigest: request.sha256!,
|
|
773
|
-
bundleId: request.bundleId!,
|
|
774
|
-
version: request.version!,
|
|
775
|
-
sourceUrl: request.sourceUrl!,
|
|
776
|
-
secureCommandManifest,
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
if (!publishResult.success) {
|
|
780
|
-
return {
|
|
781
|
-
success: false,
|
|
782
|
-
error: {
|
|
783
|
-
code: "PUBLISH_FAILED",
|
|
784
|
-
message: publishResult.error ?? "Unknown publish error",
|
|
785
|
-
},
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// Register the tool entry for runtime lookup
|
|
790
|
-
deps.registerTool({
|
|
791
|
-
toolName: request.toolName,
|
|
792
|
-
credentialHandle: request.credentialHandle!,
|
|
793
|
-
description: request.description!,
|
|
794
|
-
bundleDigest: request.sha256!,
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
return { success: true };
|
|
798
|
-
};
|
|
799
|
-
|
|
800
|
-
return (request) => {
|
|
801
|
-
// Chain each operation onto the previous one so they never interleave.
|
|
802
|
-
const result = tail.then(() => handle(request));
|
|
803
|
-
// Keep the chain alive regardless of this op's outcome — a rejection must
|
|
804
|
-
// not break serialization for subsequent operations.
|
|
805
|
-
tail = result.then(
|
|
806
|
-
() => undefined,
|
|
807
|
-
() => undefined,
|
|
808
|
-
);
|
|
809
|
-
return result;
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
/**
|
|
814
|
-
* Convenience helper to register the `manage_secure_command_tool` handler
|
|
815
|
-
* into an RPC handler registry.
|
|
816
|
-
*/
|
|
817
|
-
export function registerManageSecureCommandToolHandler(
|
|
818
|
-
registry: RpcHandlerRegistry,
|
|
819
|
-
deps: ManageSecureCommandToolHandlerDeps,
|
|
820
|
-
): void {
|
|
821
|
-
registry[CesRpcMethod.ManageSecureCommandTool] =
|
|
822
|
-
createManageSecureCommandToolHandler(deps) as RpcMethodHandler;
|
|
823
|
-
}
|