feat(setup): Blank Slate setup mode — minimal agent, opt in to everything (#36733)

* feat(setup): Blank Slate setup mode — minimal agent, opt in to everything

Adds a third first-time setup option alongside Quick Setup and Full Setup.
Blank Slate forces ON only what an agent needs to run — provider & model,
the File Operations toolset, and the Terminal toolset — and turns
everything else OFF, then walks the user through opting each capability
back in.

What it does:
- platform_toolsets.cli = [file, terminal] (explicit, authoritative list)
- agent.disabled_toolsets = every other known toolset (web, browser,
  code_execution, vision, memory, delegation, cronjob, skills, image_gen,
  kanban, …). Applied last in the resolver, so it overrides the
  non-configurable platform-toolset recovery that would otherwise re-add
  toolsets like kanban — guaranteeing a true blank slate.
- Optional config features off: compression, memory + user-profile capture,
  checkpoints, smart model routing, auto session reset.
- Bundled skills default to NONE (reuses the .no-bundled-skills marker);
  offers to seed the full catalog.
- Walks through tools / plugins / MCP / messaging, all opt-in.

Proven end-to-end: with the Blank Slate config, model_tools.get_tool_definitions
emits exactly 6 schemas — patch, process, read_file, search_files, terminal,
write_file. Nothing else reaches the model.

Re-enable later via hermes tools / hermes skills opt-in --sync /
hermes setup agent.

Tests: tests/hermes_cli/test_setup_blank_slate.py (8 tests) pin the writers,
the resolver invariant ({file, terminal}), and the 6-schema end-to-end set.
Docs: getting-started/quickstart.md documents all three setup modes.

* feat(setup): Blank Slate fork — finish minimal, or walk through configs

After applying the minimal baseline (provider/model + file + terminal,
everything else off), Blank Slate now presents a choice instead of always
running the full walkthrough:

  1. Start with everything disabled — finish now with the minimal agent.
  2. Walk through all configurations — opt in to tools, skills, plugins, MCP,
     and messaging.

Provider/model and terminal are still configured first either way (the agent
can't run without them). The finish-now path records the bundled-skill opt-out
so future `hermes update` runs don't re-inject skills. The walkthrough body
moved to a separate _blank_slate_walkthrough() helper.

Tests: TestBlankSlateFork covers both branches (finish-now applies baseline +
skill opt-out and skips the walkthrough; walkthrough path invokes it). Docs
updated to describe the fork.
This commit is contained in:
Teknium 2026-06-20 10:45:55 -07:00 committed by GitHub
parent 838daca9f4
commit 11c6f4c7bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 376 additions and 0 deletions

View file

@ -2855,6 +2855,7 @@ def run_setup_wizard(args):
[
"Quick Setup (Nous Portal) — free OAuth login, no API keys, model + tools (recommended)",
"Full setup — configure every provider, tool & option yourself (bring your own keys)",
"Blank Slate — everything off except the bare minimum; opt in to each capability",
],
0,
)
@ -2862,6 +2863,9 @@ def run_setup_wizard(args):
if setup_mode == 0:
_run_first_time_quick_setup(config, hermes_home, is_existing)
return
if setup_mode == 2:
_run_blank_slate_setup(config, hermes_home, is_existing)
return
# ── Full Setup — run all sections ──
print_header("Configuration Location")
@ -2982,6 +2986,237 @@ def _run_first_time_quick_setup(config: dict, hermes_home, is_existing: bool):
_print_setup_summary(config, hermes_home)
def _blank_slate_minimal_toolsets(config: dict):
"""Write the minimal toolset state for a Blank Slate install.
Only ``file`` and ``terminal`` are enabled. Two layers enforce this:
1. ``platform_toolsets["cli"] = ["file", "terminal"]`` an explicit list of
configurable keys, which the resolver treats as authoritative
(``has_explicit_config``) so default toolsets aren't re-expanded.
2. ``agent.disabled_toolsets`` a global hard-suppression list (applied last
in ``_get_platform_tools``, overriding every other path including the
non-configurable platform-toolset recovery that would otherwise re-add
toolsets like ``kanban``). We list every known toolset except the two we
keep, guaranteeing a true blank slate regardless of platform/recovery
quirks. The user re-enables any of them later via ``hermes tools`` (which
rewrites ``platform_toolsets``) or by editing ``agent.disabled_toolsets``.
"""
keep = {"file", "terminal"}
config.setdefault("platform_toolsets", {})["cli"] = sorted(keep)
try:
from toolsets import TOOLSETS
from hermes_cli.tools_config import CONFIGURABLE_TOOLSETS, _get_plugin_toolset_keys
all_keys = set()
all_keys.update(k for k, _, _ in CONFIGURABLE_TOOLSETS)
all_keys.update(_get_plugin_toolset_keys())
# Plain (non-composite) TOOLSETS entries — catches recovered toolsets
# like ``kanban`` that aren't in CONFIGURABLE_TOOLSETS but get re-added.
for k, tdef in TOOLSETS.items():
if k.startswith("hermes-"):
continue # platform composites — not user-facing toolsets
if isinstance(tdef, dict) and tdef.get("includes"):
continue # composite groupings, not leaf toolsets
all_keys.add(k)
disabled = sorted(all_keys - keep)
if disabled:
config.setdefault("agent", {})["disabled_toolsets"] = disabled
except Exception as exc:
logger.debug("blank-slate disabled_toolsets computation skipped: %s", exc)
def _blank_slate_minimize_config(config: dict):
"""Turn OFF the optional config features for a Blank Slate install.
Everything here is opt-in afterwards via ``hermes setup agent`` /
``hermes config set``. We keep only what's needed to run.
"""
config.setdefault("agent", {})["max_turns"] = 90
# Compression off — minimal footprint; user opts in if they want long sessions.
config.setdefault("compression", {})["enabled"] = False
# No automatic memory / user-profile capture.
mem = config.setdefault("memory", {})
mem["memory_enabled"] = False
mem["user_profile_enabled"] = False
# No filesystem checkpoints, no smart model routing, no auto session reset.
config.setdefault("checkpoints", {})["enabled"] = False
config.setdefault("smart_model_routing", {})["enabled"] = False
config.setdefault("session_reset", {})["mode"] = "none"
# Quiet, minimal display.
config.setdefault("display", {})["tool_progress"] = "all"
def _run_blank_slate_setup(config: dict, hermes_home, is_existing: bool):
"""Blank Slate setup — start with everything off except the bare minimum.
Forces only the essentials to run an agent (provider + model, the file and
terminal toolsets) and turns every other tool/skill/plugin/MCP/config
feature OFF. After applying that minimal baseline, the user chooses one of
two paths:
1. Start with everything disabled finish now with the minimal agent.
2. Walk through every configuration opt each capability back in.
Either way nothing is enabled that the user did not explicitly choose.
"""
from hermes_cli.config import load_config
print()
print_header("Blank Slate Setup")
print_info("Everything starts OFF. First we force-enable only what's required")
print_info("to run an agent, then you choose whether to stop there or walk")
print_info("through enabling more — opting in to exactly what you want.")
print_info("")
print_info("Forced on: Provider & Model, File Operations, Terminal.")
print_info("Everything else (web, browser, code exec, vision, memory,")
print_info("delegation, cron, skills, plugins, MCP, …) starts disabled.")
print()
# ── Step 1: Provider & Model (REQUIRED — the agent cannot run without it) ──
print_header("Step 1 — Provider & Model (required)")
setup_model_provider(config)
save_config(config)
# ── Step 2: Terminal backend (where commands run — a core decision) ──
print_header("Step 2 — Terminal Backend")
setup_terminal_backend(config)
# ── Step 3: Lock in the minimal toolset + minimized config knobs ──
_blank_slate_minimal_toolsets(config)
_blank_slate_minimize_config(config)
save_config(config)
print()
print_success("Minimal baseline applied:")
print_info(" Toolsets: file, terminal (everything else off)")
print_info(" Compression, memory, checkpoints, smart routing: off")
# ── The fork: stop here, or walk through enabling things ──
print()
print_header("How far do you want to go?")
path = prompt_choice(
"Your minimal agent is ready. What next?",
[
"Start with everything disabled — finish now (most minimal)",
"Walk through all configurations — opt in to tools, skills, plugins, MCP",
],
0,
)
if path == 0:
save_config(config)
# Blank Slate means no bundled skills; record the opt-out so future
# `hermes update` runs don't re-inject them.
try:
from tools.skills_sync import set_bundled_skills_opt_out
set_bundled_skills_opt_out(True)
except Exception as exc:
logger.debug("blank-slate skill opt-out error: %s", exc)
print()
print_success("Blank Slate setup complete — minimal agent ready.")
print_info("Enable anything later, on demand:")
print_info(" Enable tools: hermes tools")
print_info(" Seed skills: hermes skills opt-in --sync")
print_info(" Add MCP servers: hermes mcp add")
print_info(" Enable plugins: hermes plugins")
print_info(" Tune agent settings: hermes setup agent")
print()
_print_setup_summary(config, hermes_home)
return
# ── Walkthrough path — opt in to each capability ──
_blank_slate_walkthrough(config, hermes_home)
def _blank_slate_walkthrough(config: dict, hermes_home):
"""Opt-in walkthrough for Blank Slate: skills, tools, plugins, MCP, gateway."""
from hermes_cli.config import load_config
# ── Bundled skills — default to NONE, offer to seed all ──
print()
print_header("Bundled Skills")
print_info("Blank Slate ships with NO bundled skills by default.")
seed_skills = prompt_yes_no(
"Seed the full bundled skill catalog? (No = start with zero skills)",
default=False,
)
try:
from tools.skills_sync import set_bundled_skills_opt_out, sync_skills
if seed_skills:
# Make sure no stale opt-out marker blocks the seed, then sync.
set_bundled_skills_opt_out(False)
result = sync_skills(quiet=True)
copied = len(result.get("copied", [])) if isinstance(result, dict) else 0
print_success(f"Seeded {copied} bundled skills.")
else:
set_bundled_skills_opt_out(True)
print_info("No skills seeded. A .no-bundled-skills marker keeps future")
print_info("`hermes update` runs from re-injecting them. Opt back in any")
print_info("time with `hermes skills opt-in --sync`.")
except Exception as exc:
logger.debug("blank-slate skill handling error: %s", exc)
print_warning(f"Skill setup step encountered an error: {exc}")
# ── Walk through enabling additional tools ──
print()
print_header("Tools")
print_info("Pick exactly which additional toolsets to turn on.")
print_info("(file and terminal are already on; leave the rest off if you want")
print_info(" the most minimal agent.)")
if prompt_yes_no("Open the tool selector to enable more tools?", default=False):
try:
from hermes_cli.tools_config import tools_command
tools_command(first_install=False, config=config)
# tools_command saves via its own load/save cycle — re-sync.
_refreshed = load_config()
config.clear()
config.update(_refreshed)
except Exception as exc:
logger.debug("blank-slate tools_command error: %s", exc)
print_warning(f"Tool selector encountered an error: {exc}")
else:
print_info("Keeping the minimal toolset. Add tools later with `hermes tools`.")
# ── Built-in plugins (off unless chosen) ──
print()
print_header("Plugins")
if prompt_yes_no("Review and enable built-in plugins now?", default=False):
print_info("Manage plugins with `hermes plugins list` / `hermes plugins install`.")
else:
print_info("No plugins enabled. Add later with `hermes plugins`.")
# ── MCP servers (off unless chosen) ──
print()
print_header("MCP Servers")
if prompt_yes_no("Add an MCP server now?", default=False):
print_info("Add servers with `hermes mcp add <name> --url ... | --command ...`.")
else:
print_info("No MCP servers configured. Add later with `hermes mcp add`.")
# ── Optional messaging gateway ──
print()
if prompt_yes_no("Connect a messaging platform (Telegram, Discord, …)?", default=False):
setup_gateway(config)
save_config(config)
print()
print_success("Blank Slate setup complete — minimal agent ready.")
print_info(" Enable more tools: hermes tools")
print_info(" Seed skills: hermes skills opt-in --sync")
print_info(" Add MCP servers: hermes mcp add")
print_info(" Tune agent settings: hermes setup agent")
print()
_print_setup_summary(config, hermes_home)
def _run_quick_setup(config: dict, hermes_home):
"""Quick setup — only configure items that are missing."""
from hermes_cli.config import (

View file

@ -0,0 +1,131 @@
"""Tests for Blank Slate setup mode (hermes_cli/setup.py).
Blank Slate is the third first-time setup option: everything off except the
bare minimum needed to run an agent (provider/model + file + terminal). These
tests pin the config the writers produce and the invariant that the toolset
resolver + tool-schema builder yield exactly the file/terminal tools.
"""
import pytest
from hermes_cli.setup import (
_blank_slate_minimal_toolsets,
_blank_slate_minimize_config,
)
class TestBlankSlateMinimalToolsets:
def test_only_file_and_terminal_enabled_for_cli(self):
cfg = {}
_blank_slate_minimal_toolsets(cfg)
assert cfg["platform_toolsets"]["cli"] == ["file", "terminal"]
def test_disabled_toolsets_excludes_kept_and_covers_known(self):
cfg = {}
_blank_slate_minimal_toolsets(cfg)
disabled = set(cfg["agent"]["disabled_toolsets"])
# The two kept toolsets must NOT be in the disabled list.
assert "file" not in disabled
assert "terminal" not in disabled
# A representative spread of capabilities must be suppressed.
for ts in ("web", "browser", "code_execution", "vision", "memory",
"delegation", "cronjob", "skills", "image_gen"):
assert ts in disabled
# The recovered non-configurable toolset that used to leak is suppressed.
assert "kanban" in disabled
def test_resolver_yields_exactly_file_and_terminal(self):
from hermes_cli.tools_config import _get_platform_tools
cfg = {}
_blank_slate_minimal_toolsets(cfg)
_blank_slate_minimize_config(cfg)
resolved = set(_get_platform_tools(cfg, "cli"))
assert resolved == {"file", "terminal"}
def test_tool_schema_builder_yields_only_file_and_terminal_tools(self):
# End-to-end: the exact schema set the agent would send to the model.
import model_tools
from hermes_cli.tools_config import _get_platform_tools
cfg = {}
_blank_slate_minimal_toolsets(cfg)
_blank_slate_minimize_config(cfg)
enabled = sorted(_get_platform_tools(cfg, "cli"))
defs = model_tools.get_tool_definitions(
enabled_toolsets=enabled, disabled_toolsets=None, quiet_mode=True
)
names = sorted(
{(d.get("function") or {}).get("name") or d.get("name") for d in defs}
)
assert names == ["patch", "process", "read_file", "search_files",
"terminal", "write_file"]
class TestBlankSlateMinimizeConfig:
def test_optional_features_turned_off(self):
cfg = {}
_blank_slate_minimize_config(cfg)
assert cfg["compression"]["enabled"] is False
assert cfg["memory"]["memory_enabled"] is False
assert cfg["memory"]["user_profile_enabled"] is False
assert cfg["checkpoints"]["enabled"] is False
assert cfg["smart_model_routing"]["enabled"] is False
assert cfg["session_reset"]["mode"] == "none"
def test_does_not_clobber_unrelated_keys(self):
cfg = {"model": {"provider": "openrouter", "default": "x/y"}}
_blank_slate_minimize_config(cfg)
# Model config is untouched by the minimizer.
assert cfg["model"]["provider"] == "openrouter"
assert cfg["model"]["default"] == "x/y"
class TestBlankSlateFork:
"""The post-baseline fork: finish now vs walk through configurations."""
def _patch_common(self, monkeypatch):
import hermes_cli.setup as s
# Neutralize side-effecting setup steps and I/O.
monkeypatch.setattr(s, "setup_model_provider", lambda cfg, **k: None)
monkeypatch.setattr(s, "setup_terminal_backend", lambda cfg, **k: None)
monkeypatch.setattr(s, "save_config", lambda cfg: None)
monkeypatch.setattr(s, "_print_setup_summary", lambda cfg, home: None)
monkeypatch.setattr(s, "print_header", lambda *a, **k: None)
monkeypatch.setattr(s, "print_info", lambda *a, **k: None)
monkeypatch.setattr(s, "print_success", lambda *a, **k: None)
monkeypatch.setattr(s, "print_warning", lambda *a, **k: None)
def test_finish_now_skips_walkthrough(self, monkeypatch, tmp_path):
import hermes_cli.setup as s
self._patch_common(monkeypatch)
# Fork prompt returns 0 = finish now.
monkeypatch.setattr(s, "prompt_choice", lambda *a, **k: 0)
walked = {"called": False}
monkeypatch.setattr(s, "_blank_slate_walkthrough",
lambda cfg, home: walked.__setitem__("called", True))
opted_out = {"value": None}
monkeypatch.setattr("tools.skills_sync.set_bundled_skills_opt_out",
lambda enabled: opted_out.__setitem__("value", enabled))
cfg = {}
s._run_blank_slate_setup(cfg, tmp_path, is_existing=False)
# Minimal baseline was applied, walkthrough was NOT run.
assert cfg["platform_toolsets"]["cli"] == ["file", "terminal"]
assert walked["called"] is False
# Finish-now path records the skill opt-out (no bundled skills).
assert opted_out["value"] is True
def test_walkthrough_path_invokes_walkthrough(self, monkeypatch, tmp_path):
import hermes_cli.setup as s
self._patch_common(monkeypatch)
# Fork prompt returns 1 = walk through.
monkeypatch.setattr(s, "prompt_choice", lambda *a, **k: 1)
walked = {"called": False}
monkeypatch.setattr(s, "_blank_slate_walkthrough",
lambda cfg, home: walked.__setitem__("called", True))
cfg = {}
s._run_blank_slate_setup(cfg, tmp_path, is_existing=False)
assert cfg["platform_toolsets"]["cli"] == ["file", "terminal"]
assert walked["called"] is True

View file

@ -95,6 +95,16 @@ hermes setup --portal
That logs you in, sets Nous as your provider, and turns on the Tool Gateway in one command.
:::
:::info Setup modes
On a fresh install, `hermes setup` offers three modes:
- **Quick Setup (Nous Portal)** — free OAuth login, no API keys; sets up a model plus the Tool Gateway tools. The recommended fast path.
- **Full Setup** — walk through every provider, tool, and option yourself (bring your own keys).
- **Blank Slate** — everything starts **off** except the bare minimum needed to run an agent: **provider & model, the File Operations toolset, and the Terminal toolset**. No web, browser, code execution, vision, memory, delegation, cron, skills, plugins, or MCP servers — and compression, checkpoints, smart routing, and memory capture are all disabled. After the minimal baseline is applied, you choose one of two paths: **start with everything disabled** (finish now with the minimal agent), or **walk through all configurations** (opt in to tools, skills, plugins, MCP, and messaging). Pick this when you want a minimal, fully-controlled agent and intend to enable only exactly what you need.
Blank Slate writes an explicit `platform_toolsets.cli` list plus `agent.disabled_toolsets`, so nothing you didn't choose ever loads — not even after `hermes update`. Re-enable anything later with `hermes tools`, seed skills with `hermes skills opt-in --sync`, or tune settings with `hermes setup agent`.
:::
Good defaults:
| Provider | What it is | How to set up |