@mnemom/mnemom 0.12.0 → 0.13.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/dist/commands/card.d.ts +4 -1
- package/dist/commands/card.js +129 -16
- package/dist/commands/integrity.js +7 -3
- package/dist/commands/license.js +30 -42
- package/dist/commands/logs.js +25 -9
- package/dist/commands/protection.d.ts +4 -1
- package/dist/commands/protection.js +74 -2
- package/dist/commands/recipes.js +2 -2
- package/dist/commands/status.js +12 -4
- package/dist/commands/verify-card.d.ts +102 -0
- package/dist/commands/verify-card.js +287 -0
- package/dist/index.js +52 -8
- package/dist/lib/api.d.ts +214 -110
- package/dist/lib/api.js +305 -287
- package/dist/lib/webhooks-api.js +10 -5
- package/dist/version.d.ts +1 -0
- package/dist/version.js +16 -0
- package/package.json +5 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom verify-card` — cards-as-primitive Phase 5 D1.
|
|
3
|
+
*
|
|
4
|
+
* Offline verifier for AAP attestation tokens + transparency-log
|
|
5
|
+
* inclusion proofs. Fetches public-key material from
|
|
6
|
+
* `<api>/v1/.well-known/jwks.json` once, caches under `~/.mnemom/cache/`,
|
|
7
|
+
* then verifies the attestation against the cached JWKS without any
|
|
8
|
+
* subsequent server round-trip beyond the data the verifier asks for.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* mnemom verify-card <agent_id> [--at <ISO>] [--card-kind alignment|protection]
|
|
12
|
+
* [--api https://api.mnemom.ai] [--strict]
|
|
13
|
+
* [--jwks-cache <path>] [--no-cache]
|
|
14
|
+
*
|
|
15
|
+
* Behavior:
|
|
16
|
+
* 1. Fetch JWKS from <api>/v1/.well-known/jwks.json (or load cached).
|
|
17
|
+
* 2. If --at present: fetch /v1/transparency/log/{id}?at=<ts>&card_kind=<k>.
|
|
18
|
+
* Else: fetch the agent's current A2A AgentCard at
|
|
19
|
+
* /v1/agents/{id}/a2a-agent-card and extract the embedded attestation.
|
|
20
|
+
* 3. Verify the JWS signature against the JWKS.
|
|
21
|
+
* 4. Fetch the transparency-log row by log_index for the freshly-computed
|
|
22
|
+
* inclusion proof, plus /v1/transparency/root for the signed root.
|
|
23
|
+
* 5. Verify the Merkle inclusion proof against the root.
|
|
24
|
+
* 6. Print a care-framed result.
|
|
25
|
+
*
|
|
26
|
+
* --strict mode bypasses the JWKS cache and exits non-zero on any
|
|
27
|
+
* verification gap (signature, expiry, proof, root signature).
|
|
28
|
+
*
|
|
29
|
+
* care-framing:rule-slug care-framed-output — stderr lines use the
|
|
30
|
+
* would-benefit-from vocabulary; stdout carries machine-readable
|
|
31
|
+
* verification status.
|
|
32
|
+
*/
|
|
33
|
+
interface JwkPublic {
|
|
34
|
+
kty: "OKP";
|
|
35
|
+
crv: "Ed25519";
|
|
36
|
+
x: string;
|
|
37
|
+
kid: string;
|
|
38
|
+
alg?: string;
|
|
39
|
+
use?: string;
|
|
40
|
+
mnemom_ext?: {
|
|
41
|
+
status?: "active" | "retired";
|
|
42
|
+
activated_at?: string;
|
|
43
|
+
retired_at?: string;
|
|
44
|
+
rotation_reason?: string;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
interface Jwks {
|
|
48
|
+
keys: JwkPublic[];
|
|
49
|
+
}
|
|
50
|
+
interface AttestationPayload {
|
|
51
|
+
typ: "AAP-Attestation/v1";
|
|
52
|
+
iss: string;
|
|
53
|
+
sub: string;
|
|
54
|
+
iat: number;
|
|
55
|
+
exp: number;
|
|
56
|
+
content_hash: string;
|
|
57
|
+
version: number;
|
|
58
|
+
composed_at: string;
|
|
59
|
+
card_kind: "alignment" | "protection";
|
|
60
|
+
smolt_id?: string;
|
|
61
|
+
historic_backfill?: true;
|
|
62
|
+
}
|
|
63
|
+
interface MerkleProof {
|
|
64
|
+
leaf_hash: string;
|
|
65
|
+
log_index: number;
|
|
66
|
+
tree_size: number;
|
|
67
|
+
hashes: {
|
|
68
|
+
sibling: string;
|
|
69
|
+
position: "left" | "right";
|
|
70
|
+
}[];
|
|
71
|
+
}
|
|
72
|
+
export interface VerifyCardOptions {
|
|
73
|
+
agentId: string;
|
|
74
|
+
at?: string;
|
|
75
|
+
cardKind?: "alignment" | "protection";
|
|
76
|
+
api?: string;
|
|
77
|
+
strict?: boolean;
|
|
78
|
+
jwksCache?: string;
|
|
79
|
+
noCache?: boolean;
|
|
80
|
+
}
|
|
81
|
+
export declare function verifyCardCommand(options: VerifyCardOptions): Promise<number>;
|
|
82
|
+
interface JwsVerification {
|
|
83
|
+
ok: boolean;
|
|
84
|
+
kid?: string;
|
|
85
|
+
payload?: AttestationPayload;
|
|
86
|
+
error?: string;
|
|
87
|
+
}
|
|
88
|
+
declare function verifyJws(jws: string, jwks: Jwks): Promise<JwsVerification>;
|
|
89
|
+
declare function verifyInclusionProof(proof: MerkleProof, expectedRoot: string): Promise<boolean>;
|
|
90
|
+
declare function internalHash(left: string, right: string): Promise<string>;
|
|
91
|
+
declare function base64urlDecode(s: string): Uint8Array;
|
|
92
|
+
declare function base64urlDecodeString(s: string): string;
|
|
93
|
+
declare function hexToBytes(hex: string): Uint8Array;
|
|
94
|
+
export declare const __testing: {
|
|
95
|
+
verifyInclusionProof: typeof verifyInclusionProof;
|
|
96
|
+
internalHash: typeof internalHash;
|
|
97
|
+
hexToBytes: typeof hexToBytes;
|
|
98
|
+
base64urlDecode: typeof base64urlDecode;
|
|
99
|
+
base64urlDecodeString: typeof base64urlDecodeString;
|
|
100
|
+
verifyJws: typeof verifyJws;
|
|
101
|
+
};
|
|
102
|
+
export {};
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom verify-card` — cards-as-primitive Phase 5 D1.
|
|
3
|
+
*
|
|
4
|
+
* Offline verifier for AAP attestation tokens + transparency-log
|
|
5
|
+
* inclusion proofs. Fetches public-key material from
|
|
6
|
+
* `<api>/v1/.well-known/jwks.json` once, caches under `~/.mnemom/cache/`,
|
|
7
|
+
* then verifies the attestation against the cached JWKS without any
|
|
8
|
+
* subsequent server round-trip beyond the data the verifier asks for.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* mnemom verify-card <agent_id> [--at <ISO>] [--card-kind alignment|protection]
|
|
12
|
+
* [--api https://api.mnemom.ai] [--strict]
|
|
13
|
+
* [--jwks-cache <path>] [--no-cache]
|
|
14
|
+
*
|
|
15
|
+
* Behavior:
|
|
16
|
+
* 1. Fetch JWKS from <api>/v1/.well-known/jwks.json (or load cached).
|
|
17
|
+
* 2. If --at present: fetch /v1/transparency/log/{id}?at=<ts>&card_kind=<k>.
|
|
18
|
+
* Else: fetch the agent's current A2A AgentCard at
|
|
19
|
+
* /v1/agents/{id}/a2a-agent-card and extract the embedded attestation.
|
|
20
|
+
* 3. Verify the JWS signature against the JWKS.
|
|
21
|
+
* 4. Fetch the transparency-log row by log_index for the freshly-computed
|
|
22
|
+
* inclusion proof, plus /v1/transparency/root for the signed root.
|
|
23
|
+
* 5. Verify the Merkle inclusion proof against the root.
|
|
24
|
+
* 6. Print a care-framed result.
|
|
25
|
+
*
|
|
26
|
+
* --strict mode bypasses the JWKS cache and exits non-zero on any
|
|
27
|
+
* verification gap (signature, expiry, proof, root signature).
|
|
28
|
+
*
|
|
29
|
+
* care-framing:rule-slug care-framed-output — stderr lines use the
|
|
30
|
+
* would-benefit-from vocabulary; stdout carries machine-readable
|
|
31
|
+
* verification status.
|
|
32
|
+
*/
|
|
33
|
+
import * as fs from "node:fs";
|
|
34
|
+
import * as path from "node:path";
|
|
35
|
+
import { MNEMOM_DIR } from "../lib/config.js";
|
|
36
|
+
const JWKS_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
37
|
+
const DEFAULT_API = "https://api.mnemom.ai";
|
|
38
|
+
export async function verifyCardCommand(options) {
|
|
39
|
+
const api = (options.api ?? DEFAULT_API).replace(/\/+$/, "");
|
|
40
|
+
const cardKind = options.cardKind ?? "alignment";
|
|
41
|
+
const cachePath = options.jwksCache ?? path.join(MNEMOM_DIR, "cache", "aap-jwks.json");
|
|
42
|
+
let exitCode = 0;
|
|
43
|
+
const findings = [];
|
|
44
|
+
try {
|
|
45
|
+
const jwks = await loadJwks(api, cachePath, !options.strict && !options.noCache);
|
|
46
|
+
// Step 2: fetch the attestation + log row.
|
|
47
|
+
const target = await resolveTargetEntry(api, options.agentId, cardKind, options.at);
|
|
48
|
+
if (!target) {
|
|
49
|
+
process.stderr.write(`No attestation was integrated for ${options.agentId} (kind=${cardKind})${options.at ? ` at-or-before ${options.at}` : ""} — nothing to verify yet.\n`);
|
|
50
|
+
return options.strict ? 1 : 0;
|
|
51
|
+
}
|
|
52
|
+
// Step 3: verify the JWS signature against the JWKS.
|
|
53
|
+
const verification = await verifyJws(target.entry.signed_attestation, jwks);
|
|
54
|
+
if (!verification.ok) {
|
|
55
|
+
findings.push(`signature: ${verification.error}`);
|
|
56
|
+
exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
findings.push(`signature: ok (kid=${verification.kid})`);
|
|
60
|
+
}
|
|
61
|
+
const payload = verification.payload;
|
|
62
|
+
// Step 4: fetch signed root.
|
|
63
|
+
const root = await fetchSignedRoot(api);
|
|
64
|
+
if (!root) {
|
|
65
|
+
findings.push("merkle: transparency log is empty (no root to verify against)");
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// Step 5: verify the Merkle inclusion proof.
|
|
69
|
+
const proofOk = await verifyInclusionProof(target.inclusion_proof, root.root_hash);
|
|
70
|
+
if (!proofOk) {
|
|
71
|
+
findings.push(`merkle: inclusion proof did not reconstruct root (log_index=${target.entry.log_index}, tree_size=${target.inclusion_proof.tree_size})`);
|
|
72
|
+
exitCode = 1;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
findings.push(`merkle: inclusion proof ok (log_index=${target.entry.log_index}, tree_size=${target.inclusion_proof.tree_size})`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Step 6: print result.
|
|
79
|
+
const result = {
|
|
80
|
+
ok: exitCode === 0,
|
|
81
|
+
agent_id: target.entry.agent_id,
|
|
82
|
+
card_kind: target.entry.card_kind,
|
|
83
|
+
content_hash: target.entry.content_hash,
|
|
84
|
+
version: target.entry.version,
|
|
85
|
+
composed_at: target.entry.composed_at,
|
|
86
|
+
integrated_time: target.entry.integrated_time,
|
|
87
|
+
signing_key_id: target.entry.signing_key_id,
|
|
88
|
+
historic_backfill: payload?.historic_backfill === true,
|
|
89
|
+
findings,
|
|
90
|
+
};
|
|
91
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
92
|
+
if (exitCode === 0) {
|
|
93
|
+
process.stderr.write(`Canonical posture verifies: ${target.entry.agent_id} @ version ${target.entry.version} composed ${target.entry.composed_at}.\n`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
process.stderr.write(`Verification gaps surfaced — see findings above.\n`);
|
|
97
|
+
}
|
|
98
|
+
return exitCode;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
process.stderr.write(`verify-card couldn't complete — the underlying registry surfaced an error: ${err.message}\n`);
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// ── JWKS fetch + cache ───────────────────────────────────────────────
|
|
106
|
+
async function loadJwks(api, cachePath, useCache) {
|
|
107
|
+
if (useCache && fs.existsSync(cachePath)) {
|
|
108
|
+
try {
|
|
109
|
+
const stat = fs.statSync(cachePath);
|
|
110
|
+
if (Date.now() - stat.mtimeMs < JWKS_CACHE_TTL_MS) {
|
|
111
|
+
const raw = fs.readFileSync(cachePath, "utf8");
|
|
112
|
+
return JSON.parse(raw);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
/* fall through to network */
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const res = await fetch(`${api}/v1/.well-known/jwks.json`);
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
throw new Error(`JWKS fetch failed: ${res.status} ${await res.text()}`);
|
|
122
|
+
}
|
|
123
|
+
const jwks = (await res.json());
|
|
124
|
+
if (useCache) {
|
|
125
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
126
|
+
fs.writeFileSync(cachePath, JSON.stringify(jwks, null, 2));
|
|
127
|
+
}
|
|
128
|
+
return jwks;
|
|
129
|
+
}
|
|
130
|
+
async function resolveTargetEntry(api, agentId, cardKind, at) {
|
|
131
|
+
if (at) {
|
|
132
|
+
const url = new URL(`${api}/v1/transparency/log/${encodeURIComponent(agentId)}`);
|
|
133
|
+
url.searchParams.set("at", at);
|
|
134
|
+
url.searchParams.set("card_kind", cardKind);
|
|
135
|
+
const res = await fetch(url.toString());
|
|
136
|
+
if (res.status === 404)
|
|
137
|
+
return null;
|
|
138
|
+
if (!res.ok) {
|
|
139
|
+
throw new Error(`transparency log at lookup failed: ${res.status}`);
|
|
140
|
+
}
|
|
141
|
+
return (await res.json());
|
|
142
|
+
}
|
|
143
|
+
// No --at: fetch the A2A AgentCard for live attestation, then locate
|
|
144
|
+
// the matching log entry by content_hash + version.
|
|
145
|
+
const a2aRes = await fetch(`${api}/v1/agents/${encodeURIComponent(agentId)}/a2a-agent-card`);
|
|
146
|
+
if (a2aRes.status === 404)
|
|
147
|
+
return null;
|
|
148
|
+
if (!a2aRes.ok) {
|
|
149
|
+
throw new Error(`A2A export fetch failed: ${a2aRes.status}`);
|
|
150
|
+
}
|
|
151
|
+
const card = (await a2aRes.json());
|
|
152
|
+
const attestation = card.extensions?.find((e) => e.uri === "https://aap.mnemom.ai/v1/attestation");
|
|
153
|
+
if (!attestation || typeof attestation.body?.token !== "string") {
|
|
154
|
+
throw new Error("A2A AgentCard exists but ships no AAP attestation extension — AAP_ATTESTATION_SIGNING_ENABLED may be off on the server.");
|
|
155
|
+
}
|
|
156
|
+
// Decode payload for content_hash + version to locate the log entry.
|
|
157
|
+
const segs = attestation.body.token.split(".");
|
|
158
|
+
if (segs.length !== 3) {
|
|
159
|
+
throw new Error("Embedded attestation is not a 3-part JWS.");
|
|
160
|
+
}
|
|
161
|
+
const payload = JSON.parse(base64urlDecodeString(segs[1]));
|
|
162
|
+
// Now fetch the log row matching this content_hash + version.
|
|
163
|
+
return resolveLatestLogEntry(api, agentId, cardKind, payload.composed_at);
|
|
164
|
+
}
|
|
165
|
+
async function resolveLatestLogEntry(api, agentId, cardKind, composedAt) {
|
|
166
|
+
const url = new URL(`${api}/v1/transparency/log/${encodeURIComponent(agentId)}`);
|
|
167
|
+
url.searchParams.set("at", composedAt);
|
|
168
|
+
url.searchParams.set("card_kind", cardKind);
|
|
169
|
+
const res = await fetch(url.toString());
|
|
170
|
+
if (res.status === 404)
|
|
171
|
+
return null;
|
|
172
|
+
if (!res.ok) {
|
|
173
|
+
throw new Error(`transparency log latest lookup failed: ${res.status}`);
|
|
174
|
+
}
|
|
175
|
+
return (await res.json());
|
|
176
|
+
}
|
|
177
|
+
async function fetchSignedRoot(api) {
|
|
178
|
+
const res = await fetch(`${api}/v1/transparency/root`);
|
|
179
|
+
if (!res.ok)
|
|
180
|
+
return null;
|
|
181
|
+
const body = (await res.json());
|
|
182
|
+
if (body.tree_size === 0)
|
|
183
|
+
return null;
|
|
184
|
+
return body;
|
|
185
|
+
}
|
|
186
|
+
async function verifyJws(jws, jwks) {
|
|
187
|
+
const parts = jws.split(".");
|
|
188
|
+
if (parts.length !== 3) {
|
|
189
|
+
return { ok: false, error: "JWS does not have three parts" };
|
|
190
|
+
}
|
|
191
|
+
let header;
|
|
192
|
+
let payload;
|
|
193
|
+
try {
|
|
194
|
+
header = JSON.parse(base64urlDecodeString(parts[0]));
|
|
195
|
+
payload = JSON.parse(base64urlDecodeString(parts[1]));
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return { ok: false, error: "could not parse JWS header/payload" };
|
|
199
|
+
}
|
|
200
|
+
if (header.alg !== "EdDSA" || header.typ !== "AAP-Attestation/v1") {
|
|
201
|
+
return { ok: false, error: `unexpected header alg=${header.alg} typ=${header.typ}` };
|
|
202
|
+
}
|
|
203
|
+
const kid = header.kid ?? "";
|
|
204
|
+
const key = jwks.keys.find((k) => k.kid === kid);
|
|
205
|
+
if (!key) {
|
|
206
|
+
return { ok: false, error: `kid ${kid} not in JWKS` };
|
|
207
|
+
}
|
|
208
|
+
const publicKey = await crypto.subtle.importKey("jwk", { kty: key.kty, crv: key.crv, x: key.x, ext: true }, { name: "Ed25519" }, true, ["verify"]);
|
|
209
|
+
const signingInput = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
|
|
210
|
+
const sigBytes = base64urlDecode(parts[2]);
|
|
211
|
+
// BufferSource over a fresh ArrayBuffer keeps newer TS lib defs happy
|
|
212
|
+
// (older Uint8Array<ArrayBufferLike> trips a SharedArrayBuffer mismatch).
|
|
213
|
+
const sigBuf = new ArrayBuffer(sigBytes.length);
|
|
214
|
+
new Uint8Array(sigBuf).set(sigBytes);
|
|
215
|
+
const sigOk = await crypto.subtle.verify("Ed25519", publicKey, sigBuf, signingInput);
|
|
216
|
+
if (!sigOk) {
|
|
217
|
+
return { ok: false, kid, payload, error: "signature did not verify" };
|
|
218
|
+
}
|
|
219
|
+
const nowSecs = Math.floor(Date.now() / 1000);
|
|
220
|
+
if (nowSecs >= payload.exp) {
|
|
221
|
+
return {
|
|
222
|
+
ok: false,
|
|
223
|
+
kid,
|
|
224
|
+
payload,
|
|
225
|
+
error: `token expired at ${new Date(payload.exp * 1000).toISOString()} — the transparency log row still proves the historic posture but the embedded token is past its TTL.`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return { ok: true, kid, payload };
|
|
229
|
+
}
|
|
230
|
+
// ── Merkle proof verification ────────────────────────────────────────
|
|
231
|
+
async function verifyInclusionProof(proof, expectedRoot) {
|
|
232
|
+
let current = proof.leaf_hash;
|
|
233
|
+
for (const step of proof.hashes) {
|
|
234
|
+
if (step.position === "right") {
|
|
235
|
+
current = await internalHash(current, step.sibling);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
current = await internalHash(step.sibling, current);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return current === expectedRoot;
|
|
242
|
+
}
|
|
243
|
+
async function internalHash(left, right) {
|
|
244
|
+
const leftBytes = hexToBytes(left);
|
|
245
|
+
const rightBytes = hexToBytes(right);
|
|
246
|
+
const buf = new Uint8Array(1 + leftBytes.length + rightBytes.length);
|
|
247
|
+
buf[0] = 0x01;
|
|
248
|
+
buf.set(leftBytes, 1);
|
|
249
|
+
buf.set(rightBytes, 1 + leftBytes.length);
|
|
250
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
251
|
+
return Array.from(new Uint8Array(digest))
|
|
252
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
253
|
+
.join("");
|
|
254
|
+
}
|
|
255
|
+
// ── Encoding helpers ─────────────────────────────────────────────────
|
|
256
|
+
function base64urlDecode(s) {
|
|
257
|
+
const padded = s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice(0, (4 - (s.length % 4)) % 4);
|
|
258
|
+
const bin = atob(padded);
|
|
259
|
+
const out = new Uint8Array(bin.length);
|
|
260
|
+
for (let i = 0; i < bin.length; i++)
|
|
261
|
+
out[i] = bin.charCodeAt(i);
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
function base64urlDecodeString(s) {
|
|
265
|
+
return new TextDecoder().decode(base64urlDecode(s));
|
|
266
|
+
}
|
|
267
|
+
function hexToBytes(hex) {
|
|
268
|
+
if (hex.length % 2 !== 0) {
|
|
269
|
+
throw new Error(`hexToBytes: odd-length hex "${hex}"`);
|
|
270
|
+
}
|
|
271
|
+
const out = new Uint8Array(hex.length / 2);
|
|
272
|
+
for (let i = 0; i < out.length; i++) {
|
|
273
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
// Sentinel for tests that want to exercise individual helpers.
|
|
278
|
+
export const __testing = {
|
|
279
|
+
verifyInclusionProof,
|
|
280
|
+
internalHash,
|
|
281
|
+
hexToBytes,
|
|
282
|
+
base64urlDecode,
|
|
283
|
+
base64urlDecodeString,
|
|
284
|
+
verifyJws,
|
|
285
|
+
};
|
|
286
|
+
// Silence "MNEMOM_DIR unused" if the cache code path is taken in tests.
|
|
287
|
+
void MNEMOM_DIR;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { program } from "commander";
|
|
3
|
+
import { CLI_VERSION } from "./version.js";
|
|
3
4
|
import { statusCommand } from "./commands/status.js";
|
|
4
5
|
import { integrityCommand } from "./commands/integrity.js";
|
|
5
6
|
import { logsCommand } from "./commands/logs.js";
|
|
@@ -20,7 +21,7 @@ import { listenCommand } from "./commands/listen.js";
|
|
|
20
21
|
program
|
|
21
22
|
.name("mnemom")
|
|
22
23
|
.description("Transparent AI agent tracing")
|
|
23
|
-
.version(
|
|
24
|
+
.version(CLI_VERSION)
|
|
24
25
|
.option("--agent <name>", "Select agent by name (or set MNEMOM_AGENT)");
|
|
25
26
|
program
|
|
26
27
|
.command("status")
|
|
@@ -149,10 +150,12 @@ cardCmd
|
|
|
149
150
|
cardCmd
|
|
150
151
|
.command("validate")
|
|
151
152
|
.argument("<file>", "Path to alignment card file (YAML or JSON)")
|
|
152
|
-
.description("Validate alignment card
|
|
153
|
-
.
|
|
153
|
+
.description("Validate an alignment card (server-authoritative when --agent is set; --offline forces local)")
|
|
154
|
+
.option("--offline", "Validate locally only (skip the server preview-compose)")
|
|
155
|
+
.action(async (file, subOpts) => {
|
|
154
156
|
try {
|
|
155
|
-
|
|
157
|
+
const opts = program.opts();
|
|
158
|
+
await cardValidateCommand(file, { offline: subOpts.offline, agent: opts.agent });
|
|
156
159
|
}
|
|
157
160
|
catch (error) {
|
|
158
161
|
console.error("Error:", error instanceof Error ? error.message : error);
|
|
@@ -224,10 +227,12 @@ protectionCmd
|
|
|
224
227
|
protectionCmd
|
|
225
228
|
.command("validate")
|
|
226
229
|
.argument("<file>", "Path to protection card file (YAML or JSON)")
|
|
227
|
-
.description("Validate protection card
|
|
228
|
-
.
|
|
230
|
+
.description("Validate a protection card (server-authoritative when --agent is set; --offline forces local)")
|
|
231
|
+
.option("--offline", "Validate locally only (skip the server preview-compose)")
|
|
232
|
+
.action(async (file, subOpts) => {
|
|
229
233
|
try {
|
|
230
|
-
|
|
234
|
+
const opts = program.opts();
|
|
235
|
+
await protectionValidateCommand(file, { offline: subOpts.offline, agent: opts.agent });
|
|
231
236
|
}
|
|
232
237
|
catch (error) {
|
|
233
238
|
console.error("Error:", error instanceof Error ? error.message : error);
|
|
@@ -1214,7 +1219,7 @@ program
|
|
|
1214
1219
|
// fingerprint is folded server-side so a retry with corrected text returns
|
|
1215
1220
|
// 422 rather than silently replaying.
|
|
1216
1221
|
// ============================================================================
|
|
1217
|
-
import { recipesReportFnCommand, recipesReportFpCommand
|
|
1222
|
+
import { recipesReportFnCommand, recipesReportFpCommand } from "./commands/recipes.js";
|
|
1218
1223
|
const recipesCmd = program
|
|
1219
1224
|
.command("recipes")
|
|
1220
1225
|
.description("Customer-side recipe reporting (false-negatives, false-positives)");
|
|
@@ -1252,4 +1257,43 @@ recipesCmd
|
|
|
1252
1257
|
process.exit(1);
|
|
1253
1258
|
}
|
|
1254
1259
|
});
|
|
1260
|
+
// ============================================================================
|
|
1261
|
+
// verify-card — cards-as-primitive Phase 5 D1
|
|
1262
|
+
// ============================================================================
|
|
1263
|
+
//
|
|
1264
|
+
// Offline verifier for AAP attestation tokens + transparency-log Merkle
|
|
1265
|
+
// inclusion proofs. Validates that a Mnemom-published canonical card was
|
|
1266
|
+
// in fact composed by Mnemom at the claimed (content_hash, version,
|
|
1267
|
+
// composed_at) point in time. JWKS is cached at ~/.mnemom/cache/aap-jwks.json
|
|
1268
|
+
// for one hour; --strict bypasses the cache.
|
|
1269
|
+
program
|
|
1270
|
+
.command("verify-card")
|
|
1271
|
+
.description("Verify a Mnemom-published canonical card's AAP attestation + Merkle inclusion proof offline.")
|
|
1272
|
+
.argument("<agent_id>", "Mnemom agent id (e.g., smolt-e2ca60ef)")
|
|
1273
|
+
.option("--at <iso>", "Verify the historic posture at this ISO-8601 timestamp (defaults to current live A2A export).")
|
|
1274
|
+
.option("--card-kind <kind>", "alignment | protection (defaults to alignment)", "alignment")
|
|
1275
|
+
.option("--api <url>", "Mnemom API base (defaults to https://api.mnemom.ai)", "https://api.mnemom.ai")
|
|
1276
|
+
.option("--strict", "Bypass JWKS cache; exit non-zero on any verification gap.")
|
|
1277
|
+
.option("--jwks-cache <path>", "Override the JWKS cache path (default ~/.mnemom/cache/aap-jwks.json)")
|
|
1278
|
+
.option("--no-cache", "Skip the JWKS cache for this invocation.")
|
|
1279
|
+
.action(async (agentId, options) => {
|
|
1280
|
+
try {
|
|
1281
|
+
const { verifyCardCommand } = await import("./commands/verify-card.js");
|
|
1282
|
+
const kind = options.cardKind === "protection" ? "protection" : "alignment";
|
|
1283
|
+
const rc = await verifyCardCommand({
|
|
1284
|
+
agentId,
|
|
1285
|
+
at: options.at,
|
|
1286
|
+
cardKind: kind,
|
|
1287
|
+
api: options.api,
|
|
1288
|
+
strict: options.strict,
|
|
1289
|
+
jwksCache: options.jwksCache,
|
|
1290
|
+
noCache: options.cache === false,
|
|
1291
|
+
});
|
|
1292
|
+
process.exit(rc);
|
|
1293
|
+
}
|
|
1294
|
+
catch (error) {
|
|
1295
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
1296
|
+
process.exit(1);
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1255
1299
|
program.parse();
|