@mnemom/mnemom 0.17.38-next.0 → 0.17.38-next.2
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 +10 -2
- package/dist/commands/agent-run.js +12 -6
- package/dist/commands/agent-setup.d.ts +11 -9
- package/dist/commands/agent-setup.js +24 -19
- package/dist/commands/card.js +1 -1
- package/dist/commands/onboard.js +5 -4
- package/dist/commands/scan.d.ts +21 -7
- package/dist/commands/scan.js +93 -56
- 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 +7 -5
- package/dist/lib/runner/executor-client.d.ts +6 -0
- package/dist/lib/runner/executor-client.js +6 -0
- package/dist/lib/runner/job.d.ts +12 -3
- package/dist/lib/runner/job.js +30 -2
- package/dist/lib/scan/caps.d.ts +1 -35
- package/dist/lib/scan/caps.js +2 -67
- package/dist/lib/scan/cost-per-pr.d.ts +1 -93
- package/dist/lib/scan/cost-per-pr.js +2 -101
- package/dist/lib/scan/credential.d.ts +2 -26
- package/dist/lib/scan/credential.js +5 -58
- package/dist/lib/scan/failure.d.ts +1 -10
- package/dist/lib/scan/failure.js +2 -43
- package/dist/lib/scan/pr-source.d.ts +1 -57
- package/dist/lib/scan/pr-source.js +2 -132
- package/dist/lib/scan/report-email.d.ts +9 -89
- package/dist/lib/scan/report-email.js +76 -92
- package/dist/lib/scan/scan.d.ts +1 -67
- package/dist/lib/scan/scan.js +2 -194
- package/dist/lib/scan/share.d.ts +10 -6
- package/dist/lib/scan/share.js +28 -11
- package/dist/lib/scan/spend/ai-tool-detect.d.ts +1 -6
- package/dist/lib/scan/spend/ai-tool-detect.js +2 -18
- package/dist/lib/scan/spend/anthropic-usage.d.ts +1 -76
- package/dist/lib/scan/spend/anthropic-usage.js +2 -269
- package/dist/lib/scan/spend/prices.d.ts +1 -33
- package/dist/lib/scan/spend/prices.js +2 -57
- package/dist/lib/scan/spend/spend-estimate.d.ts +1 -30
- package/dist/lib/scan/spend/spend-estimate.js +2 -70
- package/dist/lib/scan/spend/spend-tiers.d.ts +1 -16
- package/dist/lib/scan/spend/spend-tiers.js +6 -26
- package/dist/lib/scan/spend/token-rate.d.ts +1 -30
- package/dist/lib/scan/spend/token-rate.js +2 -54
- package/dist/lib/starter-integrity.d.ts +20 -0
- package/dist/lib/starter-integrity.js +28 -0
- package/dist/vendor/shared/org-scan-core/caps.d.ts +35 -0
- package/dist/vendor/shared/org-scan-core/caps.js +68 -0
- package/dist/vendor/shared/org-scan-core/cost-per-pr.d.ts +93 -0
- package/dist/vendor/shared/org-scan-core/cost-per-pr.js +101 -0
- package/dist/vendor/shared/org-scan-core/credential.d.ts +43 -0
- package/dist/vendor/shared/org-scan-core/credential.js +74 -0
- package/dist/vendor/shared/org-scan-core/failure.d.ts +10 -0
- package/dist/vendor/shared/org-scan-core/failure.js +43 -0
- package/dist/vendor/shared/org-scan-core/index.d.ts +18 -0
- package/dist/vendor/shared/org-scan-core/index.js +19 -0
- package/dist/vendor/shared/org-scan-core/pr-source.d.ts +61 -0
- package/dist/vendor/shared/org-scan-core/pr-source.js +138 -0
- package/dist/vendor/shared/org-scan-core/report-build.d.ts +120 -0
- package/dist/vendor/shared/org-scan-core/report-build.js +119 -0
- package/dist/vendor/shared/org-scan-core/scan.d.ts +75 -0
- package/dist/vendor/shared/org-scan-core/scan.js +202 -0
- package/dist/vendor/shared/org-scan-core/spend/ai-tool-detect.d.ts +6 -0
- package/dist/vendor/shared/org-scan-core/spend/ai-tool-detect.js +18 -0
- package/dist/vendor/shared/org-scan-core/spend/anthropic-usage.d.ts +76 -0
- package/dist/vendor/shared/org-scan-core/spend/anthropic-usage.js +270 -0
- package/dist/{lib/scan → vendor/shared/org-scan-core}/spend/cost-corpus.v1.js +1 -0
- package/dist/vendor/shared/org-scan-core/spend/prices.d.ts +33 -0
- package/dist/vendor/shared/org-scan-core/spend/prices.js +58 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-estimate.d.ts +30 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-estimate.js +71 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-select.d.ts +16 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-select.js +26 -0
- package/dist/vendor/shared/org-scan-core/spend/token-rate.d.ts +30 -0
- package/dist/vendor/shared/org-scan-core/spend/token-rate.js +55 -0
- package/package.json +2 -2
- /package/dist/{lib/scan → vendor/shared/org-scan-core}/spend/cost-corpus.v1.d.ts +0 -0
package/dist/commands/scan.js
CHANGED
|
@@ -15,7 +15,7 @@ import { getAuthInfo } from "../lib/auth.js";
|
|
|
15
15
|
import { listMyOrgs } from "../lib/api.js";
|
|
16
16
|
import { getActiveOrg } from "../lib/cli-config.js";
|
|
17
17
|
import { fetchRecordTotals, isOrgId, } from "../lib/scan/record.js";
|
|
18
|
-
import {
|
|
18
|
+
import { costSpendFor, sendsSpendRange } from "../vendor/shared/org-scan-core/report-build.js";
|
|
19
19
|
import { RECORD_NO_ORG_NOTE, renderRecordSection } from "../lib/scan/report.js";
|
|
20
20
|
import { buildEmailBody, buildEmailReport, DRY_RUN_MSG, UNAVAILABLE_MSG, failureMessage, ReportEmailError, sendReportEmail, } from "../lib/scan/report-email.js";
|
|
21
21
|
import { pickRepos, REPO_PICKER_NO_TERMINAL_HINT, } from "../lib/scan/repo-picker.js";
|
|
@@ -28,29 +28,61 @@ export function parseRepoFlags(raw) {
|
|
|
28
28
|
.filter(Boolean);
|
|
29
29
|
return out.length > 0 && out.every(isValidRepoSlug) ? out : null;
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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;
|
|
54
86
|
}
|
|
55
87
|
/** The CTO's figure in USD, or null for none. Throws fixed text only; the input is never echoed. */
|
|
56
88
|
export async function resolveSelfReportedSpend(o) {
|
|
@@ -78,15 +110,8 @@ export async function resolveSelfReportedSpend(o) {
|
|
|
78
110
|
* tiers off) the modelled range. Sources are never mixed.
|
|
79
111
|
*/
|
|
80
112
|
export function resolveCostSpend(o, result, spend, measured) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (spend.tier === 1) {
|
|
84
|
-
const usd = measured?.usd ?? o.anthropicUsageUsd;
|
|
85
|
-
return typeof usd === "number" ? { source: "anthropic_usage", usd } : null;
|
|
86
|
-
}
|
|
87
|
-
if (spend.tier === 2)
|
|
88
|
-
return null;
|
|
89
|
-
return spend.tier === 3 ? modelledSpend(result.spend) : null;
|
|
113
|
+
// Shared with the Mnemom-side run (MNE-9752): same spend source, same cost figures.
|
|
114
|
+
return costSpendFor(result, spend, measured?.usd ?? o.anthropicUsageUsd);
|
|
90
115
|
}
|
|
91
116
|
export const NO_ADMIN_KEY_TERMINAL_MSG = "Set ANTHROPIC_ADMIN_KEY (there is no terminal to ask on).";
|
|
92
117
|
export const TIER1_FALLBACK_NOTE = "Tier 1 skipped: falling back to the next tier.";
|
|
@@ -238,16 +263,20 @@ export async function resolveRecordOutcome(o) {
|
|
|
238
263
|
}
|
|
239
264
|
export async function scanCommand(o) {
|
|
240
265
|
if (o.share !== undefined && parseShareLevel(o.share) === null) {
|
|
241
|
-
throw new Error("--share must be one of:
|
|
266
|
+
throw new Error("--share must be one of: full, analytics, none (summary also works, same as analytics)");
|
|
242
267
|
}
|
|
243
268
|
const noRepoFlag = o.repos.length === 0;
|
|
269
|
+
const interactive = Boolean(o.isTty ?? process.stdin.isTTY);
|
|
244
270
|
// No --repo: on a terminal pick from the repos this GitHub login can read; otherwise the error stays, with a hint.
|
|
245
|
-
if (noRepoFlag && !
|
|
271
|
+
if (noRepoFlag && !interactive)
|
|
246
272
|
throw new Error(`--repo owner/name is required (repeatable). ${REPO_PICKER_NO_TERMINAL_HINT}`);
|
|
273
|
+
// Consent comes first, before any data is processed (MNE-9634). The flag wins; on a terminal with no flag we
|
|
274
|
+
// ask (Full preselected); with no terminal and no flag the answer is Nothing (fail closed).
|
|
275
|
+
const level = parseShareLevel(o.share) ?? (interactive ? await promptShareLevel() : "none");
|
|
276
|
+
// The people added to the report email; the runner and every org admin always get it.
|
|
277
|
+
const extraEmails = await resolveExtraEmails(o);
|
|
247
278
|
// The report is emailed on every scan, so resolve the org before any scan work; failure is loud.
|
|
248
279
|
const emailOrg = await resolveScanOrg(o);
|
|
249
|
-
if (o.ctoEmail !== undefined && !isEmail(o.ctoEmail))
|
|
250
|
-
throw new Error("--cto-email must be a valid email address");
|
|
251
280
|
const getCredential = async () => {
|
|
252
281
|
const c = o.credential ?? (await resolveCredential().catch(() => null));
|
|
253
282
|
if (!c)
|
|
@@ -292,19 +321,23 @@ export async function scanCommand(o) {
|
|
|
292
321
|
else if (record !== null)
|
|
293
322
|
for (const line of renderRecordSection(record))
|
|
294
323
|
console.log(line);
|
|
295
|
-
const level = parseShareLevel(o.share) ?? (await promptShareLevel());
|
|
296
324
|
// Every successful scan emails the report to the person who ran it, at every share level, no opt-out.
|
|
297
325
|
// Mnemom keeps nothing from a share=none send (the server sends the email and does nothing else).
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return;
|
|
326
|
+
const emailResult = await emailReport(o, emailOrg, result, level, spend, period, resolveCostSpend(o, result, spend, measured), extraEmails);
|
|
327
|
+
const emailFailure = emailResult === "not_sent" || emailResult === null ? null : emailResult;
|
|
328
|
+
// What was shared with Mnemom, and how to have it deleted (MNE-9634).
|
|
329
|
+
if (emailResult !== null) {
|
|
330
|
+
console.log("Kept on file by Mnemom: nothing, because the report was not sent.");
|
|
304
331
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
332
|
+
else if (level === "none") {
|
|
333
|
+
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.");
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
const payload = buildSharePayload(level, result, { runner_email: runnerEmailFromLogin(o), extra_emails: extraEmails }, spend ? (spend.shareable ? spend.line : null) : undefined);
|
|
337
|
+
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:`);
|
|
338
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
339
|
+
}
|
|
340
|
+
console.log(`To have anything Mnemom holds about this scan deleted, email ${DELETE_CONTACT}.`);
|
|
308
341
|
if (emailFailure)
|
|
309
342
|
throw new ReportEmailError(emailFailure);
|
|
310
343
|
}
|
|
@@ -312,29 +345,33 @@ export async function scanCommand(o) {
|
|
|
312
345
|
* Emails the report. Loud on every failure: prints the reason name, never the server's words, and
|
|
313
346
|
* returns it so the caller exits non-zero after the local report is shown.
|
|
314
347
|
*/
|
|
315
|
-
async function emailReport(o, org, result, level, spend, period, costSpend) {
|
|
316
|
-
const fail = (f) => {
|
|
317
|
-
console.log(failureMessage(f));
|
|
348
|
+
async function emailReport(o, org, result, level, spend, period, costSpend, extraEmails) {
|
|
349
|
+
const fail = (f, retryAfterMs) => {
|
|
350
|
+
console.log(failureMessage(f, retryAfterMs));
|
|
318
351
|
return f;
|
|
319
352
|
};
|
|
320
353
|
const report = buildEmailReport(result, {
|
|
321
354
|
orgId: org,
|
|
322
355
|
now: o.now ?? Date.now,
|
|
323
|
-
sendSpendRange: spend
|
|
356
|
+
sendSpendRange: sendsSpendRange(spend),
|
|
324
357
|
period,
|
|
325
358
|
costSpend,
|
|
326
359
|
});
|
|
327
|
-
const body = buildEmailBody(report, level, result.coverage.complete,
|
|
360
|
+
const body = buildEmailBody(report, level, result.coverage.complete, extraEmails);
|
|
328
361
|
const outcome = await sendReportEmail(org, body, o.emailDeps);
|
|
329
362
|
if (outcome.status === "failed")
|
|
330
|
-
return fail(outcome.reason);
|
|
363
|
+
return fail(outcome.reason, outcome.retryAfterMs);
|
|
331
364
|
if (outcome.status === "unavailable") {
|
|
332
365
|
// Org Scan is off by default: the gate's 404 is not a failure and must not break the scan.
|
|
333
366
|
console.log(UNAVAILABLE_MSG);
|
|
334
|
-
return
|
|
367
|
+
return "not_sent";
|
|
335
368
|
}
|
|
336
369
|
console.log(outcome.status === "dry_run"
|
|
337
370
|
? DRY_RUN_MSG
|
|
338
|
-
:
|
|
339
|
-
|
|
371
|
+
: outcome.limited
|
|
372
|
+
? "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."
|
|
373
|
+
: `Full report emailed to you, your org admins and anyone you added (${outcome.recipients} recipient${outcome.recipients === 1 ? "" : "s"}).`);
|
|
374
|
+
if (outcome.status === "sent" && outcome.adminsMissed)
|
|
375
|
+
console.log(`${outcome.adminsMissed} org admin${outcome.adminsMissed === 1 ? "" : "s"} could not be emailed because no email address is on file.`);
|
|
376
|
+
return outcome.status === "dry_run" ? "not_sent" : null;
|
|
340
377
|
}
|
|
@@ -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,12 +2383,13 @@ 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
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)")
|
|
@@ -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,
|
|
@@ -34,6 +34,11 @@ export declare function errorDetails(body: unknown): Record<string, unknown>;
|
|
|
34
34
|
export declare const JOB_READ_PATH: (sessionId: string) => string;
|
|
35
35
|
/** The served bundle fetch, per claim (W-L12). */
|
|
36
36
|
export declare const BUNDLE_FETCH_PATH: (sessionId: string) => string;
|
|
37
|
+
/**
|
|
38
|
+
* The job's PR token (ruling 34.34): a Record App installation token narrowed to the seal's repo,
|
|
39
|
+
* contents and pull_requests write, fetched with the lease key. Never cached or logged.
|
|
40
|
+
*/
|
|
41
|
+
export declare const GITHUB_TOKEN_PATH: (sessionId: string) => string;
|
|
37
42
|
export declare const JWKS_PATH = "/v1/.well-known/jwks.json";
|
|
38
43
|
/** Record's commit index (ruling 34.05 (a)): the lease key may write the session's own commits. */
|
|
39
44
|
export declare const RECORD_COMMITS_PATH: (orgId: string) => string;
|
|
@@ -65,6 +70,7 @@ export interface RunnerApi {
|
|
|
65
70
|
fail(sessionId: string, body: Record<string, unknown>): Promise<HttpResult>;
|
|
66
71
|
job(sessionId: string): Promise<HttpResult>;
|
|
67
72
|
bundle(sessionId: string, leaseKey: string): Promise<HttpResult>;
|
|
73
|
+
githubToken(sessionId: string, leaseKey: string): Promise<HttpResult>;
|
|
68
74
|
jwks(): Promise<HttpResult>;
|
|
69
75
|
/** One RecordCommit/v1 body, with the lease key; the api pins it to the session's lineage. */
|
|
70
76
|
recordCommit(orgId: string, body: RecordCommitBody, leaseKey: string): Promise<HttpResult>;
|
|
@@ -33,6 +33,11 @@ export function errorDetails(body) {
|
|
|
33
33
|
export const JOB_READ_PATH = (sessionId) => `/v1/executor/sessions/${sessionId}/job`;
|
|
34
34
|
/** The served bundle fetch, per claim (W-L12). */
|
|
35
35
|
export const BUNDLE_FETCH_PATH = (sessionId) => `/v1/executor/sessions/${sessionId}/bundle`;
|
|
36
|
+
/**
|
|
37
|
+
* The job's PR token (ruling 34.34): a Record App installation token narrowed to the seal's repo,
|
|
38
|
+
* contents and pull_requests write, fetched with the lease key. Never cached or logged.
|
|
39
|
+
*/
|
|
40
|
+
export const GITHUB_TOKEN_PATH = (sessionId) => `/v1/executor/sessions/${sessionId}/github-token`;
|
|
36
41
|
export const JWKS_PATH = "/v1/.well-known/jwks.json";
|
|
37
42
|
/** Record's commit index (ruling 34.05 (a)): the lease key may write the session's own commits. */
|
|
38
43
|
export const RECORD_COMMITS_PATH = (orgId) => `/v1/orgs/${encodeURIComponent(orgId)}/record/commits`;
|
|
@@ -95,6 +100,7 @@ export function runnerApi(opts) {
|
|
|
95
100
|
fail: (id, body) => call(routePath("exec_fail", scoped(id)), "POST", ex, body),
|
|
96
101
|
job: (id) => call(JOB_READ_PATH(scoped(id)), "GET", ex),
|
|
97
102
|
bundle: (id, leaseKey) => call(BUNDLE_FETCH_PATH(scoped(id)), "GET", leaseKey),
|
|
103
|
+
githubToken: (id, leaseKey) => call(GITHUB_TOKEN_PATH(scoped(id)), "POST", leaseKey),
|
|
98
104
|
jwks: () => call(JWKS_PATH, "GET", null),
|
|
99
105
|
recordCommit: (orgId, body, leaseKey) => {
|
|
100
106
|
if (!ORG_ID_RE.test(orgId))
|
package/dist/lib/runner/job.d.ts
CHANGED
|
@@ -42,8 +42,11 @@ export interface JobDeps {
|
|
|
42
42
|
prepareWorkspace(repo: string, sessionId: string): Promise<Workspace>;
|
|
43
43
|
/** Run one phase's process; resolves its exit code. Rejects only when it could not start. */
|
|
44
44
|
runPhase(inv: PhaseInvocation, ws: Workspace, modelUrl: string, signal: AbortSignal): Promise<number>;
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Push the job branch and open its PR; resolves the PR URL. `token` is the job's PR token when
|
|
47
|
+
* `prTokenFromLease` is set; null means the runner's own GitHub token.
|
|
48
|
+
*/
|
|
49
|
+
openPr(repo: string, ws: Workspace, title: string, body: string, token: string | null): Promise<string>;
|
|
47
50
|
/** Commits on the job branch that the base does not have; 0 means there is nothing to open. */
|
|
48
51
|
commitsAhead(ws: Workspace): Promise<number>;
|
|
49
52
|
/** The working copy's HEAD sha before a phase runs; null only for a clone with no commit. Throws otherwise. */
|
|
@@ -53,7 +56,13 @@ export interface JobDeps {
|
|
|
53
56
|
* first, under `lineageKey` and the job's repo. A commit that cannot be read is left out.
|
|
54
57
|
*/
|
|
55
58
|
commitBodies(ws: Workspace, since: string | null, lineageKey: string, repo: string): Promise<RecordCommitBody[]>;
|
|
56
|
-
enableAutoMerge(prUrl: string): Promise<void>;
|
|
59
|
+
enableAutoMerge(prUrl: string, token: string | null): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* True when the credential is the --oidc exchange: the PR is pushed and opened with the job's PR
|
|
62
|
+
* token from the api (ruling 34.34), never the runner's own GitHub token, which in a sealed repo's
|
|
63
|
+
* Action is a GITHUB_TOKEN the org forbids from opening PRs. A refused token fails the job.
|
|
64
|
+
*/
|
|
65
|
+
prTokenFromLease?: boolean;
|
|
57
66
|
cleanup(ws: Workspace): Promise<void>;
|
|
58
67
|
}
|
|
59
68
|
export interface JobOptions {
|
package/dist/lib/runner/job.js
CHANGED
|
@@ -379,6 +379,7 @@ export async function runJob(sessionId, opts, deps) {
|
|
|
379
379
|
});
|
|
380
380
|
}
|
|
381
381
|
if (opensPr) {
|
|
382
|
+
let prToken = null;
|
|
382
383
|
try {
|
|
383
384
|
// Checked before the push, so an empty diff never reaches GitHub's 422 (amendment 59).
|
|
384
385
|
if ((await deps.commitsAhead(ws)) === 0) {
|
|
@@ -386,7 +387,13 @@ export async function runJob(sessionId, opts, deps) {
|
|
|
386
387
|
await reportFail(deps, sessionId, claim.epoch, "runner_failed", "pr_no_changes");
|
|
387
388
|
return { kind: "outcome", outcome: failedOutcome(facts, "pr_no_changes") };
|
|
388
389
|
}
|
|
389
|
-
|
|
390
|
+
if (deps.prTokenFromLease) {
|
|
391
|
+
const t = await fetchPrToken(deps, sessionId, claim.leaseKey, job.repo);
|
|
392
|
+
if (!t.ok)
|
|
393
|
+
throw new Error(t.detail);
|
|
394
|
+
prToken = t.token;
|
|
395
|
+
}
|
|
396
|
+
facts.pr_url = await deps.openPr(job.repo, ws, `mnemom: ${bundle.workflow.id} job ${sessionId.slice(0, 8)}`, prBody(job), prToken);
|
|
390
397
|
}
|
|
391
398
|
catch (e) {
|
|
392
399
|
deps.log(`opening the PR failed: ${e.message}`);
|
|
@@ -395,7 +402,7 @@ export async function runJob(sessionId, opts, deps) {
|
|
|
395
402
|
}
|
|
396
403
|
if (facts.merge_strategy === "merge_on_green") {
|
|
397
404
|
try {
|
|
398
|
-
await deps.enableAutoMerge(facts.pr_url);
|
|
405
|
+
await deps.enableAutoMerge(facts.pr_url, prToken);
|
|
399
406
|
}
|
|
400
407
|
catch (e) {
|
|
401
408
|
deps.log(`enabling auto-merge failed (the PR stays open): ${e.message}`);
|
|
@@ -469,6 +476,27 @@ export async function reportPhaseCommits(deps, report, p) {
|
|
|
469
476
|
deps.log(`commit report failed: ${e.message}`);
|
|
470
477
|
}
|
|
471
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* The job's PR token (ruling 34.34), fetched once per claim with the lease key. It must name the
|
|
481
|
+
* job's repo; the detail never carries the token.
|
|
482
|
+
*/
|
|
483
|
+
async function fetchPrToken(deps, sessionId, leaseKey, repo) {
|
|
484
|
+
const res = await deps.api.githubToken(sessionId, leaseKey);
|
|
485
|
+
if (res.status !== 200) {
|
|
486
|
+
return {
|
|
487
|
+
ok: false,
|
|
488
|
+
detail: `the PR token fetch answered ${res.code ?? res.networkError ?? res.status}`,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const b = (res.body ?? {});
|
|
492
|
+
if (typeof b.token !== "string" || b.token === "") {
|
|
493
|
+
return { ok: false, detail: "the PR token fetch answered 200 with no token" };
|
|
494
|
+
}
|
|
495
|
+
if (typeof b.repository !== "string" || b.repository.toLowerCase() !== repo.toLowerCase()) {
|
|
496
|
+
return { ok: false, detail: `the PR token names another repo than the job's ${repo}` };
|
|
497
|
+
}
|
|
498
|
+
return { ok: true, token: b.token };
|
|
499
|
+
}
|
|
472
500
|
async function fetchBundle(deps, sessionId, claim) {
|
|
473
501
|
const [jwksRes, res] = await Promise.all([
|
|
474
502
|
deps.api.jwks(),
|
package/dist/lib/scan/caps.d.ts
CHANGED
|
@@ -1,35 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Coverage caps and sampling. Port of mnemom-api src/org-scan/scan/capped-scan.ts (PR #4208).
|
|
3
|
-
*
|
|
4
|
-
* Coverage decision (Shraddha, 2026-10-28): each scan covers the last 3 months OR the last 2,000 merged
|
|
5
|
-
* PRs, whichever is MORE; above 2,000 in the window, an even deterministic sample of 2,000.
|
|
6
|
-
*/
|
|
7
|
-
export declare const COVERAGE_MONTHS = 3;
|
|
8
|
-
export interface ScanCaps {
|
|
9
|
-
maxPrs: number;
|
|
10
|
-
/** A PR with more additions+deletions than this is skipped, never read in full. */
|
|
11
|
-
maxPrSizeLines: number;
|
|
12
|
-
/** Upstream read calls. */
|
|
13
|
-
maxReads: number;
|
|
14
|
-
/** Wall-clock budget. */
|
|
15
|
-
maxMillis: number;
|
|
16
|
-
}
|
|
17
|
-
export declare const DEFAULT_SCAN_CAPS: ScanCaps;
|
|
18
|
-
export type StopReason = "pr-cap" | "read-cap" | "time-cap";
|
|
19
|
-
/** The day `months` calendar months before `end` (YYYY-MM-DD, UTC), clamped to the month's last day. */
|
|
20
|
-
export declare function monthsBefore(end: string, months: number): string;
|
|
21
|
-
/** `k` items spread evenly across `items` (order kept): index floor(i*n/k). Deterministic; n <= k returns all. */
|
|
22
|
-
export declare function evenSample<T>(items: readonly T[], k: number): T[];
|
|
23
|
-
export declare function sanitiseCaps(c: Partial<ScanCaps>): ScanCaps;
|
|
24
|
-
/** Running read budget. Every call is told how many reads it may spend (`room`). */
|
|
25
|
-
export declare class Budget {
|
|
26
|
-
private caps;
|
|
27
|
-
private now;
|
|
28
|
-
private startedAt;
|
|
29
|
-
reads: number;
|
|
30
|
-
constructor(caps: ScanCaps, now: () => number, startedAt: number);
|
|
31
|
-
block(n?: number): StopReason | null;
|
|
32
|
-
room(): number;
|
|
33
|
-
overrun(): StopReason | null;
|
|
34
|
-
charge(n: number): void;
|
|
35
|
-
}
|
|
1
|
+
export * from "../../vendor/shared/org-scan-core/caps.js";
|