feat(memory,skills): approve/deny gate for memory + skill writes (#38199)
Adds memory.write_mode and skills.write_mode (on|off|approve), applied to both foreground turns and the background self-improvement review fork — the source of the unprompted 'wrong assumption' saves users reported. - on (default): write freely, unchanged behaviour - off: never write; the tool returns a clean disabled result - approve: don't commit. Memory foreground writes prompt inline (small, reviewable in a chat bubble); background memory writes and ALL skill writes stage to a pending store instead (a SKILL.md is too large to review inline, and a daemon thread can't block on a prompt) Review staged writes from CLI or any messaging platform: /memory pending|approve|reject|mode /skills pending|approve|reject|diff|mode Skill review respects the size asymmetry: inline you see a one-line gist; the full unified diff stays out-of-band (/skills diff, dashboard, or the staged JSON file). New: tools/write_approval.py (gate + pending store), hermes_cli/ write_approval_commands.py (shared CLI+gateway handlers). Gates wired at the single entry points memory_tool() and skill_manage(), using the existing write-origin ContextVar to distinguish foreground from background_review.
This commit is contained in:
@@ -7056,6 +7056,9 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
||||
if canonical == "reasoning":
|
||||
return await self._handle_reasoning_command(event)
|
||||
|
||||
if canonical == "memory":
|
||||
return await self._handle_memory_command(event)
|
||||
|
||||
if canonical == "fast":
|
||||
return await self._handle_fast_command(event)
|
||||
|
||||
|
||||
@@ -1954,6 +1954,48 @@ class GatewaySlashCommandsMixin:
|
||||
self._evict_cached_agent(session_key)
|
||||
return t("gateway.reasoning.set_session", effort=effort)
|
||||
|
||||
async def _handle_memory_command(self, event: MessageEvent) -> str:
|
||||
"""Handle /memory — review pending memory writes + set write mode.
|
||||
|
||||
Memory entries are small enough to review inline in a chat bubble, so
|
||||
the full pending/approve/reject/mode flow works on every platform.
|
||||
Mode changes persist to config.yaml and evict the cached agent so the
|
||||
new write_mode takes effect on the next message.
|
||||
"""
|
||||
from gateway.run import _hermes_home
|
||||
from hermes_cli.write_approval_commands import handle_pending_subcommand
|
||||
from tools import write_approval as wa
|
||||
from tools.memory_tool import MemoryStore
|
||||
|
||||
raw_args = event.get_command_args().strip()
|
||||
args = raw_args.split() if raw_args else []
|
||||
session_key = self._session_key_for_source(event.source)
|
||||
config_path = _hermes_home / "config.yaml"
|
||||
|
||||
def _set_mode(mode: str):
|
||||
import yaml
|
||||
user_config = {}
|
||||
if config_path.exists():
|
||||
with open(config_path, encoding="utf-8") as f:
|
||||
user_config = yaml.safe_load(f) or {}
|
||||
user_config.setdefault("memory", {})["write_mode"] = mode
|
||||
atomic_yaml_write(config_path, user_config)
|
||||
# New write_mode must take effect next message → drop cached agent.
|
||||
self._evict_cached_agent(session_key)
|
||||
|
||||
# Apply approved writes against a fresh on-disk store (the gateway has
|
||||
# no long-lived agent; the store persists to the same MEMORY/USER.md).
|
||||
store = MemoryStore()
|
||||
store.load_from_disk()
|
||||
|
||||
out = handle_pending_subcommand(
|
||||
wa.MEMORY, args, memory_store=store, set_mode_fn=_set_mode,
|
||||
)
|
||||
if out is None:
|
||||
out = ("Unknown /memory subcommand. Use: pending, approve <id>, "
|
||||
"reject <id>, mode <on|off|approve>.")
|
||||
return out
|
||||
|
||||
async def _handle_fast_command(self, event: MessageEvent) -> str:
|
||||
"""Handle /fast — mirror the CLI Priority Processing toggle in gateway chats."""
|
||||
from gateway.run import _hermes_home, _load_gateway_config, _resolve_gateway_model
|
||||
|
||||
Reference in New Issue
Block a user