@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.
- package/dist/commands/agent-doctor.d.ts +2 -2
- package/dist/commands/agent-doctor.js +4 -4
- package/dist/commands/agent-run.d.ts +11 -0
- package/dist/commands/agent-run.js +37 -0
- package/dist/commands/agent-setup.d.ts +11 -9
- package/dist/commands/agent-setup.js +24 -19
- package/dist/commands/agent-share-render.d.ts +5 -5
- package/dist/commands/agent-share-render.js +9 -7
- package/dist/commands/agent-share-test-issuer.d.ts +1 -1
- package/dist/commands/agent-share-test-issuer.js +1 -1
- package/dist/commands/agent.d.ts +8 -1
- package/dist/commands/agent.js +14 -3
- package/dist/commands/card.js +1 -1
- package/dist/commands/onboard.js +5 -4
- package/dist/commands/scan.d.ts +35 -8
- package/dist/commands/scan.js +134 -45
- package/dist/commands/verify-card.d.ts +25 -1
- package/dist/commands/verify-card.js +29 -2
- package/dist/commands/wrap.js +4 -3
- package/dist/index.js +8 -6
- package/dist/lib/commit-report.d.ts +7 -13
- package/dist/lib/commit-report.js +12 -5
- package/dist/lib/conduct-receipt.d.ts +8 -3
- package/dist/lib/conduct-receipt.js +29 -7
- package/dist/lib/experimental.d.ts +1 -1
- package/dist/lib/experimental.js +1 -12
- package/dist/lib/runner/executor-client.d.ts +5 -0
- package/dist/lib/runner/executor-client.js +8 -0
- package/dist/lib/runner/job.d.ts +37 -0
- package/dist/lib/runner/job.js +74 -1
- package/dist/lib/runner/phases.d.ts +7 -2
- package/dist/lib/runner/phases.js +7 -2
- package/dist/lib/runner/proxy.d.ts +26 -2
- package/dist/lib/runner/proxy.js +33 -1
- package/dist/lib/runner/record-commit.d.ts +16 -0
- package/dist/lib/runner/record-commit.js +7 -0
- package/dist/lib/scan/report-email.d.ts +19 -2
- package/dist/lib/scan/report-email.js +78 -22
- package/dist/lib/scan/scan.d.ts +2 -0
- package/dist/lib/scan/scan.js +2 -1
- package/dist/lib/scan/share.d.ts +8 -6
- package/dist/lib/scan/share.js +28 -11
- package/dist/lib/scan/spend/anthropic-usage.d.ts +20 -1
- package/dist/lib/scan/spend/anthropic-usage.js +32 -7
- package/dist/lib/scan/spend/anthropic-wif.d.ts +42 -0
- package/dist/lib/scan/spend/anthropic-wif.js +153 -0
- package/dist/lib/starter-integrity.d.ts +20 -0
- package/dist/lib/starter-integrity.js +28 -0
- package/package.json +2 -5
- package/node_modules/@mnemom/aip-verifier/LICENSE +0 -190
- package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.cts +0 -478
- package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.ts +0 -478
- package/node_modules/@mnemom/aip-verifier/dist/index.cjs +0 -971
- package/node_modules/@mnemom/aip-verifier/dist/index.cjs.map +0 -1
- package/node_modules/@mnemom/aip-verifier/dist/index.d.cts +0 -330
- package/node_modules/@mnemom/aip-verifier/dist/index.d.ts +0 -330
- package/node_modules/@mnemom/aip-verifier/dist/index.js +0 -933
- package/node_modules/@mnemom/aip-verifier/dist/index.js.map +0 -1
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs +0 -879
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs.map +0 -1
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.cts +0 -96
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.ts +0 -96
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.js +0 -875
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.js.map +0 -1
- package/node_modules/@mnemom/aip-verifier/package.json +0 -86
- package/node_modules/@noble/ed25519/LICENSE +0 -21
- package/node_modules/@noble/ed25519/README.md +0 -371
- package/node_modules/@noble/ed25519/index.d.ts +0 -391
- package/node_modules/@noble/ed25519/index.js +0 -933
- package/node_modules/@noble/ed25519/index.ts +0 -1134
- package/node_modules/@noble/ed25519/package.json +0 -54
- package/node_modules/@noble/hashes/LICENSE +0 -21
- package/node_modules/@noble/hashes/README.md +0 -478
- package/node_modules/@noble/hashes/_blake.d.ts +0 -20
- package/node_modules/@noble/hashes/_blake.js +0 -59
- package/node_modules/@noble/hashes/_md.d.ts +0 -95
- package/node_modules/@noble/hashes/_md.js +0 -209
- package/node_modules/@noble/hashes/_u64.d.ts +0 -36
- package/node_modules/@noble/hashes/_u64.js +0 -77
- package/node_modules/@noble/hashes/argon2.d.ts +0 -148
- package/node_modules/@noble/hashes/argon2.js +0 -577
- package/node_modules/@noble/hashes/blake1.d.ts +0 -162
- package/node_modules/@noble/hashes/blake1.js +0 -630
- package/node_modules/@noble/hashes/blake2.d.ts +0 -203
- package/node_modules/@noble/hashes/blake2.js +0 -573
- package/node_modules/@noble/hashes/blake3.d.ts +0 -68
- package/node_modules/@noble/hashes/blake3.js +0 -310
- package/node_modules/@noble/hashes/eskdf.d.ts +0 -91
- package/node_modules/@noble/hashes/eskdf.js +0 -225
- package/node_modules/@noble/hashes/hkdf.d.ts +0 -70
- package/node_modules/@noble/hashes/hkdf.js +0 -152
- package/node_modules/@noble/hashes/hmac.d.ts +0 -50
- package/node_modules/@noble/hashes/hmac.js +0 -93
- package/node_modules/@noble/hashes/index.d.ts +0 -1
- package/node_modules/@noble/hashes/index.js +0 -34
- package/node_modules/@noble/hashes/legacy.d.ts +0 -107
- package/node_modules/@noble/hashes/legacy.js +0 -348
- package/node_modules/@noble/hashes/package.json +0 -91
- package/node_modules/@noble/hashes/pbkdf2.d.ts +0 -66
- package/node_modules/@noble/hashes/pbkdf2.js +0 -176
- package/node_modules/@noble/hashes/scrypt.d.ts +0 -98
- package/node_modules/@noble/hashes/scrypt.js +0 -304
- package/node_modules/@noble/hashes/sha2.d.ts +0 -176
- package/node_modules/@noble/hashes/sha2.js +0 -446
- package/node_modules/@noble/hashes/sha3-addons.d.ts +0 -533
- package/node_modules/@noble/hashes/sha3-addons.js +0 -973
- package/node_modules/@noble/hashes/sha3.d.ts +0 -208
- package/node_modules/@noble/hashes/sha3.js +0 -475
- package/node_modules/@noble/hashes/src/_blake.ts +0 -67
- package/node_modules/@noble/hashes/src/_md.ts +0 -227
- package/node_modules/@noble/hashes/src/_u64.ts +0 -111
- package/node_modules/@noble/hashes/src/argon2.ts +0 -693
- package/node_modules/@noble/hashes/src/blake1.ts +0 -677
- package/node_modules/@noble/hashes/src/blake2.ts +0 -648
- package/node_modules/@noble/hashes/src/blake3.ts +0 -344
- package/node_modules/@noble/hashes/src/eskdf.ts +0 -265
- package/node_modules/@noble/hashes/src/hkdf.ts +0 -161
- package/node_modules/@noble/hashes/src/hmac.ts +0 -130
- package/node_modules/@noble/hashes/src/index.ts +0 -33
- package/node_modules/@noble/hashes/src/legacy.ts +0 -361
- package/node_modules/@noble/hashes/src/pbkdf2.ts +0 -219
- package/node_modules/@noble/hashes/src/scrypt.ts +0 -375
- package/node_modules/@noble/hashes/src/sha2.ts +0 -486
- package/node_modules/@noble/hashes/src/sha3-addons.ts +0 -1157
- package/node_modules/@noble/hashes/src/sha3.ts +0 -525
- package/node_modules/@noble/hashes/src/utils.ts +0 -978
- package/node_modules/@noble/hashes/src/webcrypto.ts +0 -283
- package/node_modules/@noble/hashes/utils.d.ts +0 -560
- package/node_modules/@noble/hashes/utils.js +0 -687
- package/node_modules/@noble/hashes/webcrypto.d.ts +0 -121
- package/node_modules/@noble/hashes/webcrypto.js +0 -228
package/dist/commands/scan.js
CHANGED
|
@@ -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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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:
|
|
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 && !
|
|
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
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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,
|
|
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
|
|
374
|
+
return "not_sent";
|
|
290
375
|
}
|
|
291
376
|
console.log(outcome.status === "dry_run"
|
|
292
377
|
? DRY_RUN_MSG
|
|
293
|
-
:
|
|
294
|
-
|
|
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
|
|
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
|
|
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
|
|
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;
|
package/dist/commands/wrap.js
CHANGED
|
@@ -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:
|
|
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]", "
|
|
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
|
|
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>", "
|
|
2391
|
-
.option("--
|
|
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
|
-
|
|
55
|
-
export
|
|
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
|
-
/**
|
|
75
|
-
|
|
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
|
-
|
|
69
|
-
export
|
|
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
|
-
/**
|
|
95
|
-
|
|
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 =
|
|
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 (
|
|
3
|
-
*
|
|
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
|
|
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"). */
|