@mnemom/mnemom 0.17.38-next.0 → 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-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 +20 -6
- package/dist/commands/scan.js +86 -42
- 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/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/starter-integrity.d.ts +20 -0
- package/dist/lib/starter-integrity.js +28 -0
- package/package.json +2 -2
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (200 + the x-mnemom-verdict header that proves the governance pipeline is
|
|
11
11
|
* live), Remote-Control capability (node + openssl + the --remote-control flag),
|
|
12
12
|
* claude.ai login (informational — Remote Control needs it), the agent's governance
|
|
13
|
-
* modes (
|
|
13
|
+
* modes (integrity observe, Safe House off for new agents), and CLI auto-update status.
|
|
14
14
|
*/
|
|
15
15
|
type Mark = "ok" | "warn" | "miss" | "info";
|
|
16
16
|
export interface DoctorOptions {
|
|
@@ -51,7 +51,7 @@ export interface GovernanceProbe {
|
|
|
51
51
|
/**
|
|
52
52
|
* The governance line: the composed (every layer, strictest wins) integrity and
|
|
53
53
|
* Safe House modes of the agent a launch would use. Signed out, no such agent,
|
|
54
|
-
* or an unreadable card → info with the
|
|
54
|
+
* or an unreadable card → info with the new-agent default note, never a MISS
|
|
55
55
|
* (governance never blocks a launch). On → info naming the per-turn cost; a
|
|
56
56
|
* stricter layer outranks the agent's own card (any rung up the ladder off <
|
|
57
57
|
* observe < nudge < enforce, MNE-8612) → warn naming the layer.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (200 + the x-mnemom-verdict header that proves the governance pipeline is
|
|
11
11
|
* live), Remote-Control capability (node + openssl + the --remote-control flag),
|
|
12
12
|
* claude.ai login (informational — Remote Control needs it), the agent's governance
|
|
13
|
-
* modes (
|
|
13
|
+
* modes (integrity observe, Safe House off for new agents), and CLI auto-update status.
|
|
14
14
|
*/
|
|
15
15
|
import { spawnSync } from "node:child_process";
|
|
16
16
|
import { existsSync } from "node:fs";
|
|
@@ -119,7 +119,7 @@ export async function agentDoctorCommand(options = {}) {
|
|
|
119
119
|
add("info", "claude.ai login: could not determine");
|
|
120
120
|
}
|
|
121
121
|
add(gate.mark, gate.text, gate.hint);
|
|
122
|
-
// ── governance (
|
|
122
|
+
// ── governance (integrity observe, Safe House off for new agents) ──
|
|
123
123
|
const gov = await probeGovernance(options.agent);
|
|
124
124
|
add(gov.mark, gov.text, gov.hint);
|
|
125
125
|
// ── CLI auto-update ──
|
|
@@ -236,7 +236,7 @@ const OPT_IN_HINT = GOVERNANCE_OPT_IN_HINT.trim();
|
|
|
236
236
|
/**
|
|
237
237
|
* The governance line: the composed (every layer, strictest wins) integrity and
|
|
238
238
|
* Safe House modes of the agent a launch would use. Signed out, no such agent,
|
|
239
|
-
* or an unreadable card → info with the
|
|
239
|
+
* or an unreadable card → info with the new-agent default note, never a MISS
|
|
240
240
|
* (governance never blocks a launch). On → info naming the per-turn cost; a
|
|
241
241
|
* stricter layer outranks the agent's own card (any rung up the ladder off <
|
|
242
242
|
* observe < nudge < enforce, MNE-8612) → warn naming the layer.
|
|
@@ -244,7 +244,7 @@ const OPT_IN_HINT = GOVERNANCE_OPT_IN_HINT.trim();
|
|
|
244
244
|
export async function probeGovernance(agent) {
|
|
245
245
|
const offByDefault = {
|
|
246
246
|
mark: "info",
|
|
247
|
-
text: "governance:
|
|
247
|
+
text: "governance: new agents get AIP integrity analysis (observe); Safe House screening is off by default",
|
|
248
248
|
hint: `${OPT_IN_HINT} Agents set up before this default keep their cards.`,
|
|
249
249
|
};
|
|
250
250
|
if (!(await isLoggedIn()))
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
* governed identity `agent-<handle>`. This module makes that identity REAL in
|
|
7
7
|
* their Mnemom org: it births the agent through the gateway, claims it (so the
|
|
8
8
|
* customer owns it), and publishes a sane default coding posture. Governance that
|
|
9
|
-
* costs a model call is
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* costs a model call is split (operator 2026-11-04, MNE-9728, matching the API new-agent
|
|
10
|
+
* default): AIP integrity analysis is ON (observe) for a new agent so it is analyzed and can earn
|
|
11
|
+
* a reputation rating, billed at cost; Safe House screening stays OFF until asked for (operator
|
|
12
|
+
* 2026-10-02); governance that costs none stays ON (operator 2026-10-03: autonomy is
|
|
12
13
|
* rules-only verification + policy). The goal contract is what nudges (re-anchor
|
|
13
14
|
* on drift). Specifically:
|
|
14
|
-
* - alignment card: autonomy_mode = observe, integrity_mode =
|
|
15
|
+
* - alignment card: autonomy_mode = observe, integrity_mode = observe, the agent's
|
|
15
16
|
* org as principal (delegated_authority), Claude Code tool names as bounded
|
|
16
17
|
* actions, coding forbidden actions, defined values, grace 0, and no default
|
|
17
18
|
* escalation trigger for irreversible operations (the customer adds their own);
|
|
@@ -22,8 +23,8 @@
|
|
|
22
23
|
* older default's content and keeps the agent's modes.
|
|
23
24
|
* - goal-alignment: nudge — sealed per-conversation via the goal contract
|
|
24
25
|
* (--goal …), graded and re-anchored by the gateway.
|
|
25
|
-
* `--governance [integrity,safe-house]`
|
|
26
|
-
* cost on top of the agent fee); `--no-governance` turns every card off, autonomy
|
|
26
|
+
* `--governance [integrity,safe-house]` picks exactly those features on (observe-only,
|
|
27
|
+
* billed at cost on top of the agent fee; `--governance none` opts out of both); `--no-governance` turns every card off, autonomy
|
|
27
28
|
* included; a refresh of an already-claimed agent keeps the
|
|
28
29
|
* modes its own cards carry unless a --governance / --no-governance flag says otherwise.
|
|
29
30
|
*
|
|
@@ -40,7 +41,8 @@ import { type CardSources, type CodeEntitlement } from "../lib/api.js";
|
|
|
40
41
|
export type GovernanceMode = "off" | "observe";
|
|
41
42
|
/**
|
|
42
43
|
* The governance features a setup turns on. Each is a model call per turn that
|
|
43
|
-
* the customer pays for at cost
|
|
44
|
+
* the customer pays for at cost. Integrity is on for a new agent (opt out with `--governance none`
|
|
45
|
+
* or `--no-governance`); Safe House is off unless asked for.
|
|
44
46
|
*/
|
|
45
47
|
export interface GovernanceOptIn {
|
|
46
48
|
/**
|
|
@@ -58,7 +60,7 @@ export interface GovernanceOptIn {
|
|
|
58
60
|
export declare const GOVERNANCE_FEATURES: readonly ["integrity", "safe-house"];
|
|
59
61
|
/**
|
|
60
62
|
* Parse the `--governance [features]` / `--no-governance` flag. undefined = not
|
|
61
|
-
* given (new agents get autonomy
|
|
63
|
+
* given (new agents get autonomy + integrity; a refresh keeps the agent's current modes),
|
|
62
64
|
* true = every feature, false (`--no-governance`) = everything off, autonomy
|
|
63
65
|
* included, a string = a comma-separated list of `integrity`, `safe-house`, `all`
|
|
64
66
|
* or `none`, each keeping autonomy on. Throws on an unknown name.
|
|
@@ -140,7 +142,7 @@ interface ExistingCards {
|
|
|
140
142
|
align: Record<string, unknown> | null;
|
|
141
143
|
prot: Record<string, unknown> | null;
|
|
142
144
|
}
|
|
143
|
-
/** Printed when a setup ran without --governance: how to
|
|
145
|
+
/** Printed when a setup ran without --governance: what is on by default, how to change it, and what it costs. */
|
|
144
146
|
export declare const GOVERNANCE_OPT_IN_HINT: string;
|
|
145
147
|
/**
|
|
146
148
|
* The default governed-agent slug for the current customer: `agent-<handle>`,
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
* governed identity `agent-<handle>`. This module makes that identity REAL in
|
|
7
7
|
* their Mnemom org: it births the agent through the gateway, claims it (so the
|
|
8
8
|
* customer owns it), and publishes a sane default coding posture. Governance that
|
|
9
|
-
* costs a model call is
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* costs a model call is split (operator 2026-11-04, MNE-9728, matching the API new-agent
|
|
10
|
+
* default): AIP integrity analysis is ON (observe) for a new agent so it is analyzed and can earn
|
|
11
|
+
* a reputation rating, billed at cost; Safe House screening stays OFF until asked for (operator
|
|
12
|
+
* 2026-10-02); governance that costs none stays ON (operator 2026-10-03: autonomy is
|
|
12
13
|
* rules-only verification + policy). The goal contract is what nudges (re-anchor
|
|
13
14
|
* on drift). Specifically:
|
|
14
|
-
* - alignment card: autonomy_mode = observe, integrity_mode =
|
|
15
|
+
* - alignment card: autonomy_mode = observe, integrity_mode = observe, the agent's
|
|
15
16
|
* org as principal (delegated_authority), Claude Code tool names as bounded
|
|
16
17
|
* actions, coding forbidden actions, defined values, grace 0, and no default
|
|
17
18
|
* escalation trigger for irreversible operations (the customer adds their own);
|
|
@@ -22,8 +23,8 @@
|
|
|
22
23
|
* older default's content and keeps the agent's modes.
|
|
23
24
|
* - goal-alignment: nudge — sealed per-conversation via the goal contract
|
|
24
25
|
* (--goal …), graded and re-anchored by the gateway.
|
|
25
|
-
* `--governance [integrity,safe-house]`
|
|
26
|
-
* cost on top of the agent fee); `--no-governance` turns every card off, autonomy
|
|
26
|
+
* `--governance [integrity,safe-house]` picks exactly those features on (observe-only,
|
|
27
|
+
* billed at cost on top of the agent fee; `--governance none` opts out of both); `--no-governance` turns every card off, autonomy
|
|
27
28
|
* included; a refresh of an already-claimed agent keeps the
|
|
28
29
|
* modes its own cards carry unless a --governance / --no-governance flag says otherwise.
|
|
29
30
|
*
|
|
@@ -55,7 +56,7 @@ const say = (line = "") => console.error(line);
|
|
|
55
56
|
export const GOVERNANCE_FEATURES = ["integrity", "safe-house"];
|
|
56
57
|
/**
|
|
57
58
|
* Parse the `--governance [features]` / `--no-governance` flag. undefined = not
|
|
58
|
-
* given (new agents get autonomy
|
|
59
|
+
* given (new agents get autonomy + integrity; a refresh keeps the agent's current modes),
|
|
59
60
|
* true = every feature, false (`--no-governance`) = everything off, autonomy
|
|
60
61
|
* included, a string = a comma-separated list of `integrity`, `safe-house`, `all`
|
|
61
62
|
* or `none`, each keeping autonomy on. Throws on an unknown name.
|
|
@@ -219,15 +220,16 @@ export function newAgentModes(governance) {
|
|
|
219
220
|
const autonomy = modeOf(governance?.autonomy ?? true);
|
|
220
221
|
return {
|
|
221
222
|
autonomy,
|
|
222
|
-
integrity: modeOf(governance?.integrity ??
|
|
223
|
+
integrity: modeOf(governance?.integrity ?? true),
|
|
223
224
|
protection: modeOf(governance?.safeHouse ?? false),
|
|
224
225
|
allowUnmappedTools: autonomy === "observe" ? true : undefined,
|
|
225
226
|
};
|
|
226
227
|
}
|
|
227
228
|
/**
|
|
228
229
|
* The plan for refreshing an EXISTING agent: an explicit --governance /
|
|
229
|
-
* --no-governance wins; else it keeps the modes its own cards carry. No agent-layer
|
|
230
|
-
*
|
|
230
|
+
* --no-governance wins; else it keeps the modes its own cards carry. No agent-layer card → autonomy and Safe House get the
|
|
231
|
+
* new-agent defaults but integrity stays off (an existing agent is never silently opted in to billed
|
|
232
|
+
* analysis; MNE-9728). A card with no mode field →
|
|
231
233
|
* observe (the prior CLI always wrote observe). A failed read → null: skip the refresh.
|
|
232
234
|
*/
|
|
233
235
|
async function existingAgentPlan(agentId, governance) {
|
|
@@ -252,7 +254,7 @@ async function existingAgentPlan(agentId, governance) {
|
|
|
252
254
|
return {
|
|
253
255
|
modes: {
|
|
254
256
|
autonomy: align ? (modeField(align, "autonomy_mode") ?? "observe") : fresh.autonomy,
|
|
255
|
-
integrity: align ? (modeField(align, "integrity_mode") ?? "observe") :
|
|
257
|
+
integrity: align ? (modeField(align, "integrity_mode") ?? "observe") : "off", // an existing agent with no card of its own is not silently opted in to billed analysis (MNE-9728)
|
|
256
258
|
protection: prot ? (modeField(prot, "mode") ?? "observe") : fresh.protection,
|
|
257
259
|
allowUnmappedTools: align
|
|
258
260
|
? typeof ownAllow === "boolean"
|
|
@@ -263,9 +265,10 @@ async function existingAgentPlan(agentId, governance) {
|
|
|
263
265
|
existing: { align, prot },
|
|
264
266
|
};
|
|
265
267
|
}
|
|
266
|
-
/** Printed when a setup ran without --governance: how to
|
|
267
|
-
export const GOVERNANCE_OPT_IN_HINT = "
|
|
268
|
-
"`mnemom agent setup --governance` (or --governance
|
|
268
|
+
/** Printed when a setup ran without --governance: what is on by default, how to change it, and what it costs. */
|
|
269
|
+
export const GOVERNANCE_OPT_IN_HINT = " AIP integrity analysis is on (observe) for new agents; Safe House screening is off by default. " +
|
|
270
|
+
"Opt out of integrity with `mnemom agent setup --governance none` (or --no-governance); turn Safe House on with " +
|
|
271
|
+
"`--governance integrity,safe-house`. Both are billed at cost on top of the agent fee.";
|
|
269
272
|
/**
|
|
270
273
|
* The default governed-agent slug for the current customer: `agent-<handle>`,
|
|
271
274
|
* where <handle> is the local-part of their mnemom login email (sanitised), or
|
|
@@ -457,8 +460,8 @@ async function confirmClaimed(opts, anchorKey, fp) {
|
|
|
457
460
|
const plan = await existingAgentPlan(agentId, opts.governance);
|
|
458
461
|
if (plan) {
|
|
459
462
|
if (!opts.governance) {
|
|
460
|
-
say(fmt.dim(` Keeping its current governance (${describeModes(plan.modes)}). New agents default to
|
|
461
|
-
"`--governance`
|
|
463
|
+
say(fmt.dim(` Keeping its current governance (${describeModes(plan.modes)}). New agents default to integrity observe; ` +
|
|
464
|
+
"`--governance` picks features, `--no-governance` turns everything off."));
|
|
462
465
|
}
|
|
463
466
|
// The principal is the agent's own org, which a refresh may launch from another.
|
|
464
467
|
const ownOrg = await agentOrgId(agentId);
|
|
@@ -511,7 +514,7 @@ async function confirmClaimed(opts, anchorKey, fp) {
|
|
|
511
514
|
}
|
|
512
515
|
say(fmt.success(`mnemom agent: claimed '${slug}' (${agentId}) in ${opts.orgLabel}.`));
|
|
513
516
|
await noteSameNamedAgent(slug, agentId);
|
|
514
|
-
// Publish the default cards (autonomy
|
|
517
|
+
// Publish the default cards (autonomy + integrity observe; Safe House off unless
|
|
515
518
|
// --governance). A card write that can't complete does NOT undo the claim — the agent is
|
|
516
519
|
// claimed and governed on whatever landed; we warn how to finish it.
|
|
517
520
|
if (opts.setup !== false) {
|
|
@@ -1443,9 +1446,11 @@ function carryForward(card, kind) {
|
|
|
1443
1446
|
return card;
|
|
1444
1447
|
return upgradeInPlace(card, kind) ?? card;
|
|
1445
1448
|
}
|
|
1446
|
-
export function buildCodeAlignmentCard(agentId,
|
|
1449
|
+
export function buildCodeAlignmentCard(agentId,
|
|
1450
|
+
// Every production caller passes modes; the fallback's integrity matches the new-agent default (MNE-9728).
|
|
1451
|
+
modes = {
|
|
1447
1452
|
autonomy: "off",
|
|
1448
|
-
integrity: "
|
|
1453
|
+
integrity: "observe",
|
|
1449
1454
|
}, principal = UNSPECIFIED_PRINCIPAL) {
|
|
1450
1455
|
return {
|
|
1451
1456
|
card_version: "unified/2026-04-26",
|
package/dist/commands/card.js
CHANGED
|
@@ -885,7 +885,7 @@ export async function cardEditCommand(agentName, options = {}) {
|
|
|
885
885
|
card_version: "unified/2026-04-26",
|
|
886
886
|
agent_id: agentId,
|
|
887
887
|
autonomy_mode: "observe",
|
|
888
|
-
integrity_mode: "
|
|
888
|
+
integrity_mode: "observe", // new-agent default (MNE-9728); set to off to opt out
|
|
889
889
|
enforcement: { allow_unmapped_tools: true },
|
|
890
890
|
principal: { type: "agent", identifier: agentId, relationship: "delegated_authority" },
|
|
891
891
|
values: { declared: ["transparency", "safety", "honesty"] },
|
package/dist/commands/onboard.js
CHANGED
|
@@ -45,6 +45,7 @@ import { isInteractive, askInput, askYesNo } from "../lib/prompt.js";
|
|
|
45
45
|
import { getWebsiteUrl } from "../lib/config.js";
|
|
46
46
|
import { openBrowser } from "../lib/oauth.js";
|
|
47
47
|
import { fmt } from "../lib/format.js";
|
|
48
|
+
import { starterIntegrityMode } from "../lib/starter-integrity.js";
|
|
48
49
|
const FULL_PROOF_RE = /^[0-9a-f]{64}$/;
|
|
49
50
|
/**
|
|
50
51
|
* Entry point for `mnemom onboard`. Throws on a fatal error (caught by the
|
|
@@ -94,7 +95,7 @@ export async function onboardCommand(options = {}) {
|
|
|
94
95
|
say();
|
|
95
96
|
say(`${fmt.badge("alignment", "cyan")} Declaring your alignment card…`);
|
|
96
97
|
try {
|
|
97
|
-
await putAlignmentCard(agentId, JSON.stringify(buildStarterAlignmentCard(agentId)), "application/json",
|
|
98
|
+
await putAlignmentCard(agentId, JSON.stringify(buildStarterAlignmentCard(agentId, await starterIntegrityMode(agentId))), "application/json",
|
|
98
99
|
// A STABLE key so a re-run replays the same server-side reservation
|
|
99
100
|
// (idempotent no-op) rather than writing a divergent duplicate card.
|
|
100
101
|
{ idempotencyKey: `onboard-alignment-${agentId}` });
|
|
@@ -353,16 +354,16 @@ async function earnRating(agentId, steps, say) {
|
|
|
353
354
|
// ── starter alignment card ──────────────────────────────────────────────────────
|
|
354
355
|
/**
|
|
355
356
|
* A starter alignment card: autonomy observe (rules only, no model call), integrity
|
|
356
|
-
*
|
|
357
|
+
* as the agent's own card has it (observe for a new agent, MNE-9728; see lib/starter-integrity.ts) — the honest, minimal declaration
|
|
357
358
|
* of intent for a freshly-onboarded agent. Mirrors the shape `wrap` seeds so the
|
|
358
359
|
* two skill-runners produce a consistent first card.
|
|
359
360
|
*/
|
|
360
|
-
function buildStarterAlignmentCard(agentId) {
|
|
361
|
+
function buildStarterAlignmentCard(agentId, integrityMode) {
|
|
361
362
|
return {
|
|
362
363
|
card_version: "unified/2026-04-26",
|
|
363
364
|
agent_id: agentId,
|
|
364
365
|
autonomy_mode: "observe",
|
|
365
|
-
integrity_mode:
|
|
366
|
+
integrity_mode: integrityMode, // the agent's own mode; observe for a new agent (MNE-9728)
|
|
366
367
|
// No capability mappings here, so unmapped tools must pass or every turn fails.
|
|
367
368
|
enforcement: { allow_unmapped_tools: true },
|
|
368
369
|
principal: { type: "agent", identifier: agentId, relationship: "delegated_authority" },
|
package/dist/commands/scan.d.ts
CHANGED
|
@@ -13,7 +13,6 @@ import { type RecordDeps, type RecordOutcome } from "../lib/scan/record.js";
|
|
|
13
13
|
import { type CostSpend } from "../lib/scan/cost-per-pr.js";
|
|
14
14
|
import { type ReportEmailDeps } from "../lib/scan/report-email.js";
|
|
15
15
|
import { type RepoListDeps } from "../lib/scan/repo-picker.js";
|
|
16
|
-
import { type ReportRecipients } from "../lib/scan/share.js";
|
|
17
16
|
export interface ScanCommandOptions {
|
|
18
17
|
repos: string[];
|
|
19
18
|
period?: {
|
|
@@ -21,8 +20,10 @@ export interface ScanCommandOptions {
|
|
|
21
20
|
end: string;
|
|
22
21
|
};
|
|
23
22
|
share?: string;
|
|
24
|
-
/**
|
|
23
|
+
/** Alias of one --email (kept for older scripts). */
|
|
25
24
|
ctoEmail?: string;
|
|
25
|
+
/** Extra recipients of the report (repeatable; comma-separated accepted). The runner and every org admin always get it. */
|
|
26
|
+
email?: string[];
|
|
26
27
|
/** Tier 2: the CTO's own monthly figure, as typed. Needs MNEMOM_ORG_SCAN_MODE=true. */
|
|
27
28
|
monthlySpend?: string;
|
|
28
29
|
/** Tier 2 without a figure on the command line: ask for one on the terminal. */
|
|
@@ -71,11 +72,24 @@ export interface ScanCommandOptions {
|
|
|
71
72
|
/** Parses repeated/comma-separated `--repo owner/name`. Returns null on any invalid slug. */
|
|
72
73
|
export declare function parseRepoFlags(raw: string[] | undefined): string[] | null;
|
|
73
74
|
export declare const isEmail: (v: unknown) => v is string;
|
|
74
|
-
|
|
75
|
-
export declare
|
|
76
|
-
|
|
75
|
+
export declare const DELETE_CONTACT = "privacy@mnemom.ai";
|
|
76
|
+
export declare const EMAIL_ONE_MSG = "Enter one email address, like you@company.com.";
|
|
77
|
+
export declare const EMAIL_LIST_MSG = "Enter email addresses like you@company.com, separated by commas.";
|
|
78
|
+
export declare const MAX_EXTRA_EMAILS = 10;
|
|
79
|
+
/**
|
|
80
|
+
* The people to add to the report email (the runner and every org admin always get it). Comes from the
|
|
81
|
+
* repeatable --email flag (and its alias --cto-email), each value one address or a comma-separated list;
|
|
82
|
+
* on a terminal with none given, asks once (blank to skip). Plain-language errors; the value is never echoed.
|
|
83
|
+
*/
|
|
84
|
+
export declare function resolveExtraEmails(o: {
|
|
85
|
+
email?: string[];
|
|
77
86
|
ctoEmail?: string;
|
|
78
|
-
|
|
87
|
+
isTty?: boolean;
|
|
88
|
+
}): Promise<string[]>;
|
|
89
|
+
/** The runner's own address from the Mnemom login, or undefined (the server resolves it for the email either way). */
|
|
90
|
+
export declare function runnerEmailFromLogin(o: {
|
|
91
|
+
runnerEmail?: string;
|
|
92
|
+
}): string | undefined;
|
|
79
93
|
/** The CTO's figure in USD, or null for none. Throws fixed text only; the input is never echoed. */
|
|
80
94
|
export declare function resolveSelfReportedSpend(o: ScanCommandOptions): Promise<number | null>;
|
|
81
95
|
/**
|
package/dist/commands/scan.js
CHANGED
|
@@ -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) {
|
|
@@ -238,16 +270,20 @@ export async function resolveRecordOutcome(o) {
|
|
|
238
270
|
}
|
|
239
271
|
export async function scanCommand(o) {
|
|
240
272
|
if (o.share !== undefined && parseShareLevel(o.share) === null) {
|
|
241
|
-
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)");
|
|
242
274
|
}
|
|
243
275
|
const noRepoFlag = o.repos.length === 0;
|
|
276
|
+
const interactive = Boolean(o.isTty ?? process.stdin.isTTY);
|
|
244
277
|
// No --repo: on a terminal pick from the repos this GitHub login can read; otherwise the error stays, with a hint.
|
|
245
|
-
if (noRepoFlag && !
|
|
278
|
+
if (noRepoFlag && !interactive)
|
|
246
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);
|
|
247
285
|
// The report is emailed on every scan, so resolve the org before any scan work; failure is loud.
|
|
248
286
|
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
287
|
const getCredential = async () => {
|
|
252
288
|
const c = o.credential ?? (await resolveCredential().catch(() => null));
|
|
253
289
|
if (!c)
|
|
@@ -292,19 +328,23 @@ export async function scanCommand(o) {
|
|
|
292
328
|
else if (record !== null)
|
|
293
329
|
for (const line of renderRecordSection(record))
|
|
294
330
|
console.log(line);
|
|
295
|
-
const level = parseShareLevel(o.share) ?? (await promptShareLevel());
|
|
296
331
|
// Every successful scan emails the report to the person who ran it, at every share level, no opt-out.
|
|
297
332
|
// Mnemom keeps nothing from a share=none send (the server sends the email and does nothing else).
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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.");
|
|
338
|
+
}
|
|
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.");
|
|
304
341
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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}.`);
|
|
308
348
|
if (emailFailure)
|
|
309
349
|
throw new ReportEmailError(emailFailure);
|
|
310
350
|
}
|
|
@@ -312,7 +352,7 @@ export async function scanCommand(o) {
|
|
|
312
352
|
* Emails the report. Loud on every failure: prints the reason name, never the server's words, and
|
|
313
353
|
* returns it so the caller exits non-zero after the local report is shown.
|
|
314
354
|
*/
|
|
315
|
-
async function emailReport(o, org, result, level, spend, period, costSpend) {
|
|
355
|
+
async function emailReport(o, org, result, level, spend, period, costSpend, extraEmails) {
|
|
316
356
|
const fail = (f) => {
|
|
317
357
|
console.log(failureMessage(f));
|
|
318
358
|
return f;
|
|
@@ -324,17 +364,21 @@ async function emailReport(o, org, result, level, spend, period, costSpend) {
|
|
|
324
364
|
period,
|
|
325
365
|
costSpend,
|
|
326
366
|
});
|
|
327
|
-
const body = buildEmailBody(report, level, result.coverage.complete,
|
|
367
|
+
const body = buildEmailBody(report, level, result.coverage.complete, extraEmails);
|
|
328
368
|
const outcome = await sendReportEmail(org, body, o.emailDeps);
|
|
329
369
|
if (outcome.status === "failed")
|
|
330
370
|
return fail(outcome.reason);
|
|
331
371
|
if (outcome.status === "unavailable") {
|
|
332
372
|
// Org Scan is off by default: the gate's 404 is not a failure and must not break the scan.
|
|
333
373
|
console.log(UNAVAILABLE_MSG);
|
|
334
|
-
return
|
|
374
|
+
return "not_sent";
|
|
335
375
|
}
|
|
336
376
|
console.log(outcome.status === "dry_run"
|
|
337
377
|
? DRY_RUN_MSG
|
|
338
|
-
:
|
|
339
|
-
|
|
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;
|
|
340
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,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,
|
|
@@ -56,6 +56,18 @@ export interface EmailReport {
|
|
|
56
56
|
*/
|
|
57
57
|
cost_per_merged_pr?: CostPerPr["cost_per_merged_pr"];
|
|
58
58
|
cost_by_pr_size?: CostPerPr["cost_by_pr_size"];
|
|
59
|
+
/**
|
|
60
|
+
* The per-repo counts, by name (MNE-9634). Always sent, at every share level: the emailed report is the full
|
|
61
|
+
* report. Optional on the wire so an API that predates it can be retried without it. Capped at 500 repos
|
|
62
|
+
* (busiest first), valid names only, each name once.
|
|
63
|
+
*/
|
|
64
|
+
by_repo?: Array<{
|
|
65
|
+
repo: string;
|
|
66
|
+
pr_count: number;
|
|
67
|
+
revert_count: number;
|
|
68
|
+
automatable_count: number;
|
|
69
|
+
examined_count: number;
|
|
70
|
+
}>;
|
|
59
71
|
}
|
|
60
72
|
export interface ReportEmailBody {
|
|
61
73
|
report: EmailReport;
|
|
@@ -63,6 +75,9 @@ export interface ReportEmailBody {
|
|
|
63
75
|
coverage: {
|
|
64
76
|
complete: boolean;
|
|
65
77
|
};
|
|
78
|
+
/** The extra recipients the runner added. Absent when none. */
|
|
79
|
+
emails?: string[];
|
|
80
|
+
/** Only set on the retry to an API that predates `emails`: it reads one extra address under this name. */
|
|
66
81
|
cto_email?: string;
|
|
67
82
|
}
|
|
68
83
|
/**
|
|
@@ -84,9 +99,9 @@ export declare function buildEmailReport(result: ScanResult, o: {
|
|
|
84
99
|
/** The one spend source the cost figures are split from; null/absent leaves them out. */
|
|
85
100
|
costSpend?: CostSpend | null;
|
|
86
101
|
}): EmailReport;
|
|
87
|
-
export declare function buildEmailBody(report: EmailReport, level: ShareLevel, complete: boolean,
|
|
102
|
+
export declare function buildEmailBody(report: EmailReport, level: ShareLevel, complete: boolean, extraEmails?: string[]): ReportEmailBody;
|
|
88
103
|
/** Closed set of failure names; each has fixed text. */
|
|
89
|
-
export type EmailFailure = "presend_check_failed" | "email_not_sent" | "not_signed_in" | "not_a_member" | "report_rejected" | "no_recipients" | "runner_not_emailed" | "network_error" | "unexpected_response";
|
|
104
|
+
export type EmailFailure = "presend_check_failed" | "email_not_sent" | "not_signed_in" | "not_a_member" | "report_rejected" | "no_recipients" | "runner_not_emailed" | "emails_rejected" | "email_domain_not_allowed" | "admins_not_found" | "network_error" | "unexpected_response";
|
|
90
105
|
export declare const failureMessage: (f: EmailFailure) => string;
|
|
91
106
|
export declare const DRY_RUN_MSG = "Report email is in dry-run on this environment; nothing was sent.";
|
|
92
107
|
export declare const UNAVAILABLE_MSG = "Report email is not available for this org yet; nothing was sent.";
|
|
@@ -97,6 +112,8 @@ export declare class ReportEmailError extends Error {
|
|
|
97
112
|
export type EmailOutcome = {
|
|
98
113
|
status: "sent";
|
|
99
114
|
recipients: number;
|
|
115
|
+
adminsMissed?: number;
|
|
116
|
+
limited?: true;
|
|
100
117
|
} | {
|
|
101
118
|
status: "dry_run";
|
|
102
119
|
} | {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The report email: after every successful scan the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The report email: after every successful scan the full report, with the counts for each repo, is
|
|
3
|
+
* emailed to the person who ran it, every Org Admin of the org and anyone the runner added (MNE-9634),
|
|
4
|
+
* with no opt-out and at every share level. One call to `POST /v1/orgs/{org_id}/scan/report-email`.
|
|
5
|
+
* The server resolves the runner's address from the signed-in identity and the Org Admins from the org,
|
|
6
|
+
* so the request carries neither; `emails` holds only the extra addresses the runner added.
|
|
6
7
|
*
|
|
7
|
-
* The body is exactly the route's contract and nothing else: `report` (org-scan-report/1: team totals
|
|
8
|
-
* and counts
|
|
9
|
-
*
|
|
8
|
+
* The body is exactly the route's contract and nothing else: `report` (org-scan-report/1: team totals,
|
|
9
|
+
* counts, and the per-repo counts `by_repo`), `share_level`, `coverage.complete` and the optional
|
|
10
|
+
* `emails`. No author, title, branch or body text, no credential.
|
|
10
11
|
*
|
|
11
12
|
* Every failure has a fixed name and fixed text. The server's body, the thrown value and the
|
|
12
13
|
* credential never reach the terminal.
|
|
@@ -40,6 +41,7 @@ export function buildEmailReport(result, o) {
|
|
|
40
41
|
// Only a scan that read every merged PR in the period gives an average that means anything.
|
|
41
42
|
const exhaustive = coverage.complete && coverage.prsSkippedLarge === 0 && coverage.prsUnreadable === 0;
|
|
42
43
|
const cost = buildCostPerPr(result.prSizes, o.period ?? { start: coverage.windowStart, end: coverage.windowEnd }, o.costSpend ?? null, exhaustive);
|
|
44
|
+
const byRepo = repoRows(result);
|
|
43
45
|
return {
|
|
44
46
|
schema_version: REPORT_SCHEMA_VERSION,
|
|
45
47
|
org_id: o.orgId,
|
|
@@ -72,14 +74,38 @@ export function buildEmailReport(result, o) {
|
|
|
72
74
|
},
|
|
73
75
|
estimate: { name: ESTIMATE_NAME, value, unit, method_url: METHOD_URL },
|
|
74
76
|
...cost,
|
|
77
|
+
...(byRepo.length > 0 ? { by_repo: byRepo } : {}),
|
|
75
78
|
};
|
|
76
79
|
}
|
|
77
|
-
|
|
80
|
+
const MAX_REPOS = 500;
|
|
81
|
+
const REPO_NAME = /^[A-Za-z0-9._-]{1,100}\/[A-Za-z0-9._-]{1,100}$/;
|
|
82
|
+
/** The per-repo rows for the report: valid names only, each once (any case), busiest first, at most 500. */
|
|
83
|
+
function repoRows(result) {
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
return [...result.byRepo]
|
|
86
|
+
.sort((a, b) => b.prs - a.prs || a.repo.localeCompare(b.repo))
|
|
87
|
+
.filter((r) => {
|
|
88
|
+
const k = r.repo.toLowerCase();
|
|
89
|
+
if (!REPO_NAME.test(r.repo) || seen.has(k))
|
|
90
|
+
return false;
|
|
91
|
+
seen.add(k);
|
|
92
|
+
return true;
|
|
93
|
+
})
|
|
94
|
+
.slice(0, MAX_REPOS)
|
|
95
|
+
.map((r) => ({
|
|
96
|
+
repo: r.repo,
|
|
97
|
+
pr_count: r.prs,
|
|
98
|
+
revert_count: r.reverts,
|
|
99
|
+
automatable_count: r.ciGreen,
|
|
100
|
+
examined_count: r.examined,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
export function buildEmailBody(report, level, complete, extraEmails = []) {
|
|
78
104
|
return {
|
|
79
105
|
report,
|
|
80
106
|
share_level: level,
|
|
81
107
|
coverage: { complete },
|
|
82
|
-
...(
|
|
108
|
+
...(extraEmails.length > 0 ? { emails: extraEmails } : {}),
|
|
83
109
|
};
|
|
84
110
|
}
|
|
85
111
|
const FAILURE_TEXT = {
|
|
@@ -89,7 +115,10 @@ const FAILURE_TEXT = {
|
|
|
89
115
|
not_a_member: "your account is not a member of the org passed to --org.",
|
|
90
116
|
report_rejected: "Mnemom rejected the report as malformed.",
|
|
91
117
|
no_recipients: "the server accepted the report but reported zero recipients.",
|
|
92
|
-
runner_not_emailed: "the report was not delivered to you (the person who ran the scan); it may have gone only to the
|
|
118
|
+
runner_not_emailed: "the report was not delivered to you (the person who ran the scan); it may have gone only to the others. Re-run the scan, and check that your account has a valid email address.",
|
|
119
|
+
emails_rejected: "an address you added was not accepted. Add up to 10 valid email addresses.",
|
|
120
|
+
email_domain_not_allowed: "an address you added is not on your organization's email domain. Add addresses on your own domain or mnemom.ai.",
|
|
121
|
+
admins_not_found: "Mnemom could not look up your org admins, so no email was sent. Try again in a moment.",
|
|
93
122
|
network_error: "Mnemom could not be reached.",
|
|
94
123
|
unexpected_response: "Mnemom answered in an unexpected way.",
|
|
95
124
|
};
|
|
@@ -118,11 +147,27 @@ function classifyStatus(status) {
|
|
|
118
147
|
return "report_rejected";
|
|
119
148
|
return "unexpected_response";
|
|
120
149
|
}
|
|
121
|
-
/**
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
150
|
+
/**
|
|
151
|
+
* The body an API that predates this contract understands: no cost figures, no per-repo rows, and the
|
|
152
|
+
* share level under its old name (`analytics` was `summary`; the meaning is the same, never upgraded).
|
|
153
|
+
* The same object when nothing needs changing.
|
|
154
|
+
*/
|
|
155
|
+
function legacyBody(body) {
|
|
156
|
+
const { cost_per_merged_pr: _a, cost_by_pr_size: _b, by_repo: _c, ...rest } = body.report;
|
|
157
|
+
const changed = "cost_per_merged_pr" in body.report ||
|
|
158
|
+
"cost_by_pr_size" in body.report ||
|
|
159
|
+
"by_repo" in body.report ||
|
|
160
|
+
body.share_level === "analytics" ||
|
|
161
|
+
(body.emails?.length ?? 0) > 0;
|
|
162
|
+
const { emails, ...top } = body;
|
|
163
|
+
const extra = emails?.[0];
|
|
164
|
+
return changed
|
|
165
|
+
? {
|
|
166
|
+
...top,
|
|
167
|
+
...(extra ? { cto_email: extra } : {}),
|
|
168
|
+
report: rest,
|
|
169
|
+
share_level: body.share_level === "analytics" ? "summary" : body.share_level,
|
|
170
|
+
}
|
|
126
171
|
: body;
|
|
127
172
|
}
|
|
128
173
|
/** Never throws. The thrown value, the response body and the credential are never surfaced. */
|
|
@@ -135,15 +180,23 @@ export async function sendReportEmail(orgId, body, deps = {}) {
|
|
|
135
180
|
catch (e) {
|
|
136
181
|
if (e instanceof MnemomApiError && e.effectiveStatus === 404)
|
|
137
182
|
return { status: "unavailable" };
|
|
138
|
-
// An API that predates the cost figures
|
|
139
|
-
// go out, so retry once
|
|
183
|
+
// An API that predates the cost figures, the per-repo rows or the analytics name rejects them with a 400. The email must still
|
|
184
|
+
// go out, so retry once in the old shape; any other failure is classified as below.
|
|
140
185
|
if (e instanceof MnemomApiError && e.effectiveStatus === 400) {
|
|
141
|
-
const bare =
|
|
142
|
-
if (bare !== body)
|
|
143
|
-
|
|
186
|
+
const bare = legacyBody(body);
|
|
187
|
+
if (bare !== body && !/email|admin/i.test(e.code ?? "")) {
|
|
188
|
+
const r = await sendReportEmail(orgId, bare, deps);
|
|
189
|
+
return r.status === "sent" ? { ...r, limited: true } : r;
|
|
190
|
+
}
|
|
144
191
|
}
|
|
145
192
|
if (e instanceof MnemomApiError) {
|
|
146
193
|
// A 4xx that names the runner (the API refuses a send that would skip the runner) gets its own text.
|
|
194
|
+
if (e.code === "email_domain_not_allowed")
|
|
195
|
+
return { status: "failed", reason: "email_domain_not_allowed" };
|
|
196
|
+
if (/emails/i.test(e.code ?? ""))
|
|
197
|
+
return { status: "failed", reason: "emails_rejected" };
|
|
198
|
+
if (e.code === "admin_lookup_failed")
|
|
199
|
+
return { status: "failed", reason: "admins_not_found" };
|
|
147
200
|
if (e.effectiveStatus >= 400 && e.effectiveStatus < 500 && /runner/i.test(e.code ?? ""))
|
|
148
201
|
return { status: "failed", reason: "runner_not_emailed" };
|
|
149
202
|
return { status: "failed", reason: classifyStatus(e.effectiveStatus) };
|
|
@@ -158,8 +211,11 @@ export async function sendReportEmail(orgId, body, deps = {}) {
|
|
|
158
211
|
return { status: "failed", reason: "unexpected_response" };
|
|
159
212
|
if (recipients === 0)
|
|
160
213
|
return { status: "failed", reason: "no_recipients" };
|
|
161
|
-
// `skipped` lists the roles ("runner", "cto") that got no copy. The runner must always get the report.
|
|
214
|
+
// `skipped` lists the roles ("runner", "admin", "added", "cto") that got no copy. The runner must always get the report.
|
|
162
215
|
if (Array.isArray(o.skipped) && o.skipped.includes("runner"))
|
|
163
216
|
return { status: "failed", reason: "runner_not_emailed" };
|
|
164
|
-
|
|
217
|
+
const adminsMissed = Array.isArray(o.skipped) ? o.skipped.filter((s) => s === "admin").length : 0;
|
|
218
|
+
return o.status === "dry_run"
|
|
219
|
+
? { status: "dry_run" }
|
|
220
|
+
: { status: "sent", recipients, ...(adminsMissed > 0 ? { adminsMissed } : {}) };
|
|
165
221
|
}
|
package/dist/lib/scan/scan.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface RepoTotals {
|
|
|
43
43
|
prs: number;
|
|
44
44
|
reverts: number;
|
|
45
45
|
ciGreen: number;
|
|
46
|
+
/** PRs read and counted (not reverts, not skipped); the per-repo base of the CI-green share. */
|
|
47
|
+
examined: number;
|
|
46
48
|
}
|
|
47
49
|
export interface ScanResult {
|
|
48
50
|
coverage: Coverage;
|
package/dist/lib/scan/scan.js
CHANGED
|
@@ -104,7 +104,7 @@ export async function runLocalScan(deps, input) {
|
|
|
104
104
|
const bucket = (repo) => {
|
|
105
105
|
let b = byRepo.get(repo);
|
|
106
106
|
if (!b)
|
|
107
|
-
byRepo.set(repo, (b = { repo, prs: 0, reverts: 0, ciGreen: 0 }));
|
|
107
|
+
byRepo.set(repo, (b = { repo, prs: 0, reverts: 0, ciGreen: 0, examined: 0 }));
|
|
108
108
|
return b;
|
|
109
109
|
};
|
|
110
110
|
const totals = { prs: 0, reverts: 0, ciGreen: 0, examined: 0 };
|
|
@@ -142,6 +142,7 @@ export async function runLocalScan(deps, input) {
|
|
|
142
142
|
}
|
|
143
143
|
totals.prs += 1;
|
|
144
144
|
totals.examined += 1;
|
|
145
|
+
bucket(repo).examined += 1;
|
|
145
146
|
bucket(repo).prs += 1;
|
|
146
147
|
if (rec.ci === "success") {
|
|
147
148
|
totals.ciGreen += 1;
|
package/dist/lib/scan/share.d.ts
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import type { ScanResult } from "./scan.js";
|
|
2
|
-
export type ShareLevel = "
|
|
2
|
+
export type ShareLevel = "full" | "analytics" | "none";
|
|
3
3
|
/** The level preselected at the prompt (Enter picks it). */
|
|
4
4
|
export declare const DEFAULT_SHARE_LEVEL: ShareLevel;
|
|
5
5
|
export declare const SHARE_OPTIONS: ReadonlyArray<{
|
|
6
6
|
level: ShareLevel;
|
|
7
7
|
label: string;
|
|
8
8
|
}>;
|
|
9
|
-
/** Shown just above the
|
|
9
|
+
/** Shown just above the prompt. The full report, repo names included, is emailed whatever is chosen here. */
|
|
10
10
|
export declare const SHARE_DISCLOSURE: string;
|
|
11
11
|
/** Asks the user, with Full preselected. Anything but a recognised answer (cancel, EOF, noise) is "none": fail closed. */
|
|
12
12
|
export declare function promptShareLevel(): Promise<ShareLevel>;
|
|
13
|
+
/** `summary` is the old name for `analytics`; the meaning is unchanged. */
|
|
13
14
|
export declare function parseShareLevel(v: string | undefined): ShareLevel | null;
|
|
14
|
-
/** Who receives the report.
|
|
15
|
+
/** Who receives the report. The runner comes from the Mnemom login; the server resolves the Org Admins. */
|
|
15
16
|
export interface ReportRecipients {
|
|
16
|
-
runner_email
|
|
17
|
-
|
|
17
|
+
runner_email?: string;
|
|
18
|
+
extra_emails: string[];
|
|
18
19
|
}
|
|
19
20
|
export interface SharePayload {
|
|
20
21
|
recipients: ReportRecipients;
|
|
@@ -30,6 +31,7 @@ export interface SharePayload {
|
|
|
30
31
|
coverage_complete: boolean;
|
|
31
32
|
/** Absent when no spend line may be shown. */
|
|
32
33
|
spend_line?: string;
|
|
34
|
+
/** Present at "full" only. */
|
|
33
35
|
repos?: Array<{
|
|
34
36
|
repo: string;
|
|
35
37
|
prs: number;
|
|
@@ -37,6 +39,6 @@ export interface SharePayload {
|
|
|
37
39
|
ci_green: number;
|
|
38
40
|
}>;
|
|
39
41
|
}
|
|
40
|
-
/** Builds only what the level allows. Never includes authors, titles, branches, bodies or tokens. The only person data is the
|
|
42
|
+
/** Builds only what the level allows. Never includes authors, titles, branches, bodies or tokens. The only person data is the report recipients. */
|
|
41
43
|
export declare function buildSharePayload(level: Exclude<ShareLevel, "none">, r: ScanResult, recipients: ReportRecipients, spendLine?: string | null): SharePayload;
|
|
42
44
|
export declare function buildSharePayload(level: ShareLevel, r: ScanResult, recipients: ReportRecipients, spendLine?: string | null): SharePayload | null;
|
package/dist/lib/scan/share.js
CHANGED
|
@@ -1,32 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* "What may Mnemom keep on file?" (MNE-9634). This is a retention choice only; it never changes what is emailed. It is asked at
|
|
3
|
+
* the start, before any scan work. The scan itself is always complete: the full report, with the counts
|
|
4
|
+
* for each repo, is emailed to the runner, every Org Admin and anyone the runner adds, at every level.
|
|
5
|
+
*
|
|
6
|
+
* full Mnemom keeps the totals and the counts for each repo, by name, on file (the default).
|
|
7
|
+
* analytics Mnemom keeps the team totals only, with no repo names. `summary` is the old name for this
|
|
8
|
+
* and is accepted as an alias; it is never upgraded to full.
|
|
9
|
+
* none Mnemom keeps nothing on file. The report is still emailed.
|
|
10
|
+
*
|
|
11
|
+
* "none" is what a cancelled, empty-piped or non-interactive prompt resolves to (fail closed).
|
|
12
|
+
* Storing the shared payload on Mnemom's side is not built yet; the CLI only prints it.
|
|
5
13
|
*/
|
|
6
14
|
import { askSelect } from "../prompt.js";
|
|
7
15
|
/** The level preselected at the prompt (Enter picks it). */
|
|
8
16
|
export const DEFAULT_SHARE_LEVEL = "full";
|
|
9
17
|
export const SHARE_OPTIONS = [
|
|
10
|
-
{
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
{
|
|
19
|
+
level: "full",
|
|
20
|
+
label: "Full report (default): Mnemom keeps the totals and the counts for each repo, by name, on file",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
level: "analytics",
|
|
24
|
+
label: "Analytics: Mnemom keeps the team totals only on file, with no repo names",
|
|
25
|
+
},
|
|
26
|
+
{ level: "none", label: "Nothing: Mnemom keeps nothing on file" },
|
|
13
27
|
];
|
|
14
|
-
/** Shown just above the
|
|
28
|
+
/** Shown just above the prompt. The full report, repo names included, is emailed whatever is chosen here. */
|
|
15
29
|
export const SHARE_DISCLOSURE = [
|
|
16
|
-
"
|
|
17
|
-
"
|
|
30
|
+
"Whatever you choose, the full report, including repo names, is emailed to you, your org's admins and any addresses you added.",
|
|
31
|
+
"The next question is only about what Mnemom keeps on file. Pressing Enter chooses the Full report.",
|
|
18
32
|
].join("\n");
|
|
19
33
|
/** Asks the user, with Full preselected. Anything but a recognised answer (cancel, EOF, noise) is "none": fail closed. */
|
|
20
34
|
export async function promptShareLevel() {
|
|
21
35
|
// eslint-disable-next-line no-console -- consent text must show on stdout directly above the prompt (askSelect also prints with console.log)
|
|
22
36
|
console.log(SHARE_DISCLOSURE);
|
|
23
|
-
const answer = await askSelect("What may
|
|
37
|
+
const answer = await askSelect("What may Mnemom keep on file?", SHARE_OPTIONS.map((o) => o.label), SHARE_OPTIONS.findIndex((o) => o.level === DEFAULT_SHARE_LEVEL));
|
|
24
38
|
if (answer === null)
|
|
25
39
|
return "none";
|
|
26
40
|
return SHARE_OPTIONS.find((o) => o.label === answer)?.level ?? "none";
|
|
27
41
|
}
|
|
42
|
+
/** `summary` is the old name for `analytics`; the meaning is unchanged. */
|
|
28
43
|
export function parseShareLevel(v) {
|
|
29
|
-
|
|
44
|
+
if (v === "summary")
|
|
45
|
+
return "analytics";
|
|
46
|
+
return v === "analytics" || v === "full" || v === "none" ? v : null;
|
|
30
47
|
}
|
|
31
48
|
export function buildSharePayload(level, r, recipients,
|
|
32
49
|
/** undefined: the legacy line; null: no spend line; string: that line. */
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The integrity_mode `wrap` and `onboard` write into their starter alignment card.
|
|
3
|
+
*
|
|
4
|
+
* Both commands re-publish the starter card on every run, including a re-run on an agent that
|
|
5
|
+
* already exists (wrap: the same key + name returns the same agent; onboard: an already-claimed
|
|
6
|
+
* agent). The card must never change an existing agent's stored mode, so the mode comes from the
|
|
7
|
+
* agent's own (agent-layer) card when it has one — the same rule `mnemom agent setup` applies to
|
|
8
|
+
* an existing agent (agent-setup.ts existingAgentPlan):
|
|
9
|
+
*
|
|
10
|
+
* - own card with an integrity_mode → keep it (off stays off; nudge/enforce stay put)
|
|
11
|
+
* - own card without the field → observe (what the card already resolved to)
|
|
12
|
+
* - no own card → off (never silently opt an agent in to billed analysis)
|
|
13
|
+
*
|
|
14
|
+
* A newly born agent has its own card: mnemom-api stamps an observe birth card (ADR-API-003), and
|
|
15
|
+
* the gateway's card-less bootstrap writes observe too, so a new agent keeps observe (MNE-9728).
|
|
16
|
+
*
|
|
17
|
+
* THROWS when the read fails (anything but a 404): the caller then skips the card write rather
|
|
18
|
+
* than guess a mode in either direction.
|
|
19
|
+
*/
|
|
20
|
+
export declare function starterIntegrityMode(agentId: string): Promise<string>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The integrity_mode `wrap` and `onboard` write into their starter alignment card.
|
|
3
|
+
*
|
|
4
|
+
* Both commands re-publish the starter card on every run, including a re-run on an agent that
|
|
5
|
+
* already exists (wrap: the same key + name returns the same agent; onboard: an already-claimed
|
|
6
|
+
* agent). The card must never change an existing agent's stored mode, so the mode comes from the
|
|
7
|
+
* agent's own (agent-layer) card when it has one — the same rule `mnemom agent setup` applies to
|
|
8
|
+
* an existing agent (agent-setup.ts existingAgentPlan):
|
|
9
|
+
*
|
|
10
|
+
* - own card with an integrity_mode → keep it (off stays off; nudge/enforce stay put)
|
|
11
|
+
* - own card without the field → observe (what the card already resolved to)
|
|
12
|
+
* - no own card → off (never silently opt an agent in to billed analysis)
|
|
13
|
+
*
|
|
14
|
+
* A newly born agent has its own card: mnemom-api stamps an observe birth card (ADR-API-003), and
|
|
15
|
+
* the gateway's card-less bootstrap writes observe too, so a new agent keeps observe (MNE-9728).
|
|
16
|
+
*
|
|
17
|
+
* THROWS when the read fails (anything but a 404): the caller then skips the card write rather
|
|
18
|
+
* than guess a mode in either direction.
|
|
19
|
+
*/
|
|
20
|
+
import { getCardSources } from "./api.js";
|
|
21
|
+
export async function starterIntegrityMode(agentId) {
|
|
22
|
+
const sources = await getCardSources("alignment", agentId);
|
|
23
|
+
const own = sources?.agent?.card_json ?? null;
|
|
24
|
+
if (!own)
|
|
25
|
+
return "off";
|
|
26
|
+
const mode = own.integrity_mode;
|
|
27
|
+
return typeof mode === "string" && mode ? mode : "observe";
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mnemom/mnemom",
|
|
3
|
-
"version": "0.17.38-next.
|
|
3
|
+
"version": "0.17.38-next.1",
|
|
4
4
|
"description": "Transparent AI agent tracing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@mnemom/policy-engine": "^0.3.0",
|
|
22
|
-
"@mnemom/verify": "0.6.
|
|
22
|
+
"@mnemom/verify": "0.6.3",
|
|
23
23
|
"chalk": "^5.3.0",
|
|
24
24
|
"commander": "^12.0.0",
|
|
25
25
|
"js-yaml": "^4.3.2",
|