Skip to content

Commit 0d11f38

Browse files
authored
Merge branch 'main' into feature/a2a-zero-trust-modernization-1076727711116180867
2 parents ef19e5f + 28ce628 commit 0d11f38

12 files changed

Lines changed: 390 additions & 120 deletions

File tree

scripts/ocp-cli.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ program.command('x402:settle')
154154
return;
155155
}
156156
} else if (process.env.STRICT_MANDATE_MODE === 'true') {
157-
console.error(`Error: Mandate required for x402 settlement in STRICT_MANDATE_MODE.`);
157+
console.error(`Zero Trust Validation Failed: Mandate required for x402 settlement in STRICT_MANDATE_MODE.`);
158158
return;
159159
}
160160

scripts/verify_ucp_flow.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class DBAdapter {
1919
async createWallet(data) { return Wallet.create(data); }
2020
async createTransaction(data) { return Transaction.create(data); }
2121
async updateTransaction(id, data) { return Transaction.findByIdAndUpdate(id, data, { new: true }); }
22+
async findAgentById(id) { return Agent.findById(id); }
2223

2324
async updateWalletBalance(walletId, amount) {
2425
return Wallet.findByIdAndUpdate(walletId, { $inc: { balance: amount } }, { new: true });

src/index.js

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

4040
// Initialize Express app

src/services/a2aService.js

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@
55
* policy compliance, limit checks, and authorized counterparty validation.
66
*/
77

8+
const MandateService = require("./mandate");
89
const logger = require("../utils/logger");
10+
const MandateService = require("./mandate");
911

1012
class A2AService {
11-
constructor(walletService, db) {
13+
constructor(walletService, db, config = {}) {
1214
this.walletService = walletService;
1315
this.db = db;
16+
this.mandateService = config.mandateService || new MandateService(config.mandateConfig);
17+
this.strictMandateMode =
18+
config.strictMandateMode !== undefined
19+
? config.strictMandateMode
20+
: process.env.STRICT_MANDATE_MODE === "true";
1421
}
1522

1623
/**
@@ -19,25 +26,64 @@ class A2AService {
1926
* @param {string} params.fromAgentId - Sender Agent ID
2027
* @param {string} params.toAgentId - Recipient Agent ID
2128
* @param {number} params.amount - Amount to transfer
29+
* @param {string} params.mandate - Optional signed Mandate (AP2) for Zero Trust validation
2230
* @param {Object} params.ucpPayload - The original UCP intent/payload
31+
* @param {string} params.mandate - Optional signed Mandate (AP2) for Zero Trust validation
2332
*/
24-
async executeTransfer({ fromAgentId, toAgentId, amount, ucpPayload = {} }) {
33+
async executeTransfer({
34+
fromAgentId,
35+
toAgentId,
36+
amount,
37+
mandate,
38+
ucpPayload = {},
39+
}) {
2540
try {
26-
// 1. Validate Agents
41+
// 0. Zero Trust Mandate Validation
42+
if (mandate) {
43+
try {
44+
await this.mandateService.verifyMandate(mandate);
45+
} catch (error) {
46+
throw new Error(
47+
`Zero Trust Validation Failed: Mandate verification failed: ${error.message}`,
48+
);
49+
}
50+
} else if (this.strictMandateMode) {
51+
throw new Error(
52+
"Zero Trust Validation Failed: Mandate required for A2A transfer in strict mode",
53+
);
54+
}
55+
56+
// 1. Validate Agents using Repository Pattern
2757
const fromAgent = await this.db.findAgentById(fromAgentId);
2858
if (!fromAgent || fromAgent.status !== "active") {
29-
throw new Error(`Sender agent ${fromAgentId} not found or inactive`);
59+
throw new Error(
60+
`Zero Trust Validation Failed: Sender agent ${fromAgentId} not found or inactive`,
61+
);
3062
}
3163

3264
const toAgent = await this.db.findAgentById(toAgentId);
3365
if (!toAgent || toAgent.status !== "active") {
34-
throw new Error(`Recipient agent ${toAgentId} not found or inactive`);
66+
throw new Error(
67+
`Zero Trust Validation Failed: Recipient agent ${toAgentId} not found or inactive`,
68+
);
69+
}
70+
71+
// 2. Zero Trust Mandate Validation
72+
if (mandate) {
73+
await this.mandateService.verifyMandate(mandate, {
74+
amount,
75+
recipient: toAgentId,
76+
});
77+
} else if (this.strictMandateMode) {
78+
throw new Error(
79+
"Zero Trust Validation Failed: Mandate required for A2A transfer in strict mode",
80+
);
3581
}
3682

37-
// 2. Policy Checks (Sender)
83+
// 3. Policy Checks (Sender)
3884
await this._validateAgentPolicy(fromAgent, toAgentId, amount);
3985

40-
// 3. Execute Wallet Transfer
86+
// 4. Execute Wallet Transfer
4187
const transferResult = await this.walletService.transfer({
4288
fromWalletId: fromAgent.walletId,
4389
toWalletId: toAgent.walletId,
@@ -49,13 +95,10 @@ class A2AService {
4995
counterpartyAgentId: toAgentId,
5096
ucpPayload,
5197
type: "a2a_transfer",
98+
mandate,
5299
},
53100
});
54101

55-
// 4. Update Agent Usage (if we were tracking daily usage in db, we'd do it here)
56-
// For now, limits are stateless checks against config.
57-
// In a real implementation, we would query daily volume or update a usage record.
58-
59102
return {
60103
success: true,
61104
transferId: transferResult.transferId,
@@ -106,6 +149,15 @@ class A2AService {
106149
*/
107150
_handleError(method, error) {
108151
logger.error(`A2AService.${method} error:`, error);
152+
153+
// Normalize Zero Trust errors
154+
if (
155+
error.message &&
156+
error.message.includes("Zero Trust Validation Failed:")
157+
) {
158+
return error;
159+
}
160+
109161
return error instanceof Error ? error : new Error(error);
110162
}
111163
}

src/services/agent.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class AgentService {
7373
try {
7474
const agent = await this.db.findAgentById(agentId);
7575
if (!agent) {
76-
throw new Error("Agent not found");
76+
throw new Error("Zero Trust Validation Failed: Agent not found");
7777
}
7878
return agent;
7979
} catch (error) {
@@ -185,25 +185,26 @@ class AgentService {
185185

186186
// Fallback for cases where a2aService is not provided
187187
const fromAgent = await this.getAgent(fromAgentId);
188-
const toAgent = await this.getAgent(toAgentId);
188+
await this.getAgent(toAgentId); // Verify toAgent exists
189189

190190
// Basic policy checks
191-
const { config } = fromAgent;
192-
const perTransactionLimit =
193-
config?.limits?.perTransaction || this.config.defaultSpendingLimit;
191+
const config = fromAgent.config || {};
192+
const limits = config.limits || {};
193+
const perTransactionLimit = limits.perTransaction || 0;
194194

195-
if (amount > perTransactionLimit) {
195+
if (perTransactionLimit > 0 && amount > perTransactionLimit) {
196196
throw new Error(
197-
`Zero Trust Validation Failed: Amount ${amount} exceeds agent per-transaction limit of ${perTransactionLimit}`,
197+
`Zero Trust Validation Failed: Transfer amount ${amount} exceeds per-transaction limit of ${perTransactionLimit} for agent ${fromAgentId}`,
198198
);
199199
}
200200

201+
const authorizedCounterparties = config.authorizedCounterparties || [];
201202
if (
202-
config?.authorizedCounterparties?.length > 0 &&
203-
!config.authorizedCounterparties.includes(toAgentId)
203+
authorizedCounterparties.length > 0 &&
204+
!authorizedCounterparties.includes(toAgentId)
204205
) {
205206
throw new Error(
206-
`Zero Trust Validation Failed: Agent ${fromAgentId} is not authorized to trade with ${toAgentId}`,
207+
`Zero Trust Validation Failed: Agent ${toAgentId} is not an authorized counterparty for ${fromAgentId}`,
207208
);
208209
}
209210

src/services/mandate.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,42 @@ class MandateService {
105105
/**
106106
* Verify a Mandate (Intent or Cart)
107107
* @param {string} token - Signed JWT Mandate
108+
* @param {Object} context - Optional context for validation (amount, recipient)
108109
* @returns {Promise<Object>} Decoded mandate payload
109110
*/
110-
async verifyMandate(token) {
111+
async verifyMandate(token, context = {}) {
112+
let decoded;
111113
try {
112-
return jwt.verify(token, this.signingKey, { algorithms: ["HS256"] });
114+
decoded = jwt.verify(token, this.signingKey, { algorithms: ["HS256"] });
113115
} catch (error) {
114-
throw new Error(`Zero Trust Validation Failed: Mandate verification failed: ${error.message}`);
116+
throw new Error(
117+
`Zero Trust Validation Failed: Mandate verification failed: ${error.message}`,
118+
);
115119
}
120+
121+
// Contextual Validation
122+
if (context.amount) {
123+
if (decoded.max_budget && context.amount > decoded.max_budget.value) {
124+
throw new Error(
125+
`Zero Trust Validation Failed: Amount ${context.amount} exceeds mandate budget of ${decoded.max_budget.value}`,
126+
);
127+
}
128+
if (decoded.total_price && context.amount !== decoded.total_price) {
129+
throw new Error(
130+
`Zero Trust Validation Failed: Amount ${context.amount} does not match cart mandate total of ${decoded.total_price}`,
131+
);
132+
}
133+
}
134+
135+
if (context.recipient && decoded.allowed_merchants?.length > 0) {
136+
if (!decoded.allowed_merchants.includes(context.recipient)) {
137+
throw new Error(
138+
`Zero Trust Validation Failed: Recipient ${context.recipient} not authorized by mandate`,
139+
);
140+
}
141+
}
142+
143+
return decoded;
116144
}
117145

118146
/**

src/services/tokenization.js

Lines changed: 18 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -331,53 +331,20 @@ class TokenizationService {
331331
async signWithToken(tokenId, dataToSign, mandate, context = {}) {
332332
// Zero Trust Validation: Verify mandate BEFORE entering try/catch simulation block
333333
if (mandate) {
334-
let decodedMandate;
335334
try {
336-
decodedMandate = await this.mandateService.verifyMandate(mandate);
335+
// Normalize context for MandateService (merchant -> recipient)
336+
const validationContext = {
337+
...context,
338+
recipient: context.recipient || context.merchant,
339+
};
340+
await this.mandateService.verifyMandate(mandate, validationContext);
337341
} catch (error) {
338-
if (error.message?.includes("jwt expired")) {
339-
throw new Error("Zero Trust Validation Failed: Mandate has expired");
340-
}
341342
if (error.message?.includes("Zero Trust Validation Failed:")) {
342343
throw error;
343344
}
344-
throw new Error(`Zero Trust Validation Failed: ${error.message || error}`);
345-
}
346-
347-
// Validate budget if context amount is provided
348-
if (context.amount) {
349-
// Check Intent Mandate budget
350-
if (
351-
decodedMandate.max_budget &&
352-
context.amount > decodedMandate.max_budget.value
353-
) {
354-
throw new Error(
355-
`Zero Trust Validation Failed: Amount ${context.amount} exceeds mandate budget of ${decodedMandate.max_budget.value}`,
356-
);
357-
}
358-
// Check Cart Mandate total price
359-
if (
360-
decodedMandate.total_price &&
361-
context.amount !== decodedMandate.total_price
362-
) {
363-
throw new Error(
364-
`Zero Trust Validation Failed: Amount ${context.amount} does not match cart mandate total of ${decodedMandate.total_price}`,
365-
);
366-
}
367-
}
368-
369-
// Validate merchant if context merchant is provided
370-
if (context.merchant && decodedMandate.allowed_merchants?.length > 0) {
371-
if (!decodedMandate.allowed_merchants.includes(context.merchant)) {
372-
throw new Error(
373-
`Zero Trust Validation Failed: Merchant ${context.merchant} not authorized by mandate`,
374-
);
375-
}
376-
}
377-
378-
// Validate expiration
379-
if (decodedMandate.exp < Math.floor(Date.now() / 1000)) {
380-
throw new Error("Zero Trust Validation Failed: Mandate has expired");
345+
throw new Error(
346+
`Zero Trust Validation Failed: ${error.message || error}`,
347+
);
381348
}
382349
} else if (this.strictMandateMode) {
383350
throw new Error(
@@ -415,6 +382,14 @@ class TokenizationService {
415382
_handleError(method, error) {
416383
logger.error(`TokenizationService.${method} error:`, error);
417384

385+
// Normalize Zero Trust errors
386+
if (
387+
error.message &&
388+
error.message.includes("Zero Trust Validation Failed:")
389+
) {
390+
return error;
391+
}
392+
418393
if (error.response) {
419394
const { status, data } = error.response;
420395
return new Error(
@@ -426,7 +401,7 @@ class TokenizationService {
426401
return new Error("Tokenization service unavailable");
427402
}
428403

429-
return error;
404+
return error instanceof Error ? error : new Error(error);
430405
}
431406
}
432407

src/services/wallet.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class WalletService {
3636
// Check if user already has a wallet
3737
const existingWallet = await this.db.findWalletByUserId(userId);
3838
if (existingWallet) {
39-
throw new Error("User already has a wallet");
39+
throw new Error("Zero Trust Validation Failed: User already has a wallet");
4040
}
4141

4242
// Validate initial balance
@@ -91,7 +91,7 @@ class WalletService {
9191
try {
9292
const wallet = await this.db.findWalletById(walletId);
9393
if (!wallet) {
94-
throw new Error("Wallet not found");
94+
throw new Error("Zero Trust Validation Failed: Wallet not found");
9595
}
9696
return wallet;
9797
} catch (error) {
@@ -108,7 +108,7 @@ class WalletService {
108108
try {
109109
const wallet = await this.db.findWalletByUserId(userId);
110110
if (!wallet) {
111-
throw new Error("Wallet not found for user");
111+
throw new Error("Zero Trust Validation Failed: Wallet not found for user");
112112
}
113113
return wallet;
114114
} catch (error) {
@@ -142,7 +142,7 @@ class WalletService {
142142
// Get wallet
143143
const wallet = await this.getWallet(walletId);
144144
if (wallet.status !== "active") {
145-
throw new Error("Wallet is not active");
145+
throw new Error("Zero Trust Validation Failed: Wallet is not active");
146146
}
147147

148148
// Check if new balance would exceed max
@@ -215,12 +215,12 @@ class WalletService {
215215
// Get wallet
216216
const wallet = await this.getWallet(walletId);
217217
if (wallet.status !== "active") {
218-
throw new Error("Wallet is not active");
218+
throw new Error("Zero Trust Validation Failed: Wallet is not active");
219219
}
220220

221221
// Check sufficient balance
222222
if (wallet.balance < amount) {
223-
throw new Error("Insufficient balance");
223+
throw new Error("Zero Trust Validation Failed: Insufficient balance");
224224
}
225225

226226
const newBalance = wallet.balance - amount;
@@ -297,7 +297,7 @@ class WalletService {
297297
}
298298

299299
if (fromWalletId === toWalletId) {
300-
throw new Error("Cannot transfer to same wallet");
300+
throw new Error("Zero Trust Validation Failed: Cannot transfer to same wallet");
301301
}
302302

303303
// Start transaction

0 commit comments

Comments
 (0)