@enactprotocol/mcp-server 2.2.0 → 2.2.2
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/dist/index.js +918 -650
- package/package.json +6 -5
- package/src/index.ts +114 -13
- package/tests/trust-policy.test.ts +280 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/index.d.ts +0 -29
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types.d.ts +0 -5
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enactprotocol/mcp-server",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "MCP protocol server for Enact tool integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
|
-
"build": "
|
|
18
|
+
"build": "bun build src/index.ts --outfile dist/index.js --target node --format esm --minify",
|
|
19
|
+
"build:types": "tsc --build",
|
|
19
20
|
"clean": "rm -rf dist",
|
|
20
21
|
"test": "bun test",
|
|
21
22
|
"typecheck": "tsc --noEmit",
|
|
@@ -23,9 +24,9 @@
|
|
|
23
24
|
"dev:http": "bun run src/index.ts --http"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
|
-
"@enactprotocol/api": "2.2.
|
|
27
|
-
"@enactprotocol/execution": "2.2.
|
|
28
|
-
"@enactprotocol/shared": "2.2.
|
|
27
|
+
"@enactprotocol/api": "2.2.2",
|
|
28
|
+
"@enactprotocol/execution": "2.2.2",
|
|
29
|
+
"@enactprotocol/shared": "2.2.2",
|
|
29
30
|
"@modelcontextprotocol/sdk": "^1.10.0"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* @see https://modelcontextprotocol.io/specification/2025-03-26/basic/transports
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
16
17
|
import { randomUUID } from "node:crypto";
|
|
17
18
|
import { mkdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
18
19
|
import { type IncomingMessage, type ServerResponse, createServer } from "node:http";
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
getToolInfo,
|
|
24
25
|
getToolVersion,
|
|
25
26
|
searchTools,
|
|
27
|
+
verifyAllAttestations,
|
|
26
28
|
} from "@enactprotocol/api";
|
|
27
29
|
import { DaggerExecutionProvider } from "@enactprotocol/execution";
|
|
28
30
|
import {
|
|
@@ -33,7 +35,10 @@ import {
|
|
|
33
35
|
getActiveToolset,
|
|
34
36
|
getCacheDir,
|
|
35
37
|
getMcpToolInfo,
|
|
38
|
+
getMinimumAttestations,
|
|
36
39
|
getToolCachePath,
|
|
40
|
+
getTrustPolicy,
|
|
41
|
+
isIdentityTrusted,
|
|
37
42
|
listMcpTools,
|
|
38
43
|
loadConfig,
|
|
39
44
|
loadManifestFromDir,
|
|
@@ -111,23 +116,31 @@ async function extractBundle(bundleData: ArrayBuffer, destPath: string): Promise
|
|
|
111
116
|
|
|
112
117
|
mkdirSync(destPath, { recursive: true });
|
|
113
118
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
await new Promise<void>((resolve, reject) => {
|
|
120
|
+
const proc = spawn("tar", ["-xzf", tempFile, "-C", destPath], {
|
|
121
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let stderr = "";
|
|
125
|
+
proc.stderr?.on("data", (data) => {
|
|
126
|
+
stderr += data.toString();
|
|
127
|
+
});
|
|
118
128
|
|
|
119
|
-
|
|
129
|
+
proc.on("error", reject);
|
|
130
|
+
proc.on("close", (code) => {
|
|
131
|
+
if (code !== 0) {
|
|
132
|
+
reject(new Error(`Failed to extract bundle: ${stderr}`));
|
|
133
|
+
} else {
|
|
134
|
+
resolve();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
120
138
|
|
|
121
139
|
try {
|
|
122
140
|
unlinkSync(tempFile);
|
|
123
141
|
} catch {
|
|
124
142
|
// Ignore cleanup errors
|
|
125
143
|
}
|
|
126
|
-
|
|
127
|
-
if (exitCode !== 0) {
|
|
128
|
-
const stderr = await new Response(proc.stderr).text();
|
|
129
|
-
throw new Error(`Failed to extract bundle: ${stderr}`);
|
|
130
|
-
}
|
|
131
144
|
}
|
|
132
145
|
|
|
133
146
|
/**
|
|
@@ -327,6 +340,10 @@ async function handleMetaTool(
|
|
|
327
340
|
const toolInfo = getMcpToolInfo(toolNameArg);
|
|
328
341
|
let manifest: ToolManifest;
|
|
329
342
|
let cachePath: string;
|
|
343
|
+
let bundleHash: string | undefined;
|
|
344
|
+
let toolVersion: string | undefined;
|
|
345
|
+
|
|
346
|
+
const apiClient = getApiClient();
|
|
330
347
|
|
|
331
348
|
if (toolInfo) {
|
|
332
349
|
// Tool is installed, use cached version
|
|
@@ -339,10 +356,19 @@ async function handleMetaTool(
|
|
|
339
356
|
}
|
|
340
357
|
manifest = loaded.manifest;
|
|
341
358
|
cachePath = toolInfo.cachePath;
|
|
359
|
+
toolVersion = toolInfo.version;
|
|
360
|
+
|
|
361
|
+
// Get bundle hash for installed tool from registry
|
|
362
|
+
try {
|
|
363
|
+
const versionInfo = await getToolVersion(apiClient, toolNameArg, toolVersion);
|
|
364
|
+
bundleHash = versionInfo.bundle.hash;
|
|
365
|
+
} catch {
|
|
366
|
+
// Continue without hash - will skip verification
|
|
367
|
+
}
|
|
342
368
|
} else {
|
|
343
369
|
// Tool not installed - fetch and install temporarily
|
|
344
|
-
const apiClient = getApiClient();
|
|
345
370
|
const info = await getToolInfo(apiClient, toolNameArg);
|
|
371
|
+
toolVersion = info.latestVersion;
|
|
346
372
|
|
|
347
373
|
// Download bundle
|
|
348
374
|
const bundleResult = await downloadBundle(apiClient, {
|
|
@@ -350,6 +376,7 @@ async function handleMetaTool(
|
|
|
350
376
|
version: info.latestVersion,
|
|
351
377
|
verify: true,
|
|
352
378
|
});
|
|
379
|
+
bundleHash = bundleResult.hash;
|
|
353
380
|
|
|
354
381
|
// Extract to cache
|
|
355
382
|
cachePath = getToolCachePath(toolNameArg, info.latestVersion);
|
|
@@ -369,6 +396,79 @@ async function handleMetaTool(
|
|
|
369
396
|
manifest = loaded.manifest;
|
|
370
397
|
}
|
|
371
398
|
|
|
399
|
+
// Verify attestations before execution
|
|
400
|
+
let verificationStatus = "⚠️ UNVERIFIED";
|
|
401
|
+
|
|
402
|
+
if (bundleHash && toolVersion) {
|
|
403
|
+
try {
|
|
404
|
+
const verified = await verifyAllAttestations(
|
|
405
|
+
apiClient,
|
|
406
|
+
toolNameArg,
|
|
407
|
+
toolVersion,
|
|
408
|
+
bundleHash
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
if (verified.length > 0) {
|
|
412
|
+
const auditorList = verified.map((v) => v.providerIdentity).join(", ");
|
|
413
|
+
verificationStatus = `✅ VERIFIED by: ${auditorList}`;
|
|
414
|
+
} else {
|
|
415
|
+
verificationStatus = "⚠️ UNVERIFIED - No valid attestations found";
|
|
416
|
+
}
|
|
417
|
+
} catch (verifyErr) {
|
|
418
|
+
verificationStatus = `⚠️ UNVERIFIED - Verification failed: ${verifyErr instanceof Error ? verifyErr.message : "Unknown error"}`;
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
verificationStatus = "⚠️ UNVERIFIED - Could not determine bundle hash";
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Enforce trust policy
|
|
425
|
+
const trustPolicy = getTrustPolicy();
|
|
426
|
+
const minimumAttestations = getMinimumAttestations();
|
|
427
|
+
|
|
428
|
+
// Count verified attestations from trusted auditors
|
|
429
|
+
let verifiedCount = 0;
|
|
430
|
+
if (bundleHash && toolVersion) {
|
|
431
|
+
try {
|
|
432
|
+
const verified = await verifyAllAttestations(
|
|
433
|
+
apiClient,
|
|
434
|
+
toolNameArg,
|
|
435
|
+
toolVersion,
|
|
436
|
+
bundleHash
|
|
437
|
+
);
|
|
438
|
+
verifiedCount = verified.filter((v) => isIdentityTrusted(v.providerIdentity)).length;
|
|
439
|
+
} catch {
|
|
440
|
+
// Already handled above in verificationStatus
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Check if trust requirements are met
|
|
445
|
+
if (verifiedCount < minimumAttestations) {
|
|
446
|
+
if (trustPolicy === "require_attestation") {
|
|
447
|
+
return {
|
|
448
|
+
content: [
|
|
449
|
+
{
|
|
450
|
+
type: "text",
|
|
451
|
+
text: `Trust policy violation: Tool requires ${minimumAttestations} attestation(s) from trusted auditors, but only ${verifiedCount} found.\n\nConfigured trust policy: ${trustPolicy}\nTo run unverified tools, update your ~/.enact/config.yaml trust policy to 'allow' or 'prompt'.`,
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
isError: true,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
if (trustPolicy === "prompt") {
|
|
458
|
+
// In MCP context, we can't prompt interactively, so we block with a clear message
|
|
459
|
+
return {
|
|
460
|
+
content: [
|
|
461
|
+
{
|
|
462
|
+
type: "text",
|
|
463
|
+
text: `Trust policy violation: Tool requires ${minimumAttestations} attestation(s) from trusted auditors, but only ${verifiedCount} found.\n\nConfigured trust policy: ${trustPolicy}\nMCP server cannot prompt interactively. To run unverified tools via MCP, update your ~/.enact/config.yaml trust policy to 'allow'.`,
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
isError: true,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
// policy === 'allow' - continue execution with warning
|
|
470
|
+
}
|
|
471
|
+
|
|
372
472
|
// Validate and apply defaults
|
|
373
473
|
const inputsWithDefaults = manifest.inputSchema
|
|
374
474
|
? applyDefaults(toolArgs, manifest.inputSchema)
|
|
@@ -396,11 +496,12 @@ async function handleMetaTool(
|
|
|
396
496
|
);
|
|
397
497
|
|
|
398
498
|
if (result.success) {
|
|
499
|
+
const output = result.output?.stdout || "Tool executed successfully (no output)";
|
|
399
500
|
return {
|
|
400
501
|
content: [
|
|
401
502
|
{
|
|
402
503
|
type: "text",
|
|
403
|
-
text:
|
|
504
|
+
text: `[${verificationStatus}]\n\n${output}`,
|
|
404
505
|
},
|
|
405
506
|
],
|
|
406
507
|
};
|
|
@@ -409,7 +510,7 @@ async function handleMetaTool(
|
|
|
409
510
|
content: [
|
|
410
511
|
{
|
|
411
512
|
type: "text",
|
|
412
|
-
text: `
|
|
513
|
+
text: `[${verificationStatus}]\n\nTool execution failed: ${result.error?.message || "Unknown error"}\n\n${result.output?.stderr || ""}`,
|
|
413
514
|
},
|
|
414
515
|
],
|
|
415
516
|
isError: true,
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for MCP server trust policy enforcement
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that the enact_run handler properly enforces
|
|
5
|
+
* trust policies based on attestation verification.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
// Type for trust policy
|
|
11
|
+
type TrustPolicy = "require_attestation" | "prompt" | "allow";
|
|
12
|
+
|
|
13
|
+
// Mock the shared config functions
|
|
14
|
+
const mockGetTrustPolicy = mock((): TrustPolicy => "require_attestation");
|
|
15
|
+
const mockGetMinimumAttestations = mock(() => 1);
|
|
16
|
+
const mockIsIdentityTrusted = mock((_identity: string) => false);
|
|
17
|
+
|
|
18
|
+
// Mock the API functions
|
|
19
|
+
const mockVerifyAllAttestations = mock(async () => []);
|
|
20
|
+
|
|
21
|
+
describe("MCP Server Trust Policy Enforcement", () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
// Reset mocks before each test
|
|
24
|
+
mockGetTrustPolicy.mockReset();
|
|
25
|
+
mockGetMinimumAttestations.mockReset();
|
|
26
|
+
mockIsIdentityTrusted.mockReset();
|
|
27
|
+
mockVerifyAllAttestations.mockReset();
|
|
28
|
+
|
|
29
|
+
// Set default mock implementations
|
|
30
|
+
mockGetTrustPolicy.mockImplementation((): TrustPolicy => "require_attestation");
|
|
31
|
+
mockGetMinimumAttestations.mockImplementation(() => 1);
|
|
32
|
+
mockIsIdentityTrusted.mockImplementation(() => false);
|
|
33
|
+
mockVerifyAllAttestations.mockImplementation(async () => []);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("Trust Policy Logic", () => {
|
|
37
|
+
test("should block execution when policy is 'require_attestation' and no attestations", () => {
|
|
38
|
+
const trustPolicy = mockGetTrustPolicy();
|
|
39
|
+
const minimumAttestations = mockGetMinimumAttestations();
|
|
40
|
+
const verifiedCount = 0;
|
|
41
|
+
|
|
42
|
+
// Simulate the trust check logic from enact_run
|
|
43
|
+
const shouldBlock =
|
|
44
|
+
verifiedCount < minimumAttestations && trustPolicy === "require_attestation";
|
|
45
|
+
|
|
46
|
+
expect(shouldBlock).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("should block execution when policy is 'prompt' and no attestations", () => {
|
|
50
|
+
mockGetTrustPolicy.mockImplementation((): TrustPolicy => "prompt");
|
|
51
|
+
|
|
52
|
+
const trustPolicy = mockGetTrustPolicy();
|
|
53
|
+
const minimumAttestations = mockGetMinimumAttestations();
|
|
54
|
+
const verifiedCount = 0;
|
|
55
|
+
|
|
56
|
+
// Simulate the trust check logic from enact_run
|
|
57
|
+
const shouldBlock = verifiedCount < minimumAttestations && trustPolicy === "prompt";
|
|
58
|
+
|
|
59
|
+
expect(shouldBlock).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("should allow execution when policy is 'allow' regardless of attestations", () => {
|
|
63
|
+
mockGetTrustPolicy.mockImplementation((): TrustPolicy => "allow");
|
|
64
|
+
|
|
65
|
+
const trustPolicy = mockGetTrustPolicy();
|
|
66
|
+
const minimumAttestations = mockGetMinimumAttestations();
|
|
67
|
+
const verifiedCount = 0;
|
|
68
|
+
|
|
69
|
+
// Simulate the trust check logic from enact_run
|
|
70
|
+
const shouldBlock =
|
|
71
|
+
verifiedCount < minimumAttestations &&
|
|
72
|
+
(trustPolicy === "require_attestation" || trustPolicy === "prompt");
|
|
73
|
+
|
|
74
|
+
expect(shouldBlock).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("should allow execution when attestations meet minimum requirement", () => {
|
|
78
|
+
mockIsIdentityTrusted.mockImplementation(() => true);
|
|
79
|
+
|
|
80
|
+
const trustPolicy = mockGetTrustPolicy();
|
|
81
|
+
const minimumAttestations = mockGetMinimumAttestations();
|
|
82
|
+
const verifiedCount = 1; // Meets minimum
|
|
83
|
+
|
|
84
|
+
const shouldBlock =
|
|
85
|
+
verifiedCount < minimumAttestations &&
|
|
86
|
+
(trustPolicy === "require_attestation" || trustPolicy === "prompt");
|
|
87
|
+
|
|
88
|
+
expect(shouldBlock).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("should require higher attestation count when minimum_attestations is increased", () => {
|
|
92
|
+
mockGetMinimumAttestations.mockImplementation(() => 3);
|
|
93
|
+
|
|
94
|
+
const trustPolicy = mockGetTrustPolicy();
|
|
95
|
+
const minimumAttestations = mockGetMinimumAttestations();
|
|
96
|
+
const verifiedCount = 2; // Below new minimum of 3
|
|
97
|
+
|
|
98
|
+
const shouldBlock =
|
|
99
|
+
verifiedCount < minimumAttestations && trustPolicy === "require_attestation";
|
|
100
|
+
|
|
101
|
+
expect(shouldBlock).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("Identity Trust Filtering", () => {
|
|
106
|
+
test("should only count attestations from trusted identities", () => {
|
|
107
|
+
// Mock attestations with various identities
|
|
108
|
+
const attestations = [
|
|
109
|
+
{ providerIdentity: "github:trusted-user" },
|
|
110
|
+
{ providerIdentity: "github:untrusted-user" },
|
|
111
|
+
{ providerIdentity: "google:trusted@company.com" },
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
// Only trust specific identities
|
|
115
|
+
mockIsIdentityTrusted.mockImplementation((identity: string) => {
|
|
116
|
+
return identity === "github:trusted-user" || identity === "google:trusted@company.com";
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const verifiedCount = attestations.filter((v) =>
|
|
120
|
+
mockIsIdentityTrusted(v.providerIdentity)
|
|
121
|
+
).length;
|
|
122
|
+
|
|
123
|
+
expect(verifiedCount).toBe(2);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("should return zero when no attestations are from trusted identities", () => {
|
|
127
|
+
const attestations = [
|
|
128
|
+
{ providerIdentity: "github:unknown-user" },
|
|
129
|
+
{ providerIdentity: "github:another-unknown" },
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
mockIsIdentityTrusted.mockImplementation(() => false);
|
|
133
|
+
|
|
134
|
+
const verifiedCount = attestations.filter((v) =>
|
|
135
|
+
mockIsIdentityTrusted(v.providerIdentity)
|
|
136
|
+
).length;
|
|
137
|
+
|
|
138
|
+
expect(verifiedCount).toBe(0);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("Error Message Generation", () => {
|
|
143
|
+
test("should generate correct error message for require_attestation policy", () => {
|
|
144
|
+
const trustPolicy = "require_attestation";
|
|
145
|
+
const minimumAttestations = 1;
|
|
146
|
+
const verifiedCount = 0;
|
|
147
|
+
|
|
148
|
+
const errorMessage = `Trust policy violation: Tool requires ${minimumAttestations} attestation(s) from trusted auditors, but only ${verifiedCount} found.\n\nConfigured trust policy: ${trustPolicy}\nTo run unverified tools, update your ~/.enact/config.yaml trust policy to 'allow' or 'prompt'.`;
|
|
149
|
+
|
|
150
|
+
expect(errorMessage).toContain("Trust policy violation");
|
|
151
|
+
expect(errorMessage).toContain("require_attestation");
|
|
152
|
+
expect(errorMessage).toContain("~/.enact/config.yaml");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("should generate correct error message for prompt policy", () => {
|
|
156
|
+
const trustPolicy = "prompt";
|
|
157
|
+
const minimumAttestations = 1;
|
|
158
|
+
const verifiedCount = 0;
|
|
159
|
+
|
|
160
|
+
const errorMessage = `Trust policy violation: Tool requires ${minimumAttestations} attestation(s) from trusted auditors, but only ${verifiedCount} found.\n\nConfigured trust policy: ${trustPolicy}\nMCP server cannot prompt interactively. To run unverified tools via MCP, update your ~/.enact/config.yaml trust policy to 'allow'.`;
|
|
161
|
+
|
|
162
|
+
expect(errorMessage).toContain("Trust policy violation");
|
|
163
|
+
expect(errorMessage).toContain("prompt");
|
|
164
|
+
expect(errorMessage).toContain("cannot prompt interactively");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("Edge Cases", () => {
|
|
169
|
+
test("should handle minimum_attestations of 0 (always allow)", () => {
|
|
170
|
+
mockGetMinimumAttestations.mockImplementation(() => 0);
|
|
171
|
+
|
|
172
|
+
const trustPolicy = mockGetTrustPolicy();
|
|
173
|
+
const minimumAttestations = mockGetMinimumAttestations();
|
|
174
|
+
const verifiedCount = 0;
|
|
175
|
+
|
|
176
|
+
// 0 < 0 is false, so should not block
|
|
177
|
+
const shouldBlock =
|
|
178
|
+
verifiedCount < minimumAttestations && trustPolicy === "require_attestation";
|
|
179
|
+
|
|
180
|
+
expect(shouldBlock).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("should handle empty attestation list", async () => {
|
|
184
|
+
mockVerifyAllAttestations.mockImplementation(async () => []);
|
|
185
|
+
|
|
186
|
+
const attestations = await mockVerifyAllAttestations();
|
|
187
|
+
const verifiedCount = attestations.filter((v: { providerIdentity: string }) =>
|
|
188
|
+
mockIsIdentityTrusted(v.providerIdentity)
|
|
189
|
+
).length;
|
|
190
|
+
|
|
191
|
+
expect(verifiedCount).toBe(0);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("should handle attestation verification errors gracefully", async () => {
|
|
195
|
+
mockVerifyAllAttestations.mockImplementation(async () => {
|
|
196
|
+
throw new Error("Network error");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
let verifiedCount = 0;
|
|
200
|
+
try {
|
|
201
|
+
const attestations = await mockVerifyAllAttestations();
|
|
202
|
+
verifiedCount = attestations.length;
|
|
203
|
+
} catch {
|
|
204
|
+
// Error is caught, verifiedCount stays 0
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
expect(verifiedCount).toBe(0);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("Trust Policy Integration", () => {
|
|
213
|
+
test("complete flow: unverified tool with require_attestation policy should be blocked", () => {
|
|
214
|
+
// Setup: require_attestation policy, minimum 1 attestation, no trusted attestations
|
|
215
|
+
mockGetTrustPolicy.mockImplementation(() => "require_attestation");
|
|
216
|
+
mockGetMinimumAttestations.mockImplementation(() => 1);
|
|
217
|
+
mockIsIdentityTrusted.mockImplementation(() => false);
|
|
218
|
+
|
|
219
|
+
const attestations = [{ providerIdentity: "github:unknown" }];
|
|
220
|
+
|
|
221
|
+
const trustPolicy = mockGetTrustPolicy();
|
|
222
|
+
const minimumAttestations = mockGetMinimumAttestations();
|
|
223
|
+
const verifiedCount = attestations.filter((v) =>
|
|
224
|
+
mockIsIdentityTrusted(v.providerIdentity)
|
|
225
|
+
).length;
|
|
226
|
+
|
|
227
|
+
expect(verifiedCount).toBe(0);
|
|
228
|
+
expect(verifiedCount < minimumAttestations).toBe(true);
|
|
229
|
+
expect(trustPolicy).toBe("require_attestation");
|
|
230
|
+
|
|
231
|
+
// Should block
|
|
232
|
+
const shouldBlock =
|
|
233
|
+
verifiedCount < minimumAttestations &&
|
|
234
|
+
(trustPolicy === "require_attestation" || trustPolicy === "prompt");
|
|
235
|
+
expect(shouldBlock).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("complete flow: verified tool with trusted attestation should be allowed", () => {
|
|
239
|
+
// Setup: require_attestation policy, minimum 1 attestation, one trusted attestation
|
|
240
|
+
mockGetTrustPolicy.mockImplementation(() => "require_attestation");
|
|
241
|
+
mockGetMinimumAttestations.mockImplementation(() => 1);
|
|
242
|
+
mockIsIdentityTrusted.mockImplementation(
|
|
243
|
+
(identity: string) => identity === "github:EnactProtocol"
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const attestations = [{ providerIdentity: "github:EnactProtocol" }];
|
|
247
|
+
|
|
248
|
+
const trustPolicy = mockGetTrustPolicy();
|
|
249
|
+
const minimumAttestations = mockGetMinimumAttestations();
|
|
250
|
+
const verifiedCount = attestations.filter((v) =>
|
|
251
|
+
mockIsIdentityTrusted(v.providerIdentity)
|
|
252
|
+
).length;
|
|
253
|
+
|
|
254
|
+
expect(verifiedCount).toBe(1);
|
|
255
|
+
expect(verifiedCount >= minimumAttestations).toBe(true);
|
|
256
|
+
|
|
257
|
+
// Should not block
|
|
258
|
+
const shouldBlock =
|
|
259
|
+
verifiedCount < minimumAttestations &&
|
|
260
|
+
(trustPolicy === "require_attestation" || trustPolicy === "prompt");
|
|
261
|
+
expect(shouldBlock).toBe(false);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("complete flow: allow policy bypasses attestation check", () => {
|
|
265
|
+
// Setup: allow policy, no attestations
|
|
266
|
+
mockGetTrustPolicy.mockImplementation((): TrustPolicy => "allow");
|
|
267
|
+
mockGetMinimumAttestations.mockImplementation(() => 1);
|
|
268
|
+
mockIsIdentityTrusted.mockImplementation(() => false);
|
|
269
|
+
|
|
270
|
+
const trustPolicy = mockGetTrustPolicy();
|
|
271
|
+
const minimumAttestations = mockGetMinimumAttestations();
|
|
272
|
+
const verifiedCount = 0;
|
|
273
|
+
|
|
274
|
+
// Should not block because policy is 'allow'
|
|
275
|
+
const shouldBlock =
|
|
276
|
+
verifiedCount < minimumAttestations &&
|
|
277
|
+
(trustPolicy === "require_attestation" || trustPolicy === "prompt");
|
|
278
|
+
expect(shouldBlock).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
});
|