feat(cli): warn about public AGENTRUSH memories before first add

Adds a one-time PII warning shown before the first `mem0 agent-rush
add`. Persists acknowledgment in config so we never ask the same
machine twice.

Interactive (TTY):
  ⚠️  AGENTRUSH memories are PUBLIC — visible to any other player.
     Do not include real names, emails, secrets, work content, or PII.
     Continue? [y/N]
  → on "y" the timestamp lands in `agent_rush.acknowledged_at`.
  → anything else aborts with no add.

Non-interactive (agent invocation, no TTY): the warning prints to
stderr (visible in the agent transcript so the human notices) and
the add proceeds. Agents can't answer y/N; blocking them would defeat
the entire launch flow. Ack is intentionally not persisted in this
path so a later human run still sees the prompt.

Mirrored across Node + Python CLIs with matching JSON schema:
  config.json adds `agent_rush.acknowledged_at: ""` (ISO timestamp).
This commit is contained in:
Mgeeeek
2026-05-20 18:04:24 +05:30
parent abc57323c2
commit 4ca9d13c47
4 changed files with 126 additions and 2 deletions
+54 -1
View File
@@ -3,10 +3,18 @@
* Project routing is implicit (server-side); zero flags needed. * Project routing is implicit (server-side); zero flags needed.
*/ */
import readline from "node:readline";
import { colors, printError, printSuccess } from "../branding.js"; import { colors, printError, printSuccess } from "../branding.js";
import { loadConfig } from "../config.js"; import { loadConfig, saveConfig } from "../config.js";
import { CLI_VERSION } from "../version.js"; import { CLI_VERSION } from "../version.js";
const PII_WARNING = [
"",
"⚠️ AGENTRUSH memories are PUBLIC — visible to any other player.",
" Do not include real names, emails, secrets, work content, or PII.",
"",
].join("\n");
const ERROR_HINTS: Record<string, string> = { const ERROR_HINTS: Record<string, string> = {
agentrush_search_first: agentrush_search_first:
"Run 3 'mem0 agent-rush search' commands before adding.", "Run 3 'mem0 agent-rush search' commands before adding.",
@@ -66,7 +74,52 @@ async function callEndpoint(
return json; return json;
} }
function promptLine(question: string): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}
/**
* Ensure the human has acknowledged that AGENTRUSH memories are PUBLIC.
*
* Interactive (TTY): show the prompt; on "y" persist `agentRush.acknowledgedAt`
* so we never ask the same machine twice. On anything else, abort.
*
* Non-interactive (agent invocation, no TTY): print the warning to stderr
* for the human reading the agent's transcript and proceed — agents can't
* answer y/N prompts.
*/
async function ensureWarningAcknowledged(): Promise<void> {
const config = loadConfig();
if (config.agentRush?.acknowledgedAt) return;
if (!process.stdin.isTTY || !process.stdout.isTTY) {
// Agent context: surface the warning to stderr, don't block.
console.error(PII_WARNING);
return;
}
console.log(PII_WARNING);
const answer = (await promptLine(" Continue? [y/N]: ")).toLowerCase();
if (answer !== "y" && answer !== "yes") {
printError("Aborted.");
process.exit(1);
}
config.agentRush.acknowledgedAt = new Date().toISOString();
saveConfig(config);
}
export async function cmdAgentRushAdd(content: string): Promise<void> { export async function cmdAgentRushAdd(content: string): Promise<void> {
await ensureWarningAcknowledged();
const result = await callEndpoint("/v1/agent-rush/memories/", { content }); const result = await callEndpoint("/v1/agent-rush/memories/", { content });
printSuccess( printSuccess(
`Memory submitted (event_id: ${(result as { event_id?: string }).event_id ?? "?"})`, `Memory submitted (event_id: ${(result as { event_id?: string }).event_id ?? "?"})`,
+15
View File
@@ -40,11 +40,18 @@ export interface TelemetryConfig {
anonymousId: string; anonymousId: string;
} }
export interface AgentRushConfig {
// ISO timestamp the human acknowledged the "memories are public" warning.
// Empty until first interactive `mem0 agent-rush add`.
acknowledgedAt: string;
}
export interface Mem0Config { export interface Mem0Config {
version: number; version: number;
defaults: DefaultsConfig; defaults: DefaultsConfig;
platform: PlatformConfig; platform: PlatformConfig;
telemetry: TelemetryConfig; telemetry: TelemetryConfig;
agentRush: AgentRushConfig;
} }
export function createDefaultConfig(): Mem0Config { export function createDefaultConfig(): Mem0Config {
@@ -69,6 +76,9 @@ export function createDefaultConfig(): Mem0Config {
telemetry: { telemetry: {
anonymousId: "", anonymousId: "",
}, },
agentRush: {
acknowledgedAt: "",
},
}; };
} }
@@ -103,6 +113,8 @@ export function loadConfig(): Mem0Config {
config.defaults.runId = defaults.run_id ?? ""; config.defaults.runId = defaults.run_id ?? "";
const telemetry = data.telemetry ?? {}; const telemetry = data.telemetry ?? {};
config.telemetry.anonymousId = telemetry.anonymous_id ?? ""; config.telemetry.anonymousId = telemetry.anonymous_id ?? "";
const agentRush = data.agent_rush ?? {};
config.agentRush.acknowledgedAt = agentRush.acknowledged_at ?? "";
} }
// Environment variable overrides // Environment variable overrides
@@ -143,6 +155,9 @@ export function saveConfig(config: Mem0Config): void {
telemetry: { telemetry: {
anonymous_id: config.telemetry.anonymousId, anonymous_id: config.telemetry.anonymousId,
}, },
agent_rush: {
acknowledged_at: config.agentRush.acknowledgedAt,
},
}; };
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2)); fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
@@ -6,16 +6,26 @@ Hardcoded routing; no flags needed.
from __future__ import annotations from __future__ import annotations
import sys
from datetime import datetime, timezone
import httpx import httpx
import typer import typer
from rich.console import Console from rich.console import Console
from mem0_cli.branding import BRAND_COLOR, print_error, print_success from mem0_cli.branding import BRAND_COLOR, print_error, print_success
from mem0_cli.config import load_config from mem0_cli.config import load_config, save_config
console = Console() console = Console()
err_console = Console(stderr=True) err_console = Console(stderr=True)
_PII_WARNING_LINES = (
"",
"[yellow]⚠️ AGENTRUSH memories are PUBLIC — visible to any other player.[/yellow]",
"[yellow] Do not include real names, emails, secrets, work content, or PII.[/yellow]",
"",
)
_SOURCE_HEADERS = { _SOURCE_HEADERS = {
"X-Mem0-Source": "cli", "X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "python", "X-Mem0-Client-Language": "python",
@@ -69,7 +79,39 @@ def _call(path: str, body: dict) -> dict:
return data return data
def _ensure_warning_acknowledged() -> None:
"""Block the first interactive add on the PII warning; pass-through for agents.
Interactive (TTY): show prompt, require explicit 'y', persist
`agent_rush.acknowledged_at` so we never ask the same machine twice.
Non-interactive (no TTY — typical when an agent runs the CLI): surface
the warning to stderr for the human reading the agent transcript and
proceed without prompting (agents can't answer y/N).
"""
config = load_config()
if config.agent_rush.acknowledged_at:
return
is_tty = sys.stdin.isatty() and sys.stdout.isatty()
if not is_tty:
for line in _PII_WARNING_LINES:
err_console.print(line)
return
for line in _PII_WARNING_LINES:
console.print(line)
answer = typer.prompt(" Continue? [y/N]", default="N", show_default=False).strip().lower()
if answer not in ("y", "yes"):
print_error(err_console, "Aborted.")
raise typer.Exit(1)
config.agent_rush.acknowledged_at = datetime.now(timezone.utc).isoformat()
save_config(config)
def run_agent_rush_add(content: str) -> None: def run_agent_rush_add(content: str) -> None:
_ensure_warning_acknowledged()
result = _call("/v1/agent-rush/memories/", {"content": content}) result = _call("/v1/agent-rush/memories/", {"content": content})
event_id = result.get("event_id", "?") event_id = result.get("event_id", "?")
print_success(console, f"Memory submitted (event_id: {event_id})") print_success(console, f"Memory submitted (event_id: {event_id})")
+14
View File
@@ -51,12 +51,20 @@ class TelemetryConfig:
anonymous_id: str = "" anonymous_id: str = ""
@dataclass
class AgentRushConfig:
# ISO timestamp the human acknowledged the "memories are public" warning.
# Empty until first interactive `mem0 agent-rush add`.
acknowledged_at: str = ""
@dataclass @dataclass
class Mem0Config: class Mem0Config:
version: int = CONFIG_VERSION version: int = CONFIG_VERSION
defaults: DefaultsConfig = field(default_factory=DefaultsConfig) defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
platform: PlatformConfig = field(default_factory=PlatformConfig) platform: PlatformConfig = field(default_factory=PlatformConfig)
telemetry: TelemetryConfig = field(default_factory=TelemetryConfig) telemetry: TelemetryConfig = field(default_factory=TelemetryConfig)
agent_rush: AgentRushConfig = field(default_factory=AgentRushConfig)
SHORT_KEY_ALIASES: dict[str, str] = { SHORT_KEY_ALIASES: dict[str, str] = {
@@ -105,6 +113,9 @@ def load_config() -> Mem0Config:
telemetry = data.get("telemetry", {}) telemetry = data.get("telemetry", {})
config.telemetry.anonymous_id = telemetry.get("anonymous_id", "") config.telemetry.anonymous_id = telemetry.get("anonymous_id", "")
agent_rush = data.get("agent_rush", {})
config.agent_rush.acknowledged_at = agent_rush.get("acknowledged_at", "")
# Environment variable overrides # Environment variable overrides
env_key = os.environ.get("MEM0_API_KEY") env_key = os.environ.get("MEM0_API_KEY")
if env_key: if env_key:
@@ -158,6 +169,9 @@ def save_config(config: Mem0Config) -> None:
"telemetry": { "telemetry": {
"anonymous_id": config.telemetry.anonymous_id, "anonymous_id": config.telemetry.anonymous_id,
}, },
"agent_rush": {
"acknowledged_at": config.agent_rush.acknowledged_at,
},
} }
with open(CONFIG_FILE, "w") as f: with open(CONFIG_FILE, "w") as f: