DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

GnaneshKunal /

GnaneshKunal/dsh-hermes-facade

Verified

Drive a local DeepSeek Harness agent from the Hermex iOS app - a DSH host plugin that impersonates a hermes-webui server (HTTP + SSE): sessions, streaming tokens, thinking, tool cards, approvals, images and projects.

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHubProject homepage
READMESource: main@b40da431

dsh-hermes-facade

Makes your local DeepSeek Harness look like a hermes-webui server, so the free, MIT-licensed iOS app Hermex (4.7 stars, App Store) can drive your agent from an iPhone or iPad: sessions, streaming replies, thinking, tool cards, approvals and clarifications, in native SwiftUI instead of a web view.

Hermex (iOS)  --HTTPS/SSE-->  tailscale serve :9443  -->  127.0.0.1:9120
                                                               |
                                                      dsh-hermes-facade
                                                               | in-process
                                                      dsh typertGateway

Why a facade and not a fork

Hermex is a client, not a protocol. Its main app speaks the hermes-webui HTTP+SSE contract; a separate JSON-RPC gateway stack is used only by the Bot screens. Impersonating the webui host is therefore the smallest possible change: no Xcode, no App Store listing, no fork to maintain, and the App Store app keeps updating itself.

The facade is a DSH host plugin. It owns one loopback port, translates between the two wire formats, and never touches dsh's own port-3080 origin (which is already public through Tailscale Funnel, and whose /api is dsh's own fenced RPC namespace).

Install

dsh plugin --profile web add dsh-hermes-facade

Then add the package to the profile bundle list in ~/.dsh/profiles/web/package.json:

"dsh": { "profile": { "bundles": [ "... existing ...", "dsh-hermes-facade" ] } }

Restart the host so the plugin loads:

dsh web

The first boot prints a generated password once:

hermes-facade: FIRST RUN password = xxxxxxxxxxxx

It is stored hashed at ~/.dsh/hermes-facade/password.json (0600). To reset it, delete that file and restart.

Expose it to the phone

Tailnet only, never Funnel:

tailscale serve --bg --https=9443 http://127.0.0.1:9120     # webui contract
tailscale serve --bg --https=9444 http://127.0.0.1:9121     # Hermes contract
tailscale serve status

The second rule is only useful once you add the Hermes-protocol listener below as a separate server on the phone: the app decides a server's kind once, from GET /api/status, so one URL cannot be both.

In Hermex: Add server, URL = https://..ts.net:9443, password = the one from first boot.

Remove the exposure with:

tailscale serve --https=9443 off      # NOT 'serve reset': that also kills Funnel and :8443

Reaching it from the phone

Three addresses work, all plain HTTP except the last. Hermex permits insecure HTTP to local-network hosts and to the Tailscale 100.64.0.0/10 range, so no certificate is needed for the first two.

Where URL
same Wi-Fi http://:9120
Tailscale, plain HTTP (no DNS, no TLS) http://:9120
Tailscale, HTTPS through serve https://..ts.net:9443

Find the addresses with:

ipconfig getifaddr en0      # Wi-Fi LAN address
tailscale ip -4             # Tailscale address

The LAN and Tailscale listeners exist because cordis.patch.yml sets lanHost: auto. Without it the facade is loopback-only and only the serve rule can reach it. iOS asks for Local Network permission the first time; allow it.

If the HTTPS address hangs, use the plain-HTTP Tailscale IP: it skips MagicDNS and the TLS handshake entirely.

From a checkout

git clone git@github.com:GnaneshKunal/dsh-hermes-facade.git
cd dsh-hermes-facade
npm run check && npm test
dsh plugin --profile web add ~/self/dsh-hermes-facade

Develop without restarting dsh

dsh web loads plugins at boot, so host-plugin changes need a restart. To iterate on the wire contract, run the same server standalone against a mock agent:

node scripts/standalone.mjs --port 9120

That is what npm test uses, and it exercises the whole HTTP/SSE surface with curl.

Hermes-protocol listener (Hermex 2.x)

Hermex 2.0 talks to a Hermes host by default (REST + JSON-RPC over a WebSocket) and marks webui servers as Legacy. This plugin can serve both contracts, on two ports, from one process:

Port Contract In the app
9120 hermes-webui (HTTP + SSE) the original server, marked Legacy; shows the one-time Move to Hermes invite
9121 Hermes host (REST + WS JSON-RPC) a native Hermes server; no invite, no Legacy badge

The gateway listener does not reimplement DSH: it translates to the webui facade over loopback with an internal token, so sessions, streaming, approvals, clarifications and export keep one implementation. It covers chat, sessions, approvals, clarifications, models, commands, export and the session write fence the list's rename/pin/archive actions read first, the read-only in-chat Files browser and the in-chat Git menu (reads plus writes), Tasks/cron, Memory (read/write), Skills (read-only), goal (read/command; no loop/heartbeat), Usage (tokens + cost from dsh-cost-meter), chat attachments (images + documents) and in-chat Git writes (stage/unstage/revert, commit, push, branch switch); Bots and session import are not implemented and answer -32601 (the app hides those controls). Details, endpoint table and gaps: docs/06-hermes-gateway.md.

config:
  gatewayEnabled: true      # false keeps only the webui listener
  gatewayPort: 9121
  gatewayBindHost: 127.0.0.1
  gatewayLanHost: auto      # also bind Wi-Fi LAN + Tailscale IPv4 (plain HTTP)

Both listeners share the password file, the cookie secret and the state directory. Expose the second one with tailscale serve --bg --https=9444 http://127.0.0.1:9121 and add it in Hermex as its own server.

What is supported

Surface State
health, auth (password + signed cookie) full
session list, session detail, history full: newest 200 + 50-message scroll-back pages within the newest 800
session search full: id, message-content and title matches, content matches with an excerpt; empty queries are empty results
chat telemetry full: live session.usage plus usage on message.complete (context used/max, input/output tokens); no cost — DSH has no pricing
chat start / SSE stream / cancel / stream status full; a failed or cancelled turn ends the stream with error/cancel instead of done; the Legacy start body's profile is ignored (attachments are staged on the Hermes listener)
token deltas, thinking, tool cards full: live and reloaded transcripts carry tool results, error verdicts and durations
approvals (once / session / always / deny) full: a reconnect keeps command/description, expired prompts answer 409 {"stale":true} and are retracted. session/always are standing grants keyed by tool name (no command-pattern allowlist)
clarifications / agent questions full: live and re-attached cards carry question/choices_offered and answer correctly
reasoning effort full: levels come from the model catalog
model picker full: applied through session/selectModel; the catalog id/provider is resolved and the reasoning effort carried over
live model overlay / providers full (derived): both read the DSH model catalog; providers is read-only
workspaces partial: list only; add/remove/rename/reorder refused or 404
workspace file browser (/api/list, /api/file) full: read-only
in-chat Files on the Hermes listener (/api/fs/*) read-only: listing, 512 KiB text preview, binary downloads, fenced to the chat folder
in-chat Git on the Hermes listener (/api/fs/git-root, /api/git/*) status, changes, diffs, branches, sha; stage/unstage/revert, commit, push and branch switch; every write refuses with 409 while a turn runs; credential files filtered and refused
profiles (DSH has agent presets, not profiles) partial: one static profile
session rename / branch (fork) full
session pin / archive full: rows report the real flags; include_archived/archived_limit list archived chats
session export full (bounded): JSON, HTML or markdown download with the filename in Content-Disposition
session delete / duplicate / compress / clear / undo / retry / truncate / move / update not implemented (refusals)
projects (one per DSH workspace) partial: read-only list
Tasks/cron on the Hermes listener (/api/cron/*) full: list/create/edit/pause/resume/trigger/delete, run history; local delivery only, no cost
Memory on the Hermes listener read/write: the global $DSH_HOME/AGENTS.md as MEMORY.md, saved atomically (2 MiB cap); overwrite=false is a 409; no USER.md, no soul
Live Working / Needs-you on the Hermes listener session.active_list + sessions.changed: starting before a turn goes live, Working while it runs, Needs-you while an approval/clarify is open; idle rows omitted
Goal on the Hermes listener /goal in the slash catalogue and through the host command (command.dispatch), a goal snapshot (session.control.read) and live session.control.update; loop/heartbeat are absent, DSH-only verbs are refused
Dictation on the Hermes listener POST /api/audio/transcribe over DSH speech/transcribe: WAV passes through; m4a/mp4/aac is converted to canonical 16 kHz mono WAV (afconvert on macOS, ffmpeg elsewhere); honest 400 when no converter
Skills on the Hermes listener read-only: DSH's catalogue (newest session) and each skill's body; package-provided skills resolve their assets/*.md, pathless ones fall back to the description; the disable toggle is refused
Usage/Insights on the Hermes listener sessions (archived + subagent), input/output/cache tokens, per-model rows, daily chart; cost from dsh-cost-meter (ledger USD), messages ≈0
Chat attachments on the Hermes listener images: staged, then sent as a native DSH image part; the app's vision reference stays in the transcript so the chip survives a reload; documents: staged and referenced as @file:<path>; both preview through the uploads root
insights (Legacy), kanban stubbed: screens open empty or unavailable
bots, push, Move to Hermes different transport (Hermes gateway); not served
Hermes host contract (second listener, :9121) v1.16: chat/streaming with context/token telemetry and image/document attachments, sessions incl. the write fence the list actions need and the list's search, approvals, clarifications, models, commands, export, Tasks/cron (local delivery only, no cost), Memory (read/write, overwrite:false 409) and Skills (read-only), dictation (STT; m4a→canonical WAV), Live Working / Needs-you (starting/working/waiting), the goal read/command/update surface (no loop/heartbeat), active-turn replay for reattach, Usage/Insights (tokens + dsh-cost-meter cost), the read-only in-chat Files browser (fenced to the chat folder) and the in-chat Git menu (reads plus stage/unstage/revert, commit, push and branch switch behind an idle-turn guard); kanban/bots and import answer -32601 or 400 (the per-surface state and plan are docs/05 §2 and docs/07)

The verified matrix — every surface, the endpoint behind it, what the phone shows, and whether it is bridgeable here — is docs/05-compatibility-matrix.md. docs/00 is the frozen design note.

A 404 is not an empty state: Hermex shows "The server endpoint was not found. Check that the URL points to your configured server.", and only 408/502/503/504 fall back to cached rows. The approval/clarify family must never 404 — an unanswered pending hangs the run.

Endpoints

GET /health, GET /api/auth/status, POST /api/auth/login, POST /api/auth/logout, GET /api/sessions, GET /api/session, POST /api/chat/start, GET /api/chat/stream, GET /api/chat/cancel, GET /api/chat/stream/status, GET /api/approval/pending, GET /api/approval/stream, POST /api/approval/respond, GET /api/clarify/pending, GET /api/clarify/stream, POST /api/clarify/respond, GET /api/models, GET /api/commands, GET /api/workspaces, GET /api/profiles, GET /api/reasoning, POST /api/reasoning, POST /api/default-model, GET /api/list, GET /api/file, GET /api/media, GET /api/file/raw, GET /api/projects, GET /api/session/export, GET /api/models/live, GET /api/providers.

Hermes listener (:9121): GET /api/status, GET /api/health, POST /auth/password-login, POST /auth/logout, GET /api/auth/me, POST /api/auth/ws-ticket, GET /api/auth/providers, GET /api/profiles/active, GET /api/profiles, GET /api/config, GET /api/models, GET /api/commands, GET /api/sessions, GET /api/sessions/search, GET /api/sessions/{id}, GET /api/sessions/{id}/messages, GET /api/sessions/{id}/mutation-snapshot, GET /api/sessions/{id}/export, PATCH /api/sessions/{id} (title/pin/archive behind the write fence), POST /api/sessions/import, GET /api/fs/list, GET /api/fs/read-text, GET /api/fs/download (read-only, fenced to the chat folder), GET /api/fs/git-root, GET /api/git/status, GET /api/git/branches, GET /api/git/review/list, GET /api/git/review/diff, GET /api/git/file-diff, GET /api/git/review/rev-parse, GET /api/git/review/commit-context, POST /api/git/review/{stage,unstage,revert,commit,push}, POST /api/git/branch/switch (the Git panel's reads and writes), GET /api/cron/jobs, POST /api/cron/jobs, GET/PUT/DELETE /api/cron/jobs/{id}, POST /api/cron/jobs/{id}/{pause,resume,trigger}, GET /api/cron/jobs/{id}/runs, GET /api/cron/delivery-targets (Tasks; local delivery only), GET /api/config, GET/PUT /api/profiles/{name}/soul, GET /api/files/read, POST /api/files/upload (Memory; atomic MEMORY.md save, AGENTS.md allowlist), POST /api/audio/transcribe (dictation over speech/transcribe; m4a→WAV), GET /api/skills, GET /api/skills/content, PUT /api/skills/toggle (Skills; read-only catalogue and SKILL.md, toggle refused), GET /api/analytics/usage, GET /api/analytics/models (Usage; token-only, cost 0), WS /api/ws (JSON-RPC with server-to-client approval/clarify requests).

Diagnostics

Everything the facade sees is in ~/.dsh/hermes-facade/:

File Written Contents
access.log every request, when logRequests: true method, path, query, status, ms, user-agent, plus unknown/stub/effort/error annotations
unknown-endpoints.log always every call to an endpoint the facade does not implement, with its request body
requests.log every JSON body, when logRequests: true the literal bodies the iOS app posts
tail -f ~/.dsh/hermes-facade/unknown-endpoints.log   # what the app wants that we do not serve
tail -f ~/.dsh/hermes-facade/access.log              # status and duration of everything

GET /health also reports active_streams and the transcript history page/window sizes. The app ignores keys it does not know; they make it easy to see which build is live after a hot reload or restart, and whether a turn is still streaming.

client_gone: true on an access-log line means the client hung up before the answer about a request the facade still finished — that is what a client-side timeout looks like from the server, and it explains "the app says the server is unreachable but the server log says 200".

stub marks a request answered from a fixed empty/refusal payload, so a screen that looks broken can be traced to a route that was never implemented.

Both files rotate at 1 MiB and keep one previous generation. Logging never throws: a full disk or an unwritable directory cannot break a request.

Security

  • binds 127.0.0.1 only; reachable solely through the serve rule you add
  • password: scrypt, constant-time compare, 5 attempts / 15 min per address
  • cookie: HMAC-signed, HttpOnly, SameSite=Lax, Secure when served over TLS
  • the facade acts as an owner peer inside dsh, so remote-access rules do not apply; the cookie is the boundary. Do not widen the bind address.

Troubleshooting

Symptom Cause
Password rejected everywhere cookie not sent: check the URL scheme, and that Path=/ is intact
"The server endpoint was not found" with /health fine the app called a route we do not serve: tail ~/.dsh/hermes-facade/unknown-endpoints.log names it, with the body
A screen works on the second try, fails on the first cold catalog/workspace read outlived the client's patience: access.log shows the ms and client_gone: true. Reads are bounded to 8 s and the catalog is warmed at boot.
Effort chip always says "medium" fixed: the facade read the wrong projection shape, so it reported the fallback and sent effort changes without a provider/model
a run hangs with no approval card approval endpoint returned 404 instead of pending:null
messages roll back instantly /api/chat/start returned no stream_id
empty session list /api/sessions failed; the app falls back to its cache

Docs

  • docs/00-DESIGN.md: architecture, mapping tables, design snapshot
  • docs/05-compatibility-matrix.md: the verified matrix for both transports — Legacy webui (:9120) and Hermes host (:9121)
  • docs/06-hermes-gateway.md: the Hermes-protocol listener (port 9121), its methods, events and gaps
  • docs/08-upstream-gaps.md: the durable catalogue of surfaces DSH cannot honestly provide yet, with evidence and the smallest upstream unblock
  • docs/07-v2-roadmap.md: parity with the Legacy server first, then the V2-only screens, with effort and acceptance criteria
  • docs/01-webui-api-contract.md: hermes-webui server contract
  • docs/02-hermex-client-wire.md: the client decoder, read from Swift
  • docs/03-dsh-host-integration.md: dsh host API surface
  • docs/04-packaging-exposure.md: packaging, ports, Tailscale, test plan

MIT.

—/ 5

No ratings yet

Verified DSH bundle

Commit b40da431cc36

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout