Fix Hermes adapter integration and recovery

This commit is contained in:
Codex
2026-09-02 13:59:00 -07:00
parent 53ffdb8aa2
commit 5130152335
10 changed files with 900 additions and 357 deletions
+73 -20
View File
@@ -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