feat: Pheby platform adapter/plugin for Hermes (protocol v1)

Third-party Hermes platform plugin serving the Pheby WebSocket+HTTPS
protocol for the native Android client, behind Caddy:

- Platform adapter (BasePlatformAdapter subclass) registered via the
  documented plugin system (plugins.platforms pattern, kind: platform)
- Multiple conversations mapped to Hermes sessions (authoritative state)
- Streaming chat via GatewayStreamConsumer edit path (message.delta)
- Structured tool events (never fake tool text in chat), incl.
  post_tool_call hook relay with Hermes tool_call_ids
- Native approval + clarification round-trips via tools.approval /
  tools.clarify_gateway primitives
- Attachment delivery: adapter-owned copies, opaque IDs, persistent
  metadata, 7-day retention + safe cleanup, authenticated HTTPS download
- Model + reasoning-effort query/change via Hermes picker data and
  session/global overrides
- Shared-secret auth (constant-time, lockout), size limits, path-
  traversal-proof attachment resolution, minimal health endpoint
- Protocol v1 spec with JSON examples (docs/PROTOCOL.md)
- Install/config/Caddy/security docs + discovered Hermes limitations
- 37 passing tests (auth, conversations, protocol, attachments incl.
  expiry/traversal, tool events, approvals, clarifications, cancel,
  reconnect re-sync, live WS smoke tests) — gateway-free fakes

No Hermes core modifications required.
This commit is contained in:
2026-09-02 19:31:17 +00:00
parent 31c16de665
commit 53ffdb8aa2
19 changed files with 4688 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
"""Pheby — Hermes Agent platform adapter/plugin for the Pheby Android client.
A third-party Hermes platform plugin. Install this package directory as
``~/.hermes/plugins/pheby/`` (HERMES_HOME/plugins/pheby), enable it in
config.yaml (``plugins.enabled: [pheby]``, ``platforms.pheby.enabled: true``),
set ``PHEBY_SECRET`` in ``~/.hermes/.env``, and restart the gateway.
"""
from __future__ import annotations
import logging
import os
from typing import Any
logger = logging.getLogger(__name__)
__version__ = "1.0.0"
def register(ctx: Any) -> None:
"""Plugin entry point — called by the Hermes plugin system at startup."""
from .adapter import PhebyAdapter
from .config import (
check_requirements,
env_enablement,
is_connected,
validate_config,
)
adapter_holder: dict = {"adapter": None}
def _factory(cfg: Any) -> PhebyAdapter:
adapter = PhebyAdapter(cfg)
adapter_holder["adapter"] = adapter
return adapter
ctx.register_platform(
name="pheby",
label="Pheby",
adapter_factory=_factory,
check_fn=check_requirements,
validate_config=validate_config,
is_connected=is_connected,
required_env=["PHEBY_SECRET"],
install_hint="pip install aiohttp # already a Hermes dependency; "
"set PHEBY_SECRET in ~/.hermes/.env",
env_enablement_fn=env_enablement,
# Home channel for cron / notification delivery when configured.
cron_deliver_env_var="PHEBY_HOME_CHANNEL",
allowed_users_env="PHEBY_ALLOWED_USERS",
allow_all_env="PHEBY_ALLOW_ALL_USERS",
emoji="🐱",
pii_safe=True, # single-user private platform; no PII in routing IDs
allow_update_command=True,
platform_hint=(
"You are communicating with the user via Pheby, a private "
"native Android client over WebSocket. Respond in normal "
"markdown; the client renders it natively. Attachments you "
"produce via MEDIA: tags are delivered as downloadable files "
"and inline image previews."
),
)
# post_tool_call observer → structured tool-result events. Registered
# against the plugin context so it loads with the plugin, before any
# adapter is constructed (the hook is a no-op until the adapter serves).
def _post_tool_call(**kwargs: Any) -> None:
adapter = adapter_holder.get("adapter")
if adapter is not None:
adapter.on_post_tool_call(**kwargs)
try:
ctx.register_hook("post_tool_call", _post_tool_call)
except Exception:
logger.debug("[pheby] post_tool_call hook registration failed",
exc_info=True)
logger.info("[pheby] plugin registered (platform 'pheby')")
__all__ = ["register", "__version__"]