@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.
@@ -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("0.9.1")
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 locally")
153
- .action(async (file) => {
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
- await cardValidateCommand(file);
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 locally")
228
- .action(async (file) => {
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
- await protectionValidateCommand(file);
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, } from "./commands/recipes.js";
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();