Fix Hermes adapter integration and recovery
This commit is contained in:
+73
-20
@@ -14,8 +14,10 @@ Hermes integration (runs, approvals, clarifications, models) lives in
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import ipaddress
|
||||
import logging
|
||||
import time
|
||||
from urllib.parse import quote
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
@@ -153,13 +155,17 @@ class PhebyServer:
|
||||
"message": "Attachment unavailable"}},
|
||||
status=404)
|
||||
desc = self.store.describe(attachment_id) or {}
|
||||
safe_name = desc.get("filename", "file.bin")
|
||||
safe_name = str(desc.get("filename", "file.bin"))
|
||||
ascii_name = safe_name.encode("ascii", "replace").decode("ascii") \
|
||||
.replace('"', "_").replace("\\", "_")
|
||||
disposition = (f'attachment; filename="{ascii_name}"; '
|
||||
f"filename*=UTF-8''{quote(safe_name)}")
|
||||
logger.info("[pheby] attachment download: id=%s bytes=%s",
|
||||
attachment_id, desc.get("size"))
|
||||
return web.FileResponse(
|
||||
blob,
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{safe_name}"',
|
||||
"Content-Disposition": disposition,
|
||||
"Content-Type": desc.get("mime_type",
|
||||
"application/octet-stream"),
|
||||
},
|
||||
@@ -173,7 +179,7 @@ class PhebyServer:
|
||||
self._conn_counter += 1
|
||||
conn_id = f"c{self._conn_counter}"
|
||||
|
||||
peer = request.remote or "unknown"
|
||||
peer = self._auth_peer(request)
|
||||
if self._is_locked_out(peer):
|
||||
logger.warning("[pheby] auth lockout active for %s — refusing",
|
||||
peer)
|
||||
@@ -224,6 +230,21 @@ class PhebyServer:
|
||||
def _record_auth_failure(self, peer: str) -> None:
|
||||
self._auth_failures.setdefault(peer, []).append(time.time())
|
||||
|
||||
@staticmethod
|
||||
def _auth_peer(request: web.Request) -> str:
|
||||
"""Use Caddy's client IP only when the direct peer is loopback."""
|
||||
direct = request.remote or "unknown"
|
||||
try:
|
||||
if not ipaddress.ip_address(direct).is_loopback:
|
||||
return direct
|
||||
except ValueError:
|
||||
return direct
|
||||
forwarded = request.headers.get("X-Forwarded-For", "").split(",", 1)[0].strip()
|
||||
try:
|
||||
return str(ipaddress.ip_address(forwarded)) if forwarded else direct
|
||||
except ValueError:
|
||||
return direct
|
||||
|
||||
async def _authenticate(self, client: ClientConnection,
|
||||
peer: str) -> bool:
|
||||
"""Wait for the hello frame and validate the shared secret."""
|
||||
@@ -253,7 +274,15 @@ class PhebyServer:
|
||||
proto.ERR_UNAUTHORIZED, "Invalid secret"))
|
||||
return False
|
||||
requested = message.get("protocol_version")
|
||||
if requested is not None and int(requested) != proto.PROTOCOL_VERSION:
|
||||
try:
|
||||
requested_version = (proto.PROTOCOL_VERSION if requested is None
|
||||
else int(requested))
|
||||
except (TypeError, ValueError):
|
||||
await client.send_json(proto.error_event(
|
||||
proto.ERR_VERSION_MISMATCH,
|
||||
"protocol_version must be an integer"))
|
||||
return False
|
||||
if requested_version != proto.PROTOCOL_VERSION:
|
||||
await client.send_json(proto.error_event(
|
||||
proto.ERR_VERSION_MISMATCH,
|
||||
f"Protocol version mismatch: server={proto.PROTOCOL_VERSION}, "
|
||||
@@ -343,17 +372,20 @@ class PhebyServer:
|
||||
proto.ERR_CONVERSATION_NOT_FOUND,
|
||||
"Conversation not found", request_id))
|
||||
return
|
||||
runtime = self.bridge.runtime_snapshot(conversation_id)
|
||||
await client.send_json({
|
||||
"type": proto.S_CONVERSATION_HISTORY,
|
||||
"conversation_id": conversation_id,
|
||||
"messages": history,
|
||||
"attachments": self.store.list_for_conversation(conversation_id),
|
||||
**runtime,
|
||||
**({"request_id": request_id} if request_id else {}),
|
||||
})
|
||||
|
||||
async def _handle_conversation_create(self, client, message, request_id):
|
||||
name = message.get("name")
|
||||
cid = await self.router.new_conversation(
|
||||
str(name) if name else None)
|
||||
cid = await self.bridge.create_conversation(
|
||||
self, str(name) if name else None)
|
||||
await client.send_json({
|
||||
"type": proto.S_CONVERSATION_CREATED,
|
||||
"conversation_id": cid,
|
||||
@@ -375,7 +407,7 @@ class PhebyServer:
|
||||
proto.ERR_BAD_REQUEST,
|
||||
"conversation_id and name (≤200 chars) required", request_id))
|
||||
return
|
||||
ok = await self.router.rename(conversation_id, name)
|
||||
ok = await self.bridge.rename_conversation(conversation_id, name)
|
||||
if not ok:
|
||||
await client.send_json(proto.error_event(
|
||||
proto.ERR_CONVERSATION_NOT_FOUND, "Conversation not found",
|
||||
@@ -428,17 +460,30 @@ class PhebyServer:
|
||||
proto.ERR_TOO_LARGE,
|
||||
f"text exceeds {proto.MAX_TEXT_CHARS} chars", request_id))
|
||||
return
|
||||
await self.bridge.send_chat(conversation_id, text, client, request_id)
|
||||
await self.bridge.send_chat(
|
||||
self, conversation_id, text, client, request_id)
|
||||
|
||||
async def _handle_run_cancel(self, client, message, request_id):
|
||||
conversation_id = str(message.get("conversation_id", ""))
|
||||
run_id = message.get("run_id")
|
||||
ok = await self.bridge.cancel_run(conversation_id, run_id)
|
||||
await client.send_json({
|
||||
"type": proto.S_RUN_FINISHED if ok else proto.S_ERROR,
|
||||
**({"run_id": run_id, "status": "cancelled"}
|
||||
if ok else {"error": {"code": proto.ERR_NOT_FOUND,
|
||||
"message": "No active run"}}),
|
||||
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
|
||||
requested_run_id = message.get("run_id")
|
||||
active = self.bridge.active_run(conversation_id)
|
||||
run_id = active.get("run_id") if active else requested_run_id
|
||||
ok = await self.bridge.cancel_run(conversation_id, requested_run_id)
|
||||
if not ok:
|
||||
await client.send_json(proto.error_event(
|
||||
proto.ERR_NOT_FOUND, "No matching active run", request_id))
|
||||
return
|
||||
if self.adapter is not None:
|
||||
self.adapter._drafts.pop(conversation_id, None)
|
||||
await self.broadcast({
|
||||
"type": proto.S_RUN_FINISHED,
|
||||
"conversation_id": conversation_id,
|
||||
"run_id": run_id,
|
||||
"status": "cancelled",
|
||||
**({"request_id": request_id} if request_id else {}),
|
||||
})
|
||||
|
||||
@@ -453,10 +498,11 @@ class PhebyServer:
|
||||
proto.ERR_APPROVAL_NOT_FOUND,
|
||||
"Unknown or already-resolved approval", request_id))
|
||||
return
|
||||
await client.send_json({
|
||||
await self.broadcast({
|
||||
"type": proto.S_APPROVAL_RESOLVED,
|
||||
"approval_id": approval_id,
|
||||
"choice": choice,
|
||||
"accepted": True,
|
||||
**({"request_id": request_id} if request_id else {}),
|
||||
})
|
||||
|
||||
@@ -470,21 +516,26 @@ class PhebyServer:
|
||||
proto.ERR_CLARIFY_NOT_FOUND,
|
||||
"Unknown or already-resolved clarification", request_id))
|
||||
return
|
||||
await client.send_json({
|
||||
await self.broadcast({
|
||||
"type": proto.S_CLARIFY_RESOLVED,
|
||||
"clarify_id": clarify_id,
|
||||
"accepted": True,
|
||||
**({"request_id": request_id} if request_id else {}),
|
||||
})
|
||||
|
||||
async def _handle_models_list(self, client, message, request_id):
|
||||
snapshot = await self.bridge.models_snapshot()
|
||||
conversation_id = message.get("conversation_id")
|
||||
snapshot = await self.bridge.models_snapshot(
|
||||
str(conversation_id) if conversation_id else None)
|
||||
snapshot["type"] = proto.S_MODELS_SNAPSHOT
|
||||
if request_id:
|
||||
snapshot["request_id"] = request_id
|
||||
await client.send_json(snapshot)
|
||||
|
||||
async def _handle_model_current(self, client, message, request_id):
|
||||
snapshot = await self.bridge.current_model_snapshot()
|
||||
conversation_id = message.get("conversation_id")
|
||||
snapshot = await self.bridge.current_model_snapshot(
|
||||
str(conversation_id) if conversation_id else None)
|
||||
snapshot["type"] = proto.S_MODEL_CURRENT_SNAPSHOT
|
||||
if request_id:
|
||||
snapshot["request_id"] = request_id
|
||||
@@ -514,7 +565,9 @@ class PhebyServer:
|
||||
if k != "request_id"})
|
||||
|
||||
async def _handle_reasoning_current(self, client, message, request_id):
|
||||
snapshot = await self.bridge.reasoning_snapshot()
|
||||
conversation_id = message.get("conversation_id")
|
||||
snapshot = await self.bridge.reasoning_snapshot(
|
||||
str(conversation_id) if conversation_id else None)
|
||||
snapshot["type"] = proto.S_REASONING_SNAPSHOT
|
||||
if request_id:
|
||||
snapshot["request_id"] = request_id
|
||||
|
||||
Reference in New Issue
Block a user