mirror of
https://codeberg.org/muon/home.git
synced 2026-07-03 23:49:35 +00:00
228 lines
7.3 KiB
Nix
228 lines
7.3 KiB
Nix
{
|
|
pkgs,
|
|
lib,
|
|
config,
|
|
sources,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.mods.server.auth2api;
|
|
|
|
auth2api-pkg = pkgs.callPackage ../../../pkgs/auth2api/package.nix { inherit sources; };
|
|
|
|
# Wrap the binary so --config is always implicit, whether run by the
|
|
# service, by the user for --login, or any other invocation.
|
|
auth2api-wrapped = pkgs.symlinkJoin {
|
|
name = "auth2api-wrapped";
|
|
paths = [ auth2api-pkg ];
|
|
buildInputs = [ pkgs.makeWrapper ];
|
|
postBuild = ''
|
|
wrapProgram $out/bin/auth2api \
|
|
--add-flags "--config=${cfg.stateDir}/config.yaml" \
|
|
--run "umask 027"
|
|
'';
|
|
};
|
|
|
|
# Nix-managed config template — read-only in /nix/store.
|
|
# systemd-tmpfiles copies it into stateDir on first boot (C rule),
|
|
# so auth2api can write back to it (e.g. API key auto-generation)
|
|
# and the user can hand-edit it freely without rebuilds overwriting it.
|
|
configTemplate = pkgs.writeText "auth2api-config-template.yaml" ''
|
|
host: "${cfg.host}"
|
|
port: ${toString cfg.port}
|
|
|
|
# Directory where OAuth token files are stored (written by --login).
|
|
auth-dir: "${cfg.stateDir}/tokens"
|
|
|
|
# Empty = unauthenticated mode (safe when binding to 127.0.0.1).
|
|
api-keys: []
|
|
|
|
body-limit: "${cfg.bodyLimit}"
|
|
|
|
timeouts:
|
|
messages-ms: ${toString cfg.timeouts.messagesMs}
|
|
stream-messages-ms: ${toString cfg.timeouts.streamMessagesMs}
|
|
count-tokens-ms: ${toString cfg.timeouts.countTokensMs}
|
|
|
|
stats:
|
|
enabled: true
|
|
|
|
# off | errors | verbose
|
|
debug: "${cfg.debug}"
|
|
|
|
thinking:
|
|
# Default reasoning effort for requests without reasoning_effort set.
|
|
# none | minimal | low | medium | high | xhigh
|
|
default-effort: "${cfg.thinking.defaultEffort}"
|
|
|
|
cloaking:
|
|
cli-version: "2.1.88"
|
|
entrypoint: "cli"
|
|
'';
|
|
in
|
|
with lib; {
|
|
options.mods.server.auth2api = {
|
|
enable = mkEnableOption "auth2api OAuth-to-API proxy";
|
|
|
|
port = mkOption {
|
|
type = types.port;
|
|
default = 8317;
|
|
description = "Port auth2api listens on.";
|
|
};
|
|
|
|
host = mkOption {
|
|
type = types.str;
|
|
default = "127.0.0.1";
|
|
description = "Bind address. Defaults to localhost — safe with no API key.";
|
|
};
|
|
|
|
stateDir = mkOption {
|
|
type = types.str;
|
|
default = "/var/lib/auth2api";
|
|
description = ''
|
|
State directory. Contains config.yaml (live, writable) and
|
|
tokens/ (OAuth token files written by --login).
|
|
'';
|
|
};
|
|
|
|
bodyLimit = mkOption {
|
|
type = types.str;
|
|
default = "200mb";
|
|
description = "Maximum JSON request body size.";
|
|
};
|
|
|
|
timeouts = {
|
|
messagesMs = mkOption {
|
|
type = types.int;
|
|
default = 120000;
|
|
description = "Non-streaming /v1/messages timeout (ms).";
|
|
};
|
|
streamMessagesMs = mkOption {
|
|
type = types.int;
|
|
default = 600000;
|
|
description = "Streaming /v1/messages timeout (ms).";
|
|
};
|
|
countTokensMs = mkOption {
|
|
type = types.int;
|
|
default = 30000;
|
|
description = "/v1/messages/count_tokens timeout (ms).";
|
|
};
|
|
};
|
|
|
|
debug = mkOption {
|
|
type = types.enum [ "off" "errors" "verbose" ];
|
|
default = "off";
|
|
description = "Log level: off | errors | verbose.";
|
|
};
|
|
|
|
thinking = {
|
|
defaultEffort = mkOption {
|
|
type = types.enum [ "none" "minimal" "low" "medium" "high" "xhigh" ];
|
|
default = "medium";
|
|
description = ''
|
|
Default Claude extended-thinking budget injected into every request
|
|
that doesn't already carry a reasoning_effort field.
|
|
none = thinking disabled
|
|
minimal = 512 tokens
|
|
low = 1 024 tokens
|
|
medium = 8 192 tokens (default)
|
|
high = 24 576 tokens
|
|
xhigh = 32 768 tokens
|
|
'';
|
|
};
|
|
};
|
|
|
|
openFirewall = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Open the firewall for the auth2api port.";
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
# tmpfiles runs as root before any service starts.
|
|
# 'd' creates the directory if missing; 'C' copies the template into
|
|
# config.yaml only if that file doesn't already exist — so hand-edits
|
|
# and auth2api's own writes are never clobbered on rebuild.
|
|
systemd.tmpfiles.rules = [
|
|
# stateDir: world-traversable so any local user can reach files inside
|
|
"d '${cfg.stateDir}' 0755 auth2api auth2api - -"
|
|
"z '${cfg.stateDir}' 0755 auth2api auth2api - -"
|
|
# tokens: group-writable so members of the auth2api group can run --login
|
|
"d '${cfg.stateDir}/tokens' 0770 auth2api auth2api - -"
|
|
"z '${cfg.stateDir}/tokens' 0770 auth2api auth2api - -"
|
|
# config.yaml: contains no secrets (empty api-keys), safe to be world-readable
|
|
"C '${cfg.stateDir}/config.yaml' 0644 auth2api auth2api - ${configTemplate}"
|
|
"z '${cfg.stateDir}/config.yaml' 0644 auth2api auth2api - -"
|
|
];
|
|
|
|
# auth2api.path watches the tokens directory and activates auth2api.service
|
|
# the moment a token file appears (i.e. after --login has been run).
|
|
# This means the service is never started — and never fails — during
|
|
# activation on a fresh machine, so nixos-rebuild always succeeds cleanly.
|
|
systemd.paths.auth2api = {
|
|
description = "Watch for auth2api OAuth tokens";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "systemd-tmpfiles-setup.service" ];
|
|
|
|
pathConfig = {
|
|
# Trigger when any file is created inside the tokens directory.
|
|
DirectoryNotEmpty = "${cfg.stateDir}/tokens";
|
|
# Re-trigger if the service stops (e.g. after a logout / token removal).
|
|
Unit = "auth2api.service";
|
|
};
|
|
};
|
|
|
|
systemd.services.auth2api = {
|
|
description = "auth2api OAuth-to-API proxy";
|
|
# Started by the path unit, not directly by multi-user.target.
|
|
# Must wait for tmpfiles so config.yaml is always present before start.
|
|
after = [ "network.target" "systemd-tmpfiles-setup.service" ];
|
|
requires = [ "systemd-tmpfiles-setup.service" ];
|
|
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
ExecStart = "${auth2api-wrapped}/bin/auth2api";
|
|
|
|
Restart = "on-failure";
|
|
RestartSec = "10s";
|
|
|
|
User = "auth2api";
|
|
Group = "auth2api";
|
|
|
|
ReadWritePaths = [ cfg.stateDir ];
|
|
|
|
# Hardening
|
|
NoNewPrivileges = true;
|
|
PrivateTmp = true;
|
|
ProtectSystem = "strict";
|
|
ProtectHome = true;
|
|
PrivateDevices = true;
|
|
ProtectKernelTunables = true;
|
|
ProtectControlGroups = true;
|
|
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
|
|
RestrictNamespaces = true;
|
|
LockPersonality = true;
|
|
MemoryDenyWriteExecute = false; # Node.js JIT requires W+X pages
|
|
RestrictRealtime = true;
|
|
RestrictSUIDSGID = true;
|
|
};
|
|
};
|
|
|
|
users.users.auth2api = {
|
|
isSystemUser = true;
|
|
group = "auth2api";
|
|
home = cfg.stateDir;
|
|
description = "auth2api service user";
|
|
};
|
|
users.groups.auth2api = { };
|
|
|
|
# Allow the primary user to run --login and read/write tokens.
|
|
users.users.${config.mods.user.name}.extraGroups = [ "auth2api" ];
|
|
|
|
# Make the wrapped binary available in PATH for `auth2api --login` etc.
|
|
environment.systemPackages = [ auth2api-wrapped ];
|
|
|
|
networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
|
|
};
|
|
}
|