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:
Teknium
2026-06-09 21:51:43 -07:00
committed by GitHub
parent fdc90346ea
commit 96af61b6ef
13 changed files with 1270 additions and 11 deletions
+3
View File
@@ -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)
+42
View File
@@ -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