feat(cli): implement Dev Spec C4 rules 1-2 — reuse valid env/config key
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.
This commit is contained in:
@@ -33,6 +33,22 @@ function validateEmail(email: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
async function pingKey(
|
||||
apiKey: string,
|
||||
baseUrl: string,
|
||||
timeoutMs = 5000,
|
||||
): Promise<boolean> {
|
||||
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) {
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user