aere-quantum/agents/control-negativ-aprobare.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
9.5 KiB
JavaScript

// Controlul negativ al probelor agentilor (politica, registrul, aprobarea si revocarea, linia de comanda): fiecare paznic se strica intr-o COPIE a
// dosarului, proba lui ruleaza pe copie si TREBUIE sa iasa rosie, cu probele chiar rulate (rezumatul lor exista); pe copia neatinsa,
// verde. O plantare al carei tipar nu apare exact o data e un esec al controlului, nu o linie informativa. Trei stari: o proba care nu
// ajunge la rezumat e STRICAT, si se numara esec (rosul ei nu masoara nimic).
// 2026-09-29 (B-17): acopera si paznicii noi - argumentele in aprobare, hash-ul politicii recalculat, antedatarea (la scriere si fata
// de ancore), ancora altei ramuri, sesiunea derivata, dovada de echivocare, reluarea registrului, campul necunoscut, activul, actionHash canonic.
// node control-negativ-aprobare.mjs iesire 0 = martorii verzi si toate plantarile rosii
// Proba politicii foloseste verificatorul AIP-23 (AERE_VERIFY_PROOF sau ../aere-proof-protocol/verify.mjs); fara el, martorul ei iese
// cu 2 (partea AIP-23 NEMASURATA) si se accepta ca martor numai asa, cu zero RAU.
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const DEV_VERIFY = path.resolve(AICI, '..', 'aere-proof-protocol', 'verify.mjs');
const VERIFY = process.env.AERE_VERIFY_PROOF || (fs.existsSync(DEV_VERIFY) ? DEV_VERIFY : '');
const P = { pol: 'proba-agent-policy.mjs', reg: 'proba-agent-ledger.mjs', apr: 'proba-agent-aprobare.mjs', cli: 'proba-agent-cli.mjs' };
const PLANTARI = [
// [nume, fisier, tipar, inlocuire, proba]
['nonce-ul unei aprobari nu mai e unic', 'agent-ledger.mjs', "if (ap && folosite.has(String(ap.nonce))) { respinse.push('nonce already used'); continue; }", '', P.apr],
['un aprobator nenumit in politica numara', 'agent-aprobare.mjs', 'if (!policy.approval.approvers.includes(humanId)) return', 'if (false) return', P.apr],
['semnatura aprobarii nu se mai verifica', 'agent-aprobare.mjs', "if (!sig) return { ok: false, error: 'the approval signature does not verify' };", '', P.apr],
['aprobarea devine scutire de limita', 'agent-policy.mjs', 'if (!d.allowed || !needsApproval(policy, action)) return d;', 'if (needsApproval(policy, action) && (ctx.approvers || []).length >= policy.approval.threshold) return { allowed: true, reason: "approved (exempt)" }; if (!d.allowed || !needsApproval(policy, action)) return d;', P.apr],
['verificatorul ignora revocarile date separat', 'agent-ledger.mjs', 'let revokedAt = ext.revokedAt;', 'let revokedAt = null;', P.apr],
['revocarea nu mai cere proprietarul', 'agent-aprobare.mjs', "if (humanIdFromKey(k) !== policy.owner) return { ok: false, error: 'the revocation is not signed by the owner named in the policy' };", '', P.apr],
['aprobarea nu mai e legata de continutul actiunii', 'agent-aprobare.mjs', 'if (ap.actionHash !== actionHash(action)) return', 'if (false) return', P.apr],
['argumentele uneltei nu mai intra in continutul aprobat', 'agent-aprobare.mjs', 'if (a.args !== undefined) c.argsHash = sha(canonical(a.args));', '', P.apr],
['verificatorul crede hash-ul dat in loc sa il recalculeze', 'agent-ledger.mjs', 'if (hashFixat != null && String(hashFixat).toLowerCase() !== policyHash) return', 'if (hashFixat != null) policyHash = String(hashFixat).toLowerCase(); if (false) return', P.reg],
['registrul accepta la scriere o intrare antedatata', 'agent-ledger.mjs', 'if (Number(at) < acum - TOLERANTA_S) throw', 'if (false) throw', P.reg],
['ancora nu mai margineste timpul de jos', 'agent-ledger.mjs', 'if (i > a.seq && atI < a.at - tol) return', 'if (false) return', P.reg],
['ancora nu mai trebuie sa numeasca o intrare a registrului', 'agent-ledger.mjs', 'if (!ledger.entries[s] || ledger.entries[s].hash !== a.hash) return', 'if (false) return', P.reg],
['verificatorul nu mai cere sesiunea registrului in intrare', 'agent-ledger.mjs', ' || b.session !== ledger.session) return', ') return', P.reg],
['sesiunea registrului nu mai trebuie sa fie cea derivata', 'agent-ledger.mjs', 'if (ledger.session !== sessionId(ledger.agentId, policyHash)) return', 'if (false) return', P.reg],
['dovada de echivocare fara semnaturile agentului', 'agent-ledger.mjs', "if (!intrareSemnata(e, pub)) return { ok: false, error: 'an entry is not signed by the agent' };", '', P.reg],
['un registru atins se poate relua', 'agent-ledger.mjs', 'if (!v.ok) throw new Error(`agent-ledger: the ledger to resume does not verify', 'if (false) throw new Error(`agent-ledger: the ledger to resume does not verify', P.reg],
['reluarea uita nonce-urile folosite', 'agent-ledger.mjs', 'for (const e of entries) for (const n of nonceuri(e.body.approvals)) folosite.add(n);', '', P.apr],
['reluarea uita revocarea', 'agent-ledger.mjs', 'JSON.parse(JSON.stringify(ledger.entries)), v.revocations.revokedAt);', 'JSON.parse(JSON.stringify(ledger.entries)), null);', P.apr],
['linia de comanda suprascrie o cheie existenta', 'agent-cli.mjs', 'if (fs.existsSync(cheie)) throw new Folosire(', 'if (false) throw new Folosire(', P.cli],
['linia de comanda lasa pe oricine sa aprobe', 'agent-cli.mjs', 'if (!policy.approval || !policy.approval.approvers.includes(human.humanId)) throw', 'if (false) throw', P.cli],
['verify din linia de comanda uita hash-ul numit in fisierul politicii', 'agent-cli.mjs', "const pinned = get('--policy-hash') ?? j.policyHash;", "const pinned = get('--policy-hash');", P.cli],
['record deschide mereu un registru nou in loc sa il reia', 'agent-cli.mjs', 'const L = fs.existsSync(lf) ? resumeLedger(', 'const L = false ? resumeLedger(', P.cli],
['o plata din alt portofel decat cel numit trece', 'agent-policy.mjs', 'if (policy.wallet && String(action.from', 'if (false && String(action.from', P.pol],
['politica cu un camp necunoscut e primita', 'agent-policy.mjs', 'if (necunoscute.length) throw', 'if (false) throw', P.pol],
['o plata in alt activ trece pe langa limita', 'agent-policy.mjs', 'if (action.asset != null && String(action.asset) !== policy.spend.asset) return', 'if (false) return', P.pol],
['actionHash-ul plicului nu mai e canonic', 'agent-policy.mjs', "actionHash: sha256(Buffer.from(canonical(action), 'utf8')),", "actionHash: sha256(Buffer.from(JSON.stringify(action), 'utf8')),", P.pol],
];
const FISIERE = ['agent-policy.mjs', 'agent-ledger.mjs', 'agent-aprobare.mjs', 'agent-cli.mjs', P.pol, P.reg, P.apr, P.cli];
function copie() { const t = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-b2-ctl-')); for (const f of FISIERE) fs.copyFileSync(path.join(AICI, f), path.join(t, f)); return t; }
// 2026-09-29: probele ruleaza cate PARALEL deodata (asincron), ca tot controlul sa incapa in termenul rulatorului comun
const PARALEL = 4;
function ruleaza(t, proba) {
const env = { ...process.env }; if (VERIFY) env.AERE_VERIFY_PROOF = VERIFY; else delete env.AERE_VERIFY_PROOF;
return new Promise((resolve) => {
const c = spawn(process.execPath, [path.join(t, proba)], { env }); 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); const m = /agent-(policy|ledger|aprobare|cli): (\d+)\/(\d+)/.exec(out);
resolve({ cod, rulat: !!m, rele: (out.match(/^\s*(RAU\s|\[RAU)/gm) || []).length }); });
});
}
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(Object.values(P).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) {
const verde = m0.rulat && m0.rele === 0 && (m0.cod === 0 || (m0.cod === 2 && proba === P.pol && !VERIFY));
if (verde) console.log(` OK martorul ${proba}: copia neatinsa verde${m0.cod === 2 ? ' (fara verificatorul AIP-23: partea lui NEMASURATA)' : ''}`);
else { esecuri++; console.log(` RAU martorul ${proba} nu e verde (cod ${m0.cod}, ${m0.rulat ? m0.rele + ' RAU' : 'nu a ajuns la rezumat'})`); }
}
const linii = await inGrup(PLANTARI.map((pl) => async () => {
const [nume, f, din, inl, proba] = pl; let t = null;
try {
// o plantare scrisa gresit (fisier, tipar) e un esec al controlului, nu o cadere a lui
if (!FISIERE.includes(f) || typeof din !== 'string' || !proba) return [false, ` RAU ${nume}: plantarea e scrisa gresit (fisier ${f}, proba ${proba})`];
t = copie(); const fp = path.join(t, f); const src = fs.readFileSync(fp, 'utf8');
if (src.split(din).length !== 2) return [false, ` RAU ${nume}: tiparul nu apare exact o data in ${f}`];
fs.writeFileSync(fp, src.replace(din, inl));
const r = await ruleaza(t, proba);
if (r.rulat && r.cod !== 0 && r.rele > 0) return [true, ` OK ${nume}: ${proba} ROSIE (${r.rele} RAU)`];
return [false, ` RAU ${nume}: ${r.rulat ? `${proba} a ramas verde` : `${proba} nu a ajuns la rezumat (STRICAT)`} (cod ${r.cod})`];
} 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`);
process.exitCode = esecuri ? 1 : 0;