This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

tools.hermes-voice: web app manifest, icons, assetlinks.json

The voice page gets a web app manifest (standalone, colors of
style.css) and 192/512 px icons, drawn with imagemagick at build time
(date chunks excluded so the PNGs are reproducible). Both are needed
for the page to run as a Trusted Web Activity in the Android wrapper.

/.well-known/assetlinks.json delegates handle_all_urls to the wrapper
(us.lassul.assist) with the SHA-256 fingerprint of its signing
certificate, so Chrome verifies the app and drops the URL bar. Like
the static files it is served without the access token, and neoprism
already proxies every path of assist.lassul.us.

The signing key comes from the new hermes-voice-android-signing
generator on coaxmetal: a PKCS12 keystore and its password (secret,
deploy = false since only signing reads them) and the fingerprint as a
public file. The package reads the fingerprint from vars/ with
pathExists like the nix-cache key, so evaluation works before the
generator ran; assetlinks.json is then simply missing (404), which is
why the static routes now answer 404 for files not in the static dir.

+91 -4
+22
machines/coaxmetal/hermes-voice.nix
··· 116 116 openssl rand -hex 32 | tr -d '\n' > "$out/access-token" 117 117 ''; 118 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 + }; 119 141 }
+40 -3
tools/hermes-voice/flake-module.nix
··· 1 - { ... }: 1 + { self, ... }: 2 2 { 3 3 perSystem = 4 - { pkgs, ... }: 4 + { lib, pkgs, ... }: 5 + let 6 + # The Android wrapper's package name (android/AndroidManifest.xml). 7 + androidPackage = "us.lassul.assist"; 8 + # Written by `clan vars generate coaxmetal` (machines/coaxmetal/hermes-voice.nix); 9 + # until then there is no assetlinks.json and Chrome opens the app as a 10 + # Custom Tab. 11 + fingerprint = self + "/vars/per-machine/coaxmetal/hermes-voice-android-signing/fingerprint/value"; 12 + assetlinks = pkgs.writeText "assetlinks.json" ( 13 + builtins.toJSON [ 14 + { 15 + relation = [ "delegate_permission/common.handle_all_urls" ]; 16 + target = { 17 + namespace = "android_app"; 18 + package_name = androidPackage; 19 + sha256_cert_fingerprints = [ (lib.removeSuffix "\n" (builtins.readFile fingerprint)) ]; 20 + }; 21 + } 22 + ] 23 + ); 24 + icon = pkgs.runCommand "hermes-voice-icon" { nativeBuildInputs = [ pkgs.imagemagick ]; } '' 25 + mkdir $out 26 + for size in 192 512; do 27 + magick -size 512x512 xc:'#111' -fill '#4fb477' -draw 'circle 256,256 256,128' \ 28 + -resize ''${size}x$size -strip -define png:exclude-chunks=date,time \ 29 + $out/icon-$size.png 30 + done 31 + ''; 32 + static = pkgs.runCommand "hermes-voice-static" { } '' 33 + cp -r ${./static} $out 34 + chmod u+w $out 35 + cp ${icon}/*.png $out/ 36 + ${lib.optionalString (builtins.pathExists fingerprint) '' 37 + mkdir $out/.well-known 38 + cp ${assetlinks} $out/.well-known/assetlinks.json 39 + ''} 40 + ''; 41 + in 5 42 { 6 43 # Full-duplex voice conversations with Hermes: gpt-live-1 in the 7 44 # browser, delegating real work to the Hermes API server. ··· 10 47 name = "hermes-voice"; 11 48 # A later --config on the command line overrides the packaged one. 12 49 text = '' 13 - export HERMES_VOICE_STATIC=${./static} 50 + export HERMES_VOICE_STATIC=${static} 14 51 exec ${pkgs.python3}/bin/python3 ${./hermes_voice.py} --config ${./config.json} "$@" 15 52 ''; 16 53 }).overrideAttrs
+6 -1
tools/hermes-voice/hermes_voice.py
··· 612 612 "/": ("index.html", "text/html; charset=utf-8"), 613 613 "/app.js": ("app.js", "text/javascript; charset=utf-8"), 614 614 "/style.css": ("style.css", "text/css; charset=utf-8"), 615 + "/manifest.webmanifest": ("manifest.webmanifest", "application/manifest+json"), 616 + "/icon-192.png": ("icon-192.png", "image/png"), 617 + "/icon-512.png": ("icon-512.png", "image/png"), 618 + # Missing until the Android signing key exists. 619 + "/.well-known/assetlinks.json": (".well-known/assetlinks.json", "application/json"), 615 620 } 616 - if self.path not in names: 621 + if self.path not in names or not (self.static / names[self.path][0]).is_file(): 617 622 self.send_error(HTTPStatus.NOT_FOUND) 618 623 return 619 624 name, content_type = names[self.path]
+1
tools/hermes-voice/static/index.html
··· 8 8 /> 9 9 <meta name="theme-color" content="#111" /> 10 10 <title>Hermes voice</title> 11 + <link rel="manifest" href="/manifest.webmanifest" /> 11 12 <link rel="stylesheet" href="/style.css" /> 12 13 </head> 13 14 <body>
+22
tools/hermes-voice/static/manifest.webmanifest
··· 1 + { 2 + "name": "Hermes voice", 3 + "short_name": "Assist", 4 + "start_url": "/", 5 + "display": "standalone", 6 + "theme_color": "#111111", 7 + "background_color": "#111111", 8 + "icons": [ 9 + { 10 + "src": "/icon-192.png", 11 + "sizes": "192x192", 12 + "type": "image/png", 13 + "purpose": "any maskable" 14 + }, 15 + { 16 + "src": "/icon-512.png", 17 + "sizes": "512x512", 18 + "type": "image/png", 19 + "purpose": "any maskable" 20 + } 21 + ] 22 + }