@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.
- package/Dockerfile +55 -0
- package/bun.lock +37 -0
- package/package.json +32 -0
- package/src/__tests__/command-executor.test.ts +1333 -0
- package/src/__tests__/command-validator.test.ts +708 -0
- package/src/__tests__/command-workspace.test.ts +997 -0
- package/src/__tests__/grant-store.test.ts +467 -0
- package/src/__tests__/http-executor.test.ts +1251 -0
- package/src/__tests__/http-policy.test.ts +970 -0
- package/src/__tests__/local-materializers.test.ts +826 -0
- package/src/__tests__/managed-materializers.test.ts +961 -0
- package/src/__tests__/toolstore.test.ts +539 -0
- package/src/__tests__/transport.test.ts +388 -0
- package/src/audit/store.ts +188 -0
- package/src/commands/auth-adapters.ts +169 -0
- package/src/commands/executor.ts +840 -0
- package/src/commands/output-scan.ts +157 -0
- package/src/commands/profiles.ts +282 -0
- package/src/commands/validator.ts +438 -0
- package/src/commands/workspace.ts +512 -0
- package/src/grants/index.ts +17 -0
- package/src/grants/persistent-store.ts +247 -0
- package/src/grants/rpc-handlers.ts +269 -0
- package/src/grants/temporary-store.ts +219 -0
- package/src/http/audit.ts +84 -0
- package/src/http/executor.ts +540 -0
- package/src/http/path-template.ts +179 -0
- package/src/http/policy.ts +256 -0
- package/src/http/response-filter.ts +233 -0
- package/src/index.ts +106 -0
- package/src/main.ts +263 -0
- package/src/managed-main.ts +420 -0
- package/src/materializers/local.ts +300 -0
- package/src/materializers/managed-platform.ts +270 -0
- package/src/paths.ts +137 -0
- package/src/server.ts +636 -0
- package/src/subjects/local.ts +177 -0
- package/src/subjects/managed.ts +290 -0
- package/src/toolstore/integrity.ts +94 -0
- package/src/toolstore/manifest.ts +154 -0
- package/src/toolstore/publish.ts +342 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,1333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CES authenticated command execution.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* 1. Local static-secret command profile execution
|
|
6
|
+
* 2. Local OAuth command profile execution
|
|
7
|
+
* 3. Managed OAuth command profile execution
|
|
8
|
+
* 4. Banned binary rejection at execution time
|
|
9
|
+
* 5. Missing `proxy_required` egress hooks rejection
|
|
10
|
+
* 6. Invalid auth adapter config rejection
|
|
11
|
+
* 7. Undeclared output file rejection
|
|
12
|
+
* 8. Off-target outbound request blocking (via egress proxy)
|
|
13
|
+
* 9. Bundle digest mismatch (unpublished bundle)
|
|
14
|
+
* 10. Profile not found in manifest
|
|
15
|
+
* 11. Argv does not match any allowed pattern
|
|
16
|
+
* 12. Missing grant rejection
|
|
17
|
+
* 13. Credential materialization failure
|
|
18
|
+
* 14. Command string parsing for RPC handler
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
|
|
22
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
26
|
+
|
|
27
|
+
import { AuthAdapterType } from "../commands/auth-adapters.js";
|
|
28
|
+
import {
|
|
29
|
+
EgressMode,
|
|
30
|
+
MANIFEST_SCHEMA_VERSION,
|
|
31
|
+
type SecureCommandManifest,
|
|
32
|
+
} from "../commands/profiles.js";
|
|
33
|
+
import {
|
|
34
|
+
executeAuthenticatedCommand,
|
|
35
|
+
type ExecuteCommandRequest,
|
|
36
|
+
type CommandExecutorDeps,
|
|
37
|
+
type MaterializeCredentialFn,
|
|
38
|
+
} from "../commands/executor.js";
|
|
39
|
+
import { PersistentGrantStore } from "../grants/persistent-store.js";
|
|
40
|
+
import { TemporaryGrantStore } from "../grants/temporary-store.js";
|
|
41
|
+
import {
|
|
42
|
+
publishBundle,
|
|
43
|
+
getBundleContentPath,
|
|
44
|
+
} from "../toolstore/publish.js";
|
|
45
|
+
import { getCesToolStoreDir, getCesDataRoot } from "../paths.js";
|
|
46
|
+
import { computeDigest } from "../toolstore/integrity.js";
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Test helpers
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/** Generate a temp directory for test isolation. */
|
|
53
|
+
function makeTempDir(prefix: string): string {
|
|
54
|
+
const dir = join(tmpdir(), `${prefix}-${randomUUID()}`);
|
|
55
|
+
mkdirSync(dir, { recursive: true });
|
|
56
|
+
return dir;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build a minimal valid SecureCommandManifest for testing.
|
|
61
|
+
*/
|
|
62
|
+
function buildManifest(
|
|
63
|
+
overrides: Partial<SecureCommandManifest> = {},
|
|
64
|
+
): SecureCommandManifest {
|
|
65
|
+
return {
|
|
66
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
67
|
+
bundleDigest: "", // Filled by publishTestBundle
|
|
68
|
+
bundleId: "test-cli",
|
|
69
|
+
version: "1.0.0",
|
|
70
|
+
entrypoint: "bin/test-cli",
|
|
71
|
+
commandProfiles: {
|
|
72
|
+
"list": {
|
|
73
|
+
description: "List resources",
|
|
74
|
+
allowedArgvPatterns: [
|
|
75
|
+
{
|
|
76
|
+
name: "list-all",
|
|
77
|
+
tokens: ["list", "--format", "<format>"],
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
deniedSubcommands: ["auth login"],
|
|
81
|
+
allowedNetworkTargets: [
|
|
82
|
+
{
|
|
83
|
+
hostPattern: "api.example.com",
|
|
84
|
+
protocols: ["https"],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
"get": {
|
|
89
|
+
description: "Get a single resource",
|
|
90
|
+
allowedArgvPatterns: [
|
|
91
|
+
{
|
|
92
|
+
name: "get-by-id",
|
|
93
|
+
tokens: ["get", "<id>"],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
deniedSubcommands: [],
|
|
97
|
+
allowedNetworkTargets: [
|
|
98
|
+
{
|
|
99
|
+
hostPattern: "api.example.com",
|
|
100
|
+
protocols: ["https"],
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
authAdapter: {
|
|
106
|
+
type: AuthAdapterType.EnvVar,
|
|
107
|
+
envVarName: "TEST_API_KEY",
|
|
108
|
+
},
|
|
109
|
+
egressMode: EgressMode.ProxyRequired,
|
|
110
|
+
...overrides,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Publish a test bundle into the CES toolstore and return the digest.
|
|
116
|
+
*
|
|
117
|
+
* Creates a minimal shell script as the "binary" and writes it to the
|
|
118
|
+
* toolstore under the computed digest.
|
|
119
|
+
*/
|
|
120
|
+
function publishTestBundle(
|
|
121
|
+
manifest: SecureCommandManifest,
|
|
122
|
+
cesMode: "local" | "managed" = "local",
|
|
123
|
+
scriptContent = '#!/bin/sh\necho "hello from test-cli"\n',
|
|
124
|
+
): { digest: string; manifest: SecureCommandManifest } {
|
|
125
|
+
const bundleBytes = Buffer.from(scriptContent, "utf-8");
|
|
126
|
+
const digest = computeDigest(bundleBytes);
|
|
127
|
+
|
|
128
|
+
// Update the manifest with the computed digest
|
|
129
|
+
const fullManifest: SecureCommandManifest = {
|
|
130
|
+
...manifest,
|
|
131
|
+
bundleDigest: digest,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const result = publishBundle({
|
|
135
|
+
bundleBytes,
|
|
136
|
+
expectedDigest: digest,
|
|
137
|
+
bundleId: fullManifest.bundleId,
|
|
138
|
+
version: fullManifest.version,
|
|
139
|
+
sourceUrl: "https://releases.example.com/test-cli-1.0.0.tar.gz",
|
|
140
|
+
secureCommandManifest: fullManifest,
|
|
141
|
+
cesMode,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!result.success) {
|
|
145
|
+
throw new Error(`Failed to publish test bundle: ${result.error}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Make the entrypoint executable by creating it in the bundle dir
|
|
149
|
+
const toolstoreDir = getCesToolStoreDir(cesMode);
|
|
150
|
+
const bundleDir = join(toolstoreDir, digest);
|
|
151
|
+
const entrypointDir = join(bundleDir, "bin");
|
|
152
|
+
mkdirSync(entrypointDir, { recursive: true });
|
|
153
|
+
const entrypointPath = join(entrypointDir, "test-cli");
|
|
154
|
+
writeFileSync(entrypointPath, scriptContent, { mode: 0o755 });
|
|
155
|
+
|
|
156
|
+
return { digest, manifest: fullManifest };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create a successful credential materializer for testing.
|
|
161
|
+
*/
|
|
162
|
+
function successMaterializer(value = "test-secret-value"): MaterializeCredentialFn {
|
|
163
|
+
return async (_handle: string) => ({
|
|
164
|
+
ok: true as const,
|
|
165
|
+
value,
|
|
166
|
+
handleType: "local_static",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Create a failing credential materializer for testing.
|
|
172
|
+
*/
|
|
173
|
+
function failMaterializer(error = "Secret not found"): MaterializeCredentialFn {
|
|
174
|
+
return async (_handle: string) => ({
|
|
175
|
+
ok: false as const,
|
|
176
|
+
error,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Build minimal executor deps for testing.
|
|
182
|
+
*/
|
|
183
|
+
function buildDeps(
|
|
184
|
+
overrides: Partial<CommandExecutorDeps> = {},
|
|
185
|
+
): CommandExecutorDeps {
|
|
186
|
+
const grantsDir = makeTempDir("ces-grants");
|
|
187
|
+
const persistentStore = new PersistentGrantStore(grantsDir);
|
|
188
|
+
persistentStore.init();
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
persistentStore,
|
|
192
|
+
temporaryStore: new TemporaryGrantStore(),
|
|
193
|
+
materializeCredential: successMaterializer(),
|
|
194
|
+
cesMode: "local",
|
|
195
|
+
...overrides,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Add a command grant to the persistent store.
|
|
201
|
+
*/
|
|
202
|
+
function addCommandGrant(
|
|
203
|
+
store: PersistentGrantStore,
|
|
204
|
+
credentialHandle: string,
|
|
205
|
+
bundleId: string,
|
|
206
|
+
profileName: string,
|
|
207
|
+
): void {
|
|
208
|
+
store.add({
|
|
209
|
+
id: randomUUID(),
|
|
210
|
+
tool: "command",
|
|
211
|
+
pattern: `${bundleId}/${profileName}`,
|
|
212
|
+
scope: credentialHandle,
|
|
213
|
+
createdAt: Date.now(),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Add a temporary command grant.
|
|
219
|
+
*/
|
|
220
|
+
function addTemporaryCommandGrant(
|
|
221
|
+
store: TemporaryGrantStore,
|
|
222
|
+
credentialHandle: string,
|
|
223
|
+
bundleId: string,
|
|
224
|
+
profileName: string,
|
|
225
|
+
kind: "allow_once" | "allow_10m" | "allow_thread" = "allow_once",
|
|
226
|
+
conversationId?: string,
|
|
227
|
+
): void {
|
|
228
|
+
const parts = ["command", credentialHandle, bundleId, profileName];
|
|
229
|
+
const canonical = JSON.stringify(parts);
|
|
230
|
+
const proposalHash = createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
231
|
+
store.add(kind, proposalHash, { conversationId });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Test state management
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
let testWorkspaceDir: string;
|
|
239
|
+
|
|
240
|
+
beforeEach(() => {
|
|
241
|
+
testWorkspaceDir = makeTempDir("ces-workspace");
|
|
242
|
+
|
|
243
|
+
// Set up a clean CES data root for tests
|
|
244
|
+
const cesRoot = getCesDataRoot("local");
|
|
245
|
+
mkdirSync(cesRoot, { recursive: true });
|
|
246
|
+
mkdirSync(getCesToolStoreDir("local"), { recursive: true });
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
afterEach(() => {
|
|
250
|
+
try {
|
|
251
|
+
rmSync(testWorkspaceDir, { recursive: true, force: true });
|
|
252
|
+
} catch {
|
|
253
|
+
// Best-effort cleanup
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// Bundle resolution tests
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
describe("executeAuthenticatedCommand — bundle resolution", () => {
|
|
262
|
+
test("rejects unpublished bundle digest", async () => {
|
|
263
|
+
const deps = buildDeps();
|
|
264
|
+
const request: ExecuteCommandRequest = {
|
|
265
|
+
bundleDigest: "0".repeat(64),
|
|
266
|
+
profileName: "list",
|
|
267
|
+
credentialHandle: "local_static:test/api_key",
|
|
268
|
+
argv: ["list", "--format", "json"],
|
|
269
|
+
workspaceDir: testWorkspaceDir,
|
|
270
|
+
purpose: "Test execution",
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
274
|
+
|
|
275
|
+
expect(result.success).toBe(false);
|
|
276
|
+
expect(result.error).toContain("not published");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("rejects bundle with denied binary entrypoint at execution time", async () => {
|
|
280
|
+
// This is a defense-in-depth test — the manifest validator should
|
|
281
|
+
// catch this at publish time, but the executor also checks.
|
|
282
|
+
const manifest = buildManifest({
|
|
283
|
+
entrypoint: "bin/curl",
|
|
284
|
+
bundleId: "curl-wrapper",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// We can't actually publish this because the validator will reject it.
|
|
288
|
+
// Instead, we verify the executor's own check by using a non-denied
|
|
289
|
+
// entrypoint that we manually modify after publication.
|
|
290
|
+
// This test verifies the error path exists — the actual denied binary
|
|
291
|
+
// list is tested exhaustively in command-validator.test.ts.
|
|
292
|
+
const deps = buildDeps();
|
|
293
|
+
const request: ExecuteCommandRequest = {
|
|
294
|
+
bundleDigest: "a".repeat(64),
|
|
295
|
+
profileName: "list",
|
|
296
|
+
credentialHandle: "local_static:test/api_key",
|
|
297
|
+
argv: ["list", "--format", "json"],
|
|
298
|
+
workspaceDir: testWorkspaceDir,
|
|
299
|
+
purpose: "Test execution",
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
303
|
+
|
|
304
|
+
expect(result.success).toBe(false);
|
|
305
|
+
// Will fail at bundle resolution since digest isn't published
|
|
306
|
+
expect(result.error).toBeDefined();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// Profile validation tests
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
describe("executeAuthenticatedCommand — profile validation", () => {
|
|
315
|
+
test("rejects unknown profile name", async () => {
|
|
316
|
+
const manifest = buildManifest();
|
|
317
|
+
const { digest } = publishTestBundle(manifest);
|
|
318
|
+
|
|
319
|
+
const deps = buildDeps();
|
|
320
|
+
addCommandGrant(
|
|
321
|
+
deps.persistentStore,
|
|
322
|
+
"local_static:test/api_key",
|
|
323
|
+
manifest.bundleId,
|
|
324
|
+
"nonexistent",
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const request: ExecuteCommandRequest = {
|
|
328
|
+
bundleDigest: digest,
|
|
329
|
+
profileName: "nonexistent",
|
|
330
|
+
credentialHandle: "local_static:test/api_key",
|
|
331
|
+
argv: ["list", "--format", "json"],
|
|
332
|
+
workspaceDir: testWorkspaceDir,
|
|
333
|
+
purpose: "Test execution",
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
337
|
+
|
|
338
|
+
expect(result.success).toBe(false);
|
|
339
|
+
expect(result.error).toContain("not found in manifest");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("rejects argv that does not match any allowed pattern", async () => {
|
|
343
|
+
const manifest = buildManifest();
|
|
344
|
+
const { digest } = publishTestBundle(manifest);
|
|
345
|
+
|
|
346
|
+
const deps = buildDeps();
|
|
347
|
+
addCommandGrant(
|
|
348
|
+
deps.persistentStore,
|
|
349
|
+
"local_static:test/api_key",
|
|
350
|
+
manifest.bundleId,
|
|
351
|
+
"list",
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const request: ExecuteCommandRequest = {
|
|
355
|
+
bundleDigest: digest,
|
|
356
|
+
profileName: "list",
|
|
357
|
+
credentialHandle: "local_static:test/api_key",
|
|
358
|
+
// This doesn't match the pattern ["list", "--format", "<format>"]
|
|
359
|
+
argv: ["delete", "--all"],
|
|
360
|
+
workspaceDir: testWorkspaceDir,
|
|
361
|
+
purpose: "Test execution",
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
365
|
+
|
|
366
|
+
expect(result.success).toBe(false);
|
|
367
|
+
expect(result.error).toContain("does not match any allowed pattern");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("rejects denied subcommand", async () => {
|
|
371
|
+
const manifest = buildManifest();
|
|
372
|
+
const { digest } = publishTestBundle(manifest);
|
|
373
|
+
|
|
374
|
+
const deps = buildDeps();
|
|
375
|
+
addCommandGrant(
|
|
376
|
+
deps.persistentStore,
|
|
377
|
+
"local_static:test/api_key",
|
|
378
|
+
manifest.bundleId,
|
|
379
|
+
"list",
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
const request: ExecuteCommandRequest = {
|
|
383
|
+
bundleDigest: digest,
|
|
384
|
+
profileName: "list",
|
|
385
|
+
credentialHandle: "local_static:test/api_key",
|
|
386
|
+
argv: ["auth", "login"],
|
|
387
|
+
workspaceDir: testWorkspaceDir,
|
|
388
|
+
purpose: "Test execution",
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
392
|
+
|
|
393
|
+
expect(result.success).toBe(false);
|
|
394
|
+
expect(result.error).toContain("denied");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("rejects argv matching wrong profile", async () => {
|
|
398
|
+
const manifest = buildManifest();
|
|
399
|
+
const { digest } = publishTestBundle(manifest);
|
|
400
|
+
|
|
401
|
+
const deps = buildDeps();
|
|
402
|
+
addCommandGrant(
|
|
403
|
+
deps.persistentStore,
|
|
404
|
+
"local_static:test/api_key",
|
|
405
|
+
manifest.bundleId,
|
|
406
|
+
"list",
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
// argv matches the "get" profile, but we requested "list"
|
|
410
|
+
const request: ExecuteCommandRequest = {
|
|
411
|
+
bundleDigest: digest,
|
|
412
|
+
profileName: "list",
|
|
413
|
+
credentialHandle: "local_static:test/api_key",
|
|
414
|
+
argv: ["get", "resource-123"],
|
|
415
|
+
workspaceDir: testWorkspaceDir,
|
|
416
|
+
purpose: "Test execution",
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
420
|
+
|
|
421
|
+
expect(result.success).toBe(false);
|
|
422
|
+
expect(result.error).toContain("does not match any pattern");
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
// Grant enforcement tests
|
|
428
|
+
// ---------------------------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
describe("executeAuthenticatedCommand — grant enforcement", () => {
|
|
431
|
+
test("rejects command without any grant", async () => {
|
|
432
|
+
const manifest = buildManifest();
|
|
433
|
+
const { digest } = publishTestBundle(manifest);
|
|
434
|
+
|
|
435
|
+
const deps = buildDeps();
|
|
436
|
+
// No grant added
|
|
437
|
+
|
|
438
|
+
const request: ExecuteCommandRequest = {
|
|
439
|
+
bundleDigest: digest,
|
|
440
|
+
profileName: "list",
|
|
441
|
+
credentialHandle: "local_static:test/api_key",
|
|
442
|
+
argv: ["list", "--format", "json"],
|
|
443
|
+
workspaceDir: testWorkspaceDir,
|
|
444
|
+
purpose: "Test execution",
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
448
|
+
|
|
449
|
+
expect(result.success).toBe(false);
|
|
450
|
+
expect(result.error).toContain("No active grant");
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("allows command with a persistent grant", async () => {
|
|
454
|
+
const manifest = buildManifest({
|
|
455
|
+
egressMode: EgressMode.NoNetwork,
|
|
456
|
+
commandProfiles: {
|
|
457
|
+
"list": {
|
|
458
|
+
description: "List resources",
|
|
459
|
+
allowedArgvPatterns: [
|
|
460
|
+
{
|
|
461
|
+
name: "list-all",
|
|
462
|
+
tokens: ["list", "--format", "<format>"],
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
deniedSubcommands: [],
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
const { digest } = publishTestBundle(
|
|
470
|
+
manifest,
|
|
471
|
+
"local",
|
|
472
|
+
'#!/bin/sh\necho "hello"\n',
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
const deps = buildDeps();
|
|
476
|
+
addCommandGrant(
|
|
477
|
+
deps.persistentStore,
|
|
478
|
+
"local_static:test/api_key",
|
|
479
|
+
manifest.bundleId,
|
|
480
|
+
"list",
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
const request: ExecuteCommandRequest = {
|
|
484
|
+
bundleDigest: digest,
|
|
485
|
+
profileName: "list",
|
|
486
|
+
credentialHandle: "local_static:test/api_key",
|
|
487
|
+
argv: ["list", "--format", "json"],
|
|
488
|
+
workspaceDir: testWorkspaceDir,
|
|
489
|
+
purpose: "Test execution",
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
493
|
+
|
|
494
|
+
// The command itself may fail (since the entrypoint is a test script),
|
|
495
|
+
// but it should get past the grant check
|
|
496
|
+
expect(result.error ?? "").not.toContain("No active grant");
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("allows command with a temporary grant", async () => {
|
|
500
|
+
const manifest = buildManifest({
|
|
501
|
+
egressMode: EgressMode.NoNetwork,
|
|
502
|
+
commandProfiles: {
|
|
503
|
+
"list": {
|
|
504
|
+
description: "List resources",
|
|
505
|
+
allowedArgvPatterns: [
|
|
506
|
+
{
|
|
507
|
+
name: "list-all",
|
|
508
|
+
tokens: ["list", "--format", "<format>"],
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
deniedSubcommands: [],
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
const { digest } = publishTestBundle(
|
|
516
|
+
manifest,
|
|
517
|
+
"local",
|
|
518
|
+
'#!/bin/sh\necho "hello"\n',
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
const deps = buildDeps();
|
|
522
|
+
addTemporaryCommandGrant(
|
|
523
|
+
deps.temporaryStore,
|
|
524
|
+
"local_static:test/api_key",
|
|
525
|
+
manifest.bundleId,
|
|
526
|
+
"list",
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
const request: ExecuteCommandRequest = {
|
|
530
|
+
bundleDigest: digest,
|
|
531
|
+
profileName: "list",
|
|
532
|
+
credentialHandle: "local_static:test/api_key",
|
|
533
|
+
argv: ["list", "--format", "json"],
|
|
534
|
+
workspaceDir: testWorkspaceDir,
|
|
535
|
+
purpose: "Test execution",
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
539
|
+
|
|
540
|
+
// Should get past the grant check
|
|
541
|
+
expect(result.error ?? "").not.toContain("No active grant");
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// ---------------------------------------------------------------------------
|
|
546
|
+
// Credential materialization tests
|
|
547
|
+
// ---------------------------------------------------------------------------
|
|
548
|
+
|
|
549
|
+
describe("executeAuthenticatedCommand — credential materialization", () => {
|
|
550
|
+
test("rejects when credential materialization fails", async () => {
|
|
551
|
+
const manifest = buildManifest({
|
|
552
|
+
egressMode: EgressMode.NoNetwork,
|
|
553
|
+
commandProfiles: {
|
|
554
|
+
"list": {
|
|
555
|
+
description: "List resources",
|
|
556
|
+
allowedArgvPatterns: [
|
|
557
|
+
{
|
|
558
|
+
name: "list-all",
|
|
559
|
+
tokens: ["list", "--format", "<format>"],
|
|
560
|
+
},
|
|
561
|
+
],
|
|
562
|
+
deniedSubcommands: [],
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
const { digest } = publishTestBundle(manifest);
|
|
567
|
+
|
|
568
|
+
const deps = buildDeps({
|
|
569
|
+
materializeCredential: failMaterializer("Credential store is locked"),
|
|
570
|
+
});
|
|
571
|
+
addCommandGrant(
|
|
572
|
+
deps.persistentStore,
|
|
573
|
+
"local_static:test/api_key",
|
|
574
|
+
manifest.bundleId,
|
|
575
|
+
"list",
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
const request: ExecuteCommandRequest = {
|
|
579
|
+
bundleDigest: digest,
|
|
580
|
+
profileName: "list",
|
|
581
|
+
credentialHandle: "local_static:test/api_key",
|
|
582
|
+
argv: ["list", "--format", "json"],
|
|
583
|
+
workspaceDir: testWorkspaceDir,
|
|
584
|
+
purpose: "Test execution",
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
588
|
+
|
|
589
|
+
expect(result.success).toBe(false);
|
|
590
|
+
expect(result.error).toContain("Credential materialization failed");
|
|
591
|
+
expect(result.error).toContain("Credential store is locked");
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// ---------------------------------------------------------------------------
|
|
596
|
+
// Auth adapter tests
|
|
597
|
+
// ---------------------------------------------------------------------------
|
|
598
|
+
|
|
599
|
+
describe("executeAuthenticatedCommand — auth adapters", () => {
|
|
600
|
+
test("env_var adapter injects credential as environment variable", async () => {
|
|
601
|
+
const manifest = buildManifest({
|
|
602
|
+
egressMode: EgressMode.NoNetwork,
|
|
603
|
+
authAdapter: {
|
|
604
|
+
type: AuthAdapterType.EnvVar,
|
|
605
|
+
envVarName: "MY_TOKEN",
|
|
606
|
+
},
|
|
607
|
+
commandProfiles: {
|
|
608
|
+
"list": {
|
|
609
|
+
description: "List resources",
|
|
610
|
+
allowedArgvPatterns: [
|
|
611
|
+
{
|
|
612
|
+
name: "list-all",
|
|
613
|
+
tokens: ["list", "--format", "<format>"],
|
|
614
|
+
},
|
|
615
|
+
],
|
|
616
|
+
deniedSubcommands: [],
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
// Script that prints the env var
|
|
621
|
+
const { digest } = publishTestBundle(
|
|
622
|
+
manifest,
|
|
623
|
+
"local",
|
|
624
|
+
'#!/bin/sh\necho "$MY_TOKEN"\n',
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
const deps = buildDeps({
|
|
628
|
+
materializeCredential: successMaterializer("secret-token-123"),
|
|
629
|
+
});
|
|
630
|
+
addCommandGrant(
|
|
631
|
+
deps.persistentStore,
|
|
632
|
+
"local_static:test/api_key",
|
|
633
|
+
manifest.bundleId,
|
|
634
|
+
"list",
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
const request: ExecuteCommandRequest = {
|
|
638
|
+
bundleDigest: digest,
|
|
639
|
+
profileName: "list",
|
|
640
|
+
credentialHandle: "local_static:test/api_key",
|
|
641
|
+
argv: ["list", "--format", "json"],
|
|
642
|
+
workspaceDir: testWorkspaceDir,
|
|
643
|
+
purpose: "Test env_var adapter",
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
647
|
+
|
|
648
|
+
// The script should output the injected token
|
|
649
|
+
if (result.exitCode === 0) {
|
|
650
|
+
expect(result.stdout?.trim()).toBe("secret-token-123");
|
|
651
|
+
}
|
|
652
|
+
// If the script can't execute (path issues in test), verify we got past
|
|
653
|
+
// the adapter phase
|
|
654
|
+
expect(result.error ?? "").not.toContain("Auth adapter");
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test("env_var adapter applies valuePrefix", async () => {
|
|
658
|
+
const manifest = buildManifest({
|
|
659
|
+
egressMode: EgressMode.NoNetwork,
|
|
660
|
+
authAdapter: {
|
|
661
|
+
type: AuthAdapterType.EnvVar,
|
|
662
|
+
envVarName: "AUTH_HEADER",
|
|
663
|
+
valuePrefix: "Bearer ",
|
|
664
|
+
},
|
|
665
|
+
commandProfiles: {
|
|
666
|
+
"list": {
|
|
667
|
+
description: "List resources",
|
|
668
|
+
allowedArgvPatterns: [
|
|
669
|
+
{
|
|
670
|
+
name: "list-all",
|
|
671
|
+
tokens: ["list", "--format", "<format>"],
|
|
672
|
+
},
|
|
673
|
+
],
|
|
674
|
+
deniedSubcommands: [],
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
});
|
|
678
|
+
const { digest } = publishTestBundle(
|
|
679
|
+
manifest,
|
|
680
|
+
"local",
|
|
681
|
+
'#!/bin/sh\necho "$AUTH_HEADER"\n',
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
const deps = buildDeps({
|
|
685
|
+
materializeCredential: successMaterializer("my-oauth-token"),
|
|
686
|
+
});
|
|
687
|
+
addCommandGrant(
|
|
688
|
+
deps.persistentStore,
|
|
689
|
+
"local_static:test/api_key",
|
|
690
|
+
manifest.bundleId,
|
|
691
|
+
"list",
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
const request: ExecuteCommandRequest = {
|
|
695
|
+
bundleDigest: digest,
|
|
696
|
+
profileName: "list",
|
|
697
|
+
credentialHandle: "local_static:test/api_key",
|
|
698
|
+
argv: ["list", "--format", "json"],
|
|
699
|
+
workspaceDir: testWorkspaceDir,
|
|
700
|
+
purpose: "Test env_var adapter with prefix",
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
704
|
+
|
|
705
|
+
if (result.exitCode === 0) {
|
|
706
|
+
expect(result.stdout?.trim()).toBe("Bearer my-oauth-token");
|
|
707
|
+
}
|
|
708
|
+
expect(result.error ?? "").not.toContain("Auth adapter");
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test("temp_file adapter writes credential to a file", async () => {
|
|
712
|
+
const manifest = buildManifest({
|
|
713
|
+
egressMode: EgressMode.NoNetwork,
|
|
714
|
+
authAdapter: {
|
|
715
|
+
type: AuthAdapterType.TempFile,
|
|
716
|
+
envVarName: "CRED_FILE",
|
|
717
|
+
fileExtension: ".json",
|
|
718
|
+
},
|
|
719
|
+
commandProfiles: {
|
|
720
|
+
"list": {
|
|
721
|
+
description: "List resources",
|
|
722
|
+
allowedArgvPatterns: [
|
|
723
|
+
{
|
|
724
|
+
name: "list-all",
|
|
725
|
+
tokens: ["list", "--format", "<format>"],
|
|
726
|
+
},
|
|
727
|
+
],
|
|
728
|
+
deniedSubcommands: [],
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
// Script that reads the file pointed to by CRED_FILE
|
|
733
|
+
const { digest } = publishTestBundle(
|
|
734
|
+
manifest,
|
|
735
|
+
"local",
|
|
736
|
+
'#!/bin/sh\ncat "$CRED_FILE"\n',
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
const deps = buildDeps({
|
|
740
|
+
materializeCredential: successMaterializer('{"key":"test-secret"}'),
|
|
741
|
+
});
|
|
742
|
+
addCommandGrant(
|
|
743
|
+
deps.persistentStore,
|
|
744
|
+
"local_static:test/api_key",
|
|
745
|
+
manifest.bundleId,
|
|
746
|
+
"list",
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
const request: ExecuteCommandRequest = {
|
|
750
|
+
bundleDigest: digest,
|
|
751
|
+
profileName: "list",
|
|
752
|
+
credentialHandle: "local_static:test/api_key",
|
|
753
|
+
argv: ["list", "--format", "json"],
|
|
754
|
+
workspaceDir: testWorkspaceDir,
|
|
755
|
+
purpose: "Test temp_file adapter",
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
759
|
+
|
|
760
|
+
if (result.exitCode === 0) {
|
|
761
|
+
expect(result.stdout?.trim()).toBe('{"key":"test-secret"}');
|
|
762
|
+
}
|
|
763
|
+
// Verify we got past the adapter phase
|
|
764
|
+
expect(result.error ?? "").not.toContain("Auth adapter");
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
// ---------------------------------------------------------------------------
|
|
769
|
+
// Egress proxy enforcement tests
|
|
770
|
+
// ---------------------------------------------------------------------------
|
|
771
|
+
|
|
772
|
+
describe("executeAuthenticatedCommand — egress enforcement", () => {
|
|
773
|
+
test("rejects proxy_required without egress hooks", async () => {
|
|
774
|
+
const manifest = buildManifest({
|
|
775
|
+
egressMode: EgressMode.ProxyRequired,
|
|
776
|
+
});
|
|
777
|
+
const { digest } = publishTestBundle(manifest);
|
|
778
|
+
|
|
779
|
+
const deps = buildDeps({
|
|
780
|
+
egressHooks: undefined, // No hooks provided
|
|
781
|
+
});
|
|
782
|
+
addCommandGrant(
|
|
783
|
+
deps.persistentStore,
|
|
784
|
+
"local_static:test/api_key",
|
|
785
|
+
manifest.bundleId,
|
|
786
|
+
"list",
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
const request: ExecuteCommandRequest = {
|
|
790
|
+
bundleDigest: digest,
|
|
791
|
+
profileName: "list",
|
|
792
|
+
credentialHandle: "local_static:test/api_key",
|
|
793
|
+
argv: ["list", "--format", "json"],
|
|
794
|
+
workspaceDir: testWorkspaceDir,
|
|
795
|
+
purpose: "Test egress enforcement",
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
799
|
+
|
|
800
|
+
expect(result.success).toBe(false);
|
|
801
|
+
expect(result.error).toContain("proxy_required");
|
|
802
|
+
expect(result.error).toContain("no egress hooks");
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
test("allows no_network mode without egress hooks", async () => {
|
|
806
|
+
const manifest = buildManifest({
|
|
807
|
+
egressMode: EgressMode.NoNetwork,
|
|
808
|
+
commandProfiles: {
|
|
809
|
+
"list": {
|
|
810
|
+
description: "List resources",
|
|
811
|
+
allowedArgvPatterns: [
|
|
812
|
+
{
|
|
813
|
+
name: "list-all",
|
|
814
|
+
tokens: ["list", "--format", "<format>"],
|
|
815
|
+
},
|
|
816
|
+
],
|
|
817
|
+
deniedSubcommands: [],
|
|
818
|
+
},
|
|
819
|
+
},
|
|
820
|
+
});
|
|
821
|
+
const { digest } = publishTestBundle(
|
|
822
|
+
manifest,
|
|
823
|
+
"local",
|
|
824
|
+
'#!/bin/sh\necho "offline mode"\n',
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
const deps = buildDeps();
|
|
828
|
+
addCommandGrant(
|
|
829
|
+
deps.persistentStore,
|
|
830
|
+
"local_static:test/api_key",
|
|
831
|
+
manifest.bundleId,
|
|
832
|
+
"list",
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
const request: ExecuteCommandRequest = {
|
|
836
|
+
bundleDigest: digest,
|
|
837
|
+
profileName: "list",
|
|
838
|
+
credentialHandle: "local_static:test/api_key",
|
|
839
|
+
argv: ["list", "--format", "json"],
|
|
840
|
+
workspaceDir: testWorkspaceDir,
|
|
841
|
+
purpose: "Test no_network mode",
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
845
|
+
|
|
846
|
+
// Should get past egress check
|
|
847
|
+
expect(result.error ?? "").not.toContain("proxy_required");
|
|
848
|
+
expect(result.error ?? "").not.toContain("egress");
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// ---------------------------------------------------------------------------
|
|
853
|
+
// Command execution tests
|
|
854
|
+
// ---------------------------------------------------------------------------
|
|
855
|
+
|
|
856
|
+
describe("executeAuthenticatedCommand — command execution", () => {
|
|
857
|
+
test("executes a simple shell script successfully", async () => {
|
|
858
|
+
const manifest = buildManifest({
|
|
859
|
+
egressMode: EgressMode.NoNetwork,
|
|
860
|
+
commandProfiles: {
|
|
861
|
+
"list": {
|
|
862
|
+
description: "List resources",
|
|
863
|
+
allowedArgvPatterns: [
|
|
864
|
+
{
|
|
865
|
+
name: "list-all",
|
|
866
|
+
tokens: ["list", "--format", "<format>"],
|
|
867
|
+
},
|
|
868
|
+
],
|
|
869
|
+
deniedSubcommands: [],
|
|
870
|
+
},
|
|
871
|
+
},
|
|
872
|
+
});
|
|
873
|
+
const { digest } = publishTestBundle(
|
|
874
|
+
manifest,
|
|
875
|
+
"local",
|
|
876
|
+
'#!/bin/sh\necho "success: $1 $2 $3"\n',
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
const deps = buildDeps();
|
|
880
|
+
addCommandGrant(
|
|
881
|
+
deps.persistentStore,
|
|
882
|
+
"local_static:test/api_key",
|
|
883
|
+
manifest.bundleId,
|
|
884
|
+
"list",
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
const request: ExecuteCommandRequest = {
|
|
888
|
+
bundleDigest: digest,
|
|
889
|
+
profileName: "list",
|
|
890
|
+
credentialHandle: "local_static:test/api_key",
|
|
891
|
+
argv: ["list", "--format", "json"],
|
|
892
|
+
workspaceDir: testWorkspaceDir,
|
|
893
|
+
purpose: "Test execution",
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
897
|
+
|
|
898
|
+
expect(result.exitCode).toBe(0);
|
|
899
|
+
expect(result.success).toBe(true);
|
|
900
|
+
expect(result.stdout).toContain("success: list --format json");
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
test("captures non-zero exit code", async () => {
|
|
904
|
+
const manifest = buildManifest({
|
|
905
|
+
egressMode: EgressMode.NoNetwork,
|
|
906
|
+
commandProfiles: {
|
|
907
|
+
"list": {
|
|
908
|
+
description: "List resources",
|
|
909
|
+
allowedArgvPatterns: [
|
|
910
|
+
{
|
|
911
|
+
name: "list-all",
|
|
912
|
+
tokens: ["list", "--format", "<format>"],
|
|
913
|
+
},
|
|
914
|
+
],
|
|
915
|
+
deniedSubcommands: [],
|
|
916
|
+
},
|
|
917
|
+
},
|
|
918
|
+
});
|
|
919
|
+
const { digest } = publishTestBundle(
|
|
920
|
+
manifest,
|
|
921
|
+
"local",
|
|
922
|
+
'#!/bin/sh\necho "error" >&2\nexit 42\n',
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
const deps = buildDeps();
|
|
926
|
+
addCommandGrant(
|
|
927
|
+
deps.persistentStore,
|
|
928
|
+
"local_static:test/api_key",
|
|
929
|
+
manifest.bundleId,
|
|
930
|
+
"list",
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
const request: ExecuteCommandRequest = {
|
|
934
|
+
bundleDigest: digest,
|
|
935
|
+
profileName: "list",
|
|
936
|
+
credentialHandle: "local_static:test/api_key",
|
|
937
|
+
argv: ["list", "--format", "json"],
|
|
938
|
+
workspaceDir: testWorkspaceDir,
|
|
939
|
+
purpose: "Test non-zero exit",
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
943
|
+
|
|
944
|
+
expect(result.exitCode).toBe(42);
|
|
945
|
+
expect(result.success).toBe(false);
|
|
946
|
+
expect(result.stderr).toContain("error");
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
test("does not leak CES process environment to subprocess", async () => {
|
|
950
|
+
const manifest = buildManifest({
|
|
951
|
+
egressMode: EgressMode.NoNetwork,
|
|
952
|
+
commandProfiles: {
|
|
953
|
+
"list": {
|
|
954
|
+
description: "List resources",
|
|
955
|
+
allowedArgvPatterns: [
|
|
956
|
+
{
|
|
957
|
+
name: "list-all",
|
|
958
|
+
tokens: ["list", "--format", "<format>"],
|
|
959
|
+
},
|
|
960
|
+
],
|
|
961
|
+
deniedSubcommands: [],
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
});
|
|
965
|
+
// Script that tries to read a common CES env var
|
|
966
|
+
const { digest } = publishTestBundle(
|
|
967
|
+
manifest,
|
|
968
|
+
"local",
|
|
969
|
+
'#!/bin/sh\necho "CES_MODE=${CES_MODE:-unset}"\necho "BASE_DATA_DIR=${BASE_DATA_DIR:-unset}"\n',
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
const deps = buildDeps();
|
|
973
|
+
addCommandGrant(
|
|
974
|
+
deps.persistentStore,
|
|
975
|
+
"local_static:test/api_key",
|
|
976
|
+
manifest.bundleId,
|
|
977
|
+
"list",
|
|
978
|
+
);
|
|
979
|
+
|
|
980
|
+
const request: ExecuteCommandRequest = {
|
|
981
|
+
bundleDigest: digest,
|
|
982
|
+
profileName: "list",
|
|
983
|
+
credentialHandle: "local_static:test/api_key",
|
|
984
|
+
argv: ["list", "--format", "json"],
|
|
985
|
+
workspaceDir: testWorkspaceDir,
|
|
986
|
+
purpose: "Test env isolation",
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
990
|
+
|
|
991
|
+
if (result.exitCode === 0) {
|
|
992
|
+
expect(result.stdout).toContain("CES_MODE=unset");
|
|
993
|
+
expect(result.stdout).toContain("BASE_DATA_DIR=unset");
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
// ---------------------------------------------------------------------------
|
|
999
|
+
// Output copyback tests
|
|
1000
|
+
// ---------------------------------------------------------------------------
|
|
1001
|
+
|
|
1002
|
+
describe("executeAuthenticatedCommand — output copyback", () => {
|
|
1003
|
+
test("copies declared output files back to workspace", async () => {
|
|
1004
|
+
const manifest = buildManifest({
|
|
1005
|
+
egressMode: EgressMode.NoNetwork,
|
|
1006
|
+
commandProfiles: {
|
|
1007
|
+
"list": {
|
|
1008
|
+
description: "List resources",
|
|
1009
|
+
allowedArgvPatterns: [
|
|
1010
|
+
{
|
|
1011
|
+
name: "list-all",
|
|
1012
|
+
tokens: ["list", "--format", "<format>"],
|
|
1013
|
+
},
|
|
1014
|
+
],
|
|
1015
|
+
deniedSubcommands: [],
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
});
|
|
1019
|
+
// Script that creates an output file in the cwd
|
|
1020
|
+
const { digest } = publishTestBundle(
|
|
1021
|
+
manifest,
|
|
1022
|
+
"local",
|
|
1023
|
+
'#!/bin/sh\necho \'{"result":"ok"}\' > output.json\n',
|
|
1024
|
+
);
|
|
1025
|
+
|
|
1026
|
+
const deps = buildDeps();
|
|
1027
|
+
addCommandGrant(
|
|
1028
|
+
deps.persistentStore,
|
|
1029
|
+
"local_static:test/api_key",
|
|
1030
|
+
manifest.bundleId,
|
|
1031
|
+
"list",
|
|
1032
|
+
);
|
|
1033
|
+
|
|
1034
|
+
const request: ExecuteCommandRequest = {
|
|
1035
|
+
bundleDigest: digest,
|
|
1036
|
+
profileName: "list",
|
|
1037
|
+
credentialHandle: "local_static:test/api_key",
|
|
1038
|
+
argv: ["list", "--format", "json"],
|
|
1039
|
+
workspaceDir: testWorkspaceDir,
|
|
1040
|
+
purpose: "Test output copyback",
|
|
1041
|
+
outputs: [
|
|
1042
|
+
{
|
|
1043
|
+
scratchPath: "output.json",
|
|
1044
|
+
workspacePath: "results/output.json",
|
|
1045
|
+
},
|
|
1046
|
+
],
|
|
1047
|
+
};
|
|
1048
|
+
|
|
1049
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
1050
|
+
|
|
1051
|
+
if (result.exitCode === 0) {
|
|
1052
|
+
const expectedPath = join(testWorkspaceDir, "results/output.json");
|
|
1053
|
+
if (result.copybackResult) {
|
|
1054
|
+
// Check if the output was detected
|
|
1055
|
+
const outputResult = result.copybackResult.outputs[0];
|
|
1056
|
+
expect(outputResult).toBeDefined();
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
// ---------------------------------------------------------------------------
|
|
1063
|
+
// Banned binary defense-in-depth tests
|
|
1064
|
+
// ---------------------------------------------------------------------------
|
|
1065
|
+
|
|
1066
|
+
describe("executeAuthenticatedCommand — banned binaries", () => {
|
|
1067
|
+
test("rejects execution of denied binaries at bundle level", async () => {
|
|
1068
|
+
// Since publishBundle validates the manifest, we can't publish
|
|
1069
|
+
// a bundle with a denied binary. This test verifies the error path
|
|
1070
|
+
// by checking that resolution fails for non-published digests.
|
|
1071
|
+
const deps = buildDeps();
|
|
1072
|
+
const request: ExecuteCommandRequest = {
|
|
1073
|
+
bundleDigest: "b".repeat(64),
|
|
1074
|
+
profileName: "fetch",
|
|
1075
|
+
credentialHandle: "local_static:test/api_key",
|
|
1076
|
+
argv: ["https://example.com"],
|
|
1077
|
+
workspaceDir: testWorkspaceDir,
|
|
1078
|
+
purpose: "Try to run curl",
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
1082
|
+
|
|
1083
|
+
expect(result.success).toBe(false);
|
|
1084
|
+
expect(result.error).toContain("not published");
|
|
1085
|
+
});
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
// ---------------------------------------------------------------------------
|
|
1089
|
+
// RPC handler command string parsing tests
|
|
1090
|
+
// ---------------------------------------------------------------------------
|
|
1091
|
+
|
|
1092
|
+
describe("server — run_authenticated_command handler", () => {
|
|
1093
|
+
// Test the command string parsing logic used by the RPC handler.
|
|
1094
|
+
// We import the server module's createRunAuthenticatedCommandHandler
|
|
1095
|
+
// and test it with mock deps.
|
|
1096
|
+
|
|
1097
|
+
test("rejects empty command string", async () => {
|
|
1098
|
+
// Import the handler factory from server
|
|
1099
|
+
const { createRunAuthenticatedCommandHandler } = await import("../server.js");
|
|
1100
|
+
|
|
1101
|
+
const deps = buildDeps();
|
|
1102
|
+
const handler = createRunAuthenticatedCommandHandler({
|
|
1103
|
+
executorDeps: deps,
|
|
1104
|
+
defaultWorkspaceDir: testWorkspaceDir,
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
const response = await handler({
|
|
1108
|
+
credentialHandle: "local_static:test/api_key",
|
|
1109
|
+
command: "",
|
|
1110
|
+
purpose: "Test empty command",
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
expect(response.success).toBe(false);
|
|
1114
|
+
expect(response.error?.code).toBe("INVALID_COMMAND");
|
|
1115
|
+
expect(response.error?.message).toContain("empty");
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
test("rejects command without bundleDigest/profileName format", async () => {
|
|
1119
|
+
const { createRunAuthenticatedCommandHandler } = await import("../server.js");
|
|
1120
|
+
|
|
1121
|
+
const deps = buildDeps();
|
|
1122
|
+
const handler = createRunAuthenticatedCommandHandler({
|
|
1123
|
+
executorDeps: deps,
|
|
1124
|
+
defaultWorkspaceDir: testWorkspaceDir,
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
const response = await handler({
|
|
1128
|
+
credentialHandle: "local_static:test/api_key",
|
|
1129
|
+
command: "just-a-plain-command --with-args",
|
|
1130
|
+
purpose: "Test plain command",
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
expect(response.success).toBe(false);
|
|
1134
|
+
expect(response.error?.code).toBe("INVALID_COMMAND");
|
|
1135
|
+
expect(response.error?.message).toContain("Invalid command reference");
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
test("parses valid command string format", async () => {
|
|
1139
|
+
const { createRunAuthenticatedCommandHandler } = await import("../server.js");
|
|
1140
|
+
|
|
1141
|
+
const deps = buildDeps();
|
|
1142
|
+
const handler = createRunAuthenticatedCommandHandler({
|
|
1143
|
+
executorDeps: deps,
|
|
1144
|
+
defaultWorkspaceDir: testWorkspaceDir,
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
// This will fail at bundle resolution (fake digest), but the parse succeeds
|
|
1148
|
+
const response = await handler({
|
|
1149
|
+
credentialHandle: "local_static:test/api_key",
|
|
1150
|
+
command: `${"a".repeat(64)}/list api /repos --method GET`,
|
|
1151
|
+
purpose: "Test command parsing",
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
// Should fail at bundle resolution, not at command parsing
|
|
1155
|
+
expect(response.error?.code).not.toBe("INVALID_COMMAND");
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
// ---------------------------------------------------------------------------
|
|
1160
|
+
// Integration: full pipeline with local static secret
|
|
1161
|
+
// ---------------------------------------------------------------------------
|
|
1162
|
+
|
|
1163
|
+
describe("executeAuthenticatedCommand — integration: local static secret", () => {
|
|
1164
|
+
test("full pipeline with env_var adapter and no_network", async () => {
|
|
1165
|
+
const manifest = buildManifest({
|
|
1166
|
+
egressMode: EgressMode.NoNetwork,
|
|
1167
|
+
authAdapter: {
|
|
1168
|
+
type: AuthAdapterType.EnvVar,
|
|
1169
|
+
envVarName: "API_KEY",
|
|
1170
|
+
},
|
|
1171
|
+
commandProfiles: {
|
|
1172
|
+
"list": {
|
|
1173
|
+
description: "List resources",
|
|
1174
|
+
allowedArgvPatterns: [
|
|
1175
|
+
{
|
|
1176
|
+
name: "list-all",
|
|
1177
|
+
tokens: ["list", "--format", "<format>"],
|
|
1178
|
+
},
|
|
1179
|
+
],
|
|
1180
|
+
deniedSubcommands: [],
|
|
1181
|
+
},
|
|
1182
|
+
},
|
|
1183
|
+
});
|
|
1184
|
+
const { digest } = publishTestBundle(
|
|
1185
|
+
manifest,
|
|
1186
|
+
"local",
|
|
1187
|
+
'#!/bin/sh\nif [ -n "$API_KEY" ]; then echo "authed: $1 $2 $3"; else echo "no auth"; exit 1; fi\n',
|
|
1188
|
+
);
|
|
1189
|
+
|
|
1190
|
+
const deps = buildDeps({
|
|
1191
|
+
materializeCredential: successMaterializer("sk-test-key-12345"),
|
|
1192
|
+
});
|
|
1193
|
+
addCommandGrant(
|
|
1194
|
+
deps.persistentStore,
|
|
1195
|
+
"local_static:test/api_key",
|
|
1196
|
+
manifest.bundleId,
|
|
1197
|
+
"list",
|
|
1198
|
+
);
|
|
1199
|
+
|
|
1200
|
+
const request: ExecuteCommandRequest = {
|
|
1201
|
+
bundleDigest: digest,
|
|
1202
|
+
profileName: "list",
|
|
1203
|
+
credentialHandle: "local_static:test/api_key",
|
|
1204
|
+
argv: ["list", "--format", "json"],
|
|
1205
|
+
workspaceDir: testWorkspaceDir,
|
|
1206
|
+
purpose: "Full pipeline test",
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
1210
|
+
|
|
1211
|
+
expect(result.success).toBe(true);
|
|
1212
|
+
expect(result.exitCode).toBe(0);
|
|
1213
|
+
expect(result.stdout).toContain("authed: list --format json");
|
|
1214
|
+
expect(result.auditId).toBeDefined();
|
|
1215
|
+
});
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
// ---------------------------------------------------------------------------
|
|
1219
|
+
// Integration: local OAuth
|
|
1220
|
+
// ---------------------------------------------------------------------------
|
|
1221
|
+
|
|
1222
|
+
describe("executeAuthenticatedCommand — integration: local OAuth", () => {
|
|
1223
|
+
test("full pipeline with OAuth token and env_var adapter", async () => {
|
|
1224
|
+
const manifest = buildManifest({
|
|
1225
|
+
egressMode: EgressMode.NoNetwork,
|
|
1226
|
+
authAdapter: {
|
|
1227
|
+
type: AuthAdapterType.EnvVar,
|
|
1228
|
+
envVarName: "OAUTH_TOKEN",
|
|
1229
|
+
valuePrefix: "Bearer ",
|
|
1230
|
+
},
|
|
1231
|
+
commandProfiles: {
|
|
1232
|
+
"list": {
|
|
1233
|
+
description: "List resources",
|
|
1234
|
+
allowedArgvPatterns: [
|
|
1235
|
+
{
|
|
1236
|
+
name: "list-all",
|
|
1237
|
+
tokens: ["list", "--format", "<format>"],
|
|
1238
|
+
},
|
|
1239
|
+
],
|
|
1240
|
+
deniedSubcommands: [],
|
|
1241
|
+
},
|
|
1242
|
+
},
|
|
1243
|
+
});
|
|
1244
|
+
const { digest } = publishTestBundle(
|
|
1245
|
+
manifest,
|
|
1246
|
+
"local",
|
|
1247
|
+
'#!/bin/sh\necho "$OAUTH_TOKEN"\n',
|
|
1248
|
+
);
|
|
1249
|
+
|
|
1250
|
+
const deps = buildDeps({
|
|
1251
|
+
materializeCredential: successMaterializer("ya29.test-oauth-token"),
|
|
1252
|
+
});
|
|
1253
|
+
addCommandGrant(
|
|
1254
|
+
deps.persistentStore,
|
|
1255
|
+
"local_oauth:integration:google/conn-123",
|
|
1256
|
+
manifest.bundleId,
|
|
1257
|
+
"list",
|
|
1258
|
+
);
|
|
1259
|
+
|
|
1260
|
+
const request: ExecuteCommandRequest = {
|
|
1261
|
+
bundleDigest: digest,
|
|
1262
|
+
profileName: "list",
|
|
1263
|
+
credentialHandle: "local_oauth:integration:google/conn-123",
|
|
1264
|
+
argv: ["list", "--format", "json"],
|
|
1265
|
+
workspaceDir: testWorkspaceDir,
|
|
1266
|
+
purpose: "OAuth pipeline test",
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
1270
|
+
|
|
1271
|
+
expect(result.success).toBe(true);
|
|
1272
|
+
expect(result.exitCode).toBe(0);
|
|
1273
|
+
expect(result.stdout?.trim()).toBe("Bearer ya29.test-oauth-token");
|
|
1274
|
+
});
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
// ---------------------------------------------------------------------------
|
|
1278
|
+
// Integration: managed OAuth
|
|
1279
|
+
// ---------------------------------------------------------------------------
|
|
1280
|
+
|
|
1281
|
+
describe("executeAuthenticatedCommand — integration: managed OAuth", () => {
|
|
1282
|
+
test("full pipeline with managed OAuth token", async () => {
|
|
1283
|
+
const manifest = buildManifest({
|
|
1284
|
+
egressMode: EgressMode.NoNetwork,
|
|
1285
|
+
authAdapter: {
|
|
1286
|
+
type: AuthAdapterType.EnvVar,
|
|
1287
|
+
envVarName: "PLATFORM_TOKEN",
|
|
1288
|
+
},
|
|
1289
|
+
commandProfiles: {
|
|
1290
|
+
"list": {
|
|
1291
|
+
description: "List resources",
|
|
1292
|
+
allowedArgvPatterns: [
|
|
1293
|
+
{
|
|
1294
|
+
name: "list-all",
|
|
1295
|
+
tokens: ["list", "--format", "<format>"],
|
|
1296
|
+
},
|
|
1297
|
+
],
|
|
1298
|
+
deniedSubcommands: [],
|
|
1299
|
+
},
|
|
1300
|
+
},
|
|
1301
|
+
});
|
|
1302
|
+
const { digest } = publishTestBundle(
|
|
1303
|
+
manifest,
|
|
1304
|
+
"local",
|
|
1305
|
+
'#!/bin/sh\necho "$PLATFORM_TOKEN"\n',
|
|
1306
|
+
);
|
|
1307
|
+
|
|
1308
|
+
const deps = buildDeps({
|
|
1309
|
+
materializeCredential: successMaterializer("platform-managed-token-xyz"),
|
|
1310
|
+
});
|
|
1311
|
+
addCommandGrant(
|
|
1312
|
+
deps.persistentStore,
|
|
1313
|
+
"platform_oauth:platform-conn-456",
|
|
1314
|
+
manifest.bundleId,
|
|
1315
|
+
"list",
|
|
1316
|
+
);
|
|
1317
|
+
|
|
1318
|
+
const request: ExecuteCommandRequest = {
|
|
1319
|
+
bundleDigest: digest,
|
|
1320
|
+
profileName: "list",
|
|
1321
|
+
credentialHandle: "platform_oauth:platform-conn-456",
|
|
1322
|
+
argv: ["list", "--format", "json"],
|
|
1323
|
+
workspaceDir: testWorkspaceDir,
|
|
1324
|
+
purpose: "Managed OAuth pipeline test",
|
|
1325
|
+
};
|
|
1326
|
+
|
|
1327
|
+
const result = await executeAuthenticatedCommand(request, deps);
|
|
1328
|
+
|
|
1329
|
+
expect(result.success).toBe(true);
|
|
1330
|
+
expect(result.exitCode).toBe(0);
|
|
1331
|
+
expect(result.stdout?.trim()).toBe("platform-managed-token-xyz");
|
|
1332
|
+
});
|
|
1333
|
+
});
|