One http-kit server (dvergr.web.server) serves three things over the daemon:
app/ directory served as a static site under /apps/<room>/;/api/v1/, one route per operation of dvergr.ops, the same
operations MCP serves (doc/mcp.md).The server is optional. The daemon runs without it, and the TUI, Telegram, MCP and nREPL are other frontends over the same daemon.
clojure -M:cli --web # daemon + nREPL + TUI + web on 127.0.0.1:17880
clojure -M:cli --no-tui --web # server box: no TUI
clojure -M:cli --web --web-port 9090 # another port
A daemon started any other way (as a library, daemon/start!) starts the server when its
config has :http:
:http {:port 17880 :ip "127.0.0.1"
:allowed-hosts ["dvergr.example.com"] ; names a reverse proxy sends
:allowed-origins ["https://dvergr.example.com"]} ; origins whose writes are accepted
The JSON API needs reitit, which is not a dependency of the dvergr library. The :cli,
:dev and :test aliases bring it, and the :web alias adds it to any other command
(clojure -M:local:web). Without it the daemon logs daemon/web-server-skipped and runs
headless. The interactive Swagger UI at /api/v1/docs also needs
metosin/reitit-swagger-ui, which no alias includes; /api/v1/swagger.json works without it.
From a REPL: (dvergr.web.server/start! daemon :port 17880) and (dvergr.web.server/stop!).
One server runs per process.
Neither the dashboard nor the API authenticates anyone, and the API serves every operation of
dvergr.ops, admin ones included. The server therefore binds to loopback by default. Pass
another address (--web-bind, :http {:ip …}) only behind something that authenticates,
such as a reverse proxy.
Loopback keeps other machines out, but not the pages the user's own browser shows.
dvergr.web.guard checks every request before it is routed:
:allowed-hosts entry. A DNS name rebound to 127.0.0.1 fails here, reads included.Origin, or Referer without it) must name this server's own or an :allowed-origins
entry. A client such as curl sends neither and passes.X-CSRF-Token header or the csrf form field). The token is an HMAC of the session cookie
under a secret made when the server starts.application/json (else 415), which a cross-site page
can send only after a CORS preflight, and the server grants none.Requests are limited to 1 MiB. Every route that changes state is a POST.
/ (or /dashboard) has four sections, each polled: System (/api/system), Agents
(/api/agents), Rooms (a fork tree, /api/rooms, with fork, merge, discard and delete
buttons) and Schedules (/api/schedules). /rooms/<slug> is a room's page: its messages,
stats and per-agent context bars, a box to post as the user (POST /rooms/<slug>/post) and a
microphone that posts a voice message (POST /rooms/<slug>/voice, transcribed by
dvergr.audio.stt; doc/media.md). /agents/<id>/config edits an agent; its form uses the
same field spec (dvergr.agent.fields) as the agent_create/agent_update operations.
The dashboard's room and agent actions call dvergr.ops through dvergr.web.ops, so they act
exactly as the MCP tools of the same name do. The HTML fragment routes under /api/ (without
v1) are for the dashboard, not a stable interface.
/apps/<room-slug>/ serves the files under app/ in the room's workspace (its Geschichte
repository), for example app/index.html an agent wrote. A fork has its own workspace, so a
fork's app previews the fork's files; merging the fork publishes them. Serving is static and
GET-only: no code runs on the server, paths cannot leave app/ (symlinks included), and
dotfiles are not served.
dvergr.web.api derives every route from dvergr.ops/specification. An operation
:room/messages is the route /api/v1/room_messages, the same name as its MCP tool. A read is
a GET with its arguments as query parameters; a write is a POST with a JSON body. The
operation's malli schema coerces the request (a request that does not match is a 400); the
result is the operation's data as JSON.
curl -s 'http://127.0.0.1:17880/api/v1/room_list'
curl -s 'http://127.0.0.1:17880/api/v1/room_messages?room=general&limit=10'
curl -s -X POST -H 'Content-Type: application/json' \
-d '{"room":"general","text":"hello"}' http://127.0.0.1:17880/api/v1/room_post
curl -s 'http://127.0.0.1:17880/api/v1/swagger.json' # the OpenAPI description
An operation that fails returns its status (4xx when the request was wrong, as
dvergr.ops/error-status maps it) and {"error": …, "op": …}; without a daemon the API answers
503. /api/health reports the daemon status and the number of online agents.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |