@mnemom/mnemom 0.17.3 → 0.17.4-next.1

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,6 +14,7 @@
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 { resolveAgentAuth } from "../lib/subscription-login.js";
17
18
  import { isProdGateway, resolveAnthropicDoor, resolveGatewayHost, singleCellError, } from "../lib/agent.js";
18
19
  import { getApiUrl, getGatewayUrl } from "../lib/config.js";
19
20
  import { LAUNCHER_CONFIG_PATH } from "../lib/launcher-config.js";
@@ -45,14 +46,26 @@ export async function agentDoctorCommand(options = {}) {
45
46
  catch (err) {
46
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>`");
47
48
  }
48
- // ── Anthropic key ──
49
- const keySrc = anthropicKeySource(env);
50
- if (keySrc === "env")
51
- add("ok", "Anthropic key: found in the environment");
52
- else if (keySrc === "store")
53
- add("ok", "Anthropic key: found in ~/.mnemom/launcher.json");
54
- else
55
- 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
+ }
56
69
  // ── one cell: the launch door must be the CLI's own cell ──
57
70
  const cellError = singleCellError({
58
71
  door: resolveAnthropicDoor(env),
@@ -25,6 +25,8 @@
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";
29
+ import { type SessionRecord } from "../lib/agent-ledger.js";
28
30
  import { type ExperimentalId } from "../lib/experimental.js";
29
31
  import { type ContextProfile, type LaunchShape, type SealedContractEnvelope } from "../lib/agent.js";
30
32
  export interface AgentOptions {
@@ -48,6 +50,8 @@ export interface AgentOptions {
48
50
  cli?: string;
49
51
  remoteControl?: boolean;
50
52
  server?: boolean;
53
+ /** `--auth api-key|subscription` (lib/subscription-login.ts); else MNEMOM_AGENT_AUTH / config. */
54
+ auth?: string;
51
55
  model?: string;
52
56
  effort?: string;
53
57
  trimMcp?: boolean;
@@ -100,6 +104,34 @@ export interface AgentOptions {
100
104
  }
101
105
  /** Entry point for `mnemom agent [scenario]`. Throws on fatal misconfig. */
102
106
  export declare function agentCommand(scenarioArg: string | undefined, options?: AgentOptions): Promise<void>;
107
+ /** The Claude Code session id a passthrough `--resume <id>` / `-r <id>` / `--resume=<id>` names. */
108
+ export declare function passthroughResumeId(passthrough: readonly string[]): string | null;
109
+ /**
110
+ * The conversation id this launch runs under (IG5 #1, decision D1). The gateway keys the
111
+ * implicit goal by conversation id, so an implicit relaunch reuses the earlier one:
112
+ * - `--conversation-id` → that id, no lookup;
113
+ * - not implicit (a sealed contract, or alignment off) → a new id;
114
+ * - only an earlier implicit launch with the same session name, org, agent and gateway host
115
+ * is continued; any mismatch → a new id, no prompt;
116
+ * - `-- --resume <cc-session-id>` → the matching row that pinned that Claude Code session
117
+ * (else the newest matching row), reused without asking;
118
+ * - no `--resume` → "Resume previous session <name>? [Y/n]", default Y; Y reuses that row's
119
+ * conversation, n mints a new one. `--resume` is NOT added to Claude Code (the user often
120
+ * resumes inside it with /resume). No TTY: Y.
121
+ * The ledger read is best-effort: a failure means a new id.
122
+ */
123
+ export declare function resolveLaunchConversationId(input: {
124
+ session: string;
125
+ orgId: string;
126
+ agent: string;
127
+ gatewayHost: string;
128
+ explicit?: string;
129
+ implicit: boolean;
130
+ passthrough: readonly string[];
131
+ interactive: boolean;
132
+ records: () => SessionRecord[];
133
+ askYesNo: (question: string, defaultYes: boolean) => Promise<boolean>;
134
+ }): Promise<string>;
103
135
  /**
104
136
  * Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
105
137
  * resolved best-effort (a lookup failure or an unknown slug records null); a
@@ -124,6 +156,8 @@ export declare function recordSessionLaunch(ctx: {
124
156
  /** The resolved context profile this launch runs under. */
125
157
  contextProfile: ContextProfile;
126
158
  }): Promise<void>;
159
+ /** The real I/O behind a subscription launch's Mnemom key (lib/subscription-login.ts). */
160
+ export declare function defaultMnemomKeyDeps(storePath?: string): MnemomKeyDeps;
127
161
  /**
128
162
  * Resolve the Anthropic key with NO 1Password dependency (built for external
129
163
  * customer adoption), in order:
@@ -28,12 +28,13 @@
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
34
  import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
35
- import { getAgentByName } from "../lib/api.js";
36
- import { LEDGER_FILE, appendSessionRecord, resolveRetentionDays, toLedgerContract, } from "../lib/agent-ledger.js";
35
+ import { createApiKey, getAgentByName } from "../lib/api.js";
36
+ import { MNEMOM_KEY_STORE_FIELD, resolveAgentAuth, resolveSubscriptionMnemomKey, subscriptionChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
37
+ import { LEDGER_FILE, appendSessionRecord, findResumeCandidate, readSessionRecords, 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";
39
40
  import { experimentalNotice } from "../lib/experimental.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, singleCellError, 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, GATEWAY_HINT_HEADERS_ENV, 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
@@ -85,7 +89,6 @@ export async function agentCommand(scenarioArg, options = {}) {
85
89
  if (!agent) {
86
90
  throw new Error("mnemom agent: --agent sanitised to an empty string — pick a value with at least one letter or digit.");
87
91
  }
88
- const conversationId = resolveConversationId(options.conversationId);
89
92
  // ── contract (built only if a contract flag was given; --goal then required) ──
90
93
  const envelope = assembleContract({
91
94
  goal: session.goal,
@@ -124,7 +127,12 @@ export async function agentCommand(scenarioArg, options = {}) {
124
127
  context: options.context,
125
128
  env,
126
129
  });
127
- const contextHandoff = resolveContextHandoff({ contextHandoff: options.contextHandoff, env });
130
+ let contextHandoff = resolveContextHandoff({ contextHandoff: options.contextHandoff, env });
131
+ if (contextHandoff && auth === "subscription") {
132
+ // The handoff tool authenticates with the Anthropic key a subscription launch does not have.
133
+ say("mnemom agent: context handoff is not available with subscription login yet — launching without it.");
134
+ contextHandoff = false;
135
+ }
128
136
  const experimental = options.experimental ?? [];
129
137
  const sessionConfig = buildSessionConfig({ profile: contextProfile, contextHandoff });
130
138
  const sessionConfigB64 = buildSessionConfigB64({
@@ -164,7 +172,8 @@ export async function agentCommand(scenarioArg, options = {}) {
164
172
  scenario,
165
173
  shape: previewShape,
166
174
  agent,
167
- conversationId,
175
+ // The org is only known after sign-in, so a dry run never offers a resume (IG5 #1).
176
+ conversationId: resolveConversationId(options.conversationId),
168
177
  door,
169
178
  cellNote,
170
179
  envelope,
@@ -175,6 +184,9 @@ export async function agentCommand(scenarioArg, options = {}) {
175
184
  sessionConfig,
176
185
  experimental,
177
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
+ }
178
190
  if (expNotice)
179
191
  say(expNotice);
180
192
  if (previewShape === "remote-control") {
@@ -193,10 +205,10 @@ export async function agentCommand(scenarioArg, options = {}) {
193
205
  cli = await promptForCliPath(options.cli, env, cliError);
194
206
  }
195
207
  // ── resolve the Anthropic key (zero 1Password; never logged) ──
196
- const anthropicKey = await resolveAnthropicKey(env, {
197
- allowPrompt: true,
198
- keySource: options.keySource,
199
- });
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;
200
212
  // ── account + µ-balance gate (no free tier) ─────────────────────────────────
201
213
  // mnemom agent runs governed and on µ, so before we seal a contract or launch we
202
214
  // (1) ensure a Mnemom session — offering the try-me-style sign-in / sign-up when
@@ -245,6 +257,14 @@ export async function agentCommand(scenarioArg, options = {}) {
245
257
  rcCapable: rc.ok,
246
258
  say,
247
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
+ }
248
268
  // Now that a session exists, a defaulted slug uses the real login handle from
249
269
  // /v1/auth/me (the stored token has no email, so the pre-sign-in value was the
250
270
  // "agent-user" fallback). Only when the caller didn't name an agent. Under an
@@ -261,7 +281,12 @@ export async function agentCommand(scenarioArg, options = {}) {
261
281
  // provisions-with-confirm when interactive; warns and continues otherwise.
262
282
  // Skipped entirely by `--no-setup`. A provisioning failure never blocks the
263
283
  // launch — it degrades to the unclaimed (fail-open) identity with a warning.
264
- 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) {
265
290
  try {
266
291
  await ensureGovernedAgent({
267
292
  slug: agent,
@@ -280,6 +305,22 @@ export async function agentCommand(scenarioArg, options = {}) {
280
305
  say(fmt.warn(`mnemom agent: agent setup did not complete (${msg}) — launching with the unclaimed identity.`));
281
306
  }
282
307
  }
308
+ // ── conversation id ── an implicit relaunch continues its earlier conversation (IG5 #1, D1)
309
+ // so the goal the gateway inferred there is still the one it steers by. Resolved here, after
310
+ // sign-in and the agent refresh, because a resume needs the org, agent and gateway host to
311
+ // match the earlier launch.
312
+ const conversationId = await resolveLaunchConversationId({
313
+ session: scenario,
314
+ orgId: account.orgId,
315
+ agent,
316
+ gatewayHost,
317
+ explicit: options.conversationId,
318
+ implicit: goalAlignmentMode(envelope, goalMode) === "implicit",
319
+ passthrough: options.passthrough ?? [],
320
+ interactive: isInteractive(),
321
+ records: () => readSessionRecords({ dir: MNEMOM_DIR }),
322
+ askYesNo: (q, d) => askYesNo(q, d),
323
+ });
283
324
  // ── local session ledger (~/.mnemom/sessions.jsonl) ── one line per real
284
325
  // launch so `mnemom agent sessions` / `show` can find this conversation later.
285
326
  // Written after provisioning so a freshly claimed agent's id resolves. Holds
@@ -343,11 +384,13 @@ export async function agentCommand(scenarioArg, options = {}) {
343
384
  say(fmt.warn(`mnemom agent: context handoff could not be registered (${err instanceof Error ? err.message : String(err)}) — launching without the handoff tool.`));
344
385
  }
345
386
  }
346
- if (shape === "terminal") {
387
+ // A subscription launch (no Anthropic key) is always terminal — see subscriptionShape.
388
+ if (shape === "terminal" || !anthropicKey) {
347
389
  launchTerminal({
348
390
  cli,
349
391
  door,
350
392
  anthropicKey,
393
+ mnemomKey,
351
394
  agent,
352
395
  scenario,
353
396
  conversationId,
@@ -382,6 +425,63 @@ export async function agentCommand(scenarioArg, options = {}) {
382
425
  // ───────────────────────────────────────────────────────────────────────────
383
426
  // Session ledger (thin driver over lib/agent-ledger.ts)
384
427
  // ───────────────────────────────────────────────────────────────────────────
428
+ /** The Claude Code session id a passthrough `--resume <id>` / `-r <id>` / `--resume=<id>` names. */
429
+ export function passthroughResumeId(passthrough) {
430
+ for (let i = 0; i < passthrough.length; i++) {
431
+ const a = passthrough[i];
432
+ if (a === "--resume" || a === "-r") {
433
+ const next = passthrough[i + 1];
434
+ return next !== undefined && !next.startsWith("-") && next.trim() !== "" ? next.trim() : null;
435
+ }
436
+ if (a.startsWith("--resume="))
437
+ return a.slice("--resume=".length).trim() || null;
438
+ }
439
+ return null;
440
+ }
441
+ /**
442
+ * The conversation id this launch runs under (IG5 #1, decision D1). The gateway keys the
443
+ * implicit goal by conversation id, so an implicit relaunch reuses the earlier one:
444
+ * - `--conversation-id` → that id, no lookup;
445
+ * - not implicit (a sealed contract, or alignment off) → a new id;
446
+ * - only an earlier implicit launch with the same session name, org, agent and gateway host
447
+ * is continued; any mismatch → a new id, no prompt;
448
+ * - `-- --resume <cc-session-id>` → the matching row that pinned that Claude Code session
449
+ * (else the newest matching row), reused without asking;
450
+ * - no `--resume` → "Resume previous session <name>? [Y/n]", default Y; Y reuses that row's
451
+ * conversation, n mints a new one. `--resume` is NOT added to Claude Code (the user often
452
+ * resumes inside it with /resume). No TTY: Y.
453
+ * The ledger read is best-effort: a failure means a new id.
454
+ */
455
+ export async function resolveLaunchConversationId(input) {
456
+ if ((input.explicit ?? "").trim())
457
+ return resolveConversationId(input.explicit);
458
+ if (!input.implicit)
459
+ return resolveConversationId(undefined);
460
+ let records;
461
+ try {
462
+ records = input.records();
463
+ }
464
+ catch {
465
+ return resolveConversationId(undefined);
466
+ }
467
+ const found = findResumeCandidate(records, {
468
+ session: input.session,
469
+ orgId: input.orgId,
470
+ agent: input.agent,
471
+ gatewayHost: input.gatewayHost,
472
+ resumeId: passthroughResumeId(input.passthrough),
473
+ });
474
+ if (!found)
475
+ return resolveConversationId(undefined);
476
+ const conv = found.record.conversation_id;
477
+ if (found.source === "session_name" && input.interactive) {
478
+ const yes = await input.askYesNo(`Resume previous session ${input.session}?`, true);
479
+ if (!yes)
480
+ return resolveConversationId(undefined);
481
+ }
482
+ say(`mnemom agent: continuing conversation ${conv} from the earlier '${found.record.session}' launch (${found.record.started_at}), so its goal carries over.`);
483
+ return resolveConversationId(conv);
484
+ }
385
485
  /**
386
486
  * Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
387
487
  * resolved best-effort (a lookup failure or an unknown slug records null); a
@@ -425,6 +525,25 @@ export async function recordSessionLaunch(ctx) {
425
525
  // ───────────────────────────────────────────────────────────────────────────
426
526
  // Credential resolution (zero 1Password)
427
527
  // ───────────────────────────────────────────────────────────────────────────
528
+ /** The real I/O behind a subscription launch's Mnemom key (lib/subscription-login.ts). */
529
+ export function defaultMnemomKeyDeps(storePath = CODE_STORE) {
530
+ return {
531
+ env: process.env,
532
+ readStored: () => {
533
+ try {
534
+ const data = JSON.parse(readFileSync(storePath, "utf8"));
535
+ const v = data[MNEMOM_KEY_STORE_FIELD];
536
+ return typeof v === "string" ? stripCrlf(v) : undefined;
537
+ }
538
+ catch {
539
+ return undefined;
540
+ }
541
+ },
542
+ mint: async () => (await createApiKey(`mnemom-agent ${hostname()}`, ["gateway"])).key,
543
+ store: (key) => mergeStore({ [MNEMOM_KEY_STORE_FIELD]: key }, storePath),
544
+ say,
545
+ };
546
+ }
428
547
  /**
429
548
  * Resolve the Anthropic key with NO 1Password dependency (built for external
430
549
  * customer adoption), in order:
@@ -660,6 +779,7 @@ function launchTerminal(ctx) {
660
779
  warnAdapter(ctx.cli);
661
780
  const headers = buildCustomHeaders({
662
781
  anthropicKey: ctx.anthropicKey,
782
+ mnemomKey: ctx.mnemomKey,
663
783
  agent: ctx.agent,
664
784
  conversationId: ctx.conversationId,
665
785
  contractB64: ctx.envelope?.base64,
@@ -675,13 +795,23 @@ function launchTerminal(ctx) {
675
795
  // Claude Code logged in via claude.ai (OAuth) PREEMPTS ANTHROPIC_API_KEY, so
676
796
  // it is force-unset; the real key rides in ANTHROPIC_CUSTOM_HEADERS and
677
797
  // ANTHROPIC_AUTH_TOKEN is a fixed non-secret placeholder.
678
- const childEnv = { ...process.env };
679
- delete childEnv.ANTHROPIC_API_KEY;
680
- childEnv.ANTHROPIC_BASE_URL = ctx.door;
681
- childEnv.ANTHROPIC_AUTH_TOKEN = "mnemom-gateway";
682
- childEnv.ANTHROPIC_CUSTOM_HEADERS = headers;
798
+ //
799
+ // A subscription launch (no Anthropic key) sets NO auth token: Claude Code then sends its
800
+ // own claude.ai login to the gateway (lib/subscription-login.ts).
801
+ let childEnv;
802
+ if (ctx.anthropicKey) {
803
+ childEnv = { ...process.env };
804
+ delete childEnv.ANTHROPIC_API_KEY;
805
+ childEnv.ANTHROPIC_BASE_URL = ctx.door;
806
+ childEnv.ANTHROPIC_AUTH_TOKEN = "mnemom-gateway";
807
+ childEnv.ANTHROPIC_CUSTOM_HEADERS = headers;
808
+ }
809
+ else {
810
+ childEnv = subscriptionChildEnv(process.env, ctx.door, headers);
811
+ }
683
812
  const sessionName = passthroughName(ctx.options.passthrough ?? []) ?? sessionDisplayName(ctx.agent, ctx.scenario);
684
813
  childEnv[SESSION_NAME_ENV] = sessionName;
814
+ childEnv[GATEWAY_HINT_HEADERS_ENV] = "1";
685
815
  if (ctx.options.slack)
686
816
  args.push(...slackChannelArgs(sessionName));
687
817
  if (ctx.contextHint) {
package/dist/index.js CHANGED
@@ -142,6 +142,7 @@ const agentCmd = program
142
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,
@@ -133,3 +133,30 @@ export declare function appendSessionRecord(input: SessionRecordInput, deps: Led
133
133
  export declare function readSessionRecords(deps: Pick<LedgerDeps, "dir">): SessionRecord[];
134
134
  /** One ledger line → a record, or null when it is not a v1 record object. */
135
135
  export declare function parseRecord(line: string): SessionRecord | null;
136
+ /**
137
+ * Which earlier launch a new one continues (IG5 #1, decision D1). The gateway keys the implicit
138
+ * goal by conversation id, so a relaunch that mints a fresh id starts goal-less. A row
139
+ * qualifies only when it is an implicit-mode launch (a sealed contract is sealed on its own
140
+ * conversation and is never carried) with the same session name, org, agent and gateway host as
141
+ * this launch; an empty org never matches. `records` is file order (oldest first); the newest
142
+ * qualifying row wins.
143
+ * - `resumeId` (the `--resume <cc-session-id>` passed through to Claude Code) → the qualifying
144
+ * row that pinned that Claude Code session, else the newest qualifying row; `source:
145
+ * "resume_flag"` — reused without asking.
146
+ * - no `resumeId` → the newest qualifying row; `source: "session_name"` — the launcher asks
147
+ * first.
148
+ * - nothing qualifies → null (a new conversation id).
149
+ */
150
+ export interface ResumeCandidate {
151
+ record: SessionRecord;
152
+ source: "resume_flag" | "session_name";
153
+ /** True when the row was found by its Claude Code session id (not by the name fallback). */
154
+ byCcSession: boolean;
155
+ }
156
+ export declare function findResumeCandidate(records: readonly SessionRecord[], want: {
157
+ session: string;
158
+ orgId: string;
159
+ agent: string;
160
+ gatewayHost: string;
161
+ resumeId?: string | null;
162
+ }): ResumeCandidate | null;
@@ -170,3 +170,30 @@ export function parseRecord(line) {
170
170
  function nonEmptyLines(text) {
171
171
  return text.split("\n").filter((l) => l.trim().length > 0);
172
172
  }
173
+ export function findResumeCandidate(records, want) {
174
+ const implicitRows = records.filter((r) => r.goal_mode === "implicit" &&
175
+ typeof r.conversation_id === "string" &&
176
+ r.conversation_id &&
177
+ r.session === want.session &&
178
+ !!want.orgId &&
179
+ r.org_id === want.orgId &&
180
+ r.agent_slug === want.agent &&
181
+ r.gateway_host === want.gatewayHost);
182
+ const newest = (pred) => {
183
+ for (let i = implicitRows.length - 1; i >= 0; i--) {
184
+ const r = implicitRows[i];
185
+ if (pred(r))
186
+ return r;
187
+ }
188
+ return undefined;
189
+ };
190
+ const latest = newest(() => true);
191
+ const resumeId = want.resumeId?.trim();
192
+ if (resumeId) {
193
+ const byCc = newest((r) => r.cc_session_id === resumeId);
194
+ if (byCc)
195
+ return { record: byCc, source: "resume_flag", byCcSession: true };
196
+ return latest ? { record: latest, source: "resume_flag", byCcSession: false } : null;
197
+ }
198
+ return latest ? { record: latest, source: "session_name", byCcSession: false } : null;
199
+ }
@@ -116,6 +116,15 @@ export declare function sanitizeAgentName(raw: string): string;
116
116
  * post`) can identify themselves by the same name the claude.ai session list shows.
117
117
  */
118
118
  export declare const SESSION_NAME_ENV = "MNEMOM_AGENT_SESSION_NAME";
119
+ /**
120
+ * Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class`,
121
+ * `x-claude-code-agent-type`, `x-claude-code-prompt-id`). CC sends them to api.anthropic.com on
122
+ * its own, so a Remote Control launch (rc-proxy in front of api.anthropic.com) already carries
123
+ * them; with `ANTHROPIC_BASE_URL` pointing at the gateway door (the terminal shape) CC sends them
124
+ * only when this is "1". The gateway reads them to keep away summaries, titles, compaction and
125
+ * teammates off the main implicit goal.
126
+ */
127
+ export declare const GATEWAY_HINT_HEADERS_ENV = "CLAUDE_CODE_GATEWAY_HINT_HEADERS";
119
128
  /** The session display name: "<agent>-<scenario>" (sanitised), else the agent slug. */
120
129
  export declare function sessionDisplayName(agent: string, scenario: string): string;
121
130
  /** A caller's own `--name <x>` in the passthrough args, if any (last one wins). */
@@ -283,7 +292,10 @@ export declare function resolveRemoteControlShape(args: {
283
292
  * callers must place it only in the child's env, never in a log line.
284
293
  */
285
294
  export declare function buildCustomHeaders(opts: {
286
- anthropicKey: string;
295
+ /** Absent on a subscription-login launch: Claude Code then sends its own claude.ai login. */
296
+ anthropicKey?: string;
297
+ /** The governed-identity anchor on a subscription-login launch (lib/subscription-login.ts). */
298
+ mnemomKey?: string;
287
299
  agent: string;
288
300
  conversationId: string;
289
301
  contractB64?: string;
package/dist/lib/agent.js CHANGED
@@ -117,6 +117,15 @@ export function sanitizeAgentName(raw) {
117
117
  * post`) can identify themselves by the same name the claude.ai session list shows.
118
118
  */
119
119
  export const SESSION_NAME_ENV = "MNEMOM_AGENT_SESSION_NAME";
120
+ /**
121
+ * Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class`,
122
+ * `x-claude-code-agent-type`, `x-claude-code-prompt-id`). CC sends them to api.anthropic.com on
123
+ * its own, so a Remote Control launch (rc-proxy in front of api.anthropic.com) already carries
124
+ * them; with `ANTHROPIC_BASE_URL` pointing at the gateway door (the terminal shape) CC sends them
125
+ * only when this is "1". The gateway reads them to keep away summaries, titles, compaction and
126
+ * teammates off the main implicit goal.
127
+ */
128
+ export const GATEWAY_HINT_HEADERS_ENV = "CLAUDE_CODE_GATEWAY_HINT_HEADERS";
120
129
  /** The session display name: "<agent>-<scenario>" (sanitised), else the agent slug. */
121
130
  export function sessionDisplayName(agent, scenario) {
122
131
  return sanitizeAgentName(`${agent}-${scenario}`) || agent;
@@ -466,7 +475,8 @@ export function resolveRemoteControlShape(args) {
466
475
  */
467
476
  export function buildCustomHeaders(opts) {
468
477
  const lines = [
469
- `x-api-key: ${opts.anthropicKey}`,
478
+ ...(opts.anthropicKey ? [`x-api-key: ${opts.anthropicKey}`] : []),
479
+ ...(opts.mnemomKey ? [`x-mnemom-api-key: ${opts.mnemomKey}`] : []),
470
480
  `x-mnemom-agent: ${opts.agent}`,
471
481
  `x-mnemom-conversation-id: ${opts.conversationId}`,
472
482
  ];
@@ -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";
@@ -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
  }
@@ -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",
3
+ "version": "0.17.4-next.1",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {