@zanii/blackbox 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -1
- package/dist/a2a/index.d.ts +27 -0
- package/dist/a2a/index.js +104 -1
- package/dist/agents/index.d.ts +8 -0
- package/dist/analysis/accuracy.d.ts +24 -0
- package/dist/analysis/accuracy.js +45 -0
- package/dist/analysis/credential.d.ts +101 -0
- package/dist/analysis/credential.js +142 -0
- package/dist/analysis/faults.js +115 -0
- package/dist/analysis/grounding.d.ts +122 -0
- package/dist/analysis/grounding.js +445 -0
- package/dist/analysis/hallucination.d.ts +32 -0
- package/dist/analysis/hallucination.js +357 -0
- package/dist/analysis/index.d.ts +23 -0
- package/dist/analysis/index.js +93 -0
- package/dist/analysis/memory.d.ts +8 -0
- package/dist/analysis/memory.js +35 -8
- package/dist/analysis/reference.d.ts +49 -0
- package/dist/analysis/reference.js +164 -0
- package/dist/analysis/taxonomy.js +1 -0
- package/dist/approvals/index.d.ts +23 -0
- package/dist/approvals/index.js +48 -0
- package/dist/archive/parquet.d.ts +2 -0
- package/dist/archive/parquet.js +185 -0
- package/dist/badge/index.d.ts +16 -0
- package/dist/badge/index.js +48 -0
- package/dist/bom/index.js +20 -0
- package/dist/cli.js +114 -10
- package/dist/compliance/art12.js +36 -9
- package/dist/compliance/index.d.ts +36 -2
- package/dist/compliance/index.js +78 -11
- package/dist/compliance/zanii.d.ts +29 -0
- package/dist/compliance/zanii.js +84 -0
- package/dist/constitution/index.d.ts +57 -0
- package/dist/constitution/index.js +131 -0
- package/dist/cv/index.d.ts +39 -0
- package/dist/cv/index.js +108 -0
- package/dist/disclosure/index.d.ts +31 -0
- package/dist/disclosure/index.js +113 -0
- package/dist/encryption/index.d.ts +9 -0
- package/dist/encryption/index.js +31 -0
- package/dist/evidence/index.d.ts +60 -0
- package/dist/evidence/index.js +151 -0
- package/dist/federation/index.d.ts +35 -0
- package/dist/federation/index.js +102 -0
- package/dist/finance/index.d.ts +126 -0
- package/dist/finance/index.js +320 -0
- package/dist/fleet/index.js +9 -0
- package/dist/gov/index.d.ts +108 -0
- package/dist/gov/index.js +225 -0
- package/dist/health/index.d.ts +120 -0
- package/dist/health/index.js +233 -0
- package/dist/index.d.ts +28 -6
- package/dist/index.js +28 -6
- package/dist/memory/index.d.ts +36 -0
- package/dist/memory/index.js +85 -0
- package/dist/occurrence/index.d.ts +11 -0
- package/dist/occurrence/index.js +18 -0
- package/dist/otlp/index.js +28 -1
- package/dist/packs/index.js +44 -4
- package/dist/policy/delta.js +7 -1
- package/dist/policy/index.d.ts +23 -6
- package/dist/policy/index.js +151 -8
- package/dist/policy/zanii.d.ts +31 -0
- package/dist/policy/zanii.js +87 -0
- package/dist/pq/index.d.ts +23 -0
- package/dist/pq/index.js +104 -0
- package/dist/search/index.d.ts +23 -0
- package/dist/search/index.js +69 -0
- package/dist/session/index.d.ts +89 -1
- package/dist/session/index.js +143 -11
- package/dist/sla/index.d.ts +61 -0
- package/dist/sla/index.js +197 -0
- package/dist/succession/index.d.ts +50 -0
- package/dist/succession/index.js +123 -0
- package/dist/tokens/index.d.ts +6 -0
- package/dist/tokens/index.js +46 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walls/index.d.ts +31 -0
- package/dist/walls/index.js +119 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -37,6 +37,10 @@ const USAGE = `usage (spec/cli.md):
|
|
|
37
37
|
blackbox analyze <bundle.json> [--bodies <dir>] [--prices <file>]
|
|
38
38
|
blackbox verify-certificate <certificate.json> [--did <did:key:…>]
|
|
39
39
|
blackbox investigate <bundle.json> [--bodies <dir>] [--directives <file>] [--identity <file>] [--proofs] [--out <prefix>]
|
|
40
|
+
blackbox evidence-pack <bundle.json> [--proofs] [--identity <file>] [--lang en|ar|both] [--out <prefix>]
|
|
41
|
+
blackbox sign-constitution <draft.json> --identity <owner.json> [--out <file>]
|
|
42
|
+
blackbox policy-from-zanii <zanii-policy.json> [--out <file>]
|
|
43
|
+
blackbox policy-to-zanii <policy.json> [--out <file>]
|
|
40
44
|
blackbox statement --provider anthropic|openai --from <YYYY-MM-DD> --to <YYYY-MM-DD> [--out <file>]
|
|
41
45
|
blackbox statement --provider csv --file <f> --model-column <name> --amount-column <name> --unit usd|cents|micro_usd|aed [--delimiter <c>] --from <d> --to <d> [--out <file>]
|
|
42
46
|
blackbox statement --provider custom --connector <file> --from <d> --to <d> [--out <file>]
|
|
@@ -88,6 +92,12 @@ async function main(argv) {
|
|
|
88
92
|
return preflightCommand(rest);
|
|
89
93
|
if (command === "investigate")
|
|
90
94
|
return investigateCommand(rest);
|
|
95
|
+
if (command === "evidence-pack")
|
|
96
|
+
return evidencePackCommand(rest);
|
|
97
|
+
if (command === "sign-constitution")
|
|
98
|
+
return signConstitutionCommand(rest);
|
|
99
|
+
if (command === "policy-from-zanii" || command === "policy-to-zanii")
|
|
100
|
+
return policyZaniiCommand(command, rest);
|
|
91
101
|
if (command === "statement")
|
|
92
102
|
return statementCommand(rest);
|
|
93
103
|
if (command === "export-all")
|
|
@@ -607,6 +617,107 @@ async function certificateCommand(rest) {
|
|
|
607
617
|
return result.ok ? 0 : 1;
|
|
608
618
|
}
|
|
609
619
|
/** spec/investigations.md §2: an independent investigator's signed report, offline. */
|
|
620
|
+
/** A signing identity file `{did, private_key}` (the gateway's identity file works as is). */
|
|
621
|
+
async function readIdentity(path) {
|
|
622
|
+
const { didFromSeed } = await import("./fleet/index.js");
|
|
623
|
+
const file = JSON.parse(readFileSync(path, "utf8"));
|
|
624
|
+
if (typeof file.private_key !== "string" || !/^[0-9a-f]{64}$/.test(file.private_key))
|
|
625
|
+
throw new Error("identity file: private_key must be 64 hex characters");
|
|
626
|
+
const seed = Buffer.from(file.private_key, "hex");
|
|
627
|
+
if (file.did !== didFromSeed(seed))
|
|
628
|
+
throw new Error("identity file: did doesn't match private_key");
|
|
629
|
+
return { did: file.did, seed };
|
|
630
|
+
}
|
|
631
|
+
/** spec/policy.md §8: a Zanii policy file as a Blackbox one, or back; skipped rules are named. */
|
|
632
|
+
async function policyZaniiCommand(command, rest) {
|
|
633
|
+
const { flags, positional } = parse(rest, []);
|
|
634
|
+
if (positional.length !== 1)
|
|
635
|
+
return usage();
|
|
636
|
+
const { fromZaniiPolicy, toZaniiPolicy } = await import("./policy/zanii.js");
|
|
637
|
+
try {
|
|
638
|
+
const input = JSON.parse(readFileSync(positional[0], "utf8"));
|
|
639
|
+
const r = command === "policy-from-zanii" ? fromZaniiPolicy(input) : toZaniiPolicy(input);
|
|
640
|
+
const out = flags.out || (command === "policy-from-zanii" ? "policy.json" : "zanii-policy.json");
|
|
641
|
+
writeFileSync(out, `${JSON.stringify(r.policy, null, 2)}\n`);
|
|
642
|
+
process.stdout.write(`${JSON.stringify({ ok: true, file: out, rules: r.policy.rules.length, skipped: r.skipped })}\n`);
|
|
643
|
+
return 0;
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: error.message })}\n`);
|
|
647
|
+
return 2;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/** spec/governance.md §1: signs a draft rulebook `{name, rules, default?}` as the identity's DID. */
|
|
651
|
+
async function signConstitutionCommand(rest) {
|
|
652
|
+
const { flags, positional } = parse(rest, []);
|
|
653
|
+
if (positional.length !== 1 || !flags.identity)
|
|
654
|
+
return usage();
|
|
655
|
+
const { constitutionHash, createConstitution } = await import("./constitution/index.js");
|
|
656
|
+
try {
|
|
657
|
+
const draft = JSON.parse(readFileSync(positional[0], "utf8"));
|
|
658
|
+
const identity = await readIdentity(flags.identity);
|
|
659
|
+
const c = createConstitution({
|
|
660
|
+
issuer: identity.did,
|
|
661
|
+
name: String(draft.name ?? ""),
|
|
662
|
+
rules: (Array.isArray(draft.rules) ? draft.rules : []),
|
|
663
|
+
defaultEffect: (draft.default ?? "allow"),
|
|
664
|
+
createdAt: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
665
|
+
issuerPrivateKey: identity.seed,
|
|
666
|
+
});
|
|
667
|
+
const out = flags.out || "constitution.json";
|
|
668
|
+
writeFileSync(out, `${JSON.stringify(c, null, 2)}\n`);
|
|
669
|
+
process.stdout.write(`${JSON.stringify({ ok: true, file: out, manifest_hash: constitutionHash(c), issuer: c.issuer })}\n`);
|
|
670
|
+
return 0;
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: error.message })}\n`);
|
|
674
|
+
return 2;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
/** spec/investigations.md §7: the court evidence pack, `<prefix>.json` (signed) and `<prefix>.md`. */
|
|
678
|
+
async function evidencePackCommand(rest) {
|
|
679
|
+
const { flags, positional } = parse(rest, ["--proofs"]);
|
|
680
|
+
const lang = flags.lang ?? "both";
|
|
681
|
+
if (positional.length !== 1 || !["en", "ar", "both"].includes(lang))
|
|
682
|
+
return usage();
|
|
683
|
+
const fail = (error) => {
|
|
684
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: String(error) })}\n`);
|
|
685
|
+
return 2;
|
|
686
|
+
};
|
|
687
|
+
const { evidencePack, renderEvidencePack } = await import("./evidence/index.js");
|
|
688
|
+
const { signCertificate } = await import("./fleet/index.js");
|
|
689
|
+
let raw;
|
|
690
|
+
let bundle;
|
|
691
|
+
let identity;
|
|
692
|
+
try {
|
|
693
|
+
raw = readFileSync(positional[0]);
|
|
694
|
+
bundle = JSON.parse(raw.toString("utf8"));
|
|
695
|
+
if (!Array.isArray(bundle.lines) || bundle.lines.length === 0)
|
|
696
|
+
throw new Error("the bundle has no lines");
|
|
697
|
+
if (flags.identity)
|
|
698
|
+
identity = await readIdentity(flags.identity);
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
return fail(error instanceof Error ? error.message : error);
|
|
702
|
+
}
|
|
703
|
+
const proofs = flags.proofs === "1";
|
|
704
|
+
const pack = evidencePack(bundle, await verifyBundle(bundle, { proofs }), {
|
|
705
|
+
generated_at: new Date().toISOString(),
|
|
706
|
+
bundle_sha256: `sha256:${createHash("sha256").update(raw).digest("hex")}`,
|
|
707
|
+
proofs: proofs ? "checked" : "not_checked",
|
|
708
|
+
});
|
|
709
|
+
const final = identity ? signCertificate(pack, identity.did, identity.seed) : pack;
|
|
710
|
+
const prefix = flags.out || `evidence-${pack.session_id}`;
|
|
711
|
+
try {
|
|
712
|
+
writeFileSync(`${prefix}.json`, `${JSON.stringify(final)}\n`);
|
|
713
|
+
writeFileSync(`${prefix}.md`, renderEvidencePack(pack, lang));
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
return fail(error instanceof Error ? error.message : error);
|
|
717
|
+
}
|
|
718
|
+
process.stdout.write(`${JSON.stringify({ ok: pack.verified, json: `${prefix}.json`, md: `${prefix}.md`, signed_by: identity?.did ?? null })}\n`);
|
|
719
|
+
return pack.verified ? 0 : 1;
|
|
720
|
+
}
|
|
610
721
|
async function investigateCommand(rest) {
|
|
611
722
|
const { flags, positional } = parse(rest, ["--proofs"]);
|
|
612
723
|
if (positional.length !== 1)
|
|
@@ -617,7 +728,7 @@ async function investigateCommand(rest) {
|
|
|
617
728
|
};
|
|
618
729
|
const { investigation, renderInvestigation } = await import("./investigate/index.js");
|
|
619
730
|
const { loadPrices, sessionCost } = await import("./cost/index.js");
|
|
620
|
-
const {
|
|
731
|
+
const { sessionSummary, signCertificate } = await import("./fleet/index.js");
|
|
621
732
|
let raw;
|
|
622
733
|
let bundle;
|
|
623
734
|
let directives;
|
|
@@ -637,15 +748,8 @@ async function investigateCommand(rest) {
|
|
|
637
748
|
throw new Error("--directives must hold a list of directives");
|
|
638
749
|
directives = list;
|
|
639
750
|
}
|
|
640
|
-
if (flags.identity)
|
|
641
|
-
|
|
642
|
-
if (typeof file.private_key !== "string" || !/^[0-9a-f]{64}$/.test(file.private_key))
|
|
643
|
-
throw new Error("identity file: private_key must be 64 hex characters");
|
|
644
|
-
const seed = Buffer.from(file.private_key, "hex");
|
|
645
|
-
if (file.did !== didFromSeed(seed))
|
|
646
|
-
throw new Error("identity file: did doesn't match private_key");
|
|
647
|
-
identity = { did: file.did, seed };
|
|
648
|
-
}
|
|
751
|
+
if (flags.identity)
|
|
752
|
+
identity = await readIdentity(flags.identity);
|
|
649
753
|
}
|
|
650
754
|
catch (error) {
|
|
651
755
|
return fail(error instanceof Error ? error.message : error);
|
package/dist/compliance/art12.js
CHANGED
|
@@ -4,9 +4,26 @@
|
|
|
4
4
|
// Mirrors sdks/python/src/zanii_blackbox/art12.py.
|
|
5
5
|
/** Art. 19(1) and 26(6): "at least six months". Counted as 183 days, the longest six months. */
|
|
6
6
|
export const ART12_MIN_RETENTION_DAYS = 183;
|
|
7
|
-
|
|
7
|
+
// "ciba" closes a call its people already voted on (spec/approvals.md §7): the votes are counted
|
|
8
|
+
const NOT_PEOPLE = new Set(["timeout", "unattended", "classifier", "ciba"]);
|
|
8
9
|
const NOTES = "Evidence mapping from the public text of Regulation (EU) 2024/1689, not legal advice. Article 12 applies to high-risk AI systems; whether a system is one, and whether it complies, is for its provider, deployer and counsel to judge. One session shows one period of use, not the system's lifetime.";
|
|
9
10
|
const sorted = (s) => [...new Set(s)].sort();
|
|
11
|
+
/** The person an approval decision names: an SSO login (authenticated) or a key holder's word. */
|
|
12
|
+
function personOf(p) {
|
|
13
|
+
if (typeof p !== "object" || p === null)
|
|
14
|
+
return null;
|
|
15
|
+
const o = p;
|
|
16
|
+
const s = (v) => (typeof v === "string" && v !== "" ? v : undefined);
|
|
17
|
+
if (o.authenticated === true && s(o.sub))
|
|
18
|
+
return {
|
|
19
|
+
key: `sub:${s(o.iss) ?? ""}#${o.sub}`,
|
|
20
|
+
label: s(o.email) ?? s(o.name) ?? o.sub,
|
|
21
|
+
authenticated: true,
|
|
22
|
+
};
|
|
23
|
+
if (o.asserted === true && s(o.name))
|
|
24
|
+
return { key: `name:${o.name}`, label: o.name, authenticated: false };
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
10
27
|
const count = (into, key) => {
|
|
11
28
|
into[key] = (into[key] ?? 0) + 1;
|
|
12
29
|
};
|
|
@@ -29,8 +46,8 @@ export function art12Check(record, options) {
|
|
|
29
46
|
const models = [];
|
|
30
47
|
const providers = [];
|
|
31
48
|
const sources = [];
|
|
32
|
-
|
|
33
|
-
|
|
49
|
+
// spec/approvals.md §2.1: each human decision, and the person it names (if any)
|
|
50
|
+
const human = [];
|
|
34
51
|
for (const e of events) {
|
|
35
52
|
const m = e.meta;
|
|
36
53
|
if (e.kind === "capture.gap")
|
|
@@ -45,9 +62,11 @@ export function art12Check(record, options) {
|
|
|
45
62
|
else if (e.kind === "control" && typeof m.action === "string") {
|
|
46
63
|
count(controls, m.action);
|
|
47
64
|
if (m.action === "approval" && typeof m.by === "string") {
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
if (!NOT_PEOPLE.has(m.by))
|
|
66
|
+
human.push(personOf(m.person));
|
|
50
67
|
}
|
|
68
|
+
if (m.action === "approval_vote" && m.counted !== false && m.person !== undefined)
|
|
69
|
+
human.push(personOf(m.person));
|
|
51
70
|
}
|
|
52
71
|
else if (e.kind === "llm.request") {
|
|
53
72
|
inputs++;
|
|
@@ -70,7 +89,9 @@ export function art12Check(record, options) {
|
|
|
70
89
|
}
|
|
71
90
|
const open = events.find((e) => e.kind === "session.open");
|
|
72
91
|
const close = events.find((e) => e.kind === "session.close");
|
|
73
|
-
const
|
|
92
|
+
const named = human.filter((p) => p !== null);
|
|
93
|
+
const persons = new Map(named.map((p) => [p.key, p.label]));
|
|
94
|
+
const authenticated = named.filter((p) => p.authenticated).length;
|
|
74
95
|
const retention = options.retentionDays;
|
|
75
96
|
const checks = [
|
|
76
97
|
{
|
|
@@ -139,9 +160,15 @@ export function art12Check(record, options) {
|
|
|
139
160
|
article: "12(3)(d), 14(5)",
|
|
140
161
|
requirement: "The natural persons who verified the results; for Art. 14(5), at least two, separately.",
|
|
141
162
|
applies: annex,
|
|
142
|
-
//
|
|
143
|
-
ok:
|
|
144
|
-
evidence: {
|
|
163
|
+
// every human decision names its person, and at least two different people decided
|
|
164
|
+
ok: human.length === 0 ? null : named.length === human.length && persons.size >= 2,
|
|
165
|
+
evidence: {
|
|
166
|
+
decisions: human.length,
|
|
167
|
+
persons: sorted(persons.values()),
|
|
168
|
+
authenticated,
|
|
169
|
+
asserted: named.length - authenticated,
|
|
170
|
+
unnamed: human.length - named.length,
|
|
171
|
+
},
|
|
145
172
|
},
|
|
146
173
|
{
|
|
147
174
|
id: "log-retention",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type FactValue = number | string | null | Record<string, number>;
|
|
1
|
+
export type FactValue = number | string | null | Record<string, number | string>;
|
|
2
2
|
export type Facts = Record<string, FactValue>;
|
|
3
3
|
export interface Framework {
|
|
4
4
|
title: string;
|
|
@@ -8,6 +8,14 @@ export interface Framework {
|
|
|
8
8
|
requirement: string;
|
|
9
9
|
facts: string[];
|
|
10
10
|
}>;
|
|
11
|
+
/** spec/packs.md §1: the framework in Arabic, by section id. */
|
|
12
|
+
ar?: {
|
|
13
|
+
title: string;
|
|
14
|
+
sections: Record<string, {
|
|
15
|
+
title: string;
|
|
16
|
+
requirement: string;
|
|
17
|
+
}>;
|
|
18
|
+
};
|
|
11
19
|
}
|
|
12
20
|
export interface Report {
|
|
13
21
|
framework: string;
|
|
@@ -24,6 +32,8 @@ export interface Report {
|
|
|
24
32
|
evidence: Facts;
|
|
25
33
|
}>;
|
|
26
34
|
notes: string;
|
|
35
|
+
/** "ar" when the report is in Arabic (spec/compliance.md §3). */
|
|
36
|
+
lang?: "ar";
|
|
27
37
|
}
|
|
28
38
|
export declare function complianceReport(frameworkId: string, framework: Framework, facts: Facts, meta: {
|
|
29
39
|
tenant_id: string;
|
|
@@ -31,7 +41,8 @@ export declare function complianceReport(frameworkId: string, framework: Framewo
|
|
|
31
41
|
to: string;
|
|
32
42
|
generated_at: string;
|
|
33
43
|
notes: string;
|
|
34
|
-
|
|
44
|
+
notes_ar?: string;
|
|
45
|
+
}, lang?: "en" | "ar"): Report;
|
|
35
46
|
export declare function renderReport(r: Report): string;
|
|
36
47
|
/** spec/compliance.md §1: the facts over sessions' records (the server adds verified / anchored). */
|
|
37
48
|
export declare function complianceFacts(sessions: ReadonlyArray<{
|
|
@@ -39,3 +50,26 @@ export declare function complianceFacts(sessions: ReadonlyArray<{
|
|
|
39
50
|
verified: boolean;
|
|
40
51
|
anchored: boolean;
|
|
41
52
|
}>, dataRegion: string | null): Facts;
|
|
53
|
+
/** spec/compliance.md §1a: what a period's records say about hallucinations and their checks. */
|
|
54
|
+
export declare function hallucinationFacts(sessions: ReadonlyArray<{
|
|
55
|
+
lines: readonly string[];
|
|
56
|
+
}>): Facts;
|
|
57
|
+
/** §1a: `detector_accuracy`, the labels' overall figures (spec/findings.md §12) as percentages. */
|
|
58
|
+
export declare function accuracyFact(a: {
|
|
59
|
+
labels: number;
|
|
60
|
+
overall: {
|
|
61
|
+
precision: number | null;
|
|
62
|
+
recall: number | null;
|
|
63
|
+
precision_low: number | null;
|
|
64
|
+
recall_low: number | null;
|
|
65
|
+
};
|
|
66
|
+
}): Record<string, number | string>;
|
|
67
|
+
/** §1a: `hallucination_controls`, which checks ran (spec/findings.md §8-§12). */
|
|
68
|
+
export declare function controlsFact(o: {
|
|
69
|
+
packs: ReadonlyArray<{
|
|
70
|
+
id: string;
|
|
71
|
+
version: string;
|
|
72
|
+
}>;
|
|
73
|
+
grounding: boolean;
|
|
74
|
+
judge: boolean;
|
|
75
|
+
}): Record<string, string>;
|
package/dist/compliance/index.js
CHANGED
|
@@ -1,40 +1,53 @@
|
|
|
1
1
|
// Compliance reports (spec/compliance.md): facts from the record, mapped onto a framework's sections,
|
|
2
2
|
// and rendered as Markdown. Evidence mapping, not legal advice. Mirrors sdks/python/src/zanii_blackbox/compliance.py.
|
|
3
|
-
export function complianceReport(frameworkId, framework, facts, meta) {
|
|
3
|
+
export function complianceReport(frameworkId, framework, facts, meta, lang = "en") {
|
|
4
|
+
// spec/compliance.md §3: in Arabic when the framework has its Arabic; English otherwise
|
|
5
|
+
const ar = lang === "ar" ? framework.ar : undefined;
|
|
6
|
+
const { notes_ar, ...rest } = meta;
|
|
4
7
|
return {
|
|
5
8
|
framework: frameworkId,
|
|
6
|
-
title: framework.title,
|
|
7
|
-
...
|
|
9
|
+
title: ar?.title ?? framework.title,
|
|
10
|
+
...rest,
|
|
11
|
+
notes: ar && notes_ar ? notes_ar : meta.notes,
|
|
8
12
|
facts,
|
|
9
13
|
sections: framework.sections.map((s) => ({
|
|
10
14
|
id: s.id,
|
|
11
|
-
title: s.title,
|
|
12
|
-
requirement: s.requirement,
|
|
15
|
+
title: ar?.sections[s.id]?.title ?? s.title,
|
|
16
|
+
requirement: ar?.sections[s.id]?.requirement ?? s.requirement,
|
|
13
17
|
evidence: Object.fromEntries(s.facts.map((f) => [f, facts[f] ?? null])),
|
|
14
18
|
})),
|
|
19
|
+
...(ar ? { lang: "ar" } : {}),
|
|
15
20
|
};
|
|
16
21
|
}
|
|
17
|
-
const show = (v) => {
|
|
22
|
+
const show = (v, ar = false) => {
|
|
18
23
|
if (v === null)
|
|
19
|
-
return "not set";
|
|
24
|
+
return ar ? "غير محدد" : "not set";
|
|
20
25
|
if (typeof v === "object") {
|
|
21
26
|
const keys = Object.keys(v).sort();
|
|
22
|
-
return keys.length === 0
|
|
27
|
+
return keys.length === 0
|
|
28
|
+
? ar
|
|
29
|
+
? "لا شيء"
|
|
30
|
+
: "none"
|
|
31
|
+
: keys.map((k) => `${k}: ${v[k]}`).join(", ");
|
|
23
32
|
}
|
|
24
33
|
return String(v);
|
|
25
34
|
};
|
|
26
35
|
export function renderReport(r) {
|
|
36
|
+
const ar = r.lang === "ar";
|
|
27
37
|
const out = [
|
|
28
38
|
`# ${r.title}`,
|
|
29
39
|
"",
|
|
30
|
-
|
|
40
|
+
ar
|
|
41
|
+
? `المستأجر \`${r.tenant_id}\` · الجلسات المنشأة من ${r.from} إلى ${r.to} (غير شامل) · أُنشئ في ${r.generated_at}`
|
|
42
|
+
: `Tenant \`${r.tenant_id}\` · sessions created from ${r.from} to ${r.to} (exclusive) · generated ${r.generated_at}`,
|
|
31
43
|
"",
|
|
32
44
|
`> ${r.notes}`,
|
|
33
45
|
];
|
|
34
46
|
for (const s of r.sections) {
|
|
35
|
-
|
|
47
|
+
const head = ar ? "| الدليل | القيمة |" : "| Evidence | Value |";
|
|
48
|
+
out.push("", `## ${s.title}`, "", s.requirement, "", head, "|---|---|");
|
|
36
49
|
for (const [k, v] of Object.entries(s.evidence))
|
|
37
|
-
out.push(`| ${k} | ${show(v)} |`);
|
|
50
|
+
out.push(`| ${k} | ${show(v, ar)} |`);
|
|
38
51
|
}
|
|
39
52
|
return `${out.join("\n")}\n`;
|
|
40
53
|
}
|
|
@@ -94,3 +107,57 @@ export function complianceFacts(sessions, dataRegion) {
|
|
|
94
107
|
data_region: dataRegion,
|
|
95
108
|
};
|
|
96
109
|
}
|
|
110
|
+
// ---------------------------------------------------------------- hallucination facts (§1a, H6)
|
|
111
|
+
/** spec/compliance.md §1a: what a period's records say about hallucinations and their checks. */
|
|
112
|
+
export function hallucinationFacts(sessions) {
|
|
113
|
+
let responses = 0;
|
|
114
|
+
const codes = {};
|
|
115
|
+
const grounding = { checked: 0, outages: 0 };
|
|
116
|
+
const judge = { checked: 0, outages: 0 };
|
|
117
|
+
for (const s of sessions)
|
|
118
|
+
for (const line of s.lines) {
|
|
119
|
+
const e = JSON.parse(line);
|
|
120
|
+
const m = e.meta;
|
|
121
|
+
if (e.kind === "llm.response")
|
|
122
|
+
responses++;
|
|
123
|
+
else if (e.kind === "finding" && m.source === "hallucination" && typeof m.code === "string")
|
|
124
|
+
codes[m.code] = (codes[m.code] ?? 0) + 1;
|
|
125
|
+
else if (e.kind === "control" && (m.action === "grounding" || m.action === "judge")) {
|
|
126
|
+
const c = m.action === "grounding" ? grounding : judge;
|
|
127
|
+
if (m.outage === true)
|
|
128
|
+
c.outages++;
|
|
129
|
+
else
|
|
130
|
+
c.checked++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
model_responses: responses,
|
|
135
|
+
hallucination_findings: codes,
|
|
136
|
+
grounding_checks: grounding,
|
|
137
|
+
judge_checks: judge,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** A 0-1 figure as a percentage with two decimals, from whole basis points (exact in both SDKs). */
|
|
141
|
+
function percent(x) {
|
|
142
|
+
const bp = Math.floor(x * 10_000 + 0.5);
|
|
143
|
+
return `${Math.floor(bp / 100)}.${String(bp % 100).padStart(2, "0")}%`;
|
|
144
|
+
}
|
|
145
|
+
/** §1a: `detector_accuracy`, the labels' overall figures (spec/findings.md §12) as percentages. */
|
|
146
|
+
export function accuracyFact(a) {
|
|
147
|
+
const out = { labels: a.labels };
|
|
148
|
+
for (const k of ["precision", "precision_low", "recall", "recall_low"]) {
|
|
149
|
+
const v = a.overall[k];
|
|
150
|
+
if (v !== null)
|
|
151
|
+
out[k] = percent(v);
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
/** §1a: `hallucination_controls`, which checks ran (spec/findings.md §8-§12). */
|
|
156
|
+
export function controlsFact(o) {
|
|
157
|
+
return {
|
|
158
|
+
exact_checks: "on",
|
|
159
|
+
reference_packs: o.packs.length ? o.packs.map((p) => `${p.id}@${p.version}`).join(" ") : "none",
|
|
160
|
+
grounding_service: o.grounding ? "on" : "off",
|
|
161
|
+
judge: o.judge ? "on" : "off",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Bundle, BundleReport } from "../verify/index.ts";
|
|
2
|
+
export declare function zaniiCompliance(bundle: Bundle, report: BundleReport, opts: {
|
|
3
|
+
generated_at: string;
|
|
4
|
+
controls?: Readonly<Record<string, string>>;
|
|
5
|
+
}): {
|
|
6
|
+
agent_id: string;
|
|
7
|
+
generated_at: string;
|
|
8
|
+
verified: boolean;
|
|
9
|
+
period: {
|
|
10
|
+
first: string | null;
|
|
11
|
+
last: string | null;
|
|
12
|
+
};
|
|
13
|
+
total_actions: number;
|
|
14
|
+
by_namespace: Record<string, number>;
|
|
15
|
+
anchored: {
|
|
16
|
+
count: number;
|
|
17
|
+
total: number;
|
|
18
|
+
complete: boolean;
|
|
19
|
+
};
|
|
20
|
+
controls: {
|
|
21
|
+
namespace: string;
|
|
22
|
+
control: string;
|
|
23
|
+
actions: number;
|
|
24
|
+
}[];
|
|
25
|
+
flags: string[];
|
|
26
|
+
};
|
|
27
|
+
export type ZaniiComplianceReport = ReturnType<typeof zaniiCompliance>;
|
|
28
|
+
/** Zanii's Markdown, as zanii.compliance.render_compliance_markdown gives it. */
|
|
29
|
+
export declare function renderZaniiCompliance(r: ZaniiComplianceReport): string;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Zanii's compliance report, from a session (spec/compliance.md §4): @zanii/compliance's
|
|
2
|
+
// ComplianceReport fields and its Markdown, so a customer's own Zanii tooling reads our evidence.
|
|
3
|
+
// Pure; mirrors sdks/python/src/zanii_blackbox/compliance.py; pinned by
|
|
4
|
+
// spec/vectors/zanii-compliance.json.
|
|
5
|
+
function namespaceOf(e) {
|
|
6
|
+
const m = e.meta ?? {};
|
|
7
|
+
if (e.kind === "llm.request")
|
|
8
|
+
return `model:${String(m.provider ?? "unknown")}`;
|
|
9
|
+
if (e.kind === "tool.call")
|
|
10
|
+
return `tool:${String(m.server ?? m.via ?? "unknown")}`;
|
|
11
|
+
if (e.kind === "sdk.event" && m.type === "tool.call")
|
|
12
|
+
return "tool:sdk";
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
export function zaniiCompliance(bundle, report, opts) {
|
|
16
|
+
const events = bundle.lines.map((l) => {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(l);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const anchoredThrough = bundle.anchors.at(-1)?.count ?? 0;
|
|
25
|
+
const byNamespace = {};
|
|
26
|
+
let total = 0;
|
|
27
|
+
let anchored = 0;
|
|
28
|
+
for (const e of events) {
|
|
29
|
+
const ns = namespaceOf(e);
|
|
30
|
+
if (ns === null)
|
|
31
|
+
continue;
|
|
32
|
+
byNamespace[ns] = (byNamespace[ns] ?? 0) + 1;
|
|
33
|
+
total++;
|
|
34
|
+
if (typeof e.seq === "number" && e.seq < anchoredThrough)
|
|
35
|
+
anchored++;
|
|
36
|
+
}
|
|
37
|
+
const controls = Object.entries(byNamespace).map(([namespace, actions]) => ({
|
|
38
|
+
namespace,
|
|
39
|
+
control: opts.controls?.[namespace] ?? "(unmapped)",
|
|
40
|
+
actions,
|
|
41
|
+
}));
|
|
42
|
+
const flags = [];
|
|
43
|
+
if (!report.ok)
|
|
44
|
+
flags.push("bundle FAILED offline verification — treat all figures below as unproven");
|
|
45
|
+
if (anchored < total)
|
|
46
|
+
flags.push(`${total - anchored} of ${total} receipts are not yet anchored on-chain`);
|
|
47
|
+
if (opts.controls) {
|
|
48
|
+
const unmapped = controls.filter((c) => c.control === "(unmapped)").map((c) => c.namespace);
|
|
49
|
+
if (unmapped.length)
|
|
50
|
+
flags.push(`no control mapped for namespace(s): ${unmapped.join(", ")}`);
|
|
51
|
+
}
|
|
52
|
+
const ts = (e) => (typeof e?.ts === "string" ? e.ts : null);
|
|
53
|
+
return {
|
|
54
|
+
agent_id: bundle.anchored_by ?? "",
|
|
55
|
+
generated_at: opts.generated_at,
|
|
56
|
+
verified: report.ok,
|
|
57
|
+
period: { first: ts(events[0]), last: ts(events.at(-1)) },
|
|
58
|
+
total_actions: total,
|
|
59
|
+
by_namespace: byNamespace,
|
|
60
|
+
anchored: { count: anchored, total, complete: anchored >= total },
|
|
61
|
+
controls,
|
|
62
|
+
flags,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Zanii's Markdown, as zanii.compliance.render_compliance_markdown gives it. */
|
|
66
|
+
export function renderZaniiCompliance(r) {
|
|
67
|
+
// Python's sort is stable, as JavaScript's is: equal counts keep their order
|
|
68
|
+
const rows = [...r.controls]
|
|
69
|
+
.sort((a, b) => b.actions - a.actions)
|
|
70
|
+
.map((c) => `| \`${c.namespace}\` | ${c.control} | ${c.actions} |`)
|
|
71
|
+
.join("\n") || "| — | — | 0 |";
|
|
72
|
+
const flags = r.flags.length ? r.flags.map((f) => `- ⚠️ ${f}`).join("\n") : "- ✅ none";
|
|
73
|
+
return ("# Zanii Compliance Report\n\n" +
|
|
74
|
+
`- **Agent:** \`${r.agent_id}\`\n` +
|
|
75
|
+
`- **Generated:** ${r.generated_at}\n` +
|
|
76
|
+
`- **Verification:** ${r.verified ? "✅ PROVEN offline" : "❌ FAILED"}\n` +
|
|
77
|
+
`- **Period:** ${r.period.first ?? "—"} → ${r.period.last ?? "—"}\n` +
|
|
78
|
+
`- **Actions:** ${r.total_actions} (${r.anchored.count}/${r.anchored.total} anchored on-chain)\n\n` +
|
|
79
|
+
"## Actions by control\n\n" +
|
|
80
|
+
"| Namespace | Control | Actions |\n| --- | --- | --- |\n" +
|
|
81
|
+
`${rows}\n\n` +
|
|
82
|
+
"## Findings\n\n" +
|
|
83
|
+
`${flags}\n`);
|
|
84
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type Effect = "forbid" | "require_approval" | "allow";
|
|
2
|
+
export interface ConstitutionRule {
|
|
3
|
+
id: string;
|
|
4
|
+
effect: Effect;
|
|
5
|
+
targets: string[];
|
|
6
|
+
note?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface Constitution {
|
|
9
|
+
v: 1;
|
|
10
|
+
type: "constitution";
|
|
11
|
+
issuer: string;
|
|
12
|
+
name: string;
|
|
13
|
+
rules: ConstitutionRule[];
|
|
14
|
+
default: "allow" | "forbid";
|
|
15
|
+
created_at: string;
|
|
16
|
+
sig: string;
|
|
17
|
+
}
|
|
18
|
+
/** Builds and signs the rulebook; rules are evaluated first match wins, top to bottom. */
|
|
19
|
+
export declare function createConstitution(input: {
|
|
20
|
+
issuer: string;
|
|
21
|
+
name: string;
|
|
22
|
+
rules: ConstitutionRule[];
|
|
23
|
+
defaultEffect?: "allow" | "forbid";
|
|
24
|
+
createdAt: string;
|
|
25
|
+
issuerPrivateKey: Uint8Array;
|
|
26
|
+
}): Constitution;
|
|
27
|
+
/** Structure and the issuer's signature. */
|
|
28
|
+
export declare function verifyConstitution(c: unknown): boolean;
|
|
29
|
+
/** The `manifest_hash`: RFC 8785 JSON, SHA-256. */
|
|
30
|
+
export declare const constitutionHash: (c: Constitution) => string;
|
|
31
|
+
/** First match wins, then the default. */
|
|
32
|
+
export declare function evaluateConstitution(c: Constitution, target: string): {
|
|
33
|
+
effect: Effect;
|
|
34
|
+
rule: ConstitutionRule;
|
|
35
|
+
} | {
|
|
36
|
+
effect: Effect;
|
|
37
|
+
rule: null;
|
|
38
|
+
};
|
|
39
|
+
/** spec/governance.md §3: a session's record against the rulebook. */
|
|
40
|
+
export declare function governanceOf(lines: readonly string[], c: Constitution): {
|
|
41
|
+
manifest_hash: string;
|
|
42
|
+
governed: boolean;
|
|
43
|
+
ok: boolean;
|
|
44
|
+
actions: number;
|
|
45
|
+
violations: {
|
|
46
|
+
seq: number;
|
|
47
|
+
target: string;
|
|
48
|
+
kind: string;
|
|
49
|
+
rule_id?: string;
|
|
50
|
+
}[];
|
|
51
|
+
approvals_required: {
|
|
52
|
+
seq: number;
|
|
53
|
+
target: string;
|
|
54
|
+
rule_id: string;
|
|
55
|
+
approved: boolean;
|
|
56
|
+
}[];
|
|
57
|
+
};
|