@zanii/blackbox 0.2.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 +77 -0
- package/dist/a2a/index.js +305 -0
- 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.d.ts +18 -0
- package/dist/analysis/taxonomy.js +66 -0
- package/dist/approvals/index.d.ts +23 -0
- package/dist/approvals/index.js +48 -0
- package/dist/archive/index.d.ts +39 -0
- package/dist/archive/index.js +96 -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.d.ts +14 -0
- package/dist/bom/index.js +152 -0
- package/dist/cli.js +114 -10
- package/dist/compliance/art12.d.ts +35 -0
- package/dist/compliance/art12.js +190 -0
- 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 +34 -5
- package/dist/index.js +34 -5
- 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/ocsf/index.d.ts +1 -1
- package/dist/ocsf/index.js +36 -3
- package/dist/otlp/index.d.ts +8 -1
- package/dist/otlp/index.js +258 -1
- package/dist/packs/index.js +44 -4
- package/dist/policy/delta.js +7 -1
- package/dist/policy/index.d.ts +40 -6
- package/dist/policy/index.js +186 -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 +107 -1
- package/dist/session/index.js +189 -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/timestamp/index.d.ts +24 -0
- package/dist/timestamp/index.js +274 -0
- package/dist/tokens/index.d.ts +6 -0
- package/dist/tokens/index.js +46 -0
- package/dist/transparency/index.d.ts +188 -0
- package/dist/transparency/index.js +712 -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/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The TypeScript SDK for [Zanii Blackbox](https://zanii.agency), the flight recorder for AI agents.
|
|
4
4
|
|
|
5
|
-
Blackbox records every model call and tool call your agent makes, from a gateway that runs outside the agent's process. The records are hash-chained, so an edit or a deletion shows, and anchored as receipts that anyone can verify offline. This SDK is optional. It adds what only the agent knows (steps, its own tools, its checks, the outcome) as a second, independent record, and it reads and verifies what the gateway recorded.
|
|
5
|
+
Blackbox records every model call and tool call your agent makes, for any industry and any organisation, from a gateway that runs outside the agent's process. The records are hash-chained, so an edit or a deletion shows, and anchored as receipts that anyone can verify offline. This SDK is optional. It adds what only the agent knows (steps, its own tools, its checks, the outcome) as a second, independent record, and it reads and verifies what the gateway recorded.
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
npm install @zanii/blackbox
|
|
@@ -65,6 +65,31 @@ await s.close("success");
|
|
|
65
65
|
- **It stays out of the way.** Recording is non-blocking with bounded memory. A recorded call costs about 0.3 ms (measured over 20,000 calls on a laptop); shipping happens in the background.
|
|
66
66
|
- **It has no other runtime dependencies** besides the Zanii core libraries.
|
|
67
67
|
|
|
68
|
+
## Hallucination controls
|
|
69
|
+
|
|
70
|
+
The gateway checks every record, with no model needed: an invented tool or ID, arguments a tool's schema refuses, "done" right after an error, and amounts, doses, dates and IDs in answers that nothing the agent was given holds. With your organisation's own **reference packs** (prices, fees, rules, limits) it also catches answers that contradict them, and your own grounding model and judge can plug in. A risky answer can be held for a person before your customer sees it.
|
|
71
|
+
|
|
72
|
+
From the SDK, pin what the agent read, then check an answer's signed credential anywhere:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { verifyAnswerCredential, verifyCertificate } from "@zanii/blackbox";
|
|
76
|
+
|
|
77
|
+
s.retrieved([{ id: "returns-policy", version: "2026-10", content: policyText }]); // hashed here, never sent
|
|
78
|
+
|
|
79
|
+
// later, from GET /v1/sessions/:id/answers/:seq/credential and the session's record:
|
|
80
|
+
verifyCertificate(credential).ok; // signed by the gateway
|
|
81
|
+
verifyAnswerCredential(credential, lines, bodies, packs); // { ok, problems }: the record backs every claim
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The same checks run offline as pure functions: `toolHallucinations`, `groundingFindings`, `factsIn`, `loadReferencePacks`, `answerRisk`, `detectorAccuracy`.
|
|
85
|
+
|
|
86
|
+
## New in 0.4.0
|
|
87
|
+
|
|
88
|
+
- Hallucination controls: the checks above, reference packs, answer credentials (`answerClaims`, `answerCredential`, `verifyAnswerCredential`), `session.retrieved()`, accuracy from people's labels (`detectorAccuracy`, Wilson bounds), the grounding and judge contracts, `answerRisk` for held answers.
|
|
89
|
+
- Keyed redaction tokens (`tokenFor`, `resolveTokens`).
|
|
90
|
+
- Evidence for payments, health and public services (`payment`, `healthAccess`, `decision` and their verifiers); provable agent memory (`memoryChain`); record badges, agent CVs, SLAs, ledger co-signing and the post-quantum binding.
|
|
91
|
+
- Compliance reports in Arabic, and the hallucination-controls report (`hallucinationFacts`).
|
|
92
|
+
|
|
68
93
|
## Frameworks
|
|
69
94
|
|
|
70
95
|
| Framework | Use |
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type Json } from "../verify/envelope.ts";
|
|
2
|
+
type Meta = {
|
|
3
|
+
[key: string]: Json;
|
|
4
|
+
};
|
|
5
|
+
/** The methods whose answer is an SSE stream. */
|
|
6
|
+
export declare const A2A_STREAMING: Set<string>;
|
|
7
|
+
/** The v1.0 name of an A2A method, either name set, or undefined for one A2A doesn't define. */
|
|
8
|
+
export declare function a2aMethod(method: string): string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* §2: a request's meta. `version` and `extensions` are the A2A-Version and A2A-Extensions headers
|
|
11
|
+
* (or the A2A-Version query parameter); an empty version means 0.3.
|
|
12
|
+
*/
|
|
13
|
+
export declare function a2aRequestMeta(body: Uint8Array, headers?: {
|
|
14
|
+
version?: string | null;
|
|
15
|
+
extensions?: string | null;
|
|
16
|
+
}): Meta;
|
|
17
|
+
/** §2: an answer's meta: a JSON body, or (for a stream) one SSE event's `data:` lines. */
|
|
18
|
+
export declare function a2aResponseMeta(bytes: Uint8Array, contentType: string | undefined): Meta;
|
|
19
|
+
/** `sha256:<hex>` of the card's JCS form without `signatures`: what `agent.card_digest` names. */
|
|
20
|
+
export declare function agentCardDigest(card: Record<string, unknown>): string;
|
|
21
|
+
/** What an Agent Card's JWS signs: BASE64URL(JCS(card without signatures)). */
|
|
22
|
+
export declare function agentCardPayload(card: Record<string, unknown>): string;
|
|
23
|
+
export interface AgentCardReport {
|
|
24
|
+
ok: boolean;
|
|
25
|
+
digest: string | null;
|
|
26
|
+
/** Each signature's kid and alg, and whether it verifies under the keys given (null: no such key). */
|
|
27
|
+
signatures: Array<{
|
|
28
|
+
kid: string | null;
|
|
29
|
+
alg: string | null;
|
|
30
|
+
ok: boolean | null;
|
|
31
|
+
}>;
|
|
32
|
+
problems: string[];
|
|
33
|
+
}
|
|
34
|
+
type Jwk = {
|
|
35
|
+
kty?: string;
|
|
36
|
+
crv?: string;
|
|
37
|
+
kid?: string;
|
|
38
|
+
x?: string;
|
|
39
|
+
y?: string;
|
|
40
|
+
n?: string;
|
|
41
|
+
e?: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* §3: checks an Agent Card's signatures against trusted JWKs (by kid): each is a JWS over
|
|
45
|
+
* BASE64URL(protected) "." BASE64URL(JCS(card without signatures)), ES256, RS256 or EdDSA. The card
|
|
46
|
+
* is canonicalised as received (A2A's proto default-dropping can't be known from JSON). `ok` when at
|
|
47
|
+
* least one signature verifies.
|
|
48
|
+
*/
|
|
49
|
+
export declare function verifyAgentCard(card: unknown, keys: readonly Jwk[]): AgentCardReport;
|
|
50
|
+
/**
|
|
51
|
+
* spec/a2a.md §3a (L3): the card as the gateway serves it, so a client that follows it stays on the
|
|
52
|
+
* record: every JSON-RPC interface points at `gatewayUrl`, other bindings (which the gateway can't
|
|
53
|
+
* capture) are dropped, and the publisher's signatures, which no longer hold, are replaced by the
|
|
54
|
+
* gateway's own EdDSA signature (kid: its did:key).
|
|
55
|
+
*/
|
|
56
|
+
export declare function rewriteAgentCard(card: Record<string, unknown>, o: {
|
|
57
|
+
gatewayUrl: string;
|
|
58
|
+
signer: {
|
|
59
|
+
kid: string;
|
|
60
|
+
seed: Uint8Array;
|
|
61
|
+
};
|
|
62
|
+
}): Record<string, unknown>;
|
|
63
|
+
/** The JWK of a gateway's Ed25519 did:key, to check a card it rewrote with verifyAgentCard. */
|
|
64
|
+
export declare function gatewayCardKey(did: string): Jwk;
|
|
65
|
+
/** The A2A method and task a REST call is, or undefined for a path that isn't one of A2A's. */
|
|
66
|
+
export declare function a2aRestMethod(httpMethod: string, path: string): {
|
|
67
|
+
method: string;
|
|
68
|
+
taskId?: string;
|
|
69
|
+
} | undefined;
|
|
70
|
+
/** §2a: a REST call's meta: the same fields as a JSON-RPC call's, with `binding: "http+json"`. */
|
|
71
|
+
export declare function a2aRestRequestMeta(httpMethod: string, path: string, body: Uint8Array, headers?: {
|
|
72
|
+
version?: string | null;
|
|
73
|
+
extensions?: string | null;
|
|
74
|
+
}): Meta;
|
|
75
|
+
/** §2a: a REST answer's meta (a bare Task, a `{task | message}`, or one SSE event's StreamResponse). */
|
|
76
|
+
export declare function a2aRestResponseMeta(bytes: Uint8Array, contentType: string | undefined): Meta;
|
|
77
|
+
export {};
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// A2A v1.0 capture (spec/a2a.md): what the gateway records of an agent-to-agent JSON-RPC call (both
|
|
2
|
+
// the v1.0 PascalCase methods and the v0.3 names), of each answer or stream event, and the check of
|
|
3
|
+
// an Agent Card's JWS signatures (A2A §8.4: detached, over the JCS card without `signatures`).
|
|
4
|
+
// Mirrors sdks/python/src/zanii_blackbox/a2a.py.
|
|
5
|
+
import { createHash, createPublicKey, verify } from "node:crypto";
|
|
6
|
+
import { canonicalBytes, publicKeyFromDid } from "@zanii/core";
|
|
7
|
+
import { ed25519Sign } from "../transparency/index.js";
|
|
8
|
+
import { assertSafeIntegers } from "../verify/envelope.js";
|
|
9
|
+
/** v0.3 method names → v1.0 (A2A §9.4). */
|
|
10
|
+
const LEGACY = {
|
|
11
|
+
"message/send": "SendMessage",
|
|
12
|
+
"message/stream": "SendStreamingMessage",
|
|
13
|
+
"tasks/get": "GetTask",
|
|
14
|
+
"tasks/list": "ListTasks",
|
|
15
|
+
"tasks/cancel": "CancelTask",
|
|
16
|
+
"tasks/resubscribe": "SubscribeToTask",
|
|
17
|
+
"tasks/pushNotificationConfig/set": "CreateTaskPushNotificationConfig",
|
|
18
|
+
"tasks/pushNotificationConfig/get": "GetTaskPushNotificationConfig",
|
|
19
|
+
"tasks/pushNotificationConfig/list": "ListTaskPushNotificationConfigs",
|
|
20
|
+
"tasks/pushNotificationConfig/delete": "DeleteTaskPushNotificationConfig",
|
|
21
|
+
"agent/getAuthenticatedExtendedCard": "GetExtendedAgentCard",
|
|
22
|
+
};
|
|
23
|
+
const V1 = new Set(Object.values(LEGACY));
|
|
24
|
+
/** The methods whose answer is an SSE stream. */
|
|
25
|
+
export const A2A_STREAMING = new Set(["SendStreamingMessage", "SubscribeToTask"]);
|
|
26
|
+
/** The v1.0 name of an A2A method, either name set, or undefined for one A2A doesn't define. */
|
|
27
|
+
export function a2aMethod(method) {
|
|
28
|
+
return V1.has(method) ? method : LEGACY[method];
|
|
29
|
+
}
|
|
30
|
+
const str = (v, max) => (typeof v === "string" && v.length <= max ? v : undefined);
|
|
31
|
+
const obj = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
|
|
32
|
+
const rpcId = (id) => typeof id === "string" && id.length <= 256
|
|
33
|
+
? id
|
|
34
|
+
: typeof id === "number" && Number.isSafeInteger(id)
|
|
35
|
+
? id
|
|
36
|
+
: undefined;
|
|
37
|
+
const put = (meta, key, value) => {
|
|
38
|
+
if (value !== undefined)
|
|
39
|
+
meta[key] = value;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* §2: a request's meta. `version` and `extensions` are the A2A-Version and A2A-Extensions headers
|
|
43
|
+
* (or the A2A-Version query parameter); an empty version means 0.3.
|
|
44
|
+
*/
|
|
45
|
+
export function a2aRequestMeta(body, headers = {}) {
|
|
46
|
+
let rpc = {};
|
|
47
|
+
try {
|
|
48
|
+
rpc = obj(JSON.parse(Buffer.from(body).toString("utf8")));
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
const meta = { a2a_version: str(headers.version, 16) || "0.3" };
|
|
52
|
+
const ext = (headers.extensions ?? "")
|
|
53
|
+
.split(",")
|
|
54
|
+
.map((e) => e.trim())
|
|
55
|
+
.filter((e) => e !== "" && e.length <= 256)
|
|
56
|
+
.slice(0, 10);
|
|
57
|
+
if (ext.length > 0)
|
|
58
|
+
meta.extensions = ext;
|
|
59
|
+
const method = str(rpc.method, 128);
|
|
60
|
+
put(meta, "method", method);
|
|
61
|
+
const canonical = method === undefined ? undefined : a2aMethod(method);
|
|
62
|
+
put(meta, "a2a_method", canonical);
|
|
63
|
+
if (method !== undefined && canonical !== undefined && canonical !== method)
|
|
64
|
+
meta.legacy = true;
|
|
65
|
+
put(meta, "rpc_id", rpcId(rpc.id));
|
|
66
|
+
const p = obj(rpc.params);
|
|
67
|
+
put(meta, "tenant", str(p.tenant, 128));
|
|
68
|
+
if (canonical === "SendMessage" || canonical === "SendStreamingMessage") {
|
|
69
|
+
const m = obj(p.message);
|
|
70
|
+
put(meta, "message_id", str(m.messageId, 128));
|
|
71
|
+
put(meta, "context_id", str(m.contextId, 128));
|
|
72
|
+
put(meta, "task_id", str(m.taskId, 128));
|
|
73
|
+
put(meta, "role", str(m.role, 32));
|
|
74
|
+
}
|
|
75
|
+
else if (canonical !== undefined && /Task$/.test(canonical))
|
|
76
|
+
put(meta, "task_id", str(p.id, 128));
|
|
77
|
+
return meta;
|
|
78
|
+
}
|
|
79
|
+
const LEGACY_KINDS = {
|
|
80
|
+
task: "task",
|
|
81
|
+
message: "message",
|
|
82
|
+
"status-update": "statusUpdate",
|
|
83
|
+
"artifact-update": "artifactUpdate",
|
|
84
|
+
};
|
|
85
|
+
const STATE = /^TASK_STATE_[A-Z_]{1,32}$/;
|
|
86
|
+
const legacyState = (s) => typeof s === "string" && /^[a-z-]{1,32}$/.test(s)
|
|
87
|
+
? `TASK_STATE_${s.replaceAll("-", "_").toUpperCase()}`
|
|
88
|
+
: undefined;
|
|
89
|
+
/** One JSON-RPC response (or SSE event's data), as meta. */
|
|
90
|
+
function responseOf(rpc) {
|
|
91
|
+
const meta = {};
|
|
92
|
+
put(meta, "rpc_id", rpcId(rpc.id));
|
|
93
|
+
const code = obj(rpc.error).code;
|
|
94
|
+
if (typeof code === "number" && Number.isSafeInteger(code))
|
|
95
|
+
meta.rpc_error = code;
|
|
96
|
+
const result = obj(rpc.result);
|
|
97
|
+
// v1.0: a oneof by key; v0.3: `kind`
|
|
98
|
+
let type = ["task", "message", "statusUpdate", "artifactUpdate"].find((k) => k in result);
|
|
99
|
+
let inner = type ? obj(result[type]) : {};
|
|
100
|
+
if (!type && typeof result.kind === "string" && result.kind in LEGACY_KINDS) {
|
|
101
|
+
type = LEGACY_KINDS[result.kind];
|
|
102
|
+
inner = result;
|
|
103
|
+
}
|
|
104
|
+
if (!type)
|
|
105
|
+
return meta;
|
|
106
|
+
meta.result_type = type;
|
|
107
|
+
put(meta, "task_id", str(type === "task" ? inner.id : inner.taskId, 128));
|
|
108
|
+
put(meta, "context_id", str(inner.contextId, 128));
|
|
109
|
+
const state = obj(inner.status).state;
|
|
110
|
+
put(meta, "task_state", typeof state === "string" && STATE.test(state) ? state : legacyState(state));
|
|
111
|
+
if (type === "artifactUpdate")
|
|
112
|
+
put(meta, "artifact_id", str(obj(inner.artifact).artifactId, 128));
|
|
113
|
+
return meta;
|
|
114
|
+
}
|
|
115
|
+
/** §2: an answer's meta: a JSON body, or (for a stream) one SSE event's `data:` lines. */
|
|
116
|
+
export function a2aResponseMeta(bytes, contentType) {
|
|
117
|
+
let text = Buffer.from(bytes).toString("utf8");
|
|
118
|
+
if (contentType?.includes("text/event-stream"))
|
|
119
|
+
text = text
|
|
120
|
+
.split(/\r?\n/)
|
|
121
|
+
.filter((l) => l.startsWith("data:"))
|
|
122
|
+
.map((l) => l.slice(5).trim())
|
|
123
|
+
.join("\n");
|
|
124
|
+
try {
|
|
125
|
+
return responseOf(obj(JSON.parse(text)));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// ---------------------------------------------------------------- Agent Cards (A2A §8.4)
|
|
132
|
+
const b64u = (b) => Buffer.from(b).toString("base64url");
|
|
133
|
+
/** `sha256:<hex>` of the card's JCS form without `signatures`: what `agent.card_digest` names. */
|
|
134
|
+
export function agentCardDigest(card) {
|
|
135
|
+
const { signatures: _, ...rest } = card;
|
|
136
|
+
assertSafeIntegers(rest); // as Python's canonical JSON does
|
|
137
|
+
return `sha256:${createHash("sha256")
|
|
138
|
+
.update(canonicalBytes(rest))
|
|
139
|
+
.digest("hex")}`;
|
|
140
|
+
}
|
|
141
|
+
/** What an Agent Card's JWS signs: BASE64URL(JCS(card without signatures)). */
|
|
142
|
+
export function agentCardPayload(card) {
|
|
143
|
+
const { signatures: _, ...rest } = card;
|
|
144
|
+
assertSafeIntegers(rest);
|
|
145
|
+
return b64u(canonicalBytes(rest));
|
|
146
|
+
}
|
|
147
|
+
function verifyJws(alg, jwk, input, sig) {
|
|
148
|
+
try {
|
|
149
|
+
const key = createPublicKey({ key: jwk, format: "jwk" });
|
|
150
|
+
if (alg === "ES256" && jwk.kty === "EC" && jwk.crv === "P-256")
|
|
151
|
+
return verify("sha256", input, { key, dsaEncoding: "ieee-p1363" }, sig);
|
|
152
|
+
if (alg === "RS256" && jwk.kty === "RSA")
|
|
153
|
+
return verify("sha256", input, key, sig);
|
|
154
|
+
if (alg === "EdDSA" && jwk.kty === "OKP" && jwk.crv === "Ed25519")
|
|
155
|
+
return verify(null, input, key, sig);
|
|
156
|
+
}
|
|
157
|
+
catch { }
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* §3: checks an Agent Card's signatures against trusted JWKs (by kid): each is a JWS over
|
|
162
|
+
* BASE64URL(protected) "." BASE64URL(JCS(card without signatures)), ES256, RS256 or EdDSA. The card
|
|
163
|
+
* is canonicalised as received (A2A's proto default-dropping can't be known from JSON). `ok` when at
|
|
164
|
+
* least one signature verifies.
|
|
165
|
+
*/
|
|
166
|
+
export function verifyAgentCard(card, keys) {
|
|
167
|
+
const report = { ok: false, digest: null, signatures: [], problems: [] };
|
|
168
|
+
const c = obj(card);
|
|
169
|
+
if (Object.keys(c).length === 0)
|
|
170
|
+
return { ...report, problems: ["not an Agent Card"] };
|
|
171
|
+
let payload;
|
|
172
|
+
try {
|
|
173
|
+
report.digest = agentCardDigest(c);
|
|
174
|
+
payload = agentCardPayload(c);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return { ...report, problems: ["the card can't be canonicalised (non-integer numbers?)"] };
|
|
178
|
+
}
|
|
179
|
+
const sigs = Array.isArray(c.signatures) ? c.signatures.slice(0, 10) : [];
|
|
180
|
+
if (sigs.length === 0)
|
|
181
|
+
report.problems.push("the card isn't signed");
|
|
182
|
+
for (const s of sigs) {
|
|
183
|
+
const sig = obj(s);
|
|
184
|
+
let header = {};
|
|
185
|
+
try {
|
|
186
|
+
header = obj(JSON.parse(Buffer.from(String(sig.protected), "base64url").toString("utf8")));
|
|
187
|
+
}
|
|
188
|
+
catch { }
|
|
189
|
+
const kid = str(header.kid, 256) ?? null;
|
|
190
|
+
const alg = str(header.alg, 16) ?? null;
|
|
191
|
+
const jwk = keys.find((k) => k.kid !== undefined && k.kid === kid);
|
|
192
|
+
const ok = jwk && alg && typeof sig.protected === "string" && typeof sig.signature === "string"
|
|
193
|
+
? verifyJws(alg, jwk, Buffer.from(`${sig.protected}.${payload}`), Buffer.from(sig.signature, "base64url"))
|
|
194
|
+
: null;
|
|
195
|
+
report.signatures.push({ kid, alg, ok });
|
|
196
|
+
}
|
|
197
|
+
report.ok = report.signatures.some((s) => s.ok === true);
|
|
198
|
+
if (sigs.length > 0 && !report.ok)
|
|
199
|
+
report.problems.push(report.signatures.some((s) => s.ok === false)
|
|
200
|
+
? "a signature doesn't verify"
|
|
201
|
+
: "no signature is by a trusted key");
|
|
202
|
+
return report;
|
|
203
|
+
}
|
|
204
|
+
const JSONRPC = (t) => typeof t === "string" && t.toUpperCase().replace(/[-_]/g, "") === "JSONRPC";
|
|
205
|
+
/**
|
|
206
|
+
* spec/a2a.md §3a (L3): the card as the gateway serves it, so a client that follows it stays on the
|
|
207
|
+
* record: every JSON-RPC interface points at `gatewayUrl`, other bindings (which the gateway can't
|
|
208
|
+
* capture) are dropped, and the publisher's signatures, which no longer hold, are replaced by the
|
|
209
|
+
* gateway's own EdDSA signature (kid: its did:key).
|
|
210
|
+
*/
|
|
211
|
+
export function rewriteAgentCard(card, o) {
|
|
212
|
+
const { signatures: _drop, ...rest } = card;
|
|
213
|
+
const out = { ...rest };
|
|
214
|
+
if (Array.isArray(card.supportedInterfaces))
|
|
215
|
+
out.supportedInterfaces = card.supportedInterfaces
|
|
216
|
+
.filter((i) => i && typeof i === "object" && JSONRPC(i.protocolBinding ?? i.transport))
|
|
217
|
+
.map((i) => ({ ...i, url: o.gatewayUrl }));
|
|
218
|
+
if (typeof card.url === "string")
|
|
219
|
+
out.url = o.gatewayUrl;
|
|
220
|
+
if (card.preferredTransport !== undefined)
|
|
221
|
+
out.preferredTransport = "JSONRPC";
|
|
222
|
+
if (Array.isArray(card.additionalInterfaces))
|
|
223
|
+
out.additionalInterfaces = card.additionalInterfaces
|
|
224
|
+
.filter((i) => i && typeof i === "object" && JSONRPC(i.transport ?? i.protocolBinding))
|
|
225
|
+
.map((i) => ({ ...i, url: o.gatewayUrl }));
|
|
226
|
+
const header = b64u(new TextEncoder().encode(JSON.stringify({ alg: "EdDSA", kid: o.signer.kid, typ: "JOSE" })));
|
|
227
|
+
const input = new TextEncoder().encode(`${header}.${agentCardPayload(out)}`);
|
|
228
|
+
const signature = b64u(ed25519Sign(o.signer.seed, input));
|
|
229
|
+
return { ...out, signatures: [{ protected: header, signature }] };
|
|
230
|
+
}
|
|
231
|
+
/** The JWK of a gateway's Ed25519 did:key, to check a card it rewrote with verifyAgentCard. */
|
|
232
|
+
export function gatewayCardKey(did) {
|
|
233
|
+
const pub = publicKeyFromDid(did);
|
|
234
|
+
if (!pub)
|
|
235
|
+
throw new Error("not an Ed25519 did:key");
|
|
236
|
+
return { kty: "OKP", crv: "Ed25519", x: b64u(pub), kid: did };
|
|
237
|
+
}
|
|
238
|
+
// ---------------------------------------------------------------- the HTTP+JSON binding (§2a, L8)
|
|
239
|
+
/** A2A §11's REST routes, by HTTP method and path (after the agent's base, an optional `/v1`). */
|
|
240
|
+
const REST = [
|
|
241
|
+
["POST", /^\/message:send$/, "SendMessage"],
|
|
242
|
+
["POST", /^\/message:stream$/, "SendStreamingMessage"],
|
|
243
|
+
["GET", /^\/tasks$/, "ListTasks"],
|
|
244
|
+
["GET", /^\/tasks\/([^/:]+)$/, "GetTask"],
|
|
245
|
+
["POST", /^\/tasks\/([^/:]+):cancel$/, "CancelTask"],
|
|
246
|
+
["GET", /^\/tasks\/([^/:]+):subscribe$/, "SubscribeToTask"],
|
|
247
|
+
["POST", /^\/tasks\/([^/:]+):subscribe$/, "SubscribeToTask"],
|
|
248
|
+
["POST", /^\/tasks\/([^/:]+)\/pushNotificationConfigs$/, "CreateTaskPushNotificationConfig"],
|
|
249
|
+
["GET", /^\/tasks\/([^/:]+)\/pushNotificationConfigs$/, "ListTaskPushNotificationConfigs"],
|
|
250
|
+
["GET", /^\/tasks\/([^/:]+)\/pushNotificationConfigs\/[^/]+$/, "GetTaskPushNotificationConfig"],
|
|
251
|
+
[
|
|
252
|
+
"DELETE",
|
|
253
|
+
/^\/tasks\/([^/:]+)\/pushNotificationConfigs\/[^/]+$/,
|
|
254
|
+
"DeleteTaskPushNotificationConfig",
|
|
255
|
+
],
|
|
256
|
+
["GET", /^\/extendedAgentCard$/, "GetExtendedAgentCard"],
|
|
257
|
+
];
|
|
258
|
+
/** The A2A method and task a REST call is, or undefined for a path that isn't one of A2A's. */
|
|
259
|
+
export function a2aRestMethod(httpMethod, path) {
|
|
260
|
+
const p = (path.split("?")[0] ?? "").replace(/^\/v1(?=\/)/, "");
|
|
261
|
+
for (const [verb, re, method] of REST) {
|
|
262
|
+
if (verb !== httpMethod.toUpperCase())
|
|
263
|
+
continue;
|
|
264
|
+
const m = re.exec(p);
|
|
265
|
+
if (m)
|
|
266
|
+
return m[1] ? { method, taskId: decodeURIComponent(m[1]) } : { method };
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
/** §2a: a REST call's meta: the same fields as a JSON-RPC call's, with `binding: "http+json"`. */
|
|
271
|
+
export function a2aRestRequestMeta(httpMethod, path, body, headers = {}) {
|
|
272
|
+
const r = a2aRestMethod(httpMethod, path);
|
|
273
|
+
let params = {};
|
|
274
|
+
try {
|
|
275
|
+
params = obj(JSON.parse(Buffer.from(body).toString("utf8")));
|
|
276
|
+
}
|
|
277
|
+
catch { }
|
|
278
|
+
if (r?.taskId !== undefined)
|
|
279
|
+
params = { ...params, id: r.taskId };
|
|
280
|
+
const rpc = r ? { method: r.method, params } : { params };
|
|
281
|
+
return {
|
|
282
|
+
...a2aRequestMeta(new TextEncoder().encode(JSON.stringify(rpc)), headers),
|
|
283
|
+
binding: "http+json",
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/** §2a: a REST answer's meta (a bare Task, a `{task | message}`, or one SSE event's StreamResponse). */
|
|
287
|
+
export function a2aRestResponseMeta(bytes, contentType) {
|
|
288
|
+
let text = Buffer.from(bytes).toString("utf8");
|
|
289
|
+
if (contentType?.includes("text/event-stream"))
|
|
290
|
+
text = text
|
|
291
|
+
.split(/\r?\n/)
|
|
292
|
+
.filter((l) => l.startsWith("data:"))
|
|
293
|
+
.map((l) => l.slice(5).trim())
|
|
294
|
+
.join("\n");
|
|
295
|
+
let value;
|
|
296
|
+
try {
|
|
297
|
+
value = obj(JSON.parse(text));
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return {};
|
|
301
|
+
}
|
|
302
|
+
const oneOf = ["task", "message", "statusUpdate", "artifactUpdate"].some((k) => k in value);
|
|
303
|
+
const result = oneOf ? value : "id" in value && "status" in value ? { task: value } : value;
|
|
304
|
+
return responseOf({ result });
|
|
305
|
+
}
|
package/dist/agents/index.d.ts
CHANGED
|
@@ -8,6 +8,14 @@ export declare function memoryXray(lines: readonly string[], bodies: Bodies, rev
|
|
|
8
8
|
seq: number;
|
|
9
9
|
memory_id: string;
|
|
10
10
|
}[];
|
|
11
|
+
chain?: {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
length: number;
|
|
14
|
+
broken: Array<{
|
|
15
|
+
seq: number;
|
|
16
|
+
reason: string;
|
|
17
|
+
}>;
|
|
18
|
+
};
|
|
11
19
|
};
|
|
12
20
|
/** spec/agents.md §3. */
|
|
13
21
|
export declare function causality(records: ReadonlyArray<readonly string[]>): {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type Label = {
|
|
2
|
+
code: string;
|
|
3
|
+
verdict: "right" | "wrong" | "missed";
|
|
4
|
+
};
|
|
5
|
+
export interface Accuracy {
|
|
6
|
+
code: string;
|
|
7
|
+
right: number;
|
|
8
|
+
wrong: number;
|
|
9
|
+
missed: number;
|
|
10
|
+
/** right / (right + wrong): how often a finding was real. null with no right or wrong labels. */
|
|
11
|
+
precision: number | null;
|
|
12
|
+
/** right / (right + missed): how often a real case was found. null with neither. */
|
|
13
|
+
recall: number | null;
|
|
14
|
+
/** The Wilson 95% lower bounds of the two. */
|
|
15
|
+
precision_low: number | null;
|
|
16
|
+
recall_low: number | null;
|
|
17
|
+
}
|
|
18
|
+
/** The Wilson score interval's lower bound for `k` of `n`, at 95%; null when n = 0. */
|
|
19
|
+
export declare function wilsonLow(k: number, n: number): number | null;
|
|
20
|
+
/** §12: each code's accuracy, by code, and all codes together. */
|
|
21
|
+
export declare function detectorAccuracy(labels: readonly Label[]): {
|
|
22
|
+
codes: Accuracy[];
|
|
23
|
+
overall: Accuracy;
|
|
24
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// How right each check is (spec/findings.md §12, H5): people mark findings right or wrong, and
|
|
2
|
+
// report hallucinations no check caught; from those labels, each code's precision and recall, with
|
|
3
|
+
// a 95% lower bound an auditor can quote. Mirrors sdks/python/src/zanii_blackbox/analysis/
|
|
4
|
+
// accuracy.py; pinned by spec/vectors/accuracy.json.
|
|
5
|
+
const Z = 1.96;
|
|
6
|
+
const round4 = (x) => Math.floor(x * 10_000 + 0.5) / 10_000;
|
|
7
|
+
/** The Wilson score interval's lower bound for `k` of `n`, at 95%; null when n = 0. */
|
|
8
|
+
export function wilsonLow(k, n) {
|
|
9
|
+
if (n === 0)
|
|
10
|
+
return null;
|
|
11
|
+
const p = k / n;
|
|
12
|
+
const z2 = Z * Z;
|
|
13
|
+
const centre = p + z2 / (2 * n);
|
|
14
|
+
const spread = Z * Math.sqrt((p * (1 - p)) / n + z2 / (4 * n * n));
|
|
15
|
+
return round4(Math.max(0, (centre - spread) / (1 + z2 / n)));
|
|
16
|
+
}
|
|
17
|
+
function row(code, right, wrong, missed) {
|
|
18
|
+
const judged = right + wrong;
|
|
19
|
+
const real = right + missed;
|
|
20
|
+
return {
|
|
21
|
+
code,
|
|
22
|
+
right,
|
|
23
|
+
wrong,
|
|
24
|
+
missed,
|
|
25
|
+
precision: judged ? round4(right / judged) : null,
|
|
26
|
+
recall: real ? round4(right / real) : null,
|
|
27
|
+
precision_low: wilsonLow(right, judged),
|
|
28
|
+
recall_low: wilsonLow(right, real),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** §12: each code's accuracy, by code, and all codes together. */
|
|
32
|
+
export function detectorAccuracy(labels) {
|
|
33
|
+
const counts = new Map();
|
|
34
|
+
for (const l of labels) {
|
|
35
|
+
const c = counts.get(l.code) ?? { right: 0, wrong: 0, missed: 0 };
|
|
36
|
+
c[l.verdict]++;
|
|
37
|
+
counts.set(l.code, c);
|
|
38
|
+
}
|
|
39
|
+
const codes = [...counts.keys()].sort().map((code) => {
|
|
40
|
+
const c = counts.get(code);
|
|
41
|
+
return row(code, c.right, c.wrong, c.missed);
|
|
42
|
+
});
|
|
43
|
+
const sum = (k) => codes.reduce((n, c) => n + c[k], 0);
|
|
44
|
+
return { codes, overall: row("*", sum("right"), sum("wrong"), sum("missed")) };
|
|
45
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Bodies } from "../reconcile/record.ts";
|
|
2
|
+
import { type ReferencePack } from "./reference.ts";
|
|
3
|
+
export interface ClaimEntry {
|
|
4
|
+
index: number;
|
|
5
|
+
kind: string;
|
|
6
|
+
value_sha256: string;
|
|
7
|
+
status: "grounded" | "ungrounded" | "contradicted";
|
|
8
|
+
via: "pack" | "exact";
|
|
9
|
+
evidence: Array<{
|
|
10
|
+
seq: number;
|
|
11
|
+
line_hash: string;
|
|
12
|
+
}>;
|
|
13
|
+
pack?: {
|
|
14
|
+
id: string;
|
|
15
|
+
version: string;
|
|
16
|
+
hash: string;
|
|
17
|
+
fact: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface AnswerClaims {
|
|
21
|
+
seq: number;
|
|
22
|
+
line_hash: string;
|
|
23
|
+
text_sha256: string;
|
|
24
|
+
claims: ClaimEntry[];
|
|
25
|
+
service?: {
|
|
26
|
+
seq: number;
|
|
27
|
+
line_hash: string;
|
|
28
|
+
outage: true;
|
|
29
|
+
} | {
|
|
30
|
+
seq: number;
|
|
31
|
+
line_hash: string;
|
|
32
|
+
model?: string;
|
|
33
|
+
version?: string;
|
|
34
|
+
claims: Array<{
|
|
35
|
+
index: number;
|
|
36
|
+
status: string;
|
|
37
|
+
text_sha256: string;
|
|
38
|
+
score?: number;
|
|
39
|
+
}>;
|
|
40
|
+
};
|
|
41
|
+
sources: Array<{
|
|
42
|
+
seq: number;
|
|
43
|
+
id: string;
|
|
44
|
+
uri?: string;
|
|
45
|
+
version?: string;
|
|
46
|
+
sha256: string;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
/** spec/findings.md §11: the claims of every final answer on a record. */
|
|
50
|
+
export declare function answerClaims(lines: readonly string[], bodies: Bodies, packs?: readonly ReferencePack[]): AnswerClaims[];
|
|
51
|
+
/** spec/findings.md §11: the credential for one answer, unsigned; null when `seq` isn't a final
|
|
52
|
+
* answer. The server signs it (`signCertificate`). */
|
|
53
|
+
export declare function answerCredential(lines: readonly string[], bodies: Bodies, seq: number, o: {
|
|
54
|
+
sessionId: string;
|
|
55
|
+
packs?: readonly ReferencePack[];
|
|
56
|
+
}): {
|
|
57
|
+
v: number;
|
|
58
|
+
type: string;
|
|
59
|
+
session_id: string;
|
|
60
|
+
answer: {
|
|
61
|
+
seq: number;
|
|
62
|
+
line_hash: string;
|
|
63
|
+
text_sha256: string;
|
|
64
|
+
};
|
|
65
|
+
claims: ClaimEntry[];
|
|
66
|
+
service?: {
|
|
67
|
+
seq: number;
|
|
68
|
+
line_hash: string;
|
|
69
|
+
outage: true;
|
|
70
|
+
} | {
|
|
71
|
+
seq: number;
|
|
72
|
+
line_hash: string;
|
|
73
|
+
model?: string;
|
|
74
|
+
version?: string;
|
|
75
|
+
claims: Array<{
|
|
76
|
+
index: number;
|
|
77
|
+
status: string;
|
|
78
|
+
text_sha256: string;
|
|
79
|
+
score?: number;
|
|
80
|
+
}>;
|
|
81
|
+
};
|
|
82
|
+
sources: {
|
|
83
|
+
seq: number;
|
|
84
|
+
id: string;
|
|
85
|
+
uri?: string;
|
|
86
|
+
version?: string;
|
|
87
|
+
sha256: string;
|
|
88
|
+
}[];
|
|
89
|
+
packs: {
|
|
90
|
+
id: string;
|
|
91
|
+
version: string;
|
|
92
|
+
hash: string;
|
|
93
|
+
}[];
|
|
94
|
+
issued_at: string;
|
|
95
|
+
} | null;
|
|
96
|
+
/** §11: a credential checked against the record it names: recomputed, then compared field by field
|
|
97
|
+
* (the signature is `verifyCertificate`'s job). */
|
|
98
|
+
export declare function verifyAnswerCredential(credential: Record<string, unknown>, lines: readonly string[], bodies: Bodies, packs?: readonly ReferencePack[]): {
|
|
99
|
+
ok: boolean;
|
|
100
|
+
problems: string[];
|
|
101
|
+
};
|