ai-trust/ links six existing proofs (agent identity, model artifact attestation, host runtime record, policy and signed ledger,
Proof of AI envelope, on-chain witness) into one bundle judged by one verifier that checks only the links; 13/13 in the lab,
16/16 live on testnet 28001 (ledger head notarized). proof-of-ai/ is the AIP-23 envelope builder it uses. verifiable-data/
publishes append-only versions with an RFC 9162 root and a sorted index per field, range and equality queries proven correct
and complete, selective disclosure per field, availability by sampling, and the AIP-23 data envelope notarized (version 2 on
testnet 28001). agents/agent-ancora.mjs gains notarizeEnvelope({kind}), the general form of notarizeHead. Each README says what
is not built: zero-knowledge queries need a prover and are not here. No third party has reviewed any of this.
163 lines
17 KiB
JavaScript
163 lines
17 KiB
JavaScript
#!/usr/bin/env node
|
|
// proba-lant-de-incredere.mjs: the AI trust chain end to end in the LABORATORY, with the real children (Proof of Software attest,
|
|
// the verify-layer sidecar, the agent ledger, Proof of AI, the AIP-23 reference verifier), then the fixtures (each link broken on
|
|
// purpose, refused for its named reason), then the plantings (--control-negativ: a copy of the verifier with one check removed must
|
|
// turn exactly its fixture ROSU). --viu adds the witness: the ledger head notarized on testnet 28001 with the L1 test key, read back
|
|
// under a post-quantum certified anchor, and the bundle judged VALID with --rpc.
|
|
// node proba-lant-de-incredere.mjs [--viu] [--control-negativ]
|
|
import crypto from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { createRequire } from 'node:module';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
import { newAgentIdentity, openLedger } from '../agents/agent-ledger.mjs';
|
|
import { definePolicy } from '../agents/agent-policy.mjs';
|
|
import { buildProofOfAi } from '../proof-of-ai/proof-of-ai.mjs';
|
|
import { ledgerHeadEnvelope, notarizeHead, cheiaDinFisier } from '../agents/agent-ancora.mjs';
|
|
|
|
const AICI = path.dirname(fileURLToPath(import.meta.url));
|
|
const RAD = path.resolve(AICI, '..', '..');
|
|
// the children: the sidecar lives beside this component (public copy: ../verify-layer/, rewritten at staging); the AIP-23 reference
|
|
// verifier comes from AERE_VERIFY_PROOF (public copy, as for agents/) or from the development repository; without it: NEMASURAT (2)
|
|
const SIDECAR = fileURLToPath(new URL('../verify-layer/sidecar.mjs', import.meta.url));
|
|
const VERIFY_PROOF = process.env.AERE_VERIFY_PROOF || path.join(RAD, 'verificator-falcon', 'verify-proof.mjs');
|
|
const RPC_28001 = process.env.AERE_TESTNET_RPC || 'https://testnet-rpc.aere.network';
|
|
// --viu: the notarization key (gas only, a test key) and the folder whose node_modules hold ethers
|
|
const CHEIE = process.env.AERE_NOTARY_KEY_FILE || path.join(RAD, 'tools', 'aere-quantum-shield', 'dovezi-l1', '.chei', 'cheie-de-proba-28001.key');
|
|
const ETHERS_DIR = process.env.AERE_ETHERS_DIR || path.join(RAD, 'contracts');
|
|
// the model artifact and its Proof of Software attestation are a COMMITTED fixture (fixturi/: 4 KB of random bytes attested with
|
|
// laboratory keys by tools/proof-of-software/pos.mjs, which is not part of this component); regenerate with
|
|
// node ../proof-of-software/pos.mjs keygen --out k.json && node ../proof-of-software/pos.mjs attest --out fixturi/atestare-model.json --name aere-model-proba --version 1.0.0 --keys k.json fixturi/model-weights.bin
|
|
const FIXTURI = path.join(AICI, 'fixturi');
|
|
if (!fs.existsSync(VERIFY_PROOF)) { console.log(`NEMASURAT: verificatorul AIP-23 nu e la ${VERIFY_PROOF}; AERE_VERIFY_PROOF=<verify-proof.mjs>`); process.exit(2); }
|
|
const sha = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex');
|
|
const run = (args, cwd) => { const r = spawnSync(process.execPath, args, { cwd, encoding: 'utf8', timeout: 180000 }); if (r.status !== 0) throw new Error(`${path.basename(args[0])} ${args[1]} exited ${r.status}: ${(r.stdout + r.stderr).slice(-300)}`); return r.stdout; };
|
|
const rehash = (env) => ({ ...env, statementHash: sha(Buffer.from(JSON.stringify(env.statement), 'utf8')) });
|
|
const clone = (x) => JSON.parse(JSON.stringify(x));
|
|
|
|
/** the laboratory pieces: a model artifact attested, a host that ran it, an agent allowed to use it, an output */
|
|
export function laborator({ toolAllowed = true } = {}) {
|
|
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-ai-trust-lab-'));
|
|
// version: the committed fixture (the artifact and its hybrid-signed Proof of Software attestation); the digest is recomputed here,
|
|
// never read from the attestation, so a fixture whose artifact and attestation drifted apart is caught at P1
|
|
const art = path.join(T, 'model-weights.bin'); fs.copyFileSync(path.join(FIXTURI, 'model-weights.bin'), art); const artSha = sha(fs.readFileSync(art));
|
|
const attestation = JSON.parse(fs.readFileSync(path.join(FIXTURI, 'atestare-model.json'), 'utf8'));
|
|
// runtime: the host declares the artifact running, in its audit log, 60 s before the output
|
|
const t0 = Math.floor(Date.now() / 1000) - 120, t1 = t0 + 60;
|
|
const iso = (t) => new Date(t * 1000).toISOString();
|
|
run([SIDECAR, 'record', '--kind', 'runtime', '--artifact', art, '--attested', artSha, '--host', 'lab-probe', '--log', path.join(T, 'audit.log'), '--at', iso(t0)], T);
|
|
run([SIDECAR, 'attest-head', '--log', path.join(T, 'audit.log'), '--host', 'lab-probe', '--out', path.join(T, 'head.json')], T);
|
|
const auditLog = fs.readFileSync(path.join(T, 'audit.log'), 'utf8').split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l));
|
|
const record = auditLog[0].proof; const head = JSON.parse(fs.readFileSync(path.join(T, 'head.json'), 'utf8'));
|
|
// permissions + identity: an agent allowed (or not) to call the model tool
|
|
const identity = newAgentIdentity();
|
|
const { policy, policyHash } = definePolicy({ agentId: identity.agentId, tools: toolAllowed ? ['model:aere-model-proba'] : ['model:other'] });
|
|
const ledger = openLedger({ identity, policy, policyHash, now: () => t1 });
|
|
// provenance: the output, naming the attested artifact and the agent
|
|
const proofOfAi = buildProofOfAi({ model: { name: 'aere-model-proba', version: '1.0.0', weightsSha256: artSha }, prompt: 'summarise document X', input: 'document X, 12 kB', output: 'a summary of document X', tools: ['model:aere-model-proba'], agentId: identity.agentId, requester: 'tester', createdAt: iso(t1) });
|
|
const r = ledger.record({ kind: 'tool', tool: 'model:aere-model-proba', args: { promptHash: proofOfAi.statement.promptHash } }, { provenance: proofOfAi, at: t1 });
|
|
return { T, art, artSha, attestation, record, auditLog, head, identity, policy, policyHash, ledger, proofOfAi, allowed: r.allowed, t0, t1 };
|
|
}
|
|
|
|
export async function proba(mod, cer, { viu = false } = {}) {
|
|
const L = laborator();
|
|
const bundle = mod.construieste({ ledger: L.ledger.export(), policy: { policy: L.policy, policyHash: L.policyHash }, proofOfAi: L.proofOfAi, software: { attestation: L.attestation }, runtime: { record: L.record, auditLog: L.auditLog, head: L.head } });
|
|
const v0 = await mod.verifica(bundle, { verifyProof: VERIFY_PROOF });
|
|
const toate = ['identity', 'version', 'runtime', 'permissions', 'provenance'];
|
|
cer('P1', 'laborator: toate cele cinci fatete PASSED, martorul ABSENT, verdict PARTIAL (unwitnessed)', toate.every((l) => v0.levels[l] === 'PASSED') && v0.levels.witness === 'ABSENT' && v0.verdict === 'PARTIAL' && v0.reason === 'unwitnessed' ? 'VERDE' : 'ROSU', `${v0.verdict} (${v0.reason}) ${JSON.stringify(v0.levels)}`);
|
|
cer('P2', 'intrarea registrului a fost PERMISA de politica (unealta model:aere-model-proba in lista)', L.allowed ? 'VERDE' : 'ROSU');
|
|
const v1 = await mod.verifica(mod.construieste({ ledger: L.ledger.export(), policy: { policy: L.policy, policyHash: L.policyHash }, proofOfAi: L.proofOfAi }), { verifyProof: VERIFY_PROOF });
|
|
cer('P3', 'fara versiune si fara runtime: nivelurile ABSENT, restul PASSED, tot PARTIAL unwitnessed (ce lipseste e spus, nu inventat)', v1.levels.version === 'ABSENT' && v1.levels.runtime === 'ABSENT' && v1.levels.permissions === 'PASSED' && v1.verdict === 'PARTIAL' ? 'VERDE' : 'ROSU', JSON.stringify(v1.levels));
|
|
// fixtures: each link broken on purpose
|
|
const fix = [
|
|
['F1', 'iesirea schimbata in Proof of AI (hash refacut): nu mai e intrarea din registru -> permissions FAILED output-not-in-ledger', (b) => { b.provenance.proofOfAi.statement.outputHash = sha(Buffer.from('another output')); b.provenance.proofOfAi = rehash(b.provenance.proofOfAi); }, 'permissions', 'output-not-in-ledger'],
|
|
['F2', 'digestul ponderilor schimbat in Proof of AI: nu e artefactul atestat -> version FAILED version-artifact-mismatch', (b) => { b.provenance.proofOfAi.statement.model.weightsSha256 = '0x' + 'ab'.repeat(32); b.provenance.proofOfAi = rehash(b.provenance.proofOfAi); }, 'version', 'version-artifact-mismatch'],
|
|
['F3', 'alt agentId in Proof of AI: identity FAILED identity-mismatch', (b) => { b.provenance.proofOfAi.statement.agentId = 'aere-agent:' + '11'.repeat(20); b.provenance.proofOfAi = rehash(b.provenance.proofOfAi); }, 'identity', 'identity-mismatch'],
|
|
['F4', 'inregistrarea de runtime numeste alt artefact: runtime FAILED runtime-artifact-mismatch', (b) => { b.runtime.record.statement.artifactSha256 = '0x' + 'cd'.repeat(32); b.runtime.record = rehash(b.runtime.record); }, 'runtime', 'runtime-artifact-mismatch'],
|
|
['F6', 'inregistrarea de runtime datata DUPA iesire: runtime FAILED runtime-after-output', (b) => { b.runtime.record.statement.createdAt = new Date((L.t1 + 3600) * 1000).toISOString(); b.runtime.record = rehash(b.runtime.record); }, 'runtime', 'runtime-after-output'],
|
|
['F7', 'o intrare a registrului modificata (argumentele): permissions FAILED ledger-invalid', (b) => { b.permissions.ledger.entries[0].body.action.args = { promptHash: '0x' + '00'.repeat(32) }; }, 'permissions', 'ledger-invalid'],
|
|
['F8', 'jurnalul de audit fara inregistrarea de runtime: runtime FAILED runtime-not-in-audit-log', (b) => { b.runtime.auditLog = []; b.runtime.head = null; }, 'runtime', 'runtime-not-in-audit-log'],
|
|
['F9', 'atestarea Proof of Software modificata dupa hash: version FAILED version-attestation', (b) => { b.version.attestation.statement.artifacts[0].sha256 = '0x' + 'ef'.repeat(32); }, 'version', 'version-attestation'],
|
|
['F10', 'jurnalul de audit cu lantul rupt (hash-ul intrarii schimbat): runtime FAILED audit-log-broken', (b) => { b.runtime.auditLog[0].hash = '0x' + '77'.repeat(32); }, 'runtime', 'audit-log-broken'],
|
|
];
|
|
for (const [id, eticheta, muta, nivel, motiv] of fix) {
|
|
const b = clone(bundle); muta(b);
|
|
const v = await mod.verifica(b, { verifyProof: VERIFY_PROOF });
|
|
const ok = v.verdict === 'INVALID' && v.levels[nivel] === 'FAILED' && v.details[nivel].reason === motiv;
|
|
cer(id, eticheta, ok ? 'VERDE' : 'ROSU', `${v.verdict} (${v.reason}); ${nivel} ${v.levels[nivel]} ${v.details[nivel].reason || ''}`);
|
|
}
|
|
{
|
|
const L2 = laborator({ toolAllowed: false });
|
|
const b = mod.construieste({ ledger: L2.ledger.export(), policy: { policy: L2.policy, policyHash: L2.policyHash }, proofOfAi: L2.proofOfAi, software: { attestation: L2.attestation }, runtime: { record: L2.record, auditLog: L2.auditLog } });
|
|
const v = await mod.verifica(b, { verifyProof: VERIFY_PROOF });
|
|
cer('F5', 'unealta nepermisa de politica: intrarea REFUZATA exista, iesirea nu poate fi "permisa" -> permissions FAILED action-refused', !L2.allowed && v.levels.permissions === 'FAILED' && v.details.permissions.reason === 'action-refused' ? 'VERDE' : 'ROSU', `${v.verdict} ${v.details.permissions.reason || ''}`);
|
|
}
|
|
if (viu) {
|
|
// the witness: the ledger head notarized on 28001 with the laboratory test key (gas only), read back under a certified anchor
|
|
const require = createRequire(path.join(ETHERS_DIR, 'package.json'));
|
|
const { ethers } = require('ethers');
|
|
const headEnv = ledgerHeadEnvelope(L.ledger.export());
|
|
let notarized;
|
|
// notarizeHead returns { ok, envelope }: the ENVELOPE (with its notarization) is the head the witness level reads
|
|
try { const r = await notarizeHead({ envelope: headEnv, rpc: RPC_28001, keyFile: CHEIE, ethers }); if (!r || !r.ok) throw new Error('notarizeHead: ' + JSON.stringify(r).slice(0, 160)); notarized = r.envelope; } catch (e) { cer('V1', 'capul registrului notarizat pe 28001', 'STRICAT', String(e.message).slice(0, 160)); return; }
|
|
cer('V1', 'capul registrului notarizat pe 28001 (cheia de proba L1, numai gaz)', notarized && notarized.notarization ? 'VERDE' : 'ROSU', notarized && notarized.notarization ? `tx ${String(notarized.notarization.txHash).slice(0, 14)}.. block ${notarized.notarization.block}` : JSON.stringify(notarized).slice(0, 120));
|
|
const bw = mod.construieste({ ledger: L.ledger.export(), policy: { policy: L.policy, policyHash: L.policyHash }, proofOfAi: L.proofOfAi, software: { attestation: L.attestation }, runtime: { record: L.record, auditLog: L.auditLog, head: L.head }, witness: { heads: [notarized], chainId: 28001 } });
|
|
let vw = null;
|
|
for (let i = 0; i < 20; i++) { vw = await mod.verifica(bw, { verifyProof: VERIFY_PROOF, rpc: RPC_28001, chain: 28001 }); if (vw.levels.witness !== 'UNMEASURED') break; await new Promise((ok) => setTimeout(ok, 8000)); }
|
|
cer('V2', 'cu martorul citit sub o ancora certificata (--rpc 28001): witness PASSED, verdict VALID', vw && vw.verdict === 'VALID' && vw.levels.witness === 'PASSED' ? 'VERDE' : (vw && vw.levels.witness === 'UNMEASURED' ? 'STRICAT' : 'ROSU'), vw ? `${vw.verdict} (${vw.reason}); witness ${vw.levels.witness}: ${String(vw.details.witness.detail || vw.details.witness.reason || '').slice(0, 160)}` : 'fara verdict');
|
|
const vb = await mod.verifica(bw, { verifyProof: VERIFY_PROOF });
|
|
cer('V3', 'acelasi pachet FARA --rpc: martorul UNMEASURED (witness-needs-rpc), nu PASSED', vb.levels.witness === 'UNMEASURED' && vb.verdict === 'PARTIAL' ? 'VERDE' : 'ROSU', `${vb.verdict} (${vb.reason})`);
|
|
const d = path.join(RAD, 'tools', 'aere-ai-trust', 'dovezi'); fs.mkdirSync(d, { recursive: true });
|
|
const f = path.join(d, `lant-28001-${new Date().toISOString().replace(/[:.]/g, '').slice(0, 15)}Z.json`);
|
|
fs.writeFileSync(f, JSON.stringify({ bundle: bw, verdict: vw }, null, 1)); console.log(' dovada scrisa: ' + f);
|
|
}
|
|
}
|
|
|
|
if (process.argv.includes('--control-negativ')) {
|
|
const SURSA = fs.readFileSync(path.join(AICI, 'lant-de-incredere.mjs'), 'utf8');
|
|
const PLANTARI = [
|
|
{ nume: '(a) identitatea necomparata', de: '/*PAZA:identity*/', cu: "|| String(1) === '1' /*PLANTAT*/", rosii: ['F3'] },
|
|
{ nume: '(b) artefactul atestat necomparat cu ponderile', de: '/*PAZA:version*/', cu: "|| String(1) === '1' /*PLANTAT*/", rosii: ['F2'] },
|
|
{ nume: '(c) artefactul rulat necomparat', de: '/*PAZA:runtime*/', cu: "|| String(1) === '1' /*PLANTAT*/", rosii: ['F4'] },
|
|
{ nume: '(d) ordinea timpilor nejudecata', de: '/*PAZA:order*/', cu: "|| String(1) === '1' /*PLANTAT*/", rosii: ['F6'] },
|
|
{ nume: '(e) iesirea necautata in registru (prima intrare luata oricum)', de: '/*PAZA:output-in-ledger*/', cu: '|| ledger.entries[0] /*PLANTAT*/', rosii: ['F1'] },
|
|
// (f) and (g) sit inside a NEGATED condition (`if (!(x) /*PAZA*/)`): the planting that silences the check is `&& false`, not `|| true`
|
|
// (measured 2026-10-02: `|| true` made the honest rows fail instead of letting the fixture pass)
|
|
{ nume: '(f) decizia refuzata luata drept permisa', de: '/*PAZA:allowed*/', cu: "&& String(1) === '2' /*PLANTAT*/", rosii: ['F5'] },
|
|
{ nume: '(g) inregistrarea necautata in jurnalul de audit', de: '/*PAZA:audit*/', cu: "&& String(1) === '2' /*PLANTAT*/", rosii: ['F8'] },
|
|
{ nume: '(h) controlul controlului: o ancora care nu exista', de: '/*PAZA:nu-exista*/', cu: 'x', rosii: [], asteapta: 'STRICAT' },
|
|
];
|
|
let bune = 0;
|
|
for (const [k, p] of PLANTARI.entries()) {
|
|
const n = SURSA.split(p.de).length - 1;
|
|
const copie = path.join(AICI, `_plantat-lant-${process.pid}-${k}.mjs`);
|
|
let stare, detaliu = '';
|
|
try {
|
|
if (n !== 1) { stare = 'STRICAT'; detaliu = `ancora apare de ${n} ori (cerut 1)`; }
|
|
else {
|
|
fs.writeFileSync(copie, SURSA.replace(p.de, p.cu));
|
|
const mod = await import(pathToFileURL(copie).href);
|
|
const randuri = [];
|
|
// a probe that THROWS on the planted copy is STRICAT for that planting, not the end of the control
|
|
try { await proba(mod, (id, eticheta, st, det) => randuri.push({ id, stare: st, det })); } catch (e) { randuri.push({ id: 'proba', stare: 'STRICAT', det: String(e.message).slice(0, 160) }); }
|
|
const stricate = randuri.filter((x) => x.stare === 'STRICAT'); const rosii = randuri.filter((x) => x.stare === 'ROSU').map((x) => x.id);
|
|
if (stricate.length) { stare = 'STRICAT'; detaliu = 'proba a cazut: ' + stricate.map((x) => x.id + ' ' + x.det).join(' | ').slice(0, 200); }
|
|
else { const prezise = p.rosii.every((id) => rosii.includes(id)); const numai = rosii.every((id) => p.rosii.includes(id)); stare = prezise && numai ? 'VERDE' : 'ROSU'; detaliu = `rosii: ${rosii.join(',') || '-'} (prezis ${p.rosii.join(',') || '-'})`; }
|
|
}
|
|
} finally { fs.rmSync(copie, { force: true }); }
|
|
const ok = stare === (p.asteapta || 'VERDE'); if (ok) bune++;
|
|
console.log(` ${ok ? 'BINE' : 'ESEC'} ${p.nume}: ${stare}${p.asteapta ? ' (prezis ' + p.asteapta + ')' : ''} ${detaliu}`);
|
|
}
|
|
const ramase = fs.readdirSync(AICI).filter((f) => f.startsWith('_plantat-lant-'));
|
|
console.log(`CONTROL: ${bune} din ${PLANTARI.length} cum s-a prezis${ramase.length ? '; copii ramase: ' + ramase.join(',') : ''}`);
|
|
process.exitCode = bune === PLANTARI.length && !ramase.length ? 0 : 1;
|
|
} else {
|
|
const mod = await import('./lant-de-incredere.mjs');
|
|
let verzi = 0, rosii = 0, stricate = 0;
|
|
await proba(mod, (id, eticheta, stare, det) => { if (stare === 'VERDE') verzi++; else if (stare === 'ROSU') rosii++; else stricate++; console.log(` [${stare.padEnd(7)}] ${id.padEnd(4)} ${eticheta}${det ? ' ' + det : ''}`); }, { viu: process.argv.includes('--viu') });
|
|
console.log(`\nlantul de incredere pentru AI: ${verzi} VERDE, ${rosii} ROSU, ${stricate} STRICAT`);
|
|
process.exitCode = rosii || stricate ? 1 : 0;
|
|
}
|