@kya-os/mcp-i-core 1.2.3-canary.7 → 1.3.0-canary.clientinfo.20251126003544

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 (239) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test$colon$coverage.log +4239 -0
  3. package/.turbo/turbo-test.log +2973 -0
  4. package/COMPLIANCE_IMPROVEMENT_REPORT.md +483 -0
  5. package/Composer 3.md +615 -0
  6. package/GPT-5.md +1169 -0
  7. package/OPUS-plan.md +352 -0
  8. package/PHASE_3_AND_4.1_SUMMARY.md +585 -0
  9. package/PHASE_3_SUMMARY.md +317 -0
  10. package/PHASE_4.1.3_SUMMARY.md +428 -0
  11. package/PHASE_4.1_COMPLETE.md +525 -0
  12. package/PHASE_4_USER_DID_IDENTITY_LINKING_PLAN.md +1240 -0
  13. package/SCHEMA_COMPLIANCE_REPORT.md +275 -0
  14. package/TEST_PLAN.md +571 -0
  15. package/coverage/coverage-final.json +57 -0
  16. package/dist/__tests__/utils/mock-providers.d.ts +1 -2
  17. package/dist/__tests__/utils/mock-providers.d.ts.map +1 -1
  18. package/dist/__tests__/utils/mock-providers.js.map +1 -1
  19. package/dist/cache/oauth-config-cache.d.ts +69 -0
  20. package/dist/cache/oauth-config-cache.d.ts.map +1 -0
  21. package/dist/cache/oauth-config-cache.js +76 -0
  22. package/dist/cache/oauth-config-cache.js.map +1 -0
  23. package/dist/identity/idp-token-resolver.d.ts +53 -0
  24. package/dist/identity/idp-token-resolver.d.ts.map +1 -0
  25. package/dist/identity/idp-token-resolver.js +108 -0
  26. package/dist/identity/idp-token-resolver.js.map +1 -0
  27. package/dist/identity/idp-token-storage.interface.d.ts +42 -0
  28. package/dist/identity/idp-token-storage.interface.d.ts.map +1 -0
  29. package/dist/identity/idp-token-storage.interface.js +12 -0
  30. package/dist/identity/idp-token-storage.interface.js.map +1 -0
  31. package/dist/identity/user-did-manager.d.ts +39 -1
  32. package/dist/identity/user-did-manager.d.ts.map +1 -1
  33. package/dist/identity/user-did-manager.js +69 -3
  34. package/dist/identity/user-did-manager.js.map +1 -1
  35. package/dist/index.d.ts +24 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +43 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/runtime/audit-logger.d.ts +37 -0
  40. package/dist/runtime/audit-logger.d.ts.map +1 -0
  41. package/dist/runtime/audit-logger.js +9 -0
  42. package/dist/runtime/audit-logger.js.map +1 -0
  43. package/dist/runtime/base.d.ts +58 -2
  44. package/dist/runtime/base.d.ts.map +1 -1
  45. package/dist/runtime/base.js +266 -11
  46. package/dist/runtime/base.js.map +1 -1
  47. package/dist/services/access-control.service.d.ts.map +1 -1
  48. package/dist/services/access-control.service.js +200 -35
  49. package/dist/services/access-control.service.js.map +1 -1
  50. package/dist/services/authorization/authorization-registry.d.ts +29 -0
  51. package/dist/services/authorization/authorization-registry.d.ts.map +1 -0
  52. package/dist/services/authorization/authorization-registry.js +57 -0
  53. package/dist/services/authorization/authorization-registry.js.map +1 -0
  54. package/dist/services/authorization/types.d.ts +53 -0
  55. package/dist/services/authorization/types.d.ts.map +1 -0
  56. package/dist/services/authorization/types.js +10 -0
  57. package/dist/services/authorization/types.js.map +1 -0
  58. package/dist/services/batch-delegation.service.d.ts +53 -0
  59. package/dist/services/batch-delegation.service.d.ts.map +1 -0
  60. package/dist/services/batch-delegation.service.js +95 -0
  61. package/dist/services/batch-delegation.service.js.map +1 -0
  62. package/dist/services/index.d.ts +2 -0
  63. package/dist/services/index.d.ts.map +1 -1
  64. package/dist/services/index.js +4 -1
  65. package/dist/services/index.js.map +1 -1
  66. package/dist/services/oauth-config.service.d.ts +53 -0
  67. package/dist/services/oauth-config.service.d.ts.map +1 -0
  68. package/dist/services/oauth-config.service.js +117 -0
  69. package/dist/services/oauth-config.service.js.map +1 -0
  70. package/dist/services/oauth-provider-registry.d.ts +77 -0
  71. package/dist/services/oauth-provider-registry.d.ts.map +1 -0
  72. package/dist/services/oauth-provider-registry.js +112 -0
  73. package/dist/services/oauth-provider-registry.js.map +1 -0
  74. package/dist/services/oauth-service.d.ts +77 -0
  75. package/dist/services/oauth-service.d.ts.map +1 -0
  76. package/dist/services/oauth-service.js +348 -0
  77. package/dist/services/oauth-service.js.map +1 -0
  78. package/dist/services/oauth-token-retrieval.service.d.ts +49 -0
  79. package/dist/services/oauth-token-retrieval.service.d.ts.map +1 -0
  80. package/dist/services/oauth-token-retrieval.service.js +150 -0
  81. package/dist/services/oauth-token-retrieval.service.js.map +1 -0
  82. package/dist/services/provider-resolver.d.ts +48 -0
  83. package/dist/services/provider-resolver.d.ts.map +1 -0
  84. package/dist/services/provider-resolver.js +120 -0
  85. package/dist/services/provider-resolver.js.map +1 -0
  86. package/dist/services/provider-validator.d.ts +55 -0
  87. package/dist/services/provider-validator.d.ts.map +1 -0
  88. package/dist/services/provider-validator.js +135 -0
  89. package/dist/services/provider-validator.js.map +1 -0
  90. package/dist/services/session-registration.service.d.ts +80 -0
  91. package/dist/services/session-registration.service.d.ts.map +1 -0
  92. package/dist/services/session-registration.service.js +172 -0
  93. package/dist/services/session-registration.service.js.map +1 -0
  94. package/dist/services/tool-context-builder.d.ts +57 -0
  95. package/dist/services/tool-context-builder.d.ts.map +1 -0
  96. package/dist/services/tool-context-builder.js +125 -0
  97. package/dist/services/tool-context-builder.js.map +1 -0
  98. package/dist/services/tool-protection.service.d.ts +87 -10
  99. package/dist/services/tool-protection.service.d.ts.map +1 -1
  100. package/dist/services/tool-protection.service.js +282 -112
  101. package/dist/services/tool-protection.service.js.map +1 -1
  102. package/dist/types/oauth-required-error.d.ts +40 -0
  103. package/dist/types/oauth-required-error.d.ts.map +1 -0
  104. package/dist/types/oauth-required-error.js +40 -0
  105. package/dist/types/oauth-required-error.js.map +1 -0
  106. package/dist/utils/did-helpers.d.ts +33 -0
  107. package/dist/utils/did-helpers.d.ts.map +1 -1
  108. package/dist/utils/did-helpers.js +40 -0
  109. package/dist/utils/did-helpers.js.map +1 -1
  110. package/dist/utils/index.d.ts +1 -0
  111. package/dist/utils/index.d.ts.map +1 -1
  112. package/dist/utils/index.js +1 -0
  113. package/dist/utils/index.js.map +1 -1
  114. package/docs/API_REFERENCE.md +1362 -0
  115. package/docs/COMPLIANCE_MATRIX.md +691 -0
  116. package/docs/STATUSLIST2021_GUIDE.md +696 -0
  117. package/docs/W3C_VC_DELEGATION_GUIDE.md +710 -0
  118. package/package.json +24 -50
  119. package/scripts/audit-compliance.ts +724 -0
  120. package/src/__tests__/cache/tool-protection-cache.test.ts +640 -0
  121. package/src/__tests__/config/provider-runtime-config.test.ts +309 -0
  122. package/src/__tests__/delegation-e2e.test.ts +690 -0
  123. package/src/__tests__/identity/user-did-manager.test.ts +213 -0
  124. package/src/__tests__/index.test.ts +56 -0
  125. package/src/__tests__/integration/full-flow.test.ts +776 -0
  126. package/src/__tests__/integration.test.ts +281 -0
  127. package/src/__tests__/providers/base.test.ts +173 -0
  128. package/src/__tests__/providers/memory.test.ts +319 -0
  129. package/src/__tests__/regression/phase2-regression.test.ts +427 -0
  130. package/src/__tests__/runtime/audit-logger.test.ts +154 -0
  131. package/src/__tests__/runtime/base-extensions.test.ts +593 -0
  132. package/src/__tests__/runtime/base.test.ts +869 -0
  133. package/src/__tests__/runtime/delegation-flow.test.ts +164 -0
  134. package/src/__tests__/runtime/proof-client-did.test.ts +375 -0
  135. package/src/__tests__/runtime/route-interception.test.ts +686 -0
  136. package/src/__tests__/runtime/tool-protection-enforcement.test.ts +908 -0
  137. package/src/__tests__/services/agentshield-integration.test.ts +784 -0
  138. package/src/__tests__/services/provider-resolver-edge-cases.test.ts +487 -0
  139. package/src/__tests__/services/tool-protection-oauth-provider.test.ts +480 -0
  140. package/src/__tests__/services/tool-protection.service.test.ts +1366 -0
  141. package/src/__tests__/utils/mock-providers.ts +340 -0
  142. package/src/cache/oauth-config-cache.d.ts +69 -0
  143. package/src/cache/oauth-config-cache.d.ts.map +1 -0
  144. package/src/cache/oauth-config-cache.js +71 -0
  145. package/src/cache/oauth-config-cache.js.map +1 -0
  146. package/src/cache/oauth-config-cache.ts +123 -0
  147. package/src/cache/tool-protection-cache.ts +171 -0
  148. package/src/compliance/EXAMPLE.md +412 -0
  149. package/src/compliance/__tests__/schema-verifier.test.ts +797 -0
  150. package/src/compliance/index.ts +8 -0
  151. package/src/compliance/schema-registry.ts +460 -0
  152. package/src/compliance/schema-verifier.ts +708 -0
  153. package/src/config/__tests__/remote-config.spec.ts +268 -0
  154. package/src/config/remote-config.ts +174 -0
  155. package/src/config.ts +309 -0
  156. package/src/delegation/__tests__/audience-validator.test.ts +112 -0
  157. package/src/delegation/__tests__/bitstring.test.ts +346 -0
  158. package/src/delegation/__tests__/cascading-revocation.test.ts +628 -0
  159. package/src/delegation/__tests__/delegation-graph.test.ts +584 -0
  160. package/src/delegation/__tests__/utils.test.ts +152 -0
  161. package/src/delegation/__tests__/vc-issuer.test.ts +442 -0
  162. package/src/delegation/__tests__/vc-verifier.test.ts +922 -0
  163. package/src/delegation/audience-validator.ts +52 -0
  164. package/src/delegation/bitstring.ts +278 -0
  165. package/src/delegation/cascading-revocation.ts +370 -0
  166. package/src/delegation/delegation-graph.ts +299 -0
  167. package/src/delegation/index.ts +14 -0
  168. package/src/delegation/statuslist-manager.ts +353 -0
  169. package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +366 -0
  170. package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +228 -0
  171. package/src/delegation/storage/index.ts +9 -0
  172. package/src/delegation/storage/memory-graph-storage.ts +178 -0
  173. package/src/delegation/storage/memory-statuslist-storage.ts +77 -0
  174. package/src/delegation/utils.ts +42 -0
  175. package/src/delegation/vc-issuer.ts +232 -0
  176. package/src/delegation/vc-verifier.ts +568 -0
  177. package/src/identity/idp-token-resolver.ts +147 -0
  178. package/src/identity/idp-token-storage.interface.ts +59 -0
  179. package/src/identity/user-did-manager.ts +370 -0
  180. package/src/index.ts +271 -0
  181. package/src/providers/base.d.ts +91 -0
  182. package/src/providers/base.d.ts.map +1 -0
  183. package/src/providers/base.js +38 -0
  184. package/src/providers/base.js.map +1 -0
  185. package/src/providers/base.ts +96 -0
  186. package/src/providers/memory.ts +142 -0
  187. package/src/runtime/audit-logger.ts +39 -0
  188. package/src/runtime/base.ts +1329 -0
  189. package/src/services/__tests__/access-control.integration.test.ts +443 -0
  190. package/src/services/__tests__/access-control.proof-response-validation.test.ts +578 -0
  191. package/src/services/__tests__/access-control.service.test.ts +970 -0
  192. package/src/services/__tests__/batch-delegation.service.test.ts +351 -0
  193. package/src/services/__tests__/crypto.service.test.ts +531 -0
  194. package/src/services/__tests__/oauth-provider-registry.test.ts +142 -0
  195. package/src/services/__tests__/proof-verifier.integration.test.ts +485 -0
  196. package/src/services/__tests__/proof-verifier.test.ts +489 -0
  197. package/src/services/__tests__/provider-resolution.integration.test.ts +198 -0
  198. package/src/services/__tests__/provider-resolver.test.ts +217 -0
  199. package/src/services/__tests__/storage.service.test.ts +358 -0
  200. package/src/services/access-control.service.ts +990 -0
  201. package/src/services/authorization/authorization-registry.ts +66 -0
  202. package/src/services/authorization/types.ts +71 -0
  203. package/src/services/batch-delegation.service.ts +137 -0
  204. package/src/services/crypto.service.ts +302 -0
  205. package/src/services/errors.ts +76 -0
  206. package/src/services/index.ts +18 -0
  207. package/src/services/oauth-config.service.d.ts +53 -0
  208. package/src/services/oauth-config.service.d.ts.map +1 -0
  209. package/src/services/oauth-config.service.js +113 -0
  210. package/src/services/oauth-config.service.js.map +1 -0
  211. package/src/services/oauth-config.service.ts +166 -0
  212. package/src/services/oauth-provider-registry.d.ts +57 -0
  213. package/src/services/oauth-provider-registry.d.ts.map +1 -0
  214. package/src/services/oauth-provider-registry.js +73 -0
  215. package/src/services/oauth-provider-registry.js.map +1 -0
  216. package/src/services/oauth-provider-registry.ts +123 -0
  217. package/src/services/oauth-service.ts +510 -0
  218. package/src/services/oauth-token-retrieval.service.ts +245 -0
  219. package/src/services/proof-verifier.ts +478 -0
  220. package/src/services/provider-resolver.d.ts +48 -0
  221. package/src/services/provider-resolver.d.ts.map +1 -0
  222. package/src/services/provider-resolver.js +106 -0
  223. package/src/services/provider-resolver.js.map +1 -0
  224. package/src/services/provider-resolver.ts +144 -0
  225. package/src/services/provider-validator.ts +170 -0
  226. package/src/services/session-registration.service.ts +251 -0
  227. package/src/services/storage.service.ts +566 -0
  228. package/src/services/tool-context-builder.ts +172 -0
  229. package/src/services/tool-protection.service.ts +958 -0
  230. package/src/types/oauth-required-error.ts +63 -0
  231. package/src/types/tool-protection.ts +155 -0
  232. package/src/utils/__tests__/did-helpers.test.ts +101 -0
  233. package/src/utils/base64.ts +148 -0
  234. package/src/utils/cors.ts +83 -0
  235. package/src/utils/did-helpers.ts +150 -0
  236. package/src/utils/index.ts +8 -0
  237. package/src/utils/storage-keys.ts +278 -0
  238. package/tsconfig.json +21 -0
  239. package/vitest.config.ts +56 -0
@@ -0,0 +1,908 @@
1
+ /**
2
+ * Tool Protection Enforcement Tests
3
+ *
4
+ * Tests for tool protection enforcement in processToolCall.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, vi } from "vitest";
8
+ import { MCPIRuntimeBase } from "../../runtime/base";
9
+ import { ProviderRuntimeConfig } from "../../config";
10
+ import {
11
+ createMockProviders,
12
+ MockClockProvider,
13
+ } from "../utils/mock-providers";
14
+ import { ToolProtectionService } from "../../services/tool-protection.service";
15
+ import { DelegationRequiredError } from "../../types/tool-protection";
16
+ import { AccessControlApiService } from "../../services/access-control.service";
17
+ import type { VerifyDelegationAPIResponse } from "@kya-os/contracts/agentshield-api";
18
+
19
+ describe("MCPIRuntimeBase - Tool Protection Enforcement", () => {
20
+ let runtime: MCPIRuntimeBase;
21
+ let config: ProviderRuntimeConfig;
22
+ let mockProviders: ReturnType<typeof createMockProviders>;
23
+ let mockToolProtectionService: {
24
+ checkToolProtection: ReturnType<typeof vi.fn>;
25
+ };
26
+ let mockAccessControlService: {
27
+ verifyDelegation: ReturnType<typeof vi.fn>;
28
+ };
29
+
30
+ beforeEach(async () => {
31
+ vi.clearAllMocks();
32
+ mockProviders = createMockProviders();
33
+
34
+ // Create mock tool protection service
35
+ mockToolProtectionService = {
36
+ checkToolProtection: vi.fn(),
37
+ };
38
+
39
+ // Create mock access control service
40
+ mockAccessControlService = {
41
+ verifyDelegation: vi.fn(),
42
+ };
43
+
44
+ config = {
45
+ ...mockProviders,
46
+ environment: "development",
47
+ session: {
48
+ timestampSkewSeconds: 120,
49
+ ttlMinutes: 30,
50
+ },
51
+ audit: {
52
+ enabled: true,
53
+ logFunction: vi.fn(),
54
+ includePayloads: true,
55
+ },
56
+ toolProtectionService: mockToolProtectionService as any,
57
+ };
58
+ runtime = new MCPIRuntimeBase(config);
59
+ // Inject mock access control service
60
+ (runtime as any).accessControlService = mockAccessControlService as any;
61
+ await runtime.initialize();
62
+ });
63
+
64
+ describe("processToolCall with tool protection", () => {
65
+ const mockHandler = vi.fn().mockResolvedValue({ result: "success" });
66
+ const session = {
67
+ id: "session123",
68
+ audience: "https://client.example.com",
69
+ nonce: "test-nonce",
70
+ };
71
+
72
+ it("should allow tool execution when no protection required", async () => {
73
+ mockToolProtectionService.checkToolProtection.mockResolvedValue(null);
74
+
75
+ const result = await runtime.processToolCall(
76
+ "unprotectedTool",
77
+ { arg: "value" },
78
+ mockHandler,
79
+ session
80
+ );
81
+
82
+ expect(mockHandler).toHaveBeenCalledWith({ arg: "value" });
83
+ expect(result).toEqual({ result: "success" });
84
+ expect(
85
+ mockToolProtectionService.checkToolProtection
86
+ ).toHaveBeenCalledWith("unprotectedTool", "did:key:zmock123");
87
+ });
88
+
89
+ it("should block tool execution when protection required and no delegation", async () => {
90
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
91
+ requiresDelegation: true,
92
+ requiredScopes: ["files:write"],
93
+ });
94
+
95
+ await expect(
96
+ runtime.processToolCall(
97
+ "protectedTool",
98
+ { arg: "value" },
99
+ mockHandler,
100
+ session
101
+ )
102
+ ).rejects.toThrow(DelegationRequiredError);
103
+
104
+ expect(mockHandler).not.toHaveBeenCalled();
105
+ });
106
+
107
+ it("should allow tool execution when protection required and delegation provided", async () => {
108
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
109
+ requiresDelegation: true,
110
+ requiredScopes: ["files:write"],
111
+ });
112
+
113
+ // Mock successful delegation verification
114
+ mockAccessControlService.verifyDelegation.mockResolvedValue({
115
+ success: true,
116
+ data: {
117
+ valid: true,
118
+ delegation_id: "test-delegation-id",
119
+ credential: {
120
+ agent_did: "did:key:zmock123",
121
+ scopes: ["files:write"],
122
+ issued_at: Date.now(),
123
+ created_at: Date.now(),
124
+ },
125
+ },
126
+ } as VerifyDelegationAPIResponse);
127
+
128
+ const sessionWithDelegation = {
129
+ ...session,
130
+ delegationToken: "valid-delegation-token",
131
+ };
132
+
133
+ const result = await runtime.processToolCall(
134
+ "protectedTool",
135
+ { arg: "value" },
136
+ mockHandler,
137
+ sessionWithDelegation
138
+ );
139
+
140
+ // Verify delegation was checked
141
+ expect(mockAccessControlService.verifyDelegation).toHaveBeenCalledWith(
142
+ expect.objectContaining({
143
+ agent_did: "did:key:zmock123",
144
+ delegation_token: "valid-delegation-token",
145
+ scopes: ["files:write"],
146
+ }),
147
+ expect.objectContaining({
148
+ delegationToken: "valid-delegation-token",
149
+ })
150
+ );
151
+
152
+ expect(mockHandler).toHaveBeenCalledWith({ arg: "value" });
153
+ expect(result).toEqual({ result: "success" });
154
+ });
155
+
156
+ it("should allow tool execution when protection required and consentProof provided", async () => {
157
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
158
+ requiresDelegation: true,
159
+ requiredScopes: ["files:write"],
160
+ });
161
+
162
+ // Mock successful delegation verification using consent proof as credential_jwt
163
+ mockAccessControlService.verifyDelegation.mockResolvedValue({
164
+ success: true,
165
+ data: {
166
+ valid: true,
167
+ delegation_id: "test-delegation-id",
168
+ credential: {
169
+ agent_did: "did:key:zmock123",
170
+ scopes: ["files:write"],
171
+ issued_at: Date.now(),
172
+ created_at: Date.now(),
173
+ },
174
+ },
175
+ } as VerifyDelegationAPIResponse);
176
+
177
+ const sessionWithConsent = {
178
+ ...session,
179
+ consentProof: "valid-consent-proof",
180
+ };
181
+
182
+ const result = await runtime.processToolCall(
183
+ "protectedTool",
184
+ { arg: "value" },
185
+ mockHandler,
186
+ sessionWithConsent
187
+ );
188
+
189
+ // Verify delegation was checked using consent proof
190
+ expect(mockAccessControlService.verifyDelegation).toHaveBeenCalledWith(
191
+ expect.objectContaining({
192
+ agent_did: "did:key:zmock123",
193
+ credential_jwt: "valid-consent-proof",
194
+ scopes: ["files:write"],
195
+ }),
196
+ expect.objectContaining({
197
+ credentialJwt: "valid-consent-proof",
198
+ })
199
+ );
200
+
201
+ expect(mockHandler).toHaveBeenCalledWith({ arg: "value" });
202
+ expect(result).toEqual({ result: "success" });
203
+ });
204
+
205
+ it("should block tool execution when delegation verification fails", async () => {
206
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
207
+ requiresDelegation: true,
208
+ requiredScopes: ["files:write"],
209
+ });
210
+
211
+ // Mock failed delegation verification
212
+ mockAccessControlService.verifyDelegation.mockResolvedValue({
213
+ success: true,
214
+ data: {
215
+ valid: false,
216
+ reason: "Delegation token expired",
217
+ },
218
+ } as VerifyDelegationAPIResponse);
219
+
220
+ const sessionWithDelegation = {
221
+ ...session,
222
+ delegationToken: "expired-delegation-token",
223
+ };
224
+
225
+ await expect(
226
+ runtime.processToolCall(
227
+ "protectedTool",
228
+ { arg: "value" },
229
+ mockHandler,
230
+ sessionWithDelegation
231
+ )
232
+ ).rejects.toThrow(DelegationRequiredError);
233
+
234
+ expect(mockAccessControlService.verifyDelegation).toHaveBeenCalled();
235
+ expect(mockHandler).not.toHaveBeenCalled();
236
+ });
237
+
238
+ it("should block tool execution when delegation has wrong scopes", async () => {
239
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
240
+ requiresDelegation: true,
241
+ requiredScopes: ["files:write"],
242
+ });
243
+
244
+ // Mock delegation with insufficient scopes
245
+ mockAccessControlService.verifyDelegation.mockResolvedValue({
246
+ success: true,
247
+ data: {
248
+ valid: false,
249
+ reason: "Insufficient scopes",
250
+ credential: {
251
+ agent_did: "did:key:zmock123",
252
+ scopes: ["files:read"], // Only read, not write
253
+ issued_at: Date.now(),
254
+ created_at: Date.now(),
255
+ },
256
+ },
257
+ } as VerifyDelegationAPIResponse);
258
+
259
+ const sessionWithDelegation = {
260
+ ...session,
261
+ delegationToken: "insufficient-scope-token",
262
+ };
263
+
264
+ await expect(
265
+ runtime.processToolCall(
266
+ "protectedTool",
267
+ { arg: "value" },
268
+ mockHandler,
269
+ sessionWithDelegation
270
+ )
271
+ ).rejects.toThrow(DelegationRequiredError);
272
+
273
+ expect(mockAccessControlService.verifyDelegation).toHaveBeenCalledWith(
274
+ expect.objectContaining({
275
+ scopes: ["files:write"],
276
+ }),
277
+ expect.any(Object)
278
+ );
279
+ expect(mockHandler).not.toHaveBeenCalled();
280
+ });
281
+
282
+ it("should handle API errors during verification gracefully", async () => {
283
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
284
+ requiresDelegation: true,
285
+ requiredScopes: ["files:write"],
286
+ });
287
+
288
+ // Mock API error
289
+ const { AgentShieldAPIError } = await import(
290
+ "@kya-os/contracts/agentshield-api"
291
+ );
292
+ mockAccessControlService.verifyDelegation.mockRejectedValue(
293
+ new AgentShieldAPIError("network_error", "API unavailable", {})
294
+ );
295
+
296
+ const sessionWithDelegation = {
297
+ ...session,
298
+ delegationToken: "valid-token",
299
+ };
300
+
301
+ // Should fail securely by requiring delegation
302
+ await expect(
303
+ runtime.processToolCall(
304
+ "protectedTool",
305
+ { arg: "value" },
306
+ mockHandler,
307
+ sessionWithDelegation
308
+ )
309
+ ).rejects.toThrow(DelegationRequiredError);
310
+
311
+ expect(mockHandler).not.toHaveBeenCalled();
312
+ });
313
+
314
+ it("should allow tool execution when access control service not configured (graceful degradation)", async () => {
315
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
316
+ requiresDelegation: true,
317
+ requiredScopes: ["files:write"],
318
+ });
319
+
320
+ // Remove access control service
321
+ (runtime as any).accessControlService = undefined;
322
+
323
+ const sessionWithDelegation = {
324
+ ...session,
325
+ delegationToken: "valid-delegation-token",
326
+ };
327
+
328
+ // Should allow execution with warning (graceful degradation)
329
+ const result = await runtime.processToolCall(
330
+ "protectedTool",
331
+ { arg: "value" },
332
+ mockHandler,
333
+ sessionWithDelegation
334
+ );
335
+
336
+ expect(mockHandler).toHaveBeenCalledWith({ arg: "value" });
337
+ expect(result).toEqual({ result: "success" });
338
+ });
339
+
340
+ describe("user_identifier validation", () => {
341
+ const userDidA = "did:key:zUserA123456789";
342
+ const userDidB = "did:key:zUserB987654321";
343
+
344
+ it("should reject delegation when user_identifier does not match session userDid", async () => {
345
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
346
+ requiresDelegation: true,
347
+ requiredScopes: ["files:write"],
348
+ });
349
+
350
+ // Mock verification response with User B's user_identifier
351
+ mockAccessControlService.verifyDelegation.mockResolvedValue({
352
+ success: true,
353
+ data: {
354
+ valid: true,
355
+ delegation_id: "test-delegation-id",
356
+ credential: {
357
+ agent_did: "did:key:zmock123",
358
+ user_identifier: userDidB, // ❌ User B's identifier, not User A's
359
+ scopes: ["files:write"],
360
+ issued_at: Date.now(),
361
+ created_at: Date.now(),
362
+ },
363
+ },
364
+ } as VerifyDelegationAPIResponse);
365
+
366
+ // Session with User A's DID
367
+ const sessionWithUserA = {
368
+ ...session,
369
+ userDid: userDidA, // User A's DID
370
+ delegationToken: "delegation-token-for-user-b",
371
+ };
372
+
373
+ // Should reject delegation due to user_identifier mismatch
374
+ await expect(
375
+ runtime.processToolCall(
376
+ "protectedTool",
377
+ { arg: "value" },
378
+ mockHandler,
379
+ sessionWithUserA
380
+ )
381
+ ).rejects.toThrow(DelegationRequiredError);
382
+
383
+ expect(mockAccessControlService.verifyDelegation).toHaveBeenCalled();
384
+ expect(mockHandler).not.toHaveBeenCalled();
385
+ });
386
+
387
+ it("should accept delegation when user_identifier matches session userDid", async () => {
388
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
389
+ requiresDelegation: true,
390
+ requiredScopes: ["files:write"],
391
+ });
392
+
393
+ // Mock verification response with User A's user_identifier
394
+ mockAccessControlService.verifyDelegation.mockResolvedValue({
395
+ success: true,
396
+ data: {
397
+ valid: true,
398
+ delegation_id: "test-delegation-id",
399
+ credential: {
400
+ agent_did: "did:key:zmock123",
401
+ user_identifier: userDidA, // ✅ User A's identifier matches session
402
+ scopes: ["files:write"],
403
+ issued_at: Date.now(),
404
+ created_at: Date.now(),
405
+ },
406
+ },
407
+ } as VerifyDelegationAPIResponse);
408
+
409
+ // Session with User A's DID
410
+ const sessionWithUserA = {
411
+ ...session,
412
+ userDid: userDidA, // User A's DID
413
+ delegationToken: "delegation-token-for-user-a",
414
+ };
415
+
416
+ // Should accept delegation when user_identifier matches
417
+ const result = await runtime.processToolCall(
418
+ "protectedTool",
419
+ { arg: "value" },
420
+ mockHandler,
421
+ sessionWithUserA
422
+ );
423
+
424
+ expect(mockAccessControlService.verifyDelegation).toHaveBeenCalled();
425
+ expect(mockHandler).toHaveBeenCalledWith({ arg: "value" });
426
+ expect(result).toEqual({ result: "success" });
427
+ });
428
+
429
+ it("should handle missing user_identifier gracefully (backward compatibility)", async () => {
430
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
431
+ requiresDelegation: true,
432
+ requiredScopes: ["files:write"],
433
+ });
434
+
435
+ // Mock verification response without user_identifier (legacy format)
436
+ mockAccessControlService.verifyDelegation.mockResolvedValue({
437
+ success: true,
438
+ data: {
439
+ valid: true,
440
+ delegation_id: "test-delegation-id",
441
+ credential: {
442
+ agent_did: "did:key:zmock123",
443
+ // No user_identifier field
444
+ scopes: ["files:write"],
445
+ issued_at: Date.now(),
446
+ created_at: Date.now(),
447
+ },
448
+ },
449
+ } as VerifyDelegationAPIResponse);
450
+
451
+ const sessionWithUserA = {
452
+ ...session,
453
+ userDid: userDidA,
454
+ delegationToken: "legacy-delegation-token",
455
+ };
456
+
457
+ // Should allow execution when user_identifier is missing (backward compatibility)
458
+ const result = await runtime.processToolCall(
459
+ "protectedTool",
460
+ { arg: "value" },
461
+ mockHandler,
462
+ sessionWithUserA
463
+ );
464
+
465
+ expect(mockHandler).toHaveBeenCalledWith({ arg: "value" });
466
+ expect(result).toEqual({ result: "success" });
467
+ });
468
+
469
+ it("should handle missing session userDid gracefully", async () => {
470
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
471
+ requiresDelegation: true,
472
+ requiredScopes: ["files:write"],
473
+ });
474
+
475
+ // Mock verification response with user_identifier
476
+ mockAccessControlService.verifyDelegation.mockResolvedValue({
477
+ success: true,
478
+ data: {
479
+ valid: true,
480
+ delegation_id: "test-delegation-id",
481
+ credential: {
482
+ agent_did: "did:key:zmock123",
483
+ user_identifier: userDidA,
484
+ scopes: ["files:write"],
485
+ issued_at: Date.now(),
486
+ created_at: Date.now(),
487
+ },
488
+ },
489
+ } as VerifyDelegationAPIResponse);
490
+
491
+ // Session without userDid (legacy format)
492
+ const sessionWithoutUserDid = {
493
+ ...session,
494
+ // No userDid field
495
+ delegationToken: "delegation-token",
496
+ };
497
+
498
+ // Should allow execution when session userDid is missing (backward compatibility)
499
+ const result = await runtime.processToolCall(
500
+ "protectedTool",
501
+ { arg: "value" },
502
+ mockHandler,
503
+ sessionWithoutUserDid
504
+ );
505
+
506
+ expect(mockHandler).toHaveBeenCalledWith({ arg: "value" });
507
+ expect(result).toEqual({ result: "success" });
508
+ });
509
+ });
510
+
511
+ it("should create proof after successful tool execution", async () => {
512
+ mockToolProtectionService.checkToolProtection.mockResolvedValue(null);
513
+
514
+ await runtime.processToolCall(
515
+ "unprotectedTool",
516
+ { arg: "value" },
517
+ mockHandler,
518
+ session
519
+ );
520
+
521
+ const proof = runtime.getLastProof();
522
+ expect(proof).toBeDefined();
523
+ expect(proof.did).toBe("did:key:zmock123");
524
+ expect(proof.sessionId).toBe("session123");
525
+ });
526
+
527
+ it("should not create proof when tool execution is blocked", async () => {
528
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
529
+ requiresDelegation: true,
530
+ requiredScopes: ["files:write"],
531
+ });
532
+
533
+ const initialProof = runtime.getLastProof();
534
+
535
+ await expect(
536
+ runtime.processToolCall(
537
+ "protectedTool",
538
+ { arg: "value" },
539
+ mockHandler,
540
+ session
541
+ )
542
+ ).rejects.toThrow(DelegationRequiredError);
543
+
544
+ // Proof should not be created when tool is blocked
545
+ const proofAfterBlock = runtime.getLastProof();
546
+ expect(proofAfterBlock).toBe(initialProof);
547
+ });
548
+ });
549
+
550
+ describe("DelegationRequiredError details", () => {
551
+ const mockHandler = vi.fn().mockResolvedValue({ result: "success" });
552
+ const session = {
553
+ id: "session123",
554
+ audience: "https://client.example.com",
555
+ nonce: "test-nonce",
556
+ };
557
+
558
+ it("should include tool name in error", async () => {
559
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
560
+ requiresDelegation: true,
561
+ requiredScopes: ["files:write"],
562
+ });
563
+
564
+ try {
565
+ await runtime.processToolCall(
566
+ "protectedTool",
567
+ { arg: "value" },
568
+ mockHandler,
569
+ session
570
+ );
571
+ expect.fail("Should have thrown DelegationRequiredError");
572
+ } catch (error: any) {
573
+ expect(error).toBeInstanceOf(DelegationRequiredError);
574
+ expect(error.toolName).toBe("protectedTool");
575
+ expect(error.message).toContain("protectedTool");
576
+ }
577
+ });
578
+
579
+ it("should include required scopes in error", async () => {
580
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
581
+ requiresDelegation: true,
582
+ requiredScopes: ["files:write", "files:read"],
583
+ });
584
+
585
+ try {
586
+ await runtime.processToolCall(
587
+ "protectedTool",
588
+ { arg: "value" },
589
+ mockHandler,
590
+ session
591
+ );
592
+ expect.fail("Should have thrown DelegationRequiredError");
593
+ } catch (error: any) {
594
+ expect(error).toBeInstanceOf(DelegationRequiredError);
595
+ expect(error.requiredScopes).toEqual(["files:write", "files:read"]);
596
+ }
597
+ });
598
+
599
+ it("should include consent URL in error", async () => {
600
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
601
+ requiresDelegation: true,
602
+ requiredScopes: ["files:write"],
603
+ });
604
+
605
+ try {
606
+ await runtime.processToolCall(
607
+ "protectedTool",
608
+ { arg: "value" },
609
+ mockHandler,
610
+ session
611
+ );
612
+ expect.fail("Should have thrown DelegationRequiredError");
613
+ } catch (error: any) {
614
+ expect(error).toBeInstanceOf(DelegationRequiredError);
615
+ expect(error.consentUrl).toBeDefined();
616
+ expect(error.consentUrl).toContain("kya.vouched.id/bouncer/consent");
617
+ expect(error.consentUrl).toContain("tool=protectedTool");
618
+ expect(error.consentUrl).toContain("scopes=files%3Awrite");
619
+ }
620
+ });
621
+
622
+ it("should include resume token in error", async () => {
623
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
624
+ requiresDelegation: true,
625
+ requiredScopes: ["files:write"],
626
+ });
627
+
628
+ try {
629
+ await runtime.processToolCall(
630
+ "protectedTool",
631
+ { arg: "value" },
632
+ mockHandler,
633
+ session
634
+ );
635
+ expect.fail("Should have thrown DelegationRequiredError");
636
+ } catch (error: any) {
637
+ expect(error).toBeInstanceOf(DelegationRequiredError);
638
+ expect(error.resumeToken).toBeDefined();
639
+ expect(error.resumeToken).toMatch(/^resume_/);
640
+ }
641
+ });
642
+
643
+ it("should include intercepted call context in error", async () => {
644
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
645
+ requiresDelegation: true,
646
+ requiredScopes: ["files:write"],
647
+ });
648
+
649
+ try {
650
+ await runtime.processToolCall(
651
+ "protectedTool",
652
+ { arg: "value", nested: { data: "test" } },
653
+ mockHandler,
654
+ session
655
+ );
656
+ expect.fail("Should have thrown DelegationRequiredError");
657
+ } catch (error: any) {
658
+ expect(error).toBeInstanceOf(DelegationRequiredError);
659
+ expect(error.interceptedCall).toBeDefined();
660
+ expect(error.interceptedCall.toolName).toBe("protectedTool");
661
+ expect(error.interceptedCall.args).toEqual({
662
+ arg: "value",
663
+ nested: { data: "test" },
664
+ });
665
+ expect(error.interceptedCall.sessionId).toBe("session123");
666
+ }
667
+ });
668
+ });
669
+
670
+ describe("audit logging", () => {
671
+ const mockHandler = vi.fn().mockResolvedValue({ result: "success" });
672
+ const session = {
673
+ id: "session123",
674
+ audience: "https://client.example.com",
675
+ nonce: "test-nonce",
676
+ };
677
+
678
+ it("should log tool protection check when audit enabled", async () => {
679
+ mockToolProtectionService.checkToolProtection.mockResolvedValue(null);
680
+
681
+ await runtime.processToolCall(
682
+ "testTool",
683
+ { arg: "value" },
684
+ mockHandler,
685
+ session
686
+ );
687
+
688
+ expect(config.audit!.logFunction).toHaveBeenCalled();
689
+ const logCalls = (config.audit!.logFunction as any).mock.calls;
690
+ const protectionLogCall = logCalls.find((call: any[]) => {
691
+ try {
692
+ const log = JSON.parse(call[0]);
693
+ return log.event === "tool_executed";
694
+ } catch {
695
+ return false;
696
+ }
697
+ });
698
+ expect(protectionLogCall).toBeDefined();
699
+ });
700
+
701
+ it("should log blocked tool call when audit enabled", async () => {
702
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
703
+ requiresDelegation: true,
704
+ requiredScopes: ["files:write"],
705
+ });
706
+
707
+ const consoleWarnSpy = vi
708
+ .spyOn(console, "warn")
709
+ .mockImplementation(() => {});
710
+
711
+ try {
712
+ await runtime.processToolCall(
713
+ "protectedTool",
714
+ { arg: "value" },
715
+ mockHandler,
716
+ session
717
+ );
718
+ } catch (error) {
719
+ // Expected
720
+ }
721
+
722
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
723
+ expect.stringContaining("[MCP-I] BLOCKED"),
724
+ expect.any(Object)
725
+ );
726
+
727
+ consoleWarnSpy.mockRestore();
728
+ });
729
+
730
+ it("should not log when audit disabled", async () => {
731
+ const auditLogFn = vi.fn();
732
+ const configNoAudit = {
733
+ ...config,
734
+ audit: {
735
+ enabled: false,
736
+ logFunction: auditLogFn,
737
+ },
738
+ };
739
+ const runtimeNoAudit = new MCPIRuntimeBase(configNoAudit);
740
+ await runtimeNoAudit.initialize();
741
+
742
+ mockToolProtectionService.checkToolProtection.mockResolvedValue(null);
743
+
744
+ await runtimeNoAudit.processToolCall(
745
+ "testTool",
746
+ { arg: "value" },
747
+ mockHandler,
748
+ session
749
+ );
750
+
751
+ // Should not call log function for tool execution (initialization may still log)
752
+ const toolExecutionLogs = auditLogFn.mock.calls.filter((call: any[]) => {
753
+ try {
754
+ const log = JSON.parse(call[0]);
755
+ return log.event === "tool_executed";
756
+ } catch {
757
+ return false;
758
+ }
759
+ });
760
+ expect(toolExecutionLogs.length).toBe(0);
761
+ });
762
+ });
763
+
764
+ describe("tool protection service integration", () => {
765
+ const mockHandler = vi.fn().mockResolvedValue({ result: "success" });
766
+ const session = {
767
+ id: "session123",
768
+ audience: "https://client.example.com",
769
+ nonce: "test-nonce",
770
+ };
771
+
772
+ it("should use agent DID from identity for protection check", async () => {
773
+ mockToolProtectionService.checkToolProtection.mockResolvedValue(null);
774
+
775
+ await runtime.processToolCall(
776
+ "testTool",
777
+ { arg: "value" },
778
+ mockHandler,
779
+ session
780
+ );
781
+
782
+ expect(
783
+ mockToolProtectionService.checkToolProtection
784
+ ).toHaveBeenCalledWith("testTool", "did:key:zmock123");
785
+ });
786
+
787
+ it("should handle tool protection service errors gracefully", async () => {
788
+ mockToolProtectionService.checkToolProtection.mockRejectedValue(
789
+ new Error("Service unavailable")
790
+ );
791
+
792
+ // If the service throws, the error propagates and tool execution is blocked
793
+ // This is the current behavior - service errors prevent tool execution
794
+ await expect(
795
+ runtime.processToolCall(
796
+ "testTool",
797
+ { arg: "value" },
798
+ mockHandler,
799
+ session
800
+ )
801
+ ).rejects.toThrow("Service unavailable");
802
+ });
803
+
804
+ it("should work without tool protection service", async () => {
805
+ const configNoService = {
806
+ ...config,
807
+ toolProtectionService: undefined,
808
+ };
809
+ const runtimeNoService = new MCPIRuntimeBase(configNoService);
810
+ await runtimeNoService.initialize();
811
+
812
+ const result = await runtimeNoService.processToolCall(
813
+ "testTool",
814
+ { arg: "value" },
815
+ mockHandler,
816
+ session
817
+ );
818
+
819
+ expect(mockHandler).toHaveBeenCalled();
820
+ expect(result).toEqual({ result: "success" });
821
+ });
822
+ });
823
+
824
+ describe("edge cases", () => {
825
+ const mockHandler = vi.fn().mockResolvedValue({ result: "success" });
826
+ const session = {
827
+ id: "session123",
828
+ audience: "https://client.example.com",
829
+ nonce: "test-nonce",
830
+ };
831
+
832
+ it("should handle empty required scopes array", async () => {
833
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
834
+ requiresDelegation: true,
835
+ requiredScopes: [],
836
+ });
837
+
838
+ await expect(
839
+ runtime.processToolCall(
840
+ "protectedTool",
841
+ { arg: "value" },
842
+ mockHandler,
843
+ session
844
+ )
845
+ ).rejects.toThrow(DelegationRequiredError);
846
+ });
847
+
848
+ it("should handle multiple required scopes", async () => {
849
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
850
+ requiresDelegation: true,
851
+ requiredScopes: ["scope1", "scope2", "scope3"],
852
+ });
853
+
854
+ try {
855
+ await runtime.processToolCall(
856
+ "protectedTool",
857
+ { arg: "value" },
858
+ mockHandler,
859
+ session
860
+ );
861
+ expect.fail("Should have thrown DelegationRequiredError");
862
+ } catch (error: any) {
863
+ expect(error.requiredScopes).toEqual(["scope1", "scope2", "scope3"]);
864
+ expect(error.consentUrl).toContain("scopes=scope1%2Cscope2%2Cscope3");
865
+ }
866
+ });
867
+
868
+ it("should handle session without id", async () => {
869
+ mockToolProtectionService.checkToolProtection.mockResolvedValue({
870
+ requiresDelegation: true,
871
+ requiredScopes: ["files:write"],
872
+ });
873
+
874
+ const sessionWithoutId = {
875
+ audience: "https://client.example.com",
876
+ nonce: "test-nonce",
877
+ };
878
+
879
+ try {
880
+ await runtime.processToolCall(
881
+ "protectedTool",
882
+ { arg: "value" },
883
+ mockHandler,
884
+ sessionWithoutId
885
+ );
886
+ expect.fail("Should have thrown DelegationRequiredError");
887
+ } catch (error: any) {
888
+ expect(error.interceptedCall.sessionId).toBe("unknown");
889
+ }
890
+ });
891
+
892
+ it("should handle handler errors independently of protection", async () => {
893
+ mockToolProtectionService.checkToolProtection.mockResolvedValue(null);
894
+ const errorHandler = vi
895
+ .fn()
896
+ .mockRejectedValue(new Error("Handler failed"));
897
+
898
+ await expect(
899
+ runtime.processToolCall(
900
+ "errorTool",
901
+ { arg: "value" },
902
+ errorHandler,
903
+ session
904
+ )
905
+ ).rejects.toThrow("Handler failed");
906
+ });
907
+ });
908
+ });