@kya-os/mcp-i-core 1.3.23 → 1.3.25
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/identity/user-did-manager.d.ts +1 -1
- package/dist/identity/user-did-manager.js +32 -24
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -2
- package/dist/runtime/base.js +43 -29
- package/dist/services/provider-resolver.d.ts +18 -0
- package/dist/services/provider-resolver.js +36 -2
- package/dist/services/tool-protection.service.d.ts +25 -3
- package/dist/services/tool-protection.service.js +161 -249
- package/dist/types/tool-protection.d.ts +27 -4
- package/dist/utils/base64.js +48 -21
- package/dist/utils/storage-keys.js +6 -6
- package/package.json +2 -2
|
@@ -5,11 +5,23 @@
|
|
|
5
5
|
* This service manages tool protection configuration from AgentShield API with
|
|
6
6
|
* efficient caching and automatic synchronization support.
|
|
7
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
|
+
*
|
|
8
20
|
* CORE FUNCTIONALITY:
|
|
9
21
|
* -------------------
|
|
10
|
-
* 1. Fetches tool protection config from AgentShield API
|
|
11
|
-
* 2.
|
|
12
|
-
* 3.
|
|
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)
|
|
13
25
|
* 4. Provides delegation requirement checking before tool execution
|
|
14
26
|
*
|
|
15
27
|
* SYNCHRONIZATION WITH AGENTSHIELD:
|
|
@@ -131,7 +143,7 @@ class ToolProtectionService {
|
|
|
131
143
|
// Check if expired but within maxStaleCacheAge
|
|
132
144
|
if (now > expiresAt && now - expiresAt <= maxStaleAge) {
|
|
133
145
|
if (this.config.debug) {
|
|
134
|
-
console.
|
|
146
|
+
console.error("[ToolProtectionService] Using stale cache", {
|
|
135
147
|
cacheKey,
|
|
136
148
|
expiredAt: new Date(expiresAt).toISOString(),
|
|
137
149
|
staleAgeMs: now - expiresAt,
|
|
@@ -166,7 +178,7 @@ class ToolProtectionService {
|
|
|
166
178
|
const ttl = this.config.cacheTtl ?? 300000;
|
|
167
179
|
const cachedUntil = new Date(Date.now() + ttl).toISOString();
|
|
168
180
|
if (this.config.debug) {
|
|
169
|
-
console.
|
|
181
|
+
console.error("[ToolProtectionService] Cache hit", {
|
|
170
182
|
source: "cache",
|
|
171
183
|
cacheKey,
|
|
172
184
|
agentDid: agentDid.slice(0, 20) + "...",
|
|
@@ -182,7 +194,7 @@ class ToolProtectionService {
|
|
|
182
194
|
return cached;
|
|
183
195
|
}
|
|
184
196
|
if (this.config.debug) {
|
|
185
|
-
console.
|
|
197
|
+
console.error("[ToolProtectionService] Cache miss, fetching from API", {
|
|
186
198
|
source: "api-fetch-start",
|
|
187
199
|
cacheKey,
|
|
188
200
|
agentDid: agentDid.slice(0, 20) + "...",
|
|
@@ -197,7 +209,7 @@ class ToolProtectionService {
|
|
|
197
209
|
try {
|
|
198
210
|
const response = await this.fetchFromApi(agentDid);
|
|
199
211
|
if (this.config.debug) {
|
|
200
|
-
console.
|
|
212
|
+
console.error("[ToolProtectionService] API response received", {
|
|
201
213
|
source: "api-fetch-complete",
|
|
202
214
|
agentDid: agentDid.slice(0, 20) + "...",
|
|
203
215
|
projectId: this.config.projectId || "none",
|
|
@@ -205,166 +217,70 @@ class ToolProtectionService {
|
|
|
205
217
|
dataKeys: response.data ? Object.keys(response.data) : [],
|
|
206
218
|
rawConfig: response.data?.config || null,
|
|
207
219
|
rawConfigToolProtection: response.data?.config?.toolProtection || null,
|
|
208
|
-
rawToolProtections: response.data?.toolProtections || null,
|
|
209
|
-
rawTools: response.data?.tools || null,
|
|
210
220
|
responseMetadata: response.metadata || null,
|
|
211
221
|
});
|
|
212
222
|
}
|
|
213
|
-
// Transform API response
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (response.data.config?.toolProtection?.tools) {
|
|
222
|
-
// Merged config endpoint format: object with tool names as keys
|
|
223
|
-
if (this.config.debug) {
|
|
224
|
-
console.log("[ToolProtectionService] Using merged config format (data.config.toolProtection.tools)");
|
|
225
|
-
}
|
|
226
|
-
for (const [toolName, toolConfig] of Object.entries(response.data.config.toolProtection.tools)) {
|
|
227
|
-
const requiresDelegation = toolConfig.requiresDelegation ??
|
|
228
|
-
toolConfig.requires_delegation ??
|
|
229
|
-
false;
|
|
230
|
-
const requiredScopes = toolConfig.requiredScopes ??
|
|
231
|
-
toolConfig.required_scopes ??
|
|
232
|
-
toolConfig.scopes ??
|
|
233
|
-
[];
|
|
234
|
-
const oauthProvider = toolConfig.oauthProvider ??
|
|
235
|
-
toolConfig.oauth_provider ??
|
|
236
|
-
undefined;
|
|
237
|
-
const riskLevel = toolConfig.riskLevel ??
|
|
238
|
-
toolConfig.risk_level ??
|
|
239
|
-
undefined;
|
|
240
|
-
toolProtections[toolName] = {
|
|
241
|
-
requiresDelegation,
|
|
242
|
-
requiredScopes,
|
|
243
|
-
...(oauthProvider && { oauthProvider }),
|
|
244
|
-
...(riskLevel && { riskLevel }),
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
else if (response.data.toolProtections) {
|
|
249
|
-
// Legacy toolProtections format: object with tool names as keys
|
|
250
|
-
if (this.config.debug) {
|
|
251
|
-
console.log("[ToolProtectionService] Using legacy toolProtections format (data.toolProtections)");
|
|
252
|
-
}
|
|
253
|
-
// Prefer camelCase over snake_case when both present
|
|
254
|
-
for (const [toolName, toolConfig] of Object.entries(response.data.toolProtections)) {
|
|
255
|
-
const requiresDelegation = toolConfig.requiresDelegation ??
|
|
256
|
-
toolConfig.requires_delegation ??
|
|
257
|
-
false;
|
|
258
|
-
const requiredScopes = toolConfig.requiredScopes ??
|
|
259
|
-
toolConfig.required_scopes ??
|
|
260
|
-
toolConfig.scopes ??
|
|
261
|
-
[];
|
|
262
|
-
// NEW: Parse oauthProvider (camelCase and snake_case support)
|
|
263
|
-
const oauthProvider = toolConfig.oauthProvider ??
|
|
264
|
-
toolConfig.oauth_provider ??
|
|
265
|
-
undefined;
|
|
266
|
-
const riskLevel = toolConfig.riskLevel ??
|
|
267
|
-
toolConfig.risk_level ??
|
|
268
|
-
undefined;
|
|
269
|
-
toolProtections[toolName] = {
|
|
270
|
-
requiresDelegation,
|
|
271
|
-
requiredScopes,
|
|
272
|
-
...(oauthProvider && { oauthProvider }), // Only include if present
|
|
273
|
-
...(riskLevel && { riskLevel }), // Only include if present
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
else if (response.data.tools) {
|
|
278
|
-
// Old endpoint format: array or object
|
|
279
|
-
if (Array.isArray(response.data.tools)) {
|
|
280
|
-
// Array format: [{ name: "greet", requiresDelegation: true, ... }]
|
|
281
|
-
for (const tool of response.data.tools) {
|
|
282
|
-
const toolName = tool.name;
|
|
283
|
-
if (!toolName) {
|
|
284
|
-
if (this.config.debug) {
|
|
285
|
-
console.warn("[ToolProtectionService] Tool missing name in array format", tool);
|
|
286
|
-
}
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
// Prefer camelCase over snake_case when both present
|
|
290
|
-
const requiresDelegation = tool.requiresDelegation ??
|
|
291
|
-
tool.requires_delegation ??
|
|
292
|
-
false;
|
|
293
|
-
const requiredScopes = tool.requiredScopes ??
|
|
294
|
-
tool.required_scopes ??
|
|
295
|
-
tool.scopes ??
|
|
296
|
-
[];
|
|
297
|
-
// NEW: Parse oauthProvider
|
|
298
|
-
const oauthProvider = tool.oauthProvider ??
|
|
299
|
-
tool.oauth_provider ??
|
|
300
|
-
undefined;
|
|
301
|
-
const riskLevel = tool.riskLevel ?? tool.risk_level ?? undefined;
|
|
302
|
-
toolProtections[toolName] = {
|
|
303
|
-
requiresDelegation,
|
|
304
|
-
requiredScopes,
|
|
305
|
-
...(oauthProvider && { oauthProvider }),
|
|
306
|
-
...(riskLevel && { riskLevel }),
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
// Object format: { greet: { requiresDelegation: true, ... } }
|
|
312
|
-
for (const [toolName, toolConfig] of Object.entries(response.data.tools)) {
|
|
313
|
-
// Prefer camelCase over snake_case when both present
|
|
314
|
-
const requiresDelegation = toolConfig.requiresDelegation ??
|
|
315
|
-
toolConfig.requires_delegation ??
|
|
316
|
-
false;
|
|
317
|
-
const requiredScopes = toolConfig.requiredScopes ??
|
|
318
|
-
toolConfig.required_scopes ??
|
|
319
|
-
toolConfig.scopes ??
|
|
320
|
-
[];
|
|
321
|
-
// NEW: Parse oauthProvider
|
|
322
|
-
const oauthProvider = toolConfig.oauthProvider ??
|
|
323
|
-
toolConfig.oauth_provider ??
|
|
324
|
-
undefined;
|
|
325
|
-
const riskLevel = toolConfig.riskLevel ??
|
|
326
|
-
toolConfig.risk_level ??
|
|
327
|
-
undefined;
|
|
328
|
-
toolProtections[toolName] = {
|
|
329
|
-
requiresDelegation,
|
|
330
|
-
requiredScopes,
|
|
331
|
-
...(oauthProvider && { oauthProvider }),
|
|
332
|
-
...(riskLevel && { riskLevel }),
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
}
|
|
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
|
+
});
|
|
336
231
|
}
|
|
337
|
-
// Merge with
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
const mergedToolProtections = {
|
|
341
|
-
if
|
|
342
|
-
|
|
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)) {
|
|
343
239
|
// Skip if localConfig is empty or not a valid ToolProtection object
|
|
344
240
|
// This prevents empty objects from corrupting the merged config
|
|
345
241
|
if (!localConfig ||
|
|
346
242
|
typeof localConfig !== "object" ||
|
|
347
243
|
Object.keys(localConfig).length === 0) {
|
|
348
244
|
if (this.config.debug) {
|
|
349
|
-
console.
|
|
245
|
+
console.error("[ToolProtectionService] Skipping empty/invalid local config entry", { tool: toolName });
|
|
350
246
|
}
|
|
351
247
|
continue;
|
|
352
248
|
}
|
|
353
249
|
// Ensure requiredScopes exists (default to empty array if missing)
|
|
250
|
+
// Also preserve optional fields like oauthProvider, riskLevel, authorization
|
|
354
251
|
const validConfig = {
|
|
355
252
|
requiresDelegation: localConfig.requiresDelegation ?? false,
|
|
356
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
|
+
}),
|
|
357
263
|
};
|
|
358
|
-
// Local config overrides API config for this tool
|
|
359
264
|
mergedToolProtections[toolName] = validConfig;
|
|
360
265
|
if (this.config.debug) {
|
|
361
|
-
console.
|
|
266
|
+
console.error("[ToolProtectionService] Applied local config as base", {
|
|
362
267
|
tool: toolName,
|
|
363
268
|
localConfig: validConfig,
|
|
364
269
|
});
|
|
365
270
|
}
|
|
366
271
|
}
|
|
367
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
|
+
}
|
|
368
284
|
const config = {
|
|
369
285
|
toolProtections: mergedToolProtections,
|
|
370
286
|
};
|
|
@@ -373,7 +289,7 @@ class ToolProtectionService {
|
|
|
373
289
|
const cacheExpiration = new Date(Date.now() + ttl);
|
|
374
290
|
await this.cache.set(cacheKey, config, ttl);
|
|
375
291
|
// Always log tool count and protection status (critical for debugging)
|
|
376
|
-
console.
|
|
292
|
+
console.error("[ToolProtectionService] Config loaded from API", {
|
|
377
293
|
source: "api",
|
|
378
294
|
toolCount: Object.keys(mergedToolProtections).length,
|
|
379
295
|
protectedTools: Object.entries(mergedToolProtections)
|
|
@@ -385,7 +301,7 @@ class ToolProtectionService {
|
|
|
385
301
|
cacheExpiresAt: cacheExpiration.toISOString(),
|
|
386
302
|
});
|
|
387
303
|
if (this.config.debug) {
|
|
388
|
-
console.
|
|
304
|
+
console.error("[ToolProtectionService] API fetch successful, config cached", {
|
|
389
305
|
source: "cache-write",
|
|
390
306
|
agentDid: agentDid.slice(0, 20) + "...",
|
|
391
307
|
cacheKey,
|
|
@@ -414,11 +330,11 @@ class ToolProtectionService {
|
|
|
414
330
|
throw error;
|
|
415
331
|
}
|
|
416
332
|
// Re-throw HTTP errors (4xx, 5xx) - these indicate API issues, not network failures
|
|
417
|
-
// Exception: 429 (rate limit) should fallback if fallback config is available
|
|
333
|
+
// Exception: 429 (rate limit) should fallback if offline fallback config is available
|
|
418
334
|
if (errorMessage.includes("Failed to fetch bouncer config:")) {
|
|
419
335
|
const status = error.status;
|
|
420
336
|
// Allow 429 to fallback (rate limiting is temporary, fallback is acceptable)
|
|
421
|
-
if (status === 429 && this.config.
|
|
337
|
+
if (status === 429 && this.config.offlineFallbackConfig) {
|
|
422
338
|
// Will fall through to fallback logic below
|
|
423
339
|
}
|
|
424
340
|
else {
|
|
@@ -449,22 +365,26 @@ class ToolProtectionService {
|
|
|
449
365
|
});
|
|
450
366
|
return staleCache;
|
|
451
367
|
}
|
|
452
|
-
// 5. Fallback to
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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", {
|
|
456
375
|
agentDid: agentDid.slice(0, 20) + "...",
|
|
457
376
|
error: errorMessage,
|
|
458
377
|
});
|
|
459
378
|
// Cache the fallback config to avoid repeated API calls
|
|
460
379
|
const ttl = this.config.cacheTtl ?? 300000; // Default 5 minutes
|
|
461
|
-
await this.cache.set(cacheKey, this.config.
|
|
462
|
-
return this.config.
|
|
380
|
+
await this.cache.set(cacheKey, this.config.offlineFallbackConfig, ttl);
|
|
381
|
+
return this.config.offlineFallbackConfig;
|
|
463
382
|
}
|
|
464
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
|
|
465
385
|
const failSafeBehavior = this.config.failSafeBehavior ?? "deny-all";
|
|
466
386
|
if (failSafeBehavior === "deny-all") {
|
|
467
|
-
console.error("[ToolProtectionService] API fetch failed, no fallback, failing closed (deny-all)", {
|
|
387
|
+
console.error("[ToolProtectionService] API fetch failed, no offline fallback, failing closed (deny-all)", {
|
|
468
388
|
agentDid: agentDid.slice(0, 20) + "...",
|
|
469
389
|
error: errorMessage,
|
|
470
390
|
cacheKey,
|
|
@@ -485,7 +405,7 @@ class ToolProtectionService {
|
|
|
485
405
|
}
|
|
486
406
|
else {
|
|
487
407
|
// failSafeBehavior === 'allow-all' (insecure, not recommended)
|
|
488
|
-
console.warn("[ToolProtectionService] API fetch failed, no fallback, failing open (allow-all)", {
|
|
408
|
+
console.warn("[ToolProtectionService] API fetch failed, no offline fallback, failing open (allow-all)", {
|
|
489
409
|
agentDid: agentDid.slice(0, 20) + "...",
|
|
490
410
|
error: errorMessage,
|
|
491
411
|
cacheKey,
|
|
@@ -515,7 +435,7 @@ class ToolProtectionService {
|
|
|
515
435
|
}
|
|
516
436
|
// Always log the check result (critical for debugging)
|
|
517
437
|
if (this.config.debug || !protection || protection.requiresDelegation) {
|
|
518
|
-
console.
|
|
438
|
+
console.error("[ToolProtectionService] Protection check", {
|
|
519
439
|
tool: toolName,
|
|
520
440
|
agentDid: agentDid.slice(0, 20) + "...",
|
|
521
441
|
found: !!protection,
|
|
@@ -579,7 +499,7 @@ class ToolProtectionService {
|
|
|
579
499
|
: "(empty)";
|
|
580
500
|
const apiKeyLength = this.config.apiKey?.length || 0;
|
|
581
501
|
if (this.config.debug) {
|
|
582
|
-
console.
|
|
502
|
+
console.error("[ToolProtectionService] Fetching from API:", url, {
|
|
583
503
|
method: useMergedEndpoint
|
|
584
504
|
? "projects/{projectId}/config (merged)"
|
|
585
505
|
: "config?agent_did (legacy)",
|
|
@@ -641,20 +561,53 @@ class ToolProtectionService {
|
|
|
641
561
|
if (!data.success) {
|
|
642
562
|
throw new Error("API returned success: false");
|
|
643
563
|
}
|
|
644
|
-
// Transform merged config format to normalized format
|
|
645
|
-
// If response contains config.toolProtection.tools, extract them to data.toolProtections
|
|
646
|
-
if (useMergedEndpoint && data.data.config?.toolProtection?.tools) {
|
|
647
|
-
// Extract embedded tools to the standard toolProtections field
|
|
648
|
-
data.data.toolProtections = data.data.config.toolProtection.tools;
|
|
649
|
-
if (this.config.debug) {
|
|
650
|
-
console.log("[ToolProtectionService] Extracted tools from merged config", {
|
|
651
|
-
toolCount: Object.keys(data.data.toolProtections).length,
|
|
652
|
-
tools: Object.keys(data.data.toolProtections),
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
564
|
return data;
|
|
657
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
|
+
}
|
|
658
611
|
/**
|
|
659
612
|
* Clear the cache for a project or agent (useful for testing or manual refresh)
|
|
660
613
|
*
|
|
@@ -669,7 +622,7 @@ class ToolProtectionService {
|
|
|
669
622
|
? `config:tool-protections:${this.config.projectId}`
|
|
670
623
|
: `agent:${agentDid}`;
|
|
671
624
|
if (this.config.debug) {
|
|
672
|
-
console.
|
|
625
|
+
console.error("[ToolProtectionService] Clearing cache", {
|
|
673
626
|
cacheKey,
|
|
674
627
|
projectId: this.config.projectId || "none",
|
|
675
628
|
agentDid: agentDid.slice(0, 20) + "...",
|
|
@@ -701,105 +654,64 @@ class ToolProtectionService {
|
|
|
701
654
|
const cacheKey = this.config.projectId
|
|
702
655
|
? `config:tool-protections:${this.config.projectId}`
|
|
703
656
|
: `agent:${agentDid}`;
|
|
704
|
-
console.
|
|
657
|
+
console.error("[ToolProtectionService] clearAndRefresh starting", {
|
|
705
658
|
cacheKey,
|
|
706
659
|
projectId: this.config.projectId || "none",
|
|
707
660
|
agentDid: agentDid.slice(0, 20) + "...",
|
|
708
661
|
});
|
|
709
662
|
// 1. Delete the cache entry
|
|
710
663
|
await this.cache.delete(cacheKey);
|
|
711
|
-
console.
|
|
664
|
+
console.error("[ToolProtectionService] Cache entry deleted", { cacheKey });
|
|
712
665
|
// 2. Fetch fresh config from API with CDN cache bypass
|
|
713
666
|
// This ensures we get fresh data from origin, not stale CDN data
|
|
714
667
|
try {
|
|
715
668
|
const response = await this.fetchFromApi(agentDid, {
|
|
716
669
|
bypassCDNCache: true,
|
|
717
670
|
});
|
|
718
|
-
//
|
|
719
|
-
const toolProtections =
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
}
|
|
743
|
-
else if (response.data.tools) {
|
|
744
|
-
if (Array.isArray(response.data.tools)) {
|
|
745
|
-
for (const tool of response.data.tools) {
|
|
746
|
-
const toolName = tool.name;
|
|
747
|
-
if (!toolName)
|
|
748
|
-
continue;
|
|
749
|
-
const requiresDelegation = tool.requiresDelegation ??
|
|
750
|
-
tool.requires_delegation ??
|
|
751
|
-
false;
|
|
752
|
-
const requiredScopes = tool.requiredScopes ??
|
|
753
|
-
tool.required_scopes ??
|
|
754
|
-
tool.scopes ??
|
|
755
|
-
[];
|
|
756
|
-
const oauthProvider = tool.oauthProvider ??
|
|
757
|
-
tool.oauth_provider ??
|
|
758
|
-
undefined;
|
|
759
|
-
const riskLevel = tool.riskLevel ?? tool.risk_level ?? undefined;
|
|
760
|
-
toolProtections[toolName] = {
|
|
761
|
-
requiresDelegation,
|
|
762
|
-
requiredScopes,
|
|
763
|
-
...(oauthProvider && { oauthProvider }),
|
|
764
|
-
...(riskLevel && { riskLevel }),
|
|
765
|
-
};
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
else {
|
|
769
|
-
for (const [toolName, toolConfig] of Object.entries(response.data.tools)) {
|
|
770
|
-
const requiresDelegation = toolConfig.requiresDelegation ??
|
|
771
|
-
toolConfig.requires_delegation ??
|
|
772
|
-
false;
|
|
773
|
-
const requiredScopes = toolConfig.requiredScopes ??
|
|
774
|
-
toolConfig.required_scopes ??
|
|
775
|
-
toolConfig.scopes ??
|
|
776
|
-
[];
|
|
777
|
-
const oauthProvider = toolConfig.oauthProvider ??
|
|
778
|
-
toolConfig.oauth_provider ??
|
|
779
|
-
undefined;
|
|
780
|
-
const riskLevel = toolConfig.riskLevel ??
|
|
781
|
-
toolConfig.risk_level ??
|
|
782
|
-
undefined;
|
|
783
|
-
toolProtections[toolName] = {
|
|
784
|
-
requiresDelegation,
|
|
785
|
-
requiredScopes,
|
|
786
|
-
...(oauthProvider && { oauthProvider }),
|
|
787
|
-
...(riskLevel && { riskLevel }),
|
|
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
|
+
}),
|
|
788
695
|
};
|
|
696
|
+
mergedToolProtections[toolName] = validConfig;
|
|
789
697
|
}
|
|
790
698
|
}
|
|
791
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
|
+
}
|
|
792
704
|
// Construct fresh config (ToolProtectionConfig type)
|
|
793
705
|
const freshConfig = {
|
|
794
|
-
toolProtections,
|
|
706
|
+
toolProtections: mergedToolProtections,
|
|
795
707
|
};
|
|
796
708
|
// 3. Write fresh config to cache
|
|
797
709
|
const ttl = this.config.cacheTtl ?? 300000;
|
|
798
710
|
await this.cache.set(cacheKey, freshConfig, ttl);
|
|
799
|
-
console.
|
|
711
|
+
console.error("[ToolProtectionService] Fresh config fetched and cached", {
|
|
800
712
|
cacheKey,
|
|
801
|
-
toolCount: Object.keys(
|
|
802
|
-
protectedTools: Object.entries(
|
|
713
|
+
toolCount: Object.keys(mergedToolProtections).length,
|
|
714
|
+
protectedTools: Object.entries(mergedToolProtections)
|
|
803
715
|
.filter(([_, cfg]) => cfg.requiresDelegation)
|
|
804
716
|
.map(([name]) => name),
|
|
805
717
|
source: "api",
|
|
@@ -807,16 +719,16 @@ class ToolProtectionService {
|
|
|
807
719
|
return { config: freshConfig, cacheKey, source: "api" };
|
|
808
720
|
}
|
|
809
721
|
catch (error) {
|
|
810
|
-
console.warn("[ToolProtectionService] API fetch failed during refresh, using fallback", {
|
|
722
|
+
console.warn("[ToolProtectionService] API fetch failed during refresh, using offline fallback", {
|
|
811
723
|
error: error instanceof Error ? error.message : String(error),
|
|
812
724
|
cacheKey,
|
|
813
725
|
});
|
|
814
|
-
// Use fallback config if API fails
|
|
815
|
-
const
|
|
816
|
-
.
|
|
726
|
+
// Use offline fallback config if API fails
|
|
727
|
+
const offlineFallback = this.config
|
|
728
|
+
.offlineFallbackConfig || {
|
|
817
729
|
toolProtections: {},
|
|
818
730
|
};
|
|
819
|
-
return { config:
|
|
731
|
+
return { config: offlineFallback, cacheKey, source: "fallback" };
|
|
820
732
|
}
|
|
821
733
|
}
|
|
822
734
|
}
|
|
@@ -46,17 +46,40 @@ export interface ToolProtectionServiceConfig {
|
|
|
46
46
|
*/
|
|
47
47
|
cacheTtl?: number;
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
49
|
+
* Local/default configuration (LOWEST priority after defaults)
|
|
50
|
+
*
|
|
51
|
+
* This config serves as a base that gets OVERRIDDEN by remote config.
|
|
52
|
+
* Use this for development defaults or project-specific base settings.
|
|
53
|
+
*
|
|
54
|
+
* Precedence order (highest to lowest):
|
|
55
|
+
* 1. Remote config (AgentShield API) - WINS
|
|
56
|
+
* 2. Local config (this field)
|
|
57
|
+
* 3. Framework defaults
|
|
58
|
+
*
|
|
50
59
|
* @optional
|
|
51
60
|
*/
|
|
52
|
-
|
|
61
|
+
localConfig?: ToolProtectionConfig;
|
|
53
62
|
/**
|
|
54
|
-
*
|
|
63
|
+
* Offline fallback configuration (ONLY used when API is unavailable)
|
|
64
|
+
*
|
|
65
|
+
* Unlike localConfig, this is NOT merged with remote config.
|
|
66
|
+
* It's only used as a complete replacement when the API cannot be reached.
|
|
67
|
+
*
|
|
68
|
+
* Fallback order when API fails:
|
|
69
|
+
* 1. Stale cache (if allowStaleCache=true and within maxStaleCacheAge)
|
|
70
|
+
* 2. This offlineFallbackConfig
|
|
71
|
+
* 3. failSafeBehavior (deny-all or allow-all)
|
|
72
|
+
*
|
|
73
|
+
* @optional
|
|
74
|
+
*/
|
|
75
|
+
offlineFallbackConfig?: ToolProtectionConfig;
|
|
76
|
+
/**
|
|
77
|
+
* Fail-safe behavior when API is unavailable and no offline fallback is provided
|
|
55
78
|
* - 'deny-all': Block all tools by default (secure, recommended for production)
|
|
56
79
|
* - 'allow-all': Allow all tools (insecure, not recommended for production)
|
|
57
80
|
* @default 'deny-all'
|
|
58
81
|
*/
|
|
59
|
-
failSafeBehavior?:
|
|
82
|
+
failSafeBehavior?: "deny-all" | "allow-all";
|
|
60
83
|
/**
|
|
61
84
|
* Maximum age (in milliseconds) for stale cache to be used during outages
|
|
62
85
|
* Stale cache is used when API fails and no fallback is provided
|