@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,157 @@
1
+ /**
2
+ * Output file scanner for CES workspace copyback.
3
+ *
4
+ * Before any command output file is copied back into the assistant-visible
5
+ * workspace, it is scanned for:
6
+ *
7
+ * 1. **Exact secret matches** — The file content is checked against a set
8
+ * of known secret values that were injected into the command environment.
9
+ * If any secret appears verbatim in the output, copyback is rejected.
10
+ *
11
+ * 2. **Auth-bearing config artifacts** — Common configuration file patterns
12
+ * that typically contain credentials (e.g. `.netrc`, AWS credentials files,
13
+ * GitHub token files) are detected by filename and content heuristics.
14
+ *
15
+ * Both checks are conservative: false positives are acceptable (the user can
16
+ * explicitly re-request the file), but false negatives are not.
17
+ */
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Types
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export interface OutputScanResult {
24
+ /** Whether the file passed all scans and is safe to copy back. */
25
+ safe: boolean;
26
+ /** List of reasons the file was rejected (empty when safe). */
27
+ violations: string[];
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Auth-bearing config artifact patterns
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Filenames (basenames) that are known to contain credentials in their
36
+ * standard format. Matched case-insensitively.
37
+ */
38
+ const AUTH_BEARING_FILENAMES: ReadonlySet<string> = new Set([
39
+ ".netrc",
40
+ ".npmrc",
41
+ ".pypirc",
42
+ ".docker/config.json",
43
+ "credentials", // AWS ~/.aws/credentials
44
+ "config.json", // Docker, various
45
+ ".git-credentials",
46
+ ".env",
47
+ ".env.local",
48
+ ".env.production",
49
+ ".env.development",
50
+ ]);
51
+
52
+ /**
53
+ * Content patterns that indicate auth-bearing config artifacts.
54
+ * Each entry is a regex tested against file content.
55
+ */
56
+ const AUTH_BEARING_CONTENT_PATTERNS: ReadonlyArray<{
57
+ pattern: RegExp;
58
+ description: string;
59
+ }> = [
60
+ {
61
+ pattern: /machine\s+\S+\s+login\s+\S+\s+password\s+\S+/i,
62
+ description: "netrc-format credentials (machine/login/password)",
63
+ },
64
+ {
65
+ pattern: /\[default\]\s*\n\s*aws_access_key_id\s*=/i,
66
+ description: "AWS credentials file format",
67
+ },
68
+ {
69
+ pattern: /aws_secret_access_key\s*=\s*\S+/i,
70
+ description: "AWS secret access key in config",
71
+ },
72
+ {
73
+ pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/,
74
+ description: "PEM private key",
75
+ },
76
+ {
77
+ pattern: /-----BEGIN\s+OPENSSH\s+PRIVATE\s+KEY-----/,
78
+ description: "OpenSSH private key",
79
+ },
80
+ {
81
+ pattern: /"auth"\s*:\s*"[A-Za-z0-9+/=]{20,}"/,
82
+ description: "Docker registry auth token",
83
+ },
84
+ {
85
+ pattern: /\/\/[^:]+:_authToken\s*=\s*\S+/,
86
+ description: "npm registry auth token",
87
+ },
88
+ {
89
+ pattern: /password\s*=\s*\S+/i,
90
+ description: "Plain-text password in config file",
91
+ },
92
+ ];
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Scanner
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /**
99
+ * Scan a file's content for secret leakage before allowing copyback.
100
+ *
101
+ * @param filename — The basename of the output file (used for artifact detection).
102
+ * @param content — The raw file content (string or Buffer).
103
+ * @param secrets — Set of known secret values that were injected into the
104
+ * command's environment. Each value is checked for verbatim
105
+ * presence in the file content.
106
+ *
107
+ * @returns A {@link OutputScanResult} indicating whether the file is safe.
108
+ */
109
+ export function scanOutputFile(
110
+ filename: string,
111
+ content: string | Buffer,
112
+ secrets: ReadonlySet<string>,
113
+ ): OutputScanResult {
114
+ const violations: string[] = [];
115
+ const contentStr =
116
+ typeof content === "string" ? content : content.toString("utf-8");
117
+
118
+ // -- Check 1: Exact secret matches
119
+ for (const secret of secrets) {
120
+ if (secret.length === 0) continue;
121
+ if (contentStr.includes(secret)) {
122
+ // Don't include the actual secret in the violation message
123
+ violations.push(
124
+ `File contains an exact match of a credential value ` +
125
+ `(${secret.length} chars). Secrets must not leak into command outputs.`,
126
+ );
127
+ }
128
+ }
129
+
130
+ // -- Check 2: Auth-bearing filename detection
131
+ const lowerFilename = filename.toLowerCase();
132
+ const basenameOnly = lowerFilename.includes("/")
133
+ ? lowerFilename.slice(lowerFilename.lastIndexOf("/") + 1)
134
+ : lowerFilename;
135
+
136
+ if (AUTH_BEARING_FILENAMES.has(basenameOnly)) {
137
+ violations.push(
138
+ `Filename "${filename}" matches a known auth-bearing config artifact. ` +
139
+ `These files commonly contain credentials and cannot be copied back.`,
140
+ );
141
+ }
142
+
143
+ // -- Check 3: Auth-bearing content pattern detection
144
+ for (const { pattern, description } of AUTH_BEARING_CONTENT_PATTERNS) {
145
+ if (pattern.test(contentStr)) {
146
+ violations.push(
147
+ `File content matches auth-bearing pattern: ${description}. ` +
148
+ `Output files containing credential patterns cannot be copied back.`,
149
+ );
150
+ }
151
+ }
152
+
153
+ return {
154
+ safe: violations.length === 0,
155
+ violations,
156
+ };
157
+ }
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Secure command profile manifest schema (v1).
3
+ *
4
+ * A secure command profile defines the complete execution boundary for a
5
+ * credential-bearing command. Profiles are manifest-driven — there is no
6
+ * default of "run any subcommand on this binary." Every allowed invocation
7
+ * must be declared explicitly.
8
+ *
9
+ * Manifest fields:
10
+ *
11
+ * - **bundleDigest** — SHA-256 digest of the command bundle (for
12
+ * integrity verification).
13
+ * - **bundleId** — Unique identifier for the command bundle
14
+ * (e.g. "gh-cli", "aws-cli").
15
+ * - **version** — Semantic version of the bundle.
16
+ * - **entrypoint** — Path to the executable within the bundle
17
+ * (e.g. "bin/gh").
18
+ * - **commandProfiles** — Map of named profiles, each declaring allowed
19
+ * argv grammar, denied subcommands, and network
20
+ * targets.
21
+ * - **authAdapter** — How credentials are injected (env_var,
22
+ * temp_file, or credential_process).
23
+ * - **egressMode** — Network egress enforcement mode. Must be
24
+ * `proxy_required` unless the command has no
25
+ * network needs.
26
+ * - **cleanConfigDirs** — List of config directories to mount as empty
27
+ * tmpfs (prevents the command from reading host
28
+ * config files that could contain secrets).
29
+ */
30
+
31
+ import type { AuthAdapterConfig } from "./auth-adapters.js";
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Egress mode
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * Network egress enforcement mode for a secure command profile.
39
+ *
40
+ * - `proxy_required` — All network traffic MUST route through the CES
41
+ * egress proxy. The command's environment is
42
+ * configured with HTTP_PROXY/HTTPS_PROXY. Direct
43
+ * connections are blocked.
44
+ * - `no_network` — The command has no network requirements. Any
45
+ * network access is blocked entirely.
46
+ *
47
+ * There is intentionally no "direct" or "unrestricted" mode. Commands
48
+ * that need network access must go through the egress proxy so CES can
49
+ * enforce credential injection and audit logging.
50
+ */
51
+ export const EgressMode = {
52
+ ProxyRequired: "proxy_required",
53
+ NoNetwork: "no_network",
54
+ } as const;
55
+
56
+ export type EgressMode = (typeof EgressMode)[keyof typeof EgressMode];
57
+
58
+ /** All valid egress mode strings. */
59
+ export const EGRESS_MODES: readonly EgressMode[] = Object.values(
60
+ EgressMode,
61
+ ) as EgressMode[];
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Allowed argv grammar
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /**
68
+ * Defines the allowed argument grammar for a command profile.
69
+ *
70
+ * Allowed patterns use a simple grammar:
71
+ * - Literal strings match exactly (e.g. "api", "--json")
72
+ * - `<param>` matches any single argument (positional placeholder)
73
+ * - `<param...>` matches one or more remaining arguments (rest placeholder)
74
+ *
75
+ * Example: `["api", "<endpoint>", "--method", "<method>"]` allows
76
+ * `gh api /repos --method GET` but not `gh auth login`.
77
+ */
78
+ export interface AllowedArgvPattern {
79
+ /**
80
+ * Human-readable name for this pattern (e.g. "api-call", "list-repos").
81
+ * Used in audit logs and error messages.
82
+ */
83
+ name: string;
84
+ /**
85
+ * Ordered sequence of argv tokens. Each token is either a literal
86
+ * string or a placeholder (`<name>` or `<name...>`).
87
+ */
88
+ tokens: string[];
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Network target allowlist
93
+ // ---------------------------------------------------------------------------
94
+
95
+ /**
96
+ * Declares a network target that the command is allowed to contact.
97
+ */
98
+ export interface AllowedNetworkTarget {
99
+ /** Host pattern (glob). E.g. "api.github.com", "*.amazonaws.com". */
100
+ hostPattern: string;
101
+ /** Allowed port(s). Null means any port. */
102
+ ports?: number[];
103
+ /** Allowed protocol(s). Defaults to ["https"]. */
104
+ protocols?: Array<"http" | "https">;
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Command profile (single named profile within a manifest)
109
+ // ---------------------------------------------------------------------------
110
+
111
+ /**
112
+ * A single named command profile within a manifest.
113
+ *
114
+ * Each profile defines a narrow slice of allowed behaviour for the command.
115
+ * A manifest may contain multiple profiles (e.g. "read-repos" and
116
+ * "create-issue" for the GitHub CLI).
117
+ */
118
+ export interface CommandProfile {
119
+ /** Human-readable description of what this profile allows. */
120
+ description: string;
121
+
122
+ /**
123
+ * Allowed argv patterns. The command is rejected unless its arguments
124
+ * match at least one pattern.
125
+ */
126
+ allowedArgvPatterns: AllowedArgvPattern[];
127
+
128
+ /**
129
+ * Subcommands that are explicitly denied even if they would otherwise
130
+ * match an argv pattern. This is a safety net against overly broad
131
+ * patterns.
132
+ *
133
+ * Each entry is matched against the first N argv tokens.
134
+ */
135
+ deniedSubcommands: string[];
136
+
137
+ /**
138
+ * Flags (argv tokens starting with `-`) that are explicitly denied.
139
+ * Matched literally (e.g. "--exec", "-e").
140
+ */
141
+ deniedFlags?: string[];
142
+
143
+ /**
144
+ * Network targets this profile is allowed to contact. Only relevant
145
+ * when egressMode is `proxy_required`.
146
+ */
147
+ allowedNetworkTargets?: AllowedNetworkTarget[];
148
+ }
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // Secure command manifest (top-level)
152
+ // ---------------------------------------------------------------------------
153
+
154
+ /** Current manifest schema version. */
155
+ export const MANIFEST_SCHEMA_VERSION = "1" as const;
156
+
157
+ /**
158
+ * v1 secure command manifest.
159
+ *
160
+ * This is the top-level schema that defines a complete execution boundary
161
+ * for a credential-bearing command binary.
162
+ */
163
+ export interface SecureCommandManifest {
164
+ /** Manifest schema version. Must be "1". */
165
+ schemaVersion: typeof MANIFEST_SCHEMA_VERSION;
166
+
167
+ /**
168
+ * SHA-256 hex digest of the command bundle. Used for integrity
169
+ * verification before execution.
170
+ */
171
+ bundleDigest: string;
172
+
173
+ /** Unique identifier for the command bundle (e.g. "gh-cli"). */
174
+ bundleId: string;
175
+
176
+ /** Semantic version of the bundle. */
177
+ version: string;
178
+
179
+ /**
180
+ * Path to the executable entrypoint within the bundle
181
+ * (e.g. "bin/gh", "bin/aws").
182
+ */
183
+ entrypoint: string;
184
+
185
+ /**
186
+ * Named command profiles. Each profile defines a narrow execution
187
+ * boundary. Keyed by profile name.
188
+ */
189
+ commandProfiles: Record<string, CommandProfile>;
190
+
191
+ /**
192
+ * Auth adapter configuration describing how credentials are
193
+ * materialised into the command's environment.
194
+ */
195
+ authAdapter: AuthAdapterConfig;
196
+
197
+ /**
198
+ * Network egress enforcement mode. Must be `proxy_required` for commands
199
+ * that contact the network, or `no_network` for offline-only commands.
200
+ */
201
+ egressMode: EgressMode;
202
+
203
+ /**
204
+ * Config directories to mount as empty tmpfs during execution. Prevents
205
+ * the command from reading host config files that might contain secrets.
206
+ *
207
+ * Map from source path pattern to a description.
208
+ * E.g. `{ "~/.aws": "AWS CLI config", "~/.config/gh": "GitHub CLI config" }`.
209
+ */
210
+ cleanConfigDirs?: Record<string, string>;
211
+ }
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Denied binaries — structurally rejected as secure command profiles
215
+ // ---------------------------------------------------------------------------
216
+
217
+ /**
218
+ * Binaries that are structurally banned from being registered as secure
219
+ * command profiles. These are generic HTTP clients, interpreters, and
220
+ * shell trampolines that would undermine the manifest-driven security
221
+ * model.
222
+ *
223
+ * This list is checked against the entrypoint basename (the last path
224
+ * segment) and against bundleId.
225
+ */
226
+ export const DENIED_BINARIES: ReadonlySet<string> = new Set([
227
+ // Generic HTTP clients
228
+ "curl",
229
+ "wget",
230
+ "http", // httpie
231
+ "https", // httpie alias
232
+ "httpie",
233
+
234
+ // Interpreters
235
+ "python",
236
+ "python3",
237
+ "python3.10",
238
+ "python3.11",
239
+ "python3.12",
240
+ "python3.13",
241
+ "python3.14",
242
+ "node",
243
+ "bun",
244
+ "deno",
245
+ "ruby",
246
+ "perl",
247
+ "lua",
248
+ "php",
249
+
250
+ // Shell trampolines
251
+ "bash",
252
+ "sh",
253
+ "zsh",
254
+ "fish",
255
+ "dash",
256
+ "ksh",
257
+ "csh",
258
+ "tcsh",
259
+ "env", // /usr/bin/env can trampoline to any binary
260
+ "xargs", // can execute arbitrary commands
261
+ "exec",
262
+ "nohup",
263
+ "strace",
264
+ "ltrace",
265
+ ]);
266
+
267
+ /**
268
+ * Returns the basename of a path (last segment after the last `/`).
269
+ */
270
+ export function pathBasename(path: string): string {
271
+ const lastSlash = path.lastIndexOf("/");
272
+ return lastSlash === -1 ? path : path.slice(lastSlash + 1);
273
+ }
274
+
275
+ /**
276
+ * Check if a binary name (basename or full path) is in the denied set.
277
+ * Matches against the basename portion of the path.
278
+ */
279
+ export function isDeniedBinary(binaryNameOrPath: string): boolean {
280
+ const basename = pathBasename(binaryNameOrPath);
281
+ return DENIED_BINARIES.has(basename);
282
+ }