This repository has no description
5.1 kB
131 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) is the gate on every API route; treat it
24 # like the hermes-api key.
25 systemd.services.hermes-voice = {
26 description = "hermes-voice: live voice conversations with Hermes";
27 wantedBy = [ "multi-user.target" ];
28 wants = [ "network-online.target" ];
29 after = [
30 "network-online.target"
31 "hermes-agent.service"
32 ];
33 serviceConfig = {
34 ExecStart = lib.escapeShellArgs [
35 (lib.getExe self.packages.${pkgs.stdenv.hostPlatform.system}.hermes-voice)
36 "--listen"
37 "[::]:${toString port}"
38 # The API server binds the retiolum address (hermes.nix), not
39 # loopback; traffic to the host's own address goes through lo.
40 "--hermes-url"
41 "http://${config.networking.hostName}.r:8642"
42 "--hermes-key-file"
43 "%d/hermes-key"
44 "--openai-key-file"
45 "%d/openai-key"
46 "--access-token-file"
47 "%d/access-token"
48 "--matrix-webhook"
49 "http://127.0.0.1:${toString webhookPort}/webhooks/voice-approval"
50 ];
51 LoadCredential = [
52 "hermes-key:${config.clan.core.vars.generators.hermes-api.files."api_key".path}"
53 "openai-key:${config.clan.core.vars.generators.hermes-voice-openai.files."api-key".path}"
54 "access-token:${config.clan.core.vars.generators.hermes-voice.files."access-token".path}"
55 ];
56 DynamicUser = true;
57 Restart = "on-failure";
58 CapabilityBoundingSet = "";
59 NoNewPrivileges = true;
60 PrivateDevices = true;
61 PrivateTmp = true;
62 ProtectHome = true;
63 ProtectSystem = "strict";
64 ProtectKernelTunables = true;
65 ProtectKernelModules = true;
66 ProtectControlGroups = true;
67 RestrictAddressFamilies = [
68 "AF_INET"
69 "AF_INET6"
70 ];
71 RestrictNamespaces = true;
72 LockPersonality = true;
73 SystemCallArchitectures = "native";
74 };
75 };
76
77 # Only retiolum: neoprism's proxy and devices on the mesh. Every other
78 # interface keeps the default DROP (configs/default.nix).
79 networking.firewall.interfaces.retiolum.allowedTCPPorts = [ port ];
80
81 # hermes-voice's side in the gateway (tools/hermes-voice/hermes-plugin): the
82 # matrix_dm_send and end_call tools, on for every platform like any plugin
83 # toolset (end_call refuses outside voice sessions), and /assist, which
84 # answers approvals asked in the Matrix DM through hermes-voice
85 # (authenticated with API_SERVER_KEY).
86 services.hermes-agent.extraPlugins = [
87 (pkgs.runCommand "hermes-voice" { } ''
88 cp -r ${../../tools/hermes-voice/hermes-plugin} $out
89 '')
90 ];
91 services.hermes-agent.environment.HERMES_VOICE_URL = "http://localhost:${toString port}";
92
93 # How hermes-voice asks for an approval when no call can hear it: a
94 # deliver_only route on the gateway's webhook platform (hermes.nix) posts
95 # the text to the Matrix home room, no agent turn.
96 services.hermes-agent.settings.platforms.webhook.extra.routes.voice-approval = {
97 deliver = "matrix";
98 deliver_only = true;
99 prompt = "{text}";
100 };
101
102 # OpenAI project key with GPT-Live access; billed per second of open voice
103 # session. persist=true so the prompt value is stored once.
104 clan.core.vars.generators.hermes-voice-openai.prompts.api-key = {
105 description = "OpenAI API key for hermes-voice (gpt-live-1)";
106 type = "hidden";
107 persist = true;
108 };
109
110 # Signing key of the Android wrapper (packages.hermes-voice-android); only
111 # read when signing, so nothing is deployed. The fingerprint goes into the
112 # page's assetlinks.json (tools/hermes-voice/flake-module.nix).
113 clan.core.vars.generators.hermes-voice-android-signing = {
114 files.keystore.deploy = false;
115 files.password.deploy = false;
116 files.fingerprint.secret = false;
117 runtimeInputs = [
118 pkgs.coreutils
119 pkgs.jdk17_headless
120 pkgs.openssl
121 ];
122 script = ''
123 openssl rand -hex 32 | tr -d '\n' > "$out/password"
124 keytool -genkeypair -noprompt -storetype PKCS12 -keystore "$out/keystore" \
125 -storepass:file "$out/password" -alias assist -dname CN=Assist \
126 -keyalg RSA -keysize 4096 -validity 36500
127 keytool -exportcert -rfc -keystore "$out/keystore" -storepass:file "$out/password" -alias assist |
128 openssl x509 -noout -fingerprint -sha256 | cut -d= -f2 > "$out/fingerprint"
129 '';
130 };
131}