UNPKG

@kya-os/mcp-i

Version:

The TypeScript MCP framework with identity features built-in

298 lines (297 loc) 10.8 kB
"use strict"; /** * AgentShield API Delegation Verifier * * Queries delegations from AgentShield managed service API. * Includes local caching to minimize network calls and maximize performance. * * Performance: * - Fast path (cached): < 5ms * - Slow path (API call): < 100ms * - Cache TTL: 1 minute (configurable) * * API Endpoints: * - POST /api/v1/delegations/verify - Verify delegation by agent DID + scopes * - GET /api/v1/delegations/:id - Get delegation by ID * - POST /api/v1/delegations - Create new delegation (admin only) * - POST /api/v1/delegations/:id/revoke - Revoke delegation (admin only) * * Authentication: Bearer token via X-API-Key header * * Related: PHASE_1_XMCP_I_SERVER.md Ticket 1.3 * Related: AGENTSHIELD_DASHBOARD_PLAN.md Epic 2 (Public API) */ Object.defineProperty(exports, "__esModule", { value: true }); exports.AgentShieldAPIDelegationVerifier = void 0; const delegation_1 = require("@kya-os/contracts/delegation"); const agentshield_api_1 = require("@kya-os/contracts/agentshield-api"); const delegation_verifier_1 = require("./delegation-verifier"); const mcp_i_core_1 = require("@kya-os/mcp-i-core"); const node_providers_1 = require("../providers/node-providers"); /** * Simple in-memory cache (same as KV verifier) */ class DelegationCache { cache = new Map(); maxSize; constructor(maxSize = 1000) { this.maxSize = maxSize; } get(key) { const entry = this.cache.get(key); if (!entry) return null; if (Date.now() > entry.expiresAt) { this.cache.delete(key); return null; } return entry.data; } set(key, data, ttlMs) { if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; if (firstKey) this.cache.delete(firstKey); } this.cache.set(key, { data, expiresAt: Date.now() + ttlMs, }); } delete(key) { this.cache.delete(key); } clear() { this.cache.clear(); } } // Use standard AgentShield API response types from contracts /** * AgentShield API Delegation Verifier * * Managed mode: Queries delegations from AgentShield dashboard API */ class AgentShieldAPIDelegationVerifier { apiUrl; apiKey; cache; cacheTtl; debug; accessControlService; constructor(config) { if (!config.agentshield?.apiUrl || !config.agentshield?.apiKey) { throw new Error("AgentShieldAPIDelegationVerifier requires agentshield.apiUrl and agentshield.apiKey in config"); } this.apiUrl = config.agentshield.apiUrl.replace(/\/$/, ""); // Remove trailing slash this.apiKey = config.agentshield.apiKey; this.cache = new DelegationCache(); this.cacheTtl = config.cacheTtl || 60_000; // Default 1 minute this.debug = config.debug || false; // Create AccessControlApiService instance const fetchProvider = new node_providers_1.NodeFetchProvider(); this.accessControlService = new mcp_i_core_1.AccessControlApiService({ baseUrl: this.apiUrl, apiKey: this.apiKey, fetchProvider, logger: this.debug ? (msg, data) => console.log(`[AgentShield] ${msg}`, data) : undefined, }); } /** * Verify agent delegation via API */ async verify(agentDid, scopes, options) { // Validate inputs const validation = delegation_verifier_1.VerifyDelegationInputSchema.safeParse({ agentDid, scopes, options, }); if (!validation.success) { return { valid: false, reason: `Invalid request: ${validation.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ")}`, }; } const startTime = Date.now(); // Build cache key const scopesKey = this.buildScopesKey(scopes); const cacheKey = `verify:${agentDid}:${scopesKey}`; // Fast path: Check cache if (!options?.skipCache) { const cached = this.cache.get(cacheKey); if (cached) { if (this.debug) { console.log(`[AgentShield] Cache HIT for ${agentDid} (${Date.now() - startTime}ms)`); } return { ...cached, cached: true }; } } // Slow path: API call if (this.debug) { console.log(`[AgentShield] Cache MISS for ${agentDid}, calling API...`); } try { // Validate request input using contracts schema const requestBody = agentshield_api_1.verifyDelegationRequestSchema.parse({ agent_did: agentDid, scopes, }); // Use AccessControlApiService for API call const response = await this.accessControlService.verifyDelegation(requestBody); // Extract data from wrapped response const data = response.data; // Build result from validated response const result = { valid: data.valid, delegation: data.delegation, reason: data.error ? data.error.message : data.reason, cached: false, }; // Cache result const ttl = data.valid ? this.cacheTtl : this.cacheTtl / 2; this.cache.set(cacheKey, result, ttl); if (this.debug) { console.log(`[AgentShield] Delegation ${data.valid ? "verified" : "rejected"} (${Date.now() - startTime}ms)`); } return result; } catch (error) { console.error("[AgentShield] API call failed:", error); // Return negative result on error (don't cache failures) // Use standard error format if (error instanceof agentshield_api_1.AgentShieldAPIError) { return { valid: false, reason: error.message, cached: false, }; } const runtimeError = { error: "api_error", message: error instanceof Error ? error.message : "Unknown error", httpStatus: 500, }; return { valid: false, reason: runtimeError.message, cached: false, }; } } /** * Get delegation by ID via API */ async get(delegationId) { // Fetch from API (no caching - cache is for verification results only) try { const response = await fetch(`${this.apiUrl}/api/v1/bouncer/delegations/${delegationId}`, { method: "GET", headers: { Authorization: `Bearer ${this.apiKey}`, }, }); if (!response.ok) { if (response.status === 404) return null; throw new Error(`AgentShield API error: ${response.status}`); } const data = await response.json(); const parsed = delegation_1.DelegationRecordSchema.safeParse(data); if (!parsed.success) { console.error("[AgentShield] Invalid delegation in API response:", parsed.error); return null; } return parsed.data; } catch (error) { console.error("[AgentShield] Failed to get delegation:", error); return null; } } /** * Store delegation (admin operation via API) * * Note: This is typically done via AgentShield dashboard UI, * not by the bouncer directly. Included for completeness. */ async put(delegation) { // Validate first const parsed = delegation_1.DelegationRecordSchema.safeParse(delegation); if (!parsed.success) { throw new Error(`Invalid delegation record: ${parsed.error.message}`); } try { const response = await fetch(`${this.apiUrl}/api/v1/bouncer/delegations`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}`, }, body: JSON.stringify(delegation), }); if (!response.ok) { throw new Error(`AgentShield API error: ${response.status} ${response.statusText}`); } // Invalidate cache const delegationScopes = (0, delegation_verifier_1.extractScopes)(delegation); const scopesKey = this.buildScopesKey(delegationScopes); this.cache.delete(`delegation:${delegation.id}`); this.cache.delete(`verify:${delegation.subjectDid}:${scopesKey}`); if (this.debug) { console.log(`[AgentShield] Stored delegation ${delegation.id}`); } } catch (error) { console.error("[AgentShield] Failed to store delegation:", error); throw error; } } /** * Revoke delegation via API */ async revoke(delegationId, reason) { try { const response = await fetch(`${this.apiUrl}/api/v1/bouncer/delegations/${delegationId}/revoke`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}`, }, body: JSON.stringify({ reason }), }); if (!response.ok) { throw new Error(`AgentShield API error: ${response.status} ${response.statusText}`); } // Invalidate cache this.cache.delete(`delegation:${delegationId}`); if (this.debug) { console.log(`[AgentShield] Revoked delegation ${delegationId}`); } } catch (error) { console.error("[AgentShield] Failed to revoke delegation:", error); throw error; } } /** * Close connections (cleanup) */ async close() { this.cache.clear(); } /** * Build deterministic scopes key for caching */ buildScopesKey(scopes) { const sorted = [...scopes].sort(); const str = sorted.join(","); let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; } return Math.abs(hash).toString(36); } } exports.AgentShieldAPIDelegationVerifier = AgentShieldAPIDelegationVerifier;