forked from
lass.pds.lassul.us/superconfig
This repository has no description
5.8 kB
151 lines
1{
2 config,
3 lib,
4 pkgs,
5 self,
6 ...
7}:
8let
9 port = 8790;
10 # The gateway's webhook platform (hermes.nix), loopback only.
11 webhookPort = config.services.hermes-agent.settings.platforms.webhook.extra.port;
12in
13{
14 imports = [ ../../configs/hermes-voice-vars.nix ];
15
16 # hermes-voice (tools/hermes-voice): live voice conversations with Hermes
17 # through OpenAI's gpt-live-1. Runs next to Hermes so its API key never
18 # leaves this host; reachable over retiolum and publicly as
19 # https://assist.lassul.us through neoprism (configs/hermes-proxy.nix).
20 #
21 # SECURITY: the page drives Hermes, which has terminal tools and code
22 # execution as lass (machines/coaxmetal/hermes.nix). The access token
23 # (configs/hermes-voice-vars.nix) and the Android app's token
24 # (hermes-voice-app below) are the gates on every API route; treat both
25 # like the hermes-api key. The app takes calls from the headset button with
26 # the phone locked, approvals by voice included.
27 systemd.services.hermes-voice = {
28 description = "hermes-voice: live voice conversations with Hermes";
29 wantedBy = [ "multi-user.target" ];
30 wants = [ "network-online.target" ];
31 after = [
32 "network-online.target"
33 "hermes-agent.service"
34 ];
35 serviceConfig = {
36 ExecStart = lib.escapeShellArgs [
37 (lib.getExe self.packages.${pkgs.stdenv.hostPlatform.system}.hermes-voice)
38 "--listen"
39 "[::]:${toString port}"
40 # The API server binds the retiolum address (hermes.nix), not
41 # loopback; traffic to the host's own address goes through lo.
42 "--hermes-url"
43 "http://${config.networking.hostName}.r:8642"
44 "--hermes-key-file"
45 "%d/hermes-key"
46 "--openai-key-file"
47 "%d/openai-key"
48 "--access-token-file"
49 "%d/access-token"
50 "--app-token-file"
51 "%d/app-token"
52 "--matrix-webhook"
53 "http://127.0.0.1:${toString webhookPort}/webhooks/voice-approval"
54 ];
55 LoadCredential = [
56 "hermes-key:${config.clan.core.vars.generators.hermes-api.files."api_key".path}"
57 "openai-key:${config.clan.core.vars.generators.hermes-voice-openai.files."api-key".path}"
58 "access-token:${config.clan.core.vars.generators.hermes-voice.files."access-token".path}"
59 "app-token:${config.clan.core.vars.generators.hermes-voice-app.files.token.path}"
60 ];
61 DynamicUser = true;
62 Restart = "on-failure";
63 CapabilityBoundingSet = "";
64 NoNewPrivileges = true;
65 PrivateDevices = true;
66 PrivateTmp = true;
67 ProtectHome = true;
68 ProtectSystem = "strict";
69 ProtectKernelTunables = true;
70 ProtectKernelModules = true;
71 ProtectControlGroups = true;
72 RestrictAddressFamilies = [
73 "AF_INET"
74 "AF_INET6"
75 ];
76 RestrictNamespaces = true;
77 LockPersonality = true;
78 SystemCallArchitectures = "native";
79 };
80 };
81
82 # Only retiolum: neoprism's proxy and devices on the mesh. Every other
83 # interface keeps the default DROP (configs/default.nix).
84 networking.firewall.interfaces.retiolum.allowedTCPPorts = [ port ];
85
86 # hermes-voice's side in the gateway (tools/hermes-voice/hermes-plugin): the
87 # matrix_dm_send and end_call tools, on for every platform like any plugin
88 # toolset (end_call refuses outside voice sessions), and /assist, which
89 # answers approvals asked in the Matrix DM through hermes-voice
90 # (authenticated with API_SERVER_KEY).
91 services.hermes-agent.extraPlugins = [
92 (pkgs.runCommand "hermes-voice" { } ''
93 cp -r ${../../tools/hermes-voice/hermes-plugin} $out
94 '')
95 ];
96 services.hermes-agent.environment.HERMES_VOICE_URL = "http://localhost:${toString port}";
97
98 # How hermes-voice asks for an approval when no call can hear it: a
99 # deliver_only route on the gateway's webhook platform (hermes.nix) posts
100 # the text to the Matrix home room, no agent turn.
101 services.hermes-agent.settings.platforms.webhook.extra.routes.voice-approval = {
102 deliver = "matrix";
103 deliver_only = true;
104 prompt = "{text}";
105 };
106
107 # OpenAI project key with GPT-Live access; billed per second of open voice
108 # session. persist=true so the prompt value is stored once.
109 clan.core.vars.generators.hermes-voice-openai.prompts.api-key = {
110 description = "OpenAI API key for hermes-voice (gpt-live-1)";
111 type = "hidden";
112 persist = true;
113 };
114
115 # Bearer token of the Android app (tools/hermes-voice/android), checked by
116 # hermes-voice next to the page's access token; the proxy passes it through
117 # on assist.lassul.us/app/ without the web login (configs/hermes-proxy.nix).
118 # Paste it into the app once:
119 # clan vars get coaxmetal hermes-voice-app/token
120 clan.core.vars.generators.hermes-voice-app = {
121 files.token = { };
122 runtimeInputs = [
123 pkgs.coreutils
124 pkgs.openssl
125 ];
126 script = ''
127 openssl rand -hex 32 | tr -d '\n' > "$out/token"
128 '';
129 };
130
131 # Signing key of the Android app (packages.hermes-voice-android); only read
132 # when signing, so nothing is deployed.
133 clan.core.vars.generators.hermes-voice-android-signing = {
134 files.keystore.deploy = false;
135 files.password.deploy = false;
136 files.fingerprint.secret = false;
137 runtimeInputs = [
138 pkgs.coreutils
139 pkgs.jdk17_headless
140 pkgs.openssl
141 ];
142 script = ''
143 openssl rand -hex 32 | tr -d '\n' > "$out/password"
144 keytool -genkeypair -noprompt -storetype PKCS12 -keystore "$out/keystore" \
145 -storepass:file "$out/password" -alias assist -dname CN=Assist \
146 -keyalg RSA -keysize 4096 -validity 36500
147 keytool -exportcert -rfc -keystore "$out/keystore" -storepass:file "$out/password" -alias assist |
148 openssl x509 -noout -fingerprint -sha256 | cut -d= -f2 > "$out/fingerprint"
149 '';
150 };
151}