#!/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
new agent identity; prints the agentId
// node agent-cli.mjs human --out 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 ] [--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 ]
// 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 `);
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;
}