From 477279daeb697a8f234a7ea9638c306026c3008f Mon Sep 17 00:00:00 2001 From: Mgeeeek Date: Thu, 14 May 2026 16:10:31 +0530 Subject: [PATCH] =?UTF-8?q?feat(cli):=20implement=20Dev=20Spec=20C4=20rule?= =?UTF-8?q?s=201-2=20=E2=80=94=20reuse=20valid=20env/config=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this change, `mem0 init --agent` always minted a fresh shadow, even when a valid MEM0_API_KEY env var (set by the Claude plugin or shell rc) was already in place. The env var would then silently shadow the freshly-minted shadow key on the next CLI call — leading to the surprising "Agent Mode active but my personal account does the work" behaviour. Dev Spec §C4 already specifies the right precedence: Rule 1: env MEM0_API_KEY valid → reuse, emit existing_key, no new key Rule 2: config api_key valid → reuse, emit existing_key Rule 3: mint a fresh shadow This commit implements rules 1-3 in both runtimes (Python + Node), with a 5-second ping to validate candidate keys against /v1/ping/. Also reorders so the Agent Mode branch runs BEFORE the existing-config overwrite guard. The guard's intent is "warn before overwriting a valid key" — but rules 1/2 REUSE (not overwrite) a valid key, so the guard must not fire on the agent path. Net effect: the plugin's MEM0_API_KEY env var and the CLI's config.json::platform.api_key now naturally co-exist: - User installs plugin → MEM0_API_KEY set, persisted via shell rc - User runs `mem0 init --agent` → rule 1 fires → existing key kept, no shadow created, no confusion - Plugin's MCP server (which reads ${MEM0_API_KEY}) and the CLI use the same key No new shadow accounts on prod from CI / repeat invocations. --- cli/node/src/commands/init.ts | 64 ++++++++++++++++---- cli/python/src/mem0_cli/commands/init_cmd.py | 55 ++++++++++++++--- 2 files changed, 97 insertions(+), 22 deletions(-) diff --git a/cli/node/src/commands/init.ts b/cli/node/src/commands/init.ts index f03c45c26..9e73e3138 100644 --- a/cli/node/src/commands/init.ts +++ b/cli/node/src/commands/init.ts @@ -33,6 +33,22 @@ function validateEmail(email: string): void { } } +async function pingKey( + apiKey: string, + baseUrl: string, + timeoutMs = 5000, +): Promise { + try { + const resp = await fetch(`${baseUrl.replace(/\/+$/, "")}/v1/ping/`, { + headers: { Authorization: `Token ${apiKey}` }, + signal: AbortSignal.timeout(timeoutMs), + }); + return resp.status === 200; + } catch { + return false; + } +} + async function emailLogin( email: string, code: string | undefined, @@ -304,6 +320,40 @@ export async function runInit( return; } + // ── Agent Mode path runs BEFORE the existing-config guard ────────────── + // Rule 1/2 will REUSE a valid existing key (not overwrite), so we must + // short-circuit before the guard prompts the user about overwriting. + // Rule 3 only mints when there's no valid key to reuse — in that case + // overwriting is what the user wants. + const agentCtx = + opts.agent === true || isAgentMode() || detectAgentCaller() !== null; + if (!opts.apiKey && !opts.email && agentCtx) { + // Rule 1: env MEM0_API_KEY valid → reuse, no new key. + const envKey = (process.env.MEM0_API_KEY || "").trim(); + if (envKey && (await pingKey(envKey, baseUrl))) { + printSuccess( + "Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted.", + ); + fireInit("existing_key"); + return; + } + // Rule 2: existing config api_key valid → reuse. + if ( + savedConfig.platform.apiKey && + (await pingKey(savedConfig.platform.apiKey, baseUrl)) + ) { + printSuccess( + "Existing API key in config is valid; reusing it. No new Agent Mode key was minted.", + ); + fireInit("existing_key"); + return; + } + // Rule 3: mint a fresh shadow (no valid key to reuse). + await bootstrapViaBackend(config, { source: opts.source ?? null }); + fireInit("agent"); + return; + } + // Warn if an existing config with an API key would be overwritten if ( !opts.force && @@ -375,19 +425,9 @@ export async function runInit( return; } - // ── Agent Mode auto-bootstrap (no email, no api_key flag) ─────────── - // Positive agent signal required: --agent flag (local or global) OR a - // recognized agent env var. Pure "no TTY" alone is NOT enough — pipe - // users would get surprised by a silent shadow signup. - const agentCtx = - opts.agent === true || isAgentMode() || detectAgentCaller() !== null; - if (!opts.apiKey && !opts.email && agentCtx) { - await bootstrapViaBackend(config, { source: opts.source ?? null }); - fireInit("agent"); - return; - } - // ── API key flow ────────────────────────────────────────────────────────── + // (Agent Mode branch runs earlier — see above, before the existing-config + // guard, so Rules 1/2 can REUSE a valid key without prompting overwrite.) // Non-TTY: resolve defaults so partial flags work in pipelines / CI if (!process.stdin.isTTY) { diff --git a/cli/python/src/mem0_cli/commands/init_cmd.py b/cli/python/src/mem0_cli/commands/init_cmd.py index 775d41992..5ec1ee6be 100644 --- a/cli/python/src/mem0_cli/commands/init_cmd.py +++ b/cli/python/src/mem0_cli/commands/init_cmd.py @@ -103,6 +103,19 @@ def _validate_email(email: str) -> None: raise typer.Exit(1) +def _ping_key(api_key: str, base_url: str, timeout: float = 5.0) -> bool: + """True if api_key passes /v1/ping/ against base_url within timeout.""" + try: + resp = httpx.get( + f"{base_url.rstrip('/')}/v1/ping/", + headers={"Authorization": f"Token {api_key}"}, + timeout=timeout, + ) + return resp.status_code == 200 + except Exception: + return False + + def _email_login( email: str, code: str | None, @@ -239,6 +252,36 @@ def run_init( _fire_init("email", claimed=True) return + # ── Agent Mode path runs BEFORE the existing-config guard ────────── + # Rules 1/2 REUSE a valid existing key (not overwrite), so we must + # short-circuit before the guard prompts. Rule 3 mints only when there + # is no valid key to reuse — in that case overwriting is correct. + _agent_ctx = agent or _global_agent_mode() or (detect_agent_caller() is not None) + if not api_key and not email and _agent_ctx: + # Rule 1: env MEM0_API_KEY valid → reuse, no new key. + _env_key = (os.environ.get("MEM0_API_KEY") or "").strip() + if _env_key and _ping_key(_env_key, base_url): + print_success( + console, + "Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted.", + ) + _fire_init("existing_key") + return + # Rule 2: existing config api_key valid → reuse. + if CONFIG_FILE.exists(): + _existing = load_config() + if _existing.platform.api_key and _ping_key(_existing.platform.api_key, base_url): + print_success( + console, + "Existing API key in config is valid; reusing it. No new Agent Mode key was minted.", + ) + _fire_init("existing_key") + return + # Rule 3: mint a fresh shadow (no valid key to reuse). + bootstrap_via_backend(config, source=source) + _fire_init("agent") + return + # Warn if an existing config with an API key would be overwritten if not force and CONFIG_FILE.exists(): existing = load_config() @@ -300,17 +343,9 @@ def run_init( console.print() return - # ── Agent Mode auto-bootstrap (no email, no api_key flag) ───────── - # Positive agent signal required: explicit --agent flag (local or global) - # OR a recognized agent env var. Pure "no TTY" alone is NOT enough — pipe - # users would get surprised by a silent shadow signup. - agent_ctx = agent or _global_agent_mode() or (detect_agent_caller() is not None) - if not api_key and not email and agent_ctx: - bootstrap_via_backend(config, source=source) - _fire_init("agent") - return - # ── API key flow (existing) ─────────────────────────────────────── + # (Agent Mode branch runs earlier — see above, before the existing-config + # guard, so Rules 1/2 can REUSE a valid key without prompting overwrite.) # Non-TTY: resolve defaults so partial flags work in pipelines / CI if not sys.stdin.isatty():