This repository has no description
4.5 kB
127 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 = 8644;
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 posts the text to the Matrix home room through the
92 # gateway, no agent turn. Signed with WEBHOOK_SECRET, which is the API
93 # server key (hermes-env in hermes.nix), so no secret is added.
94 services.hermes-agent.settings.platforms.webhook = {
95 enabled = true;
96 extra = {
97 host = "127.0.0.1";
98 port = webhookPort;
99 routes.voice-approval = {
100 deliver = "matrix";
101 deliver_only = true;
102 prompt = "{text}";
103 };
104 };
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 # What the page sends as its bearer token. Read it out for the phone with
116 # clan vars get coaxmetal hermes-voice/access-token
117 clan.core.vars.generators.hermes-voice = {
118 files."access-token" = { };
119 runtimeInputs = [
120 pkgs.coreutils
121 pkgs.openssl
122 ];
123 script = ''
124 openssl rand -hex 32 | tr -d '\n' > "$out/access-token"
125 '';
126 };
127}