@mnemom/mnemom 0.17.3-next.6 → 0.17.4-next.0
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 +31 -9
- package/dist/commands/agent-sessions.d.ts +5 -0
- package/dist/commands/agent-sessions.js +7 -1
- package/dist/commands/agent-setup.js +9 -2
- package/dist/commands/agent.d.ts +5 -0
- package/dist/commands/agent.js +75 -16
- package/dist/index.js +5 -3
- package/dist/lib/agent.d.ts +25 -6
- package/dist/lib/agent.js +44 -6
- package/dist/lib/launcher-config.d.ts +3 -0
- package/dist/lib/launcher-config.js +17 -1
- package/dist/lib/slack-router.js +3 -1
- package/dist/lib/subscription-login.d.ts +53 -0
- package/dist/lib/subscription-login.js +103 -0
- package/package.json +1 -1
|
@@ -14,7 +14,9 @@
|
|
|
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 {
|
|
17
|
+
import { resolveAgentAuth } from "../lib/subscription-login.js";
|
|
18
|
+
import { isProdGateway, resolveAnthropicDoor, resolveGatewayHost, singleCellError, } from "../lib/agent.js";
|
|
19
|
+
import { getApiUrl, getGatewayUrl } from "../lib/config.js";
|
|
18
20
|
import { LAUNCHER_CONFIG_PATH } from "../lib/launcher-config.js";
|
|
19
21
|
import { getCodeEntitlement, MnemomApiError } from "../lib/api.js";
|
|
20
22
|
import { isLoggedIn } from "../lib/auth.js";
|
|
@@ -44,14 +46,34 @@ export async function agentDoctorCommand(options = {}) {
|
|
|
44
46
|
catch (err) {
|
|
45
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>`");
|
|
46
48
|
}
|
|
47
|
-
// ── Anthropic key ──
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
}
|
|
69
|
+
// ── one cell: the launch door must be the CLI's own cell ──
|
|
70
|
+
const cellError = singleCellError({
|
|
71
|
+
door: resolveAnthropicDoor(env),
|
|
72
|
+
apiUrl: getApiUrl(),
|
|
73
|
+
cellGateway: getGatewayUrl(),
|
|
74
|
+
});
|
|
75
|
+
if (cellError)
|
|
76
|
+
add("miss", "cell: mixed — a launch would be refused", cellError);
|
|
55
77
|
// ── gateway health ──
|
|
56
78
|
const host = resolveGatewayHost(env);
|
|
57
79
|
const cell = isProdGateway(env) ? "us-2/prod" : "custom";
|
|
@@ -26,6 +26,11 @@ export interface SessionCommandDeps {
|
|
|
26
26
|
* one `live goal` line an implicit session gets. Optional: absent ⇒ no line.
|
|
27
27
|
*/
|
|
28
28
|
fetchGoal?: (record: SessionRecord) => Promise<GoalShowResponse>;
|
|
29
|
+
/**
|
|
30
|
+
* The gateway of the cell this CLI reads live state from. A session that ran on another
|
|
31
|
+
* cell is not looked up there (it would always read as "no sealed contract"). Absent ⇒ no check.
|
|
32
|
+
*/
|
|
33
|
+
cellGateway?: () => string;
|
|
29
34
|
out: (line: string) => void;
|
|
30
35
|
err: (line: string) => void;
|
|
31
36
|
}
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* only the server's hash is shown, labelled "sealed hash".
|
|
15
15
|
*/
|
|
16
16
|
import { readSessionRecords } from "../lib/agent-ledger.js";
|
|
17
|
-
import { MNEMOM_DIR } from "../lib/config.js";
|
|
17
|
+
import { getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
|
|
18
|
+
import { sameOrigin } from "../lib/agent.js";
|
|
18
19
|
import { getAgentSession, MnemomApiError } from "../lib/api.js";
|
|
19
20
|
import { fmt, formatRelativeAge } from "../lib/format.js";
|
|
20
21
|
import { goalControlFetch } from "../lib/goal-api.js";
|
|
@@ -31,6 +32,7 @@ export const liveSessionDeps = {
|
|
|
31
32
|
const creds = await resolveLaunchCreds();
|
|
32
33
|
return goalControlFetch(record, "show", { thread: "main" }, { fetch, creds });
|
|
33
34
|
},
|
|
35
|
+
cellGateway: () => getGatewayUrl(),
|
|
34
36
|
out: (line) => console.log(line),
|
|
35
37
|
err: (line) => console.error(line),
|
|
36
38
|
};
|
|
@@ -318,9 +320,13 @@ export async function agentShowCommand(ref, opts, deps) {
|
|
|
318
320
|
const { record, others } = resolved;
|
|
319
321
|
let remote = null;
|
|
320
322
|
let reason;
|
|
323
|
+
const cell = deps.cellGateway?.();
|
|
321
324
|
if (record.agent_id === null) {
|
|
322
325
|
reason = "agent id was not resolved at launch";
|
|
323
326
|
}
|
|
327
|
+
else if (cell !== undefined && !sameOrigin(record.gateway_host, cell)) {
|
|
328
|
+
reason = `this session ran on ${record.gateway_host}, not this CLI's cell (${cell}) — point the CLI at that cell to read it`;
|
|
329
|
+
}
|
|
324
330
|
else {
|
|
325
331
|
try {
|
|
326
332
|
remote = await deps.fetchSession(record.conversation_id, record.agent_id, record.org_id);
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
import { claimAgent, getAgentByName, getMe, getMuBalance, getMyPersonalOrg, MnemomApiError, mintProtectionCapability, putAlignmentCard, putProtectionCard, } from "../lib/api.js";
|
|
27
27
|
import { getAuthInfo, isLoggedIn, loginWithBrowser, loginWithDeviceFlow, resolveAuth, } from "../lib/auth.js";
|
|
28
28
|
import { getActiveOrg } from "../lib/cli-config.js";
|
|
29
|
-
import { getWebsiteUrl } from "../lib/config.js";
|
|
29
|
+
import { getApiUrl, getGatewayUrl, getWebsiteUrl } from "../lib/config.js";
|
|
30
30
|
import { fmt } from "../lib/format.js";
|
|
31
31
|
import { askYesNo, isInteractive } from "../lib/prompt.js";
|
|
32
|
-
import { resolveGatewayHost, sanitizeAgentName } from "../lib/agent.js";
|
|
32
|
+
import { resolveAnthropicDoor, resolveGatewayHost, sanitizeAgentName, singleCellError, } from "../lib/agent.js";
|
|
33
33
|
import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
|
|
34
34
|
import { deriveHashProof } from "./agents.js";
|
|
35
35
|
import { birthThroughGateway } from "./wrap.js";
|
|
@@ -273,6 +273,13 @@ async function publishDefaultCards(agentId) {
|
|
|
273
273
|
/** `mnemom agent setup [--agent <slug>]` — the explicit provisioning entry point. */
|
|
274
274
|
export async function agentSetupCommand(opts = {}) {
|
|
275
275
|
const gatewayHost = resolveGatewayHost(process.env);
|
|
276
|
+
const cellError = singleCellError({
|
|
277
|
+
door: resolveAnthropicDoor(process.env),
|
|
278
|
+
apiUrl: getApiUrl(),
|
|
279
|
+
cellGateway: getGatewayUrl(),
|
|
280
|
+
});
|
|
281
|
+
if (cellError)
|
|
282
|
+
throw new Error(cellError.replace("mnemom agent:", "mnemom agent setup:"));
|
|
276
283
|
// Sign in (offer sign-up) and enforce the MU balance before any account write.
|
|
277
284
|
try {
|
|
278
285
|
await ensureSessionAndBalance({});
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
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";
|
|
28
29
|
import { type ExperimentalId } from "../lib/experimental.js";
|
|
29
30
|
import { type ContextProfile, type LaunchShape, type SealedContractEnvelope } from "../lib/agent.js";
|
|
30
31
|
export interface AgentOptions {
|
|
@@ -48,6 +49,8 @@ export interface AgentOptions {
|
|
|
48
49
|
cli?: string;
|
|
49
50
|
remoteControl?: boolean;
|
|
50
51
|
server?: boolean;
|
|
52
|
+
/** `--auth api-key|subscription` (lib/subscription-login.ts); else MNEMOM_AGENT_AUTH / config. */
|
|
53
|
+
auth?: string;
|
|
51
54
|
model?: string;
|
|
52
55
|
effort?: string;
|
|
53
56
|
trimMcp?: boolean;
|
|
@@ -124,6 +127,8 @@ export declare function recordSessionLaunch(ctx: {
|
|
|
124
127
|
/** The resolved context profile this launch runs under. */
|
|
125
128
|
contextProfile: ContextProfile;
|
|
126
129
|
}): Promise<void>;
|
|
130
|
+
/** The real I/O behind a subscription launch's Mnemom key (lib/subscription-login.ts). */
|
|
131
|
+
export declare function defaultMnemomKeyDeps(storePath?: string): MnemomKeyDeps;
|
|
127
132
|
/**
|
|
128
133
|
* Resolve the Anthropic key with NO 1Password dependency (built for external
|
|
129
134
|
* customer adoption), in order:
|
package/dist/commands/agent.js
CHANGED
|
@@ -28,11 +28,12 @@
|
|
|
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
|
-
import { MNEMOM_DIR } from "../lib/config.js";
|
|
35
|
-
import { getAgentByName } from "../lib/api.js";
|
|
34
|
+
import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
|
|
35
|
+
import { createApiKey, getAgentByName } from "../lib/api.js";
|
|
36
|
+
import { MNEMOM_KEY_STORE_FIELD, resolveAgentAuth, resolveSubscriptionMnemomKey, subscriptionChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
|
|
36
37
|
import { LEDGER_FILE, appendSessionRecord, 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";
|
|
@@ -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, 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, 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
|
|
@@ -109,6 +113,9 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
109
113
|
// ── door ──
|
|
110
114
|
const gatewayHost = resolveGatewayHost(env);
|
|
111
115
|
const door = resolveAnthropicDoor(env);
|
|
116
|
+
const cellError = singleCellError({ door, apiUrl: getApiUrl(), cellGateway: getGatewayUrl() });
|
|
117
|
+
if (cellError)
|
|
118
|
+
throw new Error(cellError);
|
|
112
119
|
const cellNote = isProdGateway(env) ? "us-2/prod" : `custom (${gatewayHost})`;
|
|
113
120
|
const yolo = options.yolo !== false; // default ON
|
|
114
121
|
const contextHint = options.contextHint !== false; // default ON
|
|
@@ -121,7 +128,12 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
121
128
|
context: options.context,
|
|
122
129
|
env,
|
|
123
130
|
});
|
|
124
|
-
|
|
131
|
+
let contextHandoff = resolveContextHandoff({ contextHandoff: options.contextHandoff, env });
|
|
132
|
+
if (contextHandoff && auth === "subscription") {
|
|
133
|
+
// The handoff tool authenticates with the Anthropic key a subscription launch does not have.
|
|
134
|
+
say("mnemom agent: context handoff is not available with subscription login yet — launching without it.");
|
|
135
|
+
contextHandoff = false;
|
|
136
|
+
}
|
|
125
137
|
const experimental = options.experimental ?? [];
|
|
126
138
|
const sessionConfig = buildSessionConfig({ profile: contextProfile, contextHandoff });
|
|
127
139
|
const sessionConfigB64 = buildSessionConfigB64({
|
|
@@ -172,6 +184,9 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
172
184
|
sessionConfig,
|
|
173
185
|
experimental,
|
|
174
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
|
+
}
|
|
175
190
|
if (expNotice)
|
|
176
191
|
say(expNotice);
|
|
177
192
|
if (previewShape === "remote-control") {
|
|
@@ -190,10 +205,10 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
190
205
|
cli = await promptForCliPath(options.cli, env, cliError);
|
|
191
206
|
}
|
|
192
207
|
// ── resolve the Anthropic key (zero 1Password; never logged) ──
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
keySource: options.keySource
|
|
196
|
-
|
|
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;
|
|
197
212
|
// ── account + µ-balance gate (no free tier) ─────────────────────────────────
|
|
198
213
|
// mnemom agent runs governed and on µ, so before we seal a contract or launch we
|
|
199
214
|
// (1) ensure a Mnemom session — offering the try-me-style sign-in / sign-up when
|
|
@@ -242,6 +257,14 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
242
257
|
rcCapable: rc.ok,
|
|
243
258
|
say,
|
|
244
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
|
+
}
|
|
245
268
|
// Now that a session exists, a defaulted slug uses the real login handle from
|
|
246
269
|
// /v1/auth/me (the stored token has no email, so the pre-sign-in value was the
|
|
247
270
|
// "agent-user" fallback). Only when the caller didn't name an agent. Under an
|
|
@@ -258,7 +281,12 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
258
281
|
// provisions-with-confirm when interactive; warns and continues otherwise.
|
|
259
282
|
// Skipped entirely by `--no-setup`. A provisioning failure never blocks the
|
|
260
283
|
// launch — it degrades to the unclaimed (fail-open) identity with a warning.
|
|
261
|
-
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) {
|
|
262
290
|
try {
|
|
263
291
|
await ensureGovernedAgent({
|
|
264
292
|
slug: agent,
|
|
@@ -340,11 +368,13 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
340
368
|
say(fmt.warn(`mnemom agent: context handoff could not be registered (${err instanceof Error ? err.message : String(err)}) — launching without the handoff tool.`));
|
|
341
369
|
}
|
|
342
370
|
}
|
|
343
|
-
|
|
371
|
+
// A subscription launch (no Anthropic key) is always terminal — see subscriptionShape.
|
|
372
|
+
if (shape === "terminal" || !anthropicKey) {
|
|
344
373
|
launchTerminal({
|
|
345
374
|
cli,
|
|
346
375
|
door,
|
|
347
376
|
anthropicKey,
|
|
377
|
+
mnemomKey,
|
|
348
378
|
agent,
|
|
349
379
|
scenario,
|
|
350
380
|
conversationId,
|
|
@@ -422,6 +452,25 @@ export async function recordSessionLaunch(ctx) {
|
|
|
422
452
|
// ───────────────────────────────────────────────────────────────────────────
|
|
423
453
|
// Credential resolution (zero 1Password)
|
|
424
454
|
// ───────────────────────────────────────────────────────────────────────────
|
|
455
|
+
/** The real I/O behind a subscription launch's Mnemom key (lib/subscription-login.ts). */
|
|
456
|
+
export function defaultMnemomKeyDeps(storePath = CODE_STORE) {
|
|
457
|
+
return {
|
|
458
|
+
env: process.env,
|
|
459
|
+
readStored: () => {
|
|
460
|
+
try {
|
|
461
|
+
const data = JSON.parse(readFileSync(storePath, "utf8"));
|
|
462
|
+
const v = data[MNEMOM_KEY_STORE_FIELD];
|
|
463
|
+
return typeof v === "string" ? stripCrlf(v) : undefined;
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
mint: async () => (await createApiKey(`mnemom-agent ${hostname()}`, ["gateway"])).key,
|
|
470
|
+
store: (key) => mergeStore({ [MNEMOM_KEY_STORE_FIELD]: key }, storePath),
|
|
471
|
+
say,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
425
474
|
/**
|
|
426
475
|
* Resolve the Anthropic key with NO 1Password dependency (built for external
|
|
427
476
|
* customer adoption), in order:
|
|
@@ -657,6 +706,7 @@ function launchTerminal(ctx) {
|
|
|
657
706
|
warnAdapter(ctx.cli);
|
|
658
707
|
const headers = buildCustomHeaders({
|
|
659
708
|
anthropicKey: ctx.anthropicKey,
|
|
709
|
+
mnemomKey: ctx.mnemomKey,
|
|
660
710
|
agent: ctx.agent,
|
|
661
711
|
conversationId: ctx.conversationId,
|
|
662
712
|
contractB64: ctx.envelope?.base64,
|
|
@@ -672,11 +722,20 @@ function launchTerminal(ctx) {
|
|
|
672
722
|
// Claude Code logged in via claude.ai (OAuth) PREEMPTS ANTHROPIC_API_KEY, so
|
|
673
723
|
// it is force-unset; the real key rides in ANTHROPIC_CUSTOM_HEADERS and
|
|
674
724
|
// ANTHROPIC_AUTH_TOKEN is a fixed non-secret placeholder.
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
childEnv
|
|
679
|
-
|
|
725
|
+
//
|
|
726
|
+
// A subscription launch (no Anthropic key) sets NO auth token: Claude Code then sends its
|
|
727
|
+
// own claude.ai login to the gateway (lib/subscription-login.ts).
|
|
728
|
+
let childEnv;
|
|
729
|
+
if (ctx.anthropicKey) {
|
|
730
|
+
childEnv = { ...process.env };
|
|
731
|
+
delete childEnv.ANTHROPIC_API_KEY;
|
|
732
|
+
childEnv.ANTHROPIC_BASE_URL = ctx.door;
|
|
733
|
+
childEnv.ANTHROPIC_AUTH_TOKEN = "mnemom-gateway";
|
|
734
|
+
childEnv.ANTHROPIC_CUSTOM_HEADERS = headers;
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
childEnv = subscriptionChildEnv(process.env, ctx.door, headers);
|
|
738
|
+
}
|
|
680
739
|
const sessionName = passthroughName(ctx.options.passthrough ?? []) ?? sessionDisplayName(ctx.agent, ctx.scenario);
|
|
681
740
|
childEnv[SESSION_NAME_ENV] = sessionName;
|
|
682
741
|
if (ctx.options.slack)
|
package/dist/index.js
CHANGED
|
@@ -100,8 +100,8 @@ program
|
|
|
100
100
|
// CONTRACT the gateway seals on the first turn. Three launch shapes — terminal
|
|
101
101
|
// (default), --remote-control (interactive RC in this terminal), --server
|
|
102
102
|
// (headless RC dispatcher) — with yolo on by default. `--cli` selects the
|
|
103
|
-
// executable (name or full path; default claude). The
|
|
104
|
-
//
|
|
103
|
+
// executable (name or full path; default claude). The door is the gateway of the
|
|
104
|
+
// cell the CLI uses (prod by default); a door on another cell is refused. The
|
|
105
105
|
// Anthropic key is resolved with zero 1Password dependency (env → ~/.mnemom store
|
|
106
106
|
// → interactive prompt) inside the launched process and never logged. See
|
|
107
107
|
// commands/code.ts.
|
|
@@ -139,9 +139,10 @@ const agentCmd = program
|
|
|
139
139
|
.option("--no-implicit", "Turn implicit goal alignment OFF for this launch (overrides launcher.toml goal_mode = implicit)")
|
|
140
140
|
.option("--conversation-id <id>", "x-mnemom-conversation-id (default: auto-generated)")
|
|
141
141
|
.option("--cli <name|path>", "Coding-agent CLI to launch: a name or full path (default: claude)")
|
|
142
|
-
.option("--gateway <url>", "Gateway host override (default:
|
|
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,
|
package/dist/lib/agent.d.ts
CHANGED
|
@@ -63,11 +63,27 @@ export interface ContractFlags {
|
|
|
63
63
|
export type LaunchShape = "terminal" | "remote-control" | "server";
|
|
64
64
|
/**
|
|
65
65
|
* Resolve the gateway HOST (scheme + host, no path). An explicit, non-empty
|
|
66
|
-
* `MNEMOM_AGENT_GATEWAY` wins verbatim; otherwise the
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
* `MNEMOM_AGENT_GATEWAY` wins verbatim; otherwise the gateway of the cell the rest
|
|
67
|
+
* of the CLI uses (`getGatewayUrl()`: prod unless MNEMOM_ENV / MNEMOM_RING /
|
|
68
|
+
* MNEMOM_GATEWAY_URL name another). An override on a different cell is refused at
|
|
69
|
+
* launch — see `singleCellError`. Trailing slash trimmed so door concatenation is clean.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveGatewayHost(env?: NodeJS.ProcessEnv, cellGateway?: string): string;
|
|
72
|
+
/** Same scheme+host+port. An unparseable URL never matches. */
|
|
73
|
+
export declare function sameOrigin(a: string, b: string): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* A launch must use ONE cell: the Anthropic `door` its traffic goes to must be the
|
|
76
|
+
* gateway of the cell the CLI signs in, resolves the agent and reads sessions on
|
|
77
|
+
* (`cellGateway`, paired with `apiUrl`). A mixed launch records one cell's agent
|
|
78
|
+
* and runs as another cell's, so `sessions`/`show` never find its goal state.
|
|
79
|
+
* Returns the refusal message, or null. A loopback door is a local dev gateway and
|
|
80
|
+
* is exempt.
|
|
81
|
+
*/
|
|
82
|
+
export declare function singleCellError(opts: {
|
|
83
|
+
door: string;
|
|
84
|
+
apiUrl: string;
|
|
85
|
+
cellGateway: string;
|
|
86
|
+
}): string | null;
|
|
71
87
|
/**
|
|
72
88
|
* The Anthropic door the CLI points Claude Code at:
|
|
73
89
|
* `${MNEMOM_AGENT_GATEWAY}/anthropic`, or the full-door override
|
|
@@ -267,7 +283,10 @@ export declare function resolveRemoteControlShape(args: {
|
|
|
267
283
|
* callers must place it only in the child's env, never in a log line.
|
|
268
284
|
*/
|
|
269
285
|
export declare function buildCustomHeaders(opts: {
|
|
270
|
-
|
|
286
|
+
/** Absent on a subscription-login launch: Claude Code then sends its own claude.ai login. */
|
|
287
|
+
anthropicKey?: string;
|
|
288
|
+
/** The governed-identity anchor on a subscription-login launch (lib/subscription-login.ts). */
|
|
289
|
+
mnemomKey?: string;
|
|
271
290
|
agent: string;
|
|
272
291
|
conversationId: string;
|
|
273
292
|
contractB64?: string;
|
package/dist/lib/agent.js
CHANGED
|
@@ -20,19 +20,56 @@
|
|
|
20
20
|
* NAMED), but prod is never selected silently — see `resolveGatewayHost`.
|
|
21
21
|
*/
|
|
22
22
|
import { createHash, randomUUID } from "node:crypto";
|
|
23
|
+
import { getGatewayUrl } from "./config.js";
|
|
23
24
|
/** The us-2/prod gateway host. The default door — never any other cell silently. */
|
|
24
25
|
export const PROD_GATEWAY_HOST = "https://gateway.mnemom.ai";
|
|
25
26
|
/**
|
|
26
27
|
* Resolve the gateway HOST (scheme + host, no path). An explicit, non-empty
|
|
27
|
-
* `MNEMOM_AGENT_GATEWAY` wins verbatim; otherwise the
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* `MNEMOM_AGENT_GATEWAY` wins verbatim; otherwise the gateway of the cell the rest
|
|
29
|
+
* of the CLI uses (`getGatewayUrl()`: prod unless MNEMOM_ENV / MNEMOM_RING /
|
|
30
|
+
* MNEMOM_GATEWAY_URL name another). An override on a different cell is refused at
|
|
31
|
+
* launch — see `singleCellError`. Trailing slash trimmed so door concatenation is clean.
|
|
30
32
|
*/
|
|
31
|
-
export function resolveGatewayHost(env = process.env) {
|
|
33
|
+
export function resolveGatewayHost(env = process.env, cellGateway = getGatewayUrl()) {
|
|
32
34
|
const raw = (env.MNEMOM_AGENT_GATEWAY ?? "").trim();
|
|
33
|
-
const host = raw.length > 0 ? raw :
|
|
35
|
+
const host = raw.length > 0 ? raw : cellGateway;
|
|
34
36
|
return host.replace(/\/+$/, "");
|
|
35
37
|
}
|
|
38
|
+
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
39
|
+
/** Same scheme+host+port. An unparseable URL never matches. */
|
|
40
|
+
export function sameOrigin(a, b) {
|
|
41
|
+
try {
|
|
42
|
+
return new URL(a).origin === new URL(b).origin;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A launch must use ONE cell: the Anthropic `door` its traffic goes to must be the
|
|
50
|
+
* gateway of the cell the CLI signs in, resolves the agent and reads sessions on
|
|
51
|
+
* (`cellGateway`, paired with `apiUrl`). A mixed launch records one cell's agent
|
|
52
|
+
* and runs as another cell's, so `sessions`/`show` never find its goal state.
|
|
53
|
+
* Returns the refusal message, or null. A loopback door is a local dev gateway and
|
|
54
|
+
* is exempt.
|
|
55
|
+
*/
|
|
56
|
+
export function singleCellError(opts) {
|
|
57
|
+
let doorUrl;
|
|
58
|
+
try {
|
|
59
|
+
doorUrl = new URL(opts.door);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return `mnemom agent: the gateway '${opts.door}' is not a valid URL.`;
|
|
63
|
+
}
|
|
64
|
+
if (LOOPBACK_HOSTS.has(doorUrl.hostname))
|
|
65
|
+
return null;
|
|
66
|
+
if (sameOrigin(opts.door, opts.cellGateway))
|
|
67
|
+
return null;
|
|
68
|
+
return (`mnemom agent: the gateway ${doorUrl.origin} is on a different cell from the one this CLI uses ` +
|
|
69
|
+
`(API ${opts.apiUrl}, gateway ${opts.cellGateway}). A launch must use one cell. Point the whole ` +
|
|
70
|
+
"CLI at that cell (MNEMOM_API_URL + MNEMOM_GATEWAY_URL, or MNEMOM_RING) and drop the gateway " +
|
|
71
|
+
"override (--gateway, MNEMOM_AGENT_GATEWAY, MNEMOM_AGENT_ANTHROPIC_GATEWAY, or `gateway` in launcher.toml).");
|
|
72
|
+
}
|
|
36
73
|
/**
|
|
37
74
|
* The Anthropic door the CLI points Claude Code at:
|
|
38
75
|
* `${MNEMOM_AGENT_GATEWAY}/anthropic`, or the full-door override
|
|
@@ -429,7 +466,8 @@ export function resolveRemoteControlShape(args) {
|
|
|
429
466
|
*/
|
|
430
467
|
export function buildCustomHeaders(opts) {
|
|
431
468
|
const lines = [
|
|
432
|
-
`x-api-key: ${opts.anthropicKey}
|
|
469
|
+
...(opts.anthropicKey ? [`x-api-key: ${opts.anthropicKey}`] : []),
|
|
470
|
+
...(opts.mnemomKey ? [`x-mnemom-api-key: ${opts.mnemomKey}`] : []),
|
|
433
471
|
`x-mnemom-agent: ${opts.agent}`,
|
|
434
472
|
`x-mnemom-conversation-id: ${opts.conversationId}`,
|
|
435
473
|
];
|
|
@@ -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";
|
|
@@ -86,7 +87,7 @@ function ensureGuardrails(cfg) {
|
|
|
86
87
|
export const CONFIG_KEYS = [
|
|
87
88
|
{
|
|
88
89
|
key: "gateway",
|
|
89
|
-
describe: "Gateway host (default
|
|
90
|
+
describe: "Gateway host (default: the gateway of the cell the CLI uses — prod unless MNEMOM_ENV/MNEMOM_RING/MNEMOM_GATEWAY_URL say otherwise; a different cell is refused)",
|
|
90
91
|
get: (c) => c.gateway,
|
|
91
92
|
set: (c, v) => {
|
|
92
93
|
c.gateway = coerceNonEmpty("gateway", v);
|
|
@@ -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
|
}
|
package/dist/lib/slack-router.js
CHANGED
|
@@ -53,7 +53,9 @@ export function ensureRouter(argv = [process.execPath, process.argv[1] ?? "mnemo
|
|
|
53
53
|
}
|
|
54
54
|
/** Run the router until the process is killed. Reconnects on disconnect. */
|
|
55
55
|
export async function runRouter(opts) {
|
|
56
|
-
|
|
56
|
+
// Lines carry Slack-supplied values (channel, handles, disconnect reason);
|
|
57
|
+
// strip CR/LF so a crafted message cannot forge extra log lines.
|
|
58
|
+
const log = opts.log ?? ((l) => console.error(`[slack-router] ${l.replace(/\n|\r/g, "")}`));
|
|
57
59
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
58
60
|
const WS = globalThis.WebSocket;
|
|
59
61
|
if (!WS)
|
|
@@ -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
|
+
}
|