@mnemom/mnemom 0.17.4-next.1 → 0.17.4-next.2
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.
- package/dist/commands/agent.js +31 -11
- package/dist/index.js +1 -1
- package/dist/lib/launcher-config.js +1 -1
- package/dist/lib/login-detect.d.ts +74 -0
- package/dist/lib/login-detect.js +147 -0
- package/dist/lib/subscription-login.d.ts +9 -5
- package/dist/lib/subscription-login.js +10 -6
- package/dist/rc-proxy.mjs +15 -5
- package/package.json +1 -1
package/dist/commands/agent.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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).
|
|
883
|
-
// Anthropic key
|
|
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
|
-
|
|
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
|
|
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
|
|
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,74 @@
|
|
|
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_CONSOLE = "Anthropic Console login (not supported yet)";
|
|
50
|
+
export declare const MENU_THIRD_PARTY = "Amazon Bedrock / Google Vertex AI / Microsoft Foundry (not supported yet)";
|
|
51
|
+
export declare const MENU_API_KEY = "Anthropic API key";
|
|
52
|
+
export interface ChooseAuthDeps {
|
|
53
|
+
login: ClaudeLogin;
|
|
54
|
+
interactive: boolean;
|
|
55
|
+
select(question: string, options: string[]): Promise<string | null>;
|
|
56
|
+
persist(auth: AgentAuth): void;
|
|
57
|
+
say(line: string): void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Pick `auth` when nothing configured it (no --auth, MNEMOM_AGENT_AUTH or launcher.toml).
|
|
61
|
+
* - No Claude login detected → api-key, silently, nothing saved (today's behaviour).
|
|
62
|
+
* - A login we can't use → api-key, say why once, save it so the note isn't repeated.
|
|
63
|
+
* - Team/Enterprise and non-interactive → api-key, nothing saved (never switch a script's
|
|
64
|
+
* auth without asking); interactive → the four-option menu, answer saved.
|
|
65
|
+
*/
|
|
66
|
+
export declare function chooseAuth(deps: ChooseAuthDeps): Promise<AgentAuth>;
|
|
67
|
+
/** True when `auth` is already decided: `--auth`, or MNEMOM_AGENT_AUTH (launcher.toml folds in). */
|
|
68
|
+
export declare function authConfigured(flag: string | undefined, env: Record<string, string | undefined>): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Read Claude Code's own state the way Claude Code locates it: CLAUDE_CONFIG_DIR holds both
|
|
71
|
+
* `.claude.json` and `settings.json` when set; otherwise `~/.claude.json` and
|
|
72
|
+
* `~/.claude/settings.json`. A missing or corrupt file reads as "nothing there".
|
|
73
|
+
*/
|
|
74
|
+
export declare function readClaudeLoginState(env: Record<string, string | undefined>, home: string, readFile: (path: string) => string): DetectInput;
|
|
@@ -0,0 +1,147 @@
|
|
|
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_CONSOLE = "Anthropic Console login (not supported yet)";
|
|
70
|
+
export const MENU_THIRD_PARTY = "Amazon Bedrock / Google Vertex AI / Microsoft Foundry (not supported yet)";
|
|
71
|
+
export const MENU_API_KEY = "Anthropic API key";
|
|
72
|
+
const SAVED_HINT = "change it any time with `mnemom agent config set auth api-key|subscription`";
|
|
73
|
+
function save(deps, auth, line) {
|
|
74
|
+
try {
|
|
75
|
+
deps.persist(auth);
|
|
76
|
+
deps.say(`${line} Saved to launcher.toml — ${SAVED_HINT}.`);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
deps.say(`${line} (Could not save it to launcher.toml; you'll be asked again.)`);
|
|
80
|
+
}
|
|
81
|
+
return auth;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Pick `auth` when nothing configured it (no --auth, MNEMOM_AGENT_AUTH or launcher.toml).
|
|
85
|
+
* - No Claude login detected → api-key, silently, nothing saved (today's behaviour).
|
|
86
|
+
* - A login we can't use → api-key, say why once, save it so the note isn't repeated.
|
|
87
|
+
* - Team/Enterprise and non-interactive → api-key, nothing saved (never switch a script's
|
|
88
|
+
* auth without asking); interactive → the four-option menu, answer saved.
|
|
89
|
+
*/
|
|
90
|
+
export async function chooseAuth(deps) {
|
|
91
|
+
const { login } = deps;
|
|
92
|
+
if (login.kind === "none")
|
|
93
|
+
return "api-key";
|
|
94
|
+
if (login.kind !== "subscription") {
|
|
95
|
+
return save(deps, "api-key", `mnemom agent: ${unsupportedReason(login)}, so this launch uses an Anthropic API key.`);
|
|
96
|
+
}
|
|
97
|
+
if (!deps.interactive)
|
|
98
|
+
return "api-key";
|
|
99
|
+
const options = [MENU_SUBSCRIPTION, MENU_CONSOLE, MENU_THIRD_PARTY, MENU_API_KEY];
|
|
100
|
+
const question = `mnemom agent: Claude Code is signed in with your Claude ${login.plan} plan. How should it sign in through Mnemom?`;
|
|
101
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
102
|
+
const picked = await deps.select(question, options);
|
|
103
|
+
if (picked === MENU_SUBSCRIPTION) {
|
|
104
|
+
return save(deps, "subscription", "mnemom agent: using your Claude subscription login.");
|
|
105
|
+
}
|
|
106
|
+
if (picked === MENU_API_KEY) {
|
|
107
|
+
return save(deps, "api-key", "mnemom agent: using an Anthropic API key.");
|
|
108
|
+
}
|
|
109
|
+
if (picked === MENU_CONSOLE || picked === MENU_THIRD_PARTY) {
|
|
110
|
+
deps.say("mnemom agent: that one isn't supported yet — pick 1 or 4.");
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
break; // no answer (EOF / out of range): don't loop forever
|
|
114
|
+
}
|
|
115
|
+
deps.say("mnemom agent: no choice made — using an Anthropic API key for this launch.");
|
|
116
|
+
return "api-key";
|
|
117
|
+
}
|
|
118
|
+
/** True when `auth` is already decided: `--auth`, or MNEMOM_AGENT_AUTH (launcher.toml folds in). */
|
|
119
|
+
export function authConfigured(flag, env) {
|
|
120
|
+
return Boolean(flag?.trim()) || Boolean((env.MNEMOM_AGENT_AUTH ?? "").trim());
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Read Claude Code's own state the way Claude Code locates it: CLAUDE_CONFIG_DIR holds both
|
|
124
|
+
* `.claude.json` and `settings.json` when set; otherwise `~/.claude.json` and
|
|
125
|
+
* `~/.claude/settings.json`. A missing or corrupt file reads as "nothing there".
|
|
126
|
+
*/
|
|
127
|
+
export function readClaudeLoginState(env, home, readFile) {
|
|
128
|
+
const dir = (env.CLAUDE_CONFIG_DIR ?? "").trim();
|
|
129
|
+
const parse = (path) => {
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(readFile(path));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const claudeJson = parse(dir ? `${dir}/.claude.json` : `${home}/.claude.json`);
|
|
138
|
+
const settings = parse(dir ? `${dir}/settings.json` : `${home}/.claude/settings.json`);
|
|
139
|
+
const rawEnv = settings && typeof settings === "object" ? settings.env : undefined;
|
|
140
|
+
const settingsEnv = {};
|
|
141
|
+
if (rawEnv && typeof rawEnv === "object") {
|
|
142
|
+
for (const [k, v] of Object.entries(rawEnv))
|
|
143
|
+
if (typeof v === "string")
|
|
144
|
+
settingsEnv[k] = v;
|
|
145
|
+
}
|
|
146
|
+
return { env, settingsEnv, claudeJson };
|
|
147
|
+
}
|
|
@@ -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
|
-
*
|
|
15
|
-
*
|
|
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
|
-
*
|
|
46
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
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
|
-
*
|
|
79
|
-
*
|
|
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 (
|
|
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
|
-
|
|
407
|
-
|
|
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())
|