@kya-os/mcp-i-core 1.2.3-canary.7 → 1.3.1

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