|
|
|
@@ -0,0 +1,209 @@
|
|
|
|
|
{
|
|
|
|
|
config,
|
|
|
|
|
lib,
|
|
|
|
|
pkgs,
|
|
|
|
|
...
|
|
|
|
|
}: let
|
|
|
|
|
serviceName = "honcho";
|
|
|
|
|
image = "ghcr.io/plastic-labs/honcho:v3.0.6";
|
|
|
|
|
|
|
|
|
|
apiIp = "10.89.0.24";
|
|
|
|
|
deriverIp = "10.89.0.25";
|
|
|
|
|
redisIp = "10.89.0.26";
|
|
|
|
|
|
|
|
|
|
postgresHost = "10.89.0.1";
|
|
|
|
|
postgresPort = config.m3ta.ports.get "postgres";
|
|
|
|
|
honchoPort = config.m3ta.ports.get "honcho";
|
|
|
|
|
|
|
|
|
|
# m3-atlas Netbird mesh address, discovered from `netbird status -d`.
|
|
|
|
|
# Binding the host port here keeps self-hosted Honcho off public interfaces.
|
|
|
|
|
netbirdBindAddress = "100.81.142.56";
|
|
|
|
|
netbirdRange = "100.64.0.0/16";
|
|
|
|
|
|
|
|
|
|
dbName = "honcho";
|
|
|
|
|
dbUser = "honcho";
|
|
|
|
|
redisName = "${serviceName}-redis";
|
|
|
|
|
runtimeDirectory = "/run/${serviceName}";
|
|
|
|
|
runtimeEnvFile = "${runtimeDirectory}/env";
|
|
|
|
|
|
|
|
|
|
# Keep auth disabled for the first deployment because Honcho clients need
|
|
|
|
|
# generated JWTs. The JWT secret is still provisioned so enabling auth later is
|
|
|
|
|
# a one-line change here plus client token generation.
|
|
|
|
|
authUseAuth = false;
|
|
|
|
|
|
|
|
|
|
sharedEnvironment = {
|
|
|
|
|
CACHE_ENABLED = "true";
|
|
|
|
|
CACHE_URL = "redis://${redisName}:6379/0?suppress=true";
|
|
|
|
|
LOG_LEVEL = "INFO";
|
|
|
|
|
TELEMETRY_ENABLED = "false";
|
|
|
|
|
VECTOR_STORE_MIGRATED = "false";
|
|
|
|
|
VECTOR_STORE_TYPE = "pgvector";
|
|
|
|
|
AUTH_USE_AUTH = lib.boolToString authUseAuth;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
sharedEnvironmentFiles = [
|
|
|
|
|
runtimeEnvFile
|
|
|
|
|
config.age.secrets."${serviceName}-selfhost-env".path
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
webNetwork = ip: [
|
|
|
|
|
"--add-host=postgres:${postgresHost}"
|
|
|
|
|
"--network=web:ip=${ip}"
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
# The shared web network is intentionally internal. API and deriver also join
|
|
|
|
|
# this egress-only network so LLM provider calls can leave the host without
|
|
|
|
|
# exposing any extra inbound ports.
|
|
|
|
|
networksWithEgress = ip:
|
|
|
|
|
(webNetwork ip)
|
|
|
|
|
++ [
|
|
|
|
|
"--network=${serviceName}-egress"
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
apiHealthCmd = ''/app/.venv/bin/python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=2).read()"'';
|
|
|
|
|
in {
|
|
|
|
|
system.activationScripts.createPodmanNetworkHonchoEgress = lib.mkAfter ''
|
|
|
|
|
if ! /run/current-system/sw/bin/podman network exists ${serviceName}-egress; then
|
|
|
|
|
/run/current-system/sw/bin/podman network create ${serviceName}-egress
|
|
|
|
|
fi
|
|
|
|
|
'';
|
|
|
|
|
|
|
|
|
|
virtualisation.oci-containers.containers = {
|
|
|
|
|
"${serviceName}-redis" = {
|
|
|
|
|
image = "docker.io/redis:8.2";
|
|
|
|
|
autoStart = true;
|
|
|
|
|
volumes = ["${serviceName}_redis_data:/data"];
|
|
|
|
|
extraOptions =
|
|
|
|
|
(webNetwork redisIp)
|
|
|
|
|
++ [
|
|
|
|
|
"--health-cmd=redis-cli ping"
|
|
|
|
|
"--health-interval=5s"
|
|
|
|
|
"--health-timeout=5s"
|
|
|
|
|
"--health-retries=5"
|
|
|
|
|
];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
"${serviceName}-api" = {
|
|
|
|
|
inherit image;
|
|
|
|
|
autoStart = true;
|
|
|
|
|
entrypoint = "sh";
|
|
|
|
|
cmd = ["docker/entrypoint.sh"];
|
|
|
|
|
environment = sharedEnvironment;
|
|
|
|
|
environmentFiles = sharedEnvironmentFiles;
|
|
|
|
|
ports = ["${netbirdBindAddress}:${toString honchoPort}:8000"];
|
|
|
|
|
dependsOn = [redisName];
|
|
|
|
|
extraOptions =
|
|
|
|
|
(networksWithEgress apiIp)
|
|
|
|
|
++ [
|
|
|
|
|
"--health-cmd=${apiHealthCmd}"
|
|
|
|
|
"--health-interval=5s"
|
|
|
|
|
"--health-timeout=5s"
|
|
|
|
|
"--health-retries=5"
|
|
|
|
|
"--health-start-period=10s"
|
|
|
|
|
];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
"${serviceName}-deriver" = {
|
|
|
|
|
inherit image;
|
|
|
|
|
autoStart = true;
|
|
|
|
|
entrypoint = "/app/.venv/bin/python";
|
|
|
|
|
cmd = ["-m" "src.deriver"];
|
|
|
|
|
environment = sharedEnvironment;
|
|
|
|
|
environmentFiles = sharedEnvironmentFiles;
|
|
|
|
|
dependsOn = ["${serviceName}-api" redisName];
|
|
|
|
|
extraOptions = networksWithEgress deriverIp;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
systemd.services = {
|
|
|
|
|
"${serviceName}-postgres-bootstrap" = {
|
|
|
|
|
description = "Bootstrap Honcho PostgreSQL role, database, password, and pgvector";
|
|
|
|
|
after = ["postgresql.service" "agenix.service"];
|
|
|
|
|
requires = ["postgresql.service" "agenix.service"];
|
|
|
|
|
before = ["${serviceName}-env.service" "podman-${serviceName}-api.service" "podman-${serviceName}-deriver.service"];
|
|
|
|
|
requiredBy = ["podman-${serviceName}-api.service" "podman-${serviceName}-deriver.service"];
|
|
|
|
|
path = [
|
|
|
|
|
config.services.postgresql.package
|
|
|
|
|
pkgs.coreutils
|
|
|
|
|
];
|
|
|
|
|
serviceConfig = {
|
|
|
|
|
Type = "oneshot";
|
|
|
|
|
User = "postgres";
|
|
|
|
|
Group = "postgres";
|
|
|
|
|
};
|
|
|
|
|
script = ''
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
test -s ${config.age.secrets."${serviceName}-selfhost-db-password".path}
|
|
|
|
|
|
|
|
|
|
psql -v ON_ERROR_STOP=1 --dbname=postgres <<'SQL'
|
|
|
|
|
DO $$
|
|
|
|
|
BEGIN
|
|
|
|
|
CREATE ROLE ${dbUser} LOGIN;
|
|
|
|
|
EXCEPTION WHEN duplicate_object THEN
|
|
|
|
|
NULL;
|
|
|
|
|
END
|
|
|
|
|
$$;
|
|
|
|
|
|
|
|
|
|
SELECT 'CREATE DATABASE ${dbName} OWNER ${dbUser}'
|
|
|
|
|
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${dbName}')\gexec
|
|
|
|
|
|
|
|
|
|
ALTER DATABASE ${dbName} OWNER TO ${dbUser};
|
|
|
|
|
\set honcho_password `cat ${config.age.secrets."${serviceName}-selfhost-db-password".path}`
|
|
|
|
|
ALTER ROLE ${dbUser} WITH LOGIN PASSWORD :'honcho_password';
|
|
|
|
|
SQL
|
|
|
|
|
|
|
|
|
|
psql -v ON_ERROR_STOP=1 --dbname=${dbName} <<'SQL'
|
|
|
|
|
CREATE EXTENSION IF NOT EXISTS vector;
|
|
|
|
|
GRANT ALL PRIVILEGES ON DATABASE ${dbName} TO ${dbUser};
|
|
|
|
|
SQL
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
"${serviceName}-env" = {
|
|
|
|
|
description = "Generate Honcho runtime environment file with agenix secrets";
|
|
|
|
|
after = ["agenix.service" "${serviceName}-postgres-bootstrap.service"];
|
|
|
|
|
requires = ["agenix.service" "${serviceName}-postgres-bootstrap.service"];
|
|
|
|
|
before = ["podman-${serviceName}-api.service" "podman-${serviceName}-deriver.service"];
|
|
|
|
|
requiredBy = ["podman-${serviceName}-api.service" "podman-${serviceName}-deriver.service"];
|
|
|
|
|
path = [
|
|
|
|
|
pkgs.coreutils
|
|
|
|
|
pkgs.python3
|
|
|
|
|
];
|
|
|
|
|
serviceConfig = {
|
|
|
|
|
Type = "oneshot";
|
|
|
|
|
RemainAfterExit = true;
|
|
|
|
|
};
|
|
|
|
|
script = ''
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
install -d -m 0750 ${runtimeDirectory}
|
|
|
|
|
|
|
|
|
|
db_password_encoded=$(
|
|
|
|
|
python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip(), safe=""))' \
|
|
|
|
|
< ${config.age.secrets."${serviceName}-selfhost-db-password".path}
|
|
|
|
|
)
|
|
|
|
|
jwt_secret=$(tr -d '\r\n' < ${config.age.secrets."${serviceName}-selfhost-jwt-secret".path})
|
|
|
|
|
|
|
|
|
|
umask 077
|
|
|
|
|
cat > ${runtimeEnvFile} <<ENV
|
|
|
|
|
DB_CONNECTION_URI=postgresql+psycopg://${dbUser}:$db_password_encoded@postgres:${toString postgresPort}/${dbName}
|
|
|
|
|
AUTH_JWT_SECRET=$jwt_secret
|
|
|
|
|
ENV
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
"podman-${serviceName}-api" = {
|
|
|
|
|
after = ["${serviceName}-env.service" "${serviceName}-postgres-bootstrap.service"];
|
|
|
|
|
requires = ["${serviceName}-env.service" "${serviceName}-postgres-bootstrap.service"];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
"podman-${serviceName}-deriver" = {
|
|
|
|
|
after = ["${serviceName}-env.service" "${serviceName}-postgres-bootstrap.service"];
|
|
|
|
|
requires = ["${serviceName}-env.service" "${serviceName}-postgres-bootstrap.service"];
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
networking.firewall.extraCommands = ''
|
|
|
|
|
# Self-hosted Honcho API: only Netbird mesh peers may reach ${netbirdBindAddress}:${toString honchoPort}.
|
|
|
|
|
ip46tables -A nixos-fw -p tcp --dport ${toString honchoPort} -s ${netbirdRange} -j nixos-fw-accept
|
|
|
|
|
'';
|
|
|
|
|
}
|