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:
@@ -0,0 +1,408 @@
|
||||
# Pheby Protocol v1 — Specification
|
||||
|
||||
JSON messages over WebSocket, plus one authenticated HTTPS endpoint for
|
||||
attachment downloads. Every message (both directions) carries a `"type"`.
|
||||
Client→server requests MAY carry a `"request_id"` (any client-chosen string);
|
||||
the direct reply echoes it. Server→client events are broadcast to all
|
||||
authenticated connections (single-user app — typically one client).
|
||||
|
||||
Timestamps (`"ts"`) are ISO-8601 UTC. All IDs are opaque strings — the client
|
||||
never constructs meaning from them and never sees server filesystem paths.
|
||||
|
||||
## Handshake
|
||||
|
||||
Connect to `wss://<host>/ws` (behind Caddy; the plugin itself is plain
|
||||
`ws://127.0.0.1:8620/ws`). The **first** client frame must be `hello` within
|
||||
10 seconds, or the server closes the socket (`auth_timeout`).
|
||||
|
||||
### Client → Server `hello`
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"secret": "<PHEBY_SECRET>",
|
||||
"protocol_version": 1,
|
||||
"request_id": "optional"
|
||||
}
|
||||
```
|
||||
|
||||
### Server → Client `ready` (success)
|
||||
|
||||
```json
|
||||
{ "type": "ready", "protocol_version": 1, "server": "pheby",
|
||||
"ts": "2026-09-02T18:00:00+00:00" }
|
||||
```
|
||||
|
||||
Failure: the server replies with an `error` event (`unauthorized`,
|
||||
`auth_timeout`, or `version_mismatch`) and closes. After 5 failed hellos from
|
||||
one source address within 60s, further connections are refused (lockout).
|
||||
|
||||
### Heartbeat
|
||||
|
||||
```json
|
||||
→ { "type": "ping" }
|
||||
← { "type": "pong", "ts": "..." }
|
||||
```
|
||||
|
||||
The server also sends WebSocket protocol-level pings (aiohttp `heartbeat=30`).
|
||||
|
||||
## Errors
|
||||
|
||||
Every error uses one shape with a machine-readable code:
|
||||
|
||||
```json
|
||||
{ "type": "error", "request_id": "r1",
|
||||
"error": { "code": "conversation_not_found", "message": "Conversation not found" },
|
||||
"ts": "..." }
|
||||
```
|
||||
|
||||
Codes: `unauthorized`, `auth_timeout`, `version_mismatch`, `bad_request`,
|
||||
`invalid_json`, `unknown_type`, `not_found`, `conversation_not_found`,
|
||||
`approval_not_found`, `clarify_not_found`, `too_large`, `rate_limited`,
|
||||
`internal_error`, `not_implemented`.
|
||||
|
||||
Limits: chat text ≤ 64,000 chars; inbound WS frame ≤ 2 MiB (violations get
|
||||
`too_large`); conversation history fetch ≤ 500 messages.
|
||||
|
||||
---
|
||||
|
||||
## Conversations
|
||||
|
||||
### `conversation.list`
|
||||
|
||||
```json
|
||||
→ { "type": "conversation.list", "request_id": "r1" }
|
||||
← { "type": "conversation.snapshot", "request_id": "r1",
|
||||
"conversations": [
|
||||
{ "conversation_id": "9f1c…", "name": "Project X",
|
||||
"session_id": "20260902_101112_ab12cd34", // Hermes session (may be null)
|
||||
"last_active": "2026-09-02T17:44:01+00:00", // may be null
|
||||
"source": "hermes" } ] }
|
||||
```
|
||||
|
||||
### `conversation.create`
|
||||
|
||||
```json
|
||||
→ { "type": "conversation.create", "name": "New chat", "request_id": "r2" }
|
||||
← { "type": "conversation.created", "conversation_id": "a1b2…", "name": "New chat", "request_id": "r2" }
|
||||
// plus, broadcast to all clients:
|
||||
← { "type": "conversation.updated", "conversation_id": "a1b2…", "name": "New chat" }
|
||||
```
|
||||
|
||||
`name` optional. Conversation IDs are server-generated 32-hex opaque strings.
|
||||
|
||||
### `conversation.open` — load history (reconnect recovery)
|
||||
|
||||
```json
|
||||
→ { "type": "conversation.open", "conversation_id": "a1b2…", "limit": 200, "request_id": "r3" }
|
||||
← { "type": "conversation.history", "conversation_id": "a1b2…", "request_id": "r3",
|
||||
"messages": [
|
||||
{ "message_id": "m12", "role": "user", "text": "hey", "ts": "…|null" },
|
||||
{ "message_id": "m13", "role": "assistant", "text": "hi!", "ts": "…|null" } ] }
|
||||
```
|
||||
|
||||
History is the authoritative Hermes transcript (`role` is always `user` or
|
||||
`assistant`). On reconnect, re-open the last-open conversations and resume —
|
||||
no client-side message cache is needed for correctness. Unknown conversation
|
||||
→ `conversation_not_found` error.
|
||||
|
||||
### `conversation.rename`
|
||||
|
||||
```json
|
||||
→ { "type": "conversation.rename", "conversation_id": "a1b2…", "name": "Renamed", "request_id": "r4" }
|
||||
← { "type": "conversation.renamed", "conversation_id": "a1b2…", "name": "Renamed", "request_id": "r4" }
|
||||
← { "type": "conversation.renamed", "conversation_id": "a1b2…", "name": "Renamed" } // broadcast
|
||||
```
|
||||
|
||||
### `conversation.delete`
|
||||
|
||||
```json
|
||||
→ { "type": "conversation.delete", "conversation_id": "a1b2…", "request_id": "r5" }
|
||||
← { "type": "conversation.deleted", "conversation_id": "a1b2…", "request_id": "r5" }
|
||||
```
|
||||
|
||||
Deletes the Hermes session transcript and the routing entry. Attachments
|
||||
belonging to the conversation age out on their own 7-day schedule.
|
||||
|
||||
**Not supported by design:** message editing, per-message deletion,
|
||||
regeneration, edit-and-resend. If you need to "undo", send a correction
|
||||
message (the agent sees the whole transcript).
|
||||
|
||||
---
|
||||
|
||||
## Chat & streaming
|
||||
|
||||
### `chat.send`
|
||||
|
||||
```json
|
||||
→ { "type": "chat.send", "conversation_id": "a1b2…", "text": "What's the weather?", "request_id": "r6" }
|
||||
```
|
||||
|
||||
### Server → Client run lifecycle
|
||||
|
||||
```json
|
||||
← { "type": "run.accepted", "conversation_id": "a1b2…", "run_id": "8c1f…", "request_id": "r6" }
|
||||
← { "type": "message.start", "conversation_id": "a1b2…", "run_id": "8c1f…", "message_id": "draft-8c1f…" }
|
||||
```
|
||||
|
||||
While the agent streams, the server pushes **cumulative** draft text (the
|
||||
client can simply replace the bubble's text each time — no delta stitching):
|
||||
|
||||
```json
|
||||
← { "type": "message.delta", "conversation_id": "a1b2…", "message_id": "draft-8c1f…",
|
||||
"text": "It's currently 27°C…", "ts": "..." }
|
||||
```
|
||||
|
||||
Completion (final text supersedes the draft — render the final, drop the
|
||||
draft):
|
||||
|
||||
```json
|
||||
← { "type": "message.complete", "conversation_id": "a1b2…",
|
||||
"message_id": "draft-8c1f…", "text": "…full final answer…", "ts": "..." }
|
||||
```
|
||||
|
||||
`message.complete` with `"kind": "notice"` is a gateway lifecycle/status
|
||||
notice rather than conversation content — render or ignore.
|
||||
|
||||
Run end:
|
||||
|
||||
```json
|
||||
← { "type": "run.finished", "conversation_id": "a1b2…", "run_id": "8c1f…",
|
||||
"status": "completed" | "cancelled" | "failed" | "idle",
|
||||
"error": "only on failure" }
|
||||
```
|
||||
|
||||
State machine per assistant turn:
|
||||
`run.accepted → message.start → (message.delta)* → message.complete → run.finished`.
|
||||
A turn with no streaming skips `message.start`/`message.delta`. Never infer
|
||||
state from text — use these events.
|
||||
|
||||
### `run.cancel` — stop an active run
|
||||
|
||||
```json
|
||||
→ { "type": "run.cancel", "conversation_id": "a1b2…", "run_id": "8c1f…", "request_id": "r7" }
|
||||
← { "type": "run.finished", "conversation_id": "a1b2…", "run_id": "8c1f…", "status": "cancelled" }
|
||||
```
|
||||
|
||||
Cancellation uses Hermes's supported interrupt mechanism (agent interrupt +
|
||||
run-generation invalidation) — the conversation stays consistent and
|
||||
resumable. Cancelling with no active run returns `run.finished`
|
||||
`status:"idle"`.
|
||||
|
||||
---
|
||||
|
||||
## Tool events (structured — never chat text)
|
||||
|
||||
Tool activity arrives as `tool.event` messages, completely separate from
|
||||
`message.*` chat content. The client renders them as compact tool components
|
||||
(ChatGPT-style) attached to the assistant turn.
|
||||
|
||||
```json
|
||||
{ "type": "tool.event",
|
||||
"conversation_id": "a1b2…",
|
||||
"tool_call_id": "t-1a2b3c4d5e6f",
|
||||
"tool_name": "web_search",
|
||||
"status": "running" | "completed" | "failed",
|
||||
"description": "cats — short preview from the agent (may be null)",
|
||||
"args_redacted": { "query": "cats" }, // only on "running"; secret-looking keys redacted
|
||||
"duration_ms": 1234, // only on completion/failure (may be null)
|
||||
"error": "only on failed, truncated", // only on "failed"
|
||||
"ts": "..." }
|
||||
```
|
||||
|
||||
Correlate `running` → `completed`/`failed` by `tool_call_id`. Note: the
|
||||
running event's ID comes from the adapter and the completion event from
|
||||
Hermes's `post_tool_call` hook; when they differ, correlate by
|
||||
`(tool_name, conversation)` as a fallback and prefer the completion event's
|
||||
ID going forward.
|
||||
|
||||
No fake "Searching the web…" text is ever injected into `message.*` events.
|
||||
|
||||
---
|
||||
|
||||
## Approvals
|
||||
|
||||
When Hermes pauses for a human decision on a dangerous action:
|
||||
|
||||
```json
|
||||
{ "type": "approval.request",
|
||||
"approval_id": "3d4e5f6070a1",
|
||||
"session_key": "agent:main:pheby:dm:a1b2…",
|
||||
"command": "rm -rf /tmp/build-output",
|
||||
"description": "Destructive shell command (rm -rf)",
|
||||
"choices": ["once", "session", "always", "deny"],
|
||||
"ts": "..." }
|
||||
```
|
||||
|
||||
Respond:
|
||||
|
||||
```json
|
||||
→ { "type": "approval.respond", "approval_id": "3d4e5f6070a1",
|
||||
"choice": "once" | "session" | "always" | "deny",
|
||||
"reason": "optional free text with deny", "request_id": "r8" }
|
||||
← { "type": "approval.resolved", "approval_id": "3d4e5f6070a1", "choice": "once", "request_id": "r8" }
|
||||
// broadcast confirmation (also informs other tabs):
|
||||
← { "type": "approval.resolved", "approval_id": "3d4e5f6070a1", "choice": "once", "accepted": true }
|
||||
```
|
||||
|
||||
Choices map to Hermes semantics: `once` (approve this action), `session`
|
||||
(approve pattern for this conversation), `always` (also persist), `deny`
|
||||
(decline; the agent is told NOT to retry). Unknown/stale ID →
|
||||
`approval_not_found` error. Hermes itself fails the approval closed after its
|
||||
own timeout, so a silently-closed socket can't leave a zombie gate.
|
||||
|
||||
---
|
||||
|
||||
## Clarifications / choices
|
||||
|
||||
```json
|
||||
{ "type": "clarify.request",
|
||||
"clarify_id": "c1a2b3d4e5",
|
||||
"session_key": "agent:main:pheby:dm:a1b2…",
|
||||
"question": "Deploy to staging or production?",
|
||||
"choices": ["staging", "production"], // null ⇒ free text only
|
||||
"allow_free_text": true,
|
||||
"ts": "..." }
|
||||
```
|
||||
|
||||
Respond (either a choice value or free text):
|
||||
|
||||
```json
|
||||
→ { "type": "clarify.respond", "clarify_id": "c1a2b3d4e5", "response": "production", "request_id": "r9" }
|
||||
← { "type": "clarify.resolved", "clarify_id": "c1a2b3d4e5", "request_id": "r9" }
|
||||
← { "type": "clarify.resolved", "clarify_id": "c1a2b3d4e5", "accepted": true } // broadcast
|
||||
```
|
||||
|
||||
`accepted:false` on the broadcast means Hermes had already resolved/timed out
|
||||
the prompt. Always render an "Other" affordance — Hermes clarifications
|
||||
accept free text.
|
||||
|
||||
---
|
||||
|
||||
## Attachments (agent → client deliverables)
|
||||
|
||||
When the agent produces a file (image, document, audio, video… via Hermes's
|
||||
normal `MEDIA:` deliverable pipeline), the server copies it into
|
||||
adapter-managed storage and broadcasts:
|
||||
|
||||
```json
|
||||
{ "type": "attachment.added",
|
||||
"conversation_id": "a1b2…",
|
||||
"attachment": {
|
||||
"attachment_id": "e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0",
|
||||
"filename": "report.pdf",
|
||||
"mime_type": "application/pdf",
|
||||
"size": 48213,
|
||||
"kind": "image" | "voice" | "video" | "audio" | "document",
|
||||
"inline_image": false,
|
||||
"conversation_id": "a1b2…",
|
||||
"message_id": null,
|
||||
"created_at": "2026-09-02T18:30:00+00:00",
|
||||
"expires_at": "2026-09-09T18:30:00+00:00", // null when retention=0
|
||||
"download_path": "/attachments/e5f6…" },
|
||||
"ts": "..." }
|
||||
```
|
||||
|
||||
Download over **HTTPS** (authenticated — same `PHEBY_SECRET`):
|
||||
|
||||
```
|
||||
GET {download_path}
|
||||
Authorization: Bearer <PHEBY_SECRET> (or ApiKey <secret>, or X-Pheby-Secret: <secret>)
|
||||
```
|
||||
|
||||
* `inline_image: true` → `kind == "image"`, safe for an inline preview
|
||||
(`BitmapFactory` / `AsyncImage` with the same authenticated GET).
|
||||
* Everything else: download and open as an Android document.
|
||||
* Unknown / expired / malformed ID → `404` with
|
||||
`{"error":{"code":"not_found","message":"Attachment unavailable"}}` —
|
||||
no implementation details.
|
||||
* **The client can never request arbitrary files** — only registered
|
||||
attachment IDs resolve.
|
||||
* **Retention:** adapter copies expire after 7 days (configurable) and are
|
||||
deleted by an hourly cleanup. Original files the agent produced elsewhere
|
||||
on the host are never touched. `expires_at` tells the client when to stop
|
||||
offering the download.
|
||||
|
||||
---
|
||||
|
||||
## Models
|
||||
|
||||
### List providers + models
|
||||
|
||||
```json
|
||||
→ { "type": "models.list", "request_id": "r10" }
|
||||
← { "type": "models.snapshot", "request_id": "r10",
|
||||
"providers": [
|
||||
{ "slug": "openrouter", "name": "OpenRouter", "is_current": true,
|
||||
"models": ["z-ai/glm-5.3-flash", "anthropic/claude-sonnet-4", "…"],
|
||||
"total_models": 42 } ],
|
||||
"current_model": "z-ai/glm-5.3-flash",
|
||||
"current_provider": "openrouter",
|
||||
"supported_reasoning_efforts": ["minimal","low","medium","high","xhigh","max","ultra"],
|
||||
"ts": "..." }
|
||||
```
|
||||
|
||||
Lists come from Hermes's own credential-aware picker data — nothing is
|
||||
hardcoded. Models are exactly what the configured providers expose.
|
||||
|
||||
### Read / change current model
|
||||
|
||||
```json
|
||||
→ { "type": "models.current", "request_id": "r11" }
|
||||
← { "type": "model.current", "request_id": "r11", "model": "z-ai/glm-5.3-flash", "provider": "openrouter", "ts": "..." }
|
||||
|
||||
→ { "type": "model.set", "model": "anthropic/claude-sonnet-4",
|
||||
"provider": "anthropic", // optional
|
||||
"conversation_id": "a1b2…", // present ⇒ session-scoped override
|
||||
"request_id": "r12" }
|
||||
← { "type": "model.changed", "model": "anthropic/claude-sonnet-4",
|
||||
"provider": "anthropic", "scope": "conversation" | "global", "request_id": "r12" }
|
||||
// plus broadcast of model.changed (without request_id) to all clients
|
||||
```
|
||||
|
||||
Omit `conversation_id` ⇒ the change is persisted globally (Hermes
|
||||
`model.default`). A `model.changed` with `scope:"global"` tells every open
|
||||
conversation the default moved.
|
||||
|
||||
---
|
||||
|
||||
## Reasoning effort
|
||||
|
||||
```json
|
||||
→ { "type": "reasoning.current", "request_id": "r13" }
|
||||
← { "type": "reasoning.snapshot", "request_id": "r13",
|
||||
"effort": "medium", // current effective effort (may be null = provider default)
|
||||
"enabled": true, // false ⇒ thinking disabled
|
||||
"supported_efforts": ["none","minimal","low","medium","high","xhigh","max","ultra"],
|
||||
"ts": "..." }
|
||||
|
||||
→ { "type": "reasoning.set", "effort": "high", "conversation_id": "a1b2…", "request_id": "r14" }
|
||||
← { "type": "reasoning.changed", "effort": "high", "scope": "conversation", "request_id": "r14" }
|
||||
```
|
||||
|
||||
`effort: "none"` disables thinking. Invalid values → `bad_request`. Scope
|
||||
rules mirror `model.set` (with `conversation_id` ⇒ session override; without ⇒
|
||||
global `agent.reasoning_effort`). **Capability note:** Hermes knows *whether*
|
||||
a model supports reasoning (models.dev metadata) but does not expose a
|
||||
per-provider enum of valid effort values; the listed levels are Hermes's
|
||||
canonical set — unsupported levels on a given provider surface as a provider
|
||||
error on the next turn, not at set time. This is a documented Hermes
|
||||
limitation, not a Pheby guess.
|
||||
|
||||
---
|
||||
|
||||
## Unsolicited messages & reconnect behavior
|
||||
|
||||
The WebSocket stays connected; any Hermes-originated output destined for the
|
||||
Pheby platform (scheduled/cron deliveries, background completions,
|
||||
notifications) is pushed as normal `message.*` / `run.*` events even when it
|
||||
is not a reply to your last request. Reconnection procedure for clients:
|
||||
|
||||
1. Reconnect WS, redo `hello`.
|
||||
2. Re-`conversation.open` the conversations you show; replace local state
|
||||
with `conversation.history` (authoritative).
|
||||
3. Re-`models.current` / `reasoning.current` if those views are visible.
|
||||
4. Live events continue from there.
|
||||
|
||||
No external push service exists (no FCM); Android notification behavior is
|
||||
the client's responsibility while the socket is down.
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
# Pheby — Hermes Platform Adapter/Plugin
|
||||
|
||||
A private, third-party **Hermes Agent platform plugin** that serves the
|
||||
**Pheby protocol** — a WebSocket + HTTPS API designed for a native Android
|
||||
client (Kotlin/Compose). It exposes multiple Hermes conversations, streamed
|
||||
chat, structured tool events, native approval/clarification round-trips,
|
||||
model + reasoning-effort selection, and agent-generated attachments with
|
||||
7-day retention.
|
||||
|
||||
No Hermes core modifications. Installs into Hermes's supported user plugin
|
||||
location and uses the documented platform-adapter extension points
|
||||
(same mechanism as the bundled Telegram/Discord/ntfy adapters).
|
||||
|
||||
- **Protocol spec (for the Kotlin client):** [`docs/PROTOCOL.md`](PROTOCOL.md)
|
||||
- **Original product spec:** [`docs/spec-source/`](spec-source/)
|
||||
|
||||
---
|
||||
|
||||
## Architecture (concise)
|
||||
|
||||
```
|
||||
Android (Kotlin/Compose)
|
||||
│ HTTPS + WSS (shared secret)
|
||||
▼
|
||||
Caddy ── TLS termination, reverse proxy
|
||||
│ plain HTTP/WS
|
||||
▼
|
||||
Pheby plugin (in Hermes gateway process)
|
||||
├─ aiohttp server: GET /ws, GET /attachments/{id}, GET /health
|
||||
├─ protocol layer: JSON message types, auth, limits, errors
|
||||
├─ PhebyAdapter (BasePlatformAdapter subclass)
|
||||
│ outbound: send/edit → chat events, format_tool_event → tool.event,
|
||||
│ send_clarify → clarify.request, send_document/… → attachments
|
||||
│ inbound: WS messages → MessageEvent → gateway pipeline
|
||||
└─ hermes_bridge: sessions, approvals (tools.approval),
|
||||
clarifications (tools.clarify_gateway), models, reasoning
|
||||
▼
|
||||
Hermes Gateway (authoritative): agent loop, sessions/state.db,
|
||||
tools, skills, cron, deliverables
|
||||
```
|
||||
|
||||
Key properties:
|
||||
|
||||
- **Hermes is authoritative.** Conversations are Hermes sessions keyed
|
||||
`agent:main:pheby:dm:<conversation_id>`; history/titles live in Hermes
|
||||
`state.db`. Pheby keeps only a thin, rebuildable name index.
|
||||
- **The full gateway pipeline works unchanged** — auth/pairing, tool
|
||||
approval, clarify, deliverables, streaming, cron delivery — because inbound
|
||||
messages are ordinary `MessageEvent`s on a registered platform.
|
||||
- **Single-user, shared-secret.** One `PHEBY_SECRET` gates every WS
|
||||
connection and attachment download (constant-time compare). No accounts,
|
||||
no registration — but IDs and message shapes are multi-client friendly.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Requirements: Hermes Agent with its bundled venv (aiohttp is already a Hermes
|
||||
dependency — nothing extra to install).
|
||||
|
||||
### 1. Copy the plugin into HERMES_HOME
|
||||
|
||||
```bash
|
||||
# HERMES_HOME is ~/.hermes by default (or your profile dir)
|
||||
mkdir -p ~/.hermes/plugins
|
||||
cp -r plugin/pheby ~/.hermes/plugins/pheby
|
||||
```
|
||||
|
||||
The plugin directory must contain `plugin.yaml`, `__init__.py`, and the
|
||||
`.py` modules.
|
||||
|
||||
### 2. Set the secret
|
||||
|
||||
Generate a strong secret and put it in Hermes's env file:
|
||||
|
||||
```bash
|
||||
echo "PHEBY_SECRET=$(openssl rand -hex 32)" >> ~/.hermes/.env
|
||||
chmod 600 ~/.hermes/.env
|
||||
```
|
||||
|
||||
### 3. Enable it in config.yaml
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
enabled:
|
||||
- pheby # user plugins are opt-in (untrusted code gate)
|
||||
|
||||
platforms:
|
||||
pheby:
|
||||
enabled: true
|
||||
extra:
|
||||
bind_host: "127.0.0.1" # default; safe behind local Caddy
|
||||
port: 8620
|
||||
attachment_storage_dir: "" # default: <HERMES_HOME>/pheby-attachments
|
||||
attachment_retention_days: 7
|
||||
debug: false # verbose protocol logging
|
||||
log_chat_content: false # debug logs include chat text (privacy)
|
||||
```
|
||||
|
||||
Env vars override YAML: `PHEBY_SECRET`, `PHEBY_BIND_HOST`, `PHEBY_PORT`,
|
||||
`PHEBY_DEBUG`, `PHEBY_LOG_CHAT_CONTENT`. Optional: `PHEBY_HOME_CHANNEL`
|
||||
(conversation ID receiving cron deliveries), `PHEBY_ALLOWED_USERS`,
|
||||
`PHEBY_ALLOW_ALL_USERS`.
|
||||
|
||||
### 4. Start Hermes with the gateway
|
||||
|
||||
```bash
|
||||
hermes gateway restart # or: hermes gateway run (foreground)
|
||||
hermes gateway status # should list pheby as connected
|
||||
```
|
||||
|
||||
Logs: `~/.hermes/logs/gateway.log` (look for `[pheby]` lines: startup,
|
||||
connections, auth failures, attachment registration, cleanup).
|
||||
|
||||
### Smoke check
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:8620/health # → {"status":"ok"}
|
||||
# WS: connect to /ws, send hello with your secret → "ready"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Caddy reverse proxy
|
||||
|
||||
Terminate TLS at Caddy; Pheby stays plain HTTP on localhost. WebSockets and
|
||||
attachment downloads need no special config beyond the reverse proxy itself
|
||||
(Caddy handles WebSocket upgrades transparently):
|
||||
|
||||
```caddy
|
||||
# Caddyfile
|
||||
pheby.example.com {
|
||||
encode zstd gzip
|
||||
|
||||
# WebSocket + API
|
||||
reverse_proxy /ws 127.0.0.1:8620
|
||||
reverse_proxy /attachments 127.0.0.1:8620
|
||||
reverse_proxy /health 127.0.0.1:8620
|
||||
|
||||
# Optionally restrict by source when on a public VPS:
|
||||
# @notlan not remote_ip 10.0.0.0/8 192.168.0.0/16
|
||||
# respond @notlan 403
|
||||
}
|
||||
```
|
||||
|
||||
A single catch-all also works:
|
||||
|
||||
```caddy
|
||||
pheby.example.com {
|
||||
reverse_proxy 127.0.0.1:8620
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Caddy provides HTTPS + automatic certificates; the plugin never sees TLS.
|
||||
- `X-Forwarded-For` is not used for auth decisions (the lockout key is the
|
||||
direct peer address — behind Caddy that is Caddy itself, so lockout is
|
||||
effectively global; that is acceptable for a single-user deployment and
|
||||
still stops brute force).
|
||||
- The Android client connects to `wss://pheby.example.com/ws` and downloads
|
||||
attachments from `https://pheby.example.com/attachments/{id}` with
|
||||
`Authorization: Bearer <PHEBY_SECRET>`.
|
||||
|
||||
---
|
||||
|
||||
## How authentication works
|
||||
|
||||
- The client presents `PHEBY_SECRET` **once** in the `hello` WS frame, and on
|
||||
**every** attachment HTTP request (`Authorization: Bearer …`,
|
||||
`ApiKey …`, or `X-Pheby-Secret: …`).
|
||||
- Comparison is constant-time (`hmac.compare_digest`); failed hellos rate
|
||||
limit the source (5 failures / 60 s → lockout).
|
||||
- `/health` is the only unauthenticated route and leaks nothing.
|
||||
- The secret is **never** Hermes's API-server key — it is an independent
|
||||
credential stored in `~/.hermes/.env` (never in config.yaml history, never
|
||||
logged; debug logs redact it and it is excluded from protocol echo).
|
||||
- It is **not** a per-user identity: anyone with the secret *is* the user.
|
||||
Keep it secret; rotate by changing `.env` + restarting the gateway.
|
||||
|
||||
---
|
||||
|
||||
## Attachment delivery & expiration
|
||||
|
||||
1. The agent produces a file through Hermes's normal deliverable pipeline
|
||||
(`MEDIA:` tags → `validate_media_delivery_path` → adapter send hooks).
|
||||
2. The adapter **copies** the file into adapter-owned storage
|
||||
(`<HERMES_HOME>/pheby-attachments/`), assigns an opaque 32-hex ID, and
|
||||
records metadata (filename, MIME, size, kind, conversation, timestamps)
|
||||
in a persistent JSON index.
|
||||
3. The client gets `attachment.added` with a `download_path`; downloads are
|
||||
streamed over authenticated HTTPS. `inline_image` marks previewable
|
||||
images.
|
||||
4. An hourly sweep deletes expired blobs (default 7 days) — **only** files
|
||||
the store registered, inside its own root. Hermes-owned originals
|
||||
elsewhere on disk are never touched.
|
||||
5. Expired/unknown/malformed IDs all return the same minimal 404.
|
||||
Metadata persists across restarts, so valid attachments survive gateway
|
||||
restarts. Path traversal is impossible: IDs are validated, blobs are
|
||||
resolved from stored metadata inside the storage root, and every resolve
|
||||
re-checks containment.
|
||||
|
||||
---
|
||||
|
||||
## Debugging & logging
|
||||
|
||||
Set `PHEBY_DEBUG=true` (env) or `extra.debug: true` (YAML) for verbose
|
||||
protocol logs (inbound/outbound types, connection lifecycle, cleanup counts).
|
||||
Debug logging **never** prints secrets, provider keys, or Authorization
|
||||
headers, and by default masks chat text (`PHEBY_LOG_CHAT_CONTENT=true` to
|
||||
include text during client development — privacy tradeoff, off by default).
|
||||
|
||||
Tool **results** are deliberately not relayed (they can embed host paths);
|
||||
clients get structured status/duration. Full transcripts always remain
|
||||
available via `conversation.open` (Hermes's authoritative store).
|
||||
|
||||
---
|
||||
|
||||
## Security considerations
|
||||
|
||||
- Treat the endpoint as access to a powerful agent with host tool access:
|
||||
bind to `127.0.0.1`, front with TLS, use a 256-bit secret.
|
||||
- No client-controllable filesystem paths exist anywhere in the protocol.
|
||||
- Inbound WS frames are size-capped (2 MiB) and strictly JSON-validated;
|
||||
chat text is length-capped (64k chars).
|
||||
- Lockout + constant-time comparison + auth timeout blunt brute force.
|
||||
- Errors are machine-readable codes; stack traces and internal paths never
|
||||
reach the client.
|
||||
- CORS is irrelevant (native client); `/health` exposes only
|
||||
`{"status":"ok"}`.
|
||||
|
||||
---
|
||||
|
||||
## Hermes-version limitations (discovered, not assumed)
|
||||
|
||||
These are current Hermes behaviors the adapter documents rather than hacks
|
||||
around. None require core modifications; all are handled cleanly.
|
||||
|
||||
1. **Session model change is not instant mid-run.** `model.set` /
|
||||
`reasoning.set` write the session override (or global config); the next
|
||||
turn picks it up. An in-flight run finishes on its current model.
|
||||
2. **Reasoning-effort capability metadata is partial.** Hermes knows *that*
|
||||
a model supports reasoning (models.dev) but has no per-provider enum of
|
||||
valid effort levels. Pheby exposes Hermes's canonical level set
|
||||
(`none, minimal, low, medium, high, xhigh, max, ultra`) and documents that
|
||||
an unsupported level surfaces as a provider error on the next turn.
|
||||
3. **Conversation delete is a documented approximation.** Hermes's
|
||||
SessionStore has no public per-routing-key delete; Pheby deletes the
|
||||
authoritative transcript row (`SessionDB.delete_session`) and resets the
|
||||
routing entry, which yields the same user-visible behavior.
|
||||
4. **Tool `running` → `completed` ID correlation.** Start events get
|
||||
adapter-generated IDs; completion events carry Hermes's authoritative
|
||||
`tool_call_id` from the `post_tool_call` hook. The protocol documents
|
||||
correlating by `(tool_name, conversation)` when IDs differ. (Gateway
|
||||
tool-start events don't carry Hermes's call ID yet.)
|
||||
5. **Standalone cron delivery.** Cron jobs targeting `pheby` are delivered
|
||||
in-process with the gateway. A `standalone_sender_fn` hook exists but
|
||||
cannot push to a WS server it isn't hosting; out-of-process cron delivery
|
||||
to Pheby is not supported (documented, fail-loud).
|
||||
6. **Reconnect recovery is state-based, not event-replay.** Missed events are
|
||||
recovered by re-opening conversations (authoritative history), not by
|
||||
replaying a server-side event log. This is the spec's preferred approach
|
||||
and keeps the protocol simple.
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv && .venv/bin/pip install pytest pytest-asyncio aiohttp pyyaml requests
|
||||
.venv/bin/python -m pytest tests/ -o addopts= -q --asyncio-mode=auto
|
||||
```
|
||||
|
||||
Tests are gateway-free (fakes; real Hermes primitives exercised in-process
|
||||
where safe — no LLM calls). Two live smoke tests bind an ephemeral localhost
|
||||
port.
|
||||
|
||||
```
|
||||
repo layout
|
||||
├── plugin/pheby/ # the plugin (install this dir)
|
||||
│ ├── __init__.py # register(ctx): platform + hook registration
|
||||
│ ├── plugin.yaml # manifest (kind: platform)
|
||||
│ ├── adapter.py # BasePlatformAdapter subclass
|
||||
│ ├── server.py # aiohttp app + WS dispatch
|
||||
│ ├── ws_client.py # per-connection state
|
||||
│ ├── hermes_bridge.py # ALL Hermes-internal integration (defensive)
|
||||
│ ├── conversations.py # conversation ID ↔ session routing
|
||||
│ ├── attachments.py # attachment store + cleanup + auth helpers
|
||||
│ ├── protocol.py # message types, errors, limits
|
||||
│ └── config.py # env/YAML config resolution
|
||||
├── tests/
|
||||
└── docs/ # PROTOCOL.md, spec-source/
|
||||
```
|
||||
|
||||
## Client implementation checklist (Kotlin/Compose)
|
||||
|
||||
Minimum client surface:
|
||||
|
||||
- WS connect + `hello` (handle `ready` / error / close 4401) + ping/pong.
|
||||
- `conversation.list/open/create/rename/delete` + `chat.send`.
|
||||
- Render state machine: `run.accepted`, `message.start`,
|
||||
`message.delta` (replace bubble text), `message.complete`,
|
||||
`run.finished`.
|
||||
- `tool.event` components keyed by `tool_call_id`.
|
||||
- `approval.request` → Approve/Deny buttons → `approval.respond`.
|
||||
- `clarify.request` → choice buttons + free text → `clarify.respond`.
|
||||
- `attachment.added` → inline preview when `inline_image`, else download
|
||||
(authenticated GET) → `run.cancel` for the stop button.
|
||||
- `models.list/current/set`, `reasoning.current/set`.
|
||||
- Reconnect: backoff, re-hello, re-open visible conversations.
|
||||
@@ -0,0 +1,274 @@
|
||||
I want you to build a private custom Hermes Agent platform adapter/plugin called Pheby.
|
||||
Before implementing anything, inspect the current Hermes Agent documentation and current Hermes source code, especially its platform adapter/plugin APIs, Gateway architecture, Telegram/Discord adapters, conversation/session handling, tool-call events, clarification/approval mechanisms, model selection, reasoning effort, generated-file/deliverable handling, and plugin loading system. Do not rely on assumptions from older Hermes versions. The implementation must target the currently installed/current upstream Hermes architecture.
|
||||
The Pheby adapter will serve a native Android application written in Kotlin with Jetpack Compose. For this task, build only the Hermes-side plugin and its protocol/API. Do not build the Android application.
|
||||
The plugin must be a clean third-party Hermes plugin/platform adapter. Do not modify Hermes core files, monkey-patch Hermes, fork Hermes, or rely on hacks that will make Hermes upgrades difficult. It should install into Hermes's supported user plugin location and use documented extension points.
|
||||
Overall architecture
|
||||
Pheby is a single-user private platform.
|
||||
The adapter should expose a network service intended to sit behind Caddy and be reachable over HTTPS/WSS from the public internet.
|
||||
Use:
|
||||
• WebSocket for realtime bidirectional chat/events.
|
||||
• Normal HTTPS endpoints for downloading generated attachments.
|
||||
• A shared secret/API-key-style credential for authentication.
|
||||
• Hermes/Gateway functionality underneath rather than reimplementing the agent loop.
|
||||
Assume Caddy handles TLS. The Pheby service itself can listen on localhost/plain HTTP and WebSocket.
|
||||
Do not expose or reuse Hermes's master API-server key as the Pheby client credential.
|
||||
The Pheby credential should be configurable through an environment variable and every WebSocket connection and HTTP attachment request must require authentication.
|
||||
This is a single-user application. Do not build account registration, users, roles, password recovery, OAuth, etc. However, avoid unnecessarily designing the protocol in a way that makes future multi-client support impossible.
|
||||
Conversations
|
||||
Pheby must expose multiple Hermes conversations.
|
||||
The Android client needs to be able to:
|
||||
• List conversations.
|
||||
• Open/load a conversation and its message history.
|
||||
• Create a new conversation.
|
||||
• Rename a conversation.
|
||||
• Delete a conversation.
|
||||
Hermes should remain the authoritative source of conversation/session state.
|
||||
The protocol should provide stable conversation IDs.
|
||||
When Pheby reconnects, it must be able to retrieve existing conversations and their messages rather than treating the connection as a new chat.
|
||||
Do not implement message editing, individual message deletion, regeneration, or edit-and-resend.
|
||||
Sending messages and streaming responses
|
||||
The client must be able to send a text message into a selected conversation.
|
||||
Use whichever response-streaming mechanism integrates most cleanly with Hermes. Token-level streaming is not a requirement. Chunk-level streaming is perfectly acceptable if it substantially simplifies the adapter.
|
||||
The protocol should distinguish between:
|
||||
• A new assistant message.
|
||||
• Incremental content being appended to an assistant message.
|
||||
• A completed assistant message.
|
||||
• A failed/cancelled assistant run.
|
||||
The UI should never have to infer these states by parsing arbitrary text.
|
||||
The client must be able to stop/cancel an active Hermes task/run.
|
||||
Stopping should use Hermes's supported cancellation/interruption mechanism rather than killing threads/processes or corrupting the conversation.
|
||||
Tool calls
|
||||
Tool execution must be represented as structured events, separate from the normal textual chat content.
|
||||
This is important.
|
||||
Pheby will display tool activity similarly to ChatGPT: tool calls can appear as compact UI components associated with an assistant turn without cluttering the actual chat transcript.
|
||||
Do not inject fake text such as:
|
||||
"Searching the web..." "Running command..." "Tool completed..."
|
||||
into the assistant's message merely to represent activity.
|
||||
Expose structured information for tool activity where Hermes makes it available, including at minimum:
|
||||
• Unique tool-call/event ID.
|
||||
• Conversation/run association.
|
||||
• Tool name/type.
|
||||
• Human-readable description if available.
|
||||
• Start/running state.
|
||||
• Completion state.
|
||||
• Failure state.
|
||||
• Result/summary information that is safe and appropriate to expose.
|
||||
Preserve enough raw structured information that the Android client can create richer custom UI later, but do not expose secrets or internal credentials.
|
||||
If Hermes provides nested/sub-agent/tool execution events, preserve their relationship where practical.
|
||||
Approvals
|
||||
Pheby must support Hermes's human approval system.
|
||||
When Hermes pauses because an action requires approval, send a structured WebSocket event to Pheby containing enough information to render native:
|
||||
Approve Deny
|
||||
controls.
|
||||
The event should include:
|
||||
• Approval/request ID.
|
||||
• Conversation/run association.
|
||||
• Human-readable description.
|
||||
• Relevant action/tool information.
|
||||
• Any choices/actions Hermes permits.
|
||||
Pheby must be able to submit an approval or denial and have the existing Hermes run continue appropriately.
|
||||
Do not simulate approval through ordinary user chat messages if Hermes exposes a proper approval mechanism.
|
||||
Clarifications / choices
|
||||
Pheby must also support Hermes's interactive clarification/choice functionality.
|
||||
If Hermes asks the user to choose between several structured options, expose the request as structured data over WebSocket so the Android app can render native buttons/options.
|
||||
Provide:
|
||||
• Clarification ID.
|
||||
• Prompt/question.
|
||||
• Available choices.
|
||||
• Choice IDs/values.
|
||||
• Whether free-text input is permitted, if Hermes supports that distinction.
|
||||
• Conversation/run association.
|
||||
The client must be able to submit the selected choice or clarification response so Hermes can continue the same pending operation.
|
||||
Use Hermes's existing clarification/elicitation mechanism rather than inventing a parallel agent workflow.
|
||||
Generated attachments / Deliverable Mode
|
||||
This is a critical requirement.
|
||||
Hermes must be able to generate files and send them to Pheby in the same general spirit as Telegram/Discord deliverables.
|
||||
Pheby does not need to upload arbitrary files to Hermes.
|
||||
Agent → Pheby attachments are required.
|
||||
Reuse Hermes's existing Deliverable Mode/media detection and gateway abstractions wherever possible rather than recreating filename parsing from scratch.
|
||||
Support arbitrary document/file types that Hermes's deliverable system supports.
|
||||
At minimum represent:
|
||||
• Attachment ID.
|
||||
• Filename.
|
||||
• MIME type.
|
||||
• File size.
|
||||
• Download endpoint/identifier.
|
||||
• Associated conversation/message.
|
||||
• Whether it is suitable for inline image preview.
|
||||
Images should be identifiable as images so Pheby can display an inline preview.
|
||||
Other files should be downloadable/openable as normal Android documents.
|
||||
Do NOT expose arbitrary server filesystem paths to the client.
|
||||
Do NOT provide an API that lets the client request arbitrary filesystem files.
|
||||
Map generated files to opaque attachment IDs and serve only explicitly registered Hermes deliverables.
|
||||
Attachment downloads must require the Pheby authentication credential.
|
||||
Use normal HTTP streaming/downloads rather than transferring large files through the WebSocket.
|
||||
Attachment cleanup
|
||||
Generated Pheby attachment files should expire after 7 days.
|
||||
Implement safe cleanup of expired adapter-managed attachments.
|
||||
Cleanup must never delete unrelated Hermes/user files.
|
||||
If Hermes's deliverable mechanism points to files Hermes owns elsewhere, do not blindly delete their originals. Prefer an adapter-managed attachment storage/copy/link strategy where the ownership and lifecycle are unambiguous.
|
||||
Persist enough attachment metadata that restarting Hermes/Pheby does not immediately make all valid attachments inaccessible.
|
||||
Expired attachment IDs should cleanly return an appropriate HTTP error rather than exposing implementation details.
|
||||
Models
|
||||
The Android client needs to query and change the active Hermes model.
|
||||
Expose the models/providers Hermes currently makes available through its supported configuration/API.
|
||||
Do not hardcode model names.
|
||||
The protocol should expose useful model metadata when Hermes provides it.
|
||||
The selected model must be changeable through Pheby using Hermes's supported mechanisms.
|
||||
Reasoning effort
|
||||
Pheby must expose and allow changing the model's reasoning effort where supported by the active provider/model.
|
||||
Do not assume every model supports the same reasoning settings.
|
||||
Expose supported values/capabilities dynamically where Hermes makes that information available.
|
||||
If Hermes does not currently provide perfect capability metadata, implement the cleanest documented approach and clearly document the limitation instead of hardcoding fragile assumptions.
|
||||
Skills and toolsets
|
||||
Pheby does not need to enable, disable, configure, or manage Hermes skills/toolsets.
|
||||
Normal Hermes tools and configured skills must continue functioning through the agent, but no management UI API is required.
|
||||
Voice and user file uploads
|
||||
Do not implement:
|
||||
• Voice messages.
|
||||
• Speech-to-text.
|
||||
• Text-to-speech.
|
||||
• Arbitrary user file uploads.
|
||||
Design the protocol cleanly enough that additional event/content types could be added later without breaking existing clients, but don't spend significant implementation effort on features outside this scope.
|
||||
Realtime / unsolicited messages
|
||||
The WebSocket is intended to remain connected while Pheby is running.
|
||||
The adapter must be able to push messages/events originating from Hermes even when they were not an immediate response to the most recent Pheby request. For example, if Hermes Gateway produces a scheduled/automated message destined for the Pheby platform, the connected client should receive it.
|
||||
Do not implement Firebase Cloud Messaging or another external push-notification service.
|
||||
Android notification behavior is the client's responsibility.
|
||||
Make reconnection resilient. The protocol should make it possible for the client to recover messages/events it missed during a temporary WebSocket disconnect, preferably by re-syncing authoritative conversation state rather than requiring a perfectly uninterrupted socket.
|
||||
Protocol
|
||||
Design a small, explicit and versioned Pheby protocol.
|
||||
Prefer JSON messages over WebSocket.
|
||||
Every event should contain a clear type and whatever IDs are needed to associate it with:
|
||||
• Conversation.
|
||||
• Message.
|
||||
• Run.
|
||||
• Tool call.
|
||||
• Approval.
|
||||
• Clarification.
|
||||
• Attachment.
|
||||
Avoid making the Android client parse prose to determine application state.
|
||||
Use stable opaque IDs.
|
||||
Include an initial handshake/protocol-version exchange if useful.
|
||||
Provide clean error responses/events with machine-readable error codes and human-readable messages.
|
||||
Consider heartbeat/ping handling and clean reconnect behavior.
|
||||
Do not build unnecessary complexity such as a custom binary protocol.
|
||||
Document every client → server and server → client message type with JSON examples.
|
||||
The protocol specification is part of the deliverable.
|
||||
Configuration
|
||||
Use Hermes-compatible configuration conventions.
|
||||
Secrets should use environment variables.
|
||||
Non-secret behavioral configuration may use Hermes/plugin YAML configuration where appropriate.
|
||||
At minimum configuration should cover:
|
||||
• Enabled/disabled.
|
||||
• Bind host.
|
||||
• Port.
|
||||
• Authentication secret.
|
||||
• Attachment storage location if configurable.
|
||||
• Attachment retention period, defaulting to 7 days.
|
||||
• Verbose/debug protocol logging.
|
||||
The default bind host should be safe for use behind a local Caddy reverse proxy rather than listening publicly on every interface without explicit configuration.
|
||||
Logging
|
||||
Provide normal useful logging for:
|
||||
• Startup/shutdown.
|
||||
• Connections/disconnections.
|
||||
• Authentication failures.
|
||||
• Conversation routing errors.
|
||||
• Hermes agent errors.
|
||||
• Attachment handling.
|
||||
• Cleanup.
|
||||
• Approval/clarification lifecycle errors.
|
||||
Include a configurable verbose/debug logging mode useful while developing the Android client.
|
||||
Debug logging must still avoid printing:
|
||||
• Authentication secrets.
|
||||
• Provider API keys.
|
||||
• Sensitive authorization headers.
|
||||
Be thoughtful about whether raw chat content or tool results should appear in verbose logs; default to protecting user content unless explicitly configured otherwise.
|
||||
Security requirements
|
||||
Treat this adapter as access to a powerful personal Hermes agent capable of using tools on the host.
|
||||
Do not trust client-provided filesystem paths.
|
||||
Do not expose arbitrary filesystem downloads.
|
||||
Prevent path traversal.
|
||||
Authenticate all non-health-check functionality.
|
||||
Use constant-time secret comparison where appropriate.
|
||||
Validate message sizes and input structure.
|
||||
Apply reasonable limits to avoid trivially exhausting server memory with malformed WebSocket messages.
|
||||
Do not leak stack traces/internal paths to remote clients.
|
||||
Design it to be safely reverse-proxied through Caddy over HTTPS/WSS.
|
||||
CORS is not important because the intended client is native Android.
|
||||
If HTTP health-check endpoints are included, they should expose minimal information.
|
||||
Plugin quality
|
||||
Keep the code maintainable and idiomatic for the existing Hermes codebase.
|
||||
Reuse Hermes abstractions instead of copying large portions of gateway logic.
|
||||
Use async code consistently with Hermes where appropriate.
|
||||
Keep networking, Hermes adapter integration, attachment management, authentication, and protocol serialization separated enough to be understandable and testable.
|
||||
Add type hints.
|
||||
Add useful docstrings/comments where behavior is non-obvious.
|
||||
Avoid unnecessary dependencies. Prefer libraries Hermes already depends on when practical.
|
||||
Tests
|
||||
Include tests for important behavior, particularly:
|
||||
• Authentication success/failure.
|
||||
• Conversation operations.
|
||||
• Protocol serialization/parsing.
|
||||
• Attachment registration and secure download.
|
||||
• Rejection of arbitrary/path-traversal file access.
|
||||
• Seven-day attachment expiration/cleanup.
|
||||
• Tool-call event translation.
|
||||
• Approval round trip.
|
||||
• Clarification/choice round trip.
|
||||
• Cancellation of active runs.
|
||||
• WebSocket reconnect/recovery behavior where practical.
|
||||
Mock the Hermes agent/gateway where appropriate rather than making tests consume paid LLM API calls.
|
||||
Documentation
|
||||
Create documentation explaining:
|
||||
• Installation into Hermes.
|
||||
• Required environment variables.
|
||||
• Optional configuration.
|
||||
• Starting Hermes with Pheby enabled.
|
||||
• Example Caddy reverse-proxy configuration supporting WebSockets and attachment downloads.
|
||||
• How authentication works.
|
||||
• Pheby protocol specification.
|
||||
• Conversation operations.
|
||||
• Chat/message flow.
|
||||
• Tool events.
|
||||
• Approvals.
|
||||
• Clarifications.
|
||||
• Cancellation.
|
||||
• Model selection.
|
||||
• Reasoning effort.
|
||||
• Attachment delivery.
|
||||
• Attachment expiration.
|
||||
• Reconnection behavior.
|
||||
• Debugging/logging.
|
||||
• Security considerations.
|
||||
Include enough examples that I can implement the Kotlin/Compose client from the protocol documentation later.
|
||||
Implementation process
|
||||
Before writing the implementation, inspect the actual current Hermes abstractions and identify how the existing first-party platform adapters accomplish:
|
||||
• registration,
|
||||
• inbound messages,
|
||||
• outbound messages,
|
||||
• conversations/sessions,
|
||||
• tool progress,
|
||||
• deliverables,
|
||||
• approvals,
|
||||
• clarification prompts,
|
||||
• cancellation,
|
||||
• unsolicited Gateway messages.
|
||||
Prefer adapting those existing mechanisms over designing parallel ones.
|
||||
If one of my requirements cannot be implemented cleanly through Hermes's public plugin/adapter APIs, do not modify Hermes core to force it.
|
||||
Instead:
|
||||
1. Explain exactly what Hermes currently exposes.
|
||||
2. Explain what limitation prevents the requested behavior.
|
||||
3. Implement the cleanest supported approximation if one exists.
|
||||
4. Keep the protocol extensible so the proper feature can be added later if Hermes exposes it.
|
||||
Do not silently omit required functionality.
|
||||
At the end, give me:
|
||||
• The complete plugin source.
|
||||
• Installation/configuration files.
|
||||
• Tests.
|
||||
• Protocol documentation.
|
||||
• Caddy example.
|
||||
• A concise explanation of the architecture.
|
||||
• Any Hermes-version-specific limitations you discovered.
|
||||
• A list of the key protocol events/endpoints that the eventual Kotlin client needs to implement.
|
||||
The resulting Pheby adapter should require no Hermes core modifications and should survive normal Hermes upgrades as well as can reasonably be expected from its documented plugin API.
|
||||
Reference in New Issue
Block a user