Skip to content

Commit ef19e5f

Browse files
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>
1 parent 99b4242 commit ef19e5f

5 files changed

Lines changed: 181 additions & 16 deletions

File tree

src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ const mobilePaymentService = new MobilePaymentService(
3333
tokenizationService,
3434
walletService,
3535
);
36-
const agentService = new AgentService(db);
3736
const a2aService = new A2AService(walletService, db);
37+
const agentService = new AgentService(db, {}, a2aService);
3838
const ucpService = new UCPService(a2aService);
3939

4040
// Initialize Express app

src/services/a2aService.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
* policy compliance, limit checks, and authorized counterparty validation.
66
*/
77

8-
const { Agent } = require("../models/agent");
98
const logger = require("../utils/logger");
109

1110
class A2AService {
@@ -25,12 +24,12 @@ class A2AService {
2524
async executeTransfer({ fromAgentId, toAgentId, amount, ucpPayload = {} }) {
2625
try {
2726
// 1. Validate Agents
28-
const fromAgent = await Agent.findById(fromAgentId);
27+
const fromAgent = await this.db.findAgentById(fromAgentId);
2928
if (!fromAgent || fromAgent.status !== "active") {
3029
throw new Error(`Sender agent ${fromAgentId} not found or inactive`);
3130
}
3231

33-
const toAgent = await Agent.findById(toAgentId);
32+
const toAgent = await this.db.findAgentById(toAgentId);
3433
if (!toAgent || toAgent.status !== "active") {
3534
throw new Error(`Recipient agent ${toAgentId} not found or inactive`);
3635
}

src/services/agent.js

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ const MandateService = require("./mandate");
1010
const logger = require("../utils/logger");
1111

1212
class AgentService {
13-
constructor(database, config = {}) {
13+
constructor(database, config = {}, a2aService = null) {
1414
this.db = database;
1515
this.config = {
1616
defaultSpendingLimit: config.defaultSpendingLimit || 1000,
1717
defaultAuthorizedCounterparties:
1818
config.defaultAuthorizedCounterparties || [],
1919
};
2020
this.mandateService = new MandateService(config.mandateConfig);
21+
this.a2aService = a2aService;
2122
}
2223

2324
/**
@@ -164,7 +165,7 @@ class AgentService {
164165
}
165166

166167
/**
167-
* Perform an Agent-to-Agent (A2A) transfer (conceptual)
168+
* Perform an Agent-to-Agent (A2A) transfer
168169
* @param {Object} params - Transfer parameters
169170
* @param {string} params.fromAgentId - Source agent ID
170171
* @param {string} params.toAgentId - Destination agent ID
@@ -173,19 +174,37 @@ class AgentService {
173174
* @returns {Promise<Object>} Transfer result
174175
*/
175176
async performA2ATransfer({ fromAgentId, toAgentId, amount, currency }) {
176-
// This is a conceptual implementation.
177-
// In a real system, this would involve interaction with the WalletService,
178-
// and policy checks for both agents.
177+
if (this.a2aService) {
178+
return await this.a2aService.executeTransfer({
179+
fromAgentId,
180+
toAgentId,
181+
amount,
182+
ucpPayload: { currency },
183+
});
184+
}
185+
186+
// Fallback for cases where a2aService is not provided
179187
const fromAgent = await this.getAgent(fromAgentId);
180188
const toAgent = await this.getAgent(toAgentId);
181189

182-
// Basic policy checks (more complex logic would be here)
183-
if (amount > fromAgent.policy.spendingLimit) {
184-
throw new Error(`Zero Trust Validation Failed: Transfer amount exceeds spending limit for agent ${fromAgentId}`);
190+
// Basic policy checks
191+
const { config } = fromAgent;
192+
const perTransactionLimit =
193+
config?.limits?.perTransaction || this.config.defaultSpendingLimit;
194+
195+
if (amount > perTransactionLimit) {
196+
throw new Error(
197+
`Zero Trust Validation Failed: Amount ${amount} exceeds agent per-transaction limit of ${perTransactionLimit}`,
198+
);
185199
}
186-
if (!fromAgent.policy.authorizedCounterparties.includes(toAgentId) &&
187-
fromAgent.policy.authorizedCounterparties.length > 0) {
188-
throw new Error(`Zero Trust Validation Failed: Agent ${toAgentId} is not an authorized counterparty for ${fromAgentId}`);
200+
201+
if (
202+
config?.authorizedCounterparties?.length > 0 &&
203+
!config.authorizedCounterparties.includes(toAgentId)
204+
) {
205+
throw new Error(
206+
`Zero Trust Validation Failed: Agent ${fromAgentId} is not authorized to trade with ${toAgentId}`,
207+
);
189208
}
190209

191210
// Simulate transfer success

tests/unit/a2a.spec.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const A2AService = require('../../src/services/a2aService');
2+
3+
describe('A2AService', () => {
4+
let a2aService;
5+
let mockWalletService;
6+
let mockDb;
7+
8+
beforeEach(() => {
9+
mockWalletService = {
10+
transfer: jest.fn(),
11+
};
12+
mockDb = {
13+
findAgentById: jest.fn(),
14+
};
15+
a2aService = new A2AService(mockWalletService, mockDb);
16+
});
17+
18+
describe('executeTransfer', () => {
19+
it('should successfully execute a transfer between two active agents', async () => {
20+
const fromAgent = {
21+
id: 'agent1',
22+
name: 'Sender',
23+
status: 'active',
24+
walletId: 'wallet1',
25+
config: { limits: { perTransaction: 1000 } }
26+
};
27+
const toAgent = {
28+
id: 'agent2',
29+
name: 'Recipient',
30+
status: 'active',
31+
walletId: 'wallet2'
32+
};
33+
34+
mockDb.findAgentById.mockResolvedValueOnce(fromAgent);
35+
mockDb.findAgentById.mockResolvedValueOnce(toAgent);
36+
mockWalletService.transfer.mockResolvedValue({ transferId: 'tx_123' });
37+
38+
const result = await a2aService.executeTransfer({
39+
fromAgentId: 'agent1',
40+
toAgentId: 'agent2',
41+
amount: 100
42+
});
43+
44+
expect(mockDb.findAgentById).toHaveBeenCalledWith('agent1');
45+
expect(mockDb.findAgentById).toHaveBeenCalledWith('agent2');
46+
expect(mockWalletService.transfer).toHaveBeenCalledWith(expect.objectContaining({
47+
fromWalletId: 'wallet1',
48+
toWalletId: 'wallet2',
49+
amount: 100
50+
}));
51+
expect(result.success).toBe(true);
52+
expect(result.transferId).toBe('tx_123');
53+
});
54+
55+
it('should throw error if sender agent is not found', async () => {
56+
mockDb.findAgentById.mockResolvedValue(null);
57+
await expect(a2aService.executeTransfer({
58+
fromAgentId: 'invalid',
59+
toAgentId: 'agent2',
60+
amount: 100
61+
})).rejects.toThrow('Sender agent invalid not found or inactive');
62+
});
63+
64+
it('should throw error if amount exceeds limit', async () => {
65+
const fromAgent = {
66+
id: 'agent1',
67+
status: 'active',
68+
config: { limits: { perTransaction: 50 } }
69+
};
70+
mockDb.findAgentById.mockResolvedValueOnce(fromAgent);
71+
mockDb.findAgentById.mockResolvedValueOnce({ status: 'active' });
72+
73+
await expect(a2aService.executeTransfer({
74+
fromAgentId: 'agent1',
75+
toAgentId: 'agent2',
76+
amount: 100
77+
})).rejects.toThrow('Zero Trust Validation Failed: Amount 100 exceeds agent per-transaction limit of 50');
78+
});
79+
});
80+
});

tests/unit/agent.spec.js

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const AgentService = require('../../src/services/agent');
33
describe('AgentService', () => {
44
let agentService;
55
let mockDb;
6+
let mockA2AService;
67

78
beforeEach(() => {
89
mockDb = {
@@ -11,7 +12,10 @@ describe('AgentService', () => {
1112
findAllAgents: jest.fn(),
1213
updateAgent: jest.fn(),
1314
};
14-
agentService = new AgentService(mockDb);
15+
mockA2AService = {
16+
executeTransfer: jest.fn()
17+
};
18+
agentService = new AgentService(mockDb, {}, mockA2AService);
1519
});
1620

1721
describe('registerAgent', () => {
@@ -38,4 +42,67 @@ describe('AgentService', () => {
3842
await expect(agentService.getAgent('nonexistent')).rejects.toThrow('Agent not found');
3943
});
4044
});
45+
46+
describe('performA2ATransfer', () => {
47+
it('should delegate to a2aService if provided', async () => {
48+
const transferParams = {
49+
fromAgentId: 'agent1',
50+
toAgentId: 'agent2',
51+
amount: 100,
52+
currency: 'USD'
53+
};
54+
const expectedResult = { success: true, transferId: 'tx123' };
55+
mockA2AService.executeTransfer.mockResolvedValue(expectedResult);
56+
57+
const result = await agentService.performA2ATransfer(transferParams);
58+
59+
expect(mockA2AService.executeTransfer).toHaveBeenCalledWith({
60+
fromAgentId: 'agent1',
61+
toAgentId: 'agent2',
62+
amount: 100,
63+
ucpPayload: { currency: 'USD' }
64+
});
65+
expect(result).toEqual(expectedResult);
66+
});
67+
68+
it('should fallback to local logic if a2aService is not provided', async () => {
69+
const localAgentService = new AgentService(mockDb);
70+
const fromAgent = {
71+
id: 'agent1',
72+
config: { limits: { perTransaction: 500 }, authorizedCounterparties: ['agent2'] }
73+
};
74+
const toAgent = { id: 'agent2' };
75+
76+
mockDb.findAgentById.mockResolvedValueOnce(fromAgent);
77+
mockDb.findAgentById.mockResolvedValueOnce(toAgent);
78+
79+
const result = await localAgentService.performA2ATransfer({
80+
fromAgentId: 'agent1',
81+
toAgentId: 'agent2',
82+
amount: 100,
83+
currency: 'USD'
84+
});
85+
86+
expect(result.success).toBe(true);
87+
expect(result.amount).toBe(100);
88+
});
89+
90+
it('should throw error in fallback if amount exceeds limit', async () => {
91+
const localAgentService = new AgentService(mockDb);
92+
const fromAgent = {
93+
id: 'agent1',
94+
config: { limits: { perTransaction: 50 } }
95+
};
96+
97+
mockDb.findAgentById.mockResolvedValueOnce(fromAgent);
98+
mockDb.findAgentById.mockResolvedValueOnce({ id: 'agent2' });
99+
100+
await expect(localAgentService.performA2ATransfer({
101+
fromAgentId: 'agent1',
102+
toAgentId: 'agent2',
103+
amount: 100,
104+
currency: 'USD'
105+
})).rejects.toThrow('Zero Trust Validation Failed: Amount 100 exceeds agent per-transaction limit of 50');
106+
});
107+
});
41108
});

0 commit comments

Comments
 (0)