@vellumai/credential-executor 0.4.55

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 (42) hide show
  1. package/Dockerfile +55 -0
  2. package/bun.lock +37 -0
  3. package/package.json +32 -0
  4. package/src/__tests__/command-executor.test.ts +1333 -0
  5. package/src/__tests__/command-validator.test.ts +708 -0
  6. package/src/__tests__/command-workspace.test.ts +997 -0
  7. package/src/__tests__/grant-store.test.ts +467 -0
  8. package/src/__tests__/http-executor.test.ts +1251 -0
  9. package/src/__tests__/http-policy.test.ts +970 -0
  10. package/src/__tests__/local-materializers.test.ts +826 -0
  11. package/src/__tests__/managed-materializers.test.ts +961 -0
  12. package/src/__tests__/toolstore.test.ts +539 -0
  13. package/src/__tests__/transport.test.ts +388 -0
  14. package/src/audit/store.ts +188 -0
  15. package/src/commands/auth-adapters.ts +169 -0
  16. package/src/commands/executor.ts +840 -0
  17. package/src/commands/output-scan.ts +157 -0
  18. package/src/commands/profiles.ts +282 -0
  19. package/src/commands/validator.ts +438 -0
  20. package/src/commands/workspace.ts +512 -0
  21. package/src/grants/index.ts +17 -0
  22. package/src/grants/persistent-store.ts +247 -0
  23. package/src/grants/rpc-handlers.ts +269 -0
  24. package/src/grants/temporary-store.ts +219 -0
  25. package/src/http/audit.ts +84 -0
  26. package/src/http/executor.ts +540 -0
  27. package/src/http/path-template.ts +179 -0
  28. package/src/http/policy.ts +256 -0
  29. package/src/http/response-filter.ts +233 -0
  30. package/src/index.ts +106 -0
  31. package/src/main.ts +263 -0
  32. package/src/managed-main.ts +420 -0
  33. package/src/materializers/local.ts +300 -0
  34. package/src/materializers/managed-platform.ts +270 -0
  35. package/src/paths.ts +137 -0
  36. package/src/server.ts +636 -0
  37. package/src/subjects/local.ts +177 -0
  38. package/src/subjects/managed.ts +290 -0
  39. package/src/toolstore/integrity.ts +94 -0
  40. package/src/toolstore/manifest.ts +154 -0
  41. package/src/toolstore/publish.ts +342 -0
  42. package/tsconfig.json +20 -0
@@ -0,0 +1,438 @@
1
+ /**
2
+ * Secure command manifest validator.
3
+ *
4
+ * Validates that a {@link SecureCommandManifest} meets the CES security
5
+ * invariants before it can be registered. Validation is fail-closed: any
6
+ * structural issue, missing field, or policy violation results in rejection.
7
+ *
8
+ * Invariants enforced:
9
+ *
10
+ * 1. The entrypoint and bundleId must not be a denied binary.
11
+ * 2. At least one command profile must be declared (no empty manifests).
12
+ * 3. Each profile must have at least one allowed argv pattern.
13
+ * 4. Denied subcommands and denied flags lists are checked for consistency.
14
+ * 5. Auth adapter config must be structurally valid.
15
+ * 6. `egressMode` must be explicitly declared.
16
+ * 7. When `egressMode` is `proxy_required`, each profile must declare at
17
+ * least one allowed network target.
18
+ * 8. When `egressMode` is `no_network`, profiles must not declare network
19
+ * targets (contradictory).
20
+ * 9. Overbroad patterns (e.g. a single `<param...>` that matches anything)
21
+ * are rejected.
22
+ */
23
+
24
+ import {
25
+ validateAuthAdapterConfig,
26
+ } from "./auth-adapters.js";
27
+ import {
28
+ type SecureCommandManifest,
29
+ type CommandProfile,
30
+ type AllowedArgvPattern,
31
+ MANIFEST_SCHEMA_VERSION,
32
+ EGRESS_MODES,
33
+ EgressMode,
34
+ isDeniedBinary,
35
+ pathBasename,
36
+ } from "./profiles.js";
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Validation result
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export interface ValidationResult {
43
+ /** Whether the manifest passed all checks. */
44
+ valid: boolean;
45
+ /** List of human-readable error messages (empty when valid). */
46
+ errors: string[];
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Top-level validator
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /**
54
+ * Validate a secure command manifest against all CES security invariants.
55
+ *
56
+ * Returns a {@link ValidationResult} with `valid: false` and a list of
57
+ * error messages if any check fails. Validation is exhaustive — all
58
+ * violations are reported, not just the first.
59
+ */
60
+ export function validateManifest(
61
+ manifest: SecureCommandManifest,
62
+ ): ValidationResult {
63
+ const errors: string[] = [];
64
+
65
+ // -- Schema version
66
+ if (manifest.schemaVersion !== MANIFEST_SCHEMA_VERSION) {
67
+ errors.push(
68
+ `Unsupported schema version "${manifest.schemaVersion}". Expected "${MANIFEST_SCHEMA_VERSION}".`,
69
+ );
70
+ }
71
+
72
+ // -- Required string fields
73
+ if (!manifest.bundleDigest || manifest.bundleDigest.trim().length === 0) {
74
+ errors.push("bundleDigest is required and must be non-empty.");
75
+ }
76
+ if (!manifest.bundleId || manifest.bundleId.trim().length === 0) {
77
+ errors.push("bundleId is required and must be non-empty.");
78
+ }
79
+ if (!manifest.version || manifest.version.trim().length === 0) {
80
+ errors.push("version is required and must be non-empty.");
81
+ }
82
+ if (!manifest.entrypoint || manifest.entrypoint.trim().length === 0) {
83
+ errors.push("entrypoint is required and must be non-empty.");
84
+ }
85
+
86
+ // -- Denied binary check (entrypoint basename and bundleId)
87
+ if (manifest.entrypoint && isDeniedBinary(manifest.entrypoint)) {
88
+ errors.push(
89
+ `Entrypoint "${manifest.entrypoint}" (basename: "${pathBasename(manifest.entrypoint)}") is a structurally denied binary. ` +
90
+ `Generic HTTP clients, interpreters, and shell trampolines cannot be secure command profiles.`,
91
+ );
92
+ }
93
+ if (manifest.bundleId && isDeniedBinary(manifest.bundleId)) {
94
+ errors.push(
95
+ `bundleId "${manifest.bundleId}" matches a structurally denied binary name. ` +
96
+ `Generic HTTP clients, interpreters, and shell trampolines cannot be secure command profiles.`,
97
+ );
98
+ }
99
+
100
+ // -- Egress mode
101
+ if (!manifest.egressMode) {
102
+ errors.push(
103
+ `egressMode is required. Valid values: ${EGRESS_MODES.join(", ")}.`,
104
+ );
105
+ } else if (!(EGRESS_MODES as readonly string[]).includes(manifest.egressMode)) {
106
+ errors.push(
107
+ `Invalid egressMode "${manifest.egressMode}". Valid values: ${EGRESS_MODES.join(", ")}.`,
108
+ );
109
+ }
110
+
111
+ // -- Auth adapter
112
+ if (!manifest.authAdapter) {
113
+ errors.push("authAdapter is required.");
114
+ } else {
115
+ const adapterErrors = validateAuthAdapterConfig(manifest.authAdapter);
116
+ for (const e of adapterErrors) {
117
+ errors.push(`authAdapter: ${e}`);
118
+ }
119
+ }
120
+
121
+ // -- Command profiles (must have at least one)
122
+ if (
123
+ !manifest.commandProfiles ||
124
+ Object.keys(manifest.commandProfiles).length === 0
125
+ ) {
126
+ errors.push(
127
+ "At least one command profile must be declared. " +
128
+ "Secure command profiles cannot default to 'run any subcommand on this binary.'",
129
+ );
130
+ } else {
131
+ for (const [profileName, profile] of Object.entries(
132
+ manifest.commandProfiles,
133
+ )) {
134
+ const profileErrors = validateProfile(
135
+ profileName,
136
+ profile,
137
+ manifest.egressMode,
138
+ );
139
+ errors.push(...profileErrors);
140
+ }
141
+ }
142
+
143
+ return {
144
+ valid: errors.length === 0,
145
+ errors,
146
+ };
147
+ }
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // Profile-level validation
151
+ // ---------------------------------------------------------------------------
152
+
153
+ function validateProfile(
154
+ profileName: string,
155
+ profile: CommandProfile,
156
+ egressMode: EgressMode | undefined,
157
+ ): string[] {
158
+ const errors: string[] = [];
159
+ const prefix = `Profile "${profileName}"`;
160
+
161
+ // -- Description
162
+ if (!profile.description || profile.description.trim().length === 0) {
163
+ errors.push(`${prefix}: description is required and must be non-empty.`);
164
+ }
165
+
166
+ // -- Allowed argv patterns (must have at least one)
167
+ if (
168
+ !profile.allowedArgvPatterns ||
169
+ profile.allowedArgvPatterns.length === 0
170
+ ) {
171
+ errors.push(
172
+ `${prefix}: at least one allowedArgvPattern is required. ` +
173
+ "Profiles must explicitly declare what invocations are allowed.",
174
+ );
175
+ } else {
176
+ for (const pattern of profile.allowedArgvPatterns) {
177
+ const patternErrors = validateArgvPattern(prefix, pattern);
178
+ errors.push(...patternErrors);
179
+ }
180
+ }
181
+
182
+ // -- Denied subcommands (optional but must be an array)
183
+ if (profile.deniedSubcommands) {
184
+ for (const sub of profile.deniedSubcommands) {
185
+ if (!sub || sub.trim().length === 0) {
186
+ errors.push(
187
+ `${prefix}: deniedSubcommands contains an empty string.`,
188
+ );
189
+ }
190
+ }
191
+ }
192
+
193
+ // -- Denied flags (optional)
194
+ if (profile.deniedFlags) {
195
+ for (const flag of profile.deniedFlags) {
196
+ if (!flag || flag.trim().length === 0) {
197
+ errors.push(`${prefix}: deniedFlags contains an empty string.`);
198
+ }
199
+ if (flag && !flag.startsWith("-")) {
200
+ errors.push(
201
+ `${prefix}: deniedFlags entry "${flag}" does not start with "-". ` +
202
+ "Flags must start with a dash.",
203
+ );
204
+ }
205
+ }
206
+ }
207
+
208
+ // -- Network targets vs egress mode consistency
209
+ if (egressMode === EgressMode.ProxyRequired) {
210
+ if (
211
+ !profile.allowedNetworkTargets ||
212
+ profile.allowedNetworkTargets.length === 0
213
+ ) {
214
+ errors.push(
215
+ `${prefix}: egressMode is "proxy_required" but no allowedNetworkTargets are declared. ` +
216
+ "Commands with network egress must declare their allowed network targets.",
217
+ );
218
+ }
219
+ }
220
+
221
+ if (egressMode === EgressMode.NoNetwork) {
222
+ if (
223
+ profile.allowedNetworkTargets &&
224
+ profile.allowedNetworkTargets.length > 0
225
+ ) {
226
+ errors.push(
227
+ `${prefix}: egressMode is "no_network" but allowedNetworkTargets are declared. ` +
228
+ "This is contradictory — remove network targets or change egressMode.",
229
+ );
230
+ }
231
+ }
232
+
233
+ return errors;
234
+ }
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // Argv pattern validation
238
+ // ---------------------------------------------------------------------------
239
+
240
+ function validateArgvPattern(
241
+ profilePrefix: string,
242
+ pattern: AllowedArgvPattern,
243
+ ): string[] {
244
+ const errors: string[] = [];
245
+
246
+ if (!pattern.name || pattern.name.trim().length === 0) {
247
+ errors.push(
248
+ `${profilePrefix}: argv pattern has no name. Each pattern must be named for audit logging.`,
249
+ );
250
+ }
251
+
252
+ if (!pattern.tokens || pattern.tokens.length === 0) {
253
+ errors.push(
254
+ `${profilePrefix}: argv pattern "${pattern.name}" has no tokens. ` +
255
+ "Empty patterns would match any invocation.",
256
+ );
257
+ return errors;
258
+ }
259
+
260
+ // Check for overbroad patterns: a single rest placeholder matches anything
261
+ if (
262
+ pattern.tokens.length === 1 &&
263
+ isRestPlaceholder(pattern.tokens[0]!)
264
+ ) {
265
+ errors.push(
266
+ `${profilePrefix}: argv pattern "${pattern.name}" contains only a rest placeholder ` +
267
+ `("${pattern.tokens[0]}"). This would match any invocation and is too broad.`,
268
+ );
269
+ }
270
+
271
+ // Rest placeholder must be last token
272
+ for (let i = 0; i < pattern.tokens.length; i++) {
273
+ const token = pattern.tokens[i]!;
274
+ if (isRestPlaceholder(token) && i < pattern.tokens.length - 1) {
275
+ errors.push(
276
+ `${profilePrefix}: argv pattern "${pattern.name}" has a rest placeholder ` +
277
+ `("${token}") at position ${i}, but rest placeholders must be the last token.`,
278
+ );
279
+ }
280
+ }
281
+
282
+ return errors;
283
+ }
284
+
285
+ // ---------------------------------------------------------------------------
286
+ // Argv matching (used by the runtime to check commands against profiles)
287
+ // ---------------------------------------------------------------------------
288
+
289
+ /**
290
+ * Returns true if the token is a single-value placeholder like `<name>`.
291
+ */
292
+ function isPlaceholder(token: string): boolean {
293
+ return token.startsWith("<") && token.endsWith(">") && !token.endsWith("...>");
294
+ }
295
+
296
+ /**
297
+ * Returns true if the token is a rest placeholder like `<name...>`.
298
+ */
299
+ function isRestPlaceholder(token: string): boolean {
300
+ return token.startsWith("<") && token.endsWith("...>");
301
+ }
302
+
303
+ /**
304
+ * Check if a concrete argv array matches an allowed argv pattern.
305
+ *
306
+ * Matching rules:
307
+ * - Literal tokens must match exactly.
308
+ * - `<name>` matches exactly one argument.
309
+ * - `<name...>` matches one or more remaining arguments (must be last token).
310
+ */
311
+ export function matchesArgvPattern(
312
+ argv: readonly string[],
313
+ pattern: AllowedArgvPattern,
314
+ ): boolean {
315
+ const { tokens } = pattern;
316
+
317
+ for (let i = 0; i < tokens.length; i++) {
318
+ const token = tokens[i]!;
319
+
320
+ if (isRestPlaceholder(token)) {
321
+ // Rest placeholder: must have at least one remaining arg
322
+ return argv.length > i;
323
+ }
324
+
325
+ // No more args but still have pattern tokens
326
+ if (i >= argv.length) return false;
327
+
328
+ if (isPlaceholder(token)) {
329
+ // Single placeholder: matches any single value
330
+ continue;
331
+ }
332
+
333
+ // Literal: must match exactly
334
+ if (argv[i] !== token) return false;
335
+ }
336
+
337
+ // All pattern tokens consumed — argv must also be fully consumed
338
+ return argv.length === tokens.length;
339
+ }
340
+
341
+ // ---------------------------------------------------------------------------
342
+ // Full command validation against a manifest
343
+ // ---------------------------------------------------------------------------
344
+
345
+ export interface CommandValidationResult {
346
+ /** Whether the command is allowed. */
347
+ allowed: boolean;
348
+ /** The profile name that matched (undefined when rejected). */
349
+ matchedProfile?: string;
350
+ /** The pattern name that matched (undefined when rejected). */
351
+ matchedPattern?: string;
352
+ /** Human-readable reason for rejection (undefined when allowed). */
353
+ reason?: string;
354
+ }
355
+
356
+ /**
357
+ * Validate a concrete command invocation (argv array) against a manifest.
358
+ *
359
+ * Checks:
360
+ * 1. The argv is non-empty.
361
+ * 2. The argv does not contain any denied subcommands (across all profiles).
362
+ * 3. The argv does not contain any denied flags (across all profiles).
363
+ * 4. At least one profile's allowed argv patterns matches.
364
+ *
365
+ * This function does NOT re-validate the manifest itself — call
366
+ * {@link validateManifest} separately during registration.
367
+ */
368
+ export function validateCommand(
369
+ manifest: SecureCommandManifest,
370
+ argv: readonly string[],
371
+ ): CommandValidationResult {
372
+ if (argv.length === 0) {
373
+ return {
374
+ allowed: false,
375
+ reason: "Empty argv — no command to validate.",
376
+ };
377
+ }
378
+
379
+ // Collect all denied subcommands and flags across profiles
380
+ const allDeniedSubcommands = new Set<string>();
381
+ const allDeniedFlags = new Set<string>();
382
+
383
+ for (const profile of Object.values(manifest.commandProfiles)) {
384
+ for (const sub of profile.deniedSubcommands) {
385
+ allDeniedSubcommands.add(sub);
386
+ }
387
+ if (profile.deniedFlags) {
388
+ for (const flag of profile.deniedFlags) {
389
+ allDeniedFlags.add(flag);
390
+ }
391
+ }
392
+ }
393
+
394
+ // Check denied subcommands (match against first N tokens of argv)
395
+ for (const denied of allDeniedSubcommands) {
396
+ const deniedParts = denied.split(/\s+/);
397
+ if (deniedParts.length <= argv.length) {
398
+ const match = deniedParts.every((part, i) => argv[i] === part);
399
+ if (match) {
400
+ return {
401
+ allowed: false,
402
+ reason: `Subcommand "${denied}" is explicitly denied.`,
403
+ };
404
+ }
405
+ }
406
+ }
407
+
408
+ // Check denied flags
409
+ for (const arg of argv) {
410
+ if (allDeniedFlags.has(arg)) {
411
+ return {
412
+ allowed: false,
413
+ reason: `Flag "${arg}" is explicitly denied.`,
414
+ };
415
+ }
416
+ }
417
+
418
+ // Try to match against allowed argv patterns in each profile
419
+ for (const [profileName, profile] of Object.entries(
420
+ manifest.commandProfiles,
421
+ )) {
422
+ for (const pattern of profile.allowedArgvPatterns) {
423
+ if (matchesArgvPattern(argv, pattern)) {
424
+ return {
425
+ allowed: true,
426
+ matchedProfile: profileName,
427
+ matchedPattern: pattern.name,
428
+ };
429
+ }
430
+ }
431
+ }
432
+
433
+ return {
434
+ allowed: false,
435
+ reason:
436
+ "Command argv does not match any allowed pattern in any profile.",
437
+ };
438
+ }