@mnemom/mnemom 0.17.37 → 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 (114) 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/index.js +1 -1
  12. package/dist/lib/commit-report.d.ts +7 -13
  13. package/dist/lib/commit-report.js +12 -5
  14. package/dist/lib/conduct-receipt.d.ts +8 -3
  15. package/dist/lib/conduct-receipt.js +29 -7
  16. package/dist/lib/experimental.d.ts +1 -1
  17. package/dist/lib/experimental.js +1 -12
  18. package/dist/lib/runner/executor-client.d.ts +5 -0
  19. package/dist/lib/runner/executor-client.js +8 -0
  20. package/dist/lib/runner/job.d.ts +37 -0
  21. package/dist/lib/runner/job.js +74 -1
  22. package/dist/lib/runner/phases.d.ts +7 -2
  23. package/dist/lib/runner/phases.js +7 -2
  24. package/dist/lib/runner/proxy.d.ts +26 -2
  25. package/dist/lib/runner/proxy.js +33 -1
  26. package/dist/lib/runner/record-commit.d.ts +16 -0
  27. package/dist/lib/runner/record-commit.js +7 -0
  28. package/dist/lib/scan/spend/anthropic-usage.d.ts +20 -1
  29. package/dist/lib/scan/spend/anthropic-usage.js +32 -7
  30. package/dist/lib/scan/spend/anthropic-wif.d.ts +42 -0
  31. package/dist/lib/scan/spend/anthropic-wif.js +153 -0
  32. package/package.json +2 -5
  33. package/node_modules/@mnemom/aip-verifier/LICENSE +0 -190
  34. package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.cts +0 -478
  35. package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.ts +0 -478
  36. package/node_modules/@mnemom/aip-verifier/dist/index.cjs +0 -971
  37. package/node_modules/@mnemom/aip-verifier/dist/index.cjs.map +0 -1
  38. package/node_modules/@mnemom/aip-verifier/dist/index.d.cts +0 -330
  39. package/node_modules/@mnemom/aip-verifier/dist/index.d.ts +0 -330
  40. package/node_modules/@mnemom/aip-verifier/dist/index.js +0 -933
  41. package/node_modules/@mnemom/aip-verifier/dist/index.js.map +0 -1
  42. package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs +0 -879
  43. package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs.map +0 -1
  44. package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.cts +0 -96
  45. package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.ts +0 -96
  46. package/node_modules/@mnemom/aip-verifier/dist/render-lines.js +0 -875
  47. package/node_modules/@mnemom/aip-verifier/dist/render-lines.js.map +0 -1
  48. package/node_modules/@mnemom/aip-verifier/package.json +0 -86
  49. package/node_modules/@noble/ed25519/LICENSE +0 -21
  50. package/node_modules/@noble/ed25519/README.md +0 -371
  51. package/node_modules/@noble/ed25519/index.d.ts +0 -391
  52. package/node_modules/@noble/ed25519/index.js +0 -933
  53. package/node_modules/@noble/ed25519/index.ts +0 -1134
  54. package/node_modules/@noble/ed25519/package.json +0 -54
  55. package/node_modules/@noble/hashes/LICENSE +0 -21
  56. package/node_modules/@noble/hashes/README.md +0 -478
  57. package/node_modules/@noble/hashes/_blake.d.ts +0 -20
  58. package/node_modules/@noble/hashes/_blake.js +0 -59
  59. package/node_modules/@noble/hashes/_md.d.ts +0 -95
  60. package/node_modules/@noble/hashes/_md.js +0 -209
  61. package/node_modules/@noble/hashes/_u64.d.ts +0 -36
  62. package/node_modules/@noble/hashes/_u64.js +0 -77
  63. package/node_modules/@noble/hashes/argon2.d.ts +0 -148
  64. package/node_modules/@noble/hashes/argon2.js +0 -577
  65. package/node_modules/@noble/hashes/blake1.d.ts +0 -162
  66. package/node_modules/@noble/hashes/blake1.js +0 -630
  67. package/node_modules/@noble/hashes/blake2.d.ts +0 -203
  68. package/node_modules/@noble/hashes/blake2.js +0 -573
  69. package/node_modules/@noble/hashes/blake3.d.ts +0 -68
  70. package/node_modules/@noble/hashes/blake3.js +0 -310
  71. package/node_modules/@noble/hashes/eskdf.d.ts +0 -91
  72. package/node_modules/@noble/hashes/eskdf.js +0 -225
  73. package/node_modules/@noble/hashes/hkdf.d.ts +0 -70
  74. package/node_modules/@noble/hashes/hkdf.js +0 -152
  75. package/node_modules/@noble/hashes/hmac.d.ts +0 -50
  76. package/node_modules/@noble/hashes/hmac.js +0 -93
  77. package/node_modules/@noble/hashes/index.d.ts +0 -1
  78. package/node_modules/@noble/hashes/index.js +0 -34
  79. package/node_modules/@noble/hashes/legacy.d.ts +0 -107
  80. package/node_modules/@noble/hashes/legacy.js +0 -348
  81. package/node_modules/@noble/hashes/package.json +0 -91
  82. package/node_modules/@noble/hashes/pbkdf2.d.ts +0 -66
  83. package/node_modules/@noble/hashes/pbkdf2.js +0 -176
  84. package/node_modules/@noble/hashes/scrypt.d.ts +0 -98
  85. package/node_modules/@noble/hashes/scrypt.js +0 -304
  86. package/node_modules/@noble/hashes/sha2.d.ts +0 -176
  87. package/node_modules/@noble/hashes/sha2.js +0 -446
  88. package/node_modules/@noble/hashes/sha3-addons.d.ts +0 -533
  89. package/node_modules/@noble/hashes/sha3-addons.js +0 -973
  90. package/node_modules/@noble/hashes/sha3.d.ts +0 -208
  91. package/node_modules/@noble/hashes/sha3.js +0 -475
  92. package/node_modules/@noble/hashes/src/_blake.ts +0 -67
  93. package/node_modules/@noble/hashes/src/_md.ts +0 -227
  94. package/node_modules/@noble/hashes/src/_u64.ts +0 -111
  95. package/node_modules/@noble/hashes/src/argon2.ts +0 -693
  96. package/node_modules/@noble/hashes/src/blake1.ts +0 -677
  97. package/node_modules/@noble/hashes/src/blake2.ts +0 -648
  98. package/node_modules/@noble/hashes/src/blake3.ts +0 -344
  99. package/node_modules/@noble/hashes/src/eskdf.ts +0 -265
  100. package/node_modules/@noble/hashes/src/hkdf.ts +0 -161
  101. package/node_modules/@noble/hashes/src/hmac.ts +0 -130
  102. package/node_modules/@noble/hashes/src/index.ts +0 -33
  103. package/node_modules/@noble/hashes/src/legacy.ts +0 -361
  104. package/node_modules/@noble/hashes/src/pbkdf2.ts +0 -219
  105. package/node_modules/@noble/hashes/src/scrypt.ts +0 -375
  106. package/node_modules/@noble/hashes/src/sha2.ts +0 -486
  107. package/node_modules/@noble/hashes/src/sha3-addons.ts +0 -1157
  108. package/node_modules/@noble/hashes/src/sha3.ts +0 -525
  109. package/node_modules/@noble/hashes/src/utils.ts +0 -978
  110. package/node_modules/@noble/hashes/src/webcrypto.ts +0 -283
  111. package/node_modules/@noble/hashes/utils.d.ts +0 -560
  112. package/node_modules/@noble/hashes/utils.js +0 -687
  113. package/node_modules/@noble/hashes/webcrypto.d.ts +0 -121
  114. 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);
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"). */
@@ -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,14 +16,14 @@
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
20
23
  * local base-URL proxy into a state file: the CLI never derives one itself.
21
24
  */
22
25
  import { readFileSync } from "node:fs";
23
- import { getRecordTotals } from "./api.js";
26
+ import { getRecordTotals, MnemomApiError } from "./api.js";
24
27
  import { getWebsiteUrl } from "./config.js";
25
28
  /** The hard ceiling on what the receipt adds to exit (W1.3: at most 2 s). */
26
29
  export const RECEIPT_DEADLINE_MS = 1800;
@@ -101,7 +104,7 @@ export function formatMu(mmu) {
101
104
  /** The receipt lines for what was read (pure). */
102
105
  export function receiptLines(read, link, trigger = "exit") {
103
106
  const head = trigger === "park" ? "Conduct receipt (parked)" : "Conduct receipt";
104
- if (!read || read.kind === "unavailable") {
107
+ if (!read || read.kind !== "ok") {
105
108
  return [head, ` record not available yet: ${link}`];
106
109
  }
107
110
  const lines = [head, ` model cost ${formatUsd(read.modelCostMmu)}`];
@@ -117,8 +120,9 @@ export function receiptLines(read, link, trigger = "exit") {
117
120
  */
118
121
  export async function buildConductReceipt(state, deps, trigger = "exit") {
119
122
  const link = state.recordUrl ?? deps.fallbackUrl;
123
+ // No lineage captured: nothing to read, so nothing to print.
120
124
  if (!state.lineageKey)
121
- return receiptLines(null, link, trigger);
125
+ return [];
122
126
  const now = deps.now ?? Date.now;
123
127
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
124
128
  const deadline = now() + (deps.deadlineMs ?? RECEIPT_DEADLINE_MS);
@@ -130,6 +134,10 @@ export async function buildConductReceipt(state, deps, trigger = "exit") {
130
134
  if (left <= 0)
131
135
  break;
132
136
  const read = await readWithin(deps.readTotals, lineageKey, left);
137
+ // Not in the cohort, or no role to read it: stop at once, no exit delay. An earlier good read
138
+ // (the cohort or role changed mid-poll) still prints; otherwise nothing does.
139
+ if (read.kind === "absent")
140
+ break;
133
141
  // A later failure never hides an earlier good read.
134
142
  if (read.kind === "ok" || !last)
135
143
  last = read;
@@ -139,6 +147,9 @@ export async function buildConductReceipt(state, deps, trigger = "exit") {
139
147
  break;
140
148
  await sleep(pollMs);
141
149
  }
150
+ // Only tally data prints: every read failing or timing out stays silent.
151
+ if (!last || last.kind !== "ok")
152
+ return [];
142
153
  return receiptLines(last, link, trigger);
143
154
  }
144
155
  async function readWithin(readTotals, lineageKey, ms) {
@@ -171,7 +182,18 @@ export async function conductReceiptFor(launch, trigger = "exit", deps = {}) {
171
182
  return buildConductReceipt(readRecordState(launch.statePath), {
172
183
  ...deps,
173
184
  readTotals: deps.readTotals ??
174
- (async (lineageKey, signal) => parseTotals(await getRecordTotals(launch.orgId, lineageKey, signal))),
185
+ (async (lineageKey, signal) => {
186
+ try {
187
+ return parseTotals(await getRecordTotals(launch.orgId, lineageKey, signal));
188
+ }
189
+ catch (err) {
190
+ // effectiveStatus, not status: the enforce hook may rewrite a 404 to a synthetic 500.
191
+ if (err instanceof MnemomApiError && [403, 404].includes(err.effectiveStatus)) {
192
+ return { kind: "absent" };
193
+ }
194
+ throw err;
195
+ }
196
+ }),
175
197
  fallbackUrl: deps.fallbackUrl ?? `${getWebsiteUrl()}/agents/${encodeURIComponent(launch.agentId)}`,
176
198
  }, trigger);
177
199
  }
@@ -12,7 +12,7 @@
12
12
  * Everything here is pure or dependency-injected; index.ts and the command
13
13
  * modules are the thin drivers.
14
14
  */
15
- export type ExperimentalId = "context-handoff" | "slack" | "implicit" | "measured-savings" | "record" | "fuel" | "durable" | "dispatch";
15
+ export type ExperimentalId = "context-handoff" | "slack" | "implicit" | "measured-savings" | "fuel" | "durable" | "dispatch";
16
16
  /**
17
17
  * Features that used to be experimental and are now on by default for everyone. Their ids are
18
18
  * no longer in the registry, but `mnemom experimental enable|disable <id>` still accepts them
@@ -24,6 +24,7 @@ export const GRADUATED_FEATURES = {
24
24
  since: "0.17.23",
25
25
  optOut: "--no-compaction-intercept, `compaction_intercept = false` in launcher.toml, or MNEMOM_AGENT_COMPACTION_INTERCEPT=0",
26
26
  },
27
+ record: { since: "0.17.38", optOut: "MNEMOM_AGENT_RECEIPT=0" },
27
28
  };
28
29
  /** The one-line note for `mnemom experimental enable|disable <graduated id>`; null otherwise. */
29
30
  export function graduatedNote(id) {
@@ -85,18 +86,6 @@ export const EXPERIMENTAL_FEATURES = [
85
86
  env: [],
86
87
  },
87
88
  },
88
- {
89
- // No flag, launcher.toml key or env of its own: `mnemom experimental enable` is the switch.
90
- id: "record",
91
- summary: "Print the conduct receipt at exit: the session's measured model cost and μ, or the record link",
92
- since: "0.17.32",
93
- gates: {
94
- flags: [],
95
- configKeys: [],
96
- commands: [],
97
- env: [],
98
- },
99
- },
100
89
  {
101
90
  id: "fuel",
102
91
  summary: "The fuel step: `mnemom agent fuel <file>` seals a batch of job contracts with one confirmation and commits them to the repo",
@@ -8,6 +8,7 @@
8
8
  * Nothing here decides an outcome: each call returns its status, body and error code, and the job
9
9
  * (job.ts) maps them to 34.12's reasons. Never throws: a network failure is status 0.
10
10
  */
11
+ import type { RecordCommitBody } from "./record-commit.js";
11
12
  import type { ExecAppendBody } from "./exec-append.js";
12
13
  export interface HttpResult {
13
14
  readonly status: number;
@@ -34,6 +35,8 @@ export declare const JOB_READ_PATH: (sessionId: string) => string;
34
35
  /** The served bundle fetch, per claim (W-L12). */
35
36
  export declare const BUNDLE_FETCH_PATH: (sessionId: string) => string;
36
37
  export declare const JWKS_PATH = "/v1/.well-known/jwks.json";
38
+ /** Record's commit index (ruling 34.05 (a)): the lease key may write the session's own commits. */
39
+ export declare const RECORD_COMMITS_PATH: (orgId: string) => string;
37
40
  export interface RunnerApi {
38
41
  readyList(limit: number): Promise<HttpResult>;
39
42
  attest(sessionId: string, nonce: string): Promise<HttpResult>;
@@ -63,6 +66,8 @@ export interface RunnerApi {
63
66
  job(sessionId: string): Promise<HttpResult>;
64
67
  bundle(sessionId: string, leaseKey: string): Promise<HttpResult>;
65
68
  jwks(): Promise<HttpResult>;
69
+ /** One RecordCommit/v1 body, with the lease key; the api pins it to the session's lineage. */
70
+ recordCommit(orgId: string, body: RecordCommitBody, leaseKey: string): Promise<HttpResult>;
66
71
  }
67
72
  export declare function runnerApi(opts: {
68
73
  apiUrl: string;
@@ -34,6 +34,9 @@ export const JOB_READ_PATH = (sessionId) => `/v1/executor/sessions/${sessionId}/
34
34
  /** The served bundle fetch, per claim (W-L12). */
35
35
  export const BUNDLE_FETCH_PATH = (sessionId) => `/v1/executor/sessions/${sessionId}/bundle`;
36
36
  export const JWKS_PATH = "/v1/.well-known/jwks.json";
37
+ /** Record's commit index (ruling 34.05 (a)): the lease key may write the session's own commits. */
38
+ export const RECORD_COMMITS_PATH = (orgId) => `/v1/orgs/${encodeURIComponent(orgId)}/record/commits`;
39
+ const ORG_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
37
40
  const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
38
41
  export function runnerApi(opts) {
39
42
  const base = opts.apiUrl.replace(/\/+$/, "");
@@ -93,5 +96,10 @@ export function runnerApi(opts) {
93
96
  job: (id) => call(JOB_READ_PATH(scoped(id)), "GET", ex),
94
97
  bundle: (id, leaseKey) => call(BUNDLE_FETCH_PATH(scoped(id)), "GET", leaseKey),
95
98
  jwks: () => call(JWKS_PATH, "GET", null),
99
+ recordCommit: (orgId, body, leaseKey) => {
100
+ if (!ORG_ID_RE.test(orgId))
101
+ throw new RangeError("org id is not a Mnemom org id");
102
+ return call(RECORD_COMMITS_PATH(orgId), "POST", leaseKey, body);
103
+ },
96
104
  };
97
105
  }