This repository has no description
4.0 kB
102 lines
1{ lib, ... }:
2let
3 upstream = "http://coaxmetal.r:8642";
4
5 # Agent turns run for minutes and /v1/chat/completions streams SSE chunks the
6 # whole time. nginx's default 60s proxy_read_timeout would cut a long turn off
7 # mid-answer, and its response buffering would hold the stream back until the
8 # turn ended, so the phone would sit silent and then dump everything at once.
9 # coaxmetal is a laptop — asleep or off the mesh most of the day — so connect
10 # fails fast, to report "unreachable" instead of hanging on the assistant
11 # button.
12 streamingProxy = ''
13 proxy_read_timeout 1h;
14 proxy_send_timeout 1h;
15 proxy_buffering off;
16 proxy_cache off;
17 proxy_connect_timeout 5s;
18 limit_req zone=hermes_api burst=20 nodelay;
19 '';
20
21 proxied = {
22 recommendedProxySettings = true;
23 proxyPass = upstream;
24 extraConfig = streamingProxy;
25 };
26
27 # Endpoints WakeHermesClaw actually calls, taken from its sources
28 # (backend/HermesApiServerClient.kt and friends): the chat/runs lanes plus the
29 # read-only lanes its settings screens populate from. Everything else — most
30 # of the adapter's route table — is unreachable from outside.
31 allowed = [
32 "= /health"
33 "/v1/"
34 "= /api/config"
35 "= /api/available-models"
36 "= /api/model/options"
37 "= /api/skills"
38 "= /api/jobs"
39 "/api/jobs/"
40 "/api/profiles"
41 ];
42in
43{
44 # Public door to the Hermes API server on coaxmetal (machines/coaxmetal/hermes.nix).
45 # This is what the WakeHermesClaw app on massulus points at when it is not on
46 # retiolum: the assistant button posts to /v1/chat/completions here.
47 #
48 # SECURITY: behind this vhost sits an agent with terminal tools and, through
49 # the herdr wrapper, code execution as lass. The gateway's only check on this
50 # lane is the API_SERVER_KEY bearer token — there is no per-user allowlist like
51 # MATRIX_ALLOWED_USERS on the Matrix lane, and the app cannot do mTLS (it
52 # builds a bare OkHttpClient with no KeyManager), so a client certificate is
53 # not an option without patching it. That token is therefore a
54 # root-equivalent credential for that laptop: rotate it by regenerating the
55 # hermes-api var. The hardening below is damage control, not authentication.
56 services.nginx.virtualHosts."hermes.lassul.us" = {
57 enableACME = true;
58 forceSSL = true;
59
60 # Attachments: the app can send images with a prompt. 25M is generous for a
61 # phone photo and still bounds what an unauthenticated request can push
62 # into the proxy before hermes rejects it.
63 extraConfig = ''
64 client_max_body_size 25m;
65 '';
66
67 # Unlisted paths never reach hermes. Notably /api/pty and /api/ws — the
68 # Hermes Desktop terminal lane — are only served with backend.mode
69 # "serve"/"dashboard", which this host does not enable; 404ing them here
70 # means enabling it later cannot quietly publish a PTY.
71 locations = lib.genAttrs allowed (_: proxied) // {
72 "/".extraConfig = ''
73 return 404;
74 '';
75 };
76 };
77
78 # Public door to hermes-voice on coaxmetal (machines/coaxmetal/hermes-voice.nix):
79 # the voice page for the phone. hermes-voice gates every API route with its
80 # own access token, so nothing is filtered by path here. /delegate streams
81 # NDJSON for the length of a Hermes turn, so the streaming settings apply.
82 services.nginx.virtualHosts."voice.lassul.us" = {
83 enableACME = true;
84 forceSSL = true;
85 extraConfig = ''
86 client_max_body_size 256k;
87 '';
88 locations."/" = {
89 recommendedProxySettings = true;
90 proxyPass = "http://coaxmetal.r:8790";
91 extraConfig = streamingProxy;
92 };
93 };
94
95 # One phone driving one agent needs a trickle of requests; a turn is a single
96 # long-lived request, not a stream of them. This is sized to absorb the
97 # settings screens refreshing (hence the burst) while making credential
98 # stuffing against the bearer token pointless.
99 services.nginx.appendHttpConfig = ''
100 limit_req_zone $binary_remote_addr zone=hermes_api:1m rate=60r/m;
101 '';
102}