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).
87 lines
8.3 KiB
JavaScript
87 lines
8.3 KiB
JavaScript
'use strict';
|
|
// Proba motorului de politica al agentilor (B2 m2), cu CONTROALE NEGATIVE: sub limita permis, peste limita refuzat, unealta/destinatar
|
|
// nepermis refuzat, iar plicul de decizie verifica sub AIP-23 si leaga policyHash (o politica schimbata -> alt hash, decizia nu se
|
|
// poate atribui altei politici). 2026-09-29 (B-17): hash-ul unei politici PRIMITE se recalculeaza (o politica laxa nu poate purta
|
|
// hash-ul celei reale), politica se valideaza, un alt activ decat al limitei e refuzat, actionHash-ul plicului e canonic.
|
|
// node proba-agent-policy.mjs -> 0 toate cum trebuia, 1 altfel, 2 fara verificatorul AIP-23 (partea lui NEMASURATA)
|
|
// Verificatorul AIP-23: AERE_VERIFY_PROOF=<verify-proof.mjs> sau, in depozitul de dezvoltare, ../aere-proof-protocol/verify.mjs.
|
|
|
|
import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path';
|
|
import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url';
|
|
import { definePolicy, hashPolicy, checkAction, decisionEnvelope, canonical } from './agent-policy.mjs';
|
|
import crypto from 'node:crypto';
|
|
|
|
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
|
const VERIFY = process.env.AERE_VERIFY_PROOF || path.resolve(AICI, '..', 'aere-proof-protocol', 'verify.mjs');
|
|
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'ap-'));
|
|
let ok = 0, rele = 0, sarite = 0;
|
|
const cer = (n, c) => { console.log(` [${c ? 'OK ' : 'RAU '}] ${n}`); c ? ok++ : rele++; };
|
|
const arunca = (fn) => { try { fn(); return null; } catch (e) { return e.message; } };
|
|
const verdict = (p) => { try { return JSON.parse(execFileSync(process.execPath, [VERIFY, p, '--json'], { encoding: 'utf8' })).verdict; } catch (e) { try { return JSON.parse(e.stdout || '').verdict; } catch { return '?'; } } };
|
|
|
|
try {
|
|
const { policy, policyHash } = definePolicy({ agentId: 'agent-1', spend: { amount: '100', windowSeconds: 3600 }, tools: ['retrieval', 'calc'], recipients: ['0xBBBB'] });
|
|
cer('policyHash e digest 0x+64', /^0x[0-9a-f]{64}$/.test(policyHash));
|
|
|
|
// unelte
|
|
cer('unealta permisa -> allowed', checkAction(policy, { kind: 'tool', tool: 'retrieval', at: 1 }).allowed === true);
|
|
cer('CONTROL: unealta nepermisa -> denied, cu motivul in engleza', /is not in the allowed list/.test(checkAction(policy, { kind: 'tool', tool: 'shell', at: 1 }).reason));
|
|
|
|
// cheltuiala
|
|
cer('plata sub limita, destinatar permis -> allowed', checkAction(policy, { kind: 'payment', to: '0xbbbb', amount: '40', at: 1000 }, [{ amount: '30', at: 999 }]).allowed === true);
|
|
const peste = checkAction(policy, { kind: 'payment', to: '0xbbbb', amount: '80', at: 1000 }, [{ amount: '30', at: 999 }]);
|
|
cer(`CONTROL: plata care depaseste limita in fereastra -> denied (${peste.reason})`, peste.allowed === false && /over the limit: 110 > 100/.test(peste.reason));
|
|
cer('CONTROL: destinatar nepermis -> denied', checkAction(policy, { kind: 'payment', to: '0xCCCC', amount: '10', at: 1000 }, []).allowed === false);
|
|
cer('cheltuiala veche (in afara ferestrei) nu conteaza', checkAction(policy, { kind: 'payment', to: '0xbbbb', amount: '90', at: 100000 }, [{ amount: '90', at: 1 }]).allowed === true);
|
|
// activul: implicit al limitei; altul e refuzat, nu adunat
|
|
cer('plata cu activul politicii (AERE) scris explicit -> allowed', checkAction(policy, { kind: 'payment', to: '0xbbbb', amount: '10', asset: 'AERE', at: 1 }).allowed === true);
|
|
const altActiv = checkAction(policy, { kind: 'payment', to: '0xbbbb', amount: '10', asset: 'USDC', at: 1 });
|
|
cer(`CONTROL: plata in alt activ decat al limitei -> denied (${altActiv.reason})`, !altActiv.allowed && /not the policy's asset/.test(altActiv.reason));
|
|
|
|
// portofelul numit (2026-09-29): o plata trebuie sa spuna `from` si sa fie chiar el
|
|
const cuPortofel = definePolicy({ agentId: 'agent-1', spend: { amount: '100', windowSeconds: 3600 }, wallet: '0xAbCd00000000000000000000000000000000Ef01' }).policy;
|
|
cer('portofelul politicii e scris cu litere mici', cuPortofel.wallet === '0xabcd00000000000000000000000000000000ef01');
|
|
cer('plata din portofelul numit (orice litere) -> allowed', checkAction(cuPortofel, { kind: 'payment', to: '0xbbbb', amount: '10', from: '0xABCD00000000000000000000000000000000EF01', at: 1 }).allowed === true);
|
|
const fara = checkAction(cuPortofel, { kind: 'payment', to: '0xbbbb', amount: '10', at: 1 });
|
|
const dinAlt = checkAction(cuPortofel, { kind: 'payment', to: '0xbbbb', amount: '10', from: '0x1111111111111111111111111111111111111111', at: 1 });
|
|
cer(`CONTROL: plata fara from, sau din alt portofel, sub o politica ce numeste portofelul -> denied (${dinAlt.reason})`, !fara.allowed && !dinAlt.allowed && /not from the wallet/.test(dinAlt.reason));
|
|
cer('CONTROL: un portofel care nu e adresa EVM -> politica refuzata', /wallet must be an EVM address/.test(arunca(() => definePolicy({ agentId: 'a', wallet: '0x12' })) || ''));
|
|
|
|
// validarea politicii
|
|
cer('CONTROL: spend.amount care nu e intreg -> politica refuzata', /spend.amount/.test(arunca(() => definePolicy({ agentId: 'a', spend: { amount: '1e9', windowSeconds: 60 } })) || ''));
|
|
cer('CONTROL: fereastra zero -> politica refuzata', /windowSeconds/.test(arunca(() => definePolicy({ agentId: 'a', spend: { amount: '1', windowSeconds: 0 } })) || ''));
|
|
|
|
// hashPolicy: hash-ul unei politici PRIMITE se recalculeaza
|
|
const dinFisier = JSON.parse(JSON.stringify(policy));
|
|
cer('hashPolicy pe politica citita dintr-un fisier = hash-ul definePolicy', hashPolicy(dinFisier).policyHash === policyHash);
|
|
const reordonata = Object.fromEntries(Object.entries(dinFisier).reverse());
|
|
cer('hashPolicy nu depinde de ordinea cheilor din fisier', hashPolicy(reordonata).policyHash === policyHash);
|
|
const laxa = { ...dinFisier, spend: { ...dinFisier.spend, amount: '1000000' } };
|
|
cer('CONTROL: o politica LAXA are alt hash (nu poate purta hash-ul celei reale)', hashPolicy(laxa).policyHash !== policyHash);
|
|
cer('CONTROL: un camp necunoscut in politica -> refuzat', /unknown policy field/.test(arunca(() => hashPolicy({ ...dinFisier, bypass: true })) || ''));
|
|
cer('politicile de dinainte de 2026-09-29 pastreaza hash-ul (forma normala neschimbata)',
|
|
definePolicy({ agentId: 'agent-7', spend: { amount: '100', windowSeconds: 3600, asset: 'AERE' }, tools: ['search'], recipients: null }).policyHash === '0xc7d983a9886a0899bd6f3f09fde526514b0075f8f6abe77a1886405d86048c27');
|
|
|
|
// plic de decizie
|
|
const act = { kind: 'payment', to: '0xbbbb', amount: '80', at: 1000 };
|
|
const dec = checkAction(policy, act, [{ amount: '30', at: 999 }]);
|
|
const env = decisionEnvelope({ policyHash, action: act, decision: dec, createdAt: '2026-09-26T09:00:00Z' });
|
|
cer('plicul leaga policyHash', env.statement.policyHash === policyHash);
|
|
cer('plicul spune allowed=false (decizia reala)', env.statement.allowed === false);
|
|
const inversa = { at: 1000, amount: '80', to: '0xbbbb', kind: 'payment' };
|
|
cer('actionHash e canonic: aceeasi actiune cu cheile in alta ordine -> acelasi digest', decisionEnvelope({ policyHash, action: inversa, decision: dec, createdAt: '2026-09-26T09:00:00Z' }).statement.actionHash === env.statement.actionHash);
|
|
cer('actionHash = sha256 peste JSON-ul canonic al actiunii (reproductibil de un strain)', env.statement.actionHash === '0x' + crypto.createHash('sha256').update(canonical(act)).digest('hex'));
|
|
if (fs.existsSync(VERIFY)) {
|
|
const f = path.join(T, 'dec.json'); fs.writeFileSync(f, JSON.stringify(env, null, 1));
|
|
cer('plicul de decizie verifica sub AIP-23', verdict(f) === 'VALID');
|
|
const rau = JSON.parse(JSON.stringify(env)); rau.statement.allowed = true; const f2 = path.join(T, 'rau.json'); fs.writeFileSync(f2, JSON.stringify(rau));
|
|
cer('CONTROL: plicul cu decizia rescrisa nu mai verifica sub AIP-23', verdict(f2) !== 'VALID');
|
|
} else { sarite += 2; console.log(` [SARIT] plicul sub AIP-23 (2 probe): verificatorul nu e la ${VERIFY}; AERE_VERIFY_PROOF=<verify-proof.mjs>`); }
|
|
|
|
// o politica schimbata -> alt hash (decizia nu se poate atribui altei politici)
|
|
const alt = definePolicy({ agentId: 'agent-1', spend: { amount: '1000000', windowSeconds: 3600 }, tools: ['retrieval'], recipients: ['0xBBBB'] });
|
|
cer('CONTROL: politica cu alta limita -> alt policyHash', alt.policyHash !== policyHash);
|
|
} finally { fs.rmSync(T, { recursive: true, force: true }); }
|
|
console.log(`\nagent-policy: ${ok}/${ok + rele} cum trebuia${sarite ? `, ${sarite} NEMASURATE (fara verificatorul AIP-23)` : ''}`);
|
|
process.exitCode = rele ? 1 : (sarite ? 2 : 0);
|