@mnemom/mnemom 0.17.3 → 0.17.4-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agent-doctor.js +21 -8
- package/dist/commands/agent.d.ts +34 -0
- package/dist/commands/agent.js +148 -18
- package/dist/index.js +2 -0
- package/dist/lib/agent-ledger.d.ts +27 -0
- package/dist/lib/agent-ledger.js +27 -0
- package/dist/lib/agent.d.ts +13 -1
- package/dist/lib/agent.js +11 -1
- package/dist/lib/launcher-config.d.ts +3 -0
- package/dist/lib/launcher-config.js +16 -0
- package/dist/lib/subscription-login.d.ts +53 -0
- package/dist/lib/subscription-login.js +103 -0
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { spawnSync } from "node:child_process";
|
|
15
15
|
import { existsSync } from "node:fs";
|
|
16
16
|
import { anthropicKeySource, checkRcCapability, resolveCliBin } from "./agent.js";
|
|
17
|
+
import { resolveAgentAuth } from "../lib/subscription-login.js";
|
|
17
18
|
import { isProdGateway, resolveAnthropicDoor, resolveGatewayHost, singleCellError, } from "../lib/agent.js";
|
|
18
19
|
import { getApiUrl, getGatewayUrl } from "../lib/config.js";
|
|
19
20
|
import { LAUNCHER_CONFIG_PATH } from "../lib/launcher-config.js";
|
|
@@ -45,14 +46,26 @@ export async function agentDoctorCommand(options = {}) {
|
|
|
45
46
|
catch (err) {
|
|
46
47
|
add("miss", `coding-agent CLI: ${err instanceof Error ? err.message.split("\n")[0] : err}`, "install Claude Code, or pass --cli <name|/full/path> / set it with `mnemom agent config set cli <path>`");
|
|
47
48
|
}
|
|
48
|
-
// ── Anthropic key ──
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
// ── auth: an Anthropic key, or the claude.ai subscription login (no key needed) ──
|
|
50
|
+
let auth = "api-key";
|
|
51
|
+
try {
|
|
52
|
+
auth = resolveAgentAuth(undefined, env);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
add("miss", `auth: ${err instanceof Error ? err.message : String(err)}`, "`mnemom agent config set auth api-key` (or subscription)");
|
|
56
|
+
}
|
|
57
|
+
if (auth === "subscription") {
|
|
58
|
+
add("ok", "auth: subscription — Claude Code uses your claude.ai login (Team/Enterprise plans only); no Anthropic key needed");
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const keySrc = anthropicKeySource(env);
|
|
62
|
+
if (keySrc === "env")
|
|
63
|
+
add("ok", "Anthropic key: found in the environment");
|
|
64
|
+
else if (keySrc === "store")
|
|
65
|
+
add("ok", "Anthropic key: found in ~/.mnemom/launcher.json");
|
|
66
|
+
else
|
|
67
|
+
add("miss", "Anthropic key: none found", "export MNEMOM_AGENT_ANTHROPIC_KEY=sk-ant-… (or ANTHROPIC_API_KEY), or run `mnemom agent` once and accept the save prompt");
|
|
68
|
+
}
|
|
56
69
|
// ── one cell: the launch door must be the CLI's own cell ──
|
|
57
70
|
const cellError = singleCellError({
|
|
58
71
|
door: resolveAnthropicDoor(env),
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* line, or the proxy's stdin under RC). It is never logged, printed, or passed as
|
|
26
26
|
* an argv value.
|
|
27
27
|
*/
|
|
28
|
+
import { type MnemomKeyDeps } from "../lib/subscription-login.js";
|
|
29
|
+
import { type SessionRecord } from "../lib/agent-ledger.js";
|
|
28
30
|
import { type ExperimentalId } from "../lib/experimental.js";
|
|
29
31
|
import { type ContextProfile, type LaunchShape, type SealedContractEnvelope } from "../lib/agent.js";
|
|
30
32
|
export interface AgentOptions {
|
|
@@ -48,6 +50,8 @@ export interface AgentOptions {
|
|
|
48
50
|
cli?: string;
|
|
49
51
|
remoteControl?: boolean;
|
|
50
52
|
server?: boolean;
|
|
53
|
+
/** `--auth api-key|subscription` (lib/subscription-login.ts); else MNEMOM_AGENT_AUTH / config. */
|
|
54
|
+
auth?: string;
|
|
51
55
|
model?: string;
|
|
52
56
|
effort?: string;
|
|
53
57
|
trimMcp?: boolean;
|
|
@@ -100,6 +104,34 @@ export interface AgentOptions {
|
|
|
100
104
|
}
|
|
101
105
|
/** Entry point for `mnemom agent [scenario]`. Throws on fatal misconfig. */
|
|
102
106
|
export declare function agentCommand(scenarioArg: string | undefined, options?: AgentOptions): Promise<void>;
|
|
107
|
+
/** The Claude Code session id a passthrough `--resume <id>` / `-r <id>` / `--resume=<id>` names. */
|
|
108
|
+
export declare function passthroughResumeId(passthrough: readonly string[]): string | null;
|
|
109
|
+
/**
|
|
110
|
+
* The conversation id this launch runs under (IG5 #1, decision D1). The gateway keys the
|
|
111
|
+
* implicit goal by conversation id, so an implicit relaunch reuses the earlier one:
|
|
112
|
+
* - `--conversation-id` → that id, no lookup;
|
|
113
|
+
* - not implicit (a sealed contract, or alignment off) → a new id;
|
|
114
|
+
* - only an earlier implicit launch with the same session name, org, agent and gateway host
|
|
115
|
+
* is continued; any mismatch → a new id, no prompt;
|
|
116
|
+
* - `-- --resume <cc-session-id>` → the matching row that pinned that Claude Code session
|
|
117
|
+
* (else the newest matching row), reused without asking;
|
|
118
|
+
* - no `--resume` → "Resume previous session <name>? [Y/n]", default Y; Y reuses that row's
|
|
119
|
+
* conversation, n mints a new one. `--resume` is NOT added to Claude Code (the user often
|
|
120
|
+
* resumes inside it with /resume). No TTY: Y.
|
|
121
|
+
* The ledger read is best-effort: a failure means a new id.
|
|
122
|
+
*/
|
|
123
|
+
export declare function resolveLaunchConversationId(input: {
|
|
124
|
+
session: string;
|
|
125
|
+
orgId: string;
|
|
126
|
+
agent: string;
|
|
127
|
+
gatewayHost: string;
|
|
128
|
+
explicit?: string;
|
|
129
|
+
implicit: boolean;
|
|
130
|
+
passthrough: readonly string[];
|
|
131
|
+
interactive: boolean;
|
|
132
|
+
records: () => SessionRecord[];
|
|
133
|
+
askYesNo: (question: string, defaultYes: boolean) => Promise<boolean>;
|
|
134
|
+
}): Promise<string>;
|
|
103
135
|
/**
|
|
104
136
|
* Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
|
|
105
137
|
* resolved best-effort (a lookup failure or an unknown slug records null); a
|
|
@@ -124,6 +156,8 @@ export declare function recordSessionLaunch(ctx: {
|
|
|
124
156
|
/** The resolved context profile this launch runs under. */
|
|
125
157
|
contextProfile: ContextProfile;
|
|
126
158
|
}): Promise<void>;
|
|
159
|
+
/** The real I/O behind a subscription launch's Mnemom key (lib/subscription-login.ts). */
|
|
160
|
+
export declare function defaultMnemomKeyDeps(storePath?: string): MnemomKeyDeps;
|
|
127
161
|
/**
|
|
128
162
|
* Resolve the Anthropic key with NO 1Password dependency (built for external
|
|
129
163
|
* customer adoption), in order:
|
package/dist/commands/agent.js
CHANGED
|
@@ -28,12 +28,13 @@
|
|
|
28
28
|
import { spawn, spawnSync } from "node:child_process";
|
|
29
29
|
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync, renameSync, } from "node:fs";
|
|
30
30
|
import { randomUUID } from "node:crypto";
|
|
31
|
-
import { tmpdir, homedir } from "node:os";
|
|
31
|
+
import { tmpdir, homedir, hostname } from "node:os";
|
|
32
32
|
import { basename, join, dirname } from "node:path";
|
|
33
33
|
import { fileURLToPath } from "node:url";
|
|
34
34
|
import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
|
|
35
|
-
import { getAgentByName } from "../lib/api.js";
|
|
36
|
-
import {
|
|
35
|
+
import { createApiKey, getAgentByName } from "../lib/api.js";
|
|
36
|
+
import { MNEMOM_KEY_STORE_FIELD, resolveAgentAuth, resolveSubscriptionMnemomKey, subscriptionChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
|
|
37
|
+
import { LEDGER_FILE, appendSessionRecord, findResumeCandidate, readSessionRecords, resolveRetentionDays, toLedgerContract, } from "../lib/agent-ledger.js";
|
|
37
38
|
import { loadCliConfig } from "../lib/cli-config.js";
|
|
38
39
|
import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
|
|
39
40
|
import { experimentalNotice } from "../lib/experimental.js";
|
|
@@ -41,7 +42,7 @@ import { fmt } from "../lib/format.js";
|
|
|
41
42
|
import { askInput, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
42
43
|
import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
|
|
43
44
|
import { AgentIdentityMismatchError, defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
|
|
44
|
-
import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
|
|
45
|
+
import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, GATEWAY_HINT_HEADERS_ENV, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
|
|
45
46
|
import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
|
|
46
47
|
import { slackChannelArgs } from "../lib/slack-launch.js";
|
|
47
48
|
/** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
|
|
@@ -77,6 +78,9 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
77
78
|
const env = process.env;
|
|
78
79
|
let shape = resolveLaunchShape({ remoteControl: options.remoteControl, server: options.server }, env);
|
|
79
80
|
const shapeExplicit = launchPreferenceExplicit({ remoteControl: options.remoteControl, server: options.server }, env);
|
|
81
|
+
// How Claude Code authenticates: an Anthropic key (default) or the user's claude.ai
|
|
82
|
+
// subscription login (Team/Enterprise; lib/subscription-login.ts).
|
|
83
|
+
const auth = resolveAgentAuth(options.auth, env);
|
|
80
84
|
// ── governed identity ── (default: per-customer `agent-<handle>`, not a shared
|
|
81
85
|
// "code-agent" — see defaultAgentSlug; provisioning claims exactly this slug.
|
|
82
86
|
// `let` because a brand-new user who signs in during the pre-launch gate below
|
|
@@ -85,7 +89,6 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
85
89
|
if (!agent) {
|
|
86
90
|
throw new Error("mnemom agent: --agent sanitised to an empty string — pick a value with at least one letter or digit.");
|
|
87
91
|
}
|
|
88
|
-
const conversationId = resolveConversationId(options.conversationId);
|
|
89
92
|
// ── contract (built only if a contract flag was given; --goal then required) ──
|
|
90
93
|
const envelope = assembleContract({
|
|
91
94
|
goal: session.goal,
|
|
@@ -124,7 +127,12 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
124
127
|
context: options.context,
|
|
125
128
|
env,
|
|
126
129
|
});
|
|
127
|
-
|
|
130
|
+
let contextHandoff = resolveContextHandoff({ contextHandoff: options.contextHandoff, env });
|
|
131
|
+
if (contextHandoff && auth === "subscription") {
|
|
132
|
+
// The handoff tool authenticates with the Anthropic key a subscription launch does not have.
|
|
133
|
+
say("mnemom agent: context handoff is not available with subscription login yet — launching without it.");
|
|
134
|
+
contextHandoff = false;
|
|
135
|
+
}
|
|
128
136
|
const experimental = options.experimental ?? [];
|
|
129
137
|
const sessionConfig = buildSessionConfig({ profile: contextProfile, contextHandoff });
|
|
130
138
|
const sessionConfigB64 = buildSessionConfigB64({
|
|
@@ -164,7 +172,8 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
164
172
|
scenario,
|
|
165
173
|
shape: previewShape,
|
|
166
174
|
agent,
|
|
167
|
-
|
|
175
|
+
// The org is only known after sign-in, so a dry run never offers a resume (IG5 #1).
|
|
176
|
+
conversationId: resolveConversationId(options.conversationId),
|
|
168
177
|
door,
|
|
169
178
|
cellNote,
|
|
170
179
|
envelope,
|
|
@@ -175,6 +184,9 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
175
184
|
sessionConfig,
|
|
176
185
|
experimental,
|
|
177
186
|
});
|
|
187
|
+
if (auth === "subscription") {
|
|
188
|
+
say("mnemom agent: auth = subscription — Claude Code would use your claude.ai login (Team/Enterprise plans only) with a Mnemom API key; no Anthropic key, terminal-only.");
|
|
189
|
+
}
|
|
178
190
|
if (expNotice)
|
|
179
191
|
say(expNotice);
|
|
180
192
|
if (previewShape === "remote-control") {
|
|
@@ -193,10 +205,10 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
193
205
|
cli = await promptForCliPath(options.cli, env, cliError);
|
|
194
206
|
}
|
|
195
207
|
// ── resolve the Anthropic key (zero 1Password; never logged) ──
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
keySource: options.keySource
|
|
199
|
-
|
|
208
|
+
// Not on a subscription launch: Claude Code then sends its own claude.ai login instead.
|
|
209
|
+
const anthropicKey = auth === "api-key"
|
|
210
|
+
? await resolveAnthropicKey(env, { allowPrompt: true, keySource: options.keySource })
|
|
211
|
+
: undefined;
|
|
200
212
|
// ── account + µ-balance gate (no free tier) ─────────────────────────────────
|
|
201
213
|
// mnemom agent runs governed and on µ, so before we seal a contract or launch we
|
|
202
214
|
// (1) ensure a Mnemom session — offering the try-me-style sign-in / sign-up when
|
|
@@ -245,6 +257,14 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
245
257
|
rcCapable: rc.ok,
|
|
246
258
|
say,
|
|
247
259
|
});
|
|
260
|
+
// Subscription login: terminal-only for now, and the governed identity anchors on a
|
|
261
|
+
// Mnemom API key (minted once and stored if there is none). Resolved after sign-in so
|
|
262
|
+
// minting can use the session.
|
|
263
|
+
let mnemomKey;
|
|
264
|
+
if (auth === "subscription") {
|
|
265
|
+
shape = subscriptionShape(shape, shapeExplicit, say);
|
|
266
|
+
mnemomKey = await resolveSubscriptionMnemomKey(defaultMnemomKeyDeps());
|
|
267
|
+
}
|
|
248
268
|
// Now that a session exists, a defaulted slug uses the real login handle from
|
|
249
269
|
// /v1/auth/me (the stored token has no email, so the pre-sign-in value was the
|
|
250
270
|
// "agent-user" fallback). Only when the caller didn't name an agent. Under an
|
|
@@ -261,7 +281,12 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
261
281
|
// provisions-with-confirm when interactive; warns and continues otherwise.
|
|
262
282
|
// Skipped entirely by `--no-setup`. A provisioning failure never blocks the
|
|
263
283
|
// launch — it degrades to the unclaimed (fail-open) identity with a warning.
|
|
264
|
-
if (options.setup !== false) {
|
|
284
|
+
if (options.setup !== false && !anthropicKey) {
|
|
285
|
+
// Subscription login: the gateway creates the agent in the Mnemom key's org on the first
|
|
286
|
+
// turn, so there is nothing to birth or claim here.
|
|
287
|
+
say(`mnemom agent: subscription login — '${agent}' is created in your org on the first turn (identity: your Mnemom API key).`);
|
|
288
|
+
}
|
|
289
|
+
else if (options.setup !== false && anthropicKey) {
|
|
265
290
|
try {
|
|
266
291
|
await ensureGovernedAgent({
|
|
267
292
|
slug: agent,
|
|
@@ -280,6 +305,22 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
280
305
|
say(fmt.warn(`mnemom agent: agent setup did not complete (${msg}) — launching with the unclaimed identity.`));
|
|
281
306
|
}
|
|
282
307
|
}
|
|
308
|
+
// ── conversation id ── an implicit relaunch continues its earlier conversation (IG5 #1, D1)
|
|
309
|
+
// so the goal the gateway inferred there is still the one it steers by. Resolved here, after
|
|
310
|
+
// sign-in and the agent refresh, because a resume needs the org, agent and gateway host to
|
|
311
|
+
// match the earlier launch.
|
|
312
|
+
const conversationId = await resolveLaunchConversationId({
|
|
313
|
+
session: scenario,
|
|
314
|
+
orgId: account.orgId,
|
|
315
|
+
agent,
|
|
316
|
+
gatewayHost,
|
|
317
|
+
explicit: options.conversationId,
|
|
318
|
+
implicit: goalAlignmentMode(envelope, goalMode) === "implicit",
|
|
319
|
+
passthrough: options.passthrough ?? [],
|
|
320
|
+
interactive: isInteractive(),
|
|
321
|
+
records: () => readSessionRecords({ dir: MNEMOM_DIR }),
|
|
322
|
+
askYesNo: (q, d) => askYesNo(q, d),
|
|
323
|
+
});
|
|
283
324
|
// ── local session ledger (~/.mnemom/sessions.jsonl) ── one line per real
|
|
284
325
|
// launch so `mnemom agent sessions` / `show` can find this conversation later.
|
|
285
326
|
// Written after provisioning so a freshly claimed agent's id resolves. Holds
|
|
@@ -343,11 +384,13 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
343
384
|
say(fmt.warn(`mnemom agent: context handoff could not be registered (${err instanceof Error ? err.message : String(err)}) — launching without the handoff tool.`));
|
|
344
385
|
}
|
|
345
386
|
}
|
|
346
|
-
|
|
387
|
+
// A subscription launch (no Anthropic key) is always terminal — see subscriptionShape.
|
|
388
|
+
if (shape === "terminal" || !anthropicKey) {
|
|
347
389
|
launchTerminal({
|
|
348
390
|
cli,
|
|
349
391
|
door,
|
|
350
392
|
anthropicKey,
|
|
393
|
+
mnemomKey,
|
|
351
394
|
agent,
|
|
352
395
|
scenario,
|
|
353
396
|
conversationId,
|
|
@@ -382,6 +425,63 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
382
425
|
// ───────────────────────────────────────────────────────────────────────────
|
|
383
426
|
// Session ledger (thin driver over lib/agent-ledger.ts)
|
|
384
427
|
// ───────────────────────────────────────────────────────────────────────────
|
|
428
|
+
/** The Claude Code session id a passthrough `--resume <id>` / `-r <id>` / `--resume=<id>` names. */
|
|
429
|
+
export function passthroughResumeId(passthrough) {
|
|
430
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
431
|
+
const a = passthrough[i];
|
|
432
|
+
if (a === "--resume" || a === "-r") {
|
|
433
|
+
const next = passthrough[i + 1];
|
|
434
|
+
return next !== undefined && !next.startsWith("-") && next.trim() !== "" ? next.trim() : null;
|
|
435
|
+
}
|
|
436
|
+
if (a.startsWith("--resume="))
|
|
437
|
+
return a.slice("--resume=".length).trim() || null;
|
|
438
|
+
}
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* The conversation id this launch runs under (IG5 #1, decision D1). The gateway keys the
|
|
443
|
+
* implicit goal by conversation id, so an implicit relaunch reuses the earlier one:
|
|
444
|
+
* - `--conversation-id` → that id, no lookup;
|
|
445
|
+
* - not implicit (a sealed contract, or alignment off) → a new id;
|
|
446
|
+
* - only an earlier implicit launch with the same session name, org, agent and gateway host
|
|
447
|
+
* is continued; any mismatch → a new id, no prompt;
|
|
448
|
+
* - `-- --resume <cc-session-id>` → the matching row that pinned that Claude Code session
|
|
449
|
+
* (else the newest matching row), reused without asking;
|
|
450
|
+
* - no `--resume` → "Resume previous session <name>? [Y/n]", default Y; Y reuses that row's
|
|
451
|
+
* conversation, n mints a new one. `--resume` is NOT added to Claude Code (the user often
|
|
452
|
+
* resumes inside it with /resume). No TTY: Y.
|
|
453
|
+
* The ledger read is best-effort: a failure means a new id.
|
|
454
|
+
*/
|
|
455
|
+
export async function resolveLaunchConversationId(input) {
|
|
456
|
+
if ((input.explicit ?? "").trim())
|
|
457
|
+
return resolveConversationId(input.explicit);
|
|
458
|
+
if (!input.implicit)
|
|
459
|
+
return resolveConversationId(undefined);
|
|
460
|
+
let records;
|
|
461
|
+
try {
|
|
462
|
+
records = input.records();
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
return resolveConversationId(undefined);
|
|
466
|
+
}
|
|
467
|
+
const found = findResumeCandidate(records, {
|
|
468
|
+
session: input.session,
|
|
469
|
+
orgId: input.orgId,
|
|
470
|
+
agent: input.agent,
|
|
471
|
+
gatewayHost: input.gatewayHost,
|
|
472
|
+
resumeId: passthroughResumeId(input.passthrough),
|
|
473
|
+
});
|
|
474
|
+
if (!found)
|
|
475
|
+
return resolveConversationId(undefined);
|
|
476
|
+
const conv = found.record.conversation_id;
|
|
477
|
+
if (found.source === "session_name" && input.interactive) {
|
|
478
|
+
const yes = await input.askYesNo(`Resume previous session ${input.session}?`, true);
|
|
479
|
+
if (!yes)
|
|
480
|
+
return resolveConversationId(undefined);
|
|
481
|
+
}
|
|
482
|
+
say(`mnemom agent: continuing conversation ${conv} from the earlier '${found.record.session}' launch (${found.record.started_at}), so its goal carries over.`);
|
|
483
|
+
return resolveConversationId(conv);
|
|
484
|
+
}
|
|
385
485
|
/**
|
|
386
486
|
* Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
|
|
387
487
|
* resolved best-effort (a lookup failure or an unknown slug records null); a
|
|
@@ -425,6 +525,25 @@ export async function recordSessionLaunch(ctx) {
|
|
|
425
525
|
// ───────────────────────────────────────────────────────────────────────────
|
|
426
526
|
// Credential resolution (zero 1Password)
|
|
427
527
|
// ───────────────────────────────────────────────────────────────────────────
|
|
528
|
+
/** The real I/O behind a subscription launch's Mnemom key (lib/subscription-login.ts). */
|
|
529
|
+
export function defaultMnemomKeyDeps(storePath = CODE_STORE) {
|
|
530
|
+
return {
|
|
531
|
+
env: process.env,
|
|
532
|
+
readStored: () => {
|
|
533
|
+
try {
|
|
534
|
+
const data = JSON.parse(readFileSync(storePath, "utf8"));
|
|
535
|
+
const v = data[MNEMOM_KEY_STORE_FIELD];
|
|
536
|
+
return typeof v === "string" ? stripCrlf(v) : undefined;
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
return undefined;
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
mint: async () => (await createApiKey(`mnemom-agent ${hostname()}`, ["gateway"])).key,
|
|
543
|
+
store: (key) => mergeStore({ [MNEMOM_KEY_STORE_FIELD]: key }, storePath),
|
|
544
|
+
say,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
428
547
|
/**
|
|
429
548
|
* Resolve the Anthropic key with NO 1Password dependency (built for external
|
|
430
549
|
* customer adoption), in order:
|
|
@@ -660,6 +779,7 @@ function launchTerminal(ctx) {
|
|
|
660
779
|
warnAdapter(ctx.cli);
|
|
661
780
|
const headers = buildCustomHeaders({
|
|
662
781
|
anthropicKey: ctx.anthropicKey,
|
|
782
|
+
mnemomKey: ctx.mnemomKey,
|
|
663
783
|
agent: ctx.agent,
|
|
664
784
|
conversationId: ctx.conversationId,
|
|
665
785
|
contractB64: ctx.envelope?.base64,
|
|
@@ -675,13 +795,23 @@ function launchTerminal(ctx) {
|
|
|
675
795
|
// Claude Code logged in via claude.ai (OAuth) PREEMPTS ANTHROPIC_API_KEY, so
|
|
676
796
|
// it is force-unset; the real key rides in ANTHROPIC_CUSTOM_HEADERS and
|
|
677
797
|
// ANTHROPIC_AUTH_TOKEN is a fixed non-secret placeholder.
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
childEnv
|
|
682
|
-
|
|
798
|
+
//
|
|
799
|
+
// A subscription launch (no Anthropic key) sets NO auth token: Claude Code then sends its
|
|
800
|
+
// own claude.ai login to the gateway (lib/subscription-login.ts).
|
|
801
|
+
let childEnv;
|
|
802
|
+
if (ctx.anthropicKey) {
|
|
803
|
+
childEnv = { ...process.env };
|
|
804
|
+
delete childEnv.ANTHROPIC_API_KEY;
|
|
805
|
+
childEnv.ANTHROPIC_BASE_URL = ctx.door;
|
|
806
|
+
childEnv.ANTHROPIC_AUTH_TOKEN = "mnemom-gateway";
|
|
807
|
+
childEnv.ANTHROPIC_CUSTOM_HEADERS = headers;
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
childEnv = subscriptionChildEnv(process.env, ctx.door, headers);
|
|
811
|
+
}
|
|
683
812
|
const sessionName = passthroughName(ctx.options.passthrough ?? []) ?? sessionDisplayName(ctx.agent, ctx.scenario);
|
|
684
813
|
childEnv[SESSION_NAME_ENV] = sessionName;
|
|
814
|
+
childEnv[GATEWAY_HINT_HEADERS_ENV] = "1";
|
|
685
815
|
if (ctx.options.slack)
|
|
686
816
|
args.push(...slackChannelArgs(sessionName));
|
|
687
817
|
if (ctx.contextHint) {
|
package/dist/index.js
CHANGED
|
@@ -142,6 +142,7 @@ const agentCmd = program
|
|
|
142
142
|
.option("--gateway <url>", "Gateway host override — must be the same cell as the rest of the CLI (default: that cell's gateway; or MNEMOM_AGENT_GATEWAY)")
|
|
143
143
|
.option("--remote-control", "Interactive Remote Control session in THIS terminal (behind a local proxy)")
|
|
144
144
|
.option("--server", "Headless `claude remote-control` dispatcher (behind a local proxy)")
|
|
145
|
+
.option("--auth <mode>", "How Claude Code authenticates: api-key (default) | subscription (your Claude Team/Enterprise login; terminal-only)")
|
|
145
146
|
.option("--model <alias|id>", "Model to launch (CLI alias or concrete id)")
|
|
146
147
|
.addOption(new Option("--effort <level>", "Reasoning effort").choices([
|
|
147
148
|
"low",
|
|
@@ -278,6 +279,7 @@ const agentCmd = program
|
|
|
278
279
|
conversationId: opts.conversationId,
|
|
279
280
|
cli: opts.cli,
|
|
280
281
|
remoteControl: opts.remoteControl,
|
|
282
|
+
auth: opts.auth,
|
|
281
283
|
server: opts.server,
|
|
282
284
|
model: opts.model ?? cfg.model,
|
|
283
285
|
effort: opts.effort ?? cfg.effort,
|
|
@@ -133,3 +133,30 @@ export declare function appendSessionRecord(input: SessionRecordInput, deps: Led
|
|
|
133
133
|
export declare function readSessionRecords(deps: Pick<LedgerDeps, "dir">): SessionRecord[];
|
|
134
134
|
/** One ledger line → a record, or null when it is not a v1 record object. */
|
|
135
135
|
export declare function parseRecord(line: string): SessionRecord | null;
|
|
136
|
+
/**
|
|
137
|
+
* Which earlier launch a new one continues (IG5 #1, decision D1). The gateway keys the implicit
|
|
138
|
+
* goal by conversation id, so a relaunch that mints a fresh id starts goal-less. A row
|
|
139
|
+
* qualifies only when it is an implicit-mode launch (a sealed contract is sealed on its own
|
|
140
|
+
* conversation and is never carried) with the same session name, org, agent and gateway host as
|
|
141
|
+
* this launch; an empty org never matches. `records` is file order (oldest first); the newest
|
|
142
|
+
* qualifying row wins.
|
|
143
|
+
* - `resumeId` (the `--resume <cc-session-id>` passed through to Claude Code) → the qualifying
|
|
144
|
+
* row that pinned that Claude Code session, else the newest qualifying row; `source:
|
|
145
|
+
* "resume_flag"` — reused without asking.
|
|
146
|
+
* - no `resumeId` → the newest qualifying row; `source: "session_name"` — the launcher asks
|
|
147
|
+
* first.
|
|
148
|
+
* - nothing qualifies → null (a new conversation id).
|
|
149
|
+
*/
|
|
150
|
+
export interface ResumeCandidate {
|
|
151
|
+
record: SessionRecord;
|
|
152
|
+
source: "resume_flag" | "session_name";
|
|
153
|
+
/** True when the row was found by its Claude Code session id (not by the name fallback). */
|
|
154
|
+
byCcSession: boolean;
|
|
155
|
+
}
|
|
156
|
+
export declare function findResumeCandidate(records: readonly SessionRecord[], want: {
|
|
157
|
+
session: string;
|
|
158
|
+
orgId: string;
|
|
159
|
+
agent: string;
|
|
160
|
+
gatewayHost: string;
|
|
161
|
+
resumeId?: string | null;
|
|
162
|
+
}): ResumeCandidate | null;
|
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). */
|
|
@@ -283,7 +292,10 @@ export declare function resolveRemoteControlShape(args: {
|
|
|
283
292
|
* callers must place it only in the child's env, never in a log line.
|
|
284
293
|
*/
|
|
285
294
|
export declare function buildCustomHeaders(opts: {
|
|
286
|
-
|
|
295
|
+
/** Absent on a subscription-login launch: Claude Code then sends its own claude.ai login. */
|
|
296
|
+
anthropicKey?: string;
|
|
297
|
+
/** The governed-identity anchor on a subscription-login launch (lib/subscription-login.ts). */
|
|
298
|
+
mnemomKey?: string;
|
|
287
299
|
agent: string;
|
|
288
300
|
conversationId: string;
|
|
289
301
|
contractB64?: string;
|
package/dist/lib/agent.js
CHANGED
|
@@ -117,6 +117,15 @@ export function sanitizeAgentName(raw) {
|
|
|
117
117
|
* post`) can identify themselves by the same name the claude.ai session list shows.
|
|
118
118
|
*/
|
|
119
119
|
export const SESSION_NAME_ENV = "MNEMOM_AGENT_SESSION_NAME";
|
|
120
|
+
/**
|
|
121
|
+
* Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class`,
|
|
122
|
+
* `x-claude-code-agent-type`, `x-claude-code-prompt-id`). CC sends them to api.anthropic.com on
|
|
123
|
+
* its own, so a Remote Control launch (rc-proxy in front of api.anthropic.com) already carries
|
|
124
|
+
* them; with `ANTHROPIC_BASE_URL` pointing at the gateway door (the terminal shape) CC sends them
|
|
125
|
+
* only when this is "1". The gateway reads them to keep away summaries, titles, compaction and
|
|
126
|
+
* teammates off the main implicit goal.
|
|
127
|
+
*/
|
|
128
|
+
export const GATEWAY_HINT_HEADERS_ENV = "CLAUDE_CODE_GATEWAY_HINT_HEADERS";
|
|
120
129
|
/** The session display name: "<agent>-<scenario>" (sanitised), else the agent slug. */
|
|
121
130
|
export function sessionDisplayName(agent, scenario) {
|
|
122
131
|
return sanitizeAgentName(`${agent}-${scenario}`) || agent;
|
|
@@ -466,7 +475,8 @@ export function resolveRemoteControlShape(args) {
|
|
|
466
475
|
*/
|
|
467
476
|
export function buildCustomHeaders(opts) {
|
|
468
477
|
const lines = [
|
|
469
|
-
`x-api-key: ${opts.anthropicKey}
|
|
478
|
+
...(opts.anthropicKey ? [`x-api-key: ${opts.anthropicKey}`] : []),
|
|
479
|
+
...(opts.mnemomKey ? [`x-mnemom-api-key: ${opts.mnemomKey}`] : []),
|
|
470
480
|
`x-mnemom-agent: ${opts.agent}`,
|
|
471
481
|
`x-mnemom-conversation-id: ${opts.conversationId}`,
|
|
472
482
|
];
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* resolvers in lib/code.ts keep the built-in default as the final fallback);
|
|
16
16
|
* the caller folds the plain-value settings under the CLI flags.
|
|
17
17
|
*/
|
|
18
|
+
import { type AgentAuth } from "./subscription-login.js";
|
|
18
19
|
import type { ExperimentalId } from "./experimental.js";
|
|
19
20
|
import { type ContextProfile, type GoalModeDefault, type LaunchShape } from "./agent.js";
|
|
20
21
|
/** The human-editable settings file. Secret key lives elsewhere (launcher.json). */
|
|
@@ -29,6 +30,8 @@ export interface LauncherConfig {
|
|
|
29
30
|
effort?: string;
|
|
30
31
|
cli?: string;
|
|
31
32
|
launch?: LaunchShape;
|
|
33
|
+
/** How Claude Code authenticates (lib/subscription-login.ts); api-key when unset. */
|
|
34
|
+
auth?: AgentAuth;
|
|
32
35
|
keySource?: KeySource;
|
|
33
36
|
/** Standing goal-alignment default when neither --goal nor --implicit is given. */
|
|
34
37
|
goalMode?: GoalModeDefault;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* resolvers in lib/code.ts keep the built-in default as the final fallback);
|
|
16
16
|
* the caller folds the plain-value settings under the CLI flags.
|
|
17
17
|
*/
|
|
18
|
+
import { AGENT_AUTH_ENV, AGENT_AUTHS, coerceAgentAuth, } from "./subscription-login.js";
|
|
18
19
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
19
20
|
import { dirname, join } from "node:path";
|
|
20
21
|
import { parse as parseToml, stringify as stringifyToml, TomlError } from "smol-toml";
|
|
@@ -132,6 +133,14 @@ export const CONFIG_KEYS = [
|
|
|
132
133
|
c.launch = coerceLaunch(v);
|
|
133
134
|
},
|
|
134
135
|
},
|
|
136
|
+
{
|
|
137
|
+
key: "auth",
|
|
138
|
+
describe: `How Claude Code authenticates (${AGENT_AUTHS.join("|")}); subscription = your Claude Team/Enterprise login, terminal-only`,
|
|
139
|
+
get: (c) => c.auth,
|
|
140
|
+
set: (c, v) => {
|
|
141
|
+
c.auth = coerceAgentAuth(v);
|
|
142
|
+
},
|
|
143
|
+
},
|
|
135
144
|
{
|
|
136
145
|
key: "key_source",
|
|
137
146
|
describe: `Where to source the Anthropic key (${KEY_SOURCES.join("|")})`,
|
|
@@ -258,6 +267,8 @@ export function parseLauncherConfig(text) {
|
|
|
258
267
|
cfg.cli = coerceNonEmpty("cli", str(raw.cli) ?? "");
|
|
259
268
|
if (raw.launch !== undefined)
|
|
260
269
|
cfg.launch = coerceLaunch(str(raw.launch) ?? "");
|
|
270
|
+
if (raw.auth !== undefined)
|
|
271
|
+
cfg.auth = coerceAgentAuth(str(raw.auth) ?? "");
|
|
261
272
|
if (raw.key_source !== undefined)
|
|
262
273
|
cfg.keySource = coerceKeySource(str(raw.key_source) ?? "");
|
|
263
274
|
if (raw.goal_mode !== undefined)
|
|
@@ -330,6 +341,8 @@ export function serializeLauncherConfig(cfg) {
|
|
|
330
341
|
doc.cli = cfg.cli;
|
|
331
342
|
if (cfg.launch !== undefined)
|
|
332
343
|
doc.launch = cfg.launch;
|
|
344
|
+
if (cfg.auth !== undefined)
|
|
345
|
+
doc.auth = cfg.auth;
|
|
333
346
|
if (cfg.keySource !== undefined)
|
|
334
347
|
doc.key_source = cfg.keySource;
|
|
335
348
|
if (cfg.goalMode !== undefined)
|
|
@@ -392,6 +405,9 @@ export function applyConfigToEnv(cfg, env = process.env) {
|
|
|
392
405
|
env.MNEMOM_AGENT_REMOTE_CONTROL =
|
|
393
406
|
cfg.launch === "server" ? "server" : cfg.launch === "remote-control" ? "1" : "0";
|
|
394
407
|
}
|
|
408
|
+
if (cfg.auth && !(env[AGENT_AUTH_ENV] ?? "").trim()) {
|
|
409
|
+
env[AGENT_AUTH_ENV] = cfg.auth;
|
|
410
|
+
}
|
|
395
411
|
if (cfg.context !== undefined && !(env.MNEMOM_AGENT_CONTEXT ?? "").trim()) {
|
|
396
412
|
env.MNEMOM_AGENT_CONTEXT = cfg.context ? "1" : "0";
|
|
397
413
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom agent` with a Claude subscription login (`auth = subscription`) — Team and
|
|
3
|
+
* Enterprise plans only; the gateway checks the plan with Anthropic and refuses anything
|
|
4
|
+
* else (gateway/src/subscription-auth.ts).
|
|
5
|
+
*
|
|
6
|
+
* Claude Code then authenticates to the gateway with its own claude.ai OAuth login: the
|
|
7
|
+
* launch sets ANTHROPIC_BASE_URL and leaves ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN unset,
|
|
8
|
+
* so Claude Code sends `Authorization: Bearer <its login>`. No Anthropic key is involved.
|
|
9
|
+
* The gateway anchors the governed identity on a Mnemom API key instead
|
|
10
|
+
* (`x-mnemom-api-key`), which this module resolves — env, then the 0600 store, then a
|
|
11
|
+
* freshly minted gateway-only key — and the agent is created in that key's org on the
|
|
12
|
+
* first turn, so there is no CLI-side birth or claim.
|
|
13
|
+
*
|
|
14
|
+
* v1 is terminal-only: the Remote Control proxy rewrites the OAuth bearer away by design,
|
|
15
|
+
* and the context-handoff tool authenticates with an Anthropic key.
|
|
16
|
+
*
|
|
17
|
+
* Pure apart from the injected deps, so every arm is unit-tested.
|
|
18
|
+
*/
|
|
19
|
+
import type { LaunchShape } from "./agent.js";
|
|
20
|
+
export type AgentAuth = "api-key" | "subscription";
|
|
21
|
+
export declare const AGENT_AUTHS: readonly AgentAuth[];
|
|
22
|
+
/** Env override for the setting (the launcher.toml `auth` key folds into it). */
|
|
23
|
+
export declare const AGENT_AUTH_ENV = "MNEMOM_AGENT_AUTH";
|
|
24
|
+
/** Env override for the Mnemom key a subscription launch sends. */
|
|
25
|
+
export declare const MNEMOM_KEY_ENV = "MNEMOM_AGENT_MNEMOM_KEY";
|
|
26
|
+
/** Field in ~/.mnemom/launcher.json (0600) holding the minted key. */
|
|
27
|
+
export declare const MNEMOM_KEY_STORE_FIELD = "mnemom_api_key";
|
|
28
|
+
export declare function coerceAgentAuth(raw: string): AgentAuth;
|
|
29
|
+
/** `--auth` flag > MNEMOM_AGENT_AUTH (launcher.toml folds in here) > api-key. */
|
|
30
|
+
export declare function resolveAgentAuth(flag: string | undefined, env: Record<string, string | undefined>): AgentAuth;
|
|
31
|
+
export interface MnemomKeyDeps {
|
|
32
|
+
env: Record<string, string | undefined>;
|
|
33
|
+
readStored(): string | undefined;
|
|
34
|
+
/** Mint a gateway-only personal key for this machine; returns the secret. */
|
|
35
|
+
mint(): Promise<string>;
|
|
36
|
+
store(key: string): void;
|
|
37
|
+
say(line: string): void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The Mnemom key a subscription launch sends: MNEMOM_AGENT_MNEMOM_KEY, else the stored
|
|
41
|
+
* key, else mint one (gateway scope only) and store it. The key is never printed.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveSubscriptionMnemomKey(deps: MnemomKeyDeps): Promise<string>;
|
|
44
|
+
/**
|
|
45
|
+
* Subscription login is terminal-only in v1. An explicit Remote Control / server request
|
|
46
|
+
* is refused; an automatic upgrade to Remote Control falls back to terminal with a notice.
|
|
47
|
+
*/
|
|
48
|
+
export declare function subscriptionShape(shape: LaunchShape, shapeExplicit: boolean, say: (line: string) => void): LaunchShape;
|
|
49
|
+
/**
|
|
50
|
+
* The child env for a subscription launch: the gateway door, the Mnemom headers, and
|
|
51
|
+
* NOTHING that would make Claude Code send a key instead of its claude.ai login.
|
|
52
|
+
*/
|
|
53
|
+
export declare function subscriptionChildEnv(base: NodeJS.ProcessEnv, door: string, headers: string): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom agent` with a Claude subscription login (`auth = subscription`) — Team and
|
|
3
|
+
* Enterprise plans only; the gateway checks the plan with Anthropic and refuses anything
|
|
4
|
+
* else (gateway/src/subscription-auth.ts).
|
|
5
|
+
*
|
|
6
|
+
* Claude Code then authenticates to the gateway with its own claude.ai OAuth login: the
|
|
7
|
+
* launch sets ANTHROPIC_BASE_URL and leaves ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN unset,
|
|
8
|
+
* so Claude Code sends `Authorization: Bearer <its login>`. No Anthropic key is involved.
|
|
9
|
+
* The gateway anchors the governed identity on a Mnemom API key instead
|
|
10
|
+
* (`x-mnemom-api-key`), which this module resolves — env, then the 0600 store, then a
|
|
11
|
+
* freshly minted gateway-only key — and the agent is created in that key's org on the
|
|
12
|
+
* first turn, so there is no CLI-side birth or claim.
|
|
13
|
+
*
|
|
14
|
+
* v1 is terminal-only: the Remote Control proxy rewrites the OAuth bearer away by design,
|
|
15
|
+
* and the context-handoff tool authenticates with an Anthropic key.
|
|
16
|
+
*
|
|
17
|
+
* Pure apart from the injected deps, so every arm is unit-tested.
|
|
18
|
+
*/
|
|
19
|
+
export const AGENT_AUTHS = ["api-key", "subscription"];
|
|
20
|
+
/** Env override for the setting (the launcher.toml `auth` key folds into it). */
|
|
21
|
+
export const AGENT_AUTH_ENV = "MNEMOM_AGENT_AUTH";
|
|
22
|
+
/** Env override for the Mnemom key a subscription launch sends. */
|
|
23
|
+
export const MNEMOM_KEY_ENV = "MNEMOM_AGENT_MNEMOM_KEY";
|
|
24
|
+
/** Field in ~/.mnemom/launcher.json (0600) holding the minted key. */
|
|
25
|
+
export const MNEMOM_KEY_STORE_FIELD = "mnemom_api_key";
|
|
26
|
+
/**
|
|
27
|
+
* The key is spliced into the newline-joined ANTHROPIC_CUSTOM_HEADERS block, so an embedded
|
|
28
|
+
* CR/LF (a multi-line paste, a bad env value) would inject extra header lines.
|
|
29
|
+
*/
|
|
30
|
+
function headerSafe(raw) {
|
|
31
|
+
return raw.replace(/[\r\n]+/g, "").trim();
|
|
32
|
+
}
|
|
33
|
+
export function coerceAgentAuth(raw) {
|
|
34
|
+
const v = raw.trim().toLowerCase();
|
|
35
|
+
if (AGENT_AUTHS.includes(v))
|
|
36
|
+
return v;
|
|
37
|
+
throw new Error(`auth must be one of ${AGENT_AUTHS.join(", ")} (got '${raw}').`);
|
|
38
|
+
}
|
|
39
|
+
/** `--auth` flag > MNEMOM_AGENT_AUTH (launcher.toml folds in here) > api-key. */
|
|
40
|
+
export function resolveAgentAuth(flag, env) {
|
|
41
|
+
if (flag !== undefined && flag.trim())
|
|
42
|
+
return coerceAgentAuth(flag);
|
|
43
|
+
const fromEnv = (env[AGENT_AUTH_ENV] ?? "").trim();
|
|
44
|
+
return fromEnv ? coerceAgentAuth(fromEnv) : "api-key";
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The Mnemom key a subscription launch sends: MNEMOM_AGENT_MNEMOM_KEY, else the stored
|
|
48
|
+
* key, else mint one (gateway scope only) and store it. The key is never printed.
|
|
49
|
+
*/
|
|
50
|
+
export async function resolveSubscriptionMnemomKey(deps) {
|
|
51
|
+
const fromEnv = headerSafe(deps.env[MNEMOM_KEY_ENV] ?? "");
|
|
52
|
+
if (fromEnv)
|
|
53
|
+
return fromEnv;
|
|
54
|
+
const stored = headerSafe(deps.readStored() ?? "");
|
|
55
|
+
if (stored)
|
|
56
|
+
return stored;
|
|
57
|
+
let minted;
|
|
58
|
+
try {
|
|
59
|
+
minted = headerSafe(await deps.mint());
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
63
|
+
throw new Error(`mnemom agent: subscription login needs a Mnemom API key and minting one failed (${msg}).\n` +
|
|
64
|
+
` Create one with \`mnemom api-key create --name mnemom-agent --scopes gateway\` and set ${MNEMOM_KEY_ENV}.`, { cause: err });
|
|
65
|
+
}
|
|
66
|
+
if (!minted)
|
|
67
|
+
throw new Error("mnemom agent: the API returned an empty Mnemom key.");
|
|
68
|
+
try {
|
|
69
|
+
deps.store(minted);
|
|
70
|
+
deps.say("mnemom agent: minted a gateway-only Mnemom API key for subscription login (saved to ~/.mnemom/launcher.json, 0600).");
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
deps.say("mnemom agent: minted a gateway-only Mnemom API key for this launch but could not save it — the next launch mints another.");
|
|
74
|
+
}
|
|
75
|
+
return minted;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Subscription login is terminal-only in v1. An explicit Remote Control / server request
|
|
79
|
+
* is refused; an automatic upgrade to Remote Control falls back to terminal with a notice.
|
|
80
|
+
*/
|
|
81
|
+
export function subscriptionShape(shape, shapeExplicit, say) {
|
|
82
|
+
if (shape === "terminal")
|
|
83
|
+
return shape;
|
|
84
|
+
if (shapeExplicit) {
|
|
85
|
+
throw new Error(`mnemom agent: subscription login runs terminal-only for now — drop --${shape === "server" ? "server" : "remote-control"} (or \`mnemom agent config set launch terminal\`), or use auth = api-key.`);
|
|
86
|
+
}
|
|
87
|
+
say("mnemom agent: subscription login runs terminal-only for now — launching in this terminal.");
|
|
88
|
+
return "terminal";
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The child env for a subscription launch: the gateway door, the Mnemom headers, and
|
|
92
|
+
* NOTHING that would make Claude Code send a key instead of its claude.ai login.
|
|
93
|
+
*/
|
|
94
|
+
export function subscriptionChildEnv(base, door, headers) {
|
|
95
|
+
const env = { ...base };
|
|
96
|
+
delete env.ANTHROPIC_API_KEY;
|
|
97
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
98
|
+
// The key already rides in the headers; the child process has no other use for it.
|
|
99
|
+
delete env.MNEMOM_AGENT_MNEMOM_KEY;
|
|
100
|
+
env.ANTHROPIC_BASE_URL = door;
|
|
101
|
+
env.ANTHROPIC_CUSTOM_HEADERS = headers;
|
|
102
|
+
return env;
|
|
103
|
+
}
|