-
-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathreputation-gate.js
More file actions
118 lines (110 loc) · 4.66 KB
/
Copy pathreputation-gate.js
File metadata and controls
118 lines (110 loc) · 4.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// Reputation gate — refuse to pay a peer agent whose on-chain ERC-8004
// reputation is below the caller's threshold.
//
// Trust is the missing half of autonomous payments: budget caps stop an agent
// overspending, but they don't stop it paying a scammer. ERC-8004's Reputation
// Registry is the standard EVM trust signal; this module reads it server-side
// and enforces a minimum average score and/or minimum review count before a
// mandate-authorized payment is allowed to proceed. (Its Solana counterpart —
// vetting a three.ws agent by its on-chain Solana track record — is
// ../trust/solana-bouncer.js, exposed publicly at /api/x402/agent-bouncer.)
//
// The read goes through the curated multi-RPC failover in api/_lib/evm/rpc.js
// (honoring A2A_REPUTATION_RPC_URL as the pinned primary when set), so the gate
// works out of the box without per-deploy RPC config. The reader is injectable
// (`read` option) so tests run without RPC. The gate is a no-op when no
// threshold is set; when a threshold IS set and the read fails, it fails closed.
import { Contract } from 'ethers';
import { env } from '../env.js';
import { evmFallbackProvider } from '../evm/rpc.js';
import { REGISTRY_DEPLOYMENTS, REPUTATION_REGISTRY_ABI } from '../../../src/erc8004/abi.js';
export class ReputationError extends Error {
constructor(code, message, status = 403) {
super(message);
this.name = 'ReputationError';
this.code = code;
this.status = status;
}
}
/**
* Read aggregated ERC-8004 reputation for an agent. getReputation returns
* (int256 avgX100, uint256 count): the average ALREADY multiplied by 100, signed
* so reputation can be negative. average = avgX100 / 100 — never divided by count
* again (the prior bug here divided the already-averaged value, understating
* every score by a factor of count and mis-decoding negatives as huge positives).
*
* @param {object} opts
* @param {number|bigint|string} opts.agentId
* @param {number} opts.chainId
* @param {string} [opts.rpcUrl] Pinned first; otherwise env + curated public failover.
* @returns {Promise<{ average: number, count: number }>}
*/
export async function readReputationOnchain({ agentId, chainId, rpcUrl }) {
const deployment = REGISTRY_DEPLOYMENTS[chainId];
if (!deployment?.reputationRegistry) {
throw new ReputationError(
'reputation_registry_missing',
`no Reputation Registry deployed on chain ${chainId}`,
500,
);
}
const provider = await evmFallbackProvider(chainId, {
primaryUrl: rpcUrl || env.A2A_REPUTATION_RPC_URL || null,
});
const contract = new Contract(deployment.reputationRegistry, REPUTATION_REGISTRY_ABI, provider);
const [avgX100, count] = await contract.getReputation(agentId);
const n = Number(count);
return { average: n === 0 ? 0 : Number(avgX100) / 100, count: n };
}
/**
* Assert a peer agent meets the reputation bar. Throws ReputationError when it
* doesn't. A no-op when neither a minimum average nor minimum count is set.
*
* @param {object} opts
* @param {number|bigint|string} [opts.agentId] On-chain agentId of the peer.
* @param {number} [opts.chainId]
* @param {number} [opts.minAverage=0] Required average score.
* @param {number} [opts.minCount=0] Required number of reviews.
* @param {string} [opts.rpcUrl]
* @param {(o:object)=>Promise<{average:number,count:number}>} [opts.read] Injectable reader.
* @returns {Promise<{ average: number, count: number } | null>} The reputation read, or null when gating was skipped.
*/
export async function assertReputationOk({
agentId,
chainId,
minAverage = 0,
minCount = 0,
rpcUrl,
read = readReputationOnchain,
}) {
const gated = minAverage > 0 || minCount > 0;
if (!gated) return null; // No threshold requested — nothing to enforce.
if (agentId === undefined || agentId === null || agentId === '') {
throw new ReputationError(
'reputation_required',
'a reputation threshold was set but no peer agentId was provided to evaluate',
);
}
let rep;
try {
rep = await read({ agentId, chainId, rpcUrl });
} catch (err) {
if (err instanceof ReputationError) throw err;
// Reader blew up (RPC down, etc). Fail closed — we cannot prove the peer is
// trustworthy, and the caller explicitly asked us to gate on trust.
throw new ReputationError('reputation_unavailable', `reputation read failed: ${err.message}`, 502);
}
if (minCount > 0 && rep.count < minCount) {
throw new ReputationError(
'reputation_too_few_reviews',
`peer has ${rep.count} review(s); ${minCount} required`,
);
}
if (minAverage > 0 && rep.average < minAverage) {
throw new ReputationError(
'reputation_too_low',
`peer average ${rep.average.toFixed(2)} is below required ${minAverage}`,
);
}
return rep;
}