'use strict'; // AERE Agent Policy (roadmap B2 / #35-37): politica unui agent AI impusa CRIPTOGRAFIC - limita de cheltuiala pe fereastra, uneltele // permise, destinatarii permisi - cu fiecare decizie inregistrata ca dovada AERE Proof Protocol (AIP-23), astfel incat "agentul nu a // depasit politica" sa fie verificabil de un tert, nu o promisiune. Politica e ancorata printr-un hash: o decizie leaga hash-ul // politicii, deci nu poti pretinde ca ai aplicat alta politica. Numai Node 24, node:crypto, fara dependinte. // // 2026-09-29 (revizuirea adversariala B-17, forma 2): motivele deciziilor si mesajele sunt in engleza (intra in plicuri si registre // publice); `hashPolicy` RECALCULEAZA hash-ul unei politici primite (verificatorul se increzuse in perechea politica+hash data de // agent, deci o politica LAXA sub hash-ul celei reale trecea: masurat, o plata de 5000 sub o limita reala de 100, verificator "ok"); // politica se valideaza (suma si fereastra), o plata intr-un alt activ decat al limitei e refuzata, iar actionHash-ul plicului de // decizie e canonic (chei sortate), ca un strain sa il reproduca din actiune indiferent de ordinea cheilor. import crypto from 'node:crypto'; const sha256 = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex'); /** serializare canonica (chei sortate, recursiv): aceeasi valoare pe orice masina */ export function canonical(v) { if (v === null || typeof v !== 'object') return JSON.stringify(v); if (Array.isArray(v)) return '[' + v.map(canonical).join(',') + ']'; return '{' + Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + canonical(v[k])).join(',') + '}'; } const CHEI_POLITICA = new Set(['v', 'kind', 'agentId', 'spend', 'tools', 'recipients', 'approval', 'owner', 'wallet']); const HUMAN_ID = /^aere-human:[0-9a-f]{40}$/; const INTREG = /^[0-9]+$/; // Forma normala a unei politici, singurul loc care o construieste: definePolicy o scrie, hashPolicy o reface din ce primeste, deci // hash-ul unei politici e acelasi oricum ar fi ordonat fisierul ei. Campurile `approval` si `owner` intra NUMAI cand sunt date, ca // hash-ul politicilor de dinainte de 2026-09-28 sa ramana acelasi. function formaNormala(p) { if (!p || !p.agentId || typeof p.agentId !== 'string') throw new Error('agent-policy: agentId is required'); let spend = null; if (p.spend) { const amount = String(p.spend.amount), windowSeconds = Number(p.spend.windowSeconds); if (!INTREG.test(amount)) throw new Error('agent-policy: spend.amount must be a decimal integer'); if (!Number.isInteger(windowSeconds) || windowSeconds < 1) throw new Error('agent-policy: spend.windowSeconds must be a positive integer'); spend = { amount, windowSeconds, asset: p.spend.asset ? String(p.spend.asset) : 'AERE' }; } if (p.tools != null && !(Array.isArray(p.tools) && p.tools.every((x) => typeof x === 'string'))) throw new Error('agent-policy: tools must be a list of names'); if (p.recipients != null && !(Array.isArray(p.recipients) && p.recipients.every((x) => typeof x === 'string'))) throw new Error('agent-policy: recipients must be a list of addresses'); const policy = { v: 1, kind: 'aere-agent-policy', agentId: p.agentId, spend, tools: Array.isArray(p.tools) ? [...p.tools].sort() : null, recipients: Array.isArray(p.recipients) ? [...p.recipients].map((x) => x.toLowerCase()).sort() : null, }; if (p.approval) { const ap = p.approval, aprobatori = Array.isArray(ap.approvers) ? [...new Set(ap.approvers)].sort() : []; if (!aprobatori.length || !aprobatori.every((x) => HUMAN_ID.test(x))) throw new Error('agent-policy: approval.approvers must be distinct aere-human: ids'); const k = Number(ap.threshold); if (!Number.isInteger(k) || k < 1 || k > aprobatori.length) throw new Error(`agent-policy: approval.threshold must be between 1 and ${aprobatori.length}`); if (ap.above == null && !(Array.isArray(ap.tools) && ap.tools.length)) throw new Error('agent-policy: approval needs `above` (payments) or `tools`, otherwise it asks for nothing'); if (ap.above != null && !INTREG.test(String(ap.above))) throw new Error('agent-policy: approval.above must be a decimal integer'); policy.approval = { approvers: aprobatori, threshold: k, above: ap.above == null ? null : String(ap.above), tools: Array.isArray(ap.tools) ? [...ap.tools].sort() : null }; } if (p.owner != null) { if (!HUMAN_ID.test(String(p.owner))) throw new Error('agent-policy: owner must be an aere-human: id'); policy.owner = String(p.owner); } // 2026-09-29 (punctul 23/25): portofelul din care agentul are voie sa plateasca (o adresa EVM). Cand e numit, o plata trebuie sa // spuna `from` si sa fie chiar el; x402/wallet.mjs refuza sa semneze pentru o politica ce numeste alt portofel. if (p.wallet != null) { if (!/^0x[0-9a-fA-F]{40}$/.test(String(p.wallet))) throw new Error('agent-policy: wallet must be an EVM address (0x + 40 hex)'); policy.wallet = String(p.wallet).toLowerCase(); } return policy; } const hashFormei = (policy) => sha256(Buffer.from(JSON.stringify(policy), 'utf8')); /** * Defineste o politica de agent. Intoarce {policy, policyHash} - hash-ul e amprenta politicii in forma ei normala (ancora deciziilor). * @param {object} p * @param {string} p.agentId * @param {{amount:string, windowSeconds:number, asset?:string}} [p.spend] limita de cheltuiala pe fereastra * @param {string[]} [p.tools] uneltele permise (lista alba); lipsa = niciuna permisa * @param {string[]} [p.recipients] destinatarii permisi pentru plati (lista alba); lipsa = oricine * @param {{approvers:string[], threshold:number, above?:string, tools?:string[]}} [p.approval] aprobarea umana (2026-09-28): * o plata cu suma STRICT peste `above`, sau o unealta din `tools`, cere `threshold` aprobari de la aprobatori distincti * din `approvers` (id-uri 'aere-human:' derivate din cheile lor ML-DSA-65; vezi agent-aprobare.mjs) * @param {string} [p.owner] proprietarul ('aere-human:'), singurul care poate revoca agentul * @param {string} [p.wallet] (2026-09-29) adresa EVM din care agentul are voie sa plateasca; o plata trebuie sa o numeasca in `from` */ export function definePolicy(p) { const policy = formaNormala(p); return { policy, policyHash: hashFormei(policy) }; } /** * Hash-ul unei politici PRIMITE (de la agent, dintr-un fisier), recalculat din forma ei normala. Refuza o politica cu campuri pe * care motorul nu le stie (un camp necunoscut ar putea fi ceva ce cititorul crede ca se aplica si motorul ignora). * @returns {{policy:object, policyHash:string}} politica normala, cea care se judeca, si hash-ul ei */ export function hashPolicy(policy) { if (!policy || policy.kind !== 'aere-agent-policy' || policy.v !== 1) throw new Error('agent-policy: not an aere-agent-policy v1'); const necunoscute = Object.keys(policy).filter((k) => !CHEI_POLITICA.has(k)); if (necunoscute.length) throw new Error(`agent-policy: unknown policy field(s): ${necunoscute.join(', ')}`); return definePolicy(policy); } /** Cere actiunea aprobare umana dupa politica? (o plata strict peste `above`, sau o unealta numita) */ export function needsApproval(policy, action) { const ap = policy && policy.approval; if (!ap) return false; if (action.kind === 'payment' && ap.above != null && INTREG.test(String(action.amount))) return BigInt(action.amount) > BigInt(ap.above); if (action.kind === 'tool' && ap.tools) return ap.tools.includes(action.tool); return false; } /** * Verifica o actiune fata de politica + istoricul de cheltuiala din fereastra. Pur: nu tine stare; primeste cheltuiala deja facuta. * @param {object} policy din definePolicy / hashPolicy * @param {object} action { kind:'tool'|'payment', tool?, args?, to?, amount?, asset?, at:epochSeconds } * @param {Array} spentInWindow [{amount, at}] platile deja facute (checkAction le filtreaza singur la fereastra) * @param {{approvers?:string[], revokedAt?:number|null}} [ctx] (2026-09-28) id-urile aprobatorilor ale caror aprobari au fost DEJA * verificate criptografic de apelant (registrul, verificatorul) si momentul revocarii valide, daca exista. checkAction ramane * pur: nu verifica semnaturi, numara ce i se da; cine ii da aprobatori neverificati isi minte singur politica. * @returns {{allowed:boolean, reason:string, remaining?:string, approvedBy?:number}} */ export function checkAction(policy, action, spentInWindow = [], ctx = {}) { if (!policy || policy.kind !== 'aere-agent-policy') throw new Error('agent-policy: invalid policy'); // revocarea bate orice: de la momentul ei nicio actiune nu mai e permisa if (ctx.revokedAt != null && Number(action.at) >= Number(ctx.revokedAt)) return { allowed: false, reason: `agent revoked by its owner at ${ctx.revokedAt}` }; const d = faraAprobare(policy, action, spentInWindow); if (!d.allowed || !needsApproval(policy, action)) return d; // aprobarea e o cerinta IN PLUS, nu o scutire: actiunea trebuie sa respecte si limitele const ap = policy.approval; const valizi = new Set((ctx.approvers || []).filter((x) => ap.approvers.includes(x))); if (valizi.size < ap.threshold) return { allowed: false, reason: `human approval required: ${valizi.size} of ${ap.threshold} valid approvals` }; return { ...d, reason: `${d.reason}; approved by ${valizi.size} of ${ap.threshold}`, approvedBy: valizi.size }; } function faraAprobare(policy, action, spentInWindow) { if (action.kind === 'tool') { if (!policy.tools || !policy.tools.includes(action.tool)) return { allowed: false, reason: `tool "${action.tool}" is not in the allowed list` }; return { allowed: true, reason: 'tool allowed' }; } if (action.kind === 'payment') { // 2026-09-27 (revizuire adversariala): suma nu era validata, deci o plata NEGATIVA scadea cheltuiala din fereastra si o plata de // 1.000.000 trecea sub o limita de 100 (masurat: allowed, si registrul verificat "ok"). Se cere un intreg zecimal STRICT pozitiv, // si un moment finit; altfel refuz, cu motivul numit. if (!INTREG.test(String(action.amount)) || BigInt(action.amount) === 0n) return { allowed: false, reason: `invalid amount (${String(action.amount).slice(0, 24)}): a strictly positive decimal integer is required` }; if (!Number.isFinite(Number(action.at))) return { allowed: false, reason: 'invalid time: action.at is not a finite number' }; if (policy.wallet && String(action.from || '').toLowerCase() !== policy.wallet) return { allowed: false, reason: `payment from ${String(action.from || 'an unnamed wallet').slice(0, 42)} is not from the wallet the policy names` }; if (policy.recipients && !policy.recipients.includes(String(action.to).toLowerCase())) return { allowed: false, reason: `recipient ${action.to} is not allowed` }; if (policy.spend) { // 2026-09-29: o plata in alt activ decat al limitei nu se aduna la ea si nu trece pe langa ea: e refuzata if (action.asset != null && String(action.asset) !== policy.spend.asset) return { allowed: false, reason: `asset ${String(action.asset).slice(0, 24)} is not the policy's asset (${policy.spend.asset})` }; const fereastra = policy.spend.windowSeconds; const acum = Number(action.at); const cheltuit = spentInWindow.filter((s) => acum - Number(s.at) < fereastra).reduce((a, s) => a + BigInt(s.amount), 0n); const limita = BigInt(policy.spend.amount); const nou = BigInt(action.amount); if (cheltuit + nou > limita) return { allowed: false, reason: `over the limit: ${cheltuit + nou} > ${limita} per ${fereastra}s`, remaining: String(limita - cheltuit > 0n ? limita - cheltuit : 0n) }; return { allowed: true, reason: 'under the limit', remaining: String(limita - cheltuit - nou) }; } return { allowed: true, reason: 'no spending limit in the policy' }; } return { allowed: false, reason: `unknown action kind: ${String(action.kind).slice(0, 40)}` }; } /** * Inregistreaza o decizie ca plic AIP-23 (fel aere-proof-of-agent-decision). Leaga policyHash (nefalsificabil) + digestul actiunii, * calculat pe forma CANONICA a actiunii (chei sortate), ca oricine sa il reproduca din actiune. */ export function decisionEnvelope({ policyHash, action, decision, createdAt }) { if (!policyHash || !action || !decision || !createdAt) throw new Error('agent-policy: decisionEnvelope needs policyHash, action, decision, createdAt'); const statement = { v: 1, kind: 'aere-proof-of-agent-decision', policyHash: policyHash.toLowerCase(), actionHash: sha256(Buffer.from(canonical(action), 'utf8')), allowed: !!decision.allowed, reason: decision.reason, createdAt, }; return { v: 1, kind: 'aere-proof-of-agent-decision-attestation', statement, statementHash: sha256(Buffer.from(JSON.stringify(statement), 'utf8')) }; }