@mnemom/mnemom 0.17.4-next.0 → 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.d.ts +29 -0
- package/dist/commands/agent.js +109 -15
- package/dist/index.js +1 -1
- package/dist/lib/agent-ledger.d.ts +27 -0
- package/dist/lib/agent-ledger.js +27 -0
- package/dist/lib/agent.d.ts +9 -0
- package/dist/lib/agent.js +9 -0
- 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.d.ts
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* an argv value.
|
|
27
27
|
*/
|
|
28
28
|
import { type MnemomKeyDeps } from "../lib/subscription-login.js";
|
|
29
|
+
import { type SessionRecord } from "../lib/agent-ledger.js";
|
|
29
30
|
import { type ExperimentalId } from "../lib/experimental.js";
|
|
30
31
|
import { type ContextProfile, type LaunchShape, type SealedContractEnvelope } from "../lib/agent.js";
|
|
31
32
|
export interface AgentOptions {
|
|
@@ -103,6 +104,34 @@ export interface AgentOptions {
|
|
|
103
104
|
}
|
|
104
105
|
/** Entry point for `mnemom agent [scenario]`. Throws on fatal misconfig. */
|
|
105
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>;
|
|
106
135
|
/**
|
|
107
136
|
* Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
|
|
108
137
|
* resolved best-effort (a lookup failure or an unknown slug records null); a
|
package/dist/commands/agent.js
CHANGED
|
@@ -34,15 +34,16 @@ import { fileURLToPath } from "node:url";
|
|
|
34
34
|
import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
|
|
35
35
|
import { createApiKey, getAgentByName } from "../lib/api.js";
|
|
36
36
|
import { MNEMOM_KEY_STORE_FIELD, resolveAgentAuth, resolveSubscriptionMnemomKey, subscriptionChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
|
|
37
|
-
import { LEDGER_FILE, appendSessionRecord, resolveRetentionDays, toLedgerContract, } from "../lib/agent-ledger.js";
|
|
37
|
+
import { LEDGER_FILE, appendSessionRecord, findResumeCandidate, readSessionRecords, resolveRetentionDays, toLedgerContract, } from "../lib/agent-ledger.js";
|
|
38
38
|
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
|
-
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";
|
|
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";
|
|
46
47
|
import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
|
|
47
48
|
import { slackChannelArgs } from "../lib/slack-launch.js";
|
|
48
49
|
/** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
|
|
@@ -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
|
|
@@ -89,7 +105,6 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
89
105
|
if (!agent) {
|
|
90
106
|
throw new Error("mnemom agent: --agent sanitised to an empty string — pick a value with at least one letter or digit.");
|
|
91
107
|
}
|
|
92
|
-
const conversationId = resolveConversationId(options.conversationId);
|
|
93
108
|
// ── contract (built only if a contract flag was given; --goal then required) ──
|
|
94
109
|
const envelope = assembleContract({
|
|
95
110
|
goal: session.goal,
|
|
@@ -173,7 +188,8 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
173
188
|
scenario,
|
|
174
189
|
shape: previewShape,
|
|
175
190
|
agent,
|
|
176
|
-
|
|
191
|
+
// The org is only known after sign-in, so a dry run never offers a resume (IG5 #1).
|
|
192
|
+
conversationId: resolveConversationId(options.conversationId),
|
|
177
193
|
door,
|
|
178
194
|
cellNote,
|
|
179
195
|
envelope,
|
|
@@ -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
|
|
@@ -305,6 +321,22 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
305
321
|
say(fmt.warn(`mnemom agent: agent setup did not complete (${msg}) — launching with the unclaimed identity.`));
|
|
306
322
|
}
|
|
307
323
|
}
|
|
324
|
+
// ── conversation id ── an implicit relaunch continues its earlier conversation (IG5 #1, D1)
|
|
325
|
+
// so the goal the gateway inferred there is still the one it steers by. Resolved here, after
|
|
326
|
+
// sign-in and the agent refresh, because a resume needs the org, agent and gateway host to
|
|
327
|
+
// match the earlier launch.
|
|
328
|
+
const conversationId = await resolveLaunchConversationId({
|
|
329
|
+
session: scenario,
|
|
330
|
+
orgId: account.orgId,
|
|
331
|
+
agent,
|
|
332
|
+
gatewayHost,
|
|
333
|
+
explicit: options.conversationId,
|
|
334
|
+
implicit: goalAlignmentMode(envelope, goalMode) === "implicit",
|
|
335
|
+
passthrough: options.passthrough ?? [],
|
|
336
|
+
interactive: isInteractive(),
|
|
337
|
+
records: () => readSessionRecords({ dir: MNEMOM_DIR }),
|
|
338
|
+
askYesNo: (q, d) => askYesNo(q, d),
|
|
339
|
+
});
|
|
308
340
|
// ── local session ledger (~/.mnemom/sessions.jsonl) ── one line per real
|
|
309
341
|
// launch so `mnemom agent sessions` / `show` can find this conversation later.
|
|
310
342
|
// Written after provisioning so a freshly claimed agent's id resolves. Holds
|
|
@@ -368,8 +400,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
368
400
|
say(fmt.warn(`mnemom agent: context handoff could not be registered (${err instanceof Error ? err.message : String(err)}) — launching without the handoff tool.`));
|
|
369
401
|
}
|
|
370
402
|
}
|
|
371
|
-
|
|
372
|
-
if (shape === "terminal" || !anthropicKey) {
|
|
403
|
+
if (shape === "terminal") {
|
|
373
404
|
launchTerminal({
|
|
374
405
|
cli,
|
|
375
406
|
door,
|
|
@@ -394,6 +425,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
394
425
|
cli,
|
|
395
426
|
door,
|
|
396
427
|
anthropicKey,
|
|
428
|
+
mnemomKey,
|
|
397
429
|
agent,
|
|
398
430
|
conversationId,
|
|
399
431
|
scenario,
|
|
@@ -409,6 +441,63 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
409
441
|
// ───────────────────────────────────────────────────────────────────────────
|
|
410
442
|
// Session ledger (thin driver over lib/agent-ledger.ts)
|
|
411
443
|
// ───────────────────────────────────────────────────────────────────────────
|
|
444
|
+
/** The Claude Code session id a passthrough `--resume <id>` / `-r <id>` / `--resume=<id>` names. */
|
|
445
|
+
export function passthroughResumeId(passthrough) {
|
|
446
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
447
|
+
const a = passthrough[i];
|
|
448
|
+
if (a === "--resume" || a === "-r") {
|
|
449
|
+
const next = passthrough[i + 1];
|
|
450
|
+
return next !== undefined && !next.startsWith("-") && next.trim() !== "" ? next.trim() : null;
|
|
451
|
+
}
|
|
452
|
+
if (a.startsWith("--resume="))
|
|
453
|
+
return a.slice("--resume=".length).trim() || null;
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* The conversation id this launch runs under (IG5 #1, decision D1). The gateway keys the
|
|
459
|
+
* implicit goal by conversation id, so an implicit relaunch reuses the earlier one:
|
|
460
|
+
* - `--conversation-id` → that id, no lookup;
|
|
461
|
+
* - not implicit (a sealed contract, or alignment off) → a new id;
|
|
462
|
+
* - only an earlier implicit launch with the same session name, org, agent and gateway host
|
|
463
|
+
* is continued; any mismatch → a new id, no prompt;
|
|
464
|
+
* - `-- --resume <cc-session-id>` → the matching row that pinned that Claude Code session
|
|
465
|
+
* (else the newest matching row), reused without asking;
|
|
466
|
+
* - no `--resume` → "Resume previous session <name>? [Y/n]", default Y; Y reuses that row's
|
|
467
|
+
* conversation, n mints a new one. `--resume` is NOT added to Claude Code (the user often
|
|
468
|
+
* resumes inside it with /resume). No TTY: Y.
|
|
469
|
+
* The ledger read is best-effort: a failure means a new id.
|
|
470
|
+
*/
|
|
471
|
+
export async function resolveLaunchConversationId(input) {
|
|
472
|
+
if ((input.explicit ?? "").trim())
|
|
473
|
+
return resolveConversationId(input.explicit);
|
|
474
|
+
if (!input.implicit)
|
|
475
|
+
return resolveConversationId(undefined);
|
|
476
|
+
let records;
|
|
477
|
+
try {
|
|
478
|
+
records = input.records();
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
return resolveConversationId(undefined);
|
|
482
|
+
}
|
|
483
|
+
const found = findResumeCandidate(records, {
|
|
484
|
+
session: input.session,
|
|
485
|
+
orgId: input.orgId,
|
|
486
|
+
agent: input.agent,
|
|
487
|
+
gatewayHost: input.gatewayHost,
|
|
488
|
+
resumeId: passthroughResumeId(input.passthrough),
|
|
489
|
+
});
|
|
490
|
+
if (!found)
|
|
491
|
+
return resolveConversationId(undefined);
|
|
492
|
+
const conv = found.record.conversation_id;
|
|
493
|
+
if (found.source === "session_name" && input.interactive) {
|
|
494
|
+
const yes = await input.askYesNo(`Resume previous session ${input.session}?`, true);
|
|
495
|
+
if (!yes)
|
|
496
|
+
return resolveConversationId(undefined);
|
|
497
|
+
}
|
|
498
|
+
say(`mnemom agent: continuing conversation ${conv} from the earlier '${found.record.session}' launch (${found.record.started_at}), so its goal carries over.`);
|
|
499
|
+
return resolveConversationId(conv);
|
|
500
|
+
}
|
|
412
501
|
/**
|
|
413
502
|
* Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
|
|
414
503
|
* resolved best-effort (a lookup failure or an unknown slug records null); a
|
|
@@ -738,6 +827,7 @@ function launchTerminal(ctx) {
|
|
|
738
827
|
}
|
|
739
828
|
const sessionName = passthroughName(ctx.options.passthrough ?? []) ?? sessionDisplayName(ctx.agent, ctx.scenario);
|
|
740
829
|
childEnv[SESSION_NAME_ENV] = sessionName;
|
|
830
|
+
childEnv[GATEWAY_HINT_HEADERS_ENV] = "1";
|
|
741
831
|
if (ctx.options.slack)
|
|
742
832
|
args.push(...slackChannelArgs(sessionName));
|
|
743
833
|
if (ctx.contextHint) {
|
|
@@ -781,6 +871,7 @@ function launchRemoteControl(ctx) {
|
|
|
781
871
|
MNEMOM_AGENT_RC_SESSION_CONFIG: ctx.sessionConfigB64 ?? "",
|
|
782
872
|
MNEMOM_AGENT_RC_PROXY_LOG: proxyLog,
|
|
783
873
|
MNEMOM_AGENT_RC_PARENT_PID: String(process.pid),
|
|
874
|
+
MNEMOM_AGENT_RC_AUTH: ctx.anthropicKey ? "api-key" : "subscription",
|
|
784
875
|
},
|
|
785
876
|
stdio: ["pipe", "pipe", "inherit"],
|
|
786
877
|
});
|
|
@@ -805,8 +896,9 @@ function launchRemoteControl(ctx) {
|
|
|
805
896
|
}
|
|
806
897
|
};
|
|
807
898
|
process.on("exit", onExit);
|
|
808
|
-
// Secrets on stdin — three lines (anthropic, mnemom, openai).
|
|
809
|
-
// 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.
|
|
810
902
|
// A proxy that exits before reading them turns this write into an EPIPE; that is
|
|
811
903
|
// not a failure of its own — the exited-before-READY check below refuses the
|
|
812
904
|
// launch. Any other stdin error fails closed like a spawn error.
|
|
@@ -817,7 +909,7 @@ function launchRemoteControl(ctx) {
|
|
|
817
909
|
onExit();
|
|
818
910
|
process.exit(1);
|
|
819
911
|
});
|
|
820
|
-
proxy.stdin?.end(`${ctx.anthropicKey}\n\n\n`);
|
|
912
|
+
proxy.stdin?.end(`${ctx.anthropicKey ?? ""}\n${ctx.anthropicKey ? "" : (ctx.mnemomKey ?? "")}\n\n`);
|
|
821
913
|
let stdoutBuf = "";
|
|
822
914
|
const start = Date.now();
|
|
823
915
|
const tryStart = () => {
|
|
@@ -902,8 +994,10 @@ function spawnRcCli(ctx) {
|
|
|
902
994
|
// ANTHROPIC_* base-url/key/socket family (same reason HTTPS_PROXY/NODE_EXTRA_CA_CERTS already
|
|
903
995
|
// ride through here ungated), so this rides through untouched. Only set when the tool is
|
|
904
996
|
// actually registered — never on a plain RC launch.
|
|
905
|
-
|
|
997
|
+
// (Never on a subscription launch — handoff is turned off there, so there is no key to set.)
|
|
998
|
+
if (ctx.handoffMcpConfigPath && ctx.anthropicKey) {
|
|
906
999
|
childEnv[HANDOFF_RC_KEY_ENV_NAME] = ctx.anthropicKey;
|
|
1000
|
+
}
|
|
907
1001
|
let args;
|
|
908
1002
|
if (ctx.shape === "server") {
|
|
909
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",
|
|
@@ -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;
|
package/dist/lib/agent-ledger.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/lib/agent.d.ts
CHANGED
|
@@ -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). */
|
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;
|
|
@@ -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())
|