UNPKG

@kya-os/mcp-i

Version:

The TypeScript MCP framework with identity features built-in

275 lines (274 loc) 9.41 kB
"use strict"; /** * Proof Batch Queue * * Collects proofs in memory and submits them in batches to KTA and AgentShield. * This prevents blocking tool execution while ensuring proofs are eventually submitted. * * Performance: * - Batch size: 10 proofs (configurable) * - Flush interval: 5 seconds (configurable) * - Fire-and-forget submission (doesn't block tool execution) * * Retry Strategy: * - Exponential backoff: 1s, 2s, 4s, 8s, 16s * - Max retries: 5 * - Failed proofs logged and dropped after max retries * * Related: PHASE_1_XMCP_I_SERVER.md Epic 3 (Proof Batching) */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ProofBatchQueue = exports.AgentShieldProofDestination = exports.KTAProofDestination = void 0; exports.createProofBatchQueue = createProofBatchQueue; /** * KTA proof submission destination */ class KTAProofDestination { name = 'KTA'; apiUrl; apiKey; constructor(apiUrl, apiKey) { this.apiUrl = apiUrl.replace(/\/$/, ''); this.apiKey = apiKey; } async submit(proofs) { const headers = { 'Content-Type': 'application/json', }; if (this.apiKey) { headers['X-API-Key'] = this.apiKey; } const response = await fetch(`${this.apiUrl}/api/v1/proofs/batch`, { method: 'POST', headers, body: JSON.stringify({ proofs }), }); if (!response.ok) { throw new Error(`KTA proof submission failed: ${response.status} ${response.statusText}`); } } } exports.KTAProofDestination = KTAProofDestination; /** * AgentShield proof submission destination * * Submits proofs to AgentShield's /api/v1/bouncer/proofs endpoint * with proper authentication and session grouping. */ class AgentShieldProofDestination { name = 'AgentShield'; apiUrl; apiKey; constructor(apiUrl, apiKey) { this.apiUrl = apiUrl.replace(/\/$/, ''); this.apiKey = apiKey; } async submit(proofs) { if (proofs.length === 0) { return; } // Extract session_id from first proof for AgentShield session grouping // AgentShield uses this for analytics and detection monitoring const sessionId = proofs[0]?.meta?.sessionId || 'unknown'; // AgentShield API format requires delegation_id and session_id wrapper const requestBody = { delegation_id: null, // null for proofs without delegation context session_id: sessionId, // AgentShield session grouping (same as meta.sessionId) proofs: proofs }; const response = await fetch(`${this.apiUrl}/api/v1/bouncer/proofs`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}`, // Bearer token format }, body: JSON.stringify(requestBody), }); if (!response.ok) { // Include response body in error for debugging const errorBody = await response.text().catch(() => 'Unable to read error body'); throw new Error(`AgentShield proof submission failed: ${response.status} ${response.statusText}\n${errorBody}`); } } } exports.AgentShieldProofDestination = AgentShieldProofDestination; /** * Proof Batch Queue * * Collects proofs and submits them in batches to multiple destinations */ class ProofBatchQueue { queue = []; pendingBatches = []; config; flushTimer; retryTimer; closed = false; // Stats stats = { queued: 0, submitted: 0, failed: 0, batchesSubmitted: 0, }; constructor(config) { this.config = { destinations: config.destinations, maxBatchSize: config.maxBatchSize || 10, flushIntervalMs: config.flushIntervalMs || 5000, maxRetries: config.maxRetries || 5, debug: config.debug || false, }; // Start flush timer this.startFlushTimer(); // Start retry timer (check every second) this.startRetryTimer(); } /** * Add proof to queue */ enqueue(proof) { if (this.closed) { console.warn('[ProofBatchQueue] Queue is closed, dropping proof'); return; } this.queue.push(proof); this.stats.queued++; if (this.config.debug) { console.log(`[ProofBatchQueue] Enqueued proof (queue size: ${this.queue.length})`); } // Flush immediately if batch size reached if (this.queue.length >= this.config.maxBatchSize) { this.flush(); } } /** * Flush queue immediately (submit all queued proofs) */ async flush() { if (this.queue.length === 0) { return; } const proofs = this.queue.splice(0, this.config.maxBatchSize); if (this.config.debug) { console.log(`[ProofBatchQueue] Flushing ${proofs.length} proofs to ${this.config.destinations.length} destinations`); } // Submit to each destination (fire-and-forget) for (const destination of this.config.destinations) { const batch = { proofs, destination, retryCount: 0, }; this.submitBatch(batch); // Fire-and-forget } } /** * Submit batch to destination (with retries) */ async submitBatch(batch) { try { await batch.destination.submit(batch.proofs); this.stats.submitted += batch.proofs.length; this.stats.batchesSubmitted++; if (this.config.debug) { console.log(`[ProofBatchQueue] Successfully submitted ${batch.proofs.length} proofs to ${batch.destination.name}`); } } catch (error) { console.error(`[ProofBatchQueue] Failed to submit to ${batch.destination.name}:`, error); // Retry with exponential backoff if (batch.retryCount < this.config.maxRetries) { batch.retryCount++; const backoffMs = Math.min(1000 * Math.pow(2, batch.retryCount - 1), 16000); batch.nextRetryAt = Date.now() + backoffMs; this.pendingBatches.push(batch); if (this.config.debug) { console.log(`[ProofBatchQueue] Scheduling retry ${batch.retryCount}/${this.config.maxRetries} in ${backoffMs}ms`); } } else { // Max retries exceeded, drop batch this.stats.failed += batch.proofs.length; console.error(`[ProofBatchQueue] Max retries exceeded for ${batch.destination.name}, dropping ${batch.proofs.length} proofs`); } } } /** * Start flush timer */ startFlushTimer() { this.flushTimer = setInterval(() => { if (this.queue.length > 0) { this.flush(); } }, this.config.flushIntervalMs); // Prevent timer from keeping process alive if (typeof this.flushTimer.unref === 'function') { this.flushTimer.unref(); } } /** * Start retry timer */ startRetryTimer() { this.retryTimer = setInterval(() => { const now = Date.now(); // Find batches ready for retry const retryBatches = this.pendingBatches.filter((batch) => batch.nextRetryAt && batch.nextRetryAt <= now); if (retryBatches.length > 0) { // Remove from pending this.pendingBatches = this.pendingBatches.filter((batch) => !retryBatches.includes(batch)); // Retry each batch for (const batch of retryBatches) { this.submitBatch(batch); // Fire-and-forget } } }, 1000); // Prevent timer from keeping process alive if (typeof this.retryTimer.unref === 'function') { this.retryTimer.unref(); } } /** * Close queue and flush remaining proofs */ async close() { this.closed = true; // Clear timers if (this.flushTimer) { clearInterval(this.flushTimer); } if (this.retryTimer) { clearInterval(this.retryTimer); } // Flush remaining proofs await this.flush(); // Wait for pending retries (with timeout) const maxWaitMs = 30000; // 30 seconds const startTime = Date.now(); while (this.pendingBatches.length > 0 && Date.now() - startTime < maxWaitMs) { await new Promise((resolve) => setTimeout(resolve, 100)); } if (this.pendingBatches.length > 0) { console.warn(`[ProofBatchQueue] Closing with ${this.pendingBatches.length} pending batches (timed out)`); } } /** * Get queue statistics */ getStats() { return { ...this.stats, queueSize: this.queue.length, pendingBatches: this.pendingBatches.length, }; } } exports.ProofBatchQueue = ProofBatchQueue; /** * Create proof batch queue from config */ function createProofBatchQueue(config) { return new ProofBatchQueue(config); }