@mnemom/mnemom 0.12.0 → 0.12.1

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.
@@ -23,7 +23,7 @@
23
23
  * filed" anchor and the candidate_id is the primary track-handle.
24
24
  */
25
25
  import chalk from "chalk";
26
- import { reportRecipeFnFp, } from "../lib/api.js";
26
+ import { reportRecipeFnFp } from "../lib/api.js";
27
27
  import { requireAuth } from "../lib/auth.js";
28
28
  async function readSummary(opts) {
29
29
  if (typeof opts.summary === "string" && opts.summary.trim()) {
@@ -42,7 +42,7 @@ async function readSummary(opts) {
42
42
  if (piped)
43
43
  return piped;
44
44
  }
45
- throw new Error("summary required: pass --summary \"…\" or pipe text on stdin.");
45
+ throw new Error('summary required: pass --summary "" or pipe text on stdin.');
46
46
  }
47
47
  async function runReport(recipeId, type, opts) {
48
48
  await requireAuth();
@@ -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
@@ -1214,7 +1214,7 @@ program
1214
1214
  // fingerprint is folded server-side so a retry with corrected text returns
1215
1215
  // 422 rather than silently replaying.
1216
1216
  // ============================================================================
1217
- import { recipesReportFnCommand, recipesReportFpCommand, } from "./commands/recipes.js";
1217
+ import { recipesReportFnCommand, recipesReportFpCommand } from "./commands/recipes.js";
1218
1218
  const recipesCmd = program
1219
1219
  .command("recipes")
1220
1220
  .description("Customer-side recipe reporting (false-negatives, false-positives)");
@@ -1252,4 +1252,43 @@ recipesCmd
1252
1252
  process.exit(1);
1253
1253
  }
1254
1254
  });
1255
+ // ============================================================================
1256
+ // verify-card — cards-as-primitive Phase 5 D1
1257
+ // ============================================================================
1258
+ //
1259
+ // Offline verifier for AAP attestation tokens + transparency-log Merkle
1260
+ // inclusion proofs. Validates that a Mnemom-published canonical card was
1261
+ // in fact composed by Mnemom at the claimed (content_hash, version,
1262
+ // composed_at) point in time. JWKS is cached at ~/.mnemom/cache/aap-jwks.json
1263
+ // for one hour; --strict bypasses the cache.
1264
+ program
1265
+ .command("verify-card")
1266
+ .description("Verify a Mnemom-published canonical card's AAP attestation + Merkle inclusion proof offline.")
1267
+ .argument("<agent_id>", "Mnemom agent id (e.g., smolt-e2ca60ef)")
1268
+ .option("--at <iso>", "Verify the historic posture at this ISO-8601 timestamp (defaults to current live A2A export).")
1269
+ .option("--card-kind <kind>", "alignment | protection (defaults to alignment)", "alignment")
1270
+ .option("--api <url>", "Mnemom API base (defaults to https://api.mnemom.ai)", "https://api.mnemom.ai")
1271
+ .option("--strict", "Bypass JWKS cache; exit non-zero on any verification gap.")
1272
+ .option("--jwks-cache <path>", "Override the JWKS cache path (default ~/.mnemom/cache/aap-jwks.json)")
1273
+ .option("--no-cache", "Skip the JWKS cache for this invocation.")
1274
+ .action(async (agentId, options) => {
1275
+ try {
1276
+ const { verifyCardCommand } = await import("./commands/verify-card.js");
1277
+ const kind = options.cardKind === "protection" ? "protection" : "alignment";
1278
+ const rc = await verifyCardCommand({
1279
+ agentId,
1280
+ at: options.at,
1281
+ cardKind: kind,
1282
+ api: options.api,
1283
+ strict: options.strict,
1284
+ jwksCache: options.jwksCache,
1285
+ noCache: options.cache === false,
1286
+ });
1287
+ process.exit(rc);
1288
+ }
1289
+ catch (error) {
1290
+ console.error("Error:", error instanceof Error ? error.message : error);
1291
+ process.exit(1);
1292
+ }
1293
+ });
1255
1294
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {