@mnemom/mnemom 0.17.32-next.4 → 0.17.33

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 (49) hide show
  1. package/dist/commands/agent-fuel.d.ts +9 -0
  2. package/dist/commands/agent-fuel.js +36 -2
  3. package/dist/commands/agent-run.d.ts +50 -0
  4. package/dist/commands/agent-run.js +221 -0
  5. package/dist/commands/agent-setup.d.ts +8 -3
  6. package/dist/commands/agent-setup.js +32 -9
  7. package/dist/commands/agent-share-fixture.d.ts +3 -1
  8. package/dist/commands/agent-share-fixture.js +11 -7
  9. package/dist/commands/agent-share-render.d.ts +23 -0
  10. package/dist/commands/agent-share-render.js +72 -0
  11. package/dist/commands/agent-share.d.ts +15 -2
  12. package/dist/commands/agent-share.js +30 -9
  13. package/dist/commands/agent.d.ts +19 -4
  14. package/dist/commands/agent.js +33 -13
  15. package/dist/index.js +20 -0
  16. package/dist/lib/api.js +17 -1
  17. package/dist/lib/auth.d.ts +6 -0
  18. package/dist/lib/auth.js +161 -14
  19. package/dist/lib/experimental.d.ts +1 -1
  20. package/dist/lib/experimental.js +13 -0
  21. package/dist/lib/oauth.d.ts +7 -0
  22. package/dist/lib/oauth.js +13 -0
  23. package/dist/lib/runner/bundle.d.ts +129 -0
  24. package/dist/lib/runner/bundle.js +319 -0
  25. package/dist/lib/runner/contract-header.d.ts +46 -0
  26. package/dist/lib/runner/contract-header.js +85 -0
  27. package/dist/lib/runner/executor-client.d.ts +71 -0
  28. package/dist/lib/runner/executor-client.js +97 -0
  29. package/dist/lib/runner/job.d.ts +70 -0
  30. package/dist/lib/runner/job.js +463 -0
  31. package/dist/lib/runner/loop.d.ts +45 -0
  32. package/dist/lib/runner/loop.js +104 -0
  33. package/dist/lib/runner/oidc.d.ts +38 -0
  34. package/dist/lib/runner/oidc.js +89 -0
  35. package/dist/lib/runner/options.d.ts +41 -0
  36. package/dist/lib/runner/options.js +47 -0
  37. package/dist/lib/runner/outcome.d.ts +47 -0
  38. package/dist/lib/runner/outcome.js +89 -0
  39. package/dist/lib/runner/phases.d.ts +49 -0
  40. package/dist/lib/runner/phases.js +99 -0
  41. package/dist/lib/runner/proxy.d.ts +45 -0
  42. package/dist/lib/runner/proxy.js +153 -0
  43. package/dist/lib/runner/settings.d.ts +31 -0
  44. package/dist/lib/runner/settings.js +44 -0
  45. package/dist/lib/statusline-feed.js +10 -11
  46. package/dist/lib/statusline.d.ts +6 -0
  47. package/dist/lib/statusline.js +19 -9
  48. package/dist/rc-proxy.mjs +69 -5
  49. package/package.json +2 -1
@@ -44,7 +44,16 @@ export interface FuelDeps {
44
44
  readCommitted(root: string, path: string): string;
45
45
  out(line: string): void;
46
46
  err(line: string): void;
47
+ /** Wait this many seconds (a 429's Retry-After); injected so tests never sleep. */
48
+ sleep?(seconds: number): Promise<void>;
47
49
  }
50
+ /**
51
+ * How many times a 429 (the org's seal budget, Doors W5.9) is waited out before giving up. Each
52
+ * resend carries the same key, so a refused batch is sealed exactly once when it gets through.
53
+ */
54
+ export declare const RATE_LIMIT_RETRIES = 5;
55
+ /** Seconds to wait before resending, for a 429; null for anything else. */
56
+ export declare function rateLimitWait(error: unknown): number | null;
48
57
  /** A contract the lead drafted, checked and hashed locally before anything is sent. */
49
58
  interface DraftedContract {
50
59
  contract: Contract;
@@ -16,6 +16,21 @@ import { dirname, join } from "node:path";
16
16
  import { createInterface } from "node:readline/promises";
17
17
  import { listMyOrgs, MnemomApiError, newIdempotencyKey, sealContractBatch, } from "../lib/api.js";
18
18
  import { checkContract, contractFileText, contractRepoPath, contractSha256, } from "../lib/contract-hash.js";
19
+ /**
20
+ * How many times a 429 (the org's seal budget, Doors W5.9) is waited out before giving up. Each
21
+ * resend carries the same key, so a refused batch is sealed exactly once when it gets through.
22
+ */
23
+ export const RATE_LIMIT_RETRIES = 5;
24
+ /** The longest single wait, whatever Retry-After says; the api's window is 60 s. */
25
+ const RATE_LIMIT_WAIT_MAX_S = 60;
26
+ /** Seconds to wait before resending, for a 429; null for anything else. */
27
+ export function rateLimitWait(error) {
28
+ if (!(error instanceof MnemomApiError) || error.effectiveStatus !== 429)
29
+ return null;
30
+ const after = error.details?.retry_after_s;
31
+ const seconds = typeof after === "number" && Number.isFinite(after) && after > 0 ? after : 1;
32
+ return Math.min(Math.ceil(seconds), RATE_LIMIT_WAIT_MAX_S);
33
+ }
19
34
  /** `owner/name` as GitHub issues it, the same rule the api applies: never `.` or `..` as a name. */
20
35
  const REPO_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/(?!\.\.?$)[A-Za-z0-9_.-]{1,100}$/;
21
36
  /** The batch size the api enforces. */
@@ -142,16 +157,32 @@ export async function agentFuelCommand(file, opts, deps) {
142
157
  source: "cli",
143
158
  contracts: drafted.map((d) => d.jobKey ? { contract: d.contract, job_key: d.jobKey } : { contract: d.contract }),
144
159
  };
160
+ // A 429 is the org's seal budget (W5.9): wait out Retry-After and resend the same request, so a
161
+ // burst is delayed, never dropped. The api refused it before writing anything.
162
+ const send = async () => {
163
+ for (let attempt = 0;; attempt++) {
164
+ try {
165
+ return await deps.seal(orgId, body, key, githubToken);
166
+ }
167
+ catch (error) {
168
+ const wait = rateLimitWait(error);
169
+ if (wait === null || attempt >= RATE_LIMIT_RETRIES)
170
+ throw error;
171
+ deps.err(`The organization's seal budget is spent; resending in ${wait} s.`);
172
+ await (deps.sleep ?? sleepSeconds)(wait);
173
+ }
174
+ }
175
+ };
145
176
  let result;
146
177
  try {
147
- result = await deps.seal(orgId, body, key, githubToken);
178
+ result = await send();
148
179
  }
149
180
  catch (error) {
150
181
  // A 5xx or a network fault may have reached the api, so retry once with the same key. A 4xx
151
182
  // is the api's answer and is not retried.
152
183
  if (!(error instanceof MnemomApiError) || error.effectiveStatus >= 500) {
153
184
  try {
154
- result = await deps.seal(orgId, body, key, githubToken);
185
+ result = await send();
155
186
  }
156
187
  catch (retryError) {
157
188
  // The api may have sealed before failing; the same key replays that batch, never a second one.
@@ -238,6 +269,9 @@ export function resolveGithubToken(env = process.env, gh = ghAuthToken) {
238
269
  export function readFuelInput(path, read = readFileSync) {
239
270
  return read(path === "-" ? 0 : path, "utf8");
240
271
  }
272
+ function sleepSeconds(seconds) {
273
+ return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
274
+ }
241
275
  export const liveFuelDeps = {
242
276
  readInput: (path) => readFuelInput(path),
243
277
  listOrgs: () => listMyOrgs(),
@@ -0,0 +1,50 @@
1
+ /**
2
+ * `mnemom agent run --watch|--once` (ruling 34.12; Doors W-L13, W3.11, W4.4, W4.12): the thin
3
+ * runner's driver.
4
+ *
5
+ * Everything that decides an outcome lives in lib/runner (and holds no prompts); this file only
6
+ * wires it to the world: the credential (env key, or the --oidc exchange), the executor client,
7
+ * the job's working copy, the phases' child processes, the PR, and SIGTERM/SIGINT. Usage and config
8
+ * errors exit 2 with no outcome line; every outcome line goes to stdout, every log to stderr.
9
+ *
10
+ * The GitHub token (GH_TOKEN, GITHUB_TOKEN, else `gh auth token`) is the runner's own: it reaches
11
+ * git only through GIT_CONFIG_* env (never argv) and the phases never see it (phaseEnv).
12
+ */
13
+ import { type FetchFn, type RunnerApi } from "../lib/runner/executor-client.js";
14
+ import type { JobDeps, Workspace } from "../lib/runner/job.js";
15
+ import { type PhaseInvocation } from "../lib/runner/phases.js";
16
+ import { type RunFlags } from "../lib/runner/options.js";
17
+ export interface RunDeps {
18
+ env: NodeJS.ProcessEnv;
19
+ fetch: FetchFn;
20
+ apiUrl: string;
21
+ /** stdout: one outcome line per job. */
22
+ emit(line: string): void;
23
+ /** stderr. */
24
+ err(line: string): void;
25
+ /** Aborted by SIGTERM or SIGINT. */
26
+ stop: AbortSignal;
27
+ jobDeps(api: RunnerApi): JobDeps;
28
+ }
29
+ /** Resolves the process exit code; never throws for a usage, config or Mnemom error. */
30
+ export declare function agentRunCommand(flags: RunFlags, deps: RunDeps): Promise<number>;
31
+ export declare const GITHUB_API = "https://api.github.com";
32
+ /** git's env with the token as an http extra header: it never reaches argv or the remote URL. */
33
+ export declare function gitAuthEnv(parent: NodeJS.ProcessEnv, token: string | null): NodeJS.ProcessEnv;
34
+ export type Git = (cwd: string | undefined, args: string[]) => Promise<string>;
35
+ export declare function liveGit(env: NodeJS.ProcessEnv): Git;
36
+ /** A fresh clone on a job branch, plus a fresh, empty HOME for the phases. */
37
+ export declare function prepareWorkspace(git: Git, cloneUrl: string, sessionId: string, root?: string): Promise<Workspace>;
38
+ export declare function cleanupWorkspace(ws: Workspace): Promise<void>;
39
+ /** Push the job branch and open its PR against the repo's default branch; resolves its URL. */
40
+ export declare function openPr(git: Git, fetch: FetchFn, token: string | null, repo: string, ws: Workspace, title: string): Promise<string>;
41
+ /** merge_on_green: GitHub's auto-merge on the PR (squash). */
42
+ export declare function enableAutoMerge(fetch: FetchFn, token: string | null, prUrl: string): Promise<void>;
43
+ /** Run one phase's process in the job's working copy; stdout goes to our stderr, never stdout. */
44
+ export declare function runPhaseProcess(inv: PhaseInvocation, ws: Workspace, modelUrl: string, signal: AbortSignal, parentEnv?: NodeJS.ProcessEnv): Promise<number>;
45
+ export declare function liveJobDeps(api: RunnerApi, opts: {
46
+ fetch: FetchFn;
47
+ env: NodeJS.ProcessEnv;
48
+ }): JobDeps;
49
+ /** The process wiring: signals, stdout and stderr. Resolves the exit code. */
50
+ export declare function runAgentRun(flags: RunFlags): Promise<number>;
@@ -0,0 +1,221 @@
1
+ /**
2
+ * `mnemom agent run --watch|--once` (ruling 34.12; Doors W-L13, W3.11, W4.4, W4.12): the thin
3
+ * runner's driver.
4
+ *
5
+ * Everything that decides an outcome lives in lib/runner (and holds no prompts); this file only
6
+ * wires it to the world: the credential (env key, or the --oidc exchange), the executor client,
7
+ * the job's working copy, the phases' child processes, the PR, and SIGTERM/SIGINT. Usage and config
8
+ * errors exit 2 with no outcome line; every outcome line goes to stdout, every log to stderr.
9
+ *
10
+ * The GitHub token (GH_TOKEN, GITHUB_TOKEN, else `gh auth token`) is the runner's own: it reaches
11
+ * git only through GIT_CONFIG_* env (never argv) and the phases never see it (phaseEnv).
12
+ */
13
+ import { spawn, execFile } from "node:child_process";
14
+ import { randomUUID } from "node:crypto";
15
+ import { mkdtemp, rm } from "node:fs/promises";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { getApiUrl, getGatewayUrl } from "../lib/config.js";
19
+ import { runnerApi } from "../lib/runner/executor-client.js";
20
+ import { runOnce, runWatch } from "../lib/runner/loop.js";
21
+ import { exchangeOidc } from "../lib/runner/oidc.js";
22
+ import { exitCodeFor, outcomeLine, refusedOutcome, USAGE_EXIT_CODE, } from "../lib/runner/outcome.js";
23
+ import { phaseEnv } from "../lib/runner/phases.js";
24
+ import { resolveRunPlan } from "../lib/runner/options.js";
25
+ import { startModelProxy } from "../lib/runner/proxy.js";
26
+ import { resolveGithubToken } from "./agent-fuel.js";
27
+ /** Resolves the process exit code; never throws for a usage, config or Mnemom error. */
28
+ export async function agentRunCommand(flags, deps) {
29
+ const resolved = resolveRunPlan(flags, deps.env);
30
+ if (!resolved.ok) {
31
+ deps.err(resolved.usage);
32
+ return USAGE_EXIT_CODE;
33
+ }
34
+ const plan = resolved.plan;
35
+ for (const w of plan.warnings)
36
+ deps.err(w);
37
+ let executorKey;
38
+ if (plan.credential.kind === "oidc") {
39
+ const ex = await exchangeOidc({
40
+ fetch: deps.fetch,
41
+ apiUrl: deps.apiUrl,
42
+ requestUrl: plan.credential.requestUrl,
43
+ requestToken: plan.credential.requestToken,
44
+ });
45
+ if (!ex.ok) {
46
+ // No job is held yet: fail closed whatever --fail-mode says (34.12 (6)).
47
+ deps.err(`mnemom agent run: refused (${ex.reason}): ${ex.detail}`);
48
+ const o = refusedOutcome({}, ex.reason);
49
+ deps.emit(outcomeLine(o));
50
+ return exitCodeFor(o.outcome);
51
+ }
52
+ executorKey = ex.executorKey;
53
+ }
54
+ else {
55
+ executorKey = plan.credential.key;
56
+ }
57
+ const api = runnerApi({ apiUrl: deps.apiUrl, executorKey, fetch: deps.fetch });
58
+ const loop = { failMode: plan.failMode, stop: deps.stop, emit: deps.emit };
59
+ const job = deps.jobDeps(api);
60
+ return plan.form === "once" ? runOnce(job, loop) : runWatch(job, loop);
61
+ }
62
+ // ── GitHub and git, for the live driver ──────────────────────────────────────
63
+ export const GITHUB_API = "https://api.github.com";
64
+ /** git's env with the token as an http extra header: it never reaches argv or the remote URL. */
65
+ export function gitAuthEnv(parent, token) {
66
+ const env = { ...parent, GIT_TERMINAL_PROMPT: "0" };
67
+ if (!token)
68
+ return env;
69
+ const basic = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64");
70
+ return {
71
+ ...env,
72
+ GIT_CONFIG_COUNT: "1",
73
+ GIT_CONFIG_KEY_0: "http.https://github.com/.extraheader",
74
+ GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${basic}`,
75
+ };
76
+ }
77
+ export function liveGit(env) {
78
+ return (cwd, args) => new Promise((resolve, reject) => execFile("git", args, { cwd, env, encoding: "utf8" }, (e, out, stderr) => e
79
+ ? reject(new Error(`git ${args[0]}: ${String(stderr).trim() || e.message}`))
80
+ : resolve(out.trim())));
81
+ }
82
+ /** A fresh clone on a job branch, plus a fresh, empty HOME for the phases. */
83
+ export async function prepareWorkspace(git, cloneUrl, sessionId, root = tmpdir()) {
84
+ const dir = await mkdtemp(join(root, "mnemom-job-"));
85
+ const home = await mkdtemp(join(root, "mnemom-home-"));
86
+ const branch = `mnemom/job-${sessionId.slice(0, 8)}`;
87
+ try {
88
+ await git(undefined, ["clone", "--quiet", "--depth", "50", "--", cloneUrl, dir]);
89
+ await git(dir, ["checkout", "--quiet", "-b", branch]);
90
+ // A phase that commits needs an identity; HOME is empty, so it is set on the repo.
91
+ await git(dir, ["config", "user.name", "mnemom runner"]);
92
+ await git(dir, ["config", "user.email", "runner@mnemom.ai"]);
93
+ }
94
+ catch (e) {
95
+ await cleanupWorkspace({ dir, home, branch });
96
+ throw e;
97
+ }
98
+ return { dir, home, branch };
99
+ }
100
+ export async function cleanupWorkspace(ws) {
101
+ await Promise.all([ws.dir, ws.home].map((d) => rm(d, { recursive: true, force: true })));
102
+ }
103
+ async function github(fetch, token, method, path, body) {
104
+ if (!token)
105
+ throw new Error("no GitHub token (set GH_TOKEN or GITHUB_TOKEN)");
106
+ const res = await fetch(`${GITHUB_API}${path}`, {
107
+ method,
108
+ headers: {
109
+ accept: "application/vnd.github+json",
110
+ authorization: `Bearer ${token}`,
111
+ ...(body === undefined ? {} : { "content-type": "application/json" }),
112
+ },
113
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
114
+ });
115
+ let json = {};
116
+ try {
117
+ json = JSON.parse(await res.text());
118
+ }
119
+ catch {
120
+ // an empty or non-JSON answer: the status decides
121
+ }
122
+ return { status: res.status, json };
123
+ }
124
+ /** Push the job branch and open its PR against the repo's default branch; resolves its URL. */
125
+ export async function openPr(git, fetch, token, repo, ws, title) {
126
+ await git(ws.dir, ["push", "--quiet", "origin", `HEAD:refs/heads/${ws.branch}`]);
127
+ const meta = await github(fetch, token, "GET", `/repos/${repo}`);
128
+ const base = meta.json.default_branch;
129
+ if (meta.status !== 200 || typeof base !== "string") {
130
+ throw new Error(`reading ${repo} answered ${meta.status}`);
131
+ }
132
+ const pr = await github(fetch, token, "POST", `/repos/${repo}/pulls`, {
133
+ title,
134
+ head: ws.branch,
135
+ base,
136
+ });
137
+ const url = pr.json.html_url;
138
+ if (pr.status !== 201 || typeof url !== "string") {
139
+ throw new Error(`opening the PR answered ${pr.status}`);
140
+ }
141
+ return url;
142
+ }
143
+ const PR_URL_RE = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)$/;
144
+ /** merge_on_green: GitHub's auto-merge on the PR (squash). */
145
+ export async function enableAutoMerge(fetch, token, prUrl) {
146
+ const m = PR_URL_RE.exec(prUrl);
147
+ if (!m)
148
+ throw new Error(`not a GitHub PR URL: ${prUrl}`);
149
+ const pr = await github(fetch, token, "GET", `/repos/${m[1]}/pulls/${m[2]}`);
150
+ if (pr.status !== 200 || typeof pr.json.node_id !== "string") {
151
+ throw new Error(`reading the PR answered ${pr.status}`);
152
+ }
153
+ const r = await github(fetch, token, "POST", "/graphql", {
154
+ query: "mutation($id: ID!) { enablePullRequestAutoMerge(input: {pullRequestId: $id, mergeMethod: SQUASH}) { clientMutationId } }",
155
+ variables: { id: pr.json.node_id },
156
+ });
157
+ if (r.status !== 200 || r.json.errors !== undefined) {
158
+ throw new Error(`enabling auto-merge answered ${r.status}`);
159
+ }
160
+ }
161
+ /** Run one phase's process in the job's working copy; stdout goes to our stderr, never stdout. */
162
+ export function runPhaseProcess(inv, ws, modelUrl, signal, parentEnv = process.env) {
163
+ return new Promise((resolve, reject) => {
164
+ let started = false;
165
+ const child = spawn(inv.command, inv.args, {
166
+ cwd: ws.dir,
167
+ env: phaseEnv(parentEnv, ws.home, modelUrl),
168
+ stdio: ["pipe", "pipe", "inherit"],
169
+ signal,
170
+ });
171
+ child.on("spawn", () => {
172
+ started = true;
173
+ child.stdin?.end(inv.stdin ?? "");
174
+ });
175
+ child.stdout?.pipe(process.stderr, { end: false });
176
+ child.on("error", (e) => {
177
+ if (!started)
178
+ reject(e);
179
+ });
180
+ child.on("close", (code) => resolve(code ?? -1));
181
+ });
182
+ }
183
+ export function liveJobDeps(api, opts) {
184
+ const token = resolveGithubToken(opts.env);
185
+ const git = liveGit(gitAuthEnv(opts.env, token));
186
+ return {
187
+ api,
188
+ nonce: randomUUID(),
189
+ newSpawnId: () => randomUUID(),
190
+ now: () => Date.now(),
191
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
192
+ log: (line) => process.stderr.write(`mnemom agent run: ${line}\n`),
193
+ gatewayUrl: `${getGatewayUrl()}/anthropic`,
194
+ startProxy: (state) => startModelProxy(`${getGatewayUrl()}/anthropic`, state),
195
+ prepareWorkspace: (repo, sessionId) => prepareWorkspace(git, `https://github.com/${repo}.git`, sessionId),
196
+ runPhase: (inv, ws, modelUrl, signal) => runPhaseProcess(inv, ws, modelUrl, signal, opts.env),
197
+ openPr: (repo, ws, title) => openPr(git, opts.fetch, token, repo, ws, title),
198
+ enableAutoMerge: (prUrl) => enableAutoMerge(opts.fetch, token, prUrl),
199
+ cleanup: cleanupWorkspace,
200
+ };
201
+ }
202
+ /** The process wiring: signals, stdout and stderr. Resolves the exit code. */
203
+ export function runAgentRun(flags) {
204
+ const stop = new AbortController();
205
+ const onSignal = () => stop.abort();
206
+ process.once("SIGTERM", onSignal);
207
+ process.once("SIGINT", onSignal);
208
+ const fetch = globalThis.fetch;
209
+ return agentRunCommand(flags, {
210
+ env: process.env,
211
+ fetch,
212
+ apiUrl: getApiUrl(),
213
+ emit: (line) => process.stdout.write(`${line}\n`),
214
+ err: (line) => process.stderr.write(`${line}\n`),
215
+ stop: stop.signal,
216
+ jobDeps: (api) => liveJobDeps(api, { fetch, env: process.env }),
217
+ }).finally(() => {
218
+ process.off("SIGTERM", onSignal);
219
+ process.off("SIGINT", onSignal);
220
+ });
221
+ }
@@ -316,14 +316,19 @@ interface PublishOpts {
316
316
  */
317
317
  export declare function keptPrincipal(own: unknown, opts: Pick<PublishOpts, "orgId" | "ownOrg">): Record<string, unknown>;
318
318
  /**
319
- * `mnemom agent setup [--agent <slug|mnm-id>] [--governance …]` — the explicit provisioning entry
320
- * point. An id (`--agent mnm-<uuid>`, or launcher.toml `agent_id`) refreshes that pinned agent's
321
- * cards and never claims or births; a malformed `mnm-…` refuses.
319
+ * `mnemom agent setup [--agent <slug|mnm-id>] [--governance …] [--auth …]` — the explicit
320
+ * provisioning entry point. An id (`--agent mnm-<uuid>`, or launcher.toml `agent_id`) refreshes
321
+ * that pinned agent's cards and never claims or births; a malformed `mnm-…` refuses.
322
+ *
323
+ * The auth mode resolves as a launch's does (`--auth`, else MNEMOM_AGENT_AUTH, which launcher.toml
324
+ * `auth` folds into, else api-key). A subscription setup asks for no Anthropic key: the identity
325
+ * anchors on the Mnemom API key, exactly as the subscription launch it provisions for (MNE-9282).
322
326
  */
323
327
  export declare function agentSetupCommand(opts?: {
324
328
  agent?: string;
325
329
  agentId?: string;
326
330
  governance?: GovernanceOptIn;
331
+ auth?: string;
327
332
  }): Promise<void>;
328
333
  /** Thrown when the agent's org has no spendable MU balance. The user-facing
329
334
  * top-up guidance is already printed when this is thrown — callers exit quietly. */
@@ -36,13 +36,13 @@
36
36
  * reinventing it.
37
37
  */
38
38
  import { claimAgent, getAgent, getAgentByName, getCardSources, getMe, getMuBalance, getMyPersonalOrg, listMyAgents, MnemomApiError, mintProtectionCapability, putAlignmentCard, putProtectionCard, } from "../lib/api.js";
39
- import { getAuthInfo, loginWithBrowser, loginWithDeviceFlow, resolveAuth } from "../lib/auth.js";
39
+ import { getAuthInfo, hasExpiredStoredSession, loginWithBrowser, loginWithDeviceFlow, resolveAuth, } from "../lib/auth.js";
40
40
  import { getActiveOrg } from "../lib/cli-config.js";
41
41
  import { getApiUrl, getGatewayUrl, getWebsiteUrl, MNEMOM_DIR } from "../lib/config.js";
42
42
  import { fmt } from "../lib/format.js";
43
43
  import { askYesNo, isInteractive } from "../lib/prompt.js";
44
44
  import { malformedAgentIdMessage, parseAgentRef, resolveAnthropicDoor, resolveGatewayHost, sanitizeAgentName, singleCellError, } from "../lib/agent.js";
45
- import { readMnemomKey, resolveSubscriptionMnemomKey } from "../lib/subscription-login.js";
45
+ import { readMnemomKey, resolveAgentAuth, resolveSubscriptionMnemomKey, sayRetiredConsoleNotice, } from "../lib/subscription-login.js";
46
46
  import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
47
47
  import { IdentityRouteMissingError, resolveServedIdentity, resolveServedIdentityDetail, } from "../lib/agent-identity-route.js";
48
48
  import { readConfirmedAgent, readServedAgent, servedFingerprint, writeConfirmedAgent, writeServedAgent, } from "../lib/served-agent-cache.js";
@@ -964,9 +964,13 @@ async function publishDefaultCards(agentId, modes, opts = {}) {
964
964
  }
965
965
  }
966
966
  /**
967
- * `mnemom agent setup [--agent <slug|mnm-id>] [--governance …]` — the explicit provisioning entry
968
- * point. An id (`--agent mnm-<uuid>`, or launcher.toml `agent_id`) refreshes that pinned agent's
969
- * cards and never claims or births; a malformed `mnm-…` refuses.
967
+ * `mnemom agent setup [--agent <slug|mnm-id>] [--governance …] [--auth …]` — the explicit
968
+ * provisioning entry point. An id (`--agent mnm-<uuid>`, or launcher.toml `agent_id`) refreshes
969
+ * that pinned agent's cards and never claims or births; a malformed `mnm-…` refuses.
970
+ *
971
+ * The auth mode resolves as a launch's does (`--auth`, else MNEMOM_AGENT_AUTH, which launcher.toml
972
+ * `auth` folds into, else api-key). A subscription setup asks for no Anthropic key: the identity
973
+ * anchors on the Mnemom API key, exactly as the subscription launch it provisions for (MNE-9282).
970
974
  */
971
975
  export async function agentSetupCommand(opts = {}) {
972
976
  const rawRef = opts.agent?.trim() || opts.agentId?.trim() || "";
@@ -974,6 +978,9 @@ export async function agentSetupCommand(opts = {}) {
974
978
  if (ref?.kind === "malformed-id") {
975
979
  throw new Error(malformedAgentIdMessage(ref.raw, opts.agent?.trim() ? "--agent" : "agent_id").replace("mnemom agent:", "mnemom agent setup:"));
976
980
  }
981
+ // Validated up front, as a launch does, so a bad --auth fails before any sign-in.
982
+ const auth = resolveAgentAuth(opts.auth, process.env);
983
+ sayRetiredConsoleNotice(opts.auth, process.env, say);
977
984
  const gatewayHost = resolveGatewayHost(process.env);
978
985
  const cellError = singleCellError({
979
986
  door: resolveAnthropicDoor(process.env),
@@ -993,13 +1000,23 @@ export async function agentSetupCommand(opts = {}) {
993
1000
  process.exit(1); // guidance already printed
994
1001
  throw err;
995
1002
  }
996
- const anthropicKey = await resolveAnthropicKey(process.env, { allowPrompt: true });
1003
+ // The credentials resolve as the launch's do (agentCommand): a subscription launch has no
1004
+ // Anthropic key and anchors on a Mnemom key (minted once if there is none); an api-key one
1005
+ // sends this machine's existing Mnemom key beside its Anthropic key, minting only for a pin.
1006
+ const subscription = auth === "subscription";
1007
+ const anthropicKey = subscription
1008
+ ? undefined
1009
+ : await resolveAnthropicKey(process.env, { allowPrompt: true });
1010
+ const subscriptionKey = subscription
1011
+ ? await resolveSubscriptionMnemomKey(defaultMnemomKeyDeps())
1012
+ : undefined;
997
1013
  if (ref?.kind === "id") {
998
1014
  const pinned = await resolvePinnedAgent(ref.id);
999
1015
  await ensureClaimedServedAgent({
1000
1016
  slug: pinned.slug,
1001
1017
  anthropicKey,
1002
- mnemomKey: await resolveSubscriptionMnemomKey(defaultMnemomKeyDeps(), "pinning an agent"),
1018
+ mnemomKey: subscriptionKey ??
1019
+ (await resolveSubscriptionMnemomKey(defaultMnemomKeyDeps(), "pinning an agent")),
1003
1020
  pin: pinned.id,
1004
1021
  gatewayHost,
1005
1022
  orgId: account.orgId,
@@ -1017,8 +1034,9 @@ export async function agentSetupCommand(opts = {}) {
1017
1034
  await ensureClaimedServedAgent({
1018
1035
  slug,
1019
1036
  anthropicKey,
1020
- // Asked the way a launch asks: with this machine's Mnemom key when it has one (never minted).
1021
- mnemomKey: readMnemomKey(defaultMnemomKeyDeps()),
1037
+ // Asked the way a launch asks: an api-key launch with this machine's Mnemom key when it has
1038
+ // one (never minted).
1039
+ mnemomKey: subscriptionKey ?? readMnemomKey(defaultMnemomKeyDeps()),
1022
1040
  gatewayHost,
1023
1041
  orgId: account.orgId,
1024
1042
  orgLabel: account.orgLabel,
@@ -1182,6 +1200,11 @@ export async function ensureSessionAndBalance(opts) {
1182
1200
  const interactive = opts.interactive ?? isInteractive();
1183
1201
  const haveSession = await ensureMnemomSession({ interactive });
1184
1202
  if (!haveSession) {
1203
+ // A stored session whose refresh was rejected needs a fresh login, not an
1204
+ // account: the generic message below sent signed-in users to the waitlist.
1205
+ if (hasExpiredStoredSession()) {
1206
+ throw new Error("Your Mnemom session expired. Run `mnemom login` to sign in again (or set MNEMOM_API_KEY).");
1207
+ }
1185
1208
  throw new Error("mnemom agent needs a Mnemom account — it runs your coding session governed and on your org's µ balance.\n" +
1186
1209
  " Sign in: mnemom login (or set MNEMOM_API_KEY)\n" +
1187
1210
  " Invited? mnemom agent --invite <token> (signs you in and unlocks Mnemom Agent)\n" +
@@ -17,4 +17,6 @@ export declare const FIXTURE_CARD_VALID: ShareCardLike;
17
17
  export declare const FIXTURE_CARD_PERIOD_REVERT: ShareCardLike;
18
18
  /** Real candidate lines plus two that must be refused. */
19
19
  export declare const FIXTURE_CANDIDATES: CandidateLine[];
20
- export declare const fixtureAgentShareParts: Pick<AgentShareDeps, "source" | "issuer">;
20
+ /** Public half of the aip TEST key (seed 0x2a) that signed the fixture cards. Test only. */
21
+ export declare const FIXTURE_TEST_PUBLIC_KEY = "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61";
22
+ export declare const fixtureAgentShareParts: Pick<AgentShareDeps, "source" | "issuer" | "renderer">;
@@ -10,6 +10,7 @@
10
10
  * the refusal path can be shown. This is not a real issuer: a production source and signer
11
11
  * are a separate, human-gated change.
12
12
  */
13
+ import { aipLineRenderer } from "./agent-share-render.js";
13
14
  export const AGENT_SHARE_FIXTURE_ENV = "MNEMOM_AGENT_SHARE_FIXTURE";
14
15
  export function agentShareFixtureEnabled(env = process.env) {
15
16
  return env.MNEMOM_AGENT_SHARE === "1" && env[AGENT_SHARE_FIXTURE_ENV] === "1";
@@ -25,11 +26,11 @@ export const FIXTURE_CARD_VALID = {
25
26
  signing_key_id: "test-sharecard-key-1",
26
27
  statements: [
27
28
  {
28
- typ: "AAP-Statement/v1",
29
+ typ: "AAP-ConductRecord/v1",
29
30
  hash: "1111111111111111111111111111111111111111111111111111111111111111",
30
31
  },
31
32
  {
32
- typ: "AAP-Statement/v1",
33
+ typ: "AAP-ConductRecord/v1",
33
34
  hash: "2222222222222222222222222222222222222222222222222222222222222222",
34
35
  },
35
36
  ],
@@ -49,7 +50,7 @@ export const FIXTURE_CARD_VALID = {
49
50
  statement_hash: "2222222222222222222222222222222222222222222222222222222222222222",
50
51
  },
51
52
  ],
52
- signature: "qsHSChAxBEnEyNoQQLQMFXZ6LKxruMyijxA5s3h9oPKl4OijJqxFm3QC0rteDXJsYwJCMeKyk_0xzGTDc3wDAQ",
53
+ signature: "PCa6dhIMUJrJoA9A_v50FTXWxmRSADYs1F_iZWWJrJiujUbsxPkadHQhS8cQvug0ChRQpkOrtNkQJ48opcK-Cg",
53
54
  };
54
55
  export const FIXTURE_CARD_PERIOD_REVERT = {
55
56
  typ: "AAP-ShareCard/v1",
@@ -62,15 +63,15 @@ export const FIXTURE_CARD_PERIOD_REVERT = {
62
63
  signing_key_id: "test-sharecard-key-1",
63
64
  statements: [
64
65
  {
65
- typ: "AAP-Statement/v1",
66
+ typ: "AAP-ConductRecord/v1",
66
67
  hash: "1111111111111111111111111111111111111111111111111111111111111111",
67
68
  },
68
69
  {
69
- typ: "AAP-Statement/v1",
70
+ typ: "AAP-ConductRecord/v1",
70
71
  hash: "2222222222222222222222222222222222222222222222222222222222222222",
71
72
  },
72
73
  {
73
- typ: "AAP-Statement/v1",
74
+ typ: "AAP-ConductRecord/v1",
74
75
  hash: "3333333333333333333333333333333333333333333333333333333333333333",
75
76
  },
76
77
  ],
@@ -97,7 +98,7 @@ export const FIXTURE_CARD_PERIOD_REVERT = {
97
98
  statement_hash: "3333333333333333333333333333333333333333333333333333333333333333",
98
99
  },
99
100
  ],
100
- signature: "OQf-4xgUQbIxOef_BsJeTehfpqLPPR2GTv8_y6EbxPX9UkFp2PpE2pnETh2VqcwDfV1U_4bSR8Pwrpsch3DwCw",
101
+ signature: "0aUrWIfIYKlp2H40Ovh4AYE6MgWokOrjfLjd7Wm9-tT0UHTEvPvo4a5lAy-WQ5iRBUI9JJncNJS9zpVPY2X6DA",
101
102
  };
102
103
  const FIXTURE_CARDS = [FIXTURE_CARD_VALID, FIXTURE_CARD_PERIOD_REVERT];
103
104
  const HASH_E = "e".repeat(64);
@@ -120,7 +121,10 @@ const same = (a, b) => a.length === b.length &&
120
121
  const m = b.find((x) => x.id === l.id);
121
122
  return m !== undefined && JSON.stringify(m) === JSON.stringify(l);
122
123
  });
124
+ /** Public half of the aip TEST key (seed 0x2a) that signed the fixture cards. Test only. */
125
+ export const FIXTURE_TEST_PUBLIC_KEY = "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61";
123
126
  export const fixtureAgentShareParts = {
127
+ renderer: aipLineRenderer(FIXTURE_TEST_PUBLIC_KEY),
124
128
  source: { listLines: async () => FIXTURE_CANDIDATES },
125
129
  issuer: {
126
130
  issue: async (lines) => {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Bridge from `mnemom agent share` to the aip verifier (`@mnemom/aip-verifier`, aip#192/#178):
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 devDependency pinned to the aip
5
+ * release tarball and is loaded lazily, so the default CLI path never touches it; if it is
6
+ * absent at runtime the share command fails closed with a clear message.
7
+ */
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";
11
+ export interface VerifierModules {
12
+ lintRendering: typeof AipMain.lintRendering;
13
+ renderLines: typeof AipRender.renderLines;
14
+ }
15
+ /** Loader seam: tests swap it to simulate a missing or broken package. */
16
+ export type VerifierLoader = () => Promise<VerifierModules>;
17
+ /**
18
+ * `publicKey` is the trusted Ed25519 key (hex) that must have signed the card. Without one the
19
+ * renderer fails closed (missing_key), so an unsigned or foreign-signed card is never linked.
20
+ */
21
+ export declare function aipLineRenderer(publicKey: string | undefined, load?: VerifierLoader): LineRenderer;
22
+ /** claim-lint one candidate line as the share card would show it; a reason when it fails. */
23
+ export declare function aipLineScreen(line: SharedCardLine, load?: VerifierLoader): Promise<string | null>;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Bridge from `mnemom agent share` to the aip verifier (`@mnemom/aip-verifier`, aip#192/#178):
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 devDependency pinned to the aip
5
+ * release tarball and is loaded lazily, so the default CLI path never touches it; if it is
6
+ * absent at runtime the share command fails closed with a clear message.
7
+ */
8
+ const defaultLoader = async () => {
9
+ const [main, render] = await Promise.all([
10
+ import("@mnemom/aip-verifier"),
11
+ import("@mnemom/aip-verifier/render-lines"),
12
+ ]);
13
+ return { lintRendering: main.lintRendering, renderLines: render.renderLines };
14
+ };
15
+ /** Keep the real cause: "not installed" only for a genuinely missing module. */
16
+ async function loadVerifier(load) {
17
+ try {
18
+ return await load();
19
+ }
20
+ catch (error) {
21
+ const e = error;
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: run `npm install https://github.com/mnemom/aip/releases/download/aip-verifier-v0.6.0/mnemom-aip-verifier-0.6.0.tgz`", { cause: error });
24
+ }
25
+ throw new Error(`the aip verifier (@mnemom/aip-verifier) failed to load: ${e.message ?? error}`, { cause: error });
26
+ }
27
+ }
28
+ /**
29
+ * `publicKey` is the trusted Ed25519 key (hex) that must have signed the card. Without one the
30
+ * renderer fails closed (missing_key), so an unsigned or foreign-signed card is never linked.
31
+ */
32
+ export function aipLineRenderer(publicKey, load = defaultLoader) {
33
+ return {
34
+ async renderLines(card, opts) {
35
+ const { renderLines } = await loadVerifier(load);
36
+ const r = await renderLines(card, {
37
+ enabled: true,
38
+ surface: opts?.period ? "period_card" : "share_card",
39
+ ...(publicKey ? { publicKey } : {}),
40
+ });
41
+ if (!r.ok)
42
+ throw new Error(`the card did not render (${r.reasons.join(", ")})`);
43
+ return r.lines.map((l) => ({
44
+ text: l.text,
45
+ kind: l.class,
46
+ ...(l.coverage ? { coverage: `${l.coverage.judged} of ${l.coverage.total}` } : {}),
47
+ }));
48
+ },
49
+ };
50
+ }
51
+ /** claim-lint one candidate line as the share card would show it; a reason when it fails. */
52
+ export async function aipLineScreen(line, load = defaultLoader) {
53
+ let v;
54
+ try {
55
+ v = await loadVerifier(load);
56
+ }
57
+ catch (error) {
58
+ return error.message;
59
+ }
60
+ const text = `${line.label}: ${line.value}`;
61
+ let res;
62
+ try {
63
+ res = v.lintRendering({
64
+ surface: "share_card",
65
+ lines: [{ class: line.kind === "revert" ? "outcome" : "measured", text }],
66
+ });
67
+ }
68
+ catch (error) {
69
+ return `claim-lint could not run (${error.message})`;
70
+ }
71
+ return res.ok ? null : `claim-lint rejects it (${res.violations.map((x) => x.rule).join(", ")})`;
72
+ }