aere-quantum/agents/control-negativ-aprobare.mjs
Aere Network 6e62b1ad1a Add agents: a post-quantum identity, a policy and a signed action ledger for AI agents, with human approval and revocation
An agent gets an ML-DSA-65 identity and a policy (spending per time window, allowed tools and recipients, which actions need human
approval, who may revoke it). Every action it proposes is judged against the policy, signed by the agent and chained; a verifier that
does not trust the agent re-runs the policy over the whole ledger. Approvals and revocations are signed by people with their own
ML-DSA-65 keys. The ledger of an agent under a policy is one ledger: a second history is a branch, and two branches are a proof of
equivocation anyone can check with the public key alone. The README says what the verifier cannot see: entry times are bounded from
below only with a witness (anchors or a start time), and someone who sees one branch cannot know of another.

Tests: policy 23/23 with the AIP-23 reference verifier (21 run without it), ledger 51/51, approval and revocation 39/39, command line
23/23; negative control 25/25.
2026-09-29 21:59:41 +03:00

74 lines
8.3 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 { spawnSync } 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],
['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; }
function ruleaza(t, proba) {
const env = { ...process.env }; if (VERIFY) env.AERE_VERIFY_PROOF = VERIFY; else delete env.AERE_VERIFY_PROOF;
const r = spawnSync(process.execPath, [path.join(t, proba)], { encoding: 'utf8', timeout: 240000, env });
const out = (r.stdout || '') + (r.stderr || '');
const m = /agent-(policy|ledger|aprobare|cli): (\d+)\/(\d+)/.exec(out);
return { cod: r.status, rulat: !!m, rele: (out.match(/^\s*(RAU\s|\[RAU)/gm) || []).length };
}
let esecuri = 0;
for (const proba of Object.values(P)) {
const t0 = copie(); const m0 = ruleaza(t0, proba); fs.rmSync(t0, { recursive: true, force: true });
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'})`); }
}
for (const [nume, f, din, inl, proba] of PLANTARI) {
const t = copie(); const fp = path.join(t, f); const src = fs.readFileSync(fp, 'utf8');
if (src.split(din).length !== 2) { esecuri++; console.log(` RAU ${nume}: tiparul nu apare exact o data in ${f}`); fs.rmSync(t, { recursive: true, force: true }); continue; }
fs.writeFileSync(fp, src.replace(din, inl));
const r = ruleaza(t, proba); fs.rmSync(t, { recursive: true, force: true });
if (r.rulat && r.cod !== 0 && r.rele > 0) console.log(` OK ${nume}: ${proba} ROSIE (${r.rele} RAU)`);
else { esecuri++; console.log(` RAU ${nume}: ${r.rulat ? `${proba} a ramas verde` : `${proba} nu a ajuns la rezumat (STRICAT)`} (cod ${r.cod})`); }
}
console.log(esecuri ? `RAU: ${esecuri} esecuri ale controlului` : `DOVEDIT: martorii verzi, ${PLANTARI.length} din ${PLANTARI.length} plantari rosii`);
process.exitCode = esecuri ? 1 : 0;