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