@mnemom/mnemom 0.17.3-next.6 → 0.17.4-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.
@@ -14,7 +14,9 @@
14
14
  import { spawnSync } from "node:child_process";
15
15
  import { existsSync } from "node:fs";
16
16
  import { anthropicKeySource, checkRcCapability, resolveCliBin } from "./agent.js";
17
- import { isProdGateway, resolveAnthropicDoor, resolveGatewayHost } from "../lib/agent.js";
17
+ import { resolveAgentAuth } from "../lib/subscription-login.js";
18
+ import { isProdGateway, resolveAnthropicDoor, resolveGatewayHost, singleCellError, } from "../lib/agent.js";
19
+ import { getApiUrl, getGatewayUrl } from "../lib/config.js";
18
20
  import { LAUNCHER_CONFIG_PATH } from "../lib/launcher-config.js";
19
21
  import { getCodeEntitlement, MnemomApiError } from "../lib/api.js";
20
22
  import { isLoggedIn } from "../lib/auth.js";
@@ -44,14 +46,34 @@ export async function agentDoctorCommand(options = {}) {
44
46
  catch (err) {
45
47
  add("miss", `coding-agent CLI: ${err instanceof Error ? err.message.split("\n")[0] : err}`, "install Claude Code, or pass --cli <name|/full/path> / set it with `mnemom agent config set cli <path>`");
46
48
  }
47
- // ── Anthropic key ──
48
- const keySrc = anthropicKeySource(env);
49
- if (keySrc === "env")
50
- add("ok", "Anthropic key: found in the environment");
51
- else if (keySrc === "store")
52
- add("ok", "Anthropic key: found in ~/.mnemom/launcher.json");
53
- else
54
- add("miss", "Anthropic key: none found", "export MNEMOM_AGENT_ANTHROPIC_KEY=sk-ant-… (or ANTHROPIC_API_KEY), or run `mnemom agent` once and accept the save prompt");
49
+ // ── auth: an Anthropic key, or the claude.ai subscription login (no key needed) ──
50
+ let auth = "api-key";
51
+ try {
52
+ auth = resolveAgentAuth(undefined, env);
53
+ }
54
+ catch (err) {
55
+ add("miss", `auth: ${err instanceof Error ? err.message : String(err)}`, "`mnemom agent config set auth api-key` (or subscription)");
56
+ }
57
+ if (auth === "subscription") {
58
+ add("ok", "auth: subscription — Claude Code uses your claude.ai login (Team/Enterprise plans only); no Anthropic key needed");
59
+ }
60
+ else {
61
+ const keySrc = anthropicKeySource(env);
62
+ if (keySrc === "env")
63
+ add("ok", "Anthropic key: found in the environment");
64
+ else if (keySrc === "store")
65
+ add("ok", "Anthropic key: found in ~/.mnemom/launcher.json");
66
+ else
67
+ add("miss", "Anthropic key: none found", "export MNEMOM_AGENT_ANTHROPIC_KEY=sk-ant-… (or ANTHROPIC_API_KEY), or run `mnemom agent` once and accept the save prompt");
68
+ }
69
+ // ── one cell: the launch door must be the CLI's own cell ──
70
+ const cellError = singleCellError({
71
+ door: resolveAnthropicDoor(env),
72
+ apiUrl: getApiUrl(),
73
+ cellGateway: getGatewayUrl(),
74
+ });
75
+ if (cellError)
76
+ add("miss", "cell: mixed — a launch would be refused", cellError);
55
77
  // ── gateway health ──
56
78
  const host = resolveGatewayHost(env);
57
79
  const cell = isProdGateway(env) ? "us-2/prod" : "custom";
@@ -26,6 +26,11 @@ export interface SessionCommandDeps {
26
26
  * one `live goal` line an implicit session gets. Optional: absent ⇒ no line.
27
27
  */
28
28
  fetchGoal?: (record: SessionRecord) => Promise<GoalShowResponse>;
29
+ /**
30
+ * The gateway of the cell this CLI reads live state from. A session that ran on another
31
+ * cell is not looked up there (it would always read as "no sealed contract"). Absent ⇒ no check.
32
+ */
33
+ cellGateway?: () => string;
29
34
  out: (line: string) => void;
30
35
  err: (line: string) => void;
31
36
  }
@@ -14,7 +14,8 @@
14
14
  * only the server's hash is shown, labelled "sealed hash".
15
15
  */
16
16
  import { readSessionRecords } from "../lib/agent-ledger.js";
17
- import { MNEMOM_DIR } from "../lib/config.js";
17
+ import { getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
18
+ import { sameOrigin } from "../lib/agent.js";
18
19
  import { getAgentSession, MnemomApiError } from "../lib/api.js";
19
20
  import { fmt, formatRelativeAge } from "../lib/format.js";
20
21
  import { goalControlFetch } from "../lib/goal-api.js";
@@ -31,6 +32,7 @@ export const liveSessionDeps = {
31
32
  const creds = await resolveLaunchCreds();
32
33
  return goalControlFetch(record, "show", { thread: "main" }, { fetch, creds });
33
34
  },
35
+ cellGateway: () => getGatewayUrl(),
34
36
  out: (line) => console.log(line),
35
37
  err: (line) => console.error(line),
36
38
  };
@@ -318,9 +320,13 @@ export async function agentShowCommand(ref, opts, deps) {
318
320
  const { record, others } = resolved;
319
321
  let remote = null;
320
322
  let reason;
323
+ const cell = deps.cellGateway?.();
321
324
  if (record.agent_id === null) {
322
325
  reason = "agent id was not resolved at launch";
323
326
  }
327
+ else if (cell !== undefined && !sameOrigin(record.gateway_host, cell)) {
328
+ reason = `this session ran on ${record.gateway_host}, not this CLI's cell (${cell}) — point the CLI at that cell to read it`;
329
+ }
324
330
  else {
325
331
  try {
326
332
  remote = await deps.fetchSession(record.conversation_id, record.agent_id, record.org_id);
@@ -26,10 +26,10 @@
26
26
  import { claimAgent, getAgentByName, getMe, getMuBalance, getMyPersonalOrg, MnemomApiError, mintProtectionCapability, putAlignmentCard, putProtectionCard, } from "../lib/api.js";
27
27
  import { getAuthInfo, isLoggedIn, loginWithBrowser, loginWithDeviceFlow, resolveAuth, } from "../lib/auth.js";
28
28
  import { getActiveOrg } from "../lib/cli-config.js";
29
- import { getWebsiteUrl } from "../lib/config.js";
29
+ import { getApiUrl, getGatewayUrl, getWebsiteUrl } from "../lib/config.js";
30
30
  import { fmt } from "../lib/format.js";
31
31
  import { askYesNo, isInteractive } from "../lib/prompt.js";
32
- import { resolveGatewayHost, sanitizeAgentName } from "../lib/agent.js";
32
+ import { resolveAnthropicDoor, resolveGatewayHost, sanitizeAgentName, singleCellError, } from "../lib/agent.js";
33
33
  import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
34
34
  import { deriveHashProof } from "./agents.js";
35
35
  import { birthThroughGateway } from "./wrap.js";
@@ -273,6 +273,13 @@ async function publishDefaultCards(agentId) {
273
273
  /** `mnemom agent setup [--agent <slug>]` — the explicit provisioning entry point. */
274
274
  export async function agentSetupCommand(opts = {}) {
275
275
  const gatewayHost = resolveGatewayHost(process.env);
276
+ const cellError = singleCellError({
277
+ door: resolveAnthropicDoor(process.env),
278
+ apiUrl: getApiUrl(),
279
+ cellGateway: getGatewayUrl(),
280
+ });
281
+ if (cellError)
282
+ throw new Error(cellError.replace("mnemom agent:", "mnemom agent setup:"));
276
283
  // Sign in (offer sign-up) and enforce the MU balance before any account write.
277
284
  try {
278
285
  await ensureSessionAndBalance({});
@@ -25,6 +25,7 @@
25
25
  * line, or the proxy's stdin under RC). It is never logged, printed, or passed as
26
26
  * an argv value.
27
27
  */
28
+ import { type MnemomKeyDeps } from "../lib/subscription-login.js";
28
29
  import { type ExperimentalId } from "../lib/experimental.js";
29
30
  import { type ContextProfile, type LaunchShape, type SealedContractEnvelope } from "../lib/agent.js";
30
31
  export interface AgentOptions {
@@ -48,6 +49,8 @@ export interface AgentOptions {
48
49
  cli?: string;
49
50
  remoteControl?: boolean;
50
51
  server?: boolean;
52
+ /** `--auth api-key|subscription` (lib/subscription-login.ts); else MNEMOM_AGENT_AUTH / config. */
53
+ auth?: string;
51
54
  model?: string;
52
55
  effort?: string;
53
56
  trimMcp?: boolean;
@@ -124,6 +127,8 @@ export declare function recordSessionLaunch(ctx: {
124
127
  /** The resolved context profile this launch runs under. */
125
128
  contextProfile: ContextProfile;
126
129
  }): Promise<void>;
130
+ /** The real I/O behind a subscription launch's Mnemom key (lib/subscription-login.ts). */
131
+ export declare function defaultMnemomKeyDeps(storePath?: string): MnemomKeyDeps;
127
132
  /**
128
133
  * Resolve the Anthropic key with NO 1Password dependency (built for external
129
134
  * customer adoption), in order:
@@ -28,11 +28,12 @@
28
28
  import { spawn, spawnSync } from "node:child_process";
29
29
  import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync, renameSync, } from "node:fs";
30
30
  import { randomUUID } from "node:crypto";
31
- import { tmpdir, homedir } from "node:os";
31
+ import { tmpdir, homedir, hostname } from "node:os";
32
32
  import { basename, join, dirname } from "node:path";
33
33
  import { fileURLToPath } from "node:url";
34
- import { MNEMOM_DIR } from "../lib/config.js";
35
- import { getAgentByName } from "../lib/api.js";
34
+ import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
35
+ import { createApiKey, getAgentByName } from "../lib/api.js";
36
+ import { MNEMOM_KEY_STORE_FIELD, resolveAgentAuth, resolveSubscriptionMnemomKey, subscriptionChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
36
37
  import { LEDGER_FILE, appendSessionRecord, resolveRetentionDays, toLedgerContract, } from "../lib/agent-ledger.js";
37
38
  import { loadCliConfig } from "../lib/cli-config.js";
38
39
  import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
@@ -41,7 +42,7 @@ import { fmt } from "../lib/format.js";
41
42
  import { askInput, askYesNo, isInteractive } from "../lib/prompt.js";
42
43
  import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
43
44
  import { AgentIdentityMismatchError, defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
44
- import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
45
+ import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
45
46
  import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
46
47
  import { slackChannelArgs } from "../lib/slack-launch.js";
47
48
  /** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
@@ -77,6 +78,9 @@ export async function agentCommand(scenarioArg, options = {}) {
77
78
  const env = process.env;
78
79
  let shape = resolveLaunchShape({ remoteControl: options.remoteControl, server: options.server }, env);
79
80
  const shapeExplicit = launchPreferenceExplicit({ remoteControl: options.remoteControl, server: options.server }, env);
81
+ // How Claude Code authenticates: an Anthropic key (default) or the user's claude.ai
82
+ // subscription login (Team/Enterprise; lib/subscription-login.ts).
83
+ const auth = resolveAgentAuth(options.auth, env);
80
84
  // ── governed identity ── (default: per-customer `agent-<handle>`, not a shared
81
85
  // "code-agent" — see defaultAgentSlug; provisioning claims exactly this slug.
82
86
  // `let` because a brand-new user who signs in during the pre-launch gate below
@@ -109,6 +113,9 @@ export async function agentCommand(scenarioArg, options = {}) {
109
113
  // ── door ──
110
114
  const gatewayHost = resolveGatewayHost(env);
111
115
  const door = resolveAnthropicDoor(env);
116
+ const cellError = singleCellError({ door, apiUrl: getApiUrl(), cellGateway: getGatewayUrl() });
117
+ if (cellError)
118
+ throw new Error(cellError);
112
119
  const cellNote = isProdGateway(env) ? "us-2/prod" : `custom (${gatewayHost})`;
113
120
  const yolo = options.yolo !== false; // default ON
114
121
  const contextHint = options.contextHint !== false; // default ON
@@ -121,7 +128,12 @@ export async function agentCommand(scenarioArg, options = {}) {
121
128
  context: options.context,
122
129
  env,
123
130
  });
124
- const contextHandoff = resolveContextHandoff({ contextHandoff: options.contextHandoff, env });
131
+ let contextHandoff = resolveContextHandoff({ contextHandoff: options.contextHandoff, env });
132
+ if (contextHandoff && auth === "subscription") {
133
+ // The handoff tool authenticates with the Anthropic key a subscription launch does not have.
134
+ say("mnemom agent: context handoff is not available with subscription login yet — launching without it.");
135
+ contextHandoff = false;
136
+ }
125
137
  const experimental = options.experimental ?? [];
126
138
  const sessionConfig = buildSessionConfig({ profile: contextProfile, contextHandoff });
127
139
  const sessionConfigB64 = buildSessionConfigB64({
@@ -172,6 +184,9 @@ export async function agentCommand(scenarioArg, options = {}) {
172
184
  sessionConfig,
173
185
  experimental,
174
186
  });
187
+ if (auth === "subscription") {
188
+ 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, terminal-only.");
189
+ }
175
190
  if (expNotice)
176
191
  say(expNotice);
177
192
  if (previewShape === "remote-control") {
@@ -190,10 +205,10 @@ export async function agentCommand(scenarioArg, options = {}) {
190
205
  cli = await promptForCliPath(options.cli, env, cliError);
191
206
  }
192
207
  // ── resolve the Anthropic key (zero 1Password; never logged) ──
193
- const anthropicKey = await resolveAnthropicKey(env, {
194
- allowPrompt: true,
195
- keySource: options.keySource,
196
- });
208
+ // Not on a subscription launch: Claude Code then sends its own claude.ai login instead.
209
+ const anthropicKey = auth === "api-key"
210
+ ? await resolveAnthropicKey(env, { allowPrompt: true, keySource: options.keySource })
211
+ : undefined;
197
212
  // ── account + µ-balance gate (no free tier) ─────────────────────────────────
198
213
  // mnemom agent runs governed and on µ, so before we seal a contract or launch we
199
214
  // (1) ensure a Mnemom session — offering the try-me-style sign-in / sign-up when
@@ -242,6 +257,14 @@ export async function agentCommand(scenarioArg, options = {}) {
242
257
  rcCapable: rc.ok,
243
258
  say,
244
259
  });
260
+ // Subscription login: terminal-only for now, and the governed identity anchors on a
261
+ // Mnemom API key (minted once and stored if there is none). Resolved after sign-in so
262
+ // minting can use the session.
263
+ let mnemomKey;
264
+ if (auth === "subscription") {
265
+ shape = subscriptionShape(shape, shapeExplicit, say);
266
+ mnemomKey = await resolveSubscriptionMnemomKey(defaultMnemomKeyDeps());
267
+ }
245
268
  // Now that a session exists, a defaulted slug uses the real login handle from
246
269
  // /v1/auth/me (the stored token has no email, so the pre-sign-in value was the
247
270
  // "agent-user" fallback). Only when the caller didn't name an agent. Under an
@@ -258,7 +281,12 @@ export async function agentCommand(scenarioArg, options = {}) {
258
281
  // provisions-with-confirm when interactive; warns and continues otherwise.
259
282
  // Skipped entirely by `--no-setup`. A provisioning failure never blocks the
260
283
  // launch — it degrades to the unclaimed (fail-open) identity with a warning.
261
- if (options.setup !== false) {
284
+ if (options.setup !== false && !anthropicKey) {
285
+ // Subscription login: the gateway creates the agent in the Mnemom key's org on the first
286
+ // turn, so there is nothing to birth or claim here.
287
+ say(`mnemom agent: subscription login — '${agent}' is created in your org on the first turn (identity: your Mnemom API key).`);
288
+ }
289
+ else if (options.setup !== false && anthropicKey) {
262
290
  try {
263
291
  await ensureGovernedAgent({
264
292
  slug: agent,
@@ -340,11 +368,13 @@ export async function agentCommand(scenarioArg, options = {}) {
340
368
  say(fmt.warn(`mnemom agent: context handoff could not be registered (${err instanceof Error ? err.message : String(err)}) — launching without the handoff tool.`));
341
369
  }
342
370
  }
343
- if (shape === "terminal") {
371
+ // A subscription launch (no Anthropic key) is always terminal — see subscriptionShape.
372
+ if (shape === "terminal" || !anthropicKey) {
344
373
  launchTerminal({
345
374
  cli,
346
375
  door,
347
376
  anthropicKey,
377
+ mnemomKey,
348
378
  agent,
349
379
  scenario,
350
380
  conversationId,
@@ -422,6 +452,25 @@ export async function recordSessionLaunch(ctx) {
422
452
  // ───────────────────────────────────────────────────────────────────────────
423
453
  // Credential resolution (zero 1Password)
424
454
  // ───────────────────────────────────────────────────────────────────────────
455
+ /** The real I/O behind a subscription launch's Mnemom key (lib/subscription-login.ts). */
456
+ export function defaultMnemomKeyDeps(storePath = CODE_STORE) {
457
+ return {
458
+ env: process.env,
459
+ readStored: () => {
460
+ try {
461
+ const data = JSON.parse(readFileSync(storePath, "utf8"));
462
+ const v = data[MNEMOM_KEY_STORE_FIELD];
463
+ return typeof v === "string" ? stripCrlf(v) : undefined;
464
+ }
465
+ catch {
466
+ return undefined;
467
+ }
468
+ },
469
+ mint: async () => (await createApiKey(`mnemom-agent ${hostname()}`, ["gateway"])).key,
470
+ store: (key) => mergeStore({ [MNEMOM_KEY_STORE_FIELD]: key }, storePath),
471
+ say,
472
+ };
473
+ }
425
474
  /**
426
475
  * Resolve the Anthropic key with NO 1Password dependency (built for external
427
476
  * customer adoption), in order:
@@ -657,6 +706,7 @@ function launchTerminal(ctx) {
657
706
  warnAdapter(ctx.cli);
658
707
  const headers = buildCustomHeaders({
659
708
  anthropicKey: ctx.anthropicKey,
709
+ mnemomKey: ctx.mnemomKey,
660
710
  agent: ctx.agent,
661
711
  conversationId: ctx.conversationId,
662
712
  contractB64: ctx.envelope?.base64,
@@ -672,11 +722,20 @@ function launchTerminal(ctx) {
672
722
  // Claude Code logged in via claude.ai (OAuth) PREEMPTS ANTHROPIC_API_KEY, so
673
723
  // it is force-unset; the real key rides in ANTHROPIC_CUSTOM_HEADERS and
674
724
  // ANTHROPIC_AUTH_TOKEN is a fixed non-secret placeholder.
675
- const childEnv = { ...process.env };
676
- delete childEnv.ANTHROPIC_API_KEY;
677
- childEnv.ANTHROPIC_BASE_URL = ctx.door;
678
- childEnv.ANTHROPIC_AUTH_TOKEN = "mnemom-gateway";
679
- childEnv.ANTHROPIC_CUSTOM_HEADERS = headers;
725
+ //
726
+ // A subscription launch (no Anthropic key) sets NO auth token: Claude Code then sends its
727
+ // own claude.ai login to the gateway (lib/subscription-login.ts).
728
+ let childEnv;
729
+ if (ctx.anthropicKey) {
730
+ childEnv = { ...process.env };
731
+ delete childEnv.ANTHROPIC_API_KEY;
732
+ childEnv.ANTHROPIC_BASE_URL = ctx.door;
733
+ childEnv.ANTHROPIC_AUTH_TOKEN = "mnemom-gateway";
734
+ childEnv.ANTHROPIC_CUSTOM_HEADERS = headers;
735
+ }
736
+ else {
737
+ childEnv = subscriptionChildEnv(process.env, ctx.door, headers);
738
+ }
680
739
  const sessionName = passthroughName(ctx.options.passthrough ?? []) ?? sessionDisplayName(ctx.agent, ctx.scenario);
681
740
  childEnv[SESSION_NAME_ENV] = sessionName;
682
741
  if (ctx.options.slack)
package/dist/index.js CHANGED
@@ -100,8 +100,8 @@ program
100
100
  // CONTRACT the gateway seals on the first turn. Three launch shapes — terminal
101
101
  // (default), --remote-control (interactive RC in this terminal), --server
102
102
  // (headless RC dispatcher) — with yolo on by default. `--cli` selects the
103
- // executable (name or full path; default claude). The us-2/prod door is the
104
- // DEFAULT; set MNEMOM_AGENT_GATEWAY (or --gateway) to point elsewhere. The
103
+ // executable (name or full path; default claude). The door is the gateway of the
104
+ // cell the CLI uses (prod by default); a door on another cell is refused. The
105
105
  // Anthropic key is resolved with zero 1Password dependency (env → ~/.mnemom store
106
106
  // → interactive prompt) inside the launched process and never logged. See
107
107
  // commands/code.ts.
@@ -139,9 +139,10 @@ const agentCmd = program
139
139
  .option("--no-implicit", "Turn implicit goal alignment OFF for this launch (overrides launcher.toml goal_mode = implicit)")
140
140
  .option("--conversation-id <id>", "x-mnemom-conversation-id (default: auto-generated)")
141
141
  .option("--cli <name|path>", "Coding-agent CLI to launch: a name or full path (default: claude)")
142
- .option("--gateway <url>", "Gateway host override (default: https://gateway.mnemom.ai; or MNEMOM_AGENT_GATEWAY)")
142
+ .option("--gateway <url>", "Gateway host override — must be the same cell as the rest of the CLI (default: that cell's gateway; or MNEMOM_AGENT_GATEWAY)")
143
143
  .option("--remote-control", "Interactive Remote Control session in THIS terminal (behind a local proxy)")
144
144
  .option("--server", "Headless `claude remote-control` dispatcher (behind a local proxy)")
145
+ .option("--auth <mode>", "How Claude Code authenticates: api-key (default) | subscription (your Claude Team/Enterprise login; terminal-only)")
145
146
  .option("--model <alias|id>", "Model to launch (CLI alias or concrete id)")
146
147
  .addOption(new Option("--effort <level>", "Reasoning effort").choices([
147
148
  "low",
@@ -278,6 +279,7 @@ const agentCmd = program
278
279
  conversationId: opts.conversationId,
279
280
  cli: opts.cli,
280
281
  remoteControl: opts.remoteControl,
282
+ auth: opts.auth,
281
283
  server: opts.server,
282
284
  model: opts.model ?? cfg.model,
283
285
  effort: opts.effort ?? cfg.effort,
@@ -63,11 +63,27 @@ export interface ContractFlags {
63
63
  export type LaunchShape = "terminal" | "remote-control" | "server";
64
64
  /**
65
65
  * Resolve the gateway HOST (scheme + host, no path). An explicit, non-empty
66
- * `MNEMOM_AGENT_GATEWAY` wins verbatim; otherwise the us-2/prod host. The default
67
- * is PROD — an unset var resolves to prod, and any other cell is reached only by
68
- * NAMING it. Trailing slash trimmed so door concatenation is clean.
69
- */
70
- export declare function resolveGatewayHost(env?: NodeJS.ProcessEnv): string;
66
+ * `MNEMOM_AGENT_GATEWAY` wins verbatim; otherwise the gateway of the cell the rest
67
+ * of the CLI uses (`getGatewayUrl()`: prod unless MNEMOM_ENV / MNEMOM_RING /
68
+ * MNEMOM_GATEWAY_URL name another). An override on a different cell is refused at
69
+ * launch — see `singleCellError`. Trailing slash trimmed so door concatenation is clean.
70
+ */
71
+ export declare function resolveGatewayHost(env?: NodeJS.ProcessEnv, cellGateway?: string): string;
72
+ /** Same scheme+host+port. An unparseable URL never matches. */
73
+ export declare function sameOrigin(a: string, b: string): boolean;
74
+ /**
75
+ * A launch must use ONE cell: the Anthropic `door` its traffic goes to must be the
76
+ * gateway of the cell the CLI signs in, resolves the agent and reads sessions on
77
+ * (`cellGateway`, paired with `apiUrl`). A mixed launch records one cell's agent
78
+ * and runs as another cell's, so `sessions`/`show` never find its goal state.
79
+ * Returns the refusal message, or null. A loopback door is a local dev gateway and
80
+ * is exempt.
81
+ */
82
+ export declare function singleCellError(opts: {
83
+ door: string;
84
+ apiUrl: string;
85
+ cellGateway: string;
86
+ }): string | null;
71
87
  /**
72
88
  * The Anthropic door the CLI points Claude Code at:
73
89
  * `${MNEMOM_AGENT_GATEWAY}/anthropic`, or the full-door override
@@ -267,7 +283,10 @@ export declare function resolveRemoteControlShape(args: {
267
283
  * callers must place it only in the child's env, never in a log line.
268
284
  */
269
285
  export declare function buildCustomHeaders(opts: {
270
- anthropicKey: string;
286
+ /** Absent on a subscription-login launch: Claude Code then sends its own claude.ai login. */
287
+ anthropicKey?: string;
288
+ /** The governed-identity anchor on a subscription-login launch (lib/subscription-login.ts). */
289
+ mnemomKey?: string;
271
290
  agent: string;
272
291
  conversationId: string;
273
292
  contractB64?: string;
package/dist/lib/agent.js CHANGED
@@ -20,19 +20,56 @@
20
20
  * NAMED), but prod is never selected silently — see `resolveGatewayHost`.
21
21
  */
22
22
  import { createHash, randomUUID } from "node:crypto";
23
+ import { getGatewayUrl } from "./config.js";
23
24
  /** The us-2/prod gateway host. The default door — never any other cell silently. */
24
25
  export const PROD_GATEWAY_HOST = "https://gateway.mnemom.ai";
25
26
  /**
26
27
  * Resolve the gateway HOST (scheme + host, no path). An explicit, non-empty
27
- * `MNEMOM_AGENT_GATEWAY` wins verbatim; otherwise the us-2/prod host. The default
28
- * is PROD — an unset var resolves to prod, and any other cell is reached only by
29
- * NAMING it. Trailing slash trimmed so door concatenation is clean.
28
+ * `MNEMOM_AGENT_GATEWAY` wins verbatim; otherwise the gateway of the cell the rest
29
+ * of the CLI uses (`getGatewayUrl()`: prod unless MNEMOM_ENV / MNEMOM_RING /
30
+ * MNEMOM_GATEWAY_URL name another). An override on a different cell is refused at
31
+ * launch — see `singleCellError`. Trailing slash trimmed so door concatenation is clean.
30
32
  */
31
- export function resolveGatewayHost(env = process.env) {
33
+ export function resolveGatewayHost(env = process.env, cellGateway = getGatewayUrl()) {
32
34
  const raw = (env.MNEMOM_AGENT_GATEWAY ?? "").trim();
33
- const host = raw.length > 0 ? raw : PROD_GATEWAY_HOST;
35
+ const host = raw.length > 0 ? raw : cellGateway;
34
36
  return host.replace(/\/+$/, "");
35
37
  }
38
+ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
39
+ /** Same scheme+host+port. An unparseable URL never matches. */
40
+ export function sameOrigin(a, b) {
41
+ try {
42
+ return new URL(a).origin === new URL(b).origin;
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ /**
49
+ * A launch must use ONE cell: the Anthropic `door` its traffic goes to must be the
50
+ * gateway of the cell the CLI signs in, resolves the agent and reads sessions on
51
+ * (`cellGateway`, paired with `apiUrl`). A mixed launch records one cell's agent
52
+ * and runs as another cell's, so `sessions`/`show` never find its goal state.
53
+ * Returns the refusal message, or null. A loopback door is a local dev gateway and
54
+ * is exempt.
55
+ */
56
+ export function singleCellError(opts) {
57
+ let doorUrl;
58
+ try {
59
+ doorUrl = new URL(opts.door);
60
+ }
61
+ catch {
62
+ return `mnemom agent: the gateway '${opts.door}' is not a valid URL.`;
63
+ }
64
+ if (LOOPBACK_HOSTS.has(doorUrl.hostname))
65
+ return null;
66
+ if (sameOrigin(opts.door, opts.cellGateway))
67
+ return null;
68
+ return (`mnemom agent: the gateway ${doorUrl.origin} is on a different cell from the one this CLI uses ` +
69
+ `(API ${opts.apiUrl}, gateway ${opts.cellGateway}). A launch must use one cell. Point the whole ` +
70
+ "CLI at that cell (MNEMOM_API_URL + MNEMOM_GATEWAY_URL, or MNEMOM_RING) and drop the gateway " +
71
+ "override (--gateway, MNEMOM_AGENT_GATEWAY, MNEMOM_AGENT_ANTHROPIC_GATEWAY, or `gateway` in launcher.toml).");
72
+ }
36
73
  /**
37
74
  * The Anthropic door the CLI points Claude Code at:
38
75
  * `${MNEMOM_AGENT_GATEWAY}/anthropic`, or the full-door override
@@ -429,7 +466,8 @@ export function resolveRemoteControlShape(args) {
429
466
  */
430
467
  export function buildCustomHeaders(opts) {
431
468
  const lines = [
432
- `x-api-key: ${opts.anthropicKey}`,
469
+ ...(opts.anthropicKey ? [`x-api-key: ${opts.anthropicKey}`] : []),
470
+ ...(opts.mnemomKey ? [`x-mnemom-api-key: ${opts.mnemomKey}`] : []),
433
471
  `x-mnemom-agent: ${opts.agent}`,
434
472
  `x-mnemom-conversation-id: ${opts.conversationId}`,
435
473
  ];
@@ -15,6 +15,7 @@
15
15
  * resolvers in lib/code.ts keep the built-in default as the final fallback);
16
16
  * the caller folds the plain-value settings under the CLI flags.
17
17
  */
18
+ import { type AgentAuth } from "./subscription-login.js";
18
19
  import type { ExperimentalId } from "./experimental.js";
19
20
  import { type ContextProfile, type GoalModeDefault, type LaunchShape } from "./agent.js";
20
21
  /** The human-editable settings file. Secret key lives elsewhere (launcher.json). */
@@ -29,6 +30,8 @@ export interface LauncherConfig {
29
30
  effort?: string;
30
31
  cli?: string;
31
32
  launch?: LaunchShape;
33
+ /** How Claude Code authenticates (lib/subscription-login.ts); api-key when unset. */
34
+ auth?: AgentAuth;
32
35
  keySource?: KeySource;
33
36
  /** Standing goal-alignment default when neither --goal nor --implicit is given. */
34
37
  goalMode?: GoalModeDefault;
@@ -15,6 +15,7 @@
15
15
  * resolvers in lib/code.ts keep the built-in default as the final fallback);
16
16
  * the caller folds the plain-value settings under the CLI flags.
17
17
  */
18
+ import { AGENT_AUTH_ENV, AGENT_AUTHS, coerceAgentAuth, } from "./subscription-login.js";
18
19
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
19
20
  import { dirname, join } from "node:path";
20
21
  import { parse as parseToml, stringify as stringifyToml, TomlError } from "smol-toml";
@@ -86,7 +87,7 @@ function ensureGuardrails(cfg) {
86
87
  export const CONFIG_KEYS = [
87
88
  {
88
89
  key: "gateway",
89
- describe: "Gateway host (default https://gateway.mnemom.ai — the us-2/prod cell)",
90
+ describe: "Gateway host (default: the gateway of the cell the CLI uses — prod unless MNEMOM_ENV/MNEMOM_RING/MNEMOM_GATEWAY_URL say otherwise; a different cell is refused)",
90
91
  get: (c) => c.gateway,
91
92
  set: (c, v) => {
92
93
  c.gateway = coerceNonEmpty("gateway", v);
@@ -132,6 +133,14 @@ export const CONFIG_KEYS = [
132
133
  c.launch = coerceLaunch(v);
133
134
  },
134
135
  },
136
+ {
137
+ key: "auth",
138
+ describe: `How Claude Code authenticates (${AGENT_AUTHS.join("|")}); subscription = your Claude Team/Enterprise login, terminal-only`,
139
+ get: (c) => c.auth,
140
+ set: (c, v) => {
141
+ c.auth = coerceAgentAuth(v);
142
+ },
143
+ },
135
144
  {
136
145
  key: "key_source",
137
146
  describe: `Where to source the Anthropic key (${KEY_SOURCES.join("|")})`,
@@ -258,6 +267,8 @@ export function parseLauncherConfig(text) {
258
267
  cfg.cli = coerceNonEmpty("cli", str(raw.cli) ?? "");
259
268
  if (raw.launch !== undefined)
260
269
  cfg.launch = coerceLaunch(str(raw.launch) ?? "");
270
+ if (raw.auth !== undefined)
271
+ cfg.auth = coerceAgentAuth(str(raw.auth) ?? "");
261
272
  if (raw.key_source !== undefined)
262
273
  cfg.keySource = coerceKeySource(str(raw.key_source) ?? "");
263
274
  if (raw.goal_mode !== undefined)
@@ -330,6 +341,8 @@ export function serializeLauncherConfig(cfg) {
330
341
  doc.cli = cfg.cli;
331
342
  if (cfg.launch !== undefined)
332
343
  doc.launch = cfg.launch;
344
+ if (cfg.auth !== undefined)
345
+ doc.auth = cfg.auth;
333
346
  if (cfg.keySource !== undefined)
334
347
  doc.key_source = cfg.keySource;
335
348
  if (cfg.goalMode !== undefined)
@@ -392,6 +405,9 @@ export function applyConfigToEnv(cfg, env = process.env) {
392
405
  env.MNEMOM_AGENT_REMOTE_CONTROL =
393
406
  cfg.launch === "server" ? "server" : cfg.launch === "remote-control" ? "1" : "0";
394
407
  }
408
+ if (cfg.auth && !(env[AGENT_AUTH_ENV] ?? "").trim()) {
409
+ env[AGENT_AUTH_ENV] = cfg.auth;
410
+ }
395
411
  if (cfg.context !== undefined && !(env.MNEMOM_AGENT_CONTEXT ?? "").trim()) {
396
412
  env.MNEMOM_AGENT_CONTEXT = cfg.context ? "1" : "0";
397
413
  }
@@ -53,7 +53,9 @@ export function ensureRouter(argv = [process.execPath, process.argv[1] ?? "mnemo
53
53
  }
54
54
  /** Run the router until the process is killed. Reconnects on disconnect. */
55
55
  export async function runRouter(opts) {
56
- const log = opts.log ?? ((l) => console.error(`[slack-router] ${l}`));
56
+ // Lines carry Slack-supplied values (channel, handles, disconnect reason);
57
+ // strip CR/LF so a crafted message cannot forge extra log lines.
58
+ const log = opts.log ?? ((l) => console.error(`[slack-router] ${l.replace(/\n|\r/g, "")}`));
57
59
  const fetchImpl = opts.fetchImpl ?? fetch;
58
60
  const WS = globalThis.WebSocket;
59
61
  if (!WS)
@@ -0,0 +1,53 @@
1
+ /**
2
+ * `mnemom agent` with a Claude subscription login (`auth = subscription`) — Team and
3
+ * Enterprise plans only; the gateway checks the plan with Anthropic and refuses anything
4
+ * else (gateway/src/subscription-auth.ts).
5
+ *
6
+ * Claude Code then authenticates to the gateway with its own claude.ai OAuth login: the
7
+ * launch sets ANTHROPIC_BASE_URL and leaves ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN unset,
8
+ * so Claude Code sends `Authorization: Bearer <its login>`. No Anthropic key is involved.
9
+ * The gateway anchors the governed identity on a Mnemom API key instead
10
+ * (`x-mnemom-api-key`), which this module resolves — env, then the 0600 store, then a
11
+ * freshly minted gateway-only key — and the agent is created in that key's org on the
12
+ * first turn, so there is no CLI-side birth or claim.
13
+ *
14
+ * v1 is terminal-only: the Remote Control proxy rewrites the OAuth bearer away by design,
15
+ * and the context-handoff tool authenticates with an Anthropic key.
16
+ *
17
+ * Pure apart from the injected deps, so every arm is unit-tested.
18
+ */
19
+ import type { LaunchShape } from "./agent.js";
20
+ export type AgentAuth = "api-key" | "subscription";
21
+ export declare const AGENT_AUTHS: readonly AgentAuth[];
22
+ /** Env override for the setting (the launcher.toml `auth` key folds into it). */
23
+ export declare const AGENT_AUTH_ENV = "MNEMOM_AGENT_AUTH";
24
+ /** Env override for the Mnemom key a subscription launch sends. */
25
+ export declare const MNEMOM_KEY_ENV = "MNEMOM_AGENT_MNEMOM_KEY";
26
+ /** Field in ~/.mnemom/launcher.json (0600) holding the minted key. */
27
+ export declare const MNEMOM_KEY_STORE_FIELD = "mnemom_api_key";
28
+ export declare function coerceAgentAuth(raw: string): AgentAuth;
29
+ /** `--auth` flag > MNEMOM_AGENT_AUTH (launcher.toml folds in here) > api-key. */
30
+ export declare function resolveAgentAuth(flag: string | undefined, env: Record<string, string | undefined>): AgentAuth;
31
+ export interface MnemomKeyDeps {
32
+ env: Record<string, string | undefined>;
33
+ readStored(): string | undefined;
34
+ /** Mint a gateway-only personal key for this machine; returns the secret. */
35
+ mint(): Promise<string>;
36
+ store(key: string): void;
37
+ say(line: string): void;
38
+ }
39
+ /**
40
+ * The Mnemom key a subscription launch sends: MNEMOM_AGENT_MNEMOM_KEY, else the stored
41
+ * key, else mint one (gateway scope only) and store it. The key is never printed.
42
+ */
43
+ export declare function resolveSubscriptionMnemomKey(deps: MnemomKeyDeps): Promise<string>;
44
+ /**
45
+ * Subscription login is terminal-only in v1. An explicit Remote Control / server request
46
+ * is refused; an automatic upgrade to Remote Control falls back to terminal with a notice.
47
+ */
48
+ export declare function subscriptionShape(shape: LaunchShape, shapeExplicit: boolean, say: (line: string) => void): LaunchShape;
49
+ /**
50
+ * The child env for a subscription launch: the gateway door, the Mnemom headers, and
51
+ * NOTHING that would make Claude Code send a key instead of its claude.ai login.
52
+ */
53
+ export declare function subscriptionChildEnv(base: NodeJS.ProcessEnv, door: string, headers: string): NodeJS.ProcessEnv;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * `mnemom agent` with a Claude subscription login (`auth = subscription`) — Team and
3
+ * Enterprise plans only; the gateway checks the plan with Anthropic and refuses anything
4
+ * else (gateway/src/subscription-auth.ts).
5
+ *
6
+ * Claude Code then authenticates to the gateway with its own claude.ai OAuth login: the
7
+ * launch sets ANTHROPIC_BASE_URL and leaves ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN unset,
8
+ * so Claude Code sends `Authorization: Bearer <its login>`. No Anthropic key is involved.
9
+ * The gateway anchors the governed identity on a Mnemom API key instead
10
+ * (`x-mnemom-api-key`), which this module resolves — env, then the 0600 store, then a
11
+ * freshly minted gateway-only key — and the agent is created in that key's org on the
12
+ * first turn, so there is no CLI-side birth or claim.
13
+ *
14
+ * v1 is terminal-only: the Remote Control proxy rewrites the OAuth bearer away by design,
15
+ * and the context-handoff tool authenticates with an Anthropic key.
16
+ *
17
+ * Pure apart from the injected deps, so every arm is unit-tested.
18
+ */
19
+ export const AGENT_AUTHS = ["api-key", "subscription"];
20
+ /** Env override for the setting (the launcher.toml `auth` key folds into it). */
21
+ export const AGENT_AUTH_ENV = "MNEMOM_AGENT_AUTH";
22
+ /** Env override for the Mnemom key a subscription launch sends. */
23
+ export const MNEMOM_KEY_ENV = "MNEMOM_AGENT_MNEMOM_KEY";
24
+ /** Field in ~/.mnemom/launcher.json (0600) holding the minted key. */
25
+ export const MNEMOM_KEY_STORE_FIELD = "mnemom_api_key";
26
+ /**
27
+ * The key is spliced into the newline-joined ANTHROPIC_CUSTOM_HEADERS block, so an embedded
28
+ * CR/LF (a multi-line paste, a bad env value) would inject extra header lines.
29
+ */
30
+ function headerSafe(raw) {
31
+ return raw.replace(/[\r\n]+/g, "").trim();
32
+ }
33
+ export function coerceAgentAuth(raw) {
34
+ const v = raw.trim().toLowerCase();
35
+ if (AGENT_AUTHS.includes(v))
36
+ return v;
37
+ throw new Error(`auth must be one of ${AGENT_AUTHS.join(", ")} (got '${raw}').`);
38
+ }
39
+ /** `--auth` flag > MNEMOM_AGENT_AUTH (launcher.toml folds in here) > api-key. */
40
+ export function resolveAgentAuth(flag, env) {
41
+ if (flag !== undefined && flag.trim())
42
+ return coerceAgentAuth(flag);
43
+ const fromEnv = (env[AGENT_AUTH_ENV] ?? "").trim();
44
+ return fromEnv ? coerceAgentAuth(fromEnv) : "api-key";
45
+ }
46
+ /**
47
+ * The Mnemom key a subscription launch sends: MNEMOM_AGENT_MNEMOM_KEY, else the stored
48
+ * key, else mint one (gateway scope only) and store it. The key is never printed.
49
+ */
50
+ export async function resolveSubscriptionMnemomKey(deps) {
51
+ const fromEnv = headerSafe(deps.env[MNEMOM_KEY_ENV] ?? "");
52
+ if (fromEnv)
53
+ return fromEnv;
54
+ const stored = headerSafe(deps.readStored() ?? "");
55
+ if (stored)
56
+ return stored;
57
+ let minted;
58
+ try {
59
+ minted = headerSafe(await deps.mint());
60
+ }
61
+ catch (err) {
62
+ const msg = err instanceof Error ? err.message : String(err);
63
+ throw new Error(`mnemom agent: subscription login needs a Mnemom API key and minting one failed (${msg}).\n` +
64
+ ` Create one with \`mnemom api-key create --name mnemom-agent --scopes gateway\` and set ${MNEMOM_KEY_ENV}.`, { cause: err });
65
+ }
66
+ if (!minted)
67
+ throw new Error("mnemom agent: the API returned an empty Mnemom key.");
68
+ try {
69
+ deps.store(minted);
70
+ deps.say("mnemom agent: minted a gateway-only Mnemom API key for subscription login (saved to ~/.mnemom/launcher.json, 0600).");
71
+ }
72
+ catch {
73
+ deps.say("mnemom agent: minted a gateway-only Mnemom API key for this launch but could not save it — the next launch mints another.");
74
+ }
75
+ return minted;
76
+ }
77
+ /**
78
+ * Subscription login is terminal-only in v1. An explicit Remote Control / server request
79
+ * is refused; an automatic upgrade to Remote Control falls back to terminal with a notice.
80
+ */
81
+ export function subscriptionShape(shape, shapeExplicit, say) {
82
+ if (shape === "terminal")
83
+ return shape;
84
+ if (shapeExplicit) {
85
+ throw new Error(`mnemom agent: subscription login runs terminal-only for now — drop --${shape === "server" ? "server" : "remote-control"} (or \`mnemom agent config set launch terminal\`), or use auth = api-key.`);
86
+ }
87
+ say("mnemom agent: subscription login runs terminal-only for now — launching in this terminal.");
88
+ return "terminal";
89
+ }
90
+ /**
91
+ * The child env for a subscription launch: the gateway door, the Mnemom headers, and
92
+ * NOTHING that would make Claude Code send a key instead of its claude.ai login.
93
+ */
94
+ export function subscriptionChildEnv(base, door, headers) {
95
+ const env = { ...base };
96
+ delete env.ANTHROPIC_API_KEY;
97
+ delete env.ANTHROPIC_AUTH_TOKEN;
98
+ // The key already rides in the headers; the child process has no other use for it.
99
+ delete env.MNEMOM_AGENT_MNEMOM_KEY;
100
+ env.ANTHROPIC_BASE_URL = door;
101
+ env.ANTHROPIC_CUSTOM_HEADERS = headers;
102
+ return env;
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.3-next.6",
3
+ "version": "0.17.4-next.0",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {