@kya-os/mcp-i-core 1.3.13 → 1.3.15

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 (255) hide show
  1. package/dist/config/remote-config.js +9 -12
  2. package/dist/runtime/base.d.ts +2 -1
  3. package/dist/runtime/base.js +34 -6
  4. package/dist/services/access-control.service.js +5 -0
  5. package/dist/services/tool-protection.service.js +17 -8
  6. package/package.json +2 -2
  7. package/.turbo/turbo-build.log +0 -4
  8. package/.turbo/turbo-test$colon$coverage.log +0 -4586
  9. package/.turbo/turbo-test.log +0 -4631
  10. package/COMPLIANCE_IMPROVEMENT_REPORT.md +0 -483
  11. package/Composer 3.md +0 -615
  12. package/GPT-5.md +0 -1169
  13. package/OPUS-plan.md +0 -352
  14. package/PHASE_3_AND_4.1_SUMMARY.md +0 -585
  15. package/PHASE_3_SUMMARY.md +0 -317
  16. package/PHASE_4.1.3_SUMMARY.md +0 -428
  17. package/PHASE_4.1_COMPLETE.md +0 -525
  18. package/PHASE_4_USER_DID_IDENTITY_LINKING_PLAN.md +0 -1240
  19. package/SCHEMA_COMPLIANCE_REPORT.md +0 -275
  20. package/TEST_PLAN.md +0 -571
  21. package/coverage/coverage-final.json +0 -60
  22. package/dist/cache/oauth-config-cache.d.ts.map +0 -1
  23. package/dist/cache/oauth-config-cache.js.map +0 -1
  24. package/dist/cache/tool-protection-cache.d.ts.map +0 -1
  25. package/dist/cache/tool-protection-cache.js.map +0 -1
  26. package/dist/compliance/index.d.ts.map +0 -1
  27. package/dist/compliance/index.js.map +0 -1
  28. package/dist/compliance/schema-registry.d.ts.map +0 -1
  29. package/dist/compliance/schema-registry.js.map +0 -1
  30. package/dist/compliance/schema-verifier.d.ts.map +0 -1
  31. package/dist/compliance/schema-verifier.js.map +0 -1
  32. package/dist/config/remote-config.d.ts.map +0 -1
  33. package/dist/config/remote-config.js.map +0 -1
  34. package/dist/config.d.ts.map +0 -1
  35. package/dist/config.js.map +0 -1
  36. package/dist/delegation/audience-validator.d.ts.map +0 -1
  37. package/dist/delegation/audience-validator.js.map +0 -1
  38. package/dist/delegation/bitstring.d.ts.map +0 -1
  39. package/dist/delegation/bitstring.js.map +0 -1
  40. package/dist/delegation/cascading-revocation.d.ts.map +0 -1
  41. package/dist/delegation/cascading-revocation.js.map +0 -1
  42. package/dist/delegation/delegation-graph.d.ts.map +0 -1
  43. package/dist/delegation/delegation-graph.js.map +0 -1
  44. package/dist/delegation/did-key-resolver.d.ts.map +0 -1
  45. package/dist/delegation/did-key-resolver.js.map +0 -1
  46. package/dist/delegation/index.d.ts.map +0 -1
  47. package/dist/delegation/index.js.map +0 -1
  48. package/dist/delegation/statuslist-manager.d.ts.map +0 -1
  49. package/dist/delegation/statuslist-manager.js.map +0 -1
  50. package/dist/delegation/storage/index.d.ts.map +0 -1
  51. package/dist/delegation/storage/index.js.map +0 -1
  52. package/dist/delegation/storage/memory-graph-storage.d.ts.map +0 -1
  53. package/dist/delegation/storage/memory-graph-storage.js.map +0 -1
  54. package/dist/delegation/storage/memory-statuslist-storage.d.ts.map +0 -1
  55. package/dist/delegation/storage/memory-statuslist-storage.js.map +0 -1
  56. package/dist/delegation/utils.d.ts.map +0 -1
  57. package/dist/delegation/utils.js.map +0 -1
  58. package/dist/delegation/vc-issuer.d.ts.map +0 -1
  59. package/dist/delegation/vc-issuer.js.map +0 -1
  60. package/dist/delegation/vc-verifier.d.ts.map +0 -1
  61. package/dist/delegation/vc-verifier.js.map +0 -1
  62. package/dist/identity/idp-token-resolver.d.ts.map +0 -1
  63. package/dist/identity/idp-token-resolver.js.map +0 -1
  64. package/dist/identity/idp-token-storage.interface.d.ts.map +0 -1
  65. package/dist/identity/idp-token-storage.interface.js.map +0 -1
  66. package/dist/identity/user-did-manager.d.ts.map +0 -1
  67. package/dist/identity/user-did-manager.js.map +0 -1
  68. package/dist/index.d.ts.map +0 -1
  69. package/dist/index.js.map +0 -1
  70. package/dist/providers/base.d.ts.map +0 -1
  71. package/dist/providers/base.js.map +0 -1
  72. package/dist/providers/memory.d.ts.map +0 -1
  73. package/dist/providers/memory.js.map +0 -1
  74. package/dist/runtime/audit-logger.d.ts.map +0 -1
  75. package/dist/runtime/audit-logger.js.map +0 -1
  76. package/dist/runtime/base.d.ts.map +0 -1
  77. package/dist/runtime/base.js.map +0 -1
  78. package/dist/services/access-control.service.d.ts.map +0 -1
  79. package/dist/services/access-control.service.js.map +0 -1
  80. package/dist/services/authorization/authorization-registry.d.ts.map +0 -1
  81. package/dist/services/authorization/authorization-registry.js.map +0 -1
  82. package/dist/services/authorization/types.d.ts.map +0 -1
  83. package/dist/services/authorization/types.js.map +0 -1
  84. package/dist/services/batch-delegation.service.d.ts.map +0 -1
  85. package/dist/services/batch-delegation.service.js.map +0 -1
  86. package/dist/services/crypto.service.d.ts.map +0 -1
  87. package/dist/services/crypto.service.js.map +0 -1
  88. package/dist/services/errors.d.ts.map +0 -1
  89. package/dist/services/errors.js.map +0 -1
  90. package/dist/services/index.d.ts.map +0 -1
  91. package/dist/services/index.js.map +0 -1
  92. package/dist/services/oauth-config.service.d.ts.map +0 -1
  93. package/dist/services/oauth-config.service.js.map +0 -1
  94. package/dist/services/oauth-provider-registry.d.ts.map +0 -1
  95. package/dist/services/oauth-provider-registry.js.map +0 -1
  96. package/dist/services/oauth-service.d.ts.map +0 -1
  97. package/dist/services/oauth-service.js.map +0 -1
  98. package/dist/services/oauth-token-retrieval.service.d.ts.map +0 -1
  99. package/dist/services/oauth-token-retrieval.service.js.map +0 -1
  100. package/dist/services/proof-verifier.d.ts.map +0 -1
  101. package/dist/services/proof-verifier.js.map +0 -1
  102. package/dist/services/provider-resolver.d.ts.map +0 -1
  103. package/dist/services/provider-resolver.js.map +0 -1
  104. package/dist/services/provider-validator.d.ts.map +0 -1
  105. package/dist/services/provider-validator.js.map +0 -1
  106. package/dist/services/session-registration.service.d.ts.map +0 -1
  107. package/dist/services/session-registration.service.js.map +0 -1
  108. package/dist/services/storage.service.d.ts.map +0 -1
  109. package/dist/services/storage.service.js.map +0 -1
  110. package/dist/services/tool-context-builder.d.ts.map +0 -1
  111. package/dist/services/tool-context-builder.js.map +0 -1
  112. package/dist/services/tool-protection.service.d.ts.map +0 -1
  113. package/dist/services/tool-protection.service.js.map +0 -1
  114. package/dist/types/oauth-required-error.d.ts.map +0 -1
  115. package/dist/types/oauth-required-error.js.map +0 -1
  116. package/dist/types/tool-protection.d.ts.map +0 -1
  117. package/dist/types/tool-protection.js.map +0 -1
  118. package/dist/utils/base58.d.ts.map +0 -1
  119. package/dist/utils/base58.js.map +0 -1
  120. package/dist/utils/base64.d.ts.map +0 -1
  121. package/dist/utils/base64.js.map +0 -1
  122. package/dist/utils/cors.d.ts.map +0 -1
  123. package/dist/utils/cors.js.map +0 -1
  124. package/dist/utils/did-helpers.d.ts.map +0 -1
  125. package/dist/utils/did-helpers.js.map +0 -1
  126. package/dist/utils/index.d.ts.map +0 -1
  127. package/dist/utils/index.js.map +0 -1
  128. package/dist/utils/storage-keys.d.ts.map +0 -1
  129. package/dist/utils/storage-keys.js.map +0 -1
  130. package/docs/API_REFERENCE.md +0 -1362
  131. package/docs/COMPLIANCE_MATRIX.md +0 -691
  132. package/docs/STATUSLIST2021_GUIDE.md +0 -696
  133. package/docs/W3C_VC_DELEGATION_GUIDE.md +0 -710
  134. package/src/__tests__/cache/tool-protection-cache.test.ts +0 -640
  135. package/src/__tests__/config/provider-runtime-config.test.ts +0 -309
  136. package/src/__tests__/delegation-e2e.test.ts +0 -690
  137. package/src/__tests__/identity/user-did-manager.test.ts +0 -232
  138. package/src/__tests__/index.test.ts +0 -56
  139. package/src/__tests__/integration/full-flow.test.ts +0 -789
  140. package/src/__tests__/integration.test.ts +0 -281
  141. package/src/__tests__/providers/base.test.ts +0 -173
  142. package/src/__tests__/providers/memory.test.ts +0 -319
  143. package/src/__tests__/regression/phase2-regression.test.ts +0 -429
  144. package/src/__tests__/runtime/audit-logger.test.ts +0 -154
  145. package/src/__tests__/runtime/base-extensions.test.ts +0 -595
  146. package/src/__tests__/runtime/base.test.ts +0 -869
  147. package/src/__tests__/runtime/delegation-flow.test.ts +0 -164
  148. package/src/__tests__/runtime/proof-client-did.test.ts +0 -376
  149. package/src/__tests__/runtime/route-interception.test.ts +0 -686
  150. package/src/__tests__/runtime/tool-protection-enforcement.test.ts +0 -908
  151. package/src/__tests__/services/agentshield-integration.test.ts +0 -791
  152. package/src/__tests__/services/cache-busting.test.ts +0 -125
  153. package/src/__tests__/services/oauth-service-pkce.test.ts +0 -556
  154. package/src/__tests__/services/provider-resolver-edge-cases.test.ts +0 -591
  155. package/src/__tests__/services/tool-protection-merged-config.test.ts +0 -485
  156. package/src/__tests__/services/tool-protection-oauth-provider.test.ts +0 -480
  157. package/src/__tests__/services/tool-protection.service.test.ts +0 -1373
  158. package/src/__tests__/utils/mock-providers.ts +0 -340
  159. package/src/cache/oauth-config-cache.d.ts +0 -69
  160. package/src/cache/oauth-config-cache.d.ts.map +0 -1
  161. package/src/cache/oauth-config-cache.js.map +0 -1
  162. package/src/cache/oauth-config-cache.ts +0 -123
  163. package/src/cache/tool-protection-cache.ts +0 -171
  164. package/src/compliance/EXAMPLE.md +0 -412
  165. package/src/compliance/__tests__/schema-verifier.test.ts +0 -797
  166. package/src/compliance/index.ts +0 -8
  167. package/src/compliance/schema-registry.ts +0 -460
  168. package/src/compliance/schema-verifier.ts +0 -708
  169. package/src/config/__tests__/merged-config.spec.ts +0 -445
  170. package/src/config/__tests__/remote-config.spec.ts +0 -268
  171. package/src/config/remote-config.ts +0 -264
  172. package/src/config.ts +0 -312
  173. package/src/delegation/__tests__/audience-validator.test.ts +0 -112
  174. package/src/delegation/__tests__/bitstring.test.ts +0 -346
  175. package/src/delegation/__tests__/cascading-revocation.test.ts +0 -628
  176. package/src/delegation/__tests__/delegation-graph.test.ts +0 -584
  177. package/src/delegation/__tests__/did-key-resolver.test.ts +0 -265
  178. package/src/delegation/__tests__/utils.test.ts +0 -152
  179. package/src/delegation/__tests__/vc-issuer.test.ts +0 -442
  180. package/src/delegation/__tests__/vc-verifier.test.ts +0 -922
  181. package/src/delegation/audience-validator.ts +0 -52
  182. package/src/delegation/bitstring.ts +0 -278
  183. package/src/delegation/cascading-revocation.ts +0 -370
  184. package/src/delegation/delegation-graph.ts +0 -299
  185. package/src/delegation/did-key-resolver.ts +0 -179
  186. package/src/delegation/index.ts +0 -14
  187. package/src/delegation/statuslist-manager.ts +0 -353
  188. package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +0 -366
  189. package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +0 -228
  190. package/src/delegation/storage/index.ts +0 -9
  191. package/src/delegation/storage/memory-graph-storage.ts +0 -178
  192. package/src/delegation/storage/memory-statuslist-storage.ts +0 -77
  193. package/src/delegation/utils.ts +0 -221
  194. package/src/delegation/vc-issuer.ts +0 -232
  195. package/src/delegation/vc-verifier.ts +0 -568
  196. package/src/identity/idp-token-resolver.ts +0 -181
  197. package/src/identity/idp-token-storage.interface.ts +0 -94
  198. package/src/identity/user-did-manager.ts +0 -526
  199. package/src/index.ts +0 -310
  200. package/src/providers/base.d.ts +0 -91
  201. package/src/providers/base.d.ts.map +0 -1
  202. package/src/providers/base.js.map +0 -1
  203. package/src/providers/base.ts +0 -96
  204. package/src/providers/memory.ts +0 -142
  205. package/src/runtime/audit-logger.ts +0 -39
  206. package/src/runtime/base.ts +0 -1392
  207. package/src/services/__tests__/access-control.integration.test.ts +0 -443
  208. package/src/services/__tests__/access-control.proof-response-validation.test.ts +0 -578
  209. package/src/services/__tests__/access-control.service.test.ts +0 -970
  210. package/src/services/__tests__/batch-delegation.service.test.ts +0 -351
  211. package/src/services/__tests__/crypto.service.test.ts +0 -531
  212. package/src/services/__tests__/oauth-provider-registry.test.ts +0 -142
  213. package/src/services/__tests__/proof-verifier.integration.test.ts +0 -485
  214. package/src/services/__tests__/proof-verifier.test.ts +0 -489
  215. package/src/services/__tests__/provider-resolution.integration.test.ts +0 -202
  216. package/src/services/__tests__/provider-resolver.test.ts +0 -213
  217. package/src/services/__tests__/storage.service.test.ts +0 -358
  218. package/src/services/access-control.service.ts +0 -990
  219. package/src/services/authorization/authorization-registry.ts +0 -66
  220. package/src/services/authorization/types.ts +0 -71
  221. package/src/services/batch-delegation.service.ts +0 -137
  222. package/src/services/crypto.service.ts +0 -302
  223. package/src/services/errors.ts +0 -76
  224. package/src/services/index.ts +0 -18
  225. package/src/services/oauth-config.service.d.ts +0 -53
  226. package/src/services/oauth-config.service.d.ts.map +0 -1
  227. package/src/services/oauth-config.service.js.map +0 -1
  228. package/src/services/oauth-config.service.ts +0 -192
  229. package/src/services/oauth-provider-registry.d.ts +0 -57
  230. package/src/services/oauth-provider-registry.d.ts.map +0 -1
  231. package/src/services/oauth-provider-registry.js.map +0 -1
  232. package/src/services/oauth-provider-registry.ts +0 -141
  233. package/src/services/oauth-service.ts +0 -544
  234. package/src/services/oauth-token-retrieval.service.ts +0 -245
  235. package/src/services/proof-verifier.ts +0 -478
  236. package/src/services/provider-resolver.d.ts +0 -48
  237. package/src/services/provider-resolver.d.ts.map +0 -1
  238. package/src/services/provider-resolver.js.map +0 -1
  239. package/src/services/provider-resolver.ts +0 -146
  240. package/src/services/provider-validator.ts +0 -170
  241. package/src/services/session-registration.service.ts +0 -251
  242. package/src/services/storage.service.ts +0 -566
  243. package/src/services/tool-context-builder.ts +0 -237
  244. package/src/services/tool-protection.service.ts +0 -1070
  245. package/src/types/oauth-required-error.ts +0 -63
  246. package/src/types/tool-protection.ts +0 -155
  247. package/src/utils/__tests__/did-helpers.test.ts +0 -156
  248. package/src/utils/base58.ts +0 -109
  249. package/src/utils/base64.ts +0 -148
  250. package/src/utils/cors.ts +0 -83
  251. package/src/utils/did-helpers.ts +0 -210
  252. package/src/utils/index.ts +0 -8
  253. package/src/utils/storage-keys.ts +0 -278
  254. package/tsconfig.json +0 -21
  255. package/vitest.config.ts +0 -56
@@ -1,1070 +0,0 @@
1
- /**
2
- * ToolProtectionService - Fetches and caches tool protection configurations
3
- *
4
- * This service manages tool protection configuration from AgentShield API with
5
- * efficient caching and automatic synchronization support.
6
- *
7
- * CORE FUNCTIONALITY:
8
- * -------------------
9
- * 1. Fetches tool protection config from AgentShield API
10
- * 2. Caches responses with configurable TTL (default 5 minutes)
11
- * 3. Falls back to local config if API unavailable
12
- * 4. Provides delegation requirement checking before tool execution
13
- *
14
- * SYNCHRONIZATION WITH AGENTSHIELD:
15
- * ----------------------------------
16
- * When you update tool protection settings in the AgentShield dashboard:
17
- *
18
- * 1. Dashboard sends PATCH /api/internal/bouncer/tools/{projectId}/{toolName}
19
- * 2. AgentShield updates the database immediately (PostgreSQL JSONB column)
20
- * 3. Dashboard sends POST /admin/clear-cache to this service (automatic)
21
- * 4. This service clears the cached config from KV storage
22
- * 5. Next tool call fetches fresh config from AgentShield API
23
- * 6. New config is cached for the configured TTL period
24
- *
25
- * CACHE INVALIDATION:
26
- * -------------------
27
- * Cache is invalidated via POST /admin/clear-cache endpoint:
28
- * - Triggered automatically by AgentShield dashboard when tool protection changes
29
- * - Can be triggered manually for testing/debugging
30
- * - Requires API key authentication for security
31
- *
32
- * If cache is NOT cleared:
33
- * - Stale config is served until TTL expires (default 5 minutes)
34
- * - Configure shorter TTL via TOOL_PROTECTION_CACHE_TTL env var for faster updates
35
- * - Set to 0 for no cache (not recommended for production)
36
- *
37
- * TOOL DISCOVERY PREREQUISITE:
38
- * ----------------------------
39
- * IMPORTANT: Tools must be discovered before they can be protected!
40
- *
41
- * Discovery happens when:
42
- * - Agent makes first tool call with proof submission
43
- * - AgentShield extracts tool info from cryptographic proof
44
- * - Tool is added to bouncerConfigs.discoveredTools in database
45
- *
46
- * If tool not discovered:
47
- * - Tool won't appear in dashboard
48
- * - Protection settings can't be configured
49
- * - GET /config returns empty toolProtection.tools object
50
- *
51
- * DEBUGGING:
52
- * ----------
53
- * Enable debug logging with:
54
- * toolProtection: { debug: true }
55
- *
56
- * Debug logs show:
57
- * - Cache hits vs API fetches
58
- * - Full API responses
59
- * - Tool protection status for each tool
60
- * - Cache TTL and expiration times
61
- * - Source of config data (cache, api, or fallback)
62
- *
63
- * TROUBLESHOOTING:
64
- * ----------------
65
- * Problem: Dashboard shows protection but tool still executes
66
- * Cause: Stale cache not invalidated
67
- * Solution: POST /admin/clear-cache or wait for TTL expiration
68
- *
69
- * Problem: Empty toolProtections returned from API
70
- * Cause: Tool not discovered yet (no proof submissions)
71
- * Solution: Make at least one tool call to trigger discovery
72
- *
73
- * Problem: Updates take 5+ minutes to apply
74
- * Cause: Long cache TTL and cache clear failed
75
- * Solution: Configure MCP server URL in AgentShield for auto cache clear
76
- *
77
- * @see https://github.com/modelcontextprotocol-identity/agent-shield/docs/bouncer/tool-protection-sync.md
78
- * @package @kya-os/mcp-i-core
79
- */
80
-
81
- import type {
82
- ToolProtection,
83
- ToolProtectionConfig,
84
- ToolProtectionServiceConfig,
85
- DelegationRequiredError,
86
- } from "../types/tool-protection.js";
87
- import type { ToolProtectionCache } from "../cache/tool-protection-cache.js";
88
- import { InMemoryToolProtectionCache } from "../cache/tool-protection-cache.js";
89
-
90
- /**
91
- * Tool protection data structure in API responses
92
- */
93
- interface ToolProtectionData {
94
- requiresDelegation?: boolean;
95
- requires_delegation?: boolean;
96
- requiredScopes?: string[];
97
- required_scopes?: string[];
98
- scopes?: string[];
99
- riskLevel?: string;
100
- risk_level?: string;
101
- oauthProvider?: string;
102
- oauth_provider?: string;
103
- authorization?: {
104
- type: string;
105
- provider?: string;
106
- issuer?: string;
107
- credentialType?: string;
108
- };
109
- }
110
-
111
- /**
112
- * Response from AgentShield API bouncer endpoints
113
- *
114
- * Supports multiple endpoint formats:
115
- * 1. Merged config (/projects/{projectId}/config): { data: { config: { toolProtection: { tools: {...} } } } }
116
- * 2. Legacy tool-protections endpoint: { data: { toolProtections: { [toolName]: {...} } } }
117
- * 3. Old config endpoint (/config?agent_did=...): { data: { tools: [{ name: string, ... }] } }
118
- * 4. Legacy format: { data: { tools: { [toolName]: {...} } } }
119
- */
120
- interface BouncerConfigApiResponse {
121
- success: boolean;
122
- data: {
123
- agent_did?: string;
124
-
125
- // NEW: Merged config format (v1.6.0+) - preferred format
126
- // The entire config is returned with tools embedded at config.toolProtection.tools
127
- config?: {
128
- toolProtection?: {
129
- source?: string;
130
- tools?: Record<string, ToolProtectionData>;
131
- };
132
- // Other config fields we don't need to parse
133
- [key: string]: unknown;
134
- };
135
-
136
- // DEPRECATED: Top-level toolProtections (backward compatibility during transition)
137
- toolProtections?: Record<string, ToolProtectionData>;
138
-
139
- // Legacy endpoint formats
140
- tools?:
141
- | Array<{
142
- name: string;
143
- requiresDelegation?: boolean;
144
- requires_delegation?: boolean;
145
- scopes?: string[];
146
- required_scopes?: string[];
147
- oauthProvider?: string;
148
- oauth_provider?: string;
149
- }>
150
- | Record<string, ToolProtectionData>;
151
- reputation_threshold?: number;
152
- denied_agents?: string[];
153
- };
154
- metadata?: {
155
- requestId?: string;
156
- timestamp?: string;
157
- cachedUntil?: string;
158
- };
159
- }
160
-
161
- /**
162
- * Service for fetching and checking tool protection configurations
163
- */
164
- export class ToolProtectionService {
165
- private config: ToolProtectionServiceConfig;
166
- private cache: ToolProtectionCache;
167
-
168
- constructor(config: ToolProtectionServiceConfig, cache: ToolProtectionCache) {
169
- this.config = config;
170
- this.cache = cache;
171
- }
172
-
173
- /**
174
- * Get the project ID from the service configuration
175
- * @returns Project ID or undefined if not configured
176
- */
177
- getProjectId(): string | undefined {
178
- return this.config.projectId;
179
- }
180
-
181
- /**
182
- * Get stale cache entry if available and within maxStaleCacheAge
183
- * Only works with InMemoryToolProtectionCache (has internal access to expiresAt)
184
- *
185
- * NOTE: This checks stale cache BEFORE calling cache.get() to avoid deletion of expired entries
186
- *
187
- * @param cacheKey Cache key to check
188
- * @returns Stale cache entry or null if not available/too old
189
- */
190
- private async getStaleCache(
191
- cacheKey: string
192
- ): Promise<ToolProtectionConfig | null> {
193
- // Only check stale cache if enabled
194
- if (this.config.allowStaleCache === false) {
195
- return null;
196
- }
197
-
198
- // Only works with InMemoryToolProtectionCache
199
- if (!(this.cache instanceof InMemoryToolProtectionCache)) {
200
- return null;
201
- }
202
-
203
- // Use public method to get stale cache entry
204
- const staleConfig = this.cache.getStale(cacheKey);
205
- if (!staleConfig) {
206
- return null;
207
- }
208
-
209
- // Check if stale cache is within maxStaleCacheAge
210
- const expiresAt = this.cache.getExpiresAt(cacheKey);
211
- if (!expiresAt) {
212
- return null;
213
- }
214
-
215
- const now = Date.now();
216
- const maxStaleAge = this.config.maxStaleCacheAge ?? 86400000; // Default 24 hours
217
-
218
- // Check if expired but within maxStaleCacheAge
219
- if (now > expiresAt && now - expiresAt <= maxStaleAge) {
220
- if (this.config.debug) {
221
- console.log("[ToolProtectionService] Using stale cache", {
222
- cacheKey,
223
- expiredAt: new Date(expiresAt).toISOString(),
224
- staleAgeMs: now - expiresAt,
225
- maxStaleAgeMs: maxStaleAge,
226
- });
227
- }
228
- return staleConfig;
229
- }
230
-
231
- return null;
232
- }
233
-
234
- /**
235
- * Get tool protection configuration for the agent
236
- *
237
- * Flow:
238
- * 1. Check cache (project-scoped if projectId available)
239
- * 2. If cache miss, fetch from API
240
- * 3. If API fails, use fallback config
241
- * 4. Cache successful API responses
242
- *
243
- * @param agentDid DID of the agent to fetch config for
244
- */
245
- async getToolProtectionConfig(
246
- agentDid: string
247
- ): Promise<ToolProtectionConfig> {
248
- // Use project-scoped cache key if projectId is available (preferred)
249
- // Falls back to agent-scoped key for backward compatibility
250
- // The cache implementation (KVToolProtectionCache) will add any necessary prefix
251
- const cacheKey = this.config.projectId
252
- ? `config:tool-protections:${this.config.projectId}`
253
- : `agent:${agentDid}`;
254
-
255
- // 1. Check cache
256
- const cached = await this.cache.get(cacheKey);
257
- if (cached) {
258
- const ttl = this.config.cacheTtl ?? 300000;
259
- const cachedUntil = new Date(Date.now() + ttl).toISOString();
260
-
261
- if (this.config.debug) {
262
- console.log("[ToolProtectionService] Cache hit", {
263
- source: "cache",
264
- cacheKey,
265
- agentDid: agentDid.slice(0, 20) + "...",
266
- projectId: this.config.projectId || "none",
267
- toolCount: Object.keys(cached.toolProtections).length,
268
- protectedTools: Object.entries(cached.toolProtections)
269
- .filter(
270
- ([_, config]: [string, ToolProtection]) =>
271
- config.requiresDelegation
272
- )
273
- .map(([name]) => name),
274
- cacheTtlMs: ttl,
275
- cachedUntil,
276
- });
277
- }
278
- return cached;
279
- }
280
-
281
- if (this.config.debug) {
282
- console.log("[ToolProtectionService] Cache miss, fetching from API", {
283
- source: "api-fetch-start",
284
- cacheKey,
285
- agentDid: agentDid.slice(0, 20) + "...",
286
- projectId: this.config.projectId || "none",
287
- apiUrl: this.config.apiUrl,
288
- endpoint: this.config.projectId
289
- ? `/api/v1/bouncer/projects/${this.config.projectId}/config`
290
- : `/api/v1/bouncer/config?agent_did=${agentDid}`,
291
- });
292
- }
293
-
294
- // 3. Fetch from API
295
- try {
296
- const response = await this.fetchFromApi(agentDid);
297
-
298
- if (this.config.debug) {
299
- console.log("[ToolProtectionService] API response received", {
300
- source: "api-fetch-complete",
301
- agentDid: agentDid.slice(0, 20) + "...",
302
- projectId: this.config.projectId || "none",
303
- responseKeys: Object.keys(response),
304
- dataKeys: response.data ? Object.keys(response.data) : [],
305
- rawConfig: response.data?.config || null,
306
- rawConfigToolProtection: response.data?.config?.toolProtection || null,
307
- rawToolProtections: response.data?.toolProtections || null,
308
- rawTools: response.data?.tools || null,
309
- responseMetadata: response.metadata || null,
310
- });
311
- }
312
-
313
- // Transform API response format to internal format
314
- // Supports multiple response formats (in priority order):
315
- // 1. Merged config endpoint: { data: { config: { toolProtection: { tools: {...} } } } }
316
- // 2. Legacy toolProtections: { data: { toolProtections: { greet: { requiresDelegation: true, ... } } } }
317
- // 3. Old endpoint (array): { data: { tools: [{ name: "greet", requiresDelegation: true, ... }] } }
318
- // 4. Old endpoint (object): { data: { tools: { greet: { requiresDelegation: true, ... } } } }
319
- const toolProtections: Record<string, ToolProtection> = {};
320
-
321
- // Check for merged config format first (data.config.toolProtection.tools)
322
- if (response.data.config?.toolProtection?.tools) {
323
- // Merged config endpoint format: object with tool names as keys
324
- if (this.config.debug) {
325
- console.log("[ToolProtectionService] Using merged config format (data.config.toolProtection.tools)");
326
- }
327
- for (const [toolName, toolConfig] of Object.entries(
328
- response.data.config.toolProtection.tools
329
- )) {
330
- const requiresDelegation =
331
- (toolConfig as any).requiresDelegation ??
332
- (toolConfig as any).requires_delegation ??
333
- false;
334
- const requiredScopes =
335
- (toolConfig as any).requiredScopes ??
336
- (toolConfig as any).required_scopes ??
337
- (toolConfig as any).scopes ??
338
- [];
339
-
340
- const oauthProvider =
341
- (toolConfig as any).oauthProvider ??
342
- (toolConfig as any).oauth_provider ??
343
- undefined;
344
-
345
- const riskLevel =
346
- (toolConfig as any).riskLevel ??
347
- (toolConfig as any).risk_level ??
348
- undefined;
349
-
350
- toolProtections[toolName] = {
351
- requiresDelegation,
352
- requiredScopes,
353
- ...(oauthProvider && { oauthProvider }),
354
- ...(riskLevel && { riskLevel }),
355
- };
356
- }
357
- } else if (response.data.toolProtections) {
358
- // Legacy toolProtections format: object with tool names as keys
359
- if (this.config.debug) {
360
- console.log("[ToolProtectionService] Using legacy toolProtections format (data.toolProtections)");
361
- }
362
- // Prefer camelCase over snake_case when both present
363
- for (const [toolName, toolConfig] of Object.entries(
364
- response.data.toolProtections
365
- )) {
366
- const requiresDelegation =
367
- (toolConfig as any).requiresDelegation ??
368
- (toolConfig as any).requires_delegation ??
369
- false;
370
- const requiredScopes =
371
- (toolConfig as any).requiredScopes ??
372
- (toolConfig as any).required_scopes ??
373
- (toolConfig as any).scopes ??
374
- [];
375
-
376
- // NEW: Parse oauthProvider (camelCase and snake_case support)
377
- const oauthProvider =
378
- (toolConfig as any).oauthProvider ??
379
- (toolConfig as any).oauth_provider ??
380
- undefined;
381
-
382
- const riskLevel =
383
- (toolConfig as any).riskLevel ??
384
- (toolConfig as any).risk_level ??
385
- undefined;
386
-
387
- toolProtections[toolName] = {
388
- requiresDelegation,
389
- requiredScopes,
390
- ...(oauthProvider && { oauthProvider }), // Only include if present
391
- ...(riskLevel && { riskLevel }), // Only include if present
392
- };
393
- }
394
- } else if (response.data.tools) {
395
- // Old endpoint format: array or object
396
- if (Array.isArray(response.data.tools)) {
397
- // Array format: [{ name: "greet", requiresDelegation: true, ... }]
398
- for (const tool of response.data.tools) {
399
- const toolName = tool.name;
400
- if (!toolName) {
401
- if (this.config.debug) {
402
- console.warn(
403
- "[ToolProtectionService] Tool missing name in array format",
404
- tool
405
- );
406
- }
407
- continue;
408
- }
409
-
410
- // Prefer camelCase over snake_case when both present
411
- const requiresDelegation =
412
- (tool as any).requiresDelegation ??
413
- (tool as any).requires_delegation ??
414
- false;
415
- const requiredScopes =
416
- (tool as any).requiredScopes ??
417
- (tool as any).required_scopes ??
418
- (tool as any).scopes ??
419
- [];
420
-
421
- // NEW: Parse oauthProvider
422
- const oauthProvider =
423
- (tool as any).oauthProvider ??
424
- (tool as any).oauth_provider ??
425
- undefined;
426
-
427
- const riskLevel =
428
- (tool as any).riskLevel ?? (tool as any).risk_level ?? undefined;
429
-
430
- toolProtections[toolName] = {
431
- requiresDelegation,
432
- requiredScopes,
433
- ...(oauthProvider && { oauthProvider }),
434
- ...(riskLevel && { riskLevel }),
435
- };
436
- }
437
- } else {
438
- // Object format: { greet: { requiresDelegation: true, ... } }
439
- for (const [toolName, toolConfig] of Object.entries(
440
- response.data.tools
441
- )) {
442
- // Prefer camelCase over snake_case when both present
443
- const requiresDelegation =
444
- (toolConfig as any).requiresDelegation ??
445
- (toolConfig as any).requires_delegation ??
446
- false;
447
- const requiredScopes =
448
- (toolConfig as any).requiredScopes ??
449
- (toolConfig as any).required_scopes ??
450
- (toolConfig as any).scopes ??
451
- [];
452
-
453
- // NEW: Parse oauthProvider
454
- const oauthProvider =
455
- (toolConfig as any).oauthProvider ??
456
- (toolConfig as any).oauth_provider ??
457
- undefined;
458
-
459
- const riskLevel =
460
- (toolConfig as any).riskLevel ??
461
- (toolConfig as any).risk_level ??
462
- undefined;
463
-
464
- toolProtections[toolName] = {
465
- requiresDelegation,
466
- requiredScopes,
467
- ...(oauthProvider && { oauthProvider }),
468
- ...(riskLevel && { riskLevel }),
469
- };
470
- }
471
- }
472
- }
473
-
474
- // Merge with fallback config (local config takes priority over API)
475
- // This allows users to override API responses with local configuration
476
- // Note: Fallback config is also used when API fails, but here we merge even when API succeeds
477
- const mergedToolProtections = { ...toolProtections };
478
- if (this.config.fallbackConfig?.toolProtections) {
479
- for (const [toolName, localConfig] of Object.entries(
480
- this.config.fallbackConfig.toolProtections
481
- )) {
482
- // Skip if localConfig is empty or not a valid ToolProtection object
483
- // This prevents empty objects from corrupting the merged config
484
- if (
485
- !localConfig ||
486
- typeof localConfig !== "object" ||
487
- Object.keys(localConfig).length === 0
488
- ) {
489
- if (this.config.debug) {
490
- console.log(
491
- "[ToolProtectionService] Skipping empty/invalid fallback config entry",
492
- { tool: toolName }
493
- );
494
- }
495
- continue;
496
- }
497
-
498
- // Ensure requiredScopes exists (default to empty array if missing)
499
- const validConfig: ToolProtection = {
500
- requiresDelegation:
501
- (localConfig as any).requiresDelegation ?? false,
502
- requiredScopes: (localConfig as any).requiredScopes ?? [],
503
- };
504
-
505
- // Local config overrides API config for this tool
506
- mergedToolProtections[toolName] = validConfig;
507
- if (this.config.debug) {
508
- console.log(
509
- "[ToolProtectionService] Overriding API config with local config",
510
- {
511
- tool: toolName,
512
- localConfig: validConfig,
513
- }
514
- );
515
- }
516
- }
517
- }
518
-
519
- const config: ToolProtectionConfig = {
520
- toolProtections: mergedToolProtections,
521
- };
522
-
523
- // 3. Cache the response
524
- const ttl = this.config.cacheTtl ?? 300000; // Default 5 minutes
525
- const cacheExpiration = new Date(Date.now() + ttl);
526
-
527
- await this.cache.set(cacheKey, config, ttl);
528
-
529
- // Always log tool count and protection status (critical for debugging)
530
- console.log("[ToolProtectionService] Config loaded from API", {
531
- source: "api",
532
- toolCount: Object.keys(mergedToolProtections).length,
533
- protectedTools: Object.entries(mergedToolProtections)
534
- .filter(
535
- ([_, config]: [string, ToolProtection]) => config.requiresDelegation
536
- )
537
- .map(([name]) => name),
538
- agentDid: agentDid.slice(0, 20) + "...",
539
- projectId: this.config.projectId || "none",
540
- cacheTtlMs: ttl,
541
- cacheExpiresAt: cacheExpiration.toISOString(),
542
- });
543
-
544
- if (this.config.debug) {
545
- console.log(
546
- "[ToolProtectionService] API fetch successful, config cached",
547
- {
548
- source: "cache-write",
549
- agentDid: agentDid.slice(0, 20) + "...",
550
- cacheKey,
551
- toolCount: Object.keys(mergedToolProtections).length,
552
- tools: Object.entries(mergedToolProtections).map(
553
- ([name, config]) => ({
554
- name,
555
- requiresDelegation: config.requiresDelegation,
556
- scopeCount: (config.requiredScopes || []).length, // Safe access
557
- })
558
- ),
559
- ttlMs: ttl,
560
- ttlMinutes: Math.round(ttl / 60000),
561
- expiresAt: cacheExpiration.toISOString(),
562
- expiresIn: `${Math.round(ttl / 1000)}s`,
563
- }
564
- );
565
- }
566
-
567
- return config;
568
- } catch (error) {
569
- const errorMessage =
570
- error instanceof Error ? error.message : String(error);
571
-
572
- // Re-throw API key validation errors (don't fallback)
573
- if (errorMessage.includes("API key is missing or empty")) {
574
- throw error;
575
- }
576
-
577
- // Re-throw HTTP errors (4xx, 5xx) - these indicate API issues, not network failures
578
- // Exception: 429 (rate limit) should fallback if fallback config is available
579
- if (errorMessage.includes("Failed to fetch bouncer config:")) {
580
- const status = (error as any).status;
581
- // Allow 429 to fallback (rate limiting is temporary, fallback is acceptable)
582
- if (status === 429 && this.config.fallbackConfig) {
583
- // Will fall through to fallback logic below
584
- } else {
585
- throw error;
586
- }
587
- }
588
-
589
- // Re-throw JSON parsing errors
590
- if (errorMessage.includes("Failed to parse API response:")) {
591
- throw error;
592
- }
593
-
594
- // Re-throw API success: false errors
595
- if (errorMessage.includes("API returned success: false")) {
596
- throw error;
597
- }
598
-
599
- if (this.config.debug) {
600
- console.error("[ToolProtectionService] API fetch failed", {
601
- agentDid: agentDid.slice(0, 20) + "...",
602
- error: errorMessage,
603
- });
604
- }
605
-
606
- // 4. Fallback to stale cache if available (before fallback config)
607
- const staleCache = await this.getStaleCache(cacheKey);
608
- if (staleCache) {
609
- console.warn(
610
- "[ToolProtectionService] API fetch failed, using stale cache",
611
- {
612
- agentDid: agentDid.slice(0, 20) + "...",
613
- error: errorMessage,
614
- cacheKey,
615
- }
616
- );
617
- return staleCache;
618
- }
619
-
620
- // 5. Fallback to local config (only for network errors, not API errors)
621
- if (this.config.fallbackConfig) {
622
- // Always warn when using fallback (not just debug mode)
623
- console.warn(
624
- "[ToolProtectionService] API fetch failed, using fallback config",
625
- {
626
- agentDid: agentDid.slice(0, 20) + "...",
627
- error: errorMessage,
628
- }
629
- );
630
-
631
- // Cache the fallback config to avoid repeated API calls
632
- const ttl = this.config.cacheTtl ?? 300000; // Default 5 minutes
633
- await this.cache.set(cacheKey, this.config.fallbackConfig, ttl);
634
-
635
- return this.config.fallbackConfig;
636
- }
637
-
638
- // 6. Fail-safe behavior: deny-all by default (secure)
639
- const failSafeBehavior = this.config.failSafeBehavior ?? "deny-all";
640
-
641
- if (failSafeBehavior === "deny-all") {
642
- console.error(
643
- "[ToolProtectionService] API fetch failed, no fallback, failing closed (deny-all)",
644
- {
645
- agentDid: agentDid.slice(0, 20) + "...",
646
- error: errorMessage,
647
- cacheKey,
648
- }
649
- );
650
-
651
- // Create deny-all config (wildcard requires delegation for all tools)
652
- const denyAllConfig: ToolProtectionConfig = {
653
- toolProtections: {
654
- "*": {
655
- requiresDelegation: true,
656
- requiredScopes: [],
657
- },
658
- },
659
- };
660
-
661
- // Cache deny-all config to avoid repeated API calls
662
- const ttl = this.config.cacheTtl ?? 300000; // Default 5 minutes
663
- await this.cache.set(cacheKey, denyAllConfig, ttl);
664
-
665
- return denyAllConfig;
666
- } else {
667
- // failSafeBehavior === 'allow-all' (insecure, not recommended)
668
- console.warn(
669
- "[ToolProtectionService] API fetch failed, no fallback, failing open (allow-all)",
670
- {
671
- agentDid: agentDid.slice(0, 20) + "...",
672
- error: errorMessage,
673
- cacheKey,
674
- }
675
- );
676
-
677
- // Cache empty config to avoid repeated API calls
678
- const emptyConfig: ToolProtectionConfig = { toolProtections: {} };
679
- const ttl = this.config.cacheTtl ?? 300000; // Default 5 minutes
680
- await this.cache.set(cacheKey, emptyConfig, ttl);
681
-
682
- return emptyConfig;
683
- }
684
- }
685
- }
686
-
687
- /**
688
- * Check if a tool requires delegation
689
- *
690
- * @param toolName Name of the tool to check
691
- * @param agentDid DID of the agent executing the tool
692
- * @returns Tool protection config or null if no protection required
693
- */
694
- async checkToolProtection(
695
- toolName: string,
696
- agentDid: string
697
- ): Promise<ToolProtection | null> {
698
- const config = await this.getToolProtectionConfig(agentDid);
699
-
700
- // Check for specific tool protection first
701
- let protection = config.toolProtections[toolName];
702
-
703
- // If not found, check for wildcard protection (fail-safe deny-all)
704
- if (!protection && config.toolProtections["*"]) {
705
- protection = config.toolProtections["*"];
706
- }
707
-
708
- // Always log the check result (critical for debugging)
709
- if (this.config.debug || !protection || protection.requiresDelegation) {
710
- console.log("[ToolProtectionService] Protection check", {
711
- tool: toolName,
712
- agentDid: agentDid.slice(0, 20) + "...",
713
- found: !!protection,
714
- isWildcard: protection === config.toolProtections["*"],
715
- requiresDelegation: protection?.requiresDelegation ?? false,
716
- availableTools: Object.keys(config.toolProtections),
717
- });
718
- }
719
-
720
- if (!protection || !protection.requiresDelegation) {
721
- return null;
722
- }
723
-
724
- return protection;
725
- }
726
-
727
- /**
728
- * Fetch tool protection config from AgentShield API
729
- *
730
- * Uses the merged /config endpoint which returns tool protections embedded
731
- * at config.toolProtection.tools. Falls back to legacy formats for backward
732
- * compatibility.
733
- *
734
- * @param agentDid DID of the agent to fetch config for
735
- * @param options Optional fetch options
736
- * @param options.bypassCDNCache When true, adds cache-busting to bypass CDN caches (used by clearAndRefresh)
737
- */
738
- private async fetchFromApi(
739
- agentDid: string,
740
- options?: { bypassCDNCache?: boolean }
741
- ): Promise<BouncerConfigApiResponse> {
742
- // Use the merged /config endpoint which includes embedded tool protections
743
- // This endpoint returns config.toolProtection.tools with all tool rules
744
- let url: string;
745
- let useMergedEndpoint = false;
746
-
747
- if (this.config.projectId) {
748
- // ✅ MERGED CONFIG ENDPOINT: Returns config with embedded toolProtection.tools
749
- url = `${this.config.apiUrl}/api/v1/bouncer/projects/${encodeURIComponent(this.config.projectId)}/config`;
750
- useMergedEndpoint = true;
751
- } else {
752
- // ⚠️ LEGACY ENDPOINT: Agent-scoped, returns tools array (backward compatibility)
753
- url = `${this.config.apiUrl}/api/v1/bouncer/config?agent_did=${encodeURIComponent(agentDid)}`;
754
- }
755
-
756
- // Add cache-busting query param when bypassing CDN cache
757
- // This is used during cache invalidation (clearAndRefresh) to ensure we get fresh data
758
- // from the origin server, not stale CDN-cached data
759
- if (options?.bypassCDNCache) {
760
- const separator = url.includes("?") ? "&" : "?";
761
- url = `${url}${separator}_t=${Date.now()}`;
762
- }
763
-
764
- // Debug: Log API key status (masked for security)
765
- // Format: sk_live... (prefix up to first underscore after type, then ...)
766
- const apiKeyMasked = this.config.apiKey
767
- ? (() => {
768
- const parts = this.config.apiKey.split("_");
769
- if (parts.length >= 2) {
770
- return `${parts[0]}_${parts[1]}...`;
771
- }
772
- return `${this.config.apiKey.substring(0, 8)}...`;
773
- })()
774
- : "(empty)";
775
- const apiKeyLength = this.config.apiKey?.length || 0;
776
-
777
- if (this.config.debug) {
778
- console.log("[ToolProtectionService] Fetching from API:", url, {
779
- method: useMergedEndpoint
780
- ? "projects/{projectId}/config (merged)"
781
- : "config?agent_did (legacy)",
782
- projectId: this.config.projectId || "none",
783
- apiKeyPresent: !!this.config.apiKey,
784
- apiKeyLength,
785
- apiKeyMasked,
786
- });
787
- }
788
-
789
- // Validate API key is present
790
- if (!this.config.apiKey || this.config.apiKey.trim() === "") {
791
- const error = "API key is missing or empty";
792
- if (this.config.debug) {
793
- console.error("[ToolProtectionService]", error, {
794
- apiKeyPresent: !!this.config.apiKey,
795
- apiKeyLength,
796
- });
797
- }
798
- throw new Error(
799
- `ToolProtectionService: ${error}. Check AGENTSHIELD_API_KEY in .dev.vars or wrangler.toml [vars]`
800
- );
801
- }
802
-
803
- // Build headers - merged endpoint uses X-API-Key, legacy uses Authorization Bearer
804
- const headers: Record<string, string> = {
805
- "Content-Type": "application/json",
806
- };
807
-
808
- if (useMergedEndpoint) {
809
- // ✅ Merged config endpoint headers
810
- headers["X-API-Key"] = this.config.apiKey;
811
- if (this.config.projectId) {
812
- headers["X-Project-Id"] = this.config.projectId;
813
- }
814
- } else {
815
- // ⚠️ Legacy endpoint headers (backward compatibility)
816
- headers["Authorization"] = `Bearer ${this.config.apiKey}`;
817
- }
818
-
819
- // Add cache-control header when bypassing CDN cache
820
- // This tells any intermediate caches (CDN, proxies) not to serve cached content
821
- if (options?.bypassCDNCache) {
822
- headers["Cache-Control"] = "no-cache, no-store, must-revalidate";
823
- headers["Pragma"] = "no-cache"; // HTTP/1.0 backward compatibility
824
- }
825
-
826
- const response = await fetch(url, {
827
- method: "GET",
828
- headers,
829
- });
830
-
831
- if (!response.ok) {
832
- const errorText = await response.text().catch(() => "Unknown error");
833
- const error = new Error(
834
- `Failed to fetch bouncer config: ${response.status} ${response.statusText} - ${errorText}`
835
- );
836
- (error as any).status = response.status;
837
- throw error;
838
- }
839
-
840
- let data: BouncerConfigApiResponse;
841
- try {
842
- data = (await response.json()) as BouncerConfigApiResponse;
843
- } catch (jsonError) {
844
- throw new Error(
845
- `Failed to parse API response: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`
846
- );
847
- }
848
-
849
- if (!data.success) {
850
- throw new Error("API returned success: false");
851
- }
852
-
853
- // Transform merged config format to normalized format
854
- // If response contains config.toolProtection.tools, extract them to data.toolProtections
855
- if (useMergedEndpoint && data.data.config?.toolProtection?.tools) {
856
- // Extract embedded tools to the standard toolProtections field
857
- data.data.toolProtections = data.data.config.toolProtection.tools;
858
- if (this.config.debug) {
859
- console.log("[ToolProtectionService] Extracted tools from merged config", {
860
- toolCount: Object.keys(data.data.toolProtections).length,
861
- tools: Object.keys(data.data.toolProtections),
862
- });
863
- }
864
- }
865
-
866
- return data;
867
- }
868
-
869
- /**
870
- * Clear the cache for a project or agent (useful for testing or manual refresh)
871
- *
872
- * If projectId is configured, clears project-scoped cache.
873
- * Otherwise, clears agent-scoped cache.
874
- *
875
- * @param agentDid DID of the agent (used for fallback if projectId not available)
876
- */
877
- async clearCache(agentDid: string): Promise<void> {
878
- // Use same cache key strategy as getToolProtectionConfig
879
- const cacheKey = this.config.projectId
880
- ? `config:tool-protections:${this.config.projectId}`
881
- : `agent:${agentDid}`;
882
-
883
- if (this.config.debug) {
884
- console.log("[ToolProtectionService] Clearing cache", {
885
- cacheKey,
886
- projectId: this.config.projectId || "none",
887
- agentDid: agentDid.slice(0, 20) + "...",
888
- });
889
- }
890
-
891
- await this.cache.delete(cacheKey);
892
- }
893
-
894
- /**
895
- * Clear cache and immediately fetch fresh config from API
896
- *
897
- * This method is designed for Cloudflare Workers where KV has edge caching.
898
- * After clearing the KV entry, it fetches fresh data from the API and writes
899
- * it back to KV. This ensures:
900
- * 1. The global KV entry is deleted
901
- * 2. Fresh data is fetched from API (with CDN cache bypass!)
902
- * 3. New data is written to KV (updating edge cache)
903
- *
904
- * The next request from the same edge location will get the fresh data.
905
- *
906
- * IMPORTANT: This method uses bypassCDNCache to ensure we get fresh data
907
- * from AgentShield's origin server, not stale CDN-cached data. This is
908
- * critical for instant cache invalidation when tool protection settings
909
- * are changed in the AgentShield dashboard.
910
- *
911
- * @param agentDid DID of the agent (used for cache key)
912
- * @returns The fresh tool protection config from API
913
- */
914
- async clearAndRefresh(agentDid: string): Promise<{
915
- config: ToolProtectionConfig;
916
- cacheKey: string;
917
- source: "api" | "fallback";
918
- }> {
919
- const cacheKey = this.config.projectId
920
- ? `config:tool-protections:${this.config.projectId}`
921
- : `agent:${agentDid}`;
922
-
923
- console.log("[ToolProtectionService] clearAndRefresh starting", {
924
- cacheKey,
925
- projectId: this.config.projectId || "none",
926
- agentDid: agentDid.slice(0, 20) + "...",
927
- });
928
-
929
- // 1. Delete the cache entry
930
- await this.cache.delete(cacheKey);
931
-
932
- console.log("[ToolProtectionService] Cache entry deleted", { cacheKey });
933
-
934
- // 2. Fetch fresh config from API with CDN cache bypass
935
- // This ensures we get fresh data from origin, not stale CDN data
936
- try {
937
- const response = await this.fetchFromApi(agentDid, {
938
- bypassCDNCache: true,
939
- });
940
-
941
- // Transform API response to internal format (same logic as getToolProtectionConfig)
942
- const toolProtections: Record<string, ToolProtection> = {};
943
-
944
- if (response.data.toolProtections) {
945
- for (const [toolName, toolConfig] of Object.entries(
946
- response.data.toolProtections
947
- )) {
948
- const requiresDelegation =
949
- (toolConfig as any).requiresDelegation ??
950
- (toolConfig as any).requires_delegation ??
951
- false;
952
- const requiredScopes =
953
- (toolConfig as any).requiredScopes ??
954
- (toolConfig as any).required_scopes ??
955
- (toolConfig as any).scopes ??
956
- [];
957
- const oauthProvider =
958
- (toolConfig as any).oauthProvider ??
959
- (toolConfig as any).oauth_provider ??
960
- undefined;
961
- const riskLevel =
962
- (toolConfig as any).riskLevel ??
963
- (toolConfig as any).risk_level ??
964
- undefined;
965
-
966
- toolProtections[toolName] = {
967
- requiresDelegation,
968
- requiredScopes,
969
- ...(oauthProvider && { oauthProvider }),
970
- ...(riskLevel && { riskLevel }),
971
- };
972
- }
973
- } else if (response.data.tools) {
974
- if (Array.isArray(response.data.tools)) {
975
- for (const tool of response.data.tools) {
976
- const toolName = (tool as any).name;
977
- if (!toolName) continue;
978
- const requiresDelegation =
979
- (tool as any).requiresDelegation ??
980
- (tool as any).requires_delegation ??
981
- false;
982
- const requiredScopes =
983
- (tool as any).requiredScopes ??
984
- (tool as any).required_scopes ??
985
- (tool as any).scopes ??
986
- [];
987
- const oauthProvider =
988
- (tool as any).oauthProvider ??
989
- (tool as any).oauth_provider ??
990
- undefined;
991
- const riskLevel =
992
- (tool as any).riskLevel ?? (tool as any).risk_level ?? undefined;
993
-
994
- toolProtections[toolName] = {
995
- requiresDelegation,
996
- requiredScopes,
997
- ...(oauthProvider && { oauthProvider }),
998
- ...(riskLevel && { riskLevel }),
999
- };
1000
- }
1001
- } else {
1002
- for (const [toolName, toolConfig] of Object.entries(
1003
- response.data.tools
1004
- )) {
1005
- const requiresDelegation =
1006
- (toolConfig as any).requiresDelegation ??
1007
- (toolConfig as any).requires_delegation ??
1008
- false;
1009
- const requiredScopes =
1010
- (toolConfig as any).requiredScopes ??
1011
- (toolConfig as any).required_scopes ??
1012
- (toolConfig as any).scopes ??
1013
- [];
1014
- const oauthProvider =
1015
- (toolConfig as any).oauthProvider ??
1016
- (toolConfig as any).oauth_provider ??
1017
- undefined;
1018
- const riskLevel =
1019
- (toolConfig as any).riskLevel ??
1020
- (toolConfig as any).risk_level ??
1021
- undefined;
1022
-
1023
- toolProtections[toolName] = {
1024
- requiresDelegation,
1025
- requiredScopes,
1026
- ...(oauthProvider && { oauthProvider }),
1027
- ...(riskLevel && { riskLevel }),
1028
- };
1029
- }
1030
- }
1031
- }
1032
-
1033
- // Construct fresh config (ToolProtectionConfig type)
1034
- const freshConfig: ToolProtectionConfig = {
1035
- toolProtections,
1036
- };
1037
-
1038
- // 3. Write fresh config to cache
1039
- const ttl = this.config.cacheTtl ?? 300000;
1040
- await this.cache.set(cacheKey, freshConfig, ttl);
1041
-
1042
- console.log("[ToolProtectionService] Fresh config fetched and cached", {
1043
- cacheKey,
1044
- toolCount: Object.keys(toolProtections).length,
1045
- protectedTools: Object.entries(toolProtections)
1046
- .filter(([_, cfg]) => cfg.requiresDelegation)
1047
- .map(([name]) => name),
1048
- source: "api",
1049
- });
1050
-
1051
- return { config: freshConfig, cacheKey, source: "api" };
1052
- } catch (error) {
1053
- console.warn(
1054
- "[ToolProtectionService] API fetch failed during refresh, using fallback",
1055
- {
1056
- error: error instanceof Error ? error.message : String(error),
1057
- cacheKey,
1058
- }
1059
- );
1060
-
1061
- // Use fallback config if API fails
1062
- const fallbackConfig: ToolProtectionConfig = this.config
1063
- .fallbackConfig || {
1064
- toolProtections: {},
1065
- };
1066
-
1067
- return { config: fallbackConfig, cacheKey, source: "fallback" };
1068
- }
1069
- }
1070
- }