@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.
Files changed (65) hide show
  1. package/Dockerfile +1 -1
  2. package/node_modules/@vellumai/service-contracts/package.json +1 -2
  3. package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +104 -0
  4. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +0 -2
  5. package/node_modules/@vellumai/service-contracts/src/attachment-naming.ts +118 -0
  6. package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +3 -5
  7. package/node_modules/@vellumai/service-contracts/src/index.ts +2 -4
  8. package/node_modules/@vellumai/service-contracts/src/rpc.ts +4 -447
  9. package/package.json +2 -3
  10. package/src/__tests__/bulk-set-credentials.test.ts +1 -1
  11. package/src/__tests__/local-standalone.test.ts +5 -36
  12. package/src/__tests__/managed-integration.test.ts +112 -91
  13. package/src/__tests__/managed-reconnect.test.ts +2 -2
  14. package/src/__tests__/transport.test.ts +23 -27
  15. package/src/cli.ts +1 -1
  16. package/src/index.ts +8 -88
  17. package/src/main.ts +228 -340
  18. package/src/paths.ts +4 -20
  19. package/src/server.ts +52 -469
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +0 -686
  21. package/node_modules/@vellumai/service-contracts/src/grants.ts +0 -184
  22. package/node_modules/@vellumai/service-contracts/src/rendering.ts +0 -135
  23. package/src/__tests__/command-executor.test.ts +0 -1879
  24. package/src/__tests__/command-validator.test.ts +0 -1405
  25. package/src/__tests__/command-workspace.test.ts +0 -1050
  26. package/src/__tests__/grant-store.test.ts +0 -689
  27. package/src/__tests__/http-executor.test.ts +0 -1336
  28. package/src/__tests__/http-policy.test.ts +0 -1069
  29. package/src/__tests__/local-materializers.test.ts +0 -860
  30. package/src/__tests__/local-token-refresh.test.ts +0 -361
  31. package/src/__tests__/manage-secure-command-tool.test.ts +0 -134
  32. package/src/__tests__/managed-lazy-getters.test.ts +0 -359
  33. package/src/__tests__/managed-materializers.test.ts +0 -1028
  34. package/src/__tests__/managed-rejection.test.ts +0 -43
  35. package/src/__tests__/toolstore.test.ts +0 -773
  36. package/src/audit/store.ts +0 -188
  37. package/src/commands/auth-adapters.ts +0 -169
  38. package/src/commands/egress-hooks.ts +0 -203
  39. package/src/commands/executor.ts +0 -1155
  40. package/src/commands/output-scan.ts +0 -157
  41. package/src/commands/profiles.ts +0 -286
  42. package/src/commands/validator.ts +0 -702
  43. package/src/commands/workspace.ts +0 -550
  44. package/src/grants/index.ts +0 -17
  45. package/src/grants/persistent-store.ts +0 -309
  46. package/src/grants/rpc-handlers.ts +0 -293
  47. package/src/grants/temporary-store.ts +0 -289
  48. package/src/http/audit.ts +0 -84
  49. package/src/http/executor.ts +0 -684
  50. package/src/http/path-template.ts +0 -245
  51. package/src/http/policy.ts +0 -238
  52. package/src/http/response-filter.ts +0 -233
  53. package/src/managed-errors.ts +0 -9
  54. package/src/managed-lazy-getters.ts +0 -106
  55. package/src/managed-main.ts +0 -822
  56. package/src/materializers/local-oauth-lookup.ts +0 -98
  57. package/src/materializers/local-token-refresh.ts +0 -287
  58. package/src/materializers/local.ts +0 -316
  59. package/src/materializers/managed-platform.ts +0 -295
  60. package/src/subjects/local.ts +0 -177
  61. package/src/subjects/managed.ts +0 -311
  62. package/src/subjects/policy.ts +0 -79
  63. package/src/toolstore/integrity.ts +0 -94
  64. package/src/toolstore/manifest.ts +0 -154
  65. package/src/toolstore/publish.ts +0 -571
@@ -1,550 +0,0 @@
1
- /**
2
- * CES workspace staging and output copyback.
3
- *
4
- * Secure commands execute inside a CES-private scratch directory, never
5
- * directly in the assistant-visible workspace. This module handles:
6
- *
7
- * 1. **Input staging** — Copies declared workspace inputs into a
8
- * CES-private scratch directory and marks them read-only. The command
9
- * reads inputs from the scratch directory, never from the workspace
10
- * directly.
11
- *
12
- * 2. **Output copyback** — After command execution, only declared output
13
- * files are copied from the scratch directory back into the workspace.
14
- * Each output file is validated:
15
- * - It must be declared in the command's output manifest.
16
- * - Its path must not escape the scratch directory (path traversal).
17
- * - It must not be a symlink pointing outside the scratch directory.
18
- * - Its content is scanned for secret leakage before copyback.
19
- *
20
- * This staging model ensures that:
21
- * - Commands cannot write arbitrary files into the workspace.
22
- * - Commands cannot read undeclared workspace files.
23
- * - Secret material never leaks into assistant-visible outputs.
24
- * - The behavior is identical for local and managed CES execution.
25
- */
26
-
27
- import {
28
- copyFileSync,
29
- chmodSync,
30
- existsSync,
31
- lstatSync,
32
- mkdirSync,
33
- readFileSync,
34
- realpathSync,
35
- rmSync,
36
- } from "node:fs";
37
- import { join, resolve, dirname, basename } from "node:path";
38
- import { randomUUID } from "node:crypto";
39
-
40
- import { getCesDataRoot, type CesMode } from "../paths.js";
41
- import { scanOutputFile, type OutputScanResult } from "./output-scan.js";
42
-
43
- // ---------------------------------------------------------------------------
44
- // Types
45
- // ---------------------------------------------------------------------------
46
-
47
- /**
48
- * Declares a file to be staged from the assistant workspace into the
49
- * CES scratch directory before command execution.
50
- */
51
- export interface WorkspaceInput {
52
- /**
53
- * Relative path within the assistant workspace directory.
54
- * Must not contain `..` segments or absolute paths.
55
- */
56
- workspacePath: string;
57
- }
58
-
59
- /**
60
- * Declares a file that the command is expected to produce in the scratch
61
- * directory. Only declared outputs are eligible for copyback.
62
- */
63
- export interface WorkspaceOutput {
64
- /**
65
- * Relative path within the scratch directory where the command writes
66
- * its output. Must not contain `..` segments or absolute paths.
67
- */
68
- scratchPath: string;
69
-
70
- /**
71
- * Relative path within the assistant workspace where the output should
72
- * be copied. Must not contain `..` segments or absolute paths.
73
- */
74
- workspacePath: string;
75
- }
76
-
77
- /**
78
- * Configuration for workspace staging and output copyback.
79
- */
80
- export interface WorkspaceStageConfig {
81
- /** Absolute path to the assistant-visible workspace directory. */
82
- workspaceDir: string;
83
- /** Files to stage as read-only inputs in the scratch directory. */
84
- inputs: WorkspaceInput[];
85
- /** Files to copy back from the scratch directory after execution. */
86
- outputs: WorkspaceOutput[];
87
- /**
88
- * Set of known secret values injected into the command environment.
89
- * Used for output scanning.
90
- */
91
- secrets: ReadonlySet<string>;
92
- }
93
-
94
- /**
95
- * Result of preparing a staged workspace for command execution.
96
- */
97
- export interface StagedWorkspace {
98
- /** Absolute path to the CES-private scratch directory. */
99
- scratchDir: string;
100
- /** List of input files that were staged (relative to scratch dir). */
101
- stagedInputs: string[];
102
- }
103
-
104
- /**
105
- * Result of attempting to copy a single output back to the workspace.
106
- */
107
- export interface OutputCopyResult {
108
- /** The declared scratch path. */
109
- scratchPath: string;
110
- /** The target workspace path. */
111
- workspacePath: string;
112
- /** Whether the copy was successful. */
113
- success: boolean;
114
- /** Reason for failure (undefined when successful). */
115
- reason?: string;
116
- /** Output scan result (undefined when copy was rejected before scanning). */
117
- scanResult?: OutputScanResult;
118
- }
119
-
120
- /**
121
- * Result of the full output copyback phase.
122
- */
123
- export interface CopybackResult {
124
- /** Individual results for each declared output. */
125
- outputs: OutputCopyResult[];
126
- /** Whether all declared outputs were copied successfully. */
127
- allSucceeded: boolean;
128
- }
129
-
130
- // ---------------------------------------------------------------------------
131
- // Path validation helpers
132
- // ---------------------------------------------------------------------------
133
-
134
- /**
135
- * Validate that a relative path does not attempt directory traversal.
136
- * Returns an error string if invalid, undefined if valid.
137
- */
138
- export function validateRelativePath(
139
- relativePath: string,
140
- label: string,
141
- ): string | undefined {
142
- // Must not be absolute
143
- if (relativePath.startsWith("/")) {
144
- return `${label}: "${relativePath}" is an absolute path. Only relative paths are allowed.`;
145
- }
146
-
147
- // Must not contain .. segments
148
- const segments = relativePath.split("/");
149
- for (const seg of segments) {
150
- if (seg === "..") {
151
- return `${label}: "${relativePath}" contains ".." path traversal. This is not allowed.`;
152
- }
153
- }
154
-
155
- // Must not be empty
156
- if (relativePath.trim().length === 0) {
157
- return `${label}: path is empty.`;
158
- }
159
-
160
- return undefined;
161
- }
162
-
163
- /**
164
- * Verify that a resolved path is contained within the expected root
165
- * directory. When the path exists on disk, symlinks are fully resolved
166
- * via `realpathSync` so that symlinked segments cannot escape the root.
167
- * When the path doesn't exist yet, its closest existing ancestor is
168
- * resolved via `realpathSync` to ensure consistent symlink handling
169
- * (e.g. `/tmp` → `/private/tmp` on macOS).
170
- */
171
- export function validateContainedPath(
172
- resolvedPath: string,
173
- rootDir: string,
174
- label: string,
175
- ): string | undefined {
176
- // Resolve symlinks when path exists; fall back to lexical resolve
177
- let normalizedRoot: string;
178
- let normalizedPath: string = resolve(resolvedPath);
179
- try {
180
- normalizedRoot = realpathSync(rootDir);
181
- } catch {
182
- normalizedRoot = resolve(rootDir);
183
- }
184
- try {
185
- normalizedPath = realpathSync(resolvedPath);
186
- } catch {
187
- // Path doesn't exist yet — walk up to the nearest existing ancestor and
188
- // resolve it via realpathSync so that symlinks in parent dirs (e.g.
189
- // /tmp → /private/tmp on macOS) are resolved consistently with the root
190
- // directory. A single dirname call isn't enough for multi-level
191
- // non-existent paths like "reports/output.json" where "reports/" also
192
- // doesn't exist.
193
- let current = resolvedPath;
194
- let resolved = false;
195
- while (!resolved) {
196
- const ancestor = dirname(current);
197
- const tail = resolvedPath.slice(ancestor.length);
198
- try {
199
- normalizedPath = realpathSync(ancestor) + tail;
200
- resolved = true;
201
- } catch {
202
- if (ancestor === current) {
203
- // Reached filesystem root without finding an existing ancestor
204
- normalizedPath = resolve(resolvedPath);
205
- resolved = true;
206
- }
207
- current = ancestor;
208
- }
209
- }
210
- }
211
-
212
- const rootPrefix = normalizedRoot + "/";
213
-
214
- // The path must start with the root directory prefix
215
- // (or be the root directory itself, though that's unusual for files)
216
- if (!normalizedPath.startsWith(rootPrefix) && normalizedPath !== normalizedRoot) {
217
- return `${label}: resolved path "${normalizedPath}" escapes the root directory "${normalizedRoot}".`;
218
- }
219
- return undefined;
220
- }
221
-
222
- /**
223
- * Check if a path (or any of its parent components) involves symlinks
224
- * that resolve outside the given root directory.
225
- *
226
- * Uses `realpathSync` to fully resolve all symlink chains (including
227
- * chained symlinks and symlinked parent directories) and then validates
228
- * that the fully-resolved path is still within the root.
229
- */
230
- export function checkSymlinkEscape(
231
- filePath: string,
232
- rootDir: string,
233
- label: string,
234
- ): string | undefined {
235
- try {
236
- // Fully resolve all symlinks (handles chained symlinks and
237
- // symlinked parent directories in a single call)
238
- const resolvedTarget = realpathSync(filePath);
239
- const resolvedRoot = realpathSync(rootDir);
240
- const rootPrefix = resolvedRoot + "/";
241
-
242
- if (
243
- !resolvedTarget.startsWith(rootPrefix) &&
244
- resolvedTarget !== resolvedRoot
245
- ) {
246
- return `${label}: path "${filePath}" resolves to "${resolvedTarget}" which is outside the scratch directory "${resolvedRoot}".`;
247
- }
248
- } catch {
249
- // If we can't resolve the file, it doesn't exist yet or is inaccessible.
250
- // This will be caught later during the actual copy.
251
- return undefined;
252
- }
253
- return undefined;
254
- }
255
-
256
- // ---------------------------------------------------------------------------
257
- // Scratch directory management
258
- // ---------------------------------------------------------------------------
259
-
260
- /**
261
- * Return the base directory for CES scratch workspaces.
262
- */
263
- export function getScratchBaseDir(mode?: CesMode): string {
264
- return join(getCesDataRoot(mode), "scratch");
265
- }
266
-
267
- /**
268
- * Create a new scratch directory for a command execution.
269
- * Returns the absolute path to the new scratch directory.
270
- */
271
- export function createScratchDir(mode?: CesMode): string {
272
- const scratchBase = getScratchBaseDir(mode);
273
- const scratchDir = join(scratchBase, randomUUID());
274
- mkdirSync(scratchDir, { recursive: true });
275
- return scratchDir;
276
- }
277
-
278
- /**
279
- * Clean up a scratch directory after command execution.
280
- */
281
- export function cleanupScratchDir(scratchDir: string): void {
282
- try {
283
- rmSync(scratchDir, { recursive: true, force: true });
284
- } catch {
285
- // Best-effort cleanup — log but don't fail
286
- }
287
- }
288
-
289
- // ---------------------------------------------------------------------------
290
- // Input staging
291
- // ---------------------------------------------------------------------------
292
-
293
- /**
294
- * Stage workspace inputs into a CES-private scratch directory.
295
- *
296
- * Each declared input is:
297
- * 1. Validated for path traversal.
298
- * 2. Copied from the workspace to the scratch directory.
299
- * 3. Made read-only (chmod 0o444).
300
- *
301
- * @returns The staged workspace descriptor, or throws on validation failure.
302
- */
303
- export function stageInputs(
304
- config: WorkspaceStageConfig,
305
- mode?: CesMode,
306
- ): StagedWorkspace {
307
- const scratchDir = createScratchDir(mode);
308
- const stagedInputs: string[] = [];
309
-
310
- try {
311
- for (const input of config.inputs) {
312
- // Validate relative path
313
- const pathError = validateRelativePath(
314
- input.workspacePath,
315
- "Workspace input",
316
- );
317
- if (pathError) {
318
- throw new Error(pathError);
319
- }
320
-
321
- const sourcePath = join(config.workspaceDir, input.workspacePath);
322
- const destPath = join(scratchDir, input.workspacePath);
323
-
324
- // Validate the resolved source is within the workspace
325
- const containedError = validateContainedPath(
326
- sourcePath,
327
- config.workspaceDir,
328
- "Workspace input source",
329
- );
330
- if (containedError) {
331
- throw new Error(containedError);
332
- }
333
-
334
- // Check source exists
335
- if (!existsSync(sourcePath)) {
336
- throw new Error(
337
- `Workspace input "${input.workspacePath}" does not exist in workspace at "${sourcePath}".`,
338
- );
339
- }
340
-
341
- // Ensure destination directory exists
342
- mkdirSync(dirname(destPath), { recursive: true });
343
-
344
- // Copy the file
345
- copyFileSync(sourcePath, destPath);
346
-
347
- // Make read-only (owner + group + others can read, nobody can write)
348
- chmodSync(destPath, 0o444);
349
-
350
- stagedInputs.push(input.workspacePath);
351
- }
352
- } catch (err) {
353
- // Clean up scratch dir on failure
354
- cleanupScratchDir(scratchDir);
355
- throw err;
356
- }
357
-
358
- return { scratchDir, stagedInputs };
359
- }
360
-
361
- // ---------------------------------------------------------------------------
362
- // Output copyback
363
- // ---------------------------------------------------------------------------
364
-
365
- /**
366
- * Copy declared output files from the scratch directory back to the
367
- * assistant workspace, after validation and scanning.
368
- *
369
- * Each declared output is:
370
- * 1. Validated for path traversal (both scratch and workspace paths).
371
- * 2. Checked that it exists in the scratch directory.
372
- * 3. Checked for symlink escape (must not point outside scratch dir).
373
- * 4. Scanned for secret leakage and auth-bearing artifacts.
374
- * 5. Copied to the workspace if all checks pass.
375
- *
376
- * @returns A {@link CopybackResult} with individual results per output.
377
- */
378
- export function copybackOutputs(
379
- config: WorkspaceStageConfig,
380
- scratchDir: string,
381
- ): CopybackResult {
382
- const results: OutputCopyResult[] = [];
383
-
384
- for (const output of config.outputs) {
385
- const result = copybackSingleOutput(
386
- output,
387
- scratchDir,
388
- config.workspaceDir,
389
- config.secrets,
390
- );
391
- results.push(result);
392
- }
393
-
394
- return {
395
- outputs: results,
396
- allSucceeded: results.every((r) => r.success),
397
- };
398
- }
399
-
400
- /**
401
- * Copy back a single output file with full validation.
402
- */
403
- function copybackSingleOutput(
404
- output: WorkspaceOutput,
405
- scratchDir: string,
406
- workspaceDir: string,
407
- secrets: ReadonlySet<string>,
408
- ): OutputCopyResult {
409
- // -- Validate scratch path
410
- const scratchPathError = validateRelativePath(
411
- output.scratchPath,
412
- "Output scratch path",
413
- );
414
- if (scratchPathError) {
415
- return {
416
- scratchPath: output.scratchPath,
417
- workspacePath: output.workspacePath,
418
- success: false,
419
- reason: scratchPathError,
420
- };
421
- }
422
-
423
- // -- Validate workspace path
424
- const workspacePathError = validateRelativePath(
425
- output.workspacePath,
426
- "Output workspace path",
427
- );
428
- if (workspacePathError) {
429
- return {
430
- scratchPath: output.scratchPath,
431
- workspacePath: output.workspacePath,
432
- success: false,
433
- reason: workspacePathError,
434
- };
435
- }
436
-
437
- const scratchFilePath = join(scratchDir, output.scratchPath);
438
- const workspaceFilePath = join(workspaceDir, output.workspacePath);
439
-
440
- // -- Validate containment (scratch)
441
- const scratchContainedError = validateContainedPath(
442
- scratchFilePath,
443
- scratchDir,
444
- "Output scratch file",
445
- );
446
- if (scratchContainedError) {
447
- return {
448
- scratchPath: output.scratchPath,
449
- workspacePath: output.workspacePath,
450
- success: false,
451
- reason: scratchContainedError,
452
- };
453
- }
454
-
455
- // -- Validate containment (workspace)
456
- const workspaceContainedError = validateContainedPath(
457
- workspaceFilePath,
458
- workspaceDir,
459
- "Output workspace file",
460
- );
461
- if (workspaceContainedError) {
462
- return {
463
- scratchPath: output.scratchPath,
464
- workspacePath: output.workspacePath,
465
- success: false,
466
- reason: workspaceContainedError,
467
- };
468
- }
469
-
470
- // -- Check file exists in scratch
471
- if (!existsSync(scratchFilePath)) {
472
- return {
473
- scratchPath: output.scratchPath,
474
- workspacePath: output.workspacePath,
475
- success: false,
476
- reason: `Output file "${output.scratchPath}" does not exist in scratch directory.`,
477
- };
478
- }
479
-
480
- // -- Check symlink escape
481
- const symlinkError = checkSymlinkEscape(
482
- scratchFilePath,
483
- scratchDir,
484
- "Output file",
485
- );
486
- if (symlinkError) {
487
- return {
488
- scratchPath: output.scratchPath,
489
- workspacePath: output.workspacePath,
490
- success: false,
491
- reason: symlinkError,
492
- };
493
- }
494
-
495
- // -- Read and scan the file
496
- let content: Buffer;
497
- try {
498
- // If it's a symlink, resolve it first to read the actual content
499
- const stat = lstatSync(scratchFilePath);
500
- if (stat.isSymbolicLink()) {
501
- const realPath = realpathSync(scratchFilePath);
502
- content = readFileSync(realPath);
503
- } else {
504
- content = readFileSync(scratchFilePath);
505
- }
506
- } catch (err) {
507
- return {
508
- scratchPath: output.scratchPath,
509
- workspacePath: output.workspacePath,
510
- success: false,
511
- reason: `Failed to read output file "${output.scratchPath}": ${err instanceof Error ? err.message : String(err)}`,
512
- };
513
- }
514
-
515
- const scanResult = scanOutputFile(
516
- basename(output.scratchPath),
517
- content,
518
- secrets,
519
- );
520
-
521
- if (!scanResult.safe) {
522
- return {
523
- scratchPath: output.scratchPath,
524
- workspacePath: output.workspacePath,
525
- success: false,
526
- reason: `Output file "${output.scratchPath}" failed security scan: ${scanResult.violations.join("; ")}`,
527
- scanResult,
528
- };
529
- }
530
-
531
- // -- Copy to workspace
532
- try {
533
- mkdirSync(dirname(workspaceFilePath), { recursive: true });
534
- copyFileSync(scratchFilePath, workspaceFilePath);
535
- } catch (err) {
536
- return {
537
- scratchPath: output.scratchPath,
538
- workspacePath: output.workspacePath,
539
- success: false,
540
- reason: `Failed to copy output to workspace: ${err instanceof Error ? err.message : String(err)}`,
541
- };
542
- }
543
-
544
- return {
545
- scratchPath: output.scratchPath,
546
- workspacePath: output.workspacePath,
547
- success: true,
548
- scanResult,
549
- };
550
- }
@@ -1,17 +0,0 @@
1
- /**
2
- * CES grant stores.
3
- *
4
- * Re-exports the persistent and temporary grant stores used by the
5
- * Credential Execution Service to track user approval decisions.
6
- *
7
- * - **Persistent store**: Durable grants (e.g. `always_allow`) persisted
8
- * to `grants.json` inside the CES-private data root. Survives restarts.
9
- * - **Temporary store**: Ephemeral grants (`allow_once`, `allow_10m`,
10
- * `allow_conversation`) held in memory. Never survives a process restart.
11
- */
12
-
13
- export { PersistentGrantStore } from "./persistent-store.js";
14
- export type { PersistentGrant } from "./persistent-store.js";
15
-
16
- export { TemporaryGrantStore } from "./temporary-store.js";
17
- export type { TemporaryGrant, TemporaryGrantKind } from "./temporary-store.js";