aere-quantum/agents/agent-cli.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

138 lines
8.5 KiB
JavaScript

#!/usr/bin/env node
// agent-cli.mjs: linia de comanda a agentilor AI (politica, registrul, aprobarea, revocarea, verificarea, dovada de echivocare), ca
// un om sa poata aproba sau revoca si un strain sa poata verifica fara sa scrie cod. Mesajele si iesirile sunt in engleza; cheile
// private stau in fisierele lor (0600) si nu se tiparesc niciodata. Numai Node 24.
//
// node agent-cli.mjs id --out <dir> new agent identity; prints the agentId
// node agent-cli.mjs human --out <dir> new human identity (approver, owner); prints the humanId
// node agent-cli.mjs policy --spec spec.json [--out p.json] a policy in normal form, with its hash
// node agent-cli.mjs check --policy p.json --action a.json [--spent s.json]
// node agent-cli.mjs record --key agent.key.pem --policy p.json --ledger l.json --action a.json [--approvals a1.json,a2.json] [--provenance f.json]
// node agent-cli.mjs approve --key human.key.pem --policy p.json --action a.json [--valid 600] [--out ap.json]
// node agent-cli.mjs revoke --key owner.key.pem --policy p.json [--at <unix>] [--reason text] [--out rv.json]
// node agent-cli.mjs verify --ledger l.json --policy p.json [--policy-hash 0x..] [--revocations r1.json,r2.json] [--anchors a.json] [--not-before <unix>]
// node agent-cli.mjs equivocation --a l1.json --b l2.json [--out proof.json]
// node agent-cli.mjs verify-equivocation --proof proof.json
// Iesiri: 0 da (permis, valid, gasit), 1 nu (invalid, negasit, refuzat de verificare), 3 actiune refuzata de politica, 2 folosire gresita.
import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { definePolicy, hashPolicy, checkAction } from './agent-policy.mjs';
import { newAgentIdentity, agentFromPrivateKeyPem, openLedger, resumeLedger, verifyLedger, findEquivocation, verifyEquivocation } from './agent-ledger.mjs';
import { newHumanIdentity, humanFromPrivateKeyPem, approve, revoke, verifyRevocation } from './agent-aprobare.mjs';
class Folosire extends Error {}
const citesteJson = (f, ce) => {
if (!f) throw new Folosire(`--${ce} is required`);
try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch (e) { throw new Folosire(`cannot read ${ce} file ${f}: ${e.message}`); }
};
// fisierul unei politici: {policy, policyHash} (cum il scrie `policy`) sau politica singura; hash-ul se recalculeaza mereu
function politica(f) {
const j = citesteJson(f, 'policy');
const { policy, policyHash } = hashPolicy(j.policy || j);
if (j.policyHash && String(j.policyHash).toLowerCase() !== policyHash) throw new Error(`the policy file names policyHash ${j.policyHash}, but the policy hashes to ${policyHash}`);
return { policy, policyHash };
}
function scrieAtomic(f, obiect, mod) {
const tmp = `${f}.${process.pid}.tmp`;
fs.writeFileSync(tmp, JSON.stringify(obiect, null, 1) + '\n', mod ? { mode: mod } : undefined);
fs.renameSync(tmp, f);
}
const iese = (obiect, out) => { if (out) scrieAtomic(out, obiect); else console.log(JSON.stringify(obiect, null, 1)); };
const acum = () => Math.floor(Date.now() / 1000);
export function main(argv) {
const [cmd, ...rest] = argv;
const get = (f) => { const i = rest.indexOf(f); return i >= 0 ? rest[i + 1] : undefined; };
const lista = (f) => (get(f) ? String(get(f)).split(',').filter(Boolean) : []);
switch (cmd) {
case 'id':
case 'human': {
const out = get('--out'); if (!out) throw new Folosire(`${cmd} --out <dir>`);
const x = cmd === 'id' ? newAgentIdentity() : newHumanIdentity();
const nume = cmd === 'id' ? 'agent' : 'human';
fs.mkdirSync(out, { recursive: true });
const cheie = path.join(out, `${nume}.key.pem`);
if (fs.existsSync(cheie)) throw new Folosire(`${cheie} exists; refusing to overwrite a key`);
fs.writeFileSync(cheie, x.privateKey.export({ type: 'pkcs8', format: 'pem' }), { mode: 0o600 });
fs.writeFileSync(path.join(out, `${nume}.pub.pem`), x.publicKeyPem, { mode: 0o644 });
console.log(cmd === 'id' ? x.agentId : x.humanId);
return 0;
}
case 'policy': {
const r = definePolicy(citesteJson(get('--spec'), 'spec'));
iese(r, get('--out')); if (get('--out')) console.log(r.policyHash);
return 0;
}
case 'check': {
const { policy } = politica(get('--policy'));
const action = citesteJson(get('--action'), 'action');
const spent = get('--spent') ? citesteJson(get('--spent'), 'spent') : [];
const d = checkAction(policy, { ...action, at: action.at ?? acum() }, spent);
console.log(JSON.stringify(d)); return d.allowed ? 0 : 3;
}
case 'record': {
const kf = get('--key'); if (!kf) throw new Folosire('--key is required');
const identity = agentFromPrivateKeyPem(fs.readFileSync(kf, 'utf8'));
const { policy, policyHash } = politica(get('--policy'));
const lf = get('--ledger'); if (!lf) throw new Folosire('--ledger is required');
const L = fs.existsSync(lf) ? resumeLedger({ identity, policy, policyHash, ledger: citesteJson(lf, 'ledger') }) : openLedger({ identity, policy, policyHash });
const action = citesteJson(get('--action'), 'action'); // momentul il pune registrul (record suprascrie `at`)
const approvals = lista('--approvals').map((f) => citesteJson(f, 'approvals'));
const provenance = get('--provenance') ? citesteJson(get('--provenance'), 'provenance') : undefined;
const r = L.record(action, { approvals, provenance });
scrieAtomic(lf, L.export());
console.log(JSON.stringify({ allowed: r.allowed, reason: r.reason, seq: r.entry.seq, hash: r.entry.hash, rejectedApprovals: r.rejectedApprovals }));
return r.allowed ? 0 : 3;
}
case 'approve': {
const kf = get('--key'); if (!kf) throw new Folosire('--key is required');
const human = humanFromPrivateKeyPem(fs.readFileSync(kf, 'utf8'));
const { policy, policyHash } = politica(get('--policy'));
if (!policy.approval || !policy.approval.approvers.includes(human.humanId)) throw new Error(`${human.humanId} is not an approver named in the policy`);
const action = citesteJson(get('--action'), 'action');
const valid = Number(get('--valid') ?? 600);
if (!Number.isInteger(valid) || valid < 1 || valid > 7 * 86400) throw new Folosire('--valid must be 1..604800 seconds');
const t = acum();
iese(approve({ human, agentId: policy.agentId, policyHash, action, issuedAt: t, expiresAt: t + valid }), get('--out'));
return 0;
}
case 'revoke': {
const kf = get('--key'); if (!kf) throw new Folosire('--key is required');
const owner = humanFromPrivateKeyPem(fs.readFileSync(kf, 'utf8'));
const { policy, policyHash } = politica(get('--policy'));
const rv = revoke({ owner, agentId: policy.agentId, policyHash, revokedAt: Number(get('--at') ?? acum()), reason: get('--reason') ?? '' });
const r = verifyRevocation(rv, { policy, policyHash });
if (!r.ok) { console.error(`agent: the revocation would not be valid: ${r.error}`); return 1; }
iese(rv, get('--out')); return 0;
}
case 'verify': {
const ledger = citesteJson(get('--ledger'), 'ledger');
const j = citesteJson(get('--policy'), 'policy');
const pinned = get('--policy-hash') ?? j.policyHash;
const v = verifyLedger(ledger, { policy: j.policy || j, policyHash: pinned, revocations: lista('--revocations').map((f) => citesteJson(f, 'revocations')),
anchors: get('--anchors') ? citesteJson(get('--anchors'), 'anchors') : [], notBefore: get('--not-before') != null ? Number(get('--not-before')) : null });
console.log(JSON.stringify(v, null, 1)); return v.ok ? 0 : 1;
}
case 'equivocation': {
const p = findEquivocation(citesteJson(get('--a'), 'a'), citesteJson(get('--b'), 'b'));
if (!p) { console.log(JSON.stringify({ found: false })); return 1; }
iese(p, get('--out')); if (get('--out')) console.log(JSON.stringify({ found: true, seq: p.seq })); return 0;
}
case 'verify-equivocation': {
const r = verifyEquivocation(citesteJson(get('--proof'), 'proof'));
console.log(JSON.stringify(r)); return r.ok ? 0 : 1;
}
default:
console.error('usage: agent-cli.mjs id|human|policy|check|record|approve|revoke|verify|equivocation|verify-equivocation ... (see README.md)');
return cmd ? 2 : 0;
}
}
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
let cod;
try { cod = main(process.argv.slice(2)); }
catch (e) { console.error(`agent: ${e.message}`); cod = e instanceof Folosire ? 2 : 1; }
process.exitCode = cod;
}