@mnemom/mnemom 0.17.37 → 0.17.38-next.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.
Files changed (131) hide show
  1. package/dist/commands/agent-doctor.d.ts +2 -2
  2. package/dist/commands/agent-doctor.js +4 -4
  3. package/dist/commands/agent-run.d.ts +11 -0
  4. package/dist/commands/agent-run.js +37 -0
  5. package/dist/commands/agent-setup.d.ts +11 -9
  6. package/dist/commands/agent-setup.js +24 -19
  7. package/dist/commands/agent-share-render.d.ts +5 -5
  8. package/dist/commands/agent-share-render.js +9 -7
  9. package/dist/commands/agent-share-test-issuer.d.ts +1 -1
  10. package/dist/commands/agent-share-test-issuer.js +1 -1
  11. package/dist/commands/agent.d.ts +8 -1
  12. package/dist/commands/agent.js +14 -3
  13. package/dist/commands/card.js +1 -1
  14. package/dist/commands/onboard.js +5 -4
  15. package/dist/commands/scan.d.ts +35 -8
  16. package/dist/commands/scan.js +134 -45
  17. package/dist/commands/verify-card.d.ts +25 -1
  18. package/dist/commands/verify-card.js +29 -2
  19. package/dist/commands/wrap.js +4 -3
  20. package/dist/index.js +8 -6
  21. package/dist/lib/commit-report.d.ts +7 -13
  22. package/dist/lib/commit-report.js +12 -5
  23. package/dist/lib/conduct-receipt.d.ts +8 -3
  24. package/dist/lib/conduct-receipt.js +29 -7
  25. package/dist/lib/experimental.d.ts +1 -1
  26. package/dist/lib/experimental.js +1 -12
  27. package/dist/lib/runner/executor-client.d.ts +5 -0
  28. package/dist/lib/runner/executor-client.js +8 -0
  29. package/dist/lib/runner/job.d.ts +37 -0
  30. package/dist/lib/runner/job.js +74 -1
  31. package/dist/lib/runner/phases.d.ts +7 -2
  32. package/dist/lib/runner/phases.js +7 -2
  33. package/dist/lib/runner/proxy.d.ts +26 -2
  34. package/dist/lib/runner/proxy.js +33 -1
  35. package/dist/lib/runner/record-commit.d.ts +16 -0
  36. package/dist/lib/runner/record-commit.js +7 -0
  37. package/dist/lib/scan/report-email.d.ts +19 -2
  38. package/dist/lib/scan/report-email.js +78 -22
  39. package/dist/lib/scan/scan.d.ts +2 -0
  40. package/dist/lib/scan/scan.js +2 -1
  41. package/dist/lib/scan/share.d.ts +8 -6
  42. package/dist/lib/scan/share.js +28 -11
  43. package/dist/lib/scan/spend/anthropic-usage.d.ts +20 -1
  44. package/dist/lib/scan/spend/anthropic-usage.js +32 -7
  45. package/dist/lib/scan/spend/anthropic-wif.d.ts +42 -0
  46. package/dist/lib/scan/spend/anthropic-wif.js +153 -0
  47. package/dist/lib/starter-integrity.d.ts +20 -0
  48. package/dist/lib/starter-integrity.js +28 -0
  49. package/package.json +2 -5
  50. package/node_modules/@mnemom/aip-verifier/LICENSE +0 -190
  51. package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.cts +0 -478
  52. package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.ts +0 -478
  53. package/node_modules/@mnemom/aip-verifier/dist/index.cjs +0 -971
  54. package/node_modules/@mnemom/aip-verifier/dist/index.cjs.map +0 -1
  55. package/node_modules/@mnemom/aip-verifier/dist/index.d.cts +0 -330
  56. package/node_modules/@mnemom/aip-verifier/dist/index.d.ts +0 -330
  57. package/node_modules/@mnemom/aip-verifier/dist/index.js +0 -933
  58. package/node_modules/@mnemom/aip-verifier/dist/index.js.map +0 -1
  59. package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs +0 -879
  60. package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs.map +0 -1
  61. package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.cts +0 -96
  62. package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.ts +0 -96
  63. package/node_modules/@mnemom/aip-verifier/dist/render-lines.js +0 -875
  64. package/node_modules/@mnemom/aip-verifier/dist/render-lines.js.map +0 -1
  65. package/node_modules/@mnemom/aip-verifier/package.json +0 -86
  66. package/node_modules/@noble/ed25519/LICENSE +0 -21
  67. package/node_modules/@noble/ed25519/README.md +0 -371
  68. package/node_modules/@noble/ed25519/index.d.ts +0 -391
  69. package/node_modules/@noble/ed25519/index.js +0 -933
  70. package/node_modules/@noble/ed25519/index.ts +0 -1134
  71. package/node_modules/@noble/ed25519/package.json +0 -54
  72. package/node_modules/@noble/hashes/LICENSE +0 -21
  73. package/node_modules/@noble/hashes/README.md +0 -478
  74. package/node_modules/@noble/hashes/_blake.d.ts +0 -20
  75. package/node_modules/@noble/hashes/_blake.js +0 -59
  76. package/node_modules/@noble/hashes/_md.d.ts +0 -95
  77. package/node_modules/@noble/hashes/_md.js +0 -209
  78. package/node_modules/@noble/hashes/_u64.d.ts +0 -36
  79. package/node_modules/@noble/hashes/_u64.js +0 -77
  80. package/node_modules/@noble/hashes/argon2.d.ts +0 -148
  81. package/node_modules/@noble/hashes/argon2.js +0 -577
  82. package/node_modules/@noble/hashes/blake1.d.ts +0 -162
  83. package/node_modules/@noble/hashes/blake1.js +0 -630
  84. package/node_modules/@noble/hashes/blake2.d.ts +0 -203
  85. package/node_modules/@noble/hashes/blake2.js +0 -573
  86. package/node_modules/@noble/hashes/blake3.d.ts +0 -68
  87. package/node_modules/@noble/hashes/blake3.js +0 -310
  88. package/node_modules/@noble/hashes/eskdf.d.ts +0 -91
  89. package/node_modules/@noble/hashes/eskdf.js +0 -225
  90. package/node_modules/@noble/hashes/hkdf.d.ts +0 -70
  91. package/node_modules/@noble/hashes/hkdf.js +0 -152
  92. package/node_modules/@noble/hashes/hmac.d.ts +0 -50
  93. package/node_modules/@noble/hashes/hmac.js +0 -93
  94. package/node_modules/@noble/hashes/index.d.ts +0 -1
  95. package/node_modules/@noble/hashes/index.js +0 -34
  96. package/node_modules/@noble/hashes/legacy.d.ts +0 -107
  97. package/node_modules/@noble/hashes/legacy.js +0 -348
  98. package/node_modules/@noble/hashes/package.json +0 -91
  99. package/node_modules/@noble/hashes/pbkdf2.d.ts +0 -66
  100. package/node_modules/@noble/hashes/pbkdf2.js +0 -176
  101. package/node_modules/@noble/hashes/scrypt.d.ts +0 -98
  102. package/node_modules/@noble/hashes/scrypt.js +0 -304
  103. package/node_modules/@noble/hashes/sha2.d.ts +0 -176
  104. package/node_modules/@noble/hashes/sha2.js +0 -446
  105. package/node_modules/@noble/hashes/sha3-addons.d.ts +0 -533
  106. package/node_modules/@noble/hashes/sha3-addons.js +0 -973
  107. package/node_modules/@noble/hashes/sha3.d.ts +0 -208
  108. package/node_modules/@noble/hashes/sha3.js +0 -475
  109. package/node_modules/@noble/hashes/src/_blake.ts +0 -67
  110. package/node_modules/@noble/hashes/src/_md.ts +0 -227
  111. package/node_modules/@noble/hashes/src/_u64.ts +0 -111
  112. package/node_modules/@noble/hashes/src/argon2.ts +0 -693
  113. package/node_modules/@noble/hashes/src/blake1.ts +0 -677
  114. package/node_modules/@noble/hashes/src/blake2.ts +0 -648
  115. package/node_modules/@noble/hashes/src/blake3.ts +0 -344
  116. package/node_modules/@noble/hashes/src/eskdf.ts +0 -265
  117. package/node_modules/@noble/hashes/src/hkdf.ts +0 -161
  118. package/node_modules/@noble/hashes/src/hmac.ts +0 -130
  119. package/node_modules/@noble/hashes/src/index.ts +0 -33
  120. package/node_modules/@noble/hashes/src/legacy.ts +0 -361
  121. package/node_modules/@noble/hashes/src/pbkdf2.ts +0 -219
  122. package/node_modules/@noble/hashes/src/scrypt.ts +0 -375
  123. package/node_modules/@noble/hashes/src/sha2.ts +0 -486
  124. package/node_modules/@noble/hashes/src/sha3-addons.ts +0 -1157
  125. package/node_modules/@noble/hashes/src/sha3.ts +0 -525
  126. package/node_modules/@noble/hashes/src/utils.ts +0 -978
  127. package/node_modules/@noble/hashes/src/webcrypto.ts +0 -283
  128. package/node_modules/@noble/hashes/utils.d.ts +0 -560
  129. package/node_modules/@noble/hashes/utils.js +0 -687
  130. package/node_modules/@noble/hashes/webcrypto.d.ts +0 -121
  131. package/node_modules/@noble/hashes/webcrypto.js +0 -228
@@ -9,7 +9,8 @@ import { failureText } from "../lib/scan/failure.js";
9
9
  import { runLocalScan } from "../lib/scan/scan.js";
10
10
  import { askInput, askSelect } from "../lib/prompt.js";
11
11
  import { isSpendTiersEnabled, NOT_ENABLED_MSG, parseMonthlySpendUsd, selectSpend, SPEND_INVALID_MSG, SPEND_NO_TERMINAL_MSG, } from "../lib/scan/spend/spend-tiers.js";
12
- import { ADMIN_KEY_ENV, AnthropicUsageError, fetchMeasuredUsage, isAdminKeyShape, usageFailureText, } from "../lib/scan/spend/anthropic-usage.js";
12
+ import { ADMIN_KEY_ENV, AnthropicUsageError, fetchMeasuredUsage, isAdminKeyShape, usageFailureText, bearerAuth, fetchMeasuredUsageWithAuth, } from "../lib/scan/spend/anthropic-usage.js";
13
+ import { obtainWifAccessToken, readWifConfig, WIF_FALLBACK_NOTE, WifError, wifFailureText, } from "../lib/scan/spend/anthropic-wif.js";
13
14
  import { getAuthInfo } from "../lib/auth.js";
14
15
  import { listMyOrgs } from "../lib/api.js";
15
16
  import { getActiveOrg } from "../lib/cli-config.js";
@@ -27,29 +28,61 @@ export function parseRepoFlags(raw) {
27
28
  .filter(Boolean);
28
29
  return out.length > 0 && out.every(isValidRepoSlug) ? out : null;
29
30
  }
30
- const EMAIL = /^[^\s@]{1,64}@[^\s@]{1,255}\.[^\s@]{2,63}$/;
31
- export const isEmail = (v) => typeof v === "string" && v.length <= 320 && EMAIL.test(v.trim());
32
- /** The runner's email (mnemom login, else asked) and the CTO's (flag, else "are you the CTO?"). */
33
- export async function resolveRecipients(o) {
34
- if (o.ctoEmail !== undefined && !isEmail(o.ctoEmail))
35
- throw new Error("--cto-email must be a valid email address");
36
- let runner = o.runnerEmail ?? getAuthInfo()?.email ?? "";
37
- if (!isEmail(runner))
38
- runner = (await askInput("Your email (the report is sent to you):")).trim();
39
- if (!isEmail(runner))
40
- throw new Error("a valid runner email is required");
41
- runner = runner.trim();
42
- if (o.ctoEmail !== undefined)
43
- return { runner_email: runner, cto_email: o.ctoEmail.trim() };
44
- const isCto = await askSelect("Are you the CTO?", ["Yes, I am the CTO", "No, someone else is"]);
45
- if (isCto === null)
46
- throw new Error("--cto-email is required when you are not the CTO");
47
- if (isCto.startsWith("Yes"))
48
- return { runner_email: runner, cto_email: runner };
49
- const cto = (await askInput("CTO email (the report is sent to them too):")).trim();
50
- if (!isEmail(cto))
51
- throw new Error("--cto-email is required when you are not the CTO");
52
- return { runner_email: runner, cto_email: cto };
31
+ /**
32
+ * Same rule as the API: isPlainEmail in mnemom/mnemom-api src/org-scan/report-email/notifier.ts (no shared
33
+ * package, so this is a copy). Drift guard: the "plain-email rule edge" table in src/__tests__/scan.test.ts;
34
+ * change both together. No spaces, control chars or <>",; ; one @; every domain label non-empty; max 254. */
35
+ const plainEmail = (a) => {
36
+ if (a.length === 0 || a.length > 254)
37
+ return false;
38
+ for (let i = 0; i < a.length; i++) {
39
+ const c = a.charCodeAt(i);
40
+ if (c <= 32 || c === 127 || '<>",;'.includes(a[i]))
41
+ return false;
42
+ }
43
+ const at = a.indexOf("@");
44
+ if (at < 1 || at !== a.lastIndexOf("@"))
45
+ return false;
46
+ const labels = a.slice(at + 1).split(".");
47
+ return labels.length >= 2 && labels.every((l) => l.length > 0);
48
+ };
49
+ export const isEmail = (v) => typeof v === "string" && plainEmail(v.trim());
50
+ export const DELETE_CONTACT = "privacy@mnemom.ai";
51
+ export const EMAIL_ONE_MSG = "Enter one email address, like you@company.com.";
52
+ export const EMAIL_LIST_MSG = "Enter email addresses like you@company.com, separated by commas.";
53
+ export const MAX_EXTRA_EMAILS = 10;
54
+ /**
55
+ * The people to add to the report email (the runner and every org admin always get it). Comes from the
56
+ * repeatable --email flag (and its alias --cto-email), each value one address or a comma-separated list;
57
+ * on a terminal with none given, asks once (blank to skip). Plain-language errors; the value is never echoed.
58
+ */
59
+ export async function resolveExtraEmails(o) {
60
+ const given = [...(o.email ?? []), ...(o.ctoEmail !== undefined ? [o.ctoEmail] : [])];
61
+ let raw = given;
62
+ if (given.length === 0) {
63
+ if (!(o.isTty ?? process.stdin.isTTY))
64
+ return [];
65
+ const typed = ((await askInput("Anyone else to email the report to? Enter addresses separated by commas, or press Enter to skip:")) ?? "").trim();
66
+ if (typed === "")
67
+ return [];
68
+ raw = [typed];
69
+ }
70
+ const list = raw
71
+ .flatMap((v) => v.split(","))
72
+ .map((v) => v.trim())
73
+ .filter(Boolean);
74
+ if (list.length === 0 || !list.every(isEmail))
75
+ throw new Error(raw.length > 1 || raw.some((v) => v.includes(",")) ? EMAIL_LIST_MSG : EMAIL_ONE_MSG);
76
+ const seen = new Set();
77
+ const unique = list.filter((a) => !seen.has(a.toLowerCase()) && seen.add(a.toLowerCase()));
78
+ if (unique.length > MAX_EXTRA_EMAILS)
79
+ throw new Error(`Add at most ${MAX_EXTRA_EMAILS} extra email addresses.`);
80
+ return unique;
81
+ }
82
+ /** The runner's own address from the Mnemom login, or undefined (the server resolves it for the email either way). */
83
+ export function runnerEmailFromLogin(o) {
84
+ const e = (o.runnerEmail ?? getAuthInfo()?.email ?? "").trim();
85
+ return isEmail(e) ? e : undefined;
53
86
  }
54
87
  /** The CTO's figure in USD, or null for none. Throws fixed text only; the input is never echoed. */
55
88
  export async function resolveSelfReportedSpend(o) {
@@ -96,7 +129,37 @@ export const TIER1_FALLBACK_NOTE = "Tier 1 skipped: falling back to the next tie
96
129
  * fixed sentence: the key, the thrown value and the response never reach the terminal. The caller then falls
97
130
  * back to tier 2, then tier 3.
98
131
  */
99
- export async function resolveMeasuredUsage(o, period) {
132
+ export const WIF_OK_NOTE = "Read Anthropic usage through workload identity federation.";
133
+ /**
134
+ * Tier 1 through WIF (MNE-9706): Mnemom issues an identity token for the org, this machine exchanges it at
135
+ * Anthropic and reads only the two usage reports with the result. "not_configured" when the WIF ids are
136
+ * not set; "failed" (after printing a fixed sentence) for every failure, so the caller falls back to the key.
137
+ */
138
+ export async function measuredUsageViaWif(o, orgId, period) {
139
+ let cfg;
140
+ try {
141
+ cfg = readWifConfig(o.env ?? process.env);
142
+ }
143
+ catch (e) {
144
+ console.log(wifFailureText(e instanceof WifError ? e.reason : "config_invalid"));
145
+ return "failed";
146
+ }
147
+ if (cfg === null)
148
+ return "not_configured";
149
+ try {
150
+ const token = await obtainWifAccessToken(orgId, cfg, o.wifDeps);
151
+ const usage = await fetchMeasuredUsageWithAuth(bearerAuth(token.accessToken), period, o.anthropicDeps);
152
+ console.log(WIF_OK_NOTE);
153
+ return usage;
154
+ }
155
+ catch (e) {
156
+ console.log(e instanceof WifError
157
+ ? wifFailureText(e.reason)
158
+ : usageFailureText(e instanceof AnthropicUsageError ? e.reason : "upstream"));
159
+ return "failed";
160
+ }
161
+ }
162
+ export async function resolveMeasuredUsage(o, period, orgId) {
100
163
  if (o.anthropicAdminKey && !isSpendTiersEnabled(o.env))
101
164
  throw new Error(NOT_ENABLED_MSG);
102
165
  if (!isSpendTiersEnabled(o.env) || o.noAnthropicUsage)
@@ -105,6 +168,20 @@ export async function resolveMeasuredUsage(o, period) {
105
168
  if (o.monthlySpend !== undefined && !o.anthropicAdminKey)
106
169
  return null;
107
170
  let key = (o.env ?? process.env)[ADMIN_KEY_ENV];
171
+ // MNE-9706: WIF first when it is set up, unless the user explicitly asked to paste a key.
172
+ let wifFailed = false;
173
+ if (!o.anthropicAdminKey && orgId !== undefined) {
174
+ const viaWif = await measuredUsageViaWif(o, orgId, period);
175
+ if (viaWif !== "not_configured" && viaWif !== "failed")
176
+ return viaWif;
177
+ wifFailed = viaWif === "failed";
178
+ }
179
+ if (wifFailed) {
180
+ const hasKey = key !== undefined && key.trim() !== "";
181
+ console.log(hasKey ? WIF_FALLBACK_NOTE : TIER1_FALLBACK_NOTE);
182
+ if (!hasKey)
183
+ return null;
184
+ }
108
185
  if (o.anthropicAdminKey) {
109
186
  if (!(o.isTty ?? process.stdin.isTTY))
110
187
  throw new Error(NO_ADMIN_KEY_TERMINAL_MSG);
@@ -193,16 +270,20 @@ export async function resolveRecordOutcome(o) {
193
270
  }
194
271
  export async function scanCommand(o) {
195
272
  if (o.share !== undefined && parseShareLevel(o.share) === null) {
196
- throw new Error("--share must be one of: summary, full, none");
273
+ throw new Error("--share must be one of: full, analytics, none (summary also works, same as analytics)");
197
274
  }
198
275
  const noRepoFlag = o.repos.length === 0;
276
+ const interactive = Boolean(o.isTty ?? process.stdin.isTTY);
199
277
  // No --repo: on a terminal pick from the repos this GitHub login can read; otherwise the error stays, with a hint.
200
- if (noRepoFlag && !(o.isTty ?? process.stdin.isTTY))
278
+ if (noRepoFlag && !interactive)
201
279
  throw new Error(`--repo owner/name is required (repeatable). ${REPO_PICKER_NO_TERMINAL_HINT}`);
280
+ // Consent comes first, before any data is processed (MNE-9634). The flag wins; on a terminal with no flag we
281
+ // ask (Full preselected); with no terminal and no flag the answer is Nothing (fail closed).
282
+ const level = parseShareLevel(o.share) ?? (interactive ? await promptShareLevel() : "none");
283
+ // The people added to the report email; the runner and every org admin always get it.
284
+ const extraEmails = await resolveExtraEmails(o);
202
285
  // The report is emailed on every scan, so resolve the org before any scan work; failure is loud.
203
286
  const emailOrg = await resolveScanOrg(o);
204
- if (o.ctoEmail !== undefined && !isEmail(o.ctoEmail))
205
- throw new Error("--cto-email must be a valid email address");
206
287
  const getCredential = async () => {
207
288
  const c = o.credential ?? (await resolveCredential().catch(() => null));
208
289
  if (!c)
@@ -218,7 +299,7 @@ export async function scanCommand(o) {
218
299
  const now = o.now ?? Date.now;
219
300
  const end = new Date(now()).toISOString().slice(0, 10);
220
301
  const period = o.period ?? { start: end, end };
221
- const measured = await resolveMeasuredUsage(o, period);
302
+ const measured = await resolveMeasuredUsage(o, period, emailOrg);
222
303
  const credential = pickedCredential ?? (await getCredential());
223
304
  // Before the local scan: a refused credential fails closed without spending the GitHub reads.
224
305
  const record = await resolveRecordOutcome(o);
@@ -247,19 +328,23 @@ export async function scanCommand(o) {
247
328
  else if (record !== null)
248
329
  for (const line of renderRecordSection(record))
249
330
  console.log(line);
250
- const level = parseShareLevel(o.share) ?? (await promptShareLevel());
251
331
  // Every successful scan emails the report to the person who ran it, at every share level, no opt-out.
252
332
  // Mnemom keeps nothing from a share=none send (the server sends the email and does nothing else).
253
- const emailFailure = await emailReport(o, emailOrg, result, level, spend, period, resolveCostSpend(o, result, spend, measured));
254
- if (level === "none") {
255
- console.log("Share level: nothing. No share payload is built; Mnemom keeps nothing (the report email is only sent to you).");
256
- if (emailFailure)
257
- throw new ReportEmailError(emailFailure);
258
- return;
333
+ const emailResult = await emailReport(o, emailOrg, result, level, spend, period, resolveCostSpend(o, result, spend, measured), extraEmails);
334
+ const emailFailure = emailResult === "not_sent" || emailResult === null ? null : emailResult;
335
+ // What was shared with Mnemom, and how to have it deleted (MNE-9634).
336
+ if (emailResult !== null) {
337
+ console.log("Kept on file by Mnemom: nothing, because the report was not sent.");
259
338
  }
260
- const payload = buildSharePayload(level, result, await resolveRecipients(o), spend ? (spend.shareable ? spend.line : null) : undefined);
261
- console.log(`Share level: ${level}. Upload is not wired in this build; this is what would be sent:`);
262
- console.log(JSON.stringify(payload, null, 2));
339
+ else if (level === "none") {
340
+ console.log("Kept on file by Mnemom: nothing. The report was emailed and Mnemom keeps nothing on file; its email provider keeps only a delivery log.");
341
+ }
342
+ else {
343
+ const payload = buildSharePayload(level, result, { runner_email: runnerEmailFromLogin(o), extra_emails: extraEmails }, spend ? (spend.shareable ? spend.line : null) : undefined);
344
+ console.log(`Kept on file by Mnemom: ${level === "full" ? "the totals and the counts for each repo, by name" : "the team totals only, with no repo names"}. Mnemom may keep a run record of only this much on file; the full report was emailed, not stored:`);
345
+ console.log(JSON.stringify(payload, null, 2));
346
+ }
347
+ console.log(`To have anything Mnemom holds about this scan deleted, email ${DELETE_CONTACT}.`);
263
348
  if (emailFailure)
264
349
  throw new ReportEmailError(emailFailure);
265
350
  }
@@ -267,7 +352,7 @@ export async function scanCommand(o) {
267
352
  * Emails the report. Loud on every failure: prints the reason name, never the server's words, and
268
353
  * returns it so the caller exits non-zero after the local report is shown.
269
354
  */
270
- async function emailReport(o, org, result, level, spend, period, costSpend) {
355
+ async function emailReport(o, org, result, level, spend, period, costSpend, extraEmails) {
271
356
  const fail = (f) => {
272
357
  console.log(failureMessage(f));
273
358
  return f;
@@ -279,17 +364,21 @@ async function emailReport(o, org, result, level, spend, period, costSpend) {
279
364
  period,
280
365
  costSpend,
281
366
  });
282
- const body = buildEmailBody(report, level, result.coverage.complete, o.ctoEmail?.trim());
367
+ const body = buildEmailBody(report, level, result.coverage.complete, extraEmails);
283
368
  const outcome = await sendReportEmail(org, body, o.emailDeps);
284
369
  if (outcome.status === "failed")
285
370
  return fail(outcome.reason);
286
371
  if (outcome.status === "unavailable") {
287
372
  // Org Scan is off by default: the gate's 404 is not a failure and must not break the scan.
288
373
  console.log(UNAVAILABLE_MSG);
289
- return null;
374
+ return "not_sent";
290
375
  }
291
376
  console.log(outcome.status === "dry_run"
292
377
  ? DRY_RUN_MSG
293
- : `Report emailed to you (${outcome.recipients} recipient${outcome.recipients === 1 ? "" : "s"}).`);
294
- return null;
378
+ : outcome.limited
379
+ ? "Report emailed in a limited form (your Mnemom service is older than this CLI): no per-repo counts, no org admins, and at most one added address."
380
+ : `Full report emailed to you, your org admins and anyone you added (${outcome.recipients} recipient${outcome.recipients === 1 ? "" : "s"}).`);
381
+ if (outcome.status === "sent" && outcome.adminsMissed)
382
+ console.log(`${outcome.adminsMissed} org admin${outcome.adminsMissed === 1 ? "" : "s"} could not be emailed because no email address is on file.`);
383
+ return outcome.status === "dry_run" ? "not_sent" : null;
295
384
  }
@@ -20,7 +20,8 @@
20
20
  * 3. Verify the JWS signature against the JWKS.
21
21
  * 4. Fetch the transparency-log row by log_index for the freshly-computed
22
22
  * inclusion proof, plus /v1/transparency/root for the signed root.
23
- * 5. Verify the Merkle inclusion proof against the root.
23
+ * 5. Verify the signed root's Ed25519 signature (typ AAP-TransparencyRoot/v1)
24
+ * with @mnemom/verify, then the Merkle inclusion proof against that root.
24
25
  * 6. Print a care-framed result.
25
26
  *
26
27
  * --strict mode bypasses the JWKS cache and exits non-zero on any
@@ -30,6 +31,7 @@
30
31
  * would-benefit-from vocabulary; stdout carries machine-readable
31
32
  * verification status.
32
33
  */
34
+ import type * as AipVerify from "@mnemom/verify";
33
35
  interface JwkPublic {
34
36
  kty: "OKP";
35
37
  crv: "Ed25519";
@@ -69,6 +71,14 @@ interface MerkleProof {
69
71
  position: "left" | "right";
70
72
  }[];
71
73
  }
74
+ interface SignedRoot {
75
+ typ?: string;
76
+ tree_size: number;
77
+ root_hash: string;
78
+ published_at: string;
79
+ signing_key_id: string;
80
+ signature: string;
81
+ }
72
82
  export interface VerifyCardOptions {
73
83
  agentId: string;
74
84
  at?: string;
@@ -86,6 +96,19 @@ interface JwsVerification {
86
96
  error?: string;
87
97
  }
88
98
  declare function verifyJws(jws: string, jwks: Jwks): Promise<JwsVerification>;
99
+ interface RootVerification {
100
+ ok: boolean;
101
+ kid?: string;
102
+ reasons: string[];
103
+ }
104
+ /**
105
+ * Verify the detached Ed25519 signature on GET /v1/transparency/root with the
106
+ * published offline verifier. `expectedTyp` pins the statement to
107
+ * AAP-TransparencyRoot/v1, so another signed statement cannot stand in for a
108
+ * root. A key missing from a cached JWKS reports key_not_found; --no-cache or
109
+ * --strict refetches it.
110
+ */
111
+ declare function verifySignedRoot(root: SignedRoot, jwks: Jwks, load?: () => Promise<Pick<typeof AipVerify, "verifyStatement">>): Promise<RootVerification>;
89
112
  declare function verifyInclusionProof(proof: MerkleProof, expectedRoot: string): Promise<boolean>;
90
113
  declare function internalHash(left: string, right: string): Promise<string>;
91
114
  declare function base64urlDecode(s: string): Uint8Array;
@@ -98,5 +121,6 @@ export declare const __testing: {
98
121
  base64urlDecode: typeof base64urlDecode;
99
122
  base64urlDecodeString: typeof base64urlDecodeString;
100
123
  verifyJws: typeof verifyJws;
124
+ verifySignedRoot: typeof verifySignedRoot;
101
125
  };
102
126
  export {};
@@ -20,7 +20,8 @@
20
20
  * 3. Verify the JWS signature against the JWKS.
21
21
  * 4. Fetch the transparency-log row by log_index for the freshly-computed
22
22
  * inclusion proof, plus /v1/transparency/root for the signed root.
23
- * 5. Verify the Merkle inclusion proof against the root.
23
+ * 5. Verify the signed root's Ed25519 signature (typ AAP-TransparencyRoot/v1)
24
+ * with @mnemom/verify, then the Merkle inclusion proof against that root.
24
25
  * 6. Print a care-framed result.
25
26
  *
26
27
  * --strict mode bypasses the JWKS cache and exits non-zero on any
@@ -65,7 +66,16 @@ export async function verifyCardCommand(options) {
65
66
  findings.push("merkle: transparency log is empty (no root to verify against)");
66
67
  }
67
68
  else {
68
- // Step 5: verify the Merkle inclusion proof.
69
+ // Step 5a: the root is only worth folding into if Mnemom signed it.
70
+ const rootCheck = await verifySignedRoot(root, jwks);
71
+ if (!rootCheck.ok) {
72
+ findings.push(`root: signature did not verify (${rootCheck.reasons.join(", ")})`);
73
+ exitCode = 1;
74
+ }
75
+ else {
76
+ findings.push(`root: signature ok (kid=${rootCheck.kid}, tree_size=${root.tree_size})`);
77
+ }
78
+ // Step 5b: verify the Merkle inclusion proof.
69
79
  const proofOk = await verifyInclusionProof(target.inclusion_proof, root.root_hash);
70
80
  if (!proofOk) {
71
81
  findings.push(`merkle: inclusion proof did not reconstruct root (log_index=${target.entry.log_index}, tree_size=${target.inclusion_proof.tree_size})`);
@@ -227,6 +237,22 @@ async function verifyJws(jws, jwks) {
227
237
  }
228
238
  return { ok: true, kid, payload };
229
239
  }
240
+ // ── Signed root verification ─────────────────────────────────────────
241
+ const ROOT_TYP = "AAP-TransparencyRoot/v1";
242
+ /**
243
+ * Verify the detached Ed25519 signature on GET /v1/transparency/root with the
244
+ * published offline verifier. `expectedTyp` pins the statement to
245
+ * AAP-TransparencyRoot/v1, so another signed statement cannot stand in for a
246
+ * root. A key missing from a cached JWKS reports key_not_found; --no-cache or
247
+ * --strict refetches it.
248
+ */
249
+ async function verifySignedRoot(root, jwks, load = () => import("@mnemom/verify")) {
250
+ const { verifyStatement } = await load();
251
+ const r = await verifyStatement(root, jwks, {
252
+ expectedTyp: ROOT_TYP,
253
+ });
254
+ return { ok: r.valid, kid: r.signing_key_id ?? undefined, reasons: r.reasons };
255
+ }
230
256
  // ── Merkle proof verification ────────────────────────────────────────
231
257
  async function verifyInclusionProof(proof, expectedRoot) {
232
258
  let current = proof.leaf_hash;
@@ -282,6 +308,7 @@ export const __testing = {
282
308
  base64urlDecode,
283
309
  base64urlDecodeString,
284
310
  verifyJws,
311
+ verifySignedRoot,
285
312
  };
286
313
  // Silence "MNEMOM_DIR unused" if the cache code path is taken in tests.
287
314
  void MNEMOM_DIR;
@@ -20,6 +20,7 @@ import { mergeAgentConfig } from "../lib/agent-config.js";
20
20
  import { askSelect, askInput, isInteractive } from "../lib/prompt.js";
21
21
  import { getGatewayUrl } from "../lib/config.js";
22
22
  import { fmt } from "../lib/format.js";
23
+ import { starterIntegrityMode } from "../lib/starter-integrity.js";
23
24
  import { OPENAI_WRAP_DEFAULT_MODEL, openaiBirthProbeBody } from "../lib/birth-probe.js";
24
25
  const AGENT_HEADER = "x-mnemom-agent";
25
26
  const PROVIDERS = {
@@ -98,7 +99,7 @@ export async function wrapCommand(options = {}) {
98
99
  say();
99
100
  say(`${fmt.badge("alignment", "cyan")} Seeding starter alignment card…`);
100
101
  try {
101
- await putAlignmentCard(agentId, JSON.stringify(buildStarterAlignmentCard(agentId)), "application/json");
102
+ await putAlignmentCard(agentId, JSON.stringify(buildStarterAlignmentCard(agentId, await starterIntegrityMode(agentId))), "application/json");
102
103
  steps.push({ step: "alignment", status: "ok" });
103
104
  say(fmt.success("Alignment card set."));
104
105
  }
@@ -261,12 +262,12 @@ export async function birthThroughGateway(gatewayUrl, provider, agentName, provi
261
262
  return agentId;
262
263
  }
263
264
  // ── Starter cards ─────────────────────────────────────────────────────────────
264
- function buildStarterAlignmentCard(agentId) {
265
+ function buildStarterAlignmentCard(agentId, integrityMode) {
265
266
  return {
266
267
  card_version: "unified/2026-04-26",
267
268
  agent_id: agentId,
268
269
  autonomy_mode: "observe",
269
- integrity_mode: "off",
270
+ integrity_mode: integrityMode, // the agent's own mode; observe for a new agent (MNE-9728)
270
271
  // No capability mappings here, so unmapped tools must pass or every turn fails.
271
272
  enforcement: { allow_unmapped_tools: true },
272
273
  principal: { type: "agent", identifier: agentId, relationship: "delegated_authority" },
package/dist/index.js CHANGED
@@ -204,8 +204,8 @@ const agentCmd = program
204
204
  .option("--invite <token>", "Redeem a Mnemom Agent invitation (pinv_…): unlocks Mnemom Agent for you + credits your starter µ; launches too when a session name is given")
205
205
  .option("--setup", "Re-check the served agent and refresh its default cards before launch")
206
206
  .option("--no-setup", "Skip the default-card writes (the agent is still confirmed and claimed)")
207
- .option("--governance [features]", "Turn governance ON for the agent's cards: AIP integrity analysis + Safe House screening, observe-only (default: off for new agents; autonomy policy checks are rules only and on either way). Pick features with a comma list: integrity, safe-house. Billed at cost on top of the agent fee")
208
- .option("--no-governance", "Turn governance OFF for the agent's cards, autonomy policy checks included (by default new agents run autonomy observe, rules only; a card refresh otherwise keeps an existing agent's modes)")
207
+ .option("--governance [features]", "Choose the governance features for the agent's cards, observe-only: a comma list naming exactly the features to turn on: integrity, safe-house, all or none (default for new agents: integrity on, Safe House off; `--governance none` opts out of integrity; autonomy policy checks are rules only and on either way). Billed at cost on top of the agent fee")
208
+ .option("--no-governance", "Turn governance OFF for the agent's cards, autonomy policy checks included (by default new agents run autonomy observe, rules only, plus integrity observe; a card refresh otherwise keeps an existing agent's modes)")
209
209
  .option("--no-yolo", "Do NOT pass --dangerously-skip-permissions (yolo is on by default)")
210
210
  .option("--no-context-hint", "Do NOT seed Claude Code's context-hint feature flag before launch")
211
211
  // The Mnemom plugin lands dark: off by default and not in the published package, so its two
@@ -2383,14 +2383,15 @@ program
2383
2383
  });
2384
2384
  program
2385
2385
  .command("scan")
2386
- .description("Read-only local scan of GitHub PR metadata; the report is emailed to you every time (counts only, no repo names) and Mnemom keeps nothing at share level none")
2386
+ .description("Read-only local scan of GitHub PR metadata; the full report, with the counts for each repo, is always emailed to you, your org admins and anyone you add. --share chooses what Mnemom may keep")
2387
2387
  .option("--repo <owner/name...>", "Repo to scan (repeatable or comma-separated; omit on a terminal to pick from your repos)")
2388
2388
  .option("--start <YYYY-MM-DD>", "Period start (default: today)")
2389
2389
  .option("--end <YYYY-MM-DD>", "Period end (default: today)")
2390
- .option("--share <level>", "summary | full | none (default: full when asked; none when not interactive)")
2391
- .option("--cto-email <email>", "CTO who receives the report (required unless you are the CTO; asked when omitted)")
2390
+ .option("--share <level>", "What Mnemom may keep: full (totals and per-repo counts, default when asked) | analytics (team totals only, no repo names; summary is the old name) | none (nothing; the default when not interactive)")
2391
+ .option("--email <address...>", "Also email the report to these people (repeatable or comma-separated, up to 10, on your organization's email domain or mnemom.ai). You and every org admin always get it")
2392
+ .option("--cto-email <email>", "Same as --email (kept for older scripts)")
2392
2393
  .option("--monthly-spend <usd>", "Your own approximate monthly AI spend (self-reported; never sent or saved; needs MNEMOM_ORG_SCAN_MODE=true)")
2393
- .option("--anthropic-admin-key", "Ask (masked) for your Anthropic admin key and read measured usage with it, locally; or set ANTHROPIC_ADMIN_KEY. Never stored, logged or sent anywhere but api.anthropic.com (needs MNEMOM_ORG_SCAN_MODE=true)")
2394
+ .option("--anthropic-admin-key", "Ask (masked) for your Anthropic admin key and read measured usage with it, locally; or set ANTHROPIC_ADMIN_KEY. Never stored, logged or sent anywhere but api.anthropic.com. Without this flag, workload identity federation is tried first when ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID and ANTHROPIC_SERVICE_ACCOUNT_ID are set (needs MNEMOM_ORG_SCAN_MODE=true)")
2394
2395
  .option("--no-anthropic-usage", "Decline Anthropic usage and be asked for a monthly figure (needs MNEMOM_ORG_SCAN_MODE=true)")
2395
2396
  .option("--org <org_id>", "Your org id; the report is emailed to you for this org (default: your only organization, else the scan stops and asks). Also selects the Record totals preview (needs MNEMOM_ORG_SCAN_MODE=true)")
2396
2397
  .action(async (options) => {
@@ -2405,6 +2406,7 @@ program
2405
2406
  period: { start: options.start ?? options.end ?? today, end: options.end ?? today },
2406
2407
  share: options.share,
2407
2408
  ctoEmail: options.ctoEmail,
2409
+ email: options.email,
2408
2410
  monthlySpend: options.monthlySpend,
2409
2411
  noAnthropicUsage: options.anthropicUsage === false,
2410
2412
  anthropicAdminKey: options.anthropicAdminKey === true,
@@ -51,17 +51,8 @@ export declare function attributionSettings(conversationId: string, user: string
51
51
  };
52
52
  /** `owner/name` from a GitHub remote URL (https, ssh or scp form), or null. */
53
53
  export declare function parseRepoSlug(url: string): string | null;
54
- /** The commit index body's schema tag (shared with the fleet worker's reporter). */
55
- export declare const COMMIT_SCHEMA = "RecordCommit/v1";
56
- /** RecordCommit/v1: strings only, every field required; the route rejects an unknown field. */
57
- export interface RecordCommitBody {
58
- schema: typeof COMMIT_SCHEMA;
59
- lineage_key: string;
60
- repo: string;
61
- branch: string;
62
- sha: string;
63
- patch_id: string;
64
- }
54
+ import { COMMIT_SCHEMA, type RecordCommitBody } from "./runner/record-commit.js";
55
+ export { COMMIT_SCHEMA, type RecordCommitBody };
65
56
  /** Runs git in the session's repository; null on any failure. */
66
57
  export type Git = (args: string[], input?: string) => string | null;
67
58
  export declare function realGit(cwd: string): Git;
@@ -71,8 +62,11 @@ export declare function realGit(cwd: string): Git;
71
62
  * teammate's fetched commit is never reported as this session's.
72
63
  */
73
64
  export declare function sessionCommits(git: Git, sinceSec: number): string[];
74
- /** The body for one commit, or the fields that could not be read. */
75
- export declare function commitBody(git: Git, sha: string, lineageKey: string): {
65
+ /**
66
+ * The body for one commit, or the fields that could not be read. `repo` overrides the slug read
67
+ * from `origin`: the runner knows its job's repo, and on a local lane `origin` is not github.com.
68
+ */
69
+ export declare function commitBody(git: Git, sha: string, lineageKey: string, repoOverride?: string): {
76
70
  ok: true;
77
71
  body: RecordCommitBody;
78
72
  } | {
@@ -65,8 +65,8 @@ export function parseRepoSlug(url) {
65
65
  const slug = `${m[1]}/${m[2]}`;
66
66
  return REPO_SLUG.test(slug) ? slug : null;
67
67
  }
68
- /** The commit index body's schema tag (shared with the fleet worker's reporter). */
69
- export const COMMIT_SCHEMA = "RecordCommit/v1";
68
+ import { COMMIT_SCHEMA } from "./runner/record-commit.js";
69
+ export { COMMIT_SCHEMA };
70
70
  export function realGit(cwd) {
71
71
  return (args, input) => {
72
72
  const r = spawnSync("git", args, { cwd, encoding: "utf8", input, timeout: 10_000 });
@@ -91,8 +91,11 @@ export function sessionCommits(git, sinceSec) {
91
91
  .filter(([sha, ce]) => sha !== undefined && HEX40.test(sha) && ce === email)
92
92
  .map(([sha]) => sha);
93
93
  }
94
- /** The body for one commit, or the fields that could not be read. */
95
- export function commitBody(git, sha, lineageKey) {
94
+ /**
95
+ * The body for one commit, or the fields that could not be read. `repo` overrides the slug read
96
+ * from `origin`: the runner knows its job's repo, and on a local lane `origin` is not github.com.
97
+ */
98
+ export function commitBody(git, sha, lineageKey, repoOverride) {
96
99
  const diff = git(["diff-tree", "-p", "--root", sha]);
97
100
  const patchId = diff
98
101
  ? (/^\s*([0-9a-f]{40})\b/.exec(git(["patch-id", "--stable"], diff) ?? "")?.[1] ?? "")
@@ -103,7 +106,11 @@ export function commitBody(git, sha, lineageKey) {
103
106
  .map((s) => s.trim())
104
107
  .filter(Boolean);
105
108
  const branch = holders.includes(current) ? current : (holders[0] ?? "");
106
- const repo = parseRepoSlug(git(["remote", "get-url", "origin"]) ?? "") ?? "";
109
+ const repo = repoOverride !== undefined
110
+ ? REPO_SLUG.test(repoOverride)
111
+ ? repoOverride
112
+ : ""
113
+ : (parseRepoSlug(git(["remote", "get-url", "origin"]) ?? "") ?? "");
107
114
  const missing = [];
108
115
  if (!HEX40.test(sha))
109
116
  missing.push("sha");
@@ -1,6 +1,9 @@
1
1
  /**
2
- * The conduct receipt (experimental `record`, Doors W1.2 / W1.3): the engineer's private view of
3
- * one session, printed when `mnemom agent` exits and when a durable session parks.
2
+ * The conduct receipt (Doors W1.2 / W1.3): the engineer's private view of one session, printed
3
+ * when `mnemom agent` exits and when a durable session parks. On by default since 0.17.38
4
+ * (MNE-9013; MNEMOM_AGENT_RECEIPT=0 opts out). It prints ONLY when the server returns tally data:
5
+ * an org outside the record cohort (404), a caller without the role (403), no captured lineage,
6
+ * or a read that never answers inside the deadline all print nothing.
4
7
  *
5
8
  * Conduct receipt
6
9
  * model cost $0.42
@@ -13,7 +16,7 @@
13
16
  *
14
17
  * NEVER BLOCKS EXIT. The whole read, auth refresh included, races a hard deadline
15
18
  * (RECEIPT_DEADLINE_MS): with the API or the record builder down, exit takes at most that much
16
- * longer, and the receipt falls back to the link. The caller keeps its exit code; nothing here
19
+ * longer, and the receipt prints nothing. The caller keeps its exit code; nothing here
17
20
  * throws.
18
21
  *
19
22
  * The lineage key is the gateway's (x-mnemom-lineage-key, set on every response), captured by the
@@ -35,6 +38,8 @@ export type TotalsRead = {
35
38
  muChargedMmu: bigint | null;
36
39
  } | {
37
40
  kind: "unavailable";
41
+ } | {
42
+ kind: "absent";
38
43
  };
39
44
  export interface ReceiptDeps {
40
45
  /** One totals read for the lineage; may throw or hang (both are "unavailable"). */