@mnemom/mnemom 0.17.9-next.0 → 0.17.10

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.
@@ -33,7 +33,7 @@ import { basename, join, dirname } from "node:path";
33
33
  import { fileURLToPath } from "node:url";
34
34
  import { statuslineLaunch } from "../lib/statusline.js";
35
35
  import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
36
- import { createApiKey, getAgentByName } from "../lib/api.js";
36
+ import { createApiKey } from "../lib/api.js";
37
37
  import { MNEMOM_KEY_STORE_FIELD, resolveAgentAuth, resolveSubscriptionMnemomKey, subscriptionChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
38
38
  import { LEDGER_FILE, appendSessionRecord, findResumeCandidate, readSessionRecords, resolveRetentionDays, resumedContextProfile, toLedgerContract, } from "../lib/agent-ledger.js";
39
39
  import { loadCliConfig } from "../lib/cli-config.js";
@@ -44,7 +44,7 @@ import { askInput, askSelect, askYesNo, isInteractive } from "../lib/prompt.js";
44
44
  import { authConfigured, chooseAuth, detectClaudeLogin, readClaudeLoginState, } from "../lib/login-detect.js";
45
45
  import { requireConsoleKey, readConsoleKey } from "../lib/console-key.js";
46
46
  import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
47
- import { AgentIdentityMismatchError, defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
47
+ import { defaultAgentSlug, ensureClaimedServedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
48
48
  import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, assertCompactAtCompatible, assertOffProfileCompatible, compactAtWindowWarning, describeCompactAt, launchWindow, resolveCompactAt, applyGatewayHintHeaders, resolveCompactionIntercept, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, applyToolSearchDefault, DISABLE_1M_ENV, readCcSettings, resolveWindowModel, resolveAnthropicDoor, resolveLaunchModel, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
49
49
  import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
50
50
  import { slackChannelArgs } from "../lib/slack-launch.js";
@@ -292,6 +292,7 @@ export async function agentCommand(scenarioArg, options = {}) {
292
292
  if (auth === "subscription") {
293
293
  say("mnemom agent: auth = subscription — Claude Code would use your claude.ai login (Team/Enterprise plans only) with a Mnemom API key; no Anthropic key.");
294
294
  }
295
+ say(`mnemom agent: the real launch first confirms the agent the gateway serves for '${agent}' is claimed — claiming it into the org the launch bills if it is not — and never runs on an unclaimed agent.`);
295
296
  if (auth === "console") {
296
297
  say("mnemom agent: auth = console — the launch would use the API key your Claude Code Anthropic Console login created.");
297
298
  }
@@ -388,40 +389,20 @@ export async function agentCommand(scenarioArg, options = {}) {
388
389
  if (refreshed)
389
390
  agent = refreshed;
390
391
  }
391
- // ── zero-friction governed agent: make sure `agent` exists + is claimed in the
392
- // customer's org (with the nudge/nudge coding posture) before we launch. AUTO
393
- // provisions-with-confirm when interactive; warns and continues otherwise.
394
- // Skipped entirely by `--no-setup`. A provisioning failure never blocks the
395
- // launch — it degrades to the unclaimed (fail-open) identity with a warning.
396
- // The agent id the gateway serves, when setup asked it (undefined = never asked, so the ledger
397
- // falls back to the org's name lookup).
398
- let servedAgentId;
399
- if (options.setup !== false && !anthropicKey) {
400
- // Subscription login: the gateway creates the agent in the Mnemom key's org on the first
401
- // turn, so there is nothing to birth or claim here.
402
- say(`mnemom agent: subscription login — '${agent}' is created in your org on the first turn (identity: your Mnemom API key).`);
403
- }
404
- else if (options.setup !== false && anthropicKey) {
405
- servedAgentId = null; // asked below; stays null if setup can't tell
406
- try {
407
- const ensured = await ensureGovernedAgent({
408
- slug: agent,
409
- anthropicKey,
410
- setup: options.setup,
411
- gatewayHost,
412
- });
413
- servedAgentId = ensured.agentId ?? null;
414
- }
415
- catch (err) {
416
- // An identity mismatch is the one setup failure that must NOT degrade: the
417
- // launch would run as an agent other than the one --setup just tried to
418
- // govern. Refuse, with both ids in the message (MNE-7650).
419
- if (err instanceof AgentIdentityMismatchError)
420
- throw err;
421
- const msg = err instanceof Error ? err.message : String(err);
422
- say(fmt.warn(`mnemom agent: agent setup did not complete (${msg}) — launching with the unclaimed identity.`));
423
- }
424
- }
392
+ // ── never an unclaimed agent ── the gateway serves hash(credential, name); the coding CLI
393
+ // starts only once THAT agent is claimed. An unclaimed one is claimed here into the org this
394
+ // launch bills (account.orgId — the active/default org is never changed); anything else
395
+ // refuses the launch. No prompt and no unclaimed fallback, interactive or not, preflighted or
396
+ // not. `--no-setup` only skips the default-card writes.
397
+ const { agentId: servedAgentId } = await ensureClaimedServedAgent({
398
+ slug: agent,
399
+ anthropicKey,
400
+ mnemomKey,
401
+ gatewayHost,
402
+ orgId: account.orgId,
403
+ orgLabel: account.orgLabel,
404
+ setup: options.setup,
405
+ });
425
406
  // ── conversation id ── an implicit relaunch continues its earlier conversation (IG5 #1, D1)
426
407
  // so the goal the gateway inferred there is still the one it steers by. Resolved here, after
427
408
  // sign-in and the agent refresh, because a resume needs the org, agent and gateway host to
@@ -499,7 +480,7 @@ export async function agentCommand(scenarioArg, options = {}) {
499
480
  if (win?.reason === "one-m") {
500
481
  say(`mnemom agent: model ${win.from} → ${win.model} (1M context window; set ${DISABLE_1M_ENV}=1, or pass the model as \`-- --model <id>\`, to keep the standard window).`);
501
482
  }
502
- await recordSessionLaunch({
483
+ recordSessionLaunch({
503
484
  session: scenario,
504
485
  conversationId,
505
486
  agent,
@@ -706,30 +687,19 @@ export async function resolveLaunchContinuation(input) {
706
687
  return { conversationId: resolveConversationId(conv), resumeCcSessionId };
707
688
  }
708
689
  /**
709
- * Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
710
- * resolved best-effort (a lookup failure or an unknown slug records null); a
711
- * ledger write failure warns to stderr and the launch proceeds regardless.
690
+ * Record this launch in ~/.mnemom/sessions.jsonl, with the claimed agent id the
691
+ * gateway serves; a ledger write failure warns to stderr and the launch proceeds
692
+ * regardless.
712
693
  */
713
- export async function recordSessionLaunch(ctx) {
714
- // Never re-derive a known id from the name: the name can match an org agent the gateway does
715
- // not serve for this key (MNE-7650), which is exactly what the ledger must not record.
716
- let agentId;
717
- if (ctx.agentId !== undefined) {
718
- agentId = ctx.agentId;
719
- }
720
- else {
721
- try {
722
- agentId = (await getAgentByName(ctx.agent))?.id ?? null;
723
- }
724
- catch {
725
- agentId = null; // unresolvable right now — the ledger still records the launch
726
- }
727
- }
694
+ export function recordSessionLaunch(ctx) {
695
+ // The SERVED id, never one re-derived from the name: the name can match an org agent the
696
+ // gateway does not serve for this key (MNE-7650), which is exactly what the ledger must not
697
+ // record.
728
698
  const result = appendSessionRecord({
729
699
  session: ctx.session,
730
700
  conversation_id: ctx.conversationId,
731
701
  agent_slug: ctx.agent,
732
- agent_id: agentId,
702
+ agent_id: ctx.agentId,
733
703
  org_id: ctx.account.orgId,
734
704
  org_label: ctx.account.orgLabel,
735
705
  gateway_host: ctx.gatewayHost,
@@ -884,8 +884,8 @@ export async function cardEditCommand(agentName, options = {}) {
884
884
  cardYaml = yaml.dump({
885
885
  card_version: "unified/2026-04-26",
886
886
  agent_id: agentId,
887
- autonomy_mode: "observe",
888
- integrity_mode: "observe",
887
+ autonomy_mode: "off",
888
+ integrity_mode: "off",
889
889
  principal: { type: "agent", identifier: agentId, relationship: "delegated_authority" },
890
890
  values: { declared: ["transparency", "safety", "honesty"] },
891
891
  autonomy: {
@@ -352,7 +352,8 @@ async function earnRating(agentId, steps, say) {
352
352
  }
353
353
  // ── starter alignment card ──────────────────────────────────────────────────────
354
354
  /**
355
- * A starter alignment card in `observe` mode — the honest, minimal declaration
355
+ * A starter alignment card, `off` by default (alignment analysis is billed; the
356
+ * user turns it on) — the honest, minimal declaration
356
357
  * of intent for a freshly-onboarded agent. Mirrors the shape `wrap` seeds so the
357
358
  * two skill-runners produce a consistent first card.
358
359
  */
@@ -360,8 +361,8 @@ function buildStarterAlignmentCard(agentId) {
360
361
  return {
361
362
  card_version: "unified/2026-04-26",
362
363
  agent_id: agentId,
363
- autonomy_mode: "observe",
364
- integrity_mode: "observe",
364
+ autonomy_mode: "off",
365
+ integrity_mode: "off",
365
366
  principal: { type: "agent", identifier: agentId, relationship: "delegated_authority" },
366
367
  values: { declared: ["transparency", "safety", "honesty"] },
367
368
  autonomy: {
@@ -729,7 +729,7 @@ export async function protectionEditCommand(agentName, options = {}) {
729
729
  yaml.dump({
730
730
  card_version: "protection/2026-04-26",
731
731
  agent_id: agentId,
732
- mode: "observe",
732
+ mode: "off",
733
733
  thresholds: { warn: 0.3, quarantine: 0.6, block: 0.9 },
734
734
  screen_surfaces: {
735
735
  incoming: true,
@@ -48,5 +48,18 @@ export interface TryMeOptions {
48
48
  * are surfaced as human guidance.
49
49
  */
50
50
  export declare function tryMeCommand(token: string, options?: TryMeOptions): Promise<void>;
51
+ /**
52
+ * Classify a 401/403 card-write failure BY ITS ERROR CODE, never by status alone.
53
+ *
54
+ * - `grant-pending`: a PROTECTION 403 whose code says the one-time CISO grant
55
+ * hasn't landed in the browser yet. This is expected and is polled for the
56
+ * window; it must NOT trigger a re-login.
57
+ * - `refused`: any OTHER protection 403 (e.g. `feature_gated`, a billing
58
+ * refusal). Waiting cannot fix it, so it fails fast with the server's reason
59
+ * instead of polling for a grant the human already approved.
60
+ * - `auth`: a 401 anywhere, or an alignment 403 — the caller isn't authorized.
61
+ * After a short propagation grace this means the session is stale/mismatched.
62
+ */
63
+ export declare function classifyAuthError(kind: "alignment" | "protection", status: number | undefined, code?: string): "grant-pending" | "refused" | "auth";
51
64
  /** Render the resolved plan as human-readable text (no side effects). */
52
65
  export declare function renderPlan(manifest: BriefingManifest): string;
@@ -23,12 +23,12 @@
23
23
  * sign-in). That session IS the human's own — it is the headless twin of the
24
24
  * host-connector authorization in the MCP flow, not a forged credential.
25
25
  */
26
- import { resolveBriefing, birthAgent, fetchAgentClaimed, buildClaimUrl, buildGrantUrl, buildDojoDeepLink, claimProof, isTokenMode, looksLikeTryMeToken, sleep, } from "../lib/try-me.js";
26
+ import { resolveBriefing, birthAgent, fetchAgentClaimed, buildClaimUrl, buildGrantUrl, buildDojoDeepLink, claimProof, describeOutOfMu, isTokenMode, looksLikeTryMeToken, sleep, } from "../lib/try-me.js";
27
27
  import { putAlignmentCard, putProtectionCard, listOrgAgents, MnemomApiError } from "../lib/api.js";
28
28
  import { resolveAuth, loginWithBrowser, loginWithDeviceFlow } from "../lib/auth.js";
29
29
  import { openBrowser } from "../lib/oauth.js";
30
30
  import { askSelect, askInput, askYesNo, isInteractive } from "../lib/prompt.js";
31
- import { getApiUrl, setApiUrlOverride } from "../lib/config.js";
31
+ import { getApiUrl, getWebsiteUrl, setApiUrlOverride } from "../lib/config.js";
32
32
  import { fmt } from "../lib/format.js";
33
33
  import { getAgentName, mergeAgentConfig } from "../lib/agent-config.js";
34
34
  const POLL_INTERVAL_MS = 3000;
@@ -352,21 +352,44 @@ async function ensureSession(ctx, opts = {}) {
352
352
  */
353
353
  const AUTH_PROPAGATION_GRACE_MS = 20_000;
354
354
  /**
355
- * Classify a retriable (401/403) card-write failure.
355
+ * The 403 codes that mean "the one-time protection grant hasn't landed yet":
356
+ * mnemom-api's `agent_scope_forbidden` (a protection write with no live one-time
357
+ * capability) and the RFC 6750 `insufficient_scope` the manifest names.
358
+ */
359
+ const GRANT_PENDING_CODES = new Set(["agent_scope_forbidden", "insufficient_scope"]);
360
+ /**
361
+ * Classify a 401/403 card-write failure BY ITS ERROR CODE, never by status alone.
356
362
  *
357
- * - `grant-pending`: a PROTECTION 403 — the one-time CISO grant hasn't landed in
358
- * the browser yet (the manifest's `insufficient_scope`). This is expected and
359
- * is polled for the full window; it must NOT trigger a re-login. (Alignment is
360
- * written first and has no grant, so a stale/wrong-account session is caught
361
- * there before protection runs — making a protection 403 reliably the grant.)
363
+ * - `grant-pending`: a PROTECTION 403 whose code says the one-time CISO grant
364
+ * hasn't landed in the browser yet. This is expected and is polled for the
365
+ * window; it must NOT trigger a re-login.
366
+ * - `refused`: any OTHER protection 403 (e.g. `feature_gated`, a billing
367
+ * refusal). Waiting cannot fix it, so it fails fast with the server's reason
368
+ * instead of polling for a grant the human already approved.
362
369
  * - `auth`: a 401 anywhere, or an alignment 403 — the caller isn't authorized.
363
370
  * After a short propagation grace this means the session is stale/mismatched.
364
371
  */
365
- function classifyAuthError(kind, status) {
366
- if (status === 403 && kind === "protection")
367
- return "grant-pending";
372
+ export function classifyAuthError(kind, status, code) {
373
+ if (status === 403 && kind === "protection") {
374
+ return code !== undefined && GRANT_PENDING_CODES.has(code) ? "grant-pending" : "refused";
375
+ }
368
376
  return "auth";
369
377
  }
378
+ /**
379
+ * The error for a card write that waiting or re-authenticating cannot fix: the
380
+ * server's own reason (and code), with a recharge link when it is about µ.
381
+ */
382
+ function cardWriteRefusal(kind, agentId, err) {
383
+ const outOfMu = describeOutOfMu({ status: err.effectiveStatus, code: err.code, message: err.message, details: err.details }, getWebsiteUrl());
384
+ if (outOfMu) {
385
+ return new Error(`Couldn't set the ${kind} card. ${outOfMu} (--resume ${agentId})`, {
386
+ cause: err,
387
+ });
388
+ }
389
+ return new Error(`The server refused the ${kind} card for ${agentId}: ${err.message}` +
390
+ (err.code ? ` (${err.code})` : "") +
391
+ `. Fix that, then re-run with --resume ${agentId}.`, { cause: err });
392
+ }
370
393
  /**
371
394
  * Write a card, tolerating the two legitimate transient unauthorized states and
372
395
  * escalating a persistent one instead of hanging:
@@ -397,10 +420,18 @@ async function writeCardWithRetry(kind, card, timeoutSeconds, ctx) {
397
420
  return;
398
421
  }
399
422
  catch (err) {
400
- const status = err instanceof MnemomApiError ? err.effectiveStatus : undefined;
423
+ if (!(err instanceof MnemomApiError))
424
+ throw err; // not a server answer — surface it
425
+ const status = err.effectiveStatus;
426
+ // Out of µ / no billing account: a recharge prompt, not the raw error.
427
+ if (status === 402)
428
+ throw cardWriteRefusal(kind, agentId, err);
401
429
  if (status !== 401 && status !== 403)
402
430
  throw err; // a real error — surface it
403
- const kindOfFailure = classifyAuthError(kind, status);
431
+ const kindOfFailure = classifyAuthError(kind, status, err.code);
432
+ // Not a grant that hasn't landed: waiting won't help — say why, now.
433
+ if (kindOfFailure === "refused")
434
+ throw cardWriteRefusal(kind, agentId, err);
404
435
  // A stale/mismatched session: once the short propagation grace is spent,
405
436
  // re-authenticate as the agent's owner and retry the write exactly once.
406
437
  if (kindOfFailure === "auth" && Date.now() >= authGraceDeadline) {
@@ -256,8 +256,8 @@ function buildStarterAlignmentCard(agentId) {
256
256
  return {
257
257
  card_version: "unified/2026-04-26",
258
258
  agent_id: agentId,
259
- autonomy_mode: "observe",
260
- integrity_mode: "observe",
259
+ autonomy_mode: "off",
260
+ integrity_mode: "off",
261
261
  principal: { type: "agent", identifier: agentId, relationship: "delegated_authority" },
262
262
  values: { declared: ["transparency", "safety", "honesty"] },
263
263
  autonomy: {
@@ -272,7 +272,7 @@ function buildStarterProtectionCard(agentId) {
272
272
  return {
273
273
  card_version: "protection/2026-04-26",
274
274
  agent_id: agentId,
275
- mode: "observe",
275
+ mode: "off",
276
276
  thresholds: { warn: 0.3, quarantine: 0.6, block: 0.9 },
277
277
  screen_surfaces: {
278
278
  incoming: true,
package/dist/index.js CHANGED
@@ -168,8 +168,8 @@ const agentCmd = program
168
168
  ]))
169
169
  .option("--trim-mcp", "Launch the CLI with no MCP servers (--strict-mcp-config + empty --mcp-config)")
170
170
  .option("--invite <token>", "Redeem a Mnemom Agent invitation (pinv_…): unlocks Mnemom Agent for you + credits your starter µ; launches too when a session name is given")
171
- .option("--setup", "Provision + claim the governed agent before launch (non-interactive opt-in)")
172
- .option("--no-setup", "Skip governed-agent provisioning entirely for this launch")
171
+ .option("--setup", "Re-check the served agent and refresh its default cards before launch")
172
+ .option("--no-setup", "Skip the default-card writes (the agent is still confirmed and claimed)")
173
173
  .option("--no-yolo", "Do NOT pass --dangerously-skip-permissions (yolo is on by default)")
174
174
  .option("--no-context-hint", "Do NOT seed Claude Code's context-hint feature flag before launch")
175
175
  .addOption(new Option("--slack", "EXPERIMENTAL (`mnemom experimental enable slack`): attach the Slack channel — messages addressed to this session arrive in it, and it can reply (see `mnemom agent slack`)").hideHelp(hiddenUnless("slack")))
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Which agent the gateway serves a SUBSCRIPTION-login launch (claude.ai login + a Mnemom API
3
+ * key) for one agent name — `POST /mnemom/agent/identity` on the gateway.
4
+ *
5
+ * An Anthropic-key launch learns its served agent from a 16-token birth completion
6
+ * (birthThroughGateway). A subscription launch has no Anthropic key to send, so the gateway
7
+ * exposes this route instead: it resolves the agent exactly as a proxied subscription request
8
+ * with the same Mnemom key + name would (the key owner's claimed agent of that name, else the
9
+ * hash(Mnemom key, name) agent, birthed if absent) and makes no upstream call. The key rides
10
+ * only in the request header and is never logged or echoed.
11
+ */
12
+ export declare const IDENTITY_ROUTE_PATH = "/mnemom/agent/identity";
13
+ /**
14
+ * The gateway answered 404: the identity route is not deployed on this cell. A subscription
15
+ * launch can't confirm its agent is claimed without it, so the launcher refuses.
16
+ */
17
+ export declare class IdentityRouteMissingError extends Error {
18
+ readonly gatewayUrl: string;
19
+ constructor(gatewayUrl: string);
20
+ }
21
+ /**
22
+ * The mnm- id the gateway serves for (Mnemom key, name). Its response also carries claimed /
23
+ * org_id / resolved_by / created; the launcher reads the claim state from the api instead, so
24
+ * only the id is used here.
25
+ */
26
+ export declare function resolveServedIdentity(gatewayUrl: string, agentName: string, mnemomKey: string, opts?: {
27
+ timeoutMs?: number;
28
+ }): Promise<string>;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Which agent the gateway serves a SUBSCRIPTION-login launch (claude.ai login + a Mnemom API
3
+ * key) for one agent name — `POST /mnemom/agent/identity` on the gateway.
4
+ *
5
+ * An Anthropic-key launch learns its served agent from a 16-token birth completion
6
+ * (birthThroughGateway). A subscription launch has no Anthropic key to send, so the gateway
7
+ * exposes this route instead: it resolves the agent exactly as a proxied subscription request
8
+ * with the same Mnemom key + name would (the key owner's claimed agent of that name, else the
9
+ * hash(Mnemom key, name) agent, birthed if absent) and makes no upstream call. The key rides
10
+ * only in the request header and is never logged or echoed.
11
+ */
12
+ import { MnemomApiError } from "./api.js";
13
+ export const IDENTITY_ROUTE_PATH = "/mnemom/agent/identity";
14
+ /**
15
+ * The gateway answered 404: the identity route is not deployed on this cell. A subscription
16
+ * launch can't confirm its agent is claimed without it, so the launcher refuses.
17
+ */
18
+ export class IdentityRouteMissingError extends Error {
19
+ gatewayUrl;
20
+ constructor(gatewayUrl) {
21
+ super(`this gateway (${gatewayUrl}) can't tell which agent a subscription login runs as yet ` +
22
+ `(POST ${IDENTITY_ROUTE_PATH} is not deployed there)`);
23
+ this.gatewayUrl = gatewayUrl;
24
+ this.name = "IdentityRouteMissingError";
25
+ }
26
+ }
27
+ /**
28
+ * The mnm- id the gateway serves for (Mnemom key, name). Its response also carries claimed /
29
+ * org_id / resolved_by / created; the launcher reads the claim state from the api instead, so
30
+ * only the id is used here.
31
+ */
32
+ export async function resolveServedIdentity(gatewayUrl, agentName, mnemomKey, opts = {}) {
33
+ const response = await fetch(`${gatewayUrl}${IDENTITY_ROUTE_PATH}`, {
34
+ method: "POST",
35
+ headers: {
36
+ Accept: "application/json",
37
+ "x-mnemom-api-key": mnemomKey,
38
+ "x-mnemom-agent": agentName,
39
+ },
40
+ signal: opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined,
41
+ });
42
+ if (response.status === 404)
43
+ throw new IdentityRouteMissingError(gatewayUrl);
44
+ if (response.status === 401) {
45
+ throw new MnemomApiError(401, "the gateway rejected your Mnemom API key");
46
+ }
47
+ if (!response.ok) {
48
+ const text = await response.text().catch(() => "");
49
+ throw new MnemomApiError(response.status, `Gateway returned ${response.status}: ${text.slice(0, 300)}`);
50
+ }
51
+ const body = (await response.json().catch(() => null));
52
+ const agentId = body?.agent_id;
53
+ if (typeof agentId !== "string" || !/^mnm-/.test(agentId)) {
54
+ throw new Error(`Gateway returned no mnm- agent id from ${IDENTITY_ROUTE_PATH}.`);
55
+ }
56
+ return agentId;
57
+ }
@@ -8,8 +8,8 @@
8
8
  * - Claude subscription, Pro / Max (individual) → not supported (Team/Enterprise only).
9
9
  * - Anthropic Console login (`/login` → Console account) → `auth = console` (supported): the
10
10
  * API key Claude Code created is read from its keychain (lib/console-key.ts). A
11
- * `claude auth login --console` profile creates no key → not supported yet.
12
- * - Third-party provider (Bedrock / Vertex / Foundry) → not supported yet (no gateway door).
11
+ * `claude auth login --console` profile creates no key → not supported.
12
+ * - Third-party provider (Bedrock / Vertex / Foundry) → not supported (no gateway door).
13
13
  * An Anthropic API key always works (`auth = api-key`, the default).
14
14
  *
15
15
  * The signals are Claude Code's own client-side state (~/.claude.json `oauthAccount`, the
@@ -8,8 +8,8 @@
8
8
  * - Claude subscription, Pro / Max (individual) → not supported (Team/Enterprise only).
9
9
  * - Anthropic Console login (`/login` → Console account) → `auth = console` (supported): the
10
10
  * API key Claude Code created is read from its keychain (lib/console-key.ts). A
11
- * `claude auth login --console` profile creates no key → not supported yet.
12
- * - Third-party provider (Bedrock / Vertex / Foundry) → not supported yet (no gateway door).
11
+ * `claude auth login --console` profile creates no key → not supported.
12
+ * - Third-party provider (Bedrock / Vertex / Foundry) → not supported (no gateway door).
13
13
  * An Anthropic API key always works (`auth = api-key`, the default).
14
14
  *
15
15
  * The signals are Claude Code's own client-side state (~/.claude.json `oauthAccount`, the
@@ -63,9 +63,9 @@ export function unsupportedReason(login) {
63
63
  case "individual":
64
64
  return `your Claude ${login.plan} plan is a personal plan — Mnemom supports subscription login on Team and Enterprise plans only`;
65
65
  case "console":
66
- return "your Anthropic Console login has no API key Claude Code created with /login (a `claude auth login --console` profile is not supported yet)";
66
+ return "your Anthropic Console login has no API key from Claude Code's /login (Mnemom uses the key /login creates)";
67
67
  case "third-party":
68
- return `${PROVIDER_NAMES[login.provider]} is not supported yet`;
68
+ return `Claude Code is set to use ${PROVIDER_NAMES[login.provider]}, and Mnemom supports Anthropic sign-in only (an Anthropic API key, an Anthropic Console login, or a Claude Team or Enterprise plan)`;
69
69
  }
70
70
  }
71
71
  export const MENU_SUBSCRIPTION = "Claude subscription — Team or Enterprise (your claude.ai login)";
@@ -5,6 +5,10 @@
5
5
  * once a launch has asked, later launches reuse it instead of paying for another probe
6
6
  * completion. Entries are keyed by a one-way fingerprint and never hold the key itself (nor the
7
7
  * sha256(key|name) claim proof). `--setup` always re-asks and overwrites.
8
+ *
9
+ * A second file records the agent this machine last CONFIRMED claimed for the same fingerprint.
10
+ * It is what a launch falls back to when Mnemom can't be reached to confirm the claim (an
11
+ * outage degrades to the last confirmed agent, never to an unclaimed one).
8
12
  */
9
13
  /**
10
14
  * A slow KDF (scrypt), not a fast hash: the fingerprint is derived from a credential and lands on
@@ -16,3 +20,7 @@ export declare function servedFingerprint(anthropicKey: string, slug: string, ga
16
20
  export declare function readServedAgent(dir: string, fingerprint: string): string | null;
17
21
  /** Best effort: a failed write only means the next launch asks again. */
18
22
  export declare function writeServedAgent(dir: string, fingerprint: string, agentId: string): void;
23
+ /** The agent last confirmed claimed for this fingerprint, or null. */
24
+ export declare function readConfirmedAgent(dir: string, fingerprint: string): string | null;
25
+ /** Best effort: a failed write only means an outage can't fall back to this agent. */
26
+ export declare function writeConfirmedAgent(dir: string, fingerprint: string, agentId: string): void;
@@ -5,11 +5,16 @@
5
5
  * once a launch has asked, later launches reuse it instead of paying for another probe
6
6
  * completion. Entries are keyed by a one-way fingerprint and never hold the key itself (nor the
7
7
  * sha256(key|name) claim proof). `--setup` always re-asks and overwrites.
8
+ *
9
+ * A second file records the agent this machine last CONFIRMED claimed for the same fingerprint.
10
+ * It is what a launch falls back to when Mnemom can't be reached to confirm the claim (an
11
+ * outage degrades to the last confirmed agent, never to an unclaimed one).
8
12
  */
9
13
  import { scryptSync } from "node:crypto";
10
14
  import * as fs from "node:fs";
11
15
  import * as path from "node:path";
12
16
  const FILE = "served-agents.json";
17
+ const CONFIRMED_FILE = "confirmed-agents.json";
13
18
  /**
14
19
  * A slow KDF (scrypt), not a fast hash: the fingerprint is derived from a credential and lands on
15
20
  * disk, so recovering the key from it must cost what guessing a password would. Name and
@@ -18,9 +23,9 @@ const FILE = "served-agents.json";
18
23
  export function servedFingerprint(anthropicKey, slug, gateway) {
19
24
  return scryptSync(anthropicKey, `mnemom-served-agent-v1\n${slug}\n${gateway}`, 32).toString("hex");
20
25
  }
21
- function readAll(dir) {
26
+ function readAll(dir, file = FILE) {
22
27
  try {
23
- const parsed = JSON.parse(fs.readFileSync(path.join(dir, FILE), "utf8"));
28
+ const parsed = JSON.parse(fs.readFileSync(path.join(dir, file), "utf8"));
24
29
  return parsed && typeof parsed === "object" && !Array.isArray(parsed)
25
30
  ? parsed
26
31
  : {};
@@ -31,18 +36,30 @@ function readAll(dir) {
31
36
  }
32
37
  /** The cached served id, or null (missing, unreadable, or not an mnm- id). */
33
38
  export function readServedAgent(dir, fingerprint) {
34
- const id = readAll(dir)[fingerprint];
35
- return typeof id === "string" && /^mnm-/.test(id) ? id : null;
39
+ return readId(dir, FILE, fingerprint);
36
40
  }
37
41
  /** Best effort: a failed write only means the next launch asks again. */
38
42
  export function writeServedAgent(dir, fingerprint, agentId) {
43
+ writeId(dir, FILE, fingerprint, agentId);
44
+ }
45
+ /** The agent last confirmed claimed for this fingerprint, or null. */
46
+ export function readConfirmedAgent(dir, fingerprint) {
47
+ return readId(dir, CONFIRMED_FILE, fingerprint);
48
+ }
49
+ /** Best effort: a failed write only means an outage can't fall back to this agent. */
50
+ export function writeConfirmedAgent(dir, fingerprint, agentId) {
51
+ writeId(dir, CONFIRMED_FILE, fingerprint, agentId);
52
+ }
53
+ function readId(dir, file, fingerprint) {
54
+ const id = readAll(dir, file)[fingerprint];
55
+ return typeof id === "string" && /^mnm-/.test(id) ? id : null;
56
+ }
57
+ function writeId(dir, file, fingerprint, agentId) {
39
58
  try {
40
59
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
41
- const tmp = path.join(dir, `${FILE}.${process.pid}.tmp`);
42
- fs.writeFileSync(tmp, JSON.stringify({ ...readAll(dir), [fingerprint]: agentId }, null, 2), {
43
- mode: 0o600,
44
- });
45
- fs.renameSync(tmp, path.join(dir, FILE));
60
+ const tmp = path.join(dir, `${file}.${process.pid}.tmp`);
61
+ fs.writeFileSync(tmp, JSON.stringify({ ...readAll(dir, file), [fingerprint]: agentId }, null, 2), { mode: 0o600 });
62
+ fs.renameSync(tmp, path.join(dir, file));
46
63
  }
47
64
  catch {
48
65
  /* best effort */
@@ -162,6 +162,24 @@ export interface BirthResult {
162
162
  export declare function birthAgent(manifest: BriefingManifest, name: string, opts?: {
163
163
  fetchImpl?: typeof fetch;
164
164
  }): Promise<BirthResult>;
165
+ /** A server refusal, as the CLI's error types carry it. */
166
+ export interface ServerRefusal {
167
+ status?: number;
168
+ code?: string;
169
+ message?: string;
170
+ details?: unknown;
171
+ }
172
+ /**
173
+ * Where to recharge: the server's `details.top_up_url` (absolute, or a path on
174
+ * the website), else the website's billing page.
175
+ */
176
+ export declare function rechargeUrl(details: unknown, websiteBase?: string): string;
177
+ /**
178
+ * The human message for a refusal that means the org is out of µ (or has no
179
+ * billing account): what happened, where to recharge, and how to resume.
180
+ * Undefined for any other refusal.
181
+ */
182
+ export declare function describeOutOfMu(refusal: ServerRefusal, websiteBase?: string): string | undefined;
165
183
  /** The slice of the public agent projection the claim poll reads. */
166
184
  export interface PublicAgentStatus {
167
185
  id?: string;