From 5021a53c1a4f4c7c6c337829747fa1d5a8930a9f Mon Sep 17 00:00:00 2001 From: Tannmay-ts Date: Sun, 25 Oct 2026 06:13:35 +0530 Subject: [PATCH 1/5] fix(seed): --theme-only and --config-only quiet the demo home too seed-memory-skills.py --theme-only and seed-tasks.py --config-only returned before seed_safety.quiet(), so a home seeded with --keep-live kept its tasks enabled and its chat deliveries on. Both shortcuts now quiet the home unless --keep-live is passed, like seed-chats-devices-channels.py --add. Closes #426 --- desktop/scripts/seed-memory-skills.py | 2 ++ desktop/scripts/seed-tasks.py | 2 ++ tests/test_keychain_namespace.py | 37 +++++++++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/desktop/scripts/seed-memory-skills.py b/desktop/scripts/seed-memory-skills.py index 6c1ba887f..c448e48c7 100644 --- a/desktop/scripts/seed-memory-skills.py +++ b/desktop/scripts/seed-memory-skills.py @@ -631,6 +631,8 @@ def main() -> None: home = Path(args.home).expanduser().resolve() if args.theme_only: set_theme(home, args.theme) + if not args.keep_live: + seed_safety.announce(seed_safety.quiet(home)) return if home.exists() and any(home.iterdir()): if not args.reset: diff --git a/desktop/scripts/seed-tasks.py b/desktop/scripts/seed-tasks.py index c6aa7de52..3ff357f73 100644 --- a/desktop/scripts/seed-tasks.py +++ b/desktop/scripts/seed-tasks.py @@ -751,6 +751,8 @@ async def main() -> None: cfg = configure() if ARGS.config_only: print(f"Updated {paths.config_file()}") + if not ARGS.keep_live: + seed_safety.announce(seed_safety.quiet(paths.home())) return app =SentientApp(cfg, llm=OfflineProvider(), db_path=paths.db_file(), enable_background=False) # type: ignore[arg-type] await app.start() diff --git a/tests/test_keychain_namespace.py b/tests/test_keychain_namespace.py index fd2838d14..7d351fe3d 100644 --- a/tests/test_keychain_namespace.py +++ b/tests/test_keychain_namespace.py @@ -3,6 +3,8 @@ from __future__ import annotations +import subprocess +import sys import time from pathlib import Path @@ -272,3 +274,38 @@ async def test_seed_safety_keeps_planning_tasks_and_live_runs_from_calling_the_m def test_seed_safety_refuses_the_default_home(user_home): with pytest.raises(SystemExit): _seed_safety().quiet(user_home / ".sentient") + + +@pytest.mark.parametrize("keep_live", [False, True], ids=["quiet", "keep-live"]) +@pytest.mark.parametrize("command", [ + ["seed-memory-skills.py", "--theme", "light", "--theme-only"], + ["seed-tasks.py", "--config-only"], +], ids=["theme-only", "config-only"]) +async def test_seed_shortcuts_quiet_the_home_unless_kept_live(config, isolated_home, user_home, command, keep_live): + """--theme-only and --config-only change an already seeded home, so they quiet it too (issue #426).""" + from sentient.config.loader import load_config, save_config + + save_config(config) + app = await SentientApp(config, llm=FakeProvider(), enable_background=False).start() + try: + await app.tasks.repo.insert_task({"id": "demo-daily", "name": "Daily digest", "status": "active", + "enabled": True, "plan": [], "created_at": "2026-10-01T00:00:00Z", + "updated_at": "2026-10-01T00:00:00Z"}) + await app.channels.store.add_chat("telegram", "100200301", "Maya Rao", deliver=True, session_id=None) + finally: + await app.stop() + + script = Path(__file__).resolve().parents[1] / "desktop" / "scripts" / command[0] + argv = [sys.executable, str(script), *command[1:], *(["--keep-live"] if keep_live else [])] + done = subprocess.run(argv, capture_output=True, text=True, timeout=120, check=False) + assert done.returncode == 0, done.stderr + + app = await SentientApp(load_config(), llm=FakeProvider(), enable_background=False).start() + try: + enabled = (await app.tasks.repo.get_task("demo-daily"))["enabled"] + deliver = (await app.channels.store.chat("telegram", "100200301"))["deliver"] + finally: + await app.stop() + live = (enabled, deliver, load_config().channels.enabled) + assert live == ((True, True, True) if keep_live else (False, False, False)) + assert ("quiet demo home:" in done.stdout) is not keep_live From d9a7227e138f5373560ef592a43bffff5de74daf Mon Sep 17 00:00:00 2001 From: Tannmay-ts Date: Sun, 25 Oct 2026 06:28:20 +0530 Subject: [PATCH 2/5] fix(seed): refuse the default data folder before a shortcut writes to it Pointed at ~/.sentient, --theme-only wrote the theme and --config-only rewrote config.yaml before quiet() refused the folder. seed_safety.check_home() is split out of quiet(); --theme-only now quiets before set_theme(), and --config-only checks the folder before ensure_layout() and configure(), then quiets at the end as before (quieting first would let configure() create a fresh home's config with messaging apps on). --- desktop/scripts/seed-memory-skills.py | 4 ++-- desktop/scripts/seed-tasks.py | 2 ++ desktop/scripts/seed_safety.py | 16 ++++++++++++---- tests/test_keychain_namespace.py | 21 +++++++++++++++++++++ 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/desktop/scripts/seed-memory-skills.py b/desktop/scripts/seed-memory-skills.py index c448e48c7..180bff6f9 100644 --- a/desktop/scripts/seed-memory-skills.py +++ b/desktop/scripts/seed-memory-skills.py @@ -630,9 +630,9 @@ def main() -> None: ap.error("--home or SENTIENT_HOME is required") home = Path(args.home).expanduser().resolve() if args.theme_only: - set_theme(home, args.theme) - if not args.keep_live: + if not args.keep_live: # first, so the default data folder is refused before the theme is written seed_safety.announce(seed_safety.quiet(home)) + set_theme(home, args.theme) return if home.exists() and any(home.iterdir()): if not args.reset: diff --git a/desktop/scripts/seed-tasks.py b/desktop/scripts/seed-tasks.py index 3ff357f73..1ec04ba38 100644 --- a/desktop/scripts/seed-tasks.py +++ b/desktop/scripts/seed-tasks.py @@ -747,6 +747,8 @@ async def seed(app: SentientApp) -> dict[str, str]: async def main() -> None: + if ARGS.config_only and not ARGS.keep_live: + seed_safety.check_home(paths.home()) # before configure() writes to the default data folder paths.ensure_layout() cfg = configure() if ARGS.config_only: diff --git a/desktop/scripts/seed_safety.py b/desktop/scripts/seed_safety.py index 6adc0935d..8c77d57a3 100644 --- a/desktop/scripts/seed_safety.py +++ b/desktop/scripts/seed_safety.py @@ -53,16 +53,24 @@ def _settle_busy_tasks(db: sqlite3.Connection) -> None: db.execute("UPDATE tasks SET status = ?, next_execution_at = NULL WHERE id = ?", (status, task_id)) -def quiet(home: str | Path) -> dict: - """Pause the tasks and turn off channel deliveries in ``home``. Returns what changed.""" +def check_home(home: str | Path) -> Path: + """``home`` resolved, or exit if it is the default data folder (the real user's). Call it before writing.""" home = Path(home).expanduser().resolve() if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) - from sentient import paths, secrets - from sentient.config.loader import load_config, save_config + from sentient import secrets if secrets.is_default_home(home): raise SystemExit(f"seed_safety: {home} is the default data folder (the real one); refusing to change it.") + return home + + +def quiet(home: str | Path) -> dict: + """Pause the tasks and turn off channel deliveries in ``home``. Returns what changed.""" + home = check_home(home) + from sentient import paths + from sentient.config.loader import load_config, save_config + os.environ["SENTIENT_HOME"] = str(home) # paths.* read it on every call report = {"tasks_paused": 0, "plans_stopped": 0, "runs_stopped": 0, "chats_muted": 0, "channels_off": False} if paths.config_file().exists() or paths.db_file().exists(): diff --git a/tests/test_keychain_namespace.py b/tests/test_keychain_namespace.py index 7d351fe3d..947cc76d9 100644 --- a/tests/test_keychain_namespace.py +++ b/tests/test_keychain_namespace.py @@ -3,6 +3,7 @@ from __future__ import annotations +import os import subprocess import sys import time @@ -309,3 +310,23 @@ async def test_seed_shortcuts_quiet_the_home_unless_kept_live(config, isolated_h live = (enabled, deliver, load_config().channels.enabled) assert live == ((True, True, True) if keep_live else (False, False, False)) assert ("quiet demo home:" in done.stdout) is not keep_live + + +@pytest.mark.parametrize("command", [ + ["seed-memory-skills.py", "--theme", "light", "--theme-only"], + ["seed-tasks.py", "--config-only"], +], ids=["theme-only", "config-only"]) +def test_seed_shortcuts_refuse_the_default_home_before_writing(tmp_path, command): + """Pointed at ~/.sentient, a shortcut exits before it changes anything there. The user folder is a stand-in.""" + user = tmp_path / "user" + default = user / ".sentient" + default.mkdir(parents=True) + (default / "config.yaml").write_text("ui:\n theme: dark\n", encoding="utf-8") + env = {**os.environ, "HOME": str(user), "USERPROFILE": str(user), "SENTIENT_HOME": str(default)} + + script = Path(__file__).resolve().parents[1] / "desktop" / "scripts" / command[0] + done = subprocess.run([sys.executable, str(script), *command[1:]], capture_output=True, text=True, timeout=120, + check=False, env=env) + assert done.returncode != 0 and "default data folder" in done.stderr + assert (default / "config.yaml").read_text(encoding="utf-8") == "ui:\n theme: dark\n" + assert [p.name for p in default.iterdir()] == ["config.yaml"] From 25b37291b9440dbf270fda4aef801c81242fcb12 Mon Sep 17 00:00:00 2001 From: Tannmay-ts Date: Sun, 25 Oct 2026 19:37:49 +0530 Subject: [PATCH 3/5] fix(seed): refuse the default data folder before any seed run writes or wipes it A full seed-tasks.py run pointed at ~/.sentient deleted its tasks and notifications, and seed-integrations-notifications.py (unless --keep-db) and seed-automations-usermodel.py --reset deleted the whole database, before quiet() refused the folder. Each now calls seed_safety.check_home() first, also with --keep-live, and so does --theme-only before it writes the theme. Adds changes/426.fixed.md. --- changes/426.fixed.md | 1 + desktop/scripts/seed-automations-usermodel.py | 1 + .../seed-integrations-notifications.py | 1 + desktop/scripts/seed-memory-skills.py | 5 +- desktop/scripts/seed-tasks.py | 3 +- tests/test_keychain_namespace.py | 55 +++++++++++++++++-- 6 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 changes/426.fixed.md diff --git a/changes/426.fixed.md b/changes/426.fixed.md new file mode 100644 index 000000000..42abbf766 --- /dev/null +++ b/changes/426.fixed.md @@ -0,0 +1 @@ +**The demo seed scripts can no longer wipe your real data folder.** Pointed at `~/.sentient`, a full `seed-tasks.py` run used to delete its tasks and notifications, and `seed-integrations-notifications.py` (or `seed-automations-usermodel.py --reset`) deleted its whole database, before refusing it. Now they, `--config-only` and `seed-memory-skills.py --theme-only` refuse that folder before changing anything, and the two shortcuts keep a demo home's tasks paused and messaging deliveries off like a full seed run. diff --git a/desktop/scripts/seed-automations-usermodel.py b/desktop/scripts/seed-automations-usermodel.py index 7df865f6a..a6e977d6c 100644 --- a/desktop/scripts/seed-automations-usermodel.py +++ b/desktop/scripts/seed-automations-usermodel.py @@ -385,6 +385,7 @@ async def seed_dreams(app: SentientApp) -> None: async def main() -> None: + seed_safety.check_home(HOME) # before --reset deletes the database, also with --keep-live HOME.mkdir(parents=True, exist_ok=True) if ARGS.reset: for suffix in ("", "-wal", "-shm"): diff --git a/desktop/scripts/seed-integrations-notifications.py b/desktop/scripts/seed-integrations-notifications.py index e1c3ccbbc..7f06c161b 100644 --- a/desktop/scripts/seed-integrations-notifications.py +++ b/desktop/scripts/seed-integrations-notifications.py @@ -162,6 +162,7 @@ async def suggestion_row(app: SentientApp, note: dict, payload: dict, when: str, # ----------------------------------------------------------------------------- seed async def seed() -> None: + seed_safety.check_home(HOME) # before the database is deleted, also with --keep-live HOME.mkdir(parents=True, exist_ok=True) if not KEEP_DB: for suffix in ("", "-wal", "-shm"): diff --git a/desktop/scripts/seed-memory-skills.py b/desktop/scripts/seed-memory-skills.py index 180bff6f9..4698c6362 100644 --- a/desktop/scripts/seed-memory-skills.py +++ b/desktop/scripts/seed-memory-skills.py @@ -630,9 +630,10 @@ def main() -> None: ap.error("--home or SENTIENT_HOME is required") home = Path(args.home).expanduser().resolve() if args.theme_only: - if not args.keep_live: # first, so the default data folder is refused before the theme is written - seed_safety.announce(seed_safety.quiet(home)) + seed_safety.check_home(home) # before the theme is written, also with --keep-live set_theme(home, args.theme) + if not args.keep_live: + seed_safety.announce(seed_safety.quiet(home)) return if home.exists() and any(home.iterdir()): if not args.reset: diff --git a/desktop/scripts/seed-tasks.py b/desktop/scripts/seed-tasks.py index 1ec04ba38..4e6c49538 100644 --- a/desktop/scripts/seed-tasks.py +++ b/desktop/scripts/seed-tasks.py @@ -747,8 +747,7 @@ async def seed(app: SentientApp) -> dict[str, str]: async def main() -> None: - if ARGS.config_only and not ARGS.keep_live: - seed_safety.check_home(paths.home()) # before configure() writes to the default data folder + seed_safety.check_home(paths.home()) # before anything is written or wiped, also with --keep-live paths.ensure_layout() cfg = configure() if ARGS.config_only: diff --git a/tests/test_keychain_namespace.py b/tests/test_keychain_namespace.py index 947cc76d9..f65d1b273 100644 --- a/tests/test_keychain_namespace.py +++ b/tests/test_keychain_namespace.py @@ -4,6 +4,7 @@ from __future__ import annotations import os +import sqlite3 import subprocess import sys import time @@ -312,21 +313,63 @@ async def test_seed_shortcuts_quiet_the_home_unless_kept_live(config, isolated_h assert ("quiet demo home:" in done.stdout) is not keep_live +def _run_seed_script(name: str, *args: str, user: Path) -> subprocess.CompletedProcess: + """Run a seed script with ``user`` standing in for the user's own folder, so ``~/.sentient`` is inside it.""" + script = Path(__file__).resolve().parents[1] / "desktop" / "scripts" / name + env = {**os.environ, "HOME": str(user), "USERPROFILE": str(user), "SENTIENT_HOME": str(user / ".sentient")} + return subprocess.run([sys.executable, str(script), *args], capture_output=True, text=True, timeout=120, + check=False, env=env) + + +@pytest.mark.parametrize("keep_live", [False, True], ids=["quiet", "keep-live"]) @pytest.mark.parametrize("command", [ ["seed-memory-skills.py", "--theme", "light", "--theme-only"], ["seed-tasks.py", "--config-only"], ], ids=["theme-only", "config-only"]) -def test_seed_shortcuts_refuse_the_default_home_before_writing(tmp_path, command): - """Pointed at ~/.sentient, a shortcut exits before it changes anything there. The user folder is a stand-in.""" +def test_seed_shortcuts_refuse_the_default_home_before_writing(tmp_path, command, keep_live): + """Pointed at ~/.sentient, a shortcut exits before it changes anything there, with --keep-live too.""" user = tmp_path / "user" default = user / ".sentient" default.mkdir(parents=True) (default / "config.yaml").write_text("ui:\n theme: dark\n", encoding="utf-8") - env = {**os.environ, "HOME": str(user), "USERPROFILE": str(user), "SENTIENT_HOME": str(default)} - script = Path(__file__).resolve().parents[1] / "desktop" / "scripts" / command[0] - done = subprocess.run([sys.executable, str(script), *command[1:]], capture_output=True, text=True, timeout=120, - check=False, env=env) + done = _run_seed_script(*command, *(["--keep-live"] if keep_live else []), user=user) assert done.returncode != 0 and "default data folder" in done.stderr assert (default / "config.yaml").read_text(encoding="utf-8") == "ui:\n theme: dark\n" assert [p.name for p in default.iterdir()] == ["config.yaml"] + + +@pytest.mark.parametrize("keep_live", [False, True], ids=["quiet", "keep-live"]) +@pytest.mark.parametrize("command", [ + ["seed-tasks.py"], + ["seed-integrations-notifications.py"], + ["seed-automations-usermodel.py", "--reset"], +], ids=["tasks", "integrations", "automations-reset"]) +async def test_a_seed_run_refuses_the_default_home_before_wiping_it(config, isolated_home, monkeypatch, tmp_path, + command, keep_live): + """These runs used to delete ~/.sentient's tasks and notifications (or its whole database) before quiet() + refused the folder.""" + from sentient.config.loader import save_config + + user = tmp_path / "user" + default = user / ".sentient" + monkeypatch.setenv("SENTIENT_HOME", str(default)) + save_config(config) + app = await SentientApp(config, llm=FakeProvider(), enable_background=False).start() + try: + await app.tasks.repo.insert_task({"id": "owner-real", "name": "Renew passport", "status": "active", + "enabled": True, "plan": [], "created_at": "2026-10-01T00:00:00Z", + "updated_at": "2026-10-01T00:00:00Z"}) + await app.notify("task", "Your passport renewal is due next week.", title="Renew passport") + finally: + await app.stop() + + done = _run_seed_script(*command, *(["--keep-live"] if keep_live else []), user=user) + assert done.returncode != 0 and "default data folder" in done.stderr + assert (default / "sentient.db").exists() + db = sqlite3.connect(default / "sentient.db") + try: + assert [row[0] for row in db.execute("SELECT id FROM tasks")] == ["owner-real"] + assert [row[0] for row in db.execute("SELECT title FROM notifications")] == ["Renew passport"] + finally: + db.close() From 18f28057773bf7a776b5159618e75224ab79451e Mon Sep 17 00:00:00 2001 From: Tannmay-ts Date: Sun, 25 Oct 2026 20:12:20 +0530 Subject: [PATCH 4/5] fix(seed): refuse the default data folder before --reset can wipe it seed-memory-skills.py and seed-chats-devices-channels.py --reset deleted the whole folder when it held a .seeded-demo marker, which an earlier seed run pointed at ~/.sentient could have left. Both now resolve the home through seed_safety.check_home() before any branch, also with --keep-live. --- changes/426.fixed.md | 2 +- desktop/scripts/seed-chats-devices-channels.py | 2 +- desktop/scripts/seed-memory-skills.py | 3 +-- tests/test_keychain_namespace.py | 9 ++++++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/changes/426.fixed.md b/changes/426.fixed.md index 42abbf766..1458dedb5 100644 --- a/changes/426.fixed.md +++ b/changes/426.fixed.md @@ -1 +1 @@ -**The demo seed scripts can no longer wipe your real data folder.** Pointed at `~/.sentient`, a full `seed-tasks.py` run used to delete its tasks and notifications, and `seed-integrations-notifications.py` (or `seed-automations-usermodel.py --reset`) deleted its whole database, before refusing it. Now they, `--config-only` and `seed-memory-skills.py --theme-only` refuse that folder before changing anything, and the two shortcuts keep a demo home's tasks paused and messaging deliveries off like a full seed run. +**The demo seed scripts can no longer wipe your real data folder.** Pointed at `~/.sentient`, some of them deleted its tasks, its database or the whole folder before refusing it. Now they all refuse that folder before changing anything, and the `--theme-only` and `--config-only` shortcuts keep a demo home's tasks paused and messaging off. diff --git a/desktop/scripts/seed-chats-devices-channels.py b/desktop/scripts/seed-chats-devices-channels.py index ff2fd72b8..f58cfbd3d 100644 --- a/desktop/scripts/seed-chats-devices-channels.py +++ b/desktop/scripts/seed-chats-devices-channels.py @@ -784,7 +784,7 @@ def main() -> None: args = ap.parse_args() if not args.home: ap.error("--home or SENTIENT_HOME is required") - home = Path(args.home).expanduser().resolve() + home = seed_safety.check_home(args.home) # before anything is written or wiped, also with --keep-live if args.add: home.mkdir(parents=True, exist_ok=True) os.environ["SENTIENT_HOME"] = str(home) diff --git a/desktop/scripts/seed-memory-skills.py b/desktop/scripts/seed-memory-skills.py index 4698c6362..698ae8cb8 100644 --- a/desktop/scripts/seed-memory-skills.py +++ b/desktop/scripts/seed-memory-skills.py @@ -628,9 +628,8 @@ def main() -> None: args = ap.parse_args() if not args.home: ap.error("--home or SENTIENT_HOME is required") - home = Path(args.home).expanduser().resolve() + home = seed_safety.check_home(args.home) # before anything is written or wiped, also with --keep-live if args.theme_only: - seed_safety.check_home(home) # before the theme is written, also with --keep-live set_theme(home, args.theme) if not args.keep_live: seed_safety.announce(seed_safety.quiet(home)) diff --git a/tests/test_keychain_namespace.py b/tests/test_keychain_namespace.py index f65d1b273..8c6b3deb5 100644 --- a/tests/test_keychain_namespace.py +++ b/tests/test_keychain_namespace.py @@ -344,11 +344,13 @@ def test_seed_shortcuts_refuse_the_default_home_before_writing(tmp_path, command ["seed-tasks.py"], ["seed-integrations-notifications.py"], ["seed-automations-usermodel.py", "--reset"], -], ids=["tasks", "integrations", "automations-reset"]) + ["seed-memory-skills.py", "--reset"], + ["seed-chats-devices-channels.py", "--reset"], +], ids=["tasks", "integrations", "automations-reset", "memory-skills-reset", "chats-reset"]) async def test_a_seed_run_refuses_the_default_home_before_wiping_it(config, isolated_home, monkeypatch, tmp_path, command, keep_live): - """These runs used to delete ~/.sentient's tasks and notifications (or its whole database) before quiet() - refused the folder.""" + """These runs used to delete ~/.sentient's tasks and notifications, its database or the whole folder before + quiet() refused it.""" from sentient.config.loader import save_config user = tmp_path / "user" @@ -363,6 +365,7 @@ async def test_a_seed_run_refuses_the_default_home_before_wiping_it(config, isol await app.notify("task", "Your passport renewal is due next week.", title="Renew passport") finally: await app.stop() + (default / ".seeded-demo").write_text("left by an earlier seed run\n", encoding="utf-8") # lets --reset wipe done = _run_seed_script(*command, *(["--keep-live"] if keep_live else []), user=user) assert done.returncode != 0 and "default data folder" in done.stderr From 3fd6d6dc629aefc4cb0830407d61cc4f3994f70f Mon Sep 17 00:00:00 2001 From: Tannmay-ts Date: Mon, 26 Oct 2026 01:38:33 +0530 Subject: [PATCH 5/5] fix(seed): note the --keep-live exception in the changelog entry --- changes/426.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/426.fixed.md b/changes/426.fixed.md index 1458dedb5..c71f4284e 100644 --- a/changes/426.fixed.md +++ b/changes/426.fixed.md @@ -1 +1 @@ -**The demo seed scripts can no longer wipe your real data folder.** Pointed at `~/.sentient`, some of them deleted its tasks, its database or the whole folder before refusing it. Now they all refuse that folder before changing anything, and the `--theme-only` and `--config-only` shortcuts keep a demo home's tasks paused and messaging off. +**The demo seed scripts can no longer wipe your real data folder.** Pointed at `~/.sentient`, some of them deleted its tasks, its database or the whole folder before refusing it. Now they all refuse that folder before changing anything, and the `--theme-only` and `--config-only` shortcuts keep a demo home's tasks paused and messaging off unless `--keep-live` is passed.