refactor(memory,skills): replace tri-state write_mode with boolean write_approval (default off) (#43354)
The shipped tri-state write_mode (on|off|approve) conflated two concepts —
whether writes are enabled and whether they're gated — so 'on' (writes flow
freely, gate inactive) read like 'gating is on'. Replace it with a single
clear boolean gate that defaults off.
memory.write_approval / skills.write_approval:
false (default) — write freely; the approval gate is off (pre-gate behaviour)
true — require approval: memory foreground prompts inline, memory
background-review + all skill writes stage for review
The old 'off = block all writes' mode is dropped; memory_enabled: false already
disables memory entirely, so a third 'block' state was redundant.
- tools/write_approval.py: get_write_mode/MODE_* → write_approval_enabled() bool;
evaluate_gate() loses the config-driven 'blocked' path (blocked now only comes
from an interactive user denial).
- tools/memory_tool.py, tools/skill_manager_tool.py: comment + behaviour follow.
- hermes_cli/config.py: memory/skills write_mode → write_approval (False);
_config_version 28→29 with a 28→29 migration that renames any persisted
write_mode (approve→true, on/off/unset→false) and drops the old key.
- slash commands: '/memory|/skills mode <on|off|approve>' → 'approval <on|off>'
('mode' kept as a back-compat alias); set_mode_fn callback now takes a bool.
- write_approval_commands.py, cli_commands_mixin.py, gateway/slash_commands.py,
commands.py: handlers + registry args/subcommands updated.
- docs + tests rewritten for the boolean model; added migration tests.
This commit is contained in:
@@ -20,7 +20,10 @@ from typing import List, Optional
|
||||
|
||||
from tools import write_approval as wa
|
||||
|
||||
_VALID_MODES = (wa.MODE_ON, wa.MODE_OFF, wa.MODE_APPROVE)
|
||||
|
||||
def _fmt_state(subsystem: str) -> str:
|
||||
on = wa.write_approval_enabled(subsystem)
|
||||
return f"{subsystem}.write_approval = {'on' if on else 'off'}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -63,18 +66,17 @@ def handle_pending_subcommand(
|
||||
memory_store: live MemoryStore for applying approved memory writes
|
||||
(CLI passes ``self.agent._memory_store``; gateway applies against a
|
||||
freshly loaded store).
|
||||
set_mode_fn: optional callable ``(mode: str) -> None`` that persists the
|
||||
new write_mode to config (gateway provides this; CLI uses its own
|
||||
``save_config_value`` and passes a closure).
|
||||
set_mode_fn: optional callable ``(enabled: bool) -> None`` that
|
||||
persists the new write_approval boolean to config (gateway provides
|
||||
this; CLI uses its own ``save_config_value`` and passes a closure).
|
||||
|
||||
Returns a text string to show the user. Returns None when the args are not
|
||||
a write-approval subcommand (caller falls through to its other handling,
|
||||
e.g. /skills search).
|
||||
"""
|
||||
if not args:
|
||||
# Bare /memory or /skills with no sub → show pending + current mode.
|
||||
mode = wa.get_write_mode(subsystem)
|
||||
return f"{subsystem}.write_mode = {mode}\n\n" + _fmt_pending_list(subsystem)
|
||||
# Bare /memory or /skills with no sub → show pending + gate state.
|
||||
return f"{_fmt_state(subsystem)}\n\n" + _fmt_pending_list(subsystem)
|
||||
|
||||
sub = args[0].lower()
|
||||
rest = args[1:]
|
||||
@@ -91,8 +93,8 @@ def handle_pending_subcommand(
|
||||
if sub == "diff" and subsystem == wa.SKILLS:
|
||||
return _diff(rest)
|
||||
|
||||
if sub == "mode":
|
||||
return _set_mode(subsystem, rest, set_mode_fn)
|
||||
if sub in {"approval", "mode"}: # 'mode' kept as a back-compat alias
|
||||
return _set_approval(subsystem, rest, set_mode_fn)
|
||||
|
||||
return None # not ours — caller handles
|
||||
|
||||
@@ -179,19 +181,29 @@ def _diff(rest: List[str]) -> str:
|
||||
return header + "\n" + diff
|
||||
|
||||
|
||||
def _set_mode(subsystem: str, rest: List[str], set_mode_fn) -> str:
|
||||
def _set_approval(subsystem: str, rest: List[str], set_mode_fn) -> str:
|
||||
"""Turn the approval gate on/off for a subsystem.
|
||||
|
||||
``set_mode_fn`` (when provided) persists the new boolean to config.
|
||||
"""
|
||||
if not rest:
|
||||
cur = wa.get_write_mode(subsystem)
|
||||
return (f"{subsystem}.write_mode = {cur}\n"
|
||||
f"Set with: /{subsystem} mode <on|off|approve>")
|
||||
mode = rest[0].lower()
|
||||
if mode not in _VALID_MODES:
|
||||
return f"Invalid mode '{mode}'. Use: on, off, approve."
|
||||
return (f"{_fmt_state(subsystem)}\n"
|
||||
f"Set with: /{subsystem} approval <on|off>")
|
||||
arg = rest[0].strip().lower()
|
||||
truthy = {"on", "true", "yes", "1", "enable", "enabled"}
|
||||
falsey = {"off", "false", "no", "0", "disable", "disabled"}
|
||||
if arg in truthy:
|
||||
enabled = True
|
||||
elif arg in falsey:
|
||||
enabled = False
|
||||
else:
|
||||
return f"Invalid value '{arg}'. Use: on or off."
|
||||
if set_mode_fn is None:
|
||||
return (f"To change {subsystem} write mode, run:\n"
|
||||
f" hermes config set {subsystem}.write_mode {mode}")
|
||||
val = "true" if enabled else "false"
|
||||
return (f"To change the {subsystem} approval gate, run:\n"
|
||||
f" hermes config set {subsystem}.write_approval {val}")
|
||||
try:
|
||||
set_mode_fn(mode)
|
||||
set_mode_fn(enabled)
|
||||
except Exception as e:
|
||||
return f"Failed to set {subsystem}.write_mode: {e}"
|
||||
return f"{subsystem}.write_mode set to '{mode}'."
|
||||
return f"Failed to set {subsystem}.write_approval: {e}"
|
||||
return f"{subsystem}.write_approval set to '{'on' if enabled else 'off'}'."
|
||||
|
||||
Reference in New Issue
Block a user