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.
78 lines
7.1 KiB
JavaScript
78 lines
7.1 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));
|
|
|
|
// 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);
|