feat: add conversation-scoped yolo controls

This commit is contained in:
2026-09-22 08:12:07 +00:00
parent b672762be8
commit 18c4b34d08
5 changed files with 218 additions and 2 deletions
+60
View File
@@ -686,6 +686,66 @@ async def resolve_clarify(clarify_id: str, response: str) -> bool:
return bool(ok)
# ═══════════════════════════════════════════════════════════════════════════
# Conversation-scoped YOLO / approval bypass
# ═══════════════════════════════════════════════════════════════════════════
async def yolo_snapshot(conversation_id: str) -> Dict[str, Any]:
"""Return the effective dangerous-command approval bypass for one chat."""
store = _session_store()
session_key = _session_key_for(conversation_id)
session_id = None
if store is not None:
try:
session_id = await asyncio.to_thread(store.peek_session_id, session_key)
except Exception:
logger.debug("[pheby] yolo session lookup failed", exc_info=True)
if not session_id:
return {"ok": False, "code": proto.ERR_CONVERSATION_NOT_FOUND,
"message": "Conversation not found"}
from tools.approval import enable_session_yolo, is_session_yolo_enabled
enabled = is_session_yolo_enabled(session_key)
if not enabled:
db = _session_db()
if db is not None:
try:
meta = await asyncio.to_thread(db.get_session, str(session_id))
from hermes_state import SessionDB
if SessionDB.session_yolo_enabled(meta):
enable_session_yolo(session_key)
enabled = True
except Exception:
logger.debug("[pheby] persisted yolo read failed", exc_info=True)
return {"ok": True, "enabled": bool(enabled), "scope": "conversation",
"conversation_id": conversation_id}
async def set_yolo(conversation_id: str, enabled: bool) -> Dict[str, Any]:
"""Set YOLO for one chat and persist it with that Hermes session."""
store = _session_store()
session_key = _session_key_for(conversation_id)
session_id = None
if store is not None:
try:
session_id = await asyncio.to_thread(store.peek_session_id, session_key)
except Exception:
logger.debug("[pheby] yolo session lookup failed", exc_info=True)
if not session_id:
return {"ok": False, "code": proto.ERR_CONVERSATION_NOT_FOUND,
"message": "Conversation not found"}
from tools.approval import disable_session_yolo, enable_session_yolo
(enable_session_yolo if enabled else disable_session_yolo)(session_key)
db = _session_db()
if db is not None:
try:
await asyncio.to_thread(db.set_session_yolo, str(session_id), bool(enabled))
except Exception:
logger.warning("[pheby] yolo persistence failed", exc_info=True)
return {"ok": True, "enabled": bool(enabled), "scope": "conversation",
"conversation_id": conversation_id}
# ═══════════════════════════════════════════════════════════════════════════
# Models & reasoning
# ═══════════════════════════════════════════════════════════════════════════
+6 -2
View File
@@ -71,6 +71,8 @@ C_MODEL_SET = "model.set"
C_MODEL_CURRENT = "models.current"
C_REASONING_SET = "reasoning.set"
C_REASONING_CURRENT = "reasoning.current"
C_YOLO_SET = "yolo.set"
C_YOLO_CURRENT = "yolo.current"
# ── Server → client message types ────────────────────────────────────────────
S_READY = "ready"
@@ -98,6 +100,8 @@ S_MODEL_CURRENT_SNAPSHOT = "model.current" # reply to models.current
S_MODEL_CHANGED = "model.changed" # after model.set accepted
S_REASONING_SNAPSHOT = "reasoning.snapshot" # reply to reasoning.current
S_REASONING_CHANGED = "reasoning.changed" # after reasoning.set accepted
S_YOLO_SNAPSHOT = "yolo.snapshot" # reply to yolo.current
S_YOLO_CHANGED = "yolo.changed" # after yolo.set accepted
# Reasoning effort levels supported by Hermes (hermes_constants).
REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh", "max", "ultra")
@@ -179,7 +183,7 @@ __all__ = [
"C_CONVERSATION_CREATE", "C_CONVERSATION_RENAME", "C_CONVERSATION_DELETE",
"C_CHAT_SEND", "C_RUN_CANCEL", "C_APPROVAL_RESPOND", "C_CLARIFY_RESPOND",
"C_MODELS_LIST", "C_MODEL_SET", "C_MODEL_CURRENT", "C_REASONING_SET",
"C_REASONING_CURRENT",
"C_REASONING_CURRENT", "C_YOLO_SET", "C_YOLO_CURRENT",
"S_READY", "S_PONG", "S_ERROR", "S_CONVERSATION_SNAPSHOT",
"S_CONVERSATION_CREATED", "S_CONVERSATION_RENAMED", "S_CONVERSATION_UPDATED",
"S_CONVERSATION_DELETED", "S_CONVERSATION_HISTORY", "S_RUN_ACCEPTED",
@@ -188,7 +192,7 @@ __all__ = [
"S_APPROVAL_RESOLVED", "S_CLARIFY_REQUEST", "S_CLARIFY_RESOLVED",
"S_ATTACHMENT_ADDED", "S_MODELS_SNAPSHOT", "S_MODEL_CURRENT_SNAPSHOT",
"S_MODEL_CHANGED", "S_REASONING_SNAPSHOT", "S_REASONING_CHANGED",
"REASONING_EFFORTS",
"S_YOLO_SNAPSHOT", "S_YOLO_CHANGED", "REASONING_EFFORTS",
"now_iso", "new_id", "is_valid_attachment_id", "encode_message",
"decode_message", "error_event", "safe_str",
]
+48
View File
@@ -531,6 +531,52 @@ class PhebyServer:
**({"request_id": request_id} if request_id else {}),
})
async def _handle_yolo_current(self, client, message, request_id):
conversation_id = str(message.get("conversation_id", ""))
if not ConversationRouter.is_valid_conversation_id(conversation_id):
await client.send_json(proto.error_event(
proto.ERR_BAD_REQUEST, "Invalid conversation_id", request_id))
return
result = await self.bridge.yolo_snapshot(conversation_id)
if not result.get("ok"):
await client.send_json(proto.error_event(
result.get("code", proto.ERR_INTERNAL),
result.get("message", "YOLO state unavailable"), request_id))
return
await client.send_json({
"type": proto.S_YOLO_SNAPSHOT,
"enabled": bool(result.get("enabled")),
"scope": result.get("scope", "conversation"),
"conversation_id": conversation_id,
**({"request_id": request_id} if request_id else {}),
})
async def _handle_yolo_set(self, client, message, request_id):
conversation_id = str(message.get("conversation_id", ""))
enabled = message.get("enabled")
if not ConversationRouter.is_valid_conversation_id(conversation_id) \
or not isinstance(enabled, bool):
await client.send_json(proto.error_event(
proto.ERR_BAD_REQUEST,
"conversation_id and boolean enabled are required", request_id))
return
result = await self.bridge.set_yolo(conversation_id, enabled)
if not result.get("ok"):
await client.send_json(proto.error_event(
result.get("code", proto.ERR_INTERNAL),
result.get("message", "YOLO change failed"), request_id))
return
event = {
"type": proto.S_YOLO_CHANGED,
"enabled": bool(result.get("enabled")),
"scope": result.get("scope", "conversation"),
"conversation_id": conversation_id,
**({"request_id": request_id} if request_id else {}),
}
await client.send_json(event)
await self.broadcast({k: v for k, v in event.items()
if k != "request_id"})
async def _handle_models_list(self, client, message, request_id):
conversation_id = message.get("conversation_id")
snapshot = await self.bridge.models_snapshot(
@@ -617,6 +663,8 @@ class PhebyServer:
proto.C_MODEL_CURRENT: _handle_model_current,
proto.C_REASONING_SET: _handle_reasoning_set,
proto.C_REASONING_CURRENT: _handle_reasoning_current,
proto.C_YOLO_SET: _handle_yolo_set,
proto.C_YOLO_CURRENT: _handle_yolo_current,
}