aere-quantum/agents/x402/control-negativ-wallet.mjs
Aere Network 2293c1da86 agents/x402: an agent wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet
The agent does not hold the payment key: the wallet holds it for the owner and signs an EIP-3009 authorization only for a payment the
agent wrote into its signed ledger, verified without trusting the agent under the policy the owner pinned and against the ledger heads
the wallet itself saw (a branch is refused with a proof of equivocation, a backdated entry is refused), naming exactly this purchase,
written now, and within the limit judged also against what the wallet itself has signed. The authorization nonce is sha256(entry hash),
so the on-chain payment names the ledger entry. The policy gains an optional `wallet` field. Also: an x402 v2 client, a minimal resource
server, a local facilitator for tests, and verifica-plati.mjs, which proves from outside that a wallet's on-chain payments were allowed
by the agent's policy (with --all-transfers, that no payment left the wallet without a ledger entry).

Tests: wallet 25/25 and payment verifier 10/10 without a network (the verifier on chain responses recorded on testnet 28001), negative
control 21/21; policy 27/27, agents control 26/26. On the public testnet 28001 through its x402 facilitator: 9/9, with the evidence in
agents/x402/dovezi-28001/. Needs ethers (npm install in agents/x402).
2026-09-30 00:01:34 +03:00

89 lines
8.8 KiB
JavaScript

// Controlul negativ al portofelului de plati x402 (proba-wallet.mjs) si al verificatorului de plati (proba-verifica-plati.mjs): fiecare paznic se strica intr-o COPIE (agent-policy/*.mjs si
// x402/*.mjs, in aceeasi asezare), proba ruleaza pe copie si trebuie sa iasa rosie EXACT pe verificarea numita, cu proba chiar rulata.
// Plantarile sunt conditii false la rulare sau randuri scoase, deci copia se incarca intotdeauna; o ancora care nu apare exact o data,
// sau o proba care nu ajunge la rezumat, e un esec al controlului (STRICAT), nu o linie informativa.
// node control-negativ-wallet.mjs iesire 0 = martorul verde si toate plantarile rosii pe proba lor
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const SUS = path.resolve(AICI, '..');
// ethers se rezolva de langa original (copia nu are node_modules); calea se da copiei prin AERE_ETHERS
let ETHERS = process.env.AERE_ETHERS || null; // o copie a acestui dosar (controlul portii) primeste calea din mediu
if (!ETHERS) try { ETHERS = createRequire(path.join(AICI, 'wallet.mjs')).resolve('ethers'); }
catch { try { ETHERS = createRequire(path.join(AICI, 'wallet.mjs')).resolve(path.resolve(SUS, '..', '..', 'contracts', 'node_modules', 'ethers')); } catch { console.log('NEMASURAT: ethers nu se gaseste (npm install aici, sau AERE_ETHERS=<cale>)'); process.exit(2); } }
const V = 'proba-verifica-plati.mjs';
const PLANTARI = [
// [nume, fisier (relativ la x402/), tipar, inlocuire, textul verificarii care trebuie sa iasa rosie]
['cererile nu mai sunt judecate una cate una', 'wallet.mjs', 'function authorize(x) { const r = coada.then(() => autorizeaza(x)); coada = r.catch(() => {}); return r; }', 'function authorize(x) { return autorizeaza(x); }', '12. ATAC'],
['registrul nu mai trebuie sa verifice', 'wallet.mjs', ' if (!v.ok) {', " if (!v.ok && process.env.AERE_PLANTA_NICIODATA === 'da') {", '3. ATAC'],
['intrarea nu mai trebuie sa numeasca aceasta cumparatura', 'wallet.mjs', "for (const k of ['from', 'to', 'amount', 'asset', 'ref']) if (", 'for (const k of []) if (', '4. ATAC'],
['aceeasi intrare poate plati de doua ori', 'wallet.mjs', 'if (S.last && e.seq <= S.last.seq) return refuz(', "if (S.last && e.seq <= S.last.seq && process.env.AERE_PLANTA_NICIODATA === 'da') return refuz(", '5. ATAC'],
['intrarea nu mai trebuie sa fie de acum', 'wallet.mjs', 'if (!(Number(b.at) >= t - TOL && Number(b.at) <= t + 5)) return', "if (!(Number(b.at) >= t - TOL && Number(b.at) <= t + 5) && process.env.AERE_PLANTA_NICIODATA === 'da') return", '10. ATAC'],
['limita nu mai e judecata fata de ce a semnat portofelul', 'wallet.mjs', 'stare.signed.map((s) => ({ amount: s.amount, at: s.at }))', '[]', '7. politica noua'],
['aprobarile intrarii nu mai sunt date limitei portofelului', 'wallet.mjs', 'if (r.ok) aprobatori.push(r.humanId); }', '}', '9. 30000'],
['dovada de echivocare nu mai e data', 'wallet.mjs', 'if (p.agentId === policy.agentId && verifyEquivocation(p).ok) r.equivocation = p;', '', '6. si portofelul da dovada'],
['nonce-ul autorizarii nu mai numeste intrarea', 'wallet.mjs', 'nonce: nonceForEntry(e.hash) };', "nonce: '0x' + crypto.randomBytes(32).toString('hex') };", '1. nonce-ul fiecarei'],
['revocarile primite nu mai ajung la verificarea registrului', 'wallet.mjs', 'revocations: stare.revocations, anchors', 'revocations: [], anchors', '8. dupa revocare'],
['serverul de resurse primeste o plata pentru alta cerinta', 'resource-server.mjs', 'if (cheie(payload.accepted) !== cheie(requirement)) return cere(', "if (cheie(payload.accepted) !== cheie(requirement) && process.env.AERE_PLANTA_NICIODATA === 'da') return cere(", '11. CONTROL'],
['clientul cere semnatura si cand politica a refuzat', 'client.mjs', "if (!r.allowed) return { paid: false, status: 402, reason: `the agent's policy refused the payment: ${r.reason}`, entry: r.entry };", '', '2. CONTROL'],
['portofelul porneste si fara limita in activul lui', 'wallet.mjs', 'if (!policy.spend || policy.spend.asset !== assetId(network, token)) throw', "if ((!policy.spend || policy.spend.asset !== assetId(network, token)) && process.env.AERE_PLANTA_NICIODATA === 'da') throw", 'o politica fara limita'],
// verificatorul platilor, pe raspunsurile inregistrate de pe 28001
['verificatorul nu mai cere ca registrul sa verifice', 'verifica-plati.mjs', 'entries)`, v.ok, v.error', 'entries)`, true, v.error', '2. CONTROL: o suma schimbata', V],
['verificatorul nu mai cere ca intrarea platii sa fie in registru', 'verifica-plati.mjs', '!!e && e.hash === p.entryHash)', '!!e)', '3. CONTROL', V],
['verificatorul nu mai cere portofelul in intrare', 'verifica-plati.mjs', "String(a.from).toLowerCase() === wallet && ", '', '4. CONTROL', V],
['verificatorul nu mai cere status 1', 'verifica-plati.mjs', "!!rc && rc.status === '0x1'", '!!rc', '5. CONTROL', V],
['verificatorul nu mai cere nonce-ul intrarii pe lant', 'verifica-plati.mjs', '&& l.topics[2].toLowerCase() === nonce));', '));', '6. CONTROL', V],
['verificatorul nu mai cere suma din Transfer', 'verifica-plati.mjs', '&& BigInt(l.data) === BigInt(a.amount)));', '));', '7. CONTROL', V],
['verificatorul nu mai cere ca orice Transfer sa fie in registru', 'verifica-plati.mjs', 'straine.length === 0,', 'true,', '8. CONTROL', V],
['verificatorul nu mai cere lantul dosarului', 'verifica-plati.mjs', 'lantul === chain,', 'true,', '9. CONTROL', V],
];
function copie() {
const t = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-wallet-ctl-'));
for (const f of fs.readdirSync(SUS).filter((n) => n.endsWith('.mjs'))) fs.copyFileSync(path.join(SUS, f), path.join(t, f));
fs.mkdirSync(path.join(t, 'x402'));
for (const f of fs.readdirSync(AICI).filter((n) => n.endsWith('.mjs'))) fs.copyFileSync(path.join(AICI, f), path.join(t, 'x402', f));
fs.cpSync(path.join(AICI, 'dovezi-28001'), path.join(t, 'x402', 'dovezi-28001'), { recursive: true });
return t;
}
// probele ruleaza cate PARALEL deodata (asincron), ca tot controlul sa incapa in termenul rulatorului comun
const PARALEL = 4;
function ruleaza(t, proba = 'proba-wallet.mjs') {
return new Promise((resolve) => {
const c = spawn(process.execPath, [path.join(t, 'x402', proba)], { env: { ...process.env, AERE_ETHERS: ETHERS } }); let out = '';
const ceas = setTimeout(() => c.kill(), 240000);
c.stdout.on('data', (x) => { out += x; }); c.stderr.on('data', (x) => { out += x; });
c.on('close', (cod) => { clearTimeout(ceas); resolve({ cod, rulat: /(agent-wallet|verifica-plati): \d+\/\d+/.test(out), rosii: out.split('\n').filter((l) => /^\s+RAU\s/.test(l)) }); });
});
}
async function inGrup(lucrari) { const rez = new Array(lucrari.length); let i = 0;
await Promise.all(Array.from({ length: PARALEL }, async () => { while (i < lucrari.length) { const k = i++; rez[k] = await lucrari[k](); } })); return rez; }
let esecuri = 0;
const martori = await inGrup(['proba-wallet.mjs', V].map((proba) => async () => { const t0 = copie(); try { return [proba, await ruleaza(t0, proba)]; } finally { fs.rmSync(t0, { recursive: true, force: true }); } }));
for (const [proba, m0] of martori) {
if (m0.cod === 0 && m0.rulat && !m0.rosii.length) console.log(` OK martorul ${proba}: copia neatinsa verde`);
else { esecuri++; console.log(` RAU martorul ${proba} nu e verde (cod ${m0.cod}, ${m0.rulat ? m0.rosii.length + ' RAU' : 'nu a ajuns la rezumat'})`); }
}
const linii = await inGrup(PLANTARI.map(([nume, f, din, inl, tinta, proba = 'proba-wallet.mjs']) => async () => {
let t = null;
try {
t = copie(); const fp = path.join(t, 'x402', f);
if (!fs.existsSync(fp)) return [false, ` RAU ${nume}: plantarea numeste un fisier care nu exista (${f})`];
const src = fs.readFileSync(fp, 'utf8');
if (src.split(din).length !== 2) return [false, ` RAU ${nume}: tiparul apare de ${src.split(din).length - 1} ori in ${f} (STRICAT)`];
fs.writeFileSync(fp, src.replace(din, inl));
const r = await ruleaza(t, proba);
if (!r.rulat) return [false, ` RAU ${nume}: proba nu a ajuns la rezumat (STRICAT, cod ${r.cod})`];
if (r.cod !== 0 && r.rosii.some((l) => l.includes(tinta))) return [true, ` OK ${nume}: '${tinta}' ROSIE (${r.rosii.length} RAU)`];
return [false, ` RAU ${nume}: '${tinta}' a ramas verde (${r.rosii.length} RAU altundeva)`];
} catch (e) { return [false, ` RAU ${nume}: controlul a cazut pe ea (${String(e.message).slice(0, 80)})`]; }
finally { if (t) fs.rmSync(t, { recursive: true, force: true }); }
}));
for (const [bun, l] of linii) { console.log(l); if (!bun) esecuri++; }
console.log(esecuri ? `RAU: ${esecuri} esecuri ale controlului` : `DOVEDIT: martorii verzi, ${PLANTARI.length} din ${PLANTARI.length} plantari rosii pe verificarea lor`);
process.exitCode = esecuri ? 1 : 0;