@mnemom/mnemom 0.17.4-next.1 → 0.17.4-next.3

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.
@@ -196,6 +196,21 @@ function deleteKey(cfg, key) {
196
196
  case "context_profile":
197
197
  delete cfg.contextProfile;
198
198
  break;
199
+ case "auth":
200
+ delete cfg.auth;
201
+ break;
202
+ case "context":
203
+ delete cfg.context;
204
+ break;
205
+ case "slack_channel":
206
+ delete cfg.slackChannel;
207
+ break;
208
+ case "slack_listen":
209
+ delete cfg.slackListen;
210
+ break;
211
+ case "slack_allow":
212
+ delete cfg.slackAllow;
213
+ break;
199
214
  default:
200
215
  requireSpec(key); // throws the known-keys message for anything unknown
201
216
  }
@@ -39,7 +39,8 @@ import { loadCliConfig } from "../lib/cli-config.js";
39
39
  import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
40
40
  import { experimentalNotice } from "../lib/experimental.js";
41
41
  import { fmt } from "../lib/format.js";
42
- import { askInput, askYesNo, isInteractive } from "../lib/prompt.js";
42
+ import { askInput, askSelect, askYesNo, isInteractive } from "../lib/prompt.js";
43
+ import { authConfigured, chooseAuth, detectClaudeLogin, readClaudeLoginState, } from "../lib/login-detect.js";
43
44
  import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
44
45
  import { AgentIdentityMismatchError, defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
45
46
  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";
@@ -80,7 +81,22 @@ export async function agentCommand(scenarioArg, options = {}) {
80
81
  const shapeExplicit = launchPreferenceExplicit({ remoteControl: options.remoteControl, server: options.server }, env);
81
82
  // How Claude Code authenticates: an Anthropic key (default) or the user's claude.ai
82
83
  // subscription login (Team/Enterprise; lib/subscription-login.ts).
83
- const auth = resolveAgentAuth(options.auth, env);
84
+ let auth = resolveAgentAuth(options.auth, env);
85
+ // Nothing configured it yet: detect Claude Code's own login and, when there is a real
86
+ // choice, ask once and save the answer (lib/login-detect.ts). Never on --dry-run.
87
+ if (!options.dryRun && !authConfigured(options.auth, env)) {
88
+ auth = await chooseAuth({
89
+ login: detectClaudeLogin(readClaudeLoginState(env, homedir(), (path) => readFileSync(path, "utf8"))),
90
+ interactive: isInteractive(),
91
+ select: askSelect,
92
+ persist: (choice) => {
93
+ const cfg = loadLauncherConfig();
94
+ cfg.auth = choice;
95
+ writeLauncherConfig(cfg);
96
+ },
97
+ say,
98
+ });
99
+ }
84
100
  // ── governed identity ── (default: per-customer `agent-<handle>`, not a shared
85
101
  // "code-agent" — see defaultAgentSlug; provisioning claims exactly this slug.
86
102
  // `let` because a brand-new user who signs in during the pre-launch gate below
@@ -185,7 +201,7 @@ export async function agentCommand(scenarioArg, options = {}) {
185
201
  experimental,
186
202
  });
187
203
  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.");
204
+ 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.");
189
205
  }
190
206
  if (expNotice)
191
207
  say(expNotice);
@@ -257,12 +273,12 @@ export async function agentCommand(scenarioArg, options = {}) {
257
273
  rcCapable: rc.ok,
258
274
  say,
259
275
  });
260
- // Subscription login: terminal-only for now, and the governed identity anchors on a
276
+ // Subscription login: Remote Control only for RC-entitled (internal) accounts, and the governed identity anchors on a
261
277
  // Mnemom API key (minted once and stored if there is none). Resolved after sign-in so
262
278
  // minting can use the session.
263
279
  let mnemomKey;
264
280
  if (auth === "subscription") {
265
- shape = subscriptionShape(shape, shapeExplicit, say);
281
+ shape = subscriptionShape(shape, shapeExplicit, rcAllowed, say);
266
282
  mnemomKey = await resolveSubscriptionMnemomKey(defaultMnemomKeyDeps());
267
283
  }
268
284
  // Now that a session exists, a defaulted slug uses the real login handle from
@@ -384,8 +400,7 @@ export async function agentCommand(scenarioArg, options = {}) {
384
400
  say(fmt.warn(`mnemom agent: context handoff could not be registered (${err instanceof Error ? err.message : String(err)}) — launching without the handoff tool.`));
385
401
  }
386
402
  }
387
- // A subscription launch (no Anthropic key) is always terminal — see subscriptionShape.
388
- if (shape === "terminal" || !anthropicKey) {
403
+ if (shape === "terminal") {
389
404
  launchTerminal({
390
405
  cli,
391
406
  door,
@@ -410,6 +425,7 @@ export async function agentCommand(scenarioArg, options = {}) {
410
425
  cli,
411
426
  door,
412
427
  anthropicKey,
428
+ mnemomKey,
413
429
  agent,
414
430
  conversationId,
415
431
  scenario,
@@ -855,6 +871,7 @@ function launchRemoteControl(ctx) {
855
871
  MNEMOM_AGENT_RC_SESSION_CONFIG: ctx.sessionConfigB64 ?? "",
856
872
  MNEMOM_AGENT_RC_PROXY_LOG: proxyLog,
857
873
  MNEMOM_AGENT_RC_PARENT_PID: String(process.pid),
874
+ MNEMOM_AGENT_RC_AUTH: ctx.anthropicKey ? "api-key" : "subscription",
858
875
  },
859
876
  stdio: ["pipe", "pipe", "inherit"],
860
877
  });
@@ -879,8 +896,9 @@ function launchRemoteControl(ctx) {
879
896
  }
880
897
  };
881
898
  process.on("exit", onExit);
882
- // Secrets on stdin — three lines (anthropic, mnemom, openai). Only the
883
- // Anthropic key is used on this Anthropic-only door; the other two are empty.
899
+ // Secrets on stdin — three lines (anthropic, mnemom, openai). An api-key launch sends the
900
+ // Anthropic key only; a subscription launch sends the Mnemom key only (the identity anchor —
901
+ // the user's OAuth bearer is what pays). openai is always empty on this Anthropic-only door.
884
902
  // A proxy that exits before reading them turns this write into an EPIPE; that is
885
903
  // not a failure of its own — the exited-before-READY check below refuses the
886
904
  // launch. Any other stdin error fails closed like a spawn error.
@@ -891,7 +909,7 @@ function launchRemoteControl(ctx) {
891
909
  onExit();
892
910
  process.exit(1);
893
911
  });
894
- proxy.stdin?.end(`${ctx.anthropicKey}\n\n\n`);
912
+ proxy.stdin?.end(`${ctx.anthropicKey ?? ""}\n${ctx.anthropicKey ? "" : (ctx.mnemomKey ?? "")}\n\n`);
895
913
  let stdoutBuf = "";
896
914
  const start = Date.now();
897
915
  const tryStart = () => {
@@ -976,8 +994,10 @@ function spawnRcCli(ctx) {
976
994
  // ANTHROPIC_* base-url/key/socket family (same reason HTTPS_PROXY/NODE_EXTRA_CA_CERTS already
977
995
  // ride through here ungated), so this rides through untouched. Only set when the tool is
978
996
  // actually registered — never on a plain RC launch.
979
- if (ctx.handoffMcpConfigPath)
997
+ // (Never on a subscription launch — handoff is turned off there, so there is no key to set.)
998
+ if (ctx.handoffMcpConfigPath && ctx.anthropicKey) {
980
999
  childEnv[HANDOFF_RC_KEY_ENV_NAME] = ctx.anthropicKey;
1000
+ }
981
1001
  let args;
982
1002
  if (ctx.shape === "server") {
983
1003
  // Headless dispatcher: no --model/--effort/--trim-mcp/yolo flags; the model
package/dist/index.js CHANGED
@@ -142,7 +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
+ .option("--auth <mode>", "How Claude Code authenticates: api-key (default) | subscription (your Claude Team/Enterprise login)")
146
146
  .option("--model <alias|id>", "Model to launch (CLI alias or concrete id)")
147
147
  .addOption(new Option("--effort <level>", "Reasoning effort").choices([
148
148
  "low",
@@ -135,7 +135,7 @@ export const CONFIG_KEYS = [
135
135
  },
136
136
  {
137
137
  key: "auth",
138
- describe: `How Claude Code authenticates (${AGENT_AUTHS.join("|")}); subscription = your Claude Team/Enterprise login, terminal-only`,
138
+ describe: `How Claude Code authenticates (${AGENT_AUTHS.join("|")}); subscription = your Claude Team/Enterprise login`,
139
139
  get: (c) => c.auth,
140
140
  set: (c, v) => {
141
141
  c.auth = coerceAgentAuth(v);
@@ -0,0 +1,72 @@
1
+ /**
2
+ * First-run login menu for `mnemom agent`: detect how Claude Code is signed in on this
3
+ * machine, then — only when there is a real choice to make — ask once and save the answer
4
+ * as launcher.toml `auth`.
5
+ *
6
+ * Four ways Claude Code can be signed in, and what the gateway does with each today:
7
+ * - Claude subscription, Team or Enterprise → `auth = subscription` (supported).
8
+ * - Claude subscription, Pro / Max (individual) → not supported (Team/Enterprise only).
9
+ * - Anthropic Console login → not supported yet.
10
+ * - Third-party provider (Bedrock / Vertex / Foundry) → not supported yet (no gateway door).
11
+ * An Anthropic API key always works (`auth = api-key`, the default).
12
+ *
13
+ * The signals are Claude Code's own client-side state (~/.claude.json `oauthAccount`, the
14
+ * CLAUDE_CODE_USE_* switches). They only pick a default: the gateway still checks the plan
15
+ * with Anthropic and refuses anything that isn't Team/Enterprise.
16
+ *
17
+ * Pure apart from the injected deps, so every arm is unit-tested.
18
+ */
19
+ import type { AgentAuth } from "./subscription-login.js";
20
+ export type ThirdPartyProvider = "bedrock" | "vertex" | "foundry";
21
+ export type ClaudeLogin = {
22
+ kind: "subscription";
23
+ plan: "team" | "enterprise";
24
+ } | {
25
+ kind: "individual";
26
+ plan: string;
27
+ } | {
28
+ kind: "console";
29
+ } | {
30
+ kind: "third-party";
31
+ provider: ThirdPartyProvider;
32
+ } | {
33
+ kind: "none";
34
+ };
35
+ export interface DetectInput {
36
+ /** Process env, then the `env` block of Claude Code's settings.json (the process wins). */
37
+ env: Record<string, string | undefined>;
38
+ settingsEnv: Record<string, string | undefined>;
39
+ /** Parsed ~/.claude.json, or undefined when missing/unreadable. */
40
+ claudeJson: unknown;
41
+ }
42
+ export declare function detectClaudeLogin(input: DetectInput): ClaudeLogin;
43
+ export type UnsupportedLogin = Extract<ClaudeLogin, {
44
+ kind: "individual" | "console" | "third-party";
45
+ }>;
46
+ /** Why a detected login can't be used through the gateway. */
47
+ export declare function unsupportedReason(login: UnsupportedLogin): string;
48
+ export declare const MENU_SUBSCRIPTION = "Claude subscription \u2014 Team or Enterprise (your claude.ai login)";
49
+ export declare const MENU_API_KEY = "Anthropic API key";
50
+ export interface ChooseAuthDeps {
51
+ login: ClaudeLogin;
52
+ interactive: boolean;
53
+ select(question: string, options: string[]): Promise<string | null>;
54
+ persist(auth: AgentAuth): void;
55
+ say(line: string): void;
56
+ }
57
+ /**
58
+ * Pick `auth` when nothing configured it (no --auth, MNEMOM_AGENT_AUTH or launcher.toml).
59
+ * - No Claude login detected → api-key, silently, nothing saved (today's behaviour).
60
+ * - A login we can't use → api-key, say why once, save it so the note isn't repeated.
61
+ * - Team/Enterprise and non-interactive → api-key, nothing saved (never switch a script's
62
+ * auth without asking); interactive → a two-option menu (subscription or API key), answer saved.
63
+ */
64
+ export declare function chooseAuth(deps: ChooseAuthDeps): Promise<AgentAuth>;
65
+ /** True when `auth` is already decided: `--auth`, or MNEMOM_AGENT_AUTH (launcher.toml folds in). */
66
+ export declare function authConfigured(flag: string | undefined, env: Record<string, string | undefined>): boolean;
67
+ /**
68
+ * Read Claude Code's own state the way Claude Code locates it: CLAUDE_CONFIG_DIR holds both
69
+ * `.claude.json` and `settings.json` when set; otherwise `~/.claude.json` and
70
+ * `~/.claude/settings.json`. A missing or corrupt file reads as "nothing there".
71
+ */
72
+ export declare function readClaudeLoginState(env: Record<string, string | undefined>, home: string, readFile: (path: string) => string): DetectInput;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * First-run login menu for `mnemom agent`: detect how Claude Code is signed in on this
3
+ * machine, then — only when there is a real choice to make — ask once and save the answer
4
+ * as launcher.toml `auth`.
5
+ *
6
+ * Four ways Claude Code can be signed in, and what the gateway does with each today:
7
+ * - Claude subscription, Team or Enterprise → `auth = subscription` (supported).
8
+ * - Claude subscription, Pro / Max (individual) → not supported (Team/Enterprise only).
9
+ * - Anthropic Console login → not supported yet.
10
+ * - Third-party provider (Bedrock / Vertex / Foundry) → not supported yet (no gateway door).
11
+ * An Anthropic API key always works (`auth = api-key`, the default).
12
+ *
13
+ * The signals are Claude Code's own client-side state (~/.claude.json `oauthAccount`, the
14
+ * CLAUDE_CODE_USE_* switches). They only pick a default: the gateway still checks the plan
15
+ * with Anthropic and refuses anything that isn't Team/Enterprise.
16
+ *
17
+ * Pure apart from the injected deps, so every arm is unit-tested.
18
+ */
19
+ const PROVIDER_SWITCHES = [
20
+ ["CLAUDE_CODE_USE_BEDROCK", "bedrock"],
21
+ ["CLAUDE_CODE_USE_VERTEX", "vertex"],
22
+ ["CLAUDE_CODE_USE_FOUNDRY", "foundry"],
23
+ ];
24
+ /** Claude Code treats any non-empty value other than 0/false as on. */
25
+ function switchOn(raw) {
26
+ const v = (raw ?? "").trim().toLowerCase();
27
+ return v !== "" && v !== "0" && v !== "false";
28
+ }
29
+ export function detectClaudeLogin(input) {
30
+ // A provider switch wins over any login: Claude Code then talks to that provider.
31
+ for (const [name, provider] of PROVIDER_SWITCHES) {
32
+ if (switchOn(input.env[name] ?? input.settingsEnv[name])) {
33
+ return { kind: "third-party", provider };
34
+ }
35
+ }
36
+ const account = input.claudeJson && typeof input.claudeJson === "object"
37
+ ? input.claudeJson.oauthAccount
38
+ : undefined;
39
+ if (!account || typeof account !== "object")
40
+ return { kind: "none" };
41
+ const orgType = String(account.organizationType ?? "");
42
+ if (orgType === "claude_team")
43
+ return { kind: "subscription", plan: "team" };
44
+ if (orgType === "claude_enterprise")
45
+ return { kind: "subscription", plan: "enterprise" };
46
+ // claude_pro, claude_max, claude_free … — a personal claude.ai plan.
47
+ if (orgType.startsWith("claude_"))
48
+ return { kind: "individual", plan: orgType.slice(7) };
49
+ // Signed in, but not to a claude.ai plan: an Anthropic Console account.
50
+ return { kind: "console" };
51
+ }
52
+ const PROVIDER_NAMES = {
53
+ bedrock: "Amazon Bedrock",
54
+ vertex: "Google Vertex AI",
55
+ foundry: "Microsoft Foundry",
56
+ };
57
+ /** Why a detected login can't be used through the gateway. */
58
+ export function unsupportedReason(login) {
59
+ switch (login.kind) {
60
+ case "individual":
61
+ return `your Claude ${login.plan} plan is a personal plan — Mnemom supports subscription login on Team and Enterprise plans only`;
62
+ case "console":
63
+ return "Anthropic Console login is not supported yet";
64
+ case "third-party":
65
+ return `${PROVIDER_NAMES[login.provider]} is not supported yet`;
66
+ }
67
+ }
68
+ export const MENU_SUBSCRIPTION = "Claude subscription — Team or Enterprise (your claude.ai login)";
69
+ export const MENU_API_KEY = "Anthropic API key";
70
+ const SAVED_HINT = "change it any time with `mnemom agent config set auth api-key|subscription`";
71
+ function save(deps, auth, line) {
72
+ try {
73
+ deps.persist(auth);
74
+ deps.say(`${line} Saved to launcher.toml — ${SAVED_HINT}.`);
75
+ }
76
+ catch {
77
+ deps.say(`${line} (Could not save it to launcher.toml; you'll be asked again.)`);
78
+ }
79
+ return auth;
80
+ }
81
+ /**
82
+ * Pick `auth` when nothing configured it (no --auth, MNEMOM_AGENT_AUTH or launcher.toml).
83
+ * - No Claude login detected → api-key, silently, nothing saved (today's behaviour).
84
+ * - A login we can't use → api-key, say why once, save it so the note isn't repeated.
85
+ * - Team/Enterprise and non-interactive → api-key, nothing saved (never switch a script's
86
+ * auth without asking); interactive → a two-option menu (subscription or API key), answer saved.
87
+ */
88
+ export async function chooseAuth(deps) {
89
+ const { login } = deps;
90
+ if (login.kind === "none")
91
+ return "api-key";
92
+ if (login.kind !== "subscription") {
93
+ return save(deps, "api-key", `mnemom agent: ${unsupportedReason(login)}, so this launch uses an Anthropic API key.`);
94
+ }
95
+ if (!deps.interactive)
96
+ return "api-key";
97
+ // Only options that work. The earlier menu also listed Console and Bedrock/Vertex/Foundry
98
+ // as "(not supported yet)" entries that re-asked when picked: a dead end. Operator ruling
99
+ // (2026-10-01): the product never offers a choice it can't fulfil, and never advertises
100
+ // what's coming. A login type comes back here only once it works end to end. Those logins
101
+ // already skip this menu above with a one-line reason.
102
+ const options = [MENU_SUBSCRIPTION, MENU_API_KEY];
103
+ const question = `mnemom agent: Claude Code is signed in with your Claude ${login.plan} plan. How should it sign in through Mnemom?`;
104
+ const picked = await deps.select(question, options);
105
+ if (picked === MENU_SUBSCRIPTION) {
106
+ return save(deps, "subscription", "mnemom agent: using your Claude subscription login.");
107
+ }
108
+ if (picked === MENU_API_KEY) {
109
+ return save(deps, "api-key", "mnemom agent: using an Anthropic API key.");
110
+ }
111
+ deps.say("mnemom agent: no choice made — using an Anthropic API key for this launch.");
112
+ return "api-key";
113
+ }
114
+ /** True when `auth` is already decided: `--auth`, or MNEMOM_AGENT_AUTH (launcher.toml folds in). */
115
+ export function authConfigured(flag, env) {
116
+ return Boolean(flag?.trim()) || Boolean((env.MNEMOM_AGENT_AUTH ?? "").trim());
117
+ }
118
+ /**
119
+ * Read Claude Code's own state the way Claude Code locates it: CLAUDE_CONFIG_DIR holds both
120
+ * `.claude.json` and `settings.json` when set; otherwise `~/.claude.json` and
121
+ * `~/.claude/settings.json`. A missing or corrupt file reads as "nothing there".
122
+ */
123
+ export function readClaudeLoginState(env, home, readFile) {
124
+ const dir = (env.CLAUDE_CONFIG_DIR ?? "").trim();
125
+ const parse = (path) => {
126
+ try {
127
+ return JSON.parse(readFile(path));
128
+ }
129
+ catch {
130
+ return undefined;
131
+ }
132
+ };
133
+ const claudeJson = parse(dir ? `${dir}/.claude.json` : `${home}/.claude.json`);
134
+ const settings = parse(dir ? `${dir}/settings.json` : `${home}/.claude/settings.json`);
135
+ const rawEnv = settings && typeof settings === "object" ? settings.env : undefined;
136
+ const settingsEnv = {};
137
+ if (rawEnv && typeof rawEnv === "object") {
138
+ for (const [k, v] of Object.entries(rawEnv))
139
+ if (typeof v === "string")
140
+ settingsEnv[k] = v;
141
+ }
142
+ return { env, settingsEnv, claudeJson };
143
+ }
@@ -11,8 +11,8 @@
11
11
  * freshly minted gateway-only key — and the agent is created in that key's org on the
12
12
  * first turn, so there is no CLI-side birth or claim.
13
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.
14
+ * Remote Control is internal-only (see subscriptionShape); the context-handoff tool
15
+ * authenticates with an Anthropic key, so it is off on a subscription launch.
16
16
  *
17
17
  * Pure apart from the injected deps, so every arm is unit-tested.
18
18
  */
@@ -42,10 +42,14 @@ export interface MnemomKeyDeps {
42
42
  */
43
43
  export declare function resolveSubscriptionMnemomKey(deps: MnemomKeyDeps): Promise<string>;
44
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.
45
+ * Remote Control on a subscription login is open to accounts entitled to Remote Control
46
+ * (internal Mnemom accounts): the RC proxy then forwards the user's own OAuth bearer to the
47
+ * gateway untouched (rc-proxy.mjs, MNEMOM_AGENT_RC_AUTH=subscription). Anyone else stays
48
+ * terminal-only: an explicit Remote Control / server request is refused, an automatic
49
+ * upgrade falls back to terminal with a notice. (resolveRemoteControlShape already degrades
50
+ * a non-entitled account to terminal; this is the fail-closed backstop.)
47
51
  */
48
- export declare function subscriptionShape(shape: LaunchShape, shapeExplicit: boolean, say: (line: string) => void): LaunchShape;
52
+ export declare function subscriptionShape(shape: LaunchShape, shapeExplicit: boolean, rcAllowed: boolean, say: (line: string) => void): LaunchShape;
49
53
  /**
50
54
  * The child env for a subscription launch: the gateway door, the Mnemom headers, and
51
55
  * NOTHING that would make Claude Code send a key instead of its claude.ai login.
@@ -11,8 +11,8 @@
11
11
  * freshly minted gateway-only key — and the agent is created in that key's org on the
12
12
  * first turn, so there is no CLI-side birth or claim.
13
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.
14
+ * Remote Control is internal-only (see subscriptionShape); the context-handoff tool
15
+ * authenticates with an Anthropic key, so it is off on a subscription launch.
16
16
  *
17
17
  * Pure apart from the injected deps, so every arm is unit-tested.
18
18
  */
@@ -75,11 +75,15 @@ export async function resolveSubscriptionMnemomKey(deps) {
75
75
  return minted;
76
76
  }
77
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.
78
+ * Remote Control on a subscription login is open to accounts entitled to Remote Control
79
+ * (internal Mnemom accounts): the RC proxy then forwards the user's own OAuth bearer to the
80
+ * gateway untouched (rc-proxy.mjs, MNEMOM_AGENT_RC_AUTH=subscription). Anyone else stays
81
+ * terminal-only: an explicit Remote Control / server request is refused, an automatic
82
+ * upgrade falls back to terminal with a notice. (resolveRemoteControlShape already degrades
83
+ * a non-entitled account to terminal; this is the fail-closed backstop.)
80
84
  */
81
- export function subscriptionShape(shape, shapeExplicit, say) {
82
- if (shape === "terminal")
85
+ export function subscriptionShape(shape, shapeExplicit, rcAllowed, say) {
86
+ if (shape === "terminal" || rcAllowed)
83
87
  return shape;
84
88
  if (shapeExplicit) {
85
89
  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.`);
package/dist/rc-proxy.mjs CHANGED
@@ -69,6 +69,11 @@
69
69
  // MNEMOM_AGENT_RC_PROVIDER x-mnemom-provider directive JSON (optional; router door)
70
70
  // MNEMOM_AGENT_RC_THREAD_MODE `reject` (default: answer thread-bearing requests locally,
71
71
  // see above) | `strip` (legacy: delete `thread`, forward)
72
+ // MNEMOM_AGENT_RC_AUTH `api-key` (default) | `subscription`: the user's own claude.ai
73
+ // Team/Enterprise login pays — the OAuth bearer and `oauth-*`
74
+ // betas are FORWARDED to the gateway untouched, no Anthropic key
75
+ // is involved (stdin line 1 empty), and the Mnemom key (line 2)
76
+ // is required: it anchors the governed identity.
72
77
  // MNEMOM_AGENT_RC_PROXY_LOG append log lines to this file instead of stderr (optional)
73
78
  // MNEMOM_AGENT_RC_PARENT_PID exit when this pid is gone, so no orphan holds the keys
74
79
  // MNEMOM_AGENT_RC_PORT listen port (default 0 = ephemeral)
@@ -107,6 +112,7 @@ const GOAL_MODE = env.MNEMOM_AGENT_RC_GOAL_MODE || "";
107
112
  const SESSION_CONFIG = env.MNEMOM_AGENT_RC_SESSION_CONFIG || "";
108
113
  const PROVIDER = env.MNEMOM_AGENT_RC_PROVIDER || "";
109
114
  const THREAD_MODE = env.MNEMOM_AGENT_RC_THREAD_MODE || "reject";
115
+ const SUBSCRIPTION = env.MNEMOM_AGENT_RC_AUTH === "subscription";
110
116
  if (THREAD_MODE !== "reject" && THREAD_MODE !== "strip")
111
117
  die(`MNEMOM_AGENT_RC_THREAD_MODE must be 'reject' or 'strip', got '${THREAD_MODE}'`);
112
118
  const LOGFILE = env.MNEMOM_AGENT_RC_PROXY_LOG || "";
@@ -140,7 +146,9 @@ try {
140
146
  } catch {
141
147
  die("expected three lines of secrets on stdin (anthropic, mnemom, openai)");
142
148
  }
143
- if (!secrets.anthropic) die("the Anthropic key (stdin line 1) is empty");
149
+ if (SUBSCRIPTION) {
150
+ if (!secrets.mnemom) die("subscription login needs the Mnemom key (stdin line 2)");
151
+ } else if (!secrets.anthropic) die("the Anthropic key (stdin line 1) is empty");
144
152
 
145
153
  // --- ephemeral CA + rotating leaf for api.anthropic.com ---------------------------------
146
154
  // Private 0700 dir under the OS tmpdir, removed on exit. Claude Code reads NODE_EXTRA_CA_CERTS
@@ -403,8 +411,10 @@ function handle(req, res, viaPlain) {
403
411
  agent: gatewayAgent,
404
412
  };
405
413
  noteCcSession(headers["x-claude-code-session-id"]);
406
- delete headers.authorization; // the OAuth bearer stays local
407
- headers["x-api-key"] = secrets.anthropic;
414
+ if (!SUBSCRIPTION) {
415
+ delete headers.authorization; // the OAuth bearer stays local
416
+ headers["x-api-key"] = secrets.anthropic;
417
+ }
408
418
  headers["x-mnemom-agent"] = AGENT;
409
419
  headers["x-mnemom-conversation-id"] = CONV;
410
420
  if (secrets.mnemom) headers["x-mnemom-api-key"] = secrets.mnemom;
@@ -413,8 +423,8 @@ function handle(req, res, viaPlain) {
413
423
  if (CONTRACT_B64) headers["x-mnemom-contract"] = CONTRACT_B64;
414
424
  if (GOAL_MODE) headers["x-mnemom-goal-mode"] = GOAL_MODE;
415
425
  if (SESSION_CONFIG) headers["x-mnemom-session-config"] = SESSION_CONFIG;
416
- if (headers["anthropic-beta"]) {
417
- // drop the OAuth-only beta tokens
426
+ if (!SUBSCRIPTION && headers["anthropic-beta"]) {
427
+ // drop the OAuth-only beta tokens (a subscription leg keeps them: the bearer rides on)
418
428
  const b = String(headers["anthropic-beta"])
419
429
  .split(",")
420
430
  .map((s) => s.trim())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.4-next.1",
3
+ "version": "0.17.4-next.3",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {