From 2ab81893d48bcf3e2a48e3f337a8d3f67bdf9d5e Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 21 Oct 2026 22:52:24 +0100 Subject: [PATCH] Add initial headless T3 Code server setup --- home/agent/default.nix | 179 ++++++++++++++++++++++++ home/base/shell.nix | 2 +- home/development/claude.nix | 13 +- home/development/codex.nix | 18 +++ home/development/cursor.nix | 17 +++ home/development/default.nix | 4 + home/development/encrypt/claude.env.age | 5 + home/development/encrypt/cursor.env.age | Bin 0 -> 332 bytes home/development/encrypt/grok.env.age | Bin 0 -> 311 bytes home/development/grok.nix | 17 +++ home/development/js.nix | 13 +- home/development/t3code.nix | 83 +++++++++++ lib/shell-env-wrapper.nix | 35 +++++ lib/system.nix | 15 +- machines/irnbru/configuration.nix | 28 ++++ modules/agent/default.nix | 145 +++++++++++++++++++ modules/agent/encrypt/agent-ssh.age | Bin 0 -> 623 bytes modules/base/default.nix | 1 + modules/base/home.nix | 31 ++++ modules/default.nix | 1 + modules/server/default.nix | 1 + modules/server/sshd.nix | 41 ++++-- modules/server/t3code/default.nix | 143 +++++++++++++++++++ modules/server/t3code/lifecycle.nix | 41 ++++++ modules/server/tailscale.nix | 8 +- secrets.nix | 6 + 26 files changed, 813 insertions(+), 34 deletions(-) create mode 100644 home/agent/default.nix create mode 100644 home/development/codex.nix create mode 100644 home/development/cursor.nix create mode 100644 home/development/encrypt/claude.env.age create mode 100644 home/development/encrypt/cursor.env.age create mode 100644 home/development/encrypt/grok.env.age create mode 100644 home/development/grok.nix create mode 100644 home/development/t3code.nix create mode 100644 lib/shell-env-wrapper.nix create mode 100644 modules/agent/default.nix create mode 100644 modules/agent/encrypt/agent-ssh.age create mode 100644 modules/base/home.nix create mode 100644 modules/server/t3code/default.nix create mode 100644 modules/server/t3code/lifecycle.nix diff --git a/home/agent/default.nix b/home/agent/default.nix new file mode 100644 index 0000000..d3b5a8a --- /dev/null +++ b/home/agent/default.nix @@ -0,0 +1,179 @@ +{ lib, config, pkgs, inputs, helpers, ... }: + +with lib; +let + cfg = config.modules.agent; + knownHostsFile = pkgs.writeText "agent-known_hosts" (concatStringsSep "\n" cfg.knownHosts + "\n"); + signingKey = if cfg.signing.key != null then cfg.signing.key else cfg.sshKey; + + # Secret options take runtime paths (agenix: /run/agenix/..., sops-nix: + # /run/secrets/...), never key material or a store path. + secretPath = types.strMatching "/run/[^\n]+" // { + description = "runtime secret path under /run (e.g. /run/agenix/...)"; + }; + + # gh reads GH_TOKEN; outside the service (where the env file is already loaded) + # take it from the token file, in a subshell, for gh only. + ghCredential = pkgs.writeShellScript "agent-gh-credential" '' + if [ -z "''${GH_TOKEN:-}" ] && [ -r ${escapeShellArg cfg.tokenEnvFile} ]; then + set -a; . ${escapeShellArg cfg.tokenEnvFile}; set +a + fi + exec ${pkgs.gh}/bin/gh auth git-credential "$@" + ''; +in { + imports = [ + inputs.android-sdk.hmModules.android + ../base/shell.nix + ../base/tools.nix + ../base/tmux.nix + ../base/xdg.nix + ../base/ssh.nix + ../base/git.nix + ../development + ]; + + options.modules.agent = { + enable = mkOption { + default = false; + example = true; + description = "Whether to give this user an agent's own git/GitHub identity."; + type = types.bool; + }; + + name = mkOption { + example = "kitten-agent"; + description = "Commit author name."; + type = types.str; + }; + + email = mkOption { + example = "phil+agent@kitten.sh"; + description = "Commit author email (an address of the agent's GitHub account)."; + type = types.str; + }; + + sshKey = mkOption { + example = "/run/agenix/agent-ssh"; + description = "Path of the private SSH key git authenticates with."; + default = null; + type = types.nullOr secretPath; + }; + + tokenEnvFile = mkOption { + default = null; + example = "/run/agenix/agent.env"; + description = '' + Path of an env file (KEY=value) with GH_TOKEN, for gh and git over HTTPS, + and any other credentials of the account's CLIs (see loadEnvInShell). + ''; + type = types.nullOr secretPath; + }; + + loadEnvInShell = mkOption { + default = true; + example = false; + description = "Whether zsh sources tokenEnvFile at startup, including SSH logins and scripts."; + type = types.bool; + }; + + knownHosts = mkOption { + # https://api.github.com/meta + default = [ + "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl" + "github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=" + "github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=" + ]; + description = "Pinned SSH host keys (known_hosts lines); git never trusts a first-seen key."; + type = types.listOf types.str; + }; + + signing = { + enable = mkOption { + default = false; + example = true; + description = "Whether to SSH-sign commits and tags (add the public key to the account as a signing key)."; + type = types.bool; + }; + + key = mkOption { + default = null; + example = "/run/agenix/agent-signing"; + description = "Private key to sign with; defaults to sshKey."; + type = types.nullOr secretPath; + }; + }; + }; + + config = mkIf cfg.enable { + modules.development.js.projectBinInPath = mkDefault false; + + # Session variables load before .zshrc; non-terminal shells can skip the UI. + programs.zsh.initContent = mkBefore '' + [[ -t 0 && -t 1 ]] || return 0 + ''; + + assertions = [ + { + assertion = !cfg.signing.enable || signingKey != null; + message = "modules.agent: signing requires signing.key or sshKey."; + } + { + assertion = config.modules.git.user.email != "phil@kitten.sh"; + message = "modules.agent: configure the agent's own Git identity."; + } + { + assertion = !config.programs.gpg.enable && !config.services.gpg-agent.enable; + message = "modules.agent: GPG and gpg-agent must remain disabled."; + } + ]; + + xdg.runtimeDir = helpers.mkIfDarwin "$(getconf DARWIN_USER_TEMP_DIR)"; + home.sessionVariables = helpers.mkIfDarwin { + TMPDIR = "$(getconf DARWIN_USER_TEMP_DIR)"; + NPM_CONFIG_TMP = mkForce "$(getconf DARWIN_USER_TEMP_DIR)/npm"; + }; + + programs.zsh.envExtra = mkIf (cfg.tokenEnvFile != null && cfg.loadEnvInShell) '' + if [[ -r ${escapeShellArg cfg.tokenEnvFile} ]]; then + set -a; . ${escapeShellArg cfg.tokenEnvFile}; set +a + export GITHUB_TOKEN="''${GITHUB_TOKEN:-''${GH_TOKEN:-}}" + fi + ''; + + modules.git = { + user = { inherit (cfg) name email; }; + signingKey = if cfg.signing.enable then signingKey else mkDefault null; + }; + + programs.git = { + signing.format = mkIf cfg.signing.enable "ssh"; + settings = mkMerge [ + { + commit.gpgSign = mkOverride 60 false; + tag.gpgSign = mkOverride 60 false; + push.gpgSign = mkOverride 60 false; + # This key only, no ssh-agent, pinned host keys. + core.sshCommand = mkIf (cfg.sshKey != null) (concatStringsSep " " [ + "${pkgs.openssh}/bin/ssh" + "-i ${cfg.sshKey}" + "-o IdentitiesOnly=yes" + "-o IdentityAgent=none" + "-o UserKnownHostsFile=${knownHostsFile}" + "-o StrictHostKeyChecking=yes" + ]); + } + (mkIf (cfg.tokenEnvFile != null) { + "credential \"https://github.com\"".helper = [ "" "!${ghCredential}" ]; + "credential \"https://gist.github.com\"".helper = [ "" "!${ghCredential}" ]; + }) + (mkIf cfg.signing.enable { + gpg.ssh.program = "${pkgs.openssh}/bin/ssh-keygen"; + commit.gpgSign = mkForce true; + tag.gpgSign = mkForce true; + }) + ]; + }; + + home.packages = [ pkgs.gh ]; + }; +} diff --git a/home/base/shell.nix b/home/base/shell.nix index 8c589ce..81b3a62 100644 --- a/home/base/shell.nix +++ b/home/base/shell.nix @@ -77,7 +77,7 @@ in { ]; initContent = /*sh*/'' setopt NO_NOMATCH - stty -ixon -ixoff + if [[ -t 0 ]]; then stty -ixon -ixoff; fi # Pre-evaluated hooks (avoid forking on every shell start) source ${pkgs.runCommand "direnv-hook.zsh" {} '' ${pkgs.direnv}/bin/direnv hook zsh > $out diff --git a/home/development/claude.nix b/home/development/claude.nix index 20a60d9..f4a44d7 100644 --- a/home/development/claude.nix +++ b/home/development/claude.nix @@ -1,22 +1,31 @@ -{ lib, config, pkgs, ... }: +{ lib, config, pkgs, osConfig, ... }: with lib; let cfg = config.modules.development; + auth = import ../../lib/shell-env-wrapper.nix { inherit lib pkgs; }; in { options.modules.development.claude = { + environmentFile = auth.environmentFileOptions ./encrypt/claude.env.age; + enable = mkOption { default = cfg.js.enable || cfg.react-native.enable; example = true; description = "Whether to enable Claude Code configuration."; type = types.bool; }; + + package = mkOption { + default = pkgs.claude-code; + description = "Claude Code CLI package."; + type = types.package; + }; }; config = mkIf cfg.claude.enable { programs.claude-code = { enable = true; - package = pkgs.claude-code; + package = auth.wrap cfg.claude.package (auth.runtimeFiles "${config.home.username}-claude" cfg.claude.environmentFile osConfig.age.secrets) [ "claude" ]; settings = { env = { DISABLE_UPDATES = "1"; diff --git a/home/development/codex.nix b/home/development/codex.nix new file mode 100644 index 0000000..2e37315 --- /dev/null +++ b/home/development/codex.nix @@ -0,0 +1,18 @@ +{ lib, config, pkgs, ... }: + +with lib; +let + cfg = config.modules.development.codex; +in { + options.modules.development.codex = { + enable = mkEnableOption "the codex CLI"; + package = mkPackageOption pkgs "codex" { }; + }; + + config = mkIf cfg.enable { + programs.codex = { + enable = true; + package = cfg.package; + }; + }; +} diff --git a/home/development/cursor.nix b/home/development/cursor.nix new file mode 100644 index 0000000..384c79b --- /dev/null +++ b/home/development/cursor.nix @@ -0,0 +1,17 @@ +{ lib, config, pkgs, osConfig, ... }: + +with lib; +let + cfg = config.modules.development.cursor; + auth = import ../../lib/shell-env-wrapper.nix { inherit lib pkgs; }; +in { + options.modules.development.cursor = { + enable = mkEnableOption "the cursor CLI"; + package = mkPackageOption pkgs "cursor-cli" { }; + environmentFile = auth.environmentFileOptions ./encrypt/cursor.env.age; + }; + + config = mkIf cfg.enable { + home.packages = [ (auth.wrap cfg.package (auth.runtimeFiles "${config.home.username}-cursor" cfg.environmentFile osConfig.age.secrets) [ "cursor-agent" ]) ]; + }; +} diff --git a/home/development/default.nix b/home/development/default.nix index afba927..1d4864b 100644 --- a/home/development/default.nix +++ b/home/development/default.nix @@ -12,9 +12,13 @@ with lib; { imports = [ ./claude.nix + ./codex.nix + ./cursor.nix + ./grok.nix ./js.nix ./react-native.nix ./terraform.nix ./zig.nix + ./t3code.nix ]; } diff --git a/home/development/encrypt/claude.env.age b/home/development/encrypt/claude.env.age new file mode 100644 index 0000000..f43649b --- /dev/null +++ b/home/development/encrypt/claude.env.age @@ -0,0 +1,5 @@ +age-encryption.org/v1 +-> ssh-ed25519 QwbpPw inmdzNGuE1tKrQnqTsdWeKRRdovNq9bxDMfxLTfl0jk +lVaimbdo053cZTwAHF4X/LyEZyepERKHR81YoMp06gs +--- wjTZvI8bhD9Cv9rz2eHW01yRa6ZKdHwO/nDCmd2Njq4 +!�[�|��;�ݏ!���0{4 ���LA��N�A������ g�e�파7?O���2������~�)ea�EgU〺��!���tw!�Z�ԜfP4C*?Me���!*��'�h�:�R�>3� ֖X�0��%�؀��S�Н�;,�tj����*�� \ No newline at end of file diff --git a/home/development/encrypt/cursor.env.age b/home/development/encrypt/cursor.env.age new file mode 100644 index 0000000000000000000000000000000000000000..3af676505c4e267a9584a598422609da4878cf2f GIT binary patch literal 332 zcmYaGPt{G$OD?J`D9Oyv)5|YP*Do{V(zR14F3!+RO))YxHMCR+EKe#3C|7Vy%??Sb zD2vh#wv5d7^)X0KFALN!Og8d!G6JlkG%(RLqte?ftjaO9up*KxC?GY+R6jr4B{d?= zHPzU}H#5(vAhF0Z!^N<`%-cVxEZZ#A*~uWy&E1nrS65fTI5NX5*W14`z%SF+Cnd|> z!ab?TB-GNqv@*lly~sBpEHq!c%(>DxGBue?V$IrV6K?OA-CVk%i6cdJ;~m9~?~g25 za-hXO{+|zjqE^43GIQmS?DSPuu}4o>9!T-wRrud=N$%&g15>KzY^a%T1pq=Rhu#1H literal 0 HcmV?d00001 diff --git a/home/development/encrypt/grok.env.age b/home/development/encrypt/grok.env.age new file mode 100644 index 0000000000000000000000000000000000000000..480d33272fe18c3e7da49506aa4478209d7f9305 GIT binary patch literal 311 zcmYaGPt{G$OD?J`D9Oyv)5|YP*Do{V(zR14F3!+RO))YxHMCR+EKe#3C|5|!jPg&3 z0!%yA)2Ya!I59QXrz$AVqB1ftxh%-gKgh$=qQE=b#4k6<-Cp|^qDBC$d*TAR9H@%>$$g?WkJjXCU$1svhS65fTt2EUm(I>YcSv$+wH!!>` z*utYMyx7a5BHzQyv>?sLqol0dvpBRM!pnr~z&%-3vH8#M?bLMom*ExjwTX4&EWSyh z%gxmI=jn@HU(>sD{+B&Zb#FYMQNLN`-_bAZ;(K#{JBRTGh%VsSxomFnD*HvP&K|Rq zKa@X_F=(*5F~4ZRvOnFEn<5jQ3!a}iTl}w&`scc16Z$S4TN5U{^S$kv9Sj=BXI-57 PM1Exx^BJa!D0XfD&n/dev/null || true + '') accounts)); + }) + + (helpers.linuxAttrs { + users.groups = mapAttrs (_: _: { }) accounts; + }) + ]; +} diff --git a/modules/agent/encrypt/agent-ssh.age b/modules/agent/encrypt/agent-ssh.age new file mode 100644 index 0000000000000000000000000000000000000000..1f24e85e551d9e4eed1007f83fc58a67526221f1 GIT binary patch literal 623 zcmYaGPt{G$OD?J`D9Oyv)5|YP*Do{V(zR14F3!+RO))YxHMCR+EKe#3C|58KbInc( zOvyBhtTGF5j!FtMb1ls%^K>!INKY&YFb}M7)i+G6ba790@#OOMP1g=JPA}4rEGhM^ zax>4*bWZn=j7mu@3NtkJDJ(N^tO|8XGjj^F2;|b$)m3nIO*ZxNOo{OJjmq;e^$3X6 zE{F)v^sX>W$_l6~^7bjn2`kCU3HA!{h~(NXyC7|2UBjX533kt_1AJH|*ge1NNy> z(_VyJ$h=_wBy4weJmX87*KYfk7B&g;ZRYRgh8ZT_U0!wi>`NSN#`A( z@h9umW%E7nLLR!MSzTT7aAux^*BZ7{|IT}-zUZ84qkZ>}zFj^;nLv2L%)~vDXP5N_ zr11(R@LLyZWj=~yel&02$>`6w3eN1=+;=y~c;!*^{p!(=g>25LtlYxv^}v)d?DTfK f#&zt0z9yBgS4/dev/null 2>&1; then + ${concatMapStringsSep "\n" (name: '' + /usr/sbin/dseditgroup -o edit -a ${escapeShellArg name} -t user com.apple.access_ssh + '') allowedUsers} + fi + ''; } // helpers.linuxAttrs { systemd.services.sshd.serviceConfig = { MemoryMax = "8G"; diff --git a/modules/server/t3code/default.nix b/modules/server/t3code/default.nix new file mode 100644 index 0000000..6124141 --- /dev/null +++ b/modules/server/t3code/default.nix @@ -0,0 +1,143 @@ +{ lib, config, pkgs, helpers, hostname, ... }: + +with lib; +let + cfgRoot = config.modules.server; + cfg = config.modules.server.t3code; + agent = config.modules.agents.${cfg.account}; + home = agent.homeDirectory; + hm = config.home-manager.users.${cfg.account}; + # Not lib.getExe: nixpkgs' mainProgram is the desktop app (t3code-desktop). + t3 = "${homeCfg.package}/bin/t3"; + logDir = "/var/log/t3code"; + auth = import ../../../lib/shell-env-wrapper.nix { inherit lib pkgs; }; + envFiles = optional (hm.modules.agent.tokenEnvFile != null) hm.modules.agent.tokenEnvFile + ++ auth.runtimeFiles "${cfg.account}-t3code" homeCfg.environmentFile config.age.secrets; + + homeCfg = hm.modules.development.t3code; + + shell = "${hm.programs.zsh.package}/bin/zsh"; + + withEnv = pkgs.writeShellScript "t3code-env" '' + set -euo pipefail + umask 077 + export HOME=${home} USER=${cfg.account} LOGNAME=${cfg.account} LANG=en_US.UTF-8 + export PATH=/usr/bin:/bin:/usr/sbin:/sbin + # Before the session vars: they're sorted, and NPM_CONFIG_TMP builds on it. + export XDG_RUNTIME_DIR="$(/usr/bin/getconf DARWIN_USER_TEMP_DIR)" + set +u + . ${config.system.build.setEnvironment} + . ${hm.home.sessionVariablesPackage}/etc/profile.d/hm-session-vars.sh + set -u + export SHELL=${shell} + mkdir -p "$TMPDIR" + + export T3CODE_HOME=${home}/.t3 T3CODE_NO_BROWSER=true + export T3CODE_TELEMETRY_ENABLED=${boolToString homeCfg.telemetry} + export GIT_TERMINAL_PROMPT=0 GH_PROMPT_DISABLED=1 GH_NO_UPDATE_NOTIFIER=1 + + # agenix installs secrets asynchronously at boot. + for envFile in ${escapeShellArgs envFiles}; do + for _ in $(seq 120); do + [ -r "$envFile" ] && break + sleep 1 + done + set -a + . "$envFile" + set +a + done + export GITHUB_TOKEN="''${GITHUB_TOKEN:-''${GH_TOKEN:-}}" + + exec "$@" + ''; + + # `t3 serve` prints a one-time admin pairing token, URL and QR code at every + # start; keep them out of the log. + serve = pkgs.writeShellScript "t3code-serve" '' + set -euo pipefail + exec > >(LC_ALL=C ${pkgs.gnugrep}/bin/grep --line-buffered -vE '^(Token|Pairing URL): |█|▀|▄') 2>&1 + exec ${t3} serve --host 0.0.0.0 --port ${toString cfg.port} --base-dir ${home}/.t3 + ''; + + shellHelper = pkgs.writeShellScriptBin "t3code-shell" '' + set -euo pipefail + if [ "$(id -u)" -ne 0 ]; then exec sudo "$0" "$@"; fi + [ "$#" -gt 0 ] || set -- ${shell} -l + cd ${home} + exec sudo -u ${cfg.account} -H /usr/bin/env -i ${withEnv} "$@" + ''; + + pairHelper = pkgs.writeShellScriptBin "t3code-pair" '' + exec ${shellHelper}/bin/t3code-shell ${t3} auth pairing create \ + --base-dir ${home}/.t3 --base-url ${cfg.publicUrl} --ttl 10m "$@" + ''; + +in helpers.darwinAttrs { + imports = [ ./lifecycle.nix ]; + + options.modules.server.t3code = { + enable = mkOption { + default = false; + example = true; + description = "Whether to enable the T3 Code server."; + type = types.bool; + }; + + account = mkOption { + default = "agent"; + description = "Agent account to enable and run T3 Code as (modules.agents.)."; + type = types.strMatching "[a-z_][a-z0-9_-]*"; + }; + + port = mkOption { + # Not T3's 3773: a desktop app on the same Mac takes the first free port + # from 3773, and would grab it while the daemon restarts. + default = 3780; + description = "Port of the server, listening on all IPv4 interfaces."; + type = types.port; + }; + + publicUrl = mkOption { + default = "http://${hostname}:${toString cfg.port}/"; + defaultText = literalExpression ''"http://''${hostname}:''${toString cfg.port}/"''; + description = "URL the apps reach the server at; used by t3code-pair."; + type = types.str; + }; + + }; + + config = mkIf (cfgRoot.enable && cfg.enable) { + modules.agents.${cfg.account} = { + enable = true; + home = { + modules.development.t3code = { + enable = true; + desktop.enable = false; + }; + }; + }; + + environment.systemPackages = [ shellHelper pairHelper ]; + + power.restartAfterPowerFailure = mkDefault true; + + launchd.daemons.t3code = { + command = "${withEnv} ${serve}"; + serviceConfig = { + Label = "sh.kitten.t3code"; + UserName = cfg.account; + GroupName = "staff"; + WorkingDirectory = home; + Umask = 63; + RunAtLoad = true; + KeepAlive = true; + ThrottleInterval = 5; + ExitTimeOut = 90; + ProcessType = "Interactive"; + StandardOutPath = "${logDir}/server.log"; + StandardErrorPath = "${logDir}/launchd.err.log"; + }; + }; + + }; +} diff --git a/modules/server/t3code/lifecycle.nix b/modules/server/t3code/lifecycle.nix new file mode 100644 index 0000000..82cee8f --- /dev/null +++ b/modules/server/t3code/lifecycle.nix @@ -0,0 +1,41 @@ +{ lib, config, pkgs, helpers, ... }: + +with lib; +let + cfg = config.modules.server.t3code; + logDir = "/var/log/t3code"; + stateDir = "/var/db/t3code"; + + rotation = pkgs.writeText "t3code-logrotate.conf" '' + ${logDir}/*.log { + size 10M + rotate 7 + compress + delaycompress + missingok + notifempty + copytruncate + } + ''; +in helpers.darwinAttrs { + config = mkIf (config.modules.server.enable && cfg.enable) { + # Users exist here; launchd and Home Manager activation run later. + system.activationScripts.users.text = mkAfter '' + install -d -o root -g wheel -m 0755 ${logDir} + install -d -o root -g wheel -m 0700 ${stateDir} + touch ${logDir}/server.log ${logDir}/launchd.err.log + chown ${cfg.account}:staff ${logDir}/server.log ${logDir}/launchd.err.log + chmod 0600 ${logDir}/server.log ${logDir}/launchd.err.log + ''; + + launchd.daemons.t3code-logrotate = { + command = "${pkgs.logrotate}/bin/logrotate --state ${stateDir}/logrotate.status ${rotation}"; + serviceConfig = { + Label = "sh.kitten.t3code-logrotate"; + RunAtLoad = true; + StartInterval = 300; + Umask = 63; + }; + }; + }; +} diff --git a/modules/server/tailscale.nix b/modules/server/tailscale.nix index 6cc73bd..56829cb 100644 --- a/modules/server/tailscale.nix +++ b/modules/server/tailscale.nix @@ -49,11 +49,7 @@ in { environment.systemPackages = mkIf config.modules.desktop.enable [ pkgs.tail-tray ]; } // helpers.darwinAttrs { - networking.search = [ cfg.domain ]; - - services.tailscale = { - enable = true; - overrideLocalDns = true; - }; + environment.etc."resolver/ts.net".enable = false; + services.tailscale.enable = true; }); } diff --git a/secrets.nix b/secrets.nix index c3f1bc6..446edaf 100644 --- a/secrets.nix +++ b/secrets.nix @@ -6,6 +6,8 @@ in { "./modules/base/encrypt/nix-access-tokens.conf.age".publicKeys = keys; + "./modules/agent/encrypt/agent-ssh.age".publicKeys = keys; + "./modules/server/encrypt/tailscale.age".publicKeys = keys; "./modules/server/encrypt/rclone.conf.age".publicKeys = keys; "./modules/server/encrypt/tangled-knot-ssh.age".publicKeys = keys; @@ -15,6 +17,10 @@ in "./modules/router/encrypt/pppoe-options.age".publicKeys = keys; + "./home/development/encrypt/claude.env.age".publicKeys = keys; + "./home/development/encrypt/cursor.env.age".publicKeys = keys; + "./home/development/encrypt/grok.env.age".publicKeys = keys; + "./home/fonts/encrypt/DankMono-Regular.otf.age".publicKeys = keys; "./home/fonts/encrypt/DankMono-Bold.otf.age".publicKeys = keys; "./home/fonts/encrypt/DankMono-Italic.otf.age".publicKeys = keys; -- knotmirror 0.0.1