@kya-os/mcp-i-core 1.8.0 → 1.8.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.
- package/dist/conformance/capability-verifier-adapter.d.ts +66 -0
- package/dist/conformance/capability-verifier-adapter.js +61 -0
- package/dist/conformance/index.d.ts +3 -1
- package/dist/conformance/index.js +7 -1
- package/dist/conformance/validate-level2.d.ts +13 -3
- package/dist/conformance/validate-level2.js +15 -5
- package/dist/conformance/vc-jwt-verifier.d.ts +55 -0
- package/dist/conformance/vc-jwt-verifier.js +147 -0
- package/dist/delegation/did-web-resolver.d.ts +60 -0
- package/dist/delegation/did-web-resolver.js +129 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +9 -1
- package/package.json +3 -3
- package/dist/auth/handshake.d.ts +0 -119
- package/dist/auth/handshake.js +0 -250
- package/dist/auth/index.d.ts +0 -6
- package/dist/auth/index.js +0 -11
- package/dist/auth/types.d.ts +0 -46
- package/dist/auth/types.js +0 -10
- package/dist/cache/oauth-config-cache.d.ts +0 -69
- package/dist/cache/oauth-config-cache.js +0 -76
- package/dist/cache/tool-protection-cache.d.ts +0 -89
- package/dist/cache/tool-protection-cache.js +0 -118
- package/dist/compliance/index.d.ts +0 -8
- package/dist/compliance/index.js +0 -24
- package/dist/compliance/schema-registry.d.ts +0 -50
- package/dist/compliance/schema-registry.js +0 -438
- package/dist/compliance/schema-verifier.d.ts +0 -110
- package/dist/compliance/schema-verifier.js +0 -518
- package/dist/config/remote-config.d.ts +0 -173
- package/dist/config/remote-config.js +0 -253
- package/dist/config.d.ts +0 -182
- package/dist/config.js +0 -148
- package/dist/identity/idp-token-resolver.d.ts +0 -69
- package/dist/identity/idp-token-resolver.js +0 -136
- package/dist/identity/idp-token-storage.interface.d.ts +0 -80
- package/dist/identity/idp-token-storage.interface.js +0 -14
- package/dist/identity/user-did-manager.d.ts +0 -228
- package/dist/identity/user-did-manager.js +0 -356
- package/dist/logging/index.d.ts +0 -5
- package/dist/logging/index.js +0 -10
- package/dist/logging/logger.d.ts +0 -33
- package/dist/logging/logger.js +0 -104
- package/dist/proof/generator.d.ts +0 -109
- package/dist/proof/generator.js +0 -236
- package/dist/proof/index.d.ts +0 -5
- package/dist/proof/index.js +0 -11
- package/dist/providers/base.d.ts +0 -95
- package/dist/providers/base.js +0 -47
- package/dist/providers/memory.d.ts +0 -42
- package/dist/providers/memory.js +0 -121
- package/dist/runtime/audit-logger.d.ts +0 -37
- package/dist/runtime/audit-logger.js +0 -9
- package/dist/runtime/base.d.ts +0 -375
- package/dist/runtime/base.js +0 -1491
- package/dist/runtime/ext-apps-constants.d.ts +0 -14
- package/dist/runtime/ext-apps-constants.js +0 -17
- package/dist/services/access-control.service.d.ts +0 -138
- package/dist/services/access-control.service.js +0 -721
- package/dist/services/authorization/authorization-registry.d.ts +0 -29
- package/dist/services/authorization/authorization-registry.js +0 -58
- package/dist/services/authorization/types.d.ts +0 -53
- package/dist/services/authorization/types.js +0 -10
- package/dist/services/batch-delegation.service.d.ts +0 -53
- package/dist/services/batch-delegation.service.js +0 -95
- package/dist/services/crypto.service.d.ts +0 -69
- package/dist/services/crypto.service.js +0 -225
- package/dist/services/delegation-error-formatter.d.ts +0 -124
- package/dist/services/delegation-error-formatter.js +0 -214
- package/dist/services/errors.d.ts +0 -49
- package/dist/services/errors.js +0 -66
- package/dist/services/index.d.ts +0 -9
- package/dist/services/index.js +0 -15
- package/dist/services/oauth-config.service.d.ts +0 -53
- package/dist/services/oauth-config.service.js +0 -141
- package/dist/services/oauth-provider-registry.d.ts +0 -88
- package/dist/services/oauth-provider-registry.js +0 -128
- package/dist/services/oauth-service.d.ts +0 -122
- package/dist/services/oauth-service.js +0 -442
- package/dist/services/oauth-token-retrieval.service.d.ts +0 -49
- package/dist/services/oauth-token-retrieval.service.js +0 -150
- package/dist/services/policy.service.d.ts +0 -130
- package/dist/services/policy.service.js +0 -399
- package/dist/services/proof-verifier.d.ts +0 -98
- package/dist/services/proof-verifier.js +0 -319
- package/dist/services/provider-resolver.d.ts +0 -89
- package/dist/services/provider-resolver.js +0 -196
- package/dist/services/provider-validator.d.ts +0 -55
- package/dist/services/provider-validator.js +0 -135
- package/dist/services/session-registration.service.d.ts +0 -80
- package/dist/services/session-registration.service.js +0 -174
- package/dist/services/storage.service.d.ts +0 -116
- package/dist/services/storage.service.js +0 -411
- package/dist/services/tool-context-builder.d.ts +0 -74
- package/dist/services/tool-context-builder.js +0 -215
- package/dist/services/tool-protection.service.d.ts +0 -193
- package/dist/services/tool-protection.service.js +0 -736
- package/dist/session/index.d.ts +0 -5
- package/dist/session/index.js +0 -11
- package/dist/session/manager.d.ts +0 -113
- package/dist/session/manager.js +0 -273
- package/dist/types/oauth-required-error.d.ts +0 -46
- package/dist/types/oauth-required-error.js +0 -42
- package/dist/types/tool-protection.d.ts +0 -144
- package/dist/types/tool-protection.js +0 -48
- package/dist/utils/cors.d.ts +0 -44
- package/dist/utils/cors.js +0 -71
- package/dist/utils/index.d.ts +0 -8
- package/dist/utils/index.js +0 -24
- package/dist/utils/storage-keys.d.ts +0 -120
- package/dist/utils/storage-keys.js +0 -217
|
@@ -1,736 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* ToolProtectionService - Fetches and caches tool protection configurations
|
|
4
|
-
*
|
|
5
|
-
* This service manages tool protection configuration from AgentShield API with
|
|
6
|
-
* efficient caching and automatic synchronization support.
|
|
7
|
-
*
|
|
8
|
-
* CONFIGURATION PRECEDENCE (highest to lowest):
|
|
9
|
-
* ---------------------------------------------
|
|
10
|
-
* 1. Remote config (AgentShield API) - WINS (source of truth)
|
|
11
|
-
* 2. Local config (localConfig) - Base defaults, overridden by remote
|
|
12
|
-
* 3. Framework defaults
|
|
13
|
-
*
|
|
14
|
-
* OFFLINE FALLBACK (only when API unavailable):
|
|
15
|
-
* ---------------------------------------------
|
|
16
|
-
* 1. Stale cache (if allowStaleCache=true and within maxStaleCacheAge)
|
|
17
|
-
* 2. offlineFallbackConfig (explicit offline-only config)
|
|
18
|
-
* 3. failSafeBehavior (deny-all or allow-all)
|
|
19
|
-
*
|
|
20
|
-
* CORE FUNCTIONALITY:
|
|
21
|
-
* -------------------
|
|
22
|
-
* 1. Fetches tool protection config from AgentShield API (remote wins)
|
|
23
|
-
* 2. Merges with local config (local serves as base defaults)
|
|
24
|
-
* 3. Caches responses with configurable TTL (default 5 minutes)
|
|
25
|
-
* 4. Provides delegation requirement checking before tool execution
|
|
26
|
-
*
|
|
27
|
-
* SYNCHRONIZATION WITH AGENTSHIELD:
|
|
28
|
-
* ----------------------------------
|
|
29
|
-
* When you update tool protection settings in the AgentShield dashboard:
|
|
30
|
-
*
|
|
31
|
-
* 1. Dashboard sends PATCH /api/internal/bouncer/tools/{projectId}/{toolName}
|
|
32
|
-
* 2. AgentShield updates the database immediately (PostgreSQL JSONB column)
|
|
33
|
-
* 3. Dashboard sends POST /admin/clear-cache to this service (automatic)
|
|
34
|
-
* 4. This service clears the cached config from KV storage
|
|
35
|
-
* 5. Next tool call fetches fresh config from AgentShield API
|
|
36
|
-
* 6. New config is cached for the configured TTL period
|
|
37
|
-
*
|
|
38
|
-
* CACHE INVALIDATION:
|
|
39
|
-
* -------------------
|
|
40
|
-
* Cache is invalidated via POST /admin/clear-cache endpoint:
|
|
41
|
-
* - Triggered automatically by AgentShield dashboard when tool protection changes
|
|
42
|
-
* - Can be triggered manually for testing/debugging
|
|
43
|
-
* - Requires API key authentication for security
|
|
44
|
-
*
|
|
45
|
-
* If cache is NOT cleared:
|
|
46
|
-
* - Stale config is served until TTL expires (default 5 minutes)
|
|
47
|
-
* - Configure shorter TTL via TOOL_PROTECTION_CACHE_TTL env var for faster updates
|
|
48
|
-
* - Set to 0 for no cache (not recommended for production)
|
|
49
|
-
*
|
|
50
|
-
* TOOL DISCOVERY PREREQUISITE:
|
|
51
|
-
* ----------------------------
|
|
52
|
-
* IMPORTANT: Tools must be discovered before they can be protected!
|
|
53
|
-
*
|
|
54
|
-
* Discovery happens when:
|
|
55
|
-
* - Agent makes first tool call with proof submission
|
|
56
|
-
* - AgentShield extracts tool info from cryptographic proof
|
|
57
|
-
* - Tool is added to bouncerConfigs.discoveredTools in database
|
|
58
|
-
*
|
|
59
|
-
* If tool not discovered:
|
|
60
|
-
* - Tool won't appear in dashboard
|
|
61
|
-
* - Protection settings can't be configured
|
|
62
|
-
* - GET /config returns empty toolProtection.tools object
|
|
63
|
-
*
|
|
64
|
-
* DEBUGGING:
|
|
65
|
-
* ----------
|
|
66
|
-
* Enable debug logging with:
|
|
67
|
-
* toolProtection: { debug: true }
|
|
68
|
-
*
|
|
69
|
-
* Debug logs show:
|
|
70
|
-
* - Cache hits vs API fetches
|
|
71
|
-
* - Full API responses
|
|
72
|
-
* - Tool protection status for each tool
|
|
73
|
-
* - Cache TTL and expiration times
|
|
74
|
-
* - Source of config data (cache, api, or fallback)
|
|
75
|
-
*
|
|
76
|
-
* TROUBLESHOOTING:
|
|
77
|
-
* ----------------
|
|
78
|
-
* Problem: Dashboard shows protection but tool still executes
|
|
79
|
-
* Cause: Stale cache not invalidated
|
|
80
|
-
* Solution: POST /admin/clear-cache or wait for TTL expiration
|
|
81
|
-
*
|
|
82
|
-
* Problem: Empty toolProtections returned from API
|
|
83
|
-
* Cause: Tool not discovered yet (no proof submissions)
|
|
84
|
-
* Solution: Make at least one tool call to trigger discovery
|
|
85
|
-
*
|
|
86
|
-
* Problem: Updates take 5+ minutes to apply
|
|
87
|
-
* Cause: Long cache TTL and cache clear failed
|
|
88
|
-
* Solution: Configure MCP server URL in AgentShield for auto cache clear
|
|
89
|
-
*
|
|
90
|
-
* @see https://github.com/modelcontextprotocol-identity/agent-shield/docs/bouncer/tool-protection-sync.md
|
|
91
|
-
* @package @kya-os/mcp-i-core
|
|
92
|
-
*/
|
|
93
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
94
|
-
exports.ToolProtectionService = void 0;
|
|
95
|
-
const tool_protection_cache_js_1 = require("../cache/tool-protection-cache.js");
|
|
96
|
-
/**
|
|
97
|
-
* Service for fetching and checking tool protection configurations
|
|
98
|
-
*/
|
|
99
|
-
class ToolProtectionService {
|
|
100
|
-
config;
|
|
101
|
-
cache;
|
|
102
|
-
constructor(config, cache) {
|
|
103
|
-
this.config = config;
|
|
104
|
-
this.cache = cache;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Get the project ID from the service configuration
|
|
108
|
-
* @returns Project ID or undefined if not configured
|
|
109
|
-
*/
|
|
110
|
-
getProjectId() {
|
|
111
|
-
return this.config.projectId;
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Get stale cache entry if available and within maxStaleCacheAge
|
|
115
|
-
* Only works with InMemoryToolProtectionCache (has internal access to expiresAt)
|
|
116
|
-
*
|
|
117
|
-
* NOTE: This checks stale cache BEFORE calling cache.get() to avoid deletion of expired entries
|
|
118
|
-
*
|
|
119
|
-
* @param cacheKey Cache key to check
|
|
120
|
-
* @returns Stale cache entry or null if not available/too old
|
|
121
|
-
*/
|
|
122
|
-
async getStaleCache(cacheKey) {
|
|
123
|
-
// Only check stale cache if enabled
|
|
124
|
-
if (this.config.allowStaleCache === false) {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
// Only works with InMemoryToolProtectionCache
|
|
128
|
-
if (!(this.cache instanceof tool_protection_cache_js_1.InMemoryToolProtectionCache)) {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
// Use public method to get stale cache entry
|
|
132
|
-
const staleConfig = this.cache.getStale(cacheKey);
|
|
133
|
-
if (!staleConfig) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
// Check if stale cache is within maxStaleCacheAge
|
|
137
|
-
const expiresAt = this.cache.getExpiresAt(cacheKey);
|
|
138
|
-
if (!expiresAt) {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
const now = Date.now();
|
|
142
|
-
const maxStaleAge = this.config.maxStaleCacheAge ?? 86400000; // Default 24 hours
|
|
143
|
-
// Check if expired but within maxStaleCacheAge
|
|
144
|
-
if (now > expiresAt && now - expiresAt <= maxStaleAge) {
|
|
145
|
-
if (this.config.debug) {
|
|
146
|
-
console.error("[ToolProtectionService] Using stale cache", {
|
|
147
|
-
cacheKey,
|
|
148
|
-
expiredAt: new Date(expiresAt).toISOString(),
|
|
149
|
-
staleAgeMs: now - expiresAt,
|
|
150
|
-
maxStaleAgeMs: maxStaleAge,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
return staleConfig;
|
|
154
|
-
}
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Get tool protection configuration for the agent
|
|
159
|
-
*
|
|
160
|
-
* Flow:
|
|
161
|
-
* 1. Check cache (project-scoped if projectId available)
|
|
162
|
-
* 2. If cache miss, fetch from API
|
|
163
|
-
* 3. If API fails, use fallback config
|
|
164
|
-
* 4. Cache successful API responses
|
|
165
|
-
*
|
|
166
|
-
* @param agentDid DID of the agent to fetch config for
|
|
167
|
-
*/
|
|
168
|
-
async getToolProtectionConfig(agentDid) {
|
|
169
|
-
// Use project-scoped cache key if projectId is available (preferred)
|
|
170
|
-
// Falls back to agent-scoped key for backward compatibility
|
|
171
|
-
// The cache implementation (KVToolProtectionCache) will add any necessary prefix
|
|
172
|
-
const cacheKey = this.config.projectId
|
|
173
|
-
? `config:tool-protections:${this.config.projectId}`
|
|
174
|
-
: `agent:${agentDid}`;
|
|
175
|
-
// 1. Check cache
|
|
176
|
-
const cached = await this.cache.get(cacheKey);
|
|
177
|
-
if (cached) {
|
|
178
|
-
const ttl = this.config.cacheTtl ?? 300000;
|
|
179
|
-
const cachedUntil = new Date(Date.now() + ttl).toISOString();
|
|
180
|
-
if (this.config.debug) {
|
|
181
|
-
console.error("[ToolProtectionService] Cache hit", {
|
|
182
|
-
source: "cache",
|
|
183
|
-
cacheKey,
|
|
184
|
-
agentDid: agentDid.slice(0, 20) + "...",
|
|
185
|
-
projectId: this.config.projectId || "none",
|
|
186
|
-
toolCount: Object.keys(cached.toolProtections).length,
|
|
187
|
-
protectedTools: Object.entries(cached.toolProtections)
|
|
188
|
-
.filter(([_, config]) => config.requiresDelegation)
|
|
189
|
-
.map(([name]) => name),
|
|
190
|
-
cacheTtlMs: ttl,
|
|
191
|
-
cachedUntil,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
return cached;
|
|
195
|
-
}
|
|
196
|
-
if (this.config.debug) {
|
|
197
|
-
console.error("[ToolProtectionService] Cache miss, fetching from API", {
|
|
198
|
-
source: "api-fetch-start",
|
|
199
|
-
cacheKey,
|
|
200
|
-
agentDid: agentDid.slice(0, 20) + "...",
|
|
201
|
-
projectId: this.config.projectId || "none",
|
|
202
|
-
apiUrl: this.config.apiUrl,
|
|
203
|
-
endpoint: this.config.projectId
|
|
204
|
-
? `/api/v1/bouncer/projects/${this.config.projectId}/config`
|
|
205
|
-
: `/api/v1/bouncer/config?agent_did=${agentDid}`,
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
// 3. Fetch from API
|
|
209
|
-
try {
|
|
210
|
-
const response = await this.fetchFromApi(agentDid);
|
|
211
|
-
if (this.config.debug) {
|
|
212
|
-
console.error("[ToolProtectionService] API response received", {
|
|
213
|
-
source: "api-fetch-complete",
|
|
214
|
-
agentDid: agentDid.slice(0, 20) + "...",
|
|
215
|
-
projectId: this.config.projectId || "none",
|
|
216
|
-
responseKeys: Object.keys(response),
|
|
217
|
-
dataKeys: response.data ? Object.keys(response.data) : [],
|
|
218
|
-
rawConfig: response.data?.config || null,
|
|
219
|
-
rawConfigToolProtection: response.data?.config?.toolProtection || null,
|
|
220
|
-
responseMetadata: response.metadata || null,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
// Transform API response to internal format
|
|
224
|
-
// Uses merged config endpoint: { data: { config: { toolProtection: { tools: {...} } } } }
|
|
225
|
-
const toolProtections = this.parseToolProtectionsFromResponse(response);
|
|
226
|
-
if (this.config.debug && Object.keys(toolProtections).length > 0) {
|
|
227
|
-
console.error("[ToolProtectionService] Parsed tool protections from merged config", {
|
|
228
|
-
toolCount: Object.keys(toolProtections).length,
|
|
229
|
-
tools: Object.keys(toolProtections),
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
// Merge with local config (REMOTE API takes priority over local)
|
|
233
|
-
// Standard precedence: Default < Local < Remote
|
|
234
|
-
// Remote (AgentShield dashboard) is the source of truth and WINS over local config
|
|
235
|
-
const mergedToolProtections = {};
|
|
236
|
-
// Step 1: Apply local config as base (if provided)
|
|
237
|
-
if (this.config.localConfig?.toolProtections) {
|
|
238
|
-
for (const [toolName, localConfig] of Object.entries(this.config.localConfig.toolProtections)) {
|
|
239
|
-
// Skip if localConfig is empty or not a valid ToolProtection object
|
|
240
|
-
// This prevents empty objects from corrupting the merged config
|
|
241
|
-
if (!localConfig ||
|
|
242
|
-
typeof localConfig !== "object" ||
|
|
243
|
-
Object.keys(localConfig).length === 0) {
|
|
244
|
-
if (this.config.debug) {
|
|
245
|
-
console.error("[ToolProtectionService] Skipping empty/invalid local config entry", { tool: toolName });
|
|
246
|
-
}
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
// Ensure requiredScopes exists (default to empty array if missing)
|
|
250
|
-
// Also preserve optional fields like oauthProvider, riskLevel, authorization
|
|
251
|
-
const validConfig = {
|
|
252
|
-
requiresDelegation: localConfig.requiresDelegation ?? false,
|
|
253
|
-
requiredScopes: localConfig.requiredScopes ?? [],
|
|
254
|
-
...(localConfig.oauthProvider && {
|
|
255
|
-
oauthProvider: localConfig.oauthProvider,
|
|
256
|
-
}),
|
|
257
|
-
...(localConfig.riskLevel && {
|
|
258
|
-
riskLevel: localConfig.riskLevel,
|
|
259
|
-
}),
|
|
260
|
-
...(localConfig.authorization && {
|
|
261
|
-
authorization: localConfig.authorization,
|
|
262
|
-
}),
|
|
263
|
-
};
|
|
264
|
-
mergedToolProtections[toolName] = validConfig;
|
|
265
|
-
if (this.config.debug) {
|
|
266
|
-
console.error("[ToolProtectionService] Applied local config as base", {
|
|
267
|
-
tool: toolName,
|
|
268
|
-
localConfig: validConfig,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
// Step 2: Apply API config (OVERWRITES local config - remote wins)
|
|
274
|
-
for (const [toolName, apiConfig] of Object.entries(toolProtections)) {
|
|
275
|
-
mergedToolProtections[toolName] = apiConfig;
|
|
276
|
-
if (this.config.debug &&
|
|
277
|
-
this.config.localConfig?.toolProtections?.[toolName]) {
|
|
278
|
-
console.error("[ToolProtectionService] Remote config overrides local config", {
|
|
279
|
-
tool: toolName,
|
|
280
|
-
remoteConfig: apiConfig,
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
const config = {
|
|
285
|
-
toolProtections: mergedToolProtections,
|
|
286
|
-
};
|
|
287
|
-
// 3. Cache the response
|
|
288
|
-
const ttl = this.config.cacheTtl ?? 300000; // Default 5 minutes
|
|
289
|
-
const cacheExpiration = new Date(Date.now() + ttl);
|
|
290
|
-
await this.cache.set(cacheKey, config, ttl);
|
|
291
|
-
// Always log tool count and protection status (critical for debugging)
|
|
292
|
-
console.error("[ToolProtectionService] Config loaded from API", {
|
|
293
|
-
source: "api",
|
|
294
|
-
toolCount: Object.keys(mergedToolProtections).length,
|
|
295
|
-
protectedTools: Object.entries(mergedToolProtections)
|
|
296
|
-
.filter(([_, config]) => config.requiresDelegation)
|
|
297
|
-
.map(([name]) => name),
|
|
298
|
-
agentDid: agentDid.slice(0, 20) + "...",
|
|
299
|
-
projectId: this.config.projectId || "none",
|
|
300
|
-
cacheTtlMs: ttl,
|
|
301
|
-
cacheExpiresAt: cacheExpiration.toISOString(),
|
|
302
|
-
});
|
|
303
|
-
if (this.config.debug) {
|
|
304
|
-
console.error("[ToolProtectionService] API fetch successful, config cached", {
|
|
305
|
-
source: "cache-write",
|
|
306
|
-
agentDid: agentDid.slice(0, 20) + "...",
|
|
307
|
-
cacheKey,
|
|
308
|
-
toolCount: Object.keys(mergedToolProtections).length,
|
|
309
|
-
tools: Object.entries(mergedToolProtections).map(([name, config]) => ({
|
|
310
|
-
name,
|
|
311
|
-
requiresDelegation: config.requiresDelegation,
|
|
312
|
-
scopeCount: (config.requiredScopes || []).length, // Safe access
|
|
313
|
-
})),
|
|
314
|
-
ttlMs: ttl,
|
|
315
|
-
ttlMinutes: Math.round(ttl / 60000),
|
|
316
|
-
expiresAt: cacheExpiration.toISOString(),
|
|
317
|
-
expiresIn: `${Math.round(ttl / 1000)}s`,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
return config;
|
|
321
|
-
}
|
|
322
|
-
catch (error) {
|
|
323
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
324
|
-
// Re-throw API key validation errors (don't fallback)
|
|
325
|
-
if (errorMessage.includes("API key is missing or empty")) {
|
|
326
|
-
throw error;
|
|
327
|
-
}
|
|
328
|
-
// Re-throw projectId required errors (security fix - don't fallback)
|
|
329
|
-
if (errorMessage.includes("projectId is required")) {
|
|
330
|
-
throw error;
|
|
331
|
-
}
|
|
332
|
-
// Re-throw HTTP errors (4xx, 5xx) - these indicate API issues, not network failures
|
|
333
|
-
// Exception: 429 (rate limit) should fallback if offline fallback config is available
|
|
334
|
-
if (errorMessage.includes("Failed to fetch bouncer config:")) {
|
|
335
|
-
const status = error.status;
|
|
336
|
-
// Allow 429 to fallback (rate limiting is temporary, fallback is acceptable)
|
|
337
|
-
if (status === 429 && this.config.offlineFallbackConfig) {
|
|
338
|
-
// Will fall through to fallback logic below
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
throw error;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
// Re-throw JSON parsing errors
|
|
345
|
-
if (errorMessage.includes("Failed to parse API response:")) {
|
|
346
|
-
throw error;
|
|
347
|
-
}
|
|
348
|
-
// Re-throw API success: false errors
|
|
349
|
-
if (errorMessage.includes("API returned success: false")) {
|
|
350
|
-
throw error;
|
|
351
|
-
}
|
|
352
|
-
if (this.config.debug) {
|
|
353
|
-
console.error("[ToolProtectionService] API fetch failed", {
|
|
354
|
-
agentDid: agentDid.slice(0, 20) + "...",
|
|
355
|
-
error: errorMessage,
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
// 4. Fallback to stale cache if available (before fallback config)
|
|
359
|
-
const staleCache = await this.getStaleCache(cacheKey);
|
|
360
|
-
if (staleCache) {
|
|
361
|
-
console.warn("[ToolProtectionService] API fetch failed, using stale cache", {
|
|
362
|
-
agentDid: agentDid.slice(0, 20) + "...",
|
|
363
|
-
error: errorMessage,
|
|
364
|
-
cacheKey,
|
|
365
|
-
});
|
|
366
|
-
return staleCache;
|
|
367
|
-
}
|
|
368
|
-
// 5. Fallback to offline config (only used when API is unavailable)
|
|
369
|
-
// Note: offlineFallbackConfig is separate from localConfig
|
|
370
|
-
// - localConfig: Merged with remote (remote wins), used always
|
|
371
|
-
// - offlineFallbackConfig: Only used when API is completely unavailable
|
|
372
|
-
if (this.config.offlineFallbackConfig) {
|
|
373
|
-
// Always warn when using offline fallback (not just debug mode)
|
|
374
|
-
console.warn("[ToolProtectionService] API fetch failed, using offline fallback config", {
|
|
375
|
-
agentDid: agentDid.slice(0, 20) + "...",
|
|
376
|
-
error: errorMessage,
|
|
377
|
-
});
|
|
378
|
-
// Cache the fallback config to avoid repeated API calls
|
|
379
|
-
const ttl = this.config.cacheTtl ?? 300000; // Default 5 minutes
|
|
380
|
-
await this.cache.set(cacheKey, this.config.offlineFallbackConfig, ttl);
|
|
381
|
-
return this.config.offlineFallbackConfig;
|
|
382
|
-
}
|
|
383
|
-
// 6. Fail-safe behavior: deny-all by default (secure)
|
|
384
|
-
// This is the last resort when: API fails AND no stale cache AND no offlineFallbackConfig
|
|
385
|
-
const failSafeBehavior = this.config.failSafeBehavior ?? "deny-all";
|
|
386
|
-
if (failSafeBehavior === "deny-all") {
|
|
387
|
-
console.error("[ToolProtectionService] API fetch failed, no offline fallback, failing closed (deny-all)", {
|
|
388
|
-
agentDid: agentDid.slice(0, 20) + "...",
|
|
389
|
-
error: errorMessage,
|
|
390
|
-
cacheKey,
|
|
391
|
-
});
|
|
392
|
-
// Create deny-all config (wildcard requires delegation for all tools)
|
|
393
|
-
const denyAllConfig = {
|
|
394
|
-
toolProtections: {
|
|
395
|
-
"*": {
|
|
396
|
-
requiresDelegation: true,
|
|
397
|
-
requiredScopes: [],
|
|
398
|
-
},
|
|
399
|
-
},
|
|
400
|
-
};
|
|
401
|
-
// Cache deny-all config to avoid repeated API calls
|
|
402
|
-
const ttl = this.config.cacheTtl ?? 300000; // Default 5 minutes
|
|
403
|
-
await this.cache.set(cacheKey, denyAllConfig, ttl);
|
|
404
|
-
return denyAllConfig;
|
|
405
|
-
}
|
|
406
|
-
else {
|
|
407
|
-
// failSafeBehavior === 'allow-all' (insecure, not recommended)
|
|
408
|
-
console.warn("[ToolProtectionService] API fetch failed, no offline fallback, failing open (allow-all)", {
|
|
409
|
-
agentDid: agentDid.slice(0, 20) + "...",
|
|
410
|
-
error: errorMessage,
|
|
411
|
-
cacheKey,
|
|
412
|
-
});
|
|
413
|
-
// Cache empty config to avoid repeated API calls
|
|
414
|
-
const emptyConfig = { toolProtections: {} };
|
|
415
|
-
const ttl = this.config.cacheTtl ?? 300000; // Default 5 minutes
|
|
416
|
-
await this.cache.set(cacheKey, emptyConfig, ttl);
|
|
417
|
-
return emptyConfig;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Check if a tool requires delegation
|
|
423
|
-
*
|
|
424
|
-
* @param toolName Name of the tool to check
|
|
425
|
-
* @param agentDid DID of the agent executing the tool
|
|
426
|
-
* @returns Tool protection config or null if no protection required
|
|
427
|
-
*/
|
|
428
|
-
async checkToolProtection(toolName, agentDid) {
|
|
429
|
-
const config = await this.getToolProtectionConfig(agentDid);
|
|
430
|
-
// Check for specific tool protection first
|
|
431
|
-
let protection = config.toolProtections[toolName];
|
|
432
|
-
// If not found, check for wildcard protection (fail-safe deny-all)
|
|
433
|
-
if (!protection && config.toolProtections["*"]) {
|
|
434
|
-
protection = config.toolProtections["*"];
|
|
435
|
-
}
|
|
436
|
-
// Always log the check result (critical for debugging)
|
|
437
|
-
if (this.config.debug || !protection || protection.requiresDelegation) {
|
|
438
|
-
console.error("[ToolProtectionService] Protection check", {
|
|
439
|
-
tool: toolName,
|
|
440
|
-
agentDid: agentDid.slice(0, 20) + "...",
|
|
441
|
-
found: !!protection,
|
|
442
|
-
isWildcard: protection === config.toolProtections["*"],
|
|
443
|
-
requiresDelegation: protection?.requiresDelegation ?? false,
|
|
444
|
-
availableTools: Object.keys(config.toolProtections),
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
if (!protection || !protection.requiresDelegation) {
|
|
448
|
-
return null;
|
|
449
|
-
}
|
|
450
|
-
return protection;
|
|
451
|
-
}
|
|
452
|
-
/**
|
|
453
|
-
* Fetch tool protection config from AgentShield API
|
|
454
|
-
*
|
|
455
|
-
* Uses the merged /config endpoint which returns tool protections embedded
|
|
456
|
-
* at config.toolProtection.tools. Falls back to legacy formats for backward
|
|
457
|
-
* compatibility.
|
|
458
|
-
*
|
|
459
|
-
* @param agentDid DID of the agent to fetch config for
|
|
460
|
-
* @param options Optional fetch options
|
|
461
|
-
* @param options.bypassCDNCache When true, adds cache-busting to bypass CDN caches (used by clearAndRefresh)
|
|
462
|
-
*/
|
|
463
|
-
async fetchFromApi(agentDid, options) {
|
|
464
|
-
// Use the merged /config endpoint which includes embedded tool protections
|
|
465
|
-
// This endpoint returns config.toolProtection.tools with all tool rules
|
|
466
|
-
let url;
|
|
467
|
-
let useMergedEndpoint = false;
|
|
468
|
-
// ❌ REMOVED: Legacy agent-scoped endpoint fallback
|
|
469
|
-
// Agent-only scoping causes cross-user delegation leakage (Priority 3 fallback issue)
|
|
470
|
-
// projectId is now REQUIRED for proper user isolation
|
|
471
|
-
if (!this.config.projectId) {
|
|
472
|
-
const error = new Error("[ToolProtectionService] projectId is required for user-scoped delegation. " +
|
|
473
|
-
"Agent-only scoping has been deprecated due to security concerns (cross-user delegation leakage). " +
|
|
474
|
-
"Please configure AGENTSHIELD_PROJECT_ID environment variable.");
|
|
475
|
-
console.error("[ToolProtectionService]", error.message);
|
|
476
|
-
throw error;
|
|
477
|
-
}
|
|
478
|
-
// ✅ PROJECT-SCOPED ENDPOINT: Returns config with embedded toolProtection.tools
|
|
479
|
-
// This endpoint ensures delegations are properly scoped to user+agent
|
|
480
|
-
url = `${this.config.apiUrl}/api/v1/bouncer/projects/${encodeURIComponent(this.config.projectId)}/config`;
|
|
481
|
-
useMergedEndpoint = true;
|
|
482
|
-
// Add cache-busting query param when bypassing CDN cache
|
|
483
|
-
// This is used during cache invalidation (clearAndRefresh) to ensure we get fresh data
|
|
484
|
-
// from the origin server, not stale CDN-cached data
|
|
485
|
-
if (options?.bypassCDNCache) {
|
|
486
|
-
const separator = url.includes("?") ? "&" : "?";
|
|
487
|
-
url = `${url}${separator}_t=${Date.now()}`;
|
|
488
|
-
}
|
|
489
|
-
// Debug: Log API key status (masked for security)
|
|
490
|
-
// Format: sk_live... (prefix up to first underscore after type, then ...)
|
|
491
|
-
const apiKeyMasked = this.config.apiKey
|
|
492
|
-
? (() => {
|
|
493
|
-
const parts = this.config.apiKey.split("_");
|
|
494
|
-
if (parts.length >= 2) {
|
|
495
|
-
return `${parts[0]}_${parts[1]}...`;
|
|
496
|
-
}
|
|
497
|
-
return `${this.config.apiKey.substring(0, 8)}...`;
|
|
498
|
-
})()
|
|
499
|
-
: "(empty)";
|
|
500
|
-
const apiKeyLength = this.config.apiKey?.length || 0;
|
|
501
|
-
if (this.config.debug) {
|
|
502
|
-
console.error("[ToolProtectionService] Fetching from API:", url, {
|
|
503
|
-
method: useMergedEndpoint
|
|
504
|
-
? "projects/{projectId}/config (merged)"
|
|
505
|
-
: "config?agent_did (legacy)",
|
|
506
|
-
projectId: this.config.projectId || "none",
|
|
507
|
-
apiKeyPresent: !!this.config.apiKey,
|
|
508
|
-
apiKeyLength,
|
|
509
|
-
apiKeyMasked,
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
// Validate API key is present
|
|
513
|
-
if (!this.config.apiKey || this.config.apiKey.trim() === "") {
|
|
514
|
-
const error = "API key is missing or empty";
|
|
515
|
-
if (this.config.debug) {
|
|
516
|
-
console.error("[ToolProtectionService]", error, {
|
|
517
|
-
apiKeyPresent: !!this.config.apiKey,
|
|
518
|
-
apiKeyLength,
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
throw new Error(`ToolProtectionService: ${error}. Check AGENTSHIELD_API_KEY in .dev.vars or wrangler.toml [vars]`);
|
|
522
|
-
}
|
|
523
|
-
// Build headers - merged endpoint uses X-API-Key, legacy uses Authorization Bearer
|
|
524
|
-
const headers = {
|
|
525
|
-
"Content-Type": "application/json",
|
|
526
|
-
};
|
|
527
|
-
if (useMergedEndpoint) {
|
|
528
|
-
// ✅ Merged config endpoint headers
|
|
529
|
-
headers["X-API-Key"] = this.config.apiKey;
|
|
530
|
-
if (this.config.projectId) {
|
|
531
|
-
headers["X-Project-Id"] = this.config.projectId;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
else {
|
|
535
|
-
// ⚠️ Legacy endpoint headers (backward compatibility)
|
|
536
|
-
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
537
|
-
}
|
|
538
|
-
// Add cache-control header when bypassing CDN cache
|
|
539
|
-
// This tells any intermediate caches (CDN, proxies) not to serve cached content
|
|
540
|
-
if (options?.bypassCDNCache) {
|
|
541
|
-
headers["Cache-Control"] = "no-cache, no-store, must-revalidate";
|
|
542
|
-
headers["Pragma"] = "no-cache"; // HTTP/1.0 backward compatibility
|
|
543
|
-
}
|
|
544
|
-
const response = await fetch(url, {
|
|
545
|
-
method: "GET",
|
|
546
|
-
headers,
|
|
547
|
-
});
|
|
548
|
-
if (!response.ok) {
|
|
549
|
-
const errorText = await response.text().catch(() => "Unknown error");
|
|
550
|
-
const error = new Error(`Failed to fetch bouncer config: ${response.status} ${response.statusText} - ${errorText}`);
|
|
551
|
-
error.status = response.status;
|
|
552
|
-
throw error;
|
|
553
|
-
}
|
|
554
|
-
let data;
|
|
555
|
-
try {
|
|
556
|
-
data = (await response.json());
|
|
557
|
-
}
|
|
558
|
-
catch (jsonError) {
|
|
559
|
-
throw new Error(`Failed to parse API response: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`);
|
|
560
|
-
}
|
|
561
|
-
if (!data.success) {
|
|
562
|
-
throw new Error("API returned success: false");
|
|
563
|
-
}
|
|
564
|
-
return data;
|
|
565
|
-
}
|
|
566
|
-
/**
|
|
567
|
-
* Parse tool protections from API response
|
|
568
|
-
*
|
|
569
|
-
* Extracts tool protection configs from merged config format:
|
|
570
|
-
* { data: { config: { toolProtection: { tools: {...} } } } }
|
|
571
|
-
*
|
|
572
|
-
* @param response API response from AgentShield
|
|
573
|
-
* @returns Record of tool name to ToolProtection
|
|
574
|
-
*/
|
|
575
|
-
parseToolProtectionsFromResponse(response) {
|
|
576
|
-
const toolProtections = {};
|
|
577
|
-
// Extract from merged config format: data.config.toolProtection.tools
|
|
578
|
-
const tools = response.data.config?.toolProtection?.tools;
|
|
579
|
-
if (!tools) {
|
|
580
|
-
return toolProtections;
|
|
581
|
-
}
|
|
582
|
-
for (const [toolName, toolConfig] of Object.entries(tools)) {
|
|
583
|
-
// Support both camelCase and snake_case for backward compatibility with DB data
|
|
584
|
-
const requiresDelegation = toolConfig.requiresDelegation ??
|
|
585
|
-
toolConfig.requires_delegation ??
|
|
586
|
-
false;
|
|
587
|
-
const requiredScopes = toolConfig.requiredScopes ??
|
|
588
|
-
toolConfig.required_scopes ??
|
|
589
|
-
toolConfig.scopes ??
|
|
590
|
-
[];
|
|
591
|
-
const oauthProvider = toolConfig.oauthProvider ?? toolConfig.oauth_provider ?? undefined;
|
|
592
|
-
const rawRiskLevel = toolConfig.riskLevel ?? toolConfig.risk_level ?? undefined;
|
|
593
|
-
// Type-safe cast for riskLevel enum
|
|
594
|
-
const riskLevel = rawRiskLevel &&
|
|
595
|
-
["low", "medium", "high", "critical"].includes(rawRiskLevel)
|
|
596
|
-
? rawRiskLevel
|
|
597
|
-
: undefined;
|
|
598
|
-
// Cast authorization to proper discriminated union type
|
|
599
|
-
// The API returns the correct shape, we just need to help TypeScript
|
|
600
|
-
const authorization = toolConfig.authorization;
|
|
601
|
-
toolProtections[toolName] = {
|
|
602
|
-
requiresDelegation,
|
|
603
|
-
requiredScopes,
|
|
604
|
-
...(oauthProvider && { oauthProvider }),
|
|
605
|
-
...(riskLevel && { riskLevel }),
|
|
606
|
-
...(authorization && { authorization }),
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
return toolProtections;
|
|
610
|
-
}
|
|
611
|
-
/**
|
|
612
|
-
* Clear the cache for a project or agent (useful for testing or manual refresh)
|
|
613
|
-
*
|
|
614
|
-
* If projectId is configured, clears project-scoped cache.
|
|
615
|
-
* Otherwise, clears agent-scoped cache.
|
|
616
|
-
*
|
|
617
|
-
* @param agentDid DID of the agent (used for fallback if projectId not available)
|
|
618
|
-
*/
|
|
619
|
-
async clearCache(agentDid) {
|
|
620
|
-
// Use same cache key strategy as getToolProtectionConfig
|
|
621
|
-
const cacheKey = this.config.projectId
|
|
622
|
-
? `config:tool-protections:${this.config.projectId}`
|
|
623
|
-
: `agent:${agentDid}`;
|
|
624
|
-
if (this.config.debug) {
|
|
625
|
-
console.error("[ToolProtectionService] Clearing cache", {
|
|
626
|
-
cacheKey,
|
|
627
|
-
projectId: this.config.projectId || "none",
|
|
628
|
-
agentDid: agentDid.slice(0, 20) + "...",
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
await this.cache.delete(cacheKey);
|
|
632
|
-
}
|
|
633
|
-
/**
|
|
634
|
-
* Clear cache and immediately fetch fresh config from API
|
|
635
|
-
*
|
|
636
|
-
* This method is designed for Cloudflare Workers where KV has edge caching.
|
|
637
|
-
* After clearing the KV entry, it fetches fresh data from the API and writes
|
|
638
|
-
* it back to KV. This ensures:
|
|
639
|
-
* 1. The global KV entry is deleted
|
|
640
|
-
* 2. Fresh data is fetched from API (with CDN cache bypass!)
|
|
641
|
-
* 3. New data is written to KV (updating edge cache)
|
|
642
|
-
*
|
|
643
|
-
* The next request from the same edge location will get the fresh data.
|
|
644
|
-
*
|
|
645
|
-
* IMPORTANT: This method uses bypassCDNCache to ensure we get fresh data
|
|
646
|
-
* from AgentShield's origin server, not stale CDN-cached data. This is
|
|
647
|
-
* critical for instant cache invalidation when tool protection settings
|
|
648
|
-
* are changed in the AgentShield dashboard.
|
|
649
|
-
*
|
|
650
|
-
* @param agentDid DID of the agent (used for cache key)
|
|
651
|
-
* @returns The fresh tool protection config from API
|
|
652
|
-
*/
|
|
653
|
-
async clearAndRefresh(agentDid) {
|
|
654
|
-
const cacheKey = this.config.projectId
|
|
655
|
-
? `config:tool-protections:${this.config.projectId}`
|
|
656
|
-
: `agent:${agentDid}`;
|
|
657
|
-
console.error("[ToolProtectionService] clearAndRefresh starting", {
|
|
658
|
-
cacheKey,
|
|
659
|
-
projectId: this.config.projectId || "none",
|
|
660
|
-
agentDid: agentDid.slice(0, 20) + "...",
|
|
661
|
-
});
|
|
662
|
-
// 1. Delete the cache entry
|
|
663
|
-
await this.cache.delete(cacheKey);
|
|
664
|
-
console.error("[ToolProtectionService] Cache entry deleted", { cacheKey });
|
|
665
|
-
// 2. Fetch fresh config from API with CDN cache bypass
|
|
666
|
-
// This ensures we get fresh data from origin, not stale CDN data
|
|
667
|
-
try {
|
|
668
|
-
const response = await this.fetchFromApi(agentDid, {
|
|
669
|
-
bypassCDNCache: true,
|
|
670
|
-
});
|
|
671
|
-
// Use shared parsing logic for consistency
|
|
672
|
-
const toolProtections = this.parseToolProtectionsFromResponse(response);
|
|
673
|
-
// Apply same merge logic as getToolProtectionConfig:
|
|
674
|
-
// Step 1: Apply localConfig as base (if provided)
|
|
675
|
-
// Step 2: Apply API config (OVERWRITES local config - remote wins)
|
|
676
|
-
const mergedToolProtections = {};
|
|
677
|
-
// Step 1: Apply local config as base
|
|
678
|
-
if (this.config.localConfig?.toolProtections) {
|
|
679
|
-
for (const [toolName, localConfig] of Object.entries(this.config.localConfig.toolProtections)) {
|
|
680
|
-
if (localConfig &&
|
|
681
|
-
typeof localConfig === "object" &&
|
|
682
|
-
Object.keys(localConfig).length > 0) {
|
|
683
|
-
const validConfig = {
|
|
684
|
-
requiresDelegation: localConfig.requiresDelegation ?? false,
|
|
685
|
-
requiredScopes: localConfig.requiredScopes ?? [],
|
|
686
|
-
...(localConfig.oauthProvider && {
|
|
687
|
-
oauthProvider: localConfig.oauthProvider,
|
|
688
|
-
}),
|
|
689
|
-
...(localConfig.riskLevel && {
|
|
690
|
-
riskLevel: localConfig.riskLevel,
|
|
691
|
-
}),
|
|
692
|
-
...(localConfig.authorization && {
|
|
693
|
-
authorization: localConfig.authorization,
|
|
694
|
-
}),
|
|
695
|
-
};
|
|
696
|
-
mergedToolProtections[toolName] = validConfig;
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
// Step 2: Apply API config (OVERWRITES local config - remote wins)
|
|
701
|
-
for (const [toolName, apiConfig] of Object.entries(toolProtections)) {
|
|
702
|
-
mergedToolProtections[toolName] = apiConfig;
|
|
703
|
-
}
|
|
704
|
-
// Construct fresh config (ToolProtectionConfig type)
|
|
705
|
-
const freshConfig = {
|
|
706
|
-
toolProtections: mergedToolProtections,
|
|
707
|
-
};
|
|
708
|
-
// 3. Write fresh config to cache
|
|
709
|
-
const ttl = this.config.cacheTtl ?? 300000;
|
|
710
|
-
await this.cache.set(cacheKey, freshConfig, ttl);
|
|
711
|
-
console.error("[ToolProtectionService] Fresh config fetched and cached", {
|
|
712
|
-
cacheKey,
|
|
713
|
-
toolCount: Object.keys(mergedToolProtections).length,
|
|
714
|
-
protectedTools: Object.entries(mergedToolProtections)
|
|
715
|
-
.filter(([_, cfg]) => cfg.requiresDelegation)
|
|
716
|
-
.map(([name]) => name),
|
|
717
|
-
source: "api",
|
|
718
|
-
});
|
|
719
|
-
return { config: freshConfig, cacheKey, source: "api" };
|
|
720
|
-
}
|
|
721
|
-
catch (error) {
|
|
722
|
-
console.warn("[ToolProtectionService] API fetch failed during refresh, using offline fallback", {
|
|
723
|
-
error: error instanceof Error ? error.message : String(error),
|
|
724
|
-
cacheKey,
|
|
725
|
-
});
|
|
726
|
-
// Use offline fallback config if API fails
|
|
727
|
-
const offlineFallback = this.config
|
|
728
|
-
.offlineFallbackConfig || {
|
|
729
|
-
toolProtections: {},
|
|
730
|
-
};
|
|
731
|
-
return { config: offlineFallback, cacheKey, source: "fallback" };
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
exports.ToolProtectionService = ToolProtectionService;
|
|
736
|
-
//# sourceMappingURL=tool-protection.service.js.map
|