@mnemom/mnemom 0.17.36 → 0.17.38-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/dist/commands/agent-run.d.ts +11 -0
  2. package/dist/commands/agent-run.js +37 -0
  3. package/dist/commands/agent-share-render.d.ts +5 -5
  4. package/dist/commands/agent-share-render.js +9 -7
  5. package/dist/commands/agent-share-test-issuer.d.ts +1 -1
  6. package/dist/commands/agent-share-test-issuer.js +1 -1
  7. package/dist/commands/agent.d.ts +8 -1
  8. package/dist/commands/agent.js +14 -3
  9. package/dist/commands/scan.d.ts +15 -2
  10. package/dist/commands/scan.js +48 -3
  11. package/dist/host/commands.js +4 -1
  12. package/dist/host/owner.d.ts +2 -0
  13. package/dist/host/verbs.d.ts +16 -1
  14. package/dist/host/verbs.js +45 -3
  15. package/dist/index.js +1 -1
  16. package/dist/lib/commit-report.d.ts +7 -13
  17. package/dist/lib/commit-report.js +12 -5
  18. package/dist/lib/conduct-receipt.d.ts +8 -3
  19. package/dist/lib/conduct-receipt.js +29 -7
  20. package/dist/lib/experimental.d.ts +1 -1
  21. package/dist/lib/experimental.js +1 -12
  22. package/dist/lib/runner/executor-client.d.ts +5 -0
  23. package/dist/lib/runner/executor-client.js +8 -0
  24. package/dist/lib/runner/job.d.ts +37 -0
  25. package/dist/lib/runner/job.js +74 -1
  26. package/dist/lib/runner/phases.d.ts +7 -2
  27. package/dist/lib/runner/phases.js +7 -2
  28. package/dist/lib/runner/proxy.d.ts +26 -2
  29. package/dist/lib/runner/proxy.js +33 -1
  30. package/dist/lib/runner/record-commit.d.ts +16 -0
  31. package/dist/lib/runner/record-commit.js +7 -0
  32. package/dist/lib/scan/spend/anthropic-usage.d.ts +20 -1
  33. package/dist/lib/scan/spend/anthropic-usage.js +32 -7
  34. package/dist/lib/scan/spend/anthropic-wif.d.ts +42 -0
  35. package/dist/lib/scan/spend/anthropic-wif.js +153 -0
  36. package/dist/vendor/durable-contract/index.js +6 -3
  37. package/dist/vendor/shared/durable-contract/cli-surface.js +2 -2
  38. package/dist/vendor/shared/durable-contract/codes.js +4 -1
  39. package/package.json +2 -5
  40. package/node_modules/@mnemom/aip-verifier/LICENSE +0 -190
  41. package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.cts +0 -478
  42. package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.ts +0 -478
  43. package/node_modules/@mnemom/aip-verifier/dist/index.cjs +0 -971
  44. package/node_modules/@mnemom/aip-verifier/dist/index.cjs.map +0 -1
  45. package/node_modules/@mnemom/aip-verifier/dist/index.d.cts +0 -330
  46. package/node_modules/@mnemom/aip-verifier/dist/index.d.ts +0 -330
  47. package/node_modules/@mnemom/aip-verifier/dist/index.js +0 -933
  48. package/node_modules/@mnemom/aip-verifier/dist/index.js.map +0 -1
  49. package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs +0 -879
  50. package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs.map +0 -1
  51. package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.cts +0 -96
  52. package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.ts +0 -96
  53. package/node_modules/@mnemom/aip-verifier/dist/render-lines.js +0 -875
  54. package/node_modules/@mnemom/aip-verifier/dist/render-lines.js.map +0 -1
  55. package/node_modules/@mnemom/aip-verifier/package.json +0 -86
  56. package/node_modules/@noble/ed25519/LICENSE +0 -21
  57. package/node_modules/@noble/ed25519/README.md +0 -371
  58. package/node_modules/@noble/ed25519/index.d.ts +0 -391
  59. package/node_modules/@noble/ed25519/index.js +0 -933
  60. package/node_modules/@noble/ed25519/index.ts +0 -1134
  61. package/node_modules/@noble/ed25519/package.json +0 -54
  62. package/node_modules/@noble/hashes/LICENSE +0 -21
  63. package/node_modules/@noble/hashes/README.md +0 -478
  64. package/node_modules/@noble/hashes/_blake.d.ts +0 -20
  65. package/node_modules/@noble/hashes/_blake.js +0 -59
  66. package/node_modules/@noble/hashes/_md.d.ts +0 -95
  67. package/node_modules/@noble/hashes/_md.js +0 -209
  68. package/node_modules/@noble/hashes/_u64.d.ts +0 -36
  69. package/node_modules/@noble/hashes/_u64.js +0 -77
  70. package/node_modules/@noble/hashes/argon2.d.ts +0 -148
  71. package/node_modules/@noble/hashes/argon2.js +0 -577
  72. package/node_modules/@noble/hashes/blake1.d.ts +0 -162
  73. package/node_modules/@noble/hashes/blake1.js +0 -630
  74. package/node_modules/@noble/hashes/blake2.d.ts +0 -203
  75. package/node_modules/@noble/hashes/blake2.js +0 -573
  76. package/node_modules/@noble/hashes/blake3.d.ts +0 -68
  77. package/node_modules/@noble/hashes/blake3.js +0 -310
  78. package/node_modules/@noble/hashes/eskdf.d.ts +0 -91
  79. package/node_modules/@noble/hashes/eskdf.js +0 -225
  80. package/node_modules/@noble/hashes/hkdf.d.ts +0 -70
  81. package/node_modules/@noble/hashes/hkdf.js +0 -152
  82. package/node_modules/@noble/hashes/hmac.d.ts +0 -50
  83. package/node_modules/@noble/hashes/hmac.js +0 -93
  84. package/node_modules/@noble/hashes/index.d.ts +0 -1
  85. package/node_modules/@noble/hashes/index.js +0 -34
  86. package/node_modules/@noble/hashes/legacy.d.ts +0 -107
  87. package/node_modules/@noble/hashes/legacy.js +0 -348
  88. package/node_modules/@noble/hashes/package.json +0 -91
  89. package/node_modules/@noble/hashes/pbkdf2.d.ts +0 -66
  90. package/node_modules/@noble/hashes/pbkdf2.js +0 -176
  91. package/node_modules/@noble/hashes/scrypt.d.ts +0 -98
  92. package/node_modules/@noble/hashes/scrypt.js +0 -304
  93. package/node_modules/@noble/hashes/sha2.d.ts +0 -176
  94. package/node_modules/@noble/hashes/sha2.js +0 -446
  95. package/node_modules/@noble/hashes/sha3-addons.d.ts +0 -533
  96. package/node_modules/@noble/hashes/sha3-addons.js +0 -973
  97. package/node_modules/@noble/hashes/sha3.d.ts +0 -208
  98. package/node_modules/@noble/hashes/sha3.js +0 -475
  99. package/node_modules/@noble/hashes/src/_blake.ts +0 -67
  100. package/node_modules/@noble/hashes/src/_md.ts +0 -227
  101. package/node_modules/@noble/hashes/src/_u64.ts +0 -111
  102. package/node_modules/@noble/hashes/src/argon2.ts +0 -693
  103. package/node_modules/@noble/hashes/src/blake1.ts +0 -677
  104. package/node_modules/@noble/hashes/src/blake2.ts +0 -648
  105. package/node_modules/@noble/hashes/src/blake3.ts +0 -344
  106. package/node_modules/@noble/hashes/src/eskdf.ts +0 -265
  107. package/node_modules/@noble/hashes/src/hkdf.ts +0 -161
  108. package/node_modules/@noble/hashes/src/hmac.ts +0 -130
  109. package/node_modules/@noble/hashes/src/index.ts +0 -33
  110. package/node_modules/@noble/hashes/src/legacy.ts +0 -361
  111. package/node_modules/@noble/hashes/src/pbkdf2.ts +0 -219
  112. package/node_modules/@noble/hashes/src/scrypt.ts +0 -375
  113. package/node_modules/@noble/hashes/src/sha2.ts +0 -486
  114. package/node_modules/@noble/hashes/src/sha3-addons.ts +0 -1157
  115. package/node_modules/@noble/hashes/src/sha3.ts +0 -525
  116. package/node_modules/@noble/hashes/src/utils.ts +0 -978
  117. package/node_modules/@noble/hashes/src/webcrypto.ts +0 -283
  118. package/node_modules/@noble/hashes/utils.d.ts +0 -560
  119. package/node_modules/@noble/hashes/utils.js +0 -687
  120. package/node_modules/@noble/hashes/webcrypto.d.ts +0 -121
  121. package/node_modules/@noble/hashes/webcrypto.js +0 -228
@@ -20,6 +20,7 @@ import { type GithubBases } from "../lib/runner/github-base.js";
20
20
  import type { JobDeps, Workspace } from "../lib/runner/job.js";
21
21
  import { type PhaseInvocation } from "../lib/runner/phases.js";
22
22
  import { type RunFlags } from "../lib/runner/options.js";
23
+ import { type RecordCommitBody } from "../lib/commit-report.js";
23
24
  export interface RunDeps {
24
25
  env: NodeJS.ProcessEnv;
25
26
  fetch: FetchFn;
@@ -55,6 +56,16 @@ export declare function prepareWorkspace(git: Git, cloneUrl: string, sessionId:
55
56
  export declare function cleanupWorkspace(ws: Workspace): Promise<void>;
56
57
  /** Commits on the job branch that no origin ref has: the clone's base is origin's default branch. */
57
58
  export declare function commitsAhead(git: Git, ws: Workspace): Promise<number>;
59
+ /**
60
+ * The working copy's HEAD; null only when the clone has no commit at all. Any other failure throws,
61
+ * so the job never mistakes an unreadable HEAD for "since the base" and re-reports earlier phases.
62
+ */
63
+ export declare function headSha(git: Git, ws: Workspace): Promise<string | null>;
64
+ /**
65
+ * RecordCommit/v1 bodies for the commits since `since` (null: since the base), oldest first. The
66
+ * repo is the job's: on a local lane origin is the fake GitHub, not github.com.
67
+ */
68
+ export declare function commitBodies(git: Git, ws: Workspace, since: string | null, lineageKey: string, repo: string): Promise<RecordCommitBody[]>;
58
69
  /** Push the job branch and open its PR against the repo's default branch; resolves its URL. */
59
70
  export declare function openPr(git: Git, fetch: FetchFn, token: string | null, repo: string, ws: Workspace, title: string, body: string, apiBase?: string): Promise<string>;
60
71
  /** merge_on_green: GitHub's auto-merge on the PR (squash). */
@@ -31,6 +31,7 @@ import { exitCodeFor, outcomeLine, refusedOutcome, USAGE_EXIT_CODE, } from "../l
31
31
  import { phaseEnv } from "../lib/runner/phases.js";
32
32
  import { resolveRunPlan } from "../lib/runner/options.js";
33
33
  import { startModelProxy } from "../lib/runner/proxy.js";
34
+ import { commitBody, realGit } from "../lib/commit-report.js";
34
35
  import { resolveGithubToken } from "./agent-fuel.js";
35
36
  import { executorStatePath } from "../vendor/durable-contract/index.js";
36
37
  /** The refusal of a runner with no ANTHROPIC_API_KEY (34.30.1); a config error, exit 2. */
@@ -165,6 +166,40 @@ export async function commitsAhead(git, ws) {
165
166
  throw new Error(`git rev-list answered ${n}`);
166
167
  return n;
167
168
  }
169
+ /**
170
+ * The working copy's HEAD; null only when the clone has no commit at all. Any other failure throws,
171
+ * so the job never mistakes an unreadable HEAD for "since the base" and re-reports earlier phases.
172
+ */
173
+ export async function headSha(git, ws) {
174
+ let sha;
175
+ try {
176
+ sha = await git(ws.dir, ["rev-parse", "--verify", "--quiet", "HEAD"]);
177
+ }
178
+ catch (e) {
179
+ if ((await git(ws.dir, ["rev-list", "-n", "1", "--all"])) === "")
180
+ return null;
181
+ throw e;
182
+ }
183
+ if (!/^[0-9a-f]{40}$/.test(sha))
184
+ throw new Error(`git rev-parse answered ${sha.slice(0, 40)}`);
185
+ return sha;
186
+ }
187
+ /**
188
+ * RecordCommit/v1 bodies for the commits since `since` (null: since the base), oldest first. The
189
+ * repo is the job's: on a local lane origin is the fake GitHub, not github.com.
190
+ */
191
+ export async function commitBodies(git, ws, since, lineageKey, repo) {
192
+ const range = since === null ? ["HEAD", "--not", "--remotes=origin"] : [`${since}..HEAD`];
193
+ const out = await git(ws.dir, ["rev-list", "--reverse", ...range]);
194
+ const local = realGit(ws.dir);
195
+ const bodies = [];
196
+ for (const sha of out.split("\n").filter(Boolean)) {
197
+ const built = commitBody(local, sha, lineageKey, repo);
198
+ if (built.ok)
199
+ bodies.push(built.body);
200
+ }
201
+ return bodies;
202
+ }
168
203
  /** Push the job branch and open its PR against the repo's default branch; resolves its URL. */
169
204
  export async function openPr(git, fetch, token, repo, ws, title, body, apiBase = GITHUB_BASES.api) {
170
205
  await git(ws.dir, ["push", "--quiet", "origin", `HEAD:refs/heads/${ws.branch}`]);
@@ -239,6 +274,8 @@ export function liveJobDeps(api, opts) {
239
274
  runPhase: (inv, ws, modelUrl, signal) => runPhaseProcess(inv, ws, modelUrl, signal, opts.env),
240
275
  openPr: (repo, ws, title, body) => openPr(git, opts.fetch, token, repo, ws, title, body, gh.api),
241
276
  commitsAhead: (ws) => commitsAhead(git, ws),
277
+ headSha: (ws) => headSha(git, ws),
278
+ commitBodies: (ws, since, lineageKey, repo) => commitBodies(git, ws, since, lineageKey, repo),
242
279
  enableAutoMerge: (prUrl) => enableAutoMerge(opts.fetch, token, prUrl, gh),
243
280
  cleanup: cleanupWorkspace,
244
281
  };
@@ -1,13 +1,13 @@
1
1
  /**
2
- * Bridge from `mnemom agent share` to the aip verifier (`@mnemom/aip-verifier`, aip#192/#178):
2
+ * Bridge from `mnemom agent share` to the aip verifier (`@mnemom/verify`, aip#192/#178):
3
3
  * the renderer (`renderLines`) and claim-lint (`lintRendering`) are the ONLY source of what a
4
- * card may say. No word lists live here. The package is a runtime dependency pinned to the aip
5
- * release tarball and is loaded lazily, so the default CLI path never touches it; if it is
4
+ * card may say. No word lists live here. The package is a runtime dependency pinned to an exact
5
+ * npm registry version and is loaded lazily, so the default CLI path never touches it; if it is
6
6
  * absent at runtime the share command fails closed with a clear message.
7
7
  */
8
8
  import type { LineRenderer, SharedCardLine } from "./agent-share.js";
9
- import type * as AipMain from "@mnemom/aip-verifier";
10
- import type * as AipRender from "@mnemom/aip-verifier/render-lines";
9
+ import type * as AipMain from "@mnemom/verify";
10
+ import type * as AipRender from "@mnemom/verify/render-lines";
11
11
  export interface VerifierModules {
12
12
  lintRendering: typeof AipMain.lintRendering;
13
13
  renderLines: typeof AipRender.renderLines;
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Bridge from `mnemom agent share` to the aip verifier (`@mnemom/aip-verifier`, aip#192/#178):
2
+ * Bridge from `mnemom agent share` to the aip verifier (`@mnemom/verify`, aip#192/#178):
3
3
  * the renderer (`renderLines`) and claim-lint (`lintRendering`) are the ONLY source of what a
4
- * card may say. No word lists live here. The package is a runtime dependency pinned to the aip
5
- * release tarball and is loaded lazily, so the default CLI path never touches it; if it is
4
+ * card may say. No word lists live here. The package is a runtime dependency pinned to an exact
5
+ * npm registry version and is loaded lazily, so the default CLI path never touches it; if it is
6
6
  * absent at runtime the share command fails closed with a clear message.
7
7
  */
8
8
  const defaultLoader = async () => {
9
9
  const [main, render] = await Promise.all([
10
- import("@mnemom/aip-verifier"),
11
- import("@mnemom/aip-verifier/render-lines"),
10
+ import("@mnemom/verify"),
11
+ import("@mnemom/verify/render-lines"),
12
12
  ]);
13
13
  return { lintRendering: main.lintRendering, renderLines: render.renderLines };
14
14
  };
@@ -20,9 +20,11 @@ async function loadVerifier(load) {
20
20
  catch (error) {
21
21
  const e = error;
22
22
  if (e.code === "ERR_MODULE_NOT_FOUND" || e.code === "MODULE_NOT_FOUND") {
23
- throw new Error("the aip verifier (@mnemom/aip-verifier) is not installed: it ships bundled inside @mnemom/mnemom, so reinstall with `npm install -g @mnemom/mnemom`", { cause: error });
23
+ throw new Error("the aip verifier (@mnemom/verify) is not installed: it is a dependency of @mnemom/mnemom, so reinstall with `npm install -g @mnemom/mnemom`", { cause: error });
24
24
  }
25
- throw new Error(`the aip verifier (@mnemom/aip-verifier) failed to load: ${e.message ?? error}`, { cause: error });
25
+ throw new Error(`the aip verifier (@mnemom/verify) failed to load: ${e.message ?? error}`, {
26
+ cause: error,
27
+ });
26
28
  }
27
29
  }
28
30
  /**
@@ -23,7 +23,7 @@
23
23
  * TEST_SHARECARD_PUBLIC_KEY_HEX / TEST_SHARECARD_JWKS below, which a verifier must be given
24
24
  * explicitly as the trusted key; the production JWKS never contains it.
25
25
  */
26
- import type * as AipVerifier from "@mnemom/aip-verifier";
26
+ import type * as AipVerifier from "@mnemom/verify";
27
27
  import type { AgentShareDeps, CardIssuer } from "./agent-share.js";
28
28
  /** aip test key id; ids of every test key start with this prefix. */
29
29
  export declare const TEST_KEY_ID = "test-sharecard-key-1";
@@ -80,7 +80,7 @@ const PKCS8_ED25519_PREFIX = Buffer.from("302e020100300506032b657004220420", "he
80
80
  export function createTestCardIssuer(opts = {}) {
81
81
  const now = opts.now ?? (() => new Date());
82
82
  const cardId = opts.cardId ?? (() => `card-test-${randomBytes(6).toString("hex")}`);
83
- const load = opts.loadVerifier ?? (() => import("@mnemom/aip-verifier"));
83
+ const load = opts.loadVerifier ?? (() => import("@mnemom/verify"));
84
84
  return {
85
85
  async issue(lines) {
86
86
  const { shareCardSigningInput } = await load();
@@ -401,11 +401,18 @@ export declare function fedStatuslineWanted(version: string): boolean;
401
401
  */
402
402
  export declare function exemptLoopbackFromProxy(env: NodeJS.ProcessEnv): void;
403
403
  /**
404
- * Print the conduct receipt (experimental `record`) — the exit call site here, and the one a
404
+ * Print the conduct receipt (on by default; MNEMOM_AGENT_RECEIPT=0 opts out) — the exit call site here, and the one a
405
405
  * durable park calls. Never throws: a failure is one warning line, and the caller's exit code
406
406
  * is untouched.
407
407
  */
408
408
  export declare function sayConductReceipt(launch: ReceiptLaunch, trigger?: "exit" | "park", receipt?: typeof conductReceiptFor): Promise<void>;
409
+ /**
410
+ * Whether `mnemom agent` reads and prints the conduct receipt at exit (MNE-9013: on by default,
411
+ * graduated from experimental `record`; MNEMOM_AGENT_RECEIPT=0 opts out). Claude only: the
412
+ * receipt reads the lineage key the base-URL proxy captures, and that proxy fronts
413
+ * ANTHROPIC_BASE_URL alone, so another CLI would only gain a proxy that can capture nothing.
414
+ */
415
+ export declare function receiptWanted(env: NodeJS.ProcessEnv): boolean;
409
416
  /** Whether a terminal implicit launch runs the turn-origin proxy (MNEMOM_AGENT_TURN_HEADER=0 opts out). */
410
417
  export declare function turnProxyWanted(env: NodeJS.ProcessEnv): boolean;
411
418
  /** The flag that lets base-url-proxy.mjs use an http door on a loopback literal (C17-R16 T3). */
@@ -847,7 +847,7 @@ export async function agentCommand(scenarioArg, options = {}) {
847
847
  if (shape === "terminal") {
848
848
  // Record keys the lineage under the served agent's org, not the account org (lib/record-org).
849
849
  const recordOrgId = recordOrgWanted({
850
- receipt: experimental.includes("record"),
850
+ receipt: cli.name === "claude" && receiptWanted(process.env),
851
851
  commitReport: cli.name === "claude" && commitReportEnabled(process.env[COMMIT_REPORT_ENV]),
852
852
  fedStatusline: cli.name === "claude" && fedStatuslineWanted(CLI_VERSION),
853
853
  }) && account.orgId
@@ -895,7 +895,8 @@ export async function agentCommand(scenarioArg, options = {}) {
895
895
  },
896
896
  }
897
897
  : {}),
898
- receipt: experimental.includes("record")
898
+ // On by default (MNE-9013); prints only when the org's record returns tally data.
899
+ receipt: cli.name === "claude" && Boolean(account.orgId) && receiptWanted(process.env)
899
900
  ? { orgId: recordOrgId, agentId: servedAgentId }
900
901
  : undefined,
901
902
  orgId: recordOrgId,
@@ -1671,7 +1672,7 @@ export function exemptLoopbackFromProxy(env) {
1671
1672
  env.no_proxy = [lower, "127.0.0.1,localhost"].filter(Boolean).join(",");
1672
1673
  }
1673
1674
  /**
1674
- * Print the conduct receipt (experimental `record`) — the exit call site here, and the one a
1675
+ * Print the conduct receipt (on by default; MNEMOM_AGENT_RECEIPT=0 opts out) — the exit call site here, and the one a
1675
1676
  * durable park calls. Never throws: a failure is one warning line, and the caller's exit code
1676
1677
  * is untouched.
1677
1678
  */
@@ -1684,6 +1685,16 @@ export async function sayConductReceipt(launch, trigger = "exit", receipt = cond
1684
1685
  say(fmt.warn(`mnemom agent: no conduct receipt (${err instanceof Error ? err.message : String(err)})`));
1685
1686
  }
1686
1687
  }
1688
+ /**
1689
+ * Whether `mnemom agent` reads and prints the conduct receipt at exit (MNE-9013: on by default,
1690
+ * graduated from experimental `record`; MNEMOM_AGENT_RECEIPT=0 opts out). Claude only: the
1691
+ * receipt reads the lineage key the base-URL proxy captures, and that proxy fronts
1692
+ * ANTHROPIC_BASE_URL alone, so another CLI would only gain a proxy that can capture nothing.
1693
+ */
1694
+ export function receiptWanted(env) {
1695
+ const raw = (env.MNEMOM_AGENT_RECEIPT ?? "").trim().toLowerCase();
1696
+ return !["0", "off", "false", "no"].includes(raw);
1697
+ }
1687
1698
  /** Whether a terminal implicit launch runs the turn-origin proxy (MNEMOM_AGENT_TURN_HEADER=0 opts out). */
1688
1699
  export function turnProxyWanted(env) {
1689
1700
  const raw = (env.MNEMOM_AGENT_TURN_HEADER ?? "").trim().toLowerCase();
@@ -7,7 +7,8 @@
7
7
  import { type GitHubCredential } from "../lib/scan/credential.js";
8
8
  import { type ScanDeps, type ScanResult } from "../lib/scan/scan.js";
9
9
  import { selectSpend } from "../lib/scan/spend/spend-tiers.js";
10
- import { type AnthropicUsageDeps } from "../lib/scan/spend/anthropic-usage.js";
10
+ import { type AnthropicUsageDeps, type MeasuredUsage } from "../lib/scan/spend/anthropic-usage.js";
11
+ import { type WifDeps } from "../lib/scan/spend/anthropic-wif.js";
11
12
  import { type RecordDeps, type RecordOutcome } from "../lib/scan/record.js";
12
13
  import { type CostSpend } from "../lib/scan/cost-per-pr.js";
13
14
  import { type ReportEmailDeps } from "../lib/scan/report-email.js";
@@ -33,6 +34,8 @@ export interface ScanCommandOptions {
33
34
  anthropicOver200kShare?: number | null;
34
35
  /** Test seam: the fetch behind the two Anthropic reads. */
35
36
  anthropicDeps?: AnthropicUsageDeps;
37
+ /** Test seam: the WIF assertion mint and Anthropic token exchange (MNE-9706). */
38
+ wifDeps?: WifDeps;
36
39
  /**
37
40
  * The org the report is emailed for; inferred from the signed-in user's single non-personal org when
38
41
  * omitted (see resolveScanOrg). The Record totals preview (MNEMOM_ORG_SCAN_MODE=true) reads ONLY an
@@ -93,10 +96,20 @@ export declare const TIER1_FALLBACK_NOTE = "Tier 1 skipped: falling back to the
93
96
  * fixed sentence: the key, the thrown value and the response never reach the terminal. The caller then falls
94
97
  * back to tier 2, then tier 3.
95
98
  */
99
+ export declare const WIF_OK_NOTE = "Read Anthropic usage through workload identity federation.";
100
+ /**
101
+ * Tier 1 through WIF (MNE-9706): Mnemom issues an identity token for the org, this machine exchanges it at
102
+ * Anthropic and reads only the two usage reports with the result. "not_configured" when the WIF ids are
103
+ * not set; "failed" (after printing a fixed sentence) for every failure, so the caller falls back to the key.
104
+ */
105
+ export declare function measuredUsageViaWif(o: ScanCommandOptions, orgId: string, period: {
106
+ start: string;
107
+ end: string;
108
+ }): Promise<MeasuredUsage | "not_configured" | "failed">;
96
109
  export declare function resolveMeasuredUsage(o: ScanCommandOptions, period: {
97
110
  start: string;
98
111
  end: string;
99
- }): Promise<{
112
+ }, orgId?: string): Promise<{
100
113
  usd: number;
101
114
  over200kShare: number | null;
102
115
  } | null>;
@@ -9,7 +9,8 @@ import { failureText } from "../lib/scan/failure.js";
9
9
  import { runLocalScan } from "../lib/scan/scan.js";
10
10
  import { askInput, askSelect } from "../lib/prompt.js";
11
11
  import { isSpendTiersEnabled, NOT_ENABLED_MSG, parseMonthlySpendUsd, selectSpend, SPEND_INVALID_MSG, SPEND_NO_TERMINAL_MSG, } from "../lib/scan/spend/spend-tiers.js";
12
- import { ADMIN_KEY_ENV, AnthropicUsageError, fetchMeasuredUsage, isAdminKeyShape, usageFailureText, } from "../lib/scan/spend/anthropic-usage.js";
12
+ import { ADMIN_KEY_ENV, AnthropicUsageError, fetchMeasuredUsage, isAdminKeyShape, usageFailureText, bearerAuth, fetchMeasuredUsageWithAuth, } from "../lib/scan/spend/anthropic-usage.js";
13
+ import { obtainWifAccessToken, readWifConfig, WIF_FALLBACK_NOTE, WifError, wifFailureText, } from "../lib/scan/spend/anthropic-wif.js";
13
14
  import { getAuthInfo } from "../lib/auth.js";
14
15
  import { listMyOrgs } from "../lib/api.js";
15
16
  import { getActiveOrg } from "../lib/cli-config.js";
@@ -96,7 +97,37 @@ export const TIER1_FALLBACK_NOTE = "Tier 1 skipped: falling back to the next tie
96
97
  * fixed sentence: the key, the thrown value and the response never reach the terminal. The caller then falls
97
98
  * back to tier 2, then tier 3.
98
99
  */
99
- export async function resolveMeasuredUsage(o, period) {
100
+ export const WIF_OK_NOTE = "Read Anthropic usage through workload identity federation.";
101
+ /**
102
+ * Tier 1 through WIF (MNE-9706): Mnemom issues an identity token for the org, this machine exchanges it at
103
+ * Anthropic and reads only the two usage reports with the result. "not_configured" when the WIF ids are
104
+ * not set; "failed" (after printing a fixed sentence) for every failure, so the caller falls back to the key.
105
+ */
106
+ export async function measuredUsageViaWif(o, orgId, period) {
107
+ let cfg;
108
+ try {
109
+ cfg = readWifConfig(o.env ?? process.env);
110
+ }
111
+ catch (e) {
112
+ console.log(wifFailureText(e instanceof WifError ? e.reason : "config_invalid"));
113
+ return "failed";
114
+ }
115
+ if (cfg === null)
116
+ return "not_configured";
117
+ try {
118
+ const token = await obtainWifAccessToken(orgId, cfg, o.wifDeps);
119
+ const usage = await fetchMeasuredUsageWithAuth(bearerAuth(token.accessToken), period, o.anthropicDeps);
120
+ console.log(WIF_OK_NOTE);
121
+ return usage;
122
+ }
123
+ catch (e) {
124
+ console.log(e instanceof WifError
125
+ ? wifFailureText(e.reason)
126
+ : usageFailureText(e instanceof AnthropicUsageError ? e.reason : "upstream"));
127
+ return "failed";
128
+ }
129
+ }
130
+ export async function resolveMeasuredUsage(o, period, orgId) {
100
131
  if (o.anthropicAdminKey && !isSpendTiersEnabled(o.env))
101
132
  throw new Error(NOT_ENABLED_MSG);
102
133
  if (!isSpendTiersEnabled(o.env) || o.noAnthropicUsage)
@@ -105,6 +136,20 @@ export async function resolveMeasuredUsage(o, period) {
105
136
  if (o.monthlySpend !== undefined && !o.anthropicAdminKey)
106
137
  return null;
107
138
  let key = (o.env ?? process.env)[ADMIN_KEY_ENV];
139
+ // MNE-9706: WIF first when it is set up, unless the user explicitly asked to paste a key.
140
+ let wifFailed = false;
141
+ if (!o.anthropicAdminKey && orgId !== undefined) {
142
+ const viaWif = await measuredUsageViaWif(o, orgId, period);
143
+ if (viaWif !== "not_configured" && viaWif !== "failed")
144
+ return viaWif;
145
+ wifFailed = viaWif === "failed";
146
+ }
147
+ if (wifFailed) {
148
+ const hasKey = key !== undefined && key.trim() !== "";
149
+ console.log(hasKey ? WIF_FALLBACK_NOTE : TIER1_FALLBACK_NOTE);
150
+ if (!hasKey)
151
+ return null;
152
+ }
108
153
  if (o.anthropicAdminKey) {
109
154
  if (!(o.isTty ?? process.stdin.isTTY))
110
155
  throw new Error(NO_ADMIN_KEY_TERMINAL_MSG);
@@ -218,7 +263,7 @@ export async function scanCommand(o) {
218
263
  const now = o.now ?? Date.now;
219
264
  const end = new Date(now()).toISOString().slice(0, 10);
220
265
  const period = o.period ?? { start: end, end };
221
- const measured = await resolveMeasuredUsage(o, period);
266
+ const measured = await resolveMeasuredUsage(o, period, emailOrg);
222
267
  const credential = pickedCredential ?? (await getCredential());
223
268
  // Before the local scan: a refused credential fails closed without spending the GitHub reads.
224
269
  const record = await resolveRecordOutcome(o);
@@ -8,6 +8,7 @@ import * as os from "node:os";
8
8
  import { CLI_VERSION } from "../version.js";
9
9
  import { getApiUrl } from "../lib/config.js";
10
10
  import { getAccessToken } from "../lib/auth.js";
11
+ import { getActiveOrg } from "../lib/cli-config.js";
11
12
  import { resolveBootId, defaultBootIdDeps } from "./bootid.js";
12
13
  import { realLaunchctl } from "./launchd.js";
13
14
  import { ownerApi, parseExecutorList } from "./owner.js";
@@ -37,6 +38,7 @@ function verbIo(json) {
37
38
  bootId: () => resolveBootId({ ...defaultBootIdDeps(), env }).bootId,
38
39
  programArguments: () => [...cliExec(), "agent", "host", "run"],
39
40
  claudeBin: () => resolveClaudeBin(env),
41
+ activeOrg: () => getActiveOrg()?.org_id ?? null,
40
42
  };
41
43
  }
42
44
  async function guarded(json, fn) {
@@ -66,7 +68,8 @@ export function registerHostCommands(agent, hidden) {
66
68
  host
67
69
  .command("enroll")
68
70
  .description("Enroll this machine as an executor (device step); `install` does this when needed")
69
- .action(async () => guarded(false, hostEnroll));
71
+ .option("--org <org_id>", "Join this organization (you must be a member); defaults to the active org, else your personal org")
72
+ .action(async (o) => guarded(false, (io) => hostEnroll(io, { org: o.org })));
70
73
  host
71
74
  .command("renew")
72
75
  .description("Renew this machine's executor grant (device step)")
@@ -11,6 +11,8 @@ export interface OwnerApi {
11
11
  runtime: string;
12
12
  contract_version: string;
13
13
  machine_label?: string;
14
+ /** Ruling 34.03.4: the org this host joins; the api checks membership. */
15
+ org_id?: string;
14
16
  }): Promise<OwnerHttp>;
15
17
  enrollPoll(pollSecret: string, approvalId: string | null): Promise<OwnerHttp>;
16
18
  list(): Promise<OwnerHttp>;
@@ -20,6 +20,8 @@ export interface VerbIo {
20
20
  readonly pollSleep?: (ms: number) => Promise<void>;
21
21
  /** Takes the enrollment lock; acquireHostLock (macOS flock) when absent. */
22
22
  readonly acquireLock?: (lockFile: string) => LockResult;
23
+ /** The CLI's active org id (`mnemom org use`), or null with none set; never read when absent. */
24
+ readonly activeOrg?: () => string | null;
23
25
  }
24
26
  /** Fresh-invocation gate for every host verb (C12-R25): exit 2 with the gated text, nothing else. */
25
27
  export declare function freshGate(io: VerbIo): number | null;
@@ -40,13 +42,26 @@ export declare function executorLiveness(io: VerbIo, executorId: string): Promis
40
42
  export declare function printRiskLine(io: VerbIo): void;
41
43
  /** "<org name> (<org_id>)", or the bare id when the api sent no name. */
42
44
  export declare function joinTarget(orgId: string, orgName: string | null): string;
45
+ /**
46
+ * The org a start names (ruling 34.03.4): `--org`, else the active org, else none, so the api
47
+ * keeps its fallback (the token's org, else the personal org).
48
+ */
49
+ export declare function enrollOrg(io: VerbIo, opts: {
50
+ org?: string;
51
+ }): {
52
+ orgId: string;
53
+ from: "flag" | "active";
54
+ } | null;
43
55
  /** The device step: start, show the code, poll, write the state file. Returns an exit code. */
44
56
  export declare function enrollDevice(io: VerbIo, opts?: {
45
57
  label?: string;
58
+ org?: string;
46
59
  }): Promise<number>;
47
60
  /** Write the plist and (re)load the job. */
48
61
  export declare function writeAndLoad(io: VerbIo): Promise<number>;
49
62
  export declare function hostInstall(io: VerbIo): Promise<number>;
50
- export declare function hostEnroll(io: VerbIo): Promise<number>;
63
+ export declare function hostEnroll(io: VerbIo, opts?: {
64
+ org?: string;
65
+ }): Promise<number>;
51
66
  export declare function hostRenew(io: VerbIo): Promise<number>;
52
67
  export declare function hostUninstall(io: VerbIo): Promise<number>;
@@ -23,6 +23,7 @@ import { renderPlist } from "./plist.js";
23
23
  import { loadJob, unloadJob } from "./launchd.js";
24
24
  import { classifyPoll, parseEnrollStart, parseExecutorList, EXECUTOR_CONTRACT, } from "./owner.js";
25
25
  import { readRuntime } from "./runtime.js";
26
+ import { CODE } from "./s1-names.js";
26
27
  import { RENEW_FIX, GRANT_EXPIRED_LINE } from "./grant.js";
27
28
  export const INSTALL_FIX = "`mnemom agent host install`";
28
29
  export const UNINSTALL_SESSION_LINE = (name) => `${name}: they stop restarting until ${INSTALL_FIX}`;
@@ -63,6 +64,22 @@ export function printRiskLine(io) {
63
64
  export function joinTarget(orgId, orgName) {
64
65
  return orgName ? `${orgName} (${orgId})` : orgId;
65
66
  }
67
+ /**
68
+ * The org a start names (ruling 34.03.4): `--org`, else the active org, else none, so the api
69
+ * keeps its fallback (the token's org, else the personal org).
70
+ */
71
+ export function enrollOrg(io, opts) {
72
+ if (opts.org)
73
+ return { orgId: opts.org, from: "flag" };
74
+ const active = io.activeOrg?.() ?? null;
75
+ return active ? { orgId: active, from: "active" } : null;
76
+ }
77
+ /** The api's refusal code from an error body, or null. */
78
+ function errorCode(body) {
79
+ const e = body && typeof body === "object" ? body.error : undefined;
80
+ const code = e && typeof e === "object" ? e.code : undefined;
81
+ return typeof code === "string" ? code : null;
82
+ }
66
83
  /** The device step: start, show the code, poll, write the state file. Returns an exit code. */
67
84
  export async function enrollDevice(io, opts = {}) {
68
85
  const { paths } = ctx(io);
@@ -71,16 +88,41 @@ export async function enrollDevice(io, opts = {}) {
71
88
  io.err(`mnemom agent host: an enrollment is already in progress here (${lock.reason}): wait for it, then ${INSTALL_FIX}`);
72
89
  return 1;
73
90
  }
91
+ let org = enrollOrg(io, opts);
74
92
  try {
75
- const st = await io.owner.enrollStart({
93
+ const startWith = (o) => io.owner.enrollStart({
76
94
  kind: "host",
77
95
  runtime: "node",
78
96
  contract_version: EXECUTOR_CONTRACT,
79
97
  ...(opts.label ? { machine_label: opts.label } : {}),
98
+ ...(o ? { org_id: o.orgId } : {}),
80
99
  });
100
+ let st = await startWith(org);
101
+ // An api from before 34.03.4 refuses org_id on a host start with its plain 400 invalid_scope
102
+ // (34.03.2 (1); mnemom-api enroll.ts "org_id is only accepted with scope org"; recorded beside
103
+ // exec_enroll in S1 codes.ts). An org taken from the active org then falls back to the start this
104
+ // CLI made before; an explicit --org cannot.
105
+ if (org && st.status === 400 && errorCode(st.body) === "invalid_scope") {
106
+ if (org.from === "flag") {
107
+ io.err(`mnemom agent host: this Mnemom server cannot enroll a machine into ${org.orgId} yet.`);
108
+ io.err("fix: `mnemom agent host enroll` without --org (joins your personal org)");
109
+ return 1;
110
+ }
111
+ io.err(`mnemom agent host: this Mnemom server cannot enroll a machine into your active org ${org.orgId} yet; it joins your personal org.`);
112
+ org = null;
113
+ st = await startWith(null);
114
+ }
81
115
  const start = st.status === 202 || st.status === 200 || st.status === 201
82
116
  ? parseEnrollStart(st.body)
83
117
  : null;
118
+ if (!start && org && errorCode(st.body) === CODE.durable_approval_not_eligible) {
119
+ // 34.03.4: only a member of the org may enroll a machine into it.
120
+ io.err(`mnemom agent host: you are not a member of ${org.orgId}, so this machine cannot join it.`);
121
+ io.err(org.from === "flag"
122
+ ? "fix: `mnemom org list` shows the organizations you belong to"
123
+ : "fix: `mnemom org use --clear`, then `mnemom agent host enroll` (joins your personal org)");
124
+ return 1;
125
+ }
84
126
  if (!start) {
85
127
  const code = st.body && typeof st.body === "object"
86
128
  ? JSON.stringify(st.body).slice(0, 300)
@@ -211,8 +253,8 @@ export async function hostInstall(io) {
211
253
  io.out(reload ? "Host daemon reloaded on this machine's enrollment." : "Host daemon installed.");
212
254
  return 0;
213
255
  }
214
- export async function hostEnroll(io) {
215
- return enrollDevice(io);
256
+ export async function hostEnroll(io, opts = {}) {
257
+ return enrollDevice(io, opts);
216
258
  }
217
259
  export async function hostRenew(io) {
218
260
  const { paths } = ctx(io);
package/dist/index.js CHANGED
@@ -2390,7 +2390,7 @@ program
2390
2390
  .option("--share <level>", "summary | full | none (default: full when asked; none when not interactive)")
2391
2391
  .option("--cto-email <email>", "CTO who receives the report (required unless you are the CTO; asked when omitted)")
2392
2392
  .option("--monthly-spend <usd>", "Your own approximate monthly AI spend (self-reported; never sent or saved; needs MNEMOM_ORG_SCAN_MODE=true)")
2393
- .option("--anthropic-admin-key", "Ask (masked) for your Anthropic admin key and read measured usage with it, locally; or set ANTHROPIC_ADMIN_KEY. Never stored, logged or sent anywhere but api.anthropic.com (needs MNEMOM_ORG_SCAN_MODE=true)")
2393
+ .option("--anthropic-admin-key", "Ask (masked) for your Anthropic admin key and read measured usage with it, locally; or set ANTHROPIC_ADMIN_KEY. Never stored, logged or sent anywhere but api.anthropic.com. 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
2394
  .option("--no-anthropic-usage", "Decline Anthropic usage and be asked for a monthly figure (needs MNEMOM_ORG_SCAN_MODE=true)")
2395
2395
  .option("--org <org_id>", "Your org id; the report is emailed to you for this org (default: your only organization, else the scan stops and asks). Also selects the Record totals preview (needs MNEMOM_ORG_SCAN_MODE=true)")
2396
2396
  .action(async (options) => {
@@ -51,17 +51,8 @@ export declare function attributionSettings(conversationId: string, user: string
51
51
  };
52
52
  /** `owner/name` from a GitHub remote URL (https, ssh or scp form), or null. */
53
53
  export declare function parseRepoSlug(url: string): string | null;
54
- /** The commit index body's schema tag (shared with the fleet worker's reporter). */
55
- export declare const COMMIT_SCHEMA = "RecordCommit/v1";
56
- /** RecordCommit/v1: strings only, every field required; the route rejects an unknown field. */
57
- export interface RecordCommitBody {
58
- schema: typeof COMMIT_SCHEMA;
59
- lineage_key: string;
60
- repo: string;
61
- branch: string;
62
- sha: string;
63
- patch_id: string;
64
- }
54
+ import { COMMIT_SCHEMA, type RecordCommitBody } from "./runner/record-commit.js";
55
+ export { COMMIT_SCHEMA, type RecordCommitBody };
65
56
  /** Runs git in the session's repository; null on any failure. */
66
57
  export type Git = (args: string[], input?: string) => string | null;
67
58
  export declare function realGit(cwd: string): Git;
@@ -71,8 +62,11 @@ export declare function realGit(cwd: string): Git;
71
62
  * teammate's fetched commit is never reported as this session's.
72
63
  */
73
64
  export declare function sessionCommits(git: Git, sinceSec: number): string[];
74
- /** The body for one commit, or the fields that could not be read. */
75
- export declare function commitBody(git: Git, sha: string, lineageKey: string): {
65
+ /**
66
+ * The body for one commit, or the fields that could not be read. `repo` overrides the slug read
67
+ * from `origin`: the runner knows its job's repo, and on a local lane `origin` is not github.com.
68
+ */
69
+ export declare function commitBody(git: Git, sha: string, lineageKey: string, repoOverride?: string): {
76
70
  ok: true;
77
71
  body: RecordCommitBody;
78
72
  } | {
@@ -65,8 +65,8 @@ export function parseRepoSlug(url) {
65
65
  const slug = `${m[1]}/${m[2]}`;
66
66
  return REPO_SLUG.test(slug) ? slug : null;
67
67
  }
68
- /** The commit index body's schema tag (shared with the fleet worker's reporter). */
69
- export const COMMIT_SCHEMA = "RecordCommit/v1";
68
+ import { COMMIT_SCHEMA } from "./runner/record-commit.js";
69
+ export { COMMIT_SCHEMA };
70
70
  export function realGit(cwd) {
71
71
  return (args, input) => {
72
72
  const r = spawnSync("git", args, { cwd, encoding: "utf8", input, timeout: 10_000 });
@@ -91,8 +91,11 @@ export function sessionCommits(git, sinceSec) {
91
91
  .filter(([sha, ce]) => sha !== undefined && HEX40.test(sha) && ce === email)
92
92
  .map(([sha]) => sha);
93
93
  }
94
- /** The body for one commit, or the fields that could not be read. */
95
- export function commitBody(git, sha, lineageKey) {
94
+ /**
95
+ * The body for one commit, or the fields that could not be read. `repo` overrides the slug read
96
+ * from `origin`: the runner knows its job's repo, and on a local lane `origin` is not github.com.
97
+ */
98
+ export function commitBody(git, sha, lineageKey, repoOverride) {
96
99
  const diff = git(["diff-tree", "-p", "--root", sha]);
97
100
  const patchId = diff
98
101
  ? (/^\s*([0-9a-f]{40})\b/.exec(git(["patch-id", "--stable"], diff) ?? "")?.[1] ?? "")
@@ -103,7 +106,11 @@ export function commitBody(git, sha, lineageKey) {
103
106
  .map((s) => s.trim())
104
107
  .filter(Boolean);
105
108
  const branch = holders.includes(current) ? current : (holders[0] ?? "");
106
- const repo = parseRepoSlug(git(["remote", "get-url", "origin"]) ?? "") ?? "";
109
+ const repo = repoOverride !== undefined
110
+ ? REPO_SLUG.test(repoOverride)
111
+ ? repoOverride
112
+ : ""
113
+ : (parseRepoSlug(git(["remote", "get-url", "origin"]) ?? "") ?? "");
107
114
  const missing = [];
108
115
  if (!HEX40.test(sha))
109
116
  missing.push("sha");
@@ -1,6 +1,9 @@
1
1
  /**
2
- * The conduct receipt (experimental `record`, Doors W1.2 / W1.3): the engineer's private view of
3
- * one session, printed when `mnemom agent` exits and when a durable session parks.
2
+ * The conduct receipt (Doors W1.2 / W1.3): the engineer's private view of one session, printed
3
+ * when `mnemom agent` exits and when a durable session parks. On by default since 0.17.38
4
+ * (MNE-9013; MNEMOM_AGENT_RECEIPT=0 opts out). It prints ONLY when the server returns tally data:
5
+ * an org outside the record cohort (404), a caller without the role (403), no captured lineage,
6
+ * or a read that never answers inside the deadline all print nothing.
4
7
  *
5
8
  * Conduct receipt
6
9
  * model cost $0.42
@@ -13,7 +16,7 @@
13
16
  *
14
17
  * NEVER BLOCKS EXIT. The whole read, auth refresh included, races a hard deadline
15
18
  * (RECEIPT_DEADLINE_MS): with the API or the record builder down, exit takes at most that much
16
- * longer, and the receipt falls back to the link. The caller keeps its exit code; nothing here
19
+ * longer, and the receipt prints nothing. The caller keeps its exit code; nothing here
17
20
  * throws.
18
21
  *
19
22
  * The lineage key is the gateway's (x-mnemom-lineage-key, set on every response), captured by the
@@ -35,6 +38,8 @@ export type TotalsRead = {
35
38
  muChargedMmu: bigint | null;
36
39
  } | {
37
40
  kind: "unavailable";
41
+ } | {
42
+ kind: "absent";
38
43
  };
39
44
  export interface ReceiptDeps {
40
45
  /** One totals read for the lineage; may throw or hang (both are "unavailable"). */