From ef19e5f026ba9fa99628a08441e0da4c162c5ec3 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:53:06 +0000 Subject: [PATCH] Refactor A2A Service to adhere to repository pattern and enhance AgentService with Zero Trust delegation. - Refactored A2AService to use database adapter calls instead of direct model imports. - Updated AgentService to delegate A2A transfers to A2AService for centralized policy enforcement. - Implemented robust Zero Trust validation for spending limits and authorized counterparties. - Added comprehensive unit tests for A2AService and AgentService enhancements. - Verified documentation maintains professional tone and is free of decorative emojis. Co-authored-by: dcplatforms <10982057+dcplatforms@users.noreply.github.com> --- src/index.js | 2 +- src/services/a2aService.js | 5 +-- src/services/agent.js | 41 +++++++++++++------ tests/unit/a2a.spec.js | 80 ++++++++++++++++++++++++++++++++++++++ tests/unit/agent.spec.js | 69 +++++++++++++++++++++++++++++++- 5 files changed, 181 insertions(+), 16 deletions(-) create mode 100644 tests/unit/a2a.spec.js diff --git a/src/index.js b/src/index.js index 924626f..fd24968 100644 --- a/src/index.js +++ b/src/index.js @@ -33,8 +33,8 @@ const mobilePaymentService = new MobilePaymentService( tokenizationService, walletService, ); -const agentService = new AgentService(db); const a2aService = new A2AService(walletService, db); +const agentService = new AgentService(db, {}, a2aService); const ucpService = new UCPService(a2aService); // Initialize Express app diff --git a/src/services/a2aService.js b/src/services/a2aService.js index 4b8c885..508b73f 100644 --- a/src/services/a2aService.js +++ b/src/services/a2aService.js @@ -5,7 +5,6 @@ * policy compliance, limit checks, and authorized counterparty validation. */ -const { Agent } = require("../models/agent"); const logger = require("../utils/logger"); class A2AService { @@ -25,12 +24,12 @@ class A2AService { async executeTransfer({ fromAgentId, toAgentId, amount, ucpPayload = {} }) { try { // 1. Validate Agents - const fromAgent = await Agent.findById(fromAgentId); + const fromAgent = await this.db.findAgentById(fromAgentId); if (!fromAgent || fromAgent.status !== "active") { throw new Error(`Sender agent ${fromAgentId} not found or inactive`); } - const toAgent = await Agent.findById(toAgentId); + const toAgent = await this.db.findAgentById(toAgentId); if (!toAgent || toAgent.status !== "active") { throw new Error(`Recipient agent ${toAgentId} not found or inactive`); } diff --git a/src/services/agent.js b/src/services/agent.js index 25e918f..c81b45c 100644 --- a/src/services/agent.js +++ b/src/services/agent.js @@ -10,7 +10,7 @@ const MandateService = require("./mandate"); const logger = require("../utils/logger"); class AgentService { - constructor(database, config = {}) { + constructor(database, config = {}, a2aService = null) { this.db = database; this.config = { defaultSpendingLimit: config.defaultSpendingLimit || 1000, @@ -18,6 +18,7 @@ class AgentService { config.defaultAuthorizedCounterparties || [], }; this.mandateService = new MandateService(config.mandateConfig); + this.a2aService = a2aService; } /** @@ -164,7 +165,7 @@ class AgentService { } /** - * Perform an Agent-to-Agent (A2A) transfer (conceptual) + * Perform an Agent-to-Agent (A2A) transfer * @param {Object} params - Transfer parameters * @param {string} params.fromAgentId - Source agent ID * @param {string} params.toAgentId - Destination agent ID @@ -173,19 +174,37 @@ class AgentService { * @returns {Promise} Transfer result */ async performA2ATransfer({ fromAgentId, toAgentId, amount, currency }) { - // This is a conceptual implementation. - // In a real system, this would involve interaction with the WalletService, - // and policy checks for both agents. + if (this.a2aService) { + return await this.a2aService.executeTransfer({ + fromAgentId, + toAgentId, + amount, + ucpPayload: { currency }, + }); + } + + // Fallback for cases where a2aService is not provided const fromAgent = await this.getAgent(fromAgentId); const toAgent = await this.getAgent(toAgentId); - // Basic policy checks (more complex logic would be here) - if (amount > fromAgent.policy.spendingLimit) { - throw new Error(`Zero Trust Validation Failed: Transfer amount exceeds spending limit for agent ${fromAgentId}`); + // Basic policy checks + const { config } = fromAgent; + const perTransactionLimit = + config?.limits?.perTransaction || this.config.defaultSpendingLimit; + + if (amount > perTransactionLimit) { + throw new Error( + `Zero Trust Validation Failed: Amount ${amount} exceeds agent per-transaction limit of ${perTransactionLimit}`, + ); } - if (!fromAgent.policy.authorizedCounterparties.includes(toAgentId) && - fromAgent.policy.authorizedCounterparties.length > 0) { - throw new Error(`Zero Trust Validation Failed: Agent ${toAgentId} is not an authorized counterparty for ${fromAgentId}`); + + if ( + config?.authorizedCounterparties?.length > 0 && + !config.authorizedCounterparties.includes(toAgentId) + ) { + throw new Error( + `Zero Trust Validation Failed: Agent ${fromAgentId} is not authorized to trade with ${toAgentId}`, + ); } // Simulate transfer success diff --git a/tests/unit/a2a.spec.js b/tests/unit/a2a.spec.js new file mode 100644 index 0000000..4a806a4 --- /dev/null +++ b/tests/unit/a2a.spec.js @@ -0,0 +1,80 @@ +const A2AService = require('../../src/services/a2aService'); + +describe('A2AService', () => { + let a2aService; + let mockWalletService; + let mockDb; + + beforeEach(() => { + mockWalletService = { + transfer: jest.fn(), + }; + mockDb = { + findAgentById: jest.fn(), + }; + a2aService = new A2AService(mockWalletService, mockDb); + }); + + describe('executeTransfer', () => { + it('should successfully execute a transfer between two active agents', async () => { + const fromAgent = { + id: 'agent1', + name: 'Sender', + status: 'active', + walletId: 'wallet1', + config: { limits: { perTransaction: 1000 } } + }; + const toAgent = { + id: 'agent2', + name: 'Recipient', + status: 'active', + walletId: 'wallet2' + }; + + mockDb.findAgentById.mockResolvedValueOnce(fromAgent); + mockDb.findAgentById.mockResolvedValueOnce(toAgent); + mockWalletService.transfer.mockResolvedValue({ transferId: 'tx_123' }); + + const result = await a2aService.executeTransfer({ + fromAgentId: 'agent1', + toAgentId: 'agent2', + amount: 100 + }); + + expect(mockDb.findAgentById).toHaveBeenCalledWith('agent1'); + expect(mockDb.findAgentById).toHaveBeenCalledWith('agent2'); + expect(mockWalletService.transfer).toHaveBeenCalledWith(expect.objectContaining({ + fromWalletId: 'wallet1', + toWalletId: 'wallet2', + amount: 100 + })); + expect(result.success).toBe(true); + expect(result.transferId).toBe('tx_123'); + }); + + it('should throw error if sender agent is not found', async () => { + mockDb.findAgentById.mockResolvedValue(null); + await expect(a2aService.executeTransfer({ + fromAgentId: 'invalid', + toAgentId: 'agent2', + amount: 100 + })).rejects.toThrow('Sender agent invalid not found or inactive'); + }); + + it('should throw error if amount exceeds limit', async () => { + const fromAgent = { + id: 'agent1', + status: 'active', + config: { limits: { perTransaction: 50 } } + }; + mockDb.findAgentById.mockResolvedValueOnce(fromAgent); + mockDb.findAgentById.mockResolvedValueOnce({ status: 'active' }); + + await expect(a2aService.executeTransfer({ + fromAgentId: 'agent1', + toAgentId: 'agent2', + amount: 100 + })).rejects.toThrow('Zero Trust Validation Failed: Amount 100 exceeds agent per-transaction limit of 50'); + }); + }); +}); diff --git a/tests/unit/agent.spec.js b/tests/unit/agent.spec.js index 015a2e4..e7a1e72 100644 --- a/tests/unit/agent.spec.js +++ b/tests/unit/agent.spec.js @@ -3,6 +3,7 @@ const AgentService = require('../../src/services/agent'); describe('AgentService', () => { let agentService; let mockDb; + let mockA2AService; beforeEach(() => { mockDb = { @@ -11,7 +12,10 @@ describe('AgentService', () => { findAllAgents: jest.fn(), updateAgent: jest.fn(), }; - agentService = new AgentService(mockDb); + mockA2AService = { + executeTransfer: jest.fn() + }; + agentService = new AgentService(mockDb, {}, mockA2AService); }); describe('registerAgent', () => { @@ -38,4 +42,67 @@ describe('AgentService', () => { await expect(agentService.getAgent('nonexistent')).rejects.toThrow('Agent not found'); }); }); + + describe('performA2ATransfer', () => { + it('should delegate to a2aService if provided', async () => { + const transferParams = { + fromAgentId: 'agent1', + toAgentId: 'agent2', + amount: 100, + currency: 'USD' + }; + const expectedResult = { success: true, transferId: 'tx123' }; + mockA2AService.executeTransfer.mockResolvedValue(expectedResult); + + const result = await agentService.performA2ATransfer(transferParams); + + expect(mockA2AService.executeTransfer).toHaveBeenCalledWith({ + fromAgentId: 'agent1', + toAgentId: 'agent2', + amount: 100, + ucpPayload: { currency: 'USD' } + }); + expect(result).toEqual(expectedResult); + }); + + it('should fallback to local logic if a2aService is not provided', async () => { + const localAgentService = new AgentService(mockDb); + const fromAgent = { + id: 'agent1', + config: { limits: { perTransaction: 500 }, authorizedCounterparties: ['agent2'] } + }; + const toAgent = { id: 'agent2' }; + + mockDb.findAgentById.mockResolvedValueOnce(fromAgent); + mockDb.findAgentById.mockResolvedValueOnce(toAgent); + + const result = await localAgentService.performA2ATransfer({ + fromAgentId: 'agent1', + toAgentId: 'agent2', + amount: 100, + currency: 'USD' + }); + + expect(result.success).toBe(true); + expect(result.amount).toBe(100); + }); + + it('should throw error in fallback if amount exceeds limit', async () => { + const localAgentService = new AgentService(mockDb); + const fromAgent = { + id: 'agent1', + config: { limits: { perTransaction: 50 } } + }; + + mockDb.findAgentById.mockResolvedValueOnce(fromAgent); + mockDb.findAgentById.mockResolvedValueOnce({ id: 'agent2' }); + + await expect(localAgentService.performA2ATransfer({ + fromAgentId: 'agent1', + toAgentId: 'agent2', + amount: 100, + currency: 'USD' + })).rejects.toThrow('Zero Trust Validation Failed: Amount 100 exceeds agent per-transaction limit of 50'); + }); + }); });