From e19c6cb395c1fd7936bc87a0558d150421532732 Mon Sep 17 00:00:00 2001 From: servius Date: Thu, 11 Jun 2026 20:35:58 +0530 Subject: [PATCH] feat: Add goose mobile access (self-hosted) and claude-agent-acp Enable the ai overlay and add goose tooling: - goosed-bin: extract the goosed agent server from the prebuilt Desktop .deb (not shipped in the CLI tarball) and autopatchelf it; x86_64-linux only. - goosed runs as a ryu user service with the GOOSE_SERVER__SECRET_KEY sops token; GOOSE_TUNNEL=none disables the public Cloudflare/lapstone relay. - Front it at goose.darksailor.dev via ryu Caddy (Cloudflare DNS-01, tailnet-only, SSE flush) so the iOS app pairs directly over Tailscale. Also wires in claude-agent-acp and re-enables pi.dev. --- home/programs/claude-agent-acp.nix | 3 + home/programs/default.nix | 4 +- home/programs/goose.nix | 51 +++++++++++++ nixos/ryu/services/caddy.nix | 20 +++++- overlays.nix | 2 +- overlays/ai.nix | 111 +++++++++++++++++++++++++++++ secrets/secrets.yaml | 6 +- 7 files changed, 192 insertions(+), 5 deletions(-) create mode 100644 home/programs/claude-agent-acp.nix create mode 100644 home/programs/goose.nix create mode 100644 overlays/ai.nix diff --git a/home/programs/claude-agent-acp.nix b/home/programs/claude-agent-acp.nix new file mode 100644 index 0000000..df02146 --- /dev/null +++ b/home/programs/claude-agent-acp.nix @@ -0,0 +1,3 @@ +{pkgs, ...}: { + home.packages = [pkgs.claude-agent-acp]; +} diff --git a/home/programs/default.nix b/home/programs/default.nix index 274ab02..0eba780 100644 --- a/home/programs/default.nix +++ b/home/programs/default.nix @@ -84,14 +84,16 @@ ./alejandra.nix ./ast-grep.nix ./cargo.nix + ./claude-agent-acp.nix ./claude-code ./codex.nix ./deploy-rs.nix + ./goose.nix ./jujutsu.nix ./nixify.nix ./omnix.nix ./opencode.nix - # ./pi.dev.nix + ./pi.dev.nix ./rustup.nix ] ++ lib.optionals (device.isDev || device.is "tako") [ diff --git a/home/programs/goose.nix b/home/programs/goose.nix new file mode 100644 index 0000000..012cd25 --- /dev/null +++ b/home/programs/goose.nix @@ -0,0 +1,51 @@ +{ + pkgs, + lib, + config, + device, + ... +}: +lib.mkMerge [ + { + # goose-ai = the `goose` CLI; goosed-bin = the `goosed` agent server (mobile + # tunnel API), prebuilt-extracted and x86_64-linux only. + home.packages = + [pkgs.goose-ai] + ++ lib.optionals (pkgs.stdenv.hostPlatform.system == "x86_64-linux") [pkgs.goosed-bin]; + } + + # Mobile-tunnel server + secret, ryu only (goosed-bin is x86_64-linux and the + # tunnel is paired from the desktop). The token authenticates goosed's local + # API; the per-pairing tunnel secret is generated by goosed itself. + (lib.mkIf (device.is "ryu") { + sops.secrets."goose/server_secret" = {}; + + sops.templates."goose.env".content = '' + GOOSE_SERVER__SECRET_KEY=${config.sops.placeholder."goose/server_secret"} + ''; + + systemd.user.services.goosed = { + Unit = { + Description = "goose agent server (mobile tunnel)"; + After = ["sops-nix.service"]; + Wants = ["sops-nix.service"]; + }; + Service = { + # Non-secret config inline; the secret token comes from the sops template. + # GOOSE_TUNNEL=none disables the public Cloudflare/lapstone relay — the + # iOS app reaches goosed directly via the goose.darksailor.dev Caddy + # vhost over Tailscale (see nixos/ryu/services/caddy.nix). + Environment = [ + "GOOSE_TLS=false" + "GOOSE_PORT=3000" + "GOOSE_TUNNEL=none" + ]; + EnvironmentFile = config.sops.templates."goose.env".path; + ExecStart = "${pkgs.goosed-bin}/bin/goosed agent"; + Restart = "on-failure"; + RestartSec = 5; + }; + Install.WantedBy = ["default.target"]; + }; + }) +] diff --git a/nixos/ryu/services/caddy.nix b/nixos/ryu/services/caddy.nix index e9c5a83..b869938 100644 --- a/nixos/ryu/services/caddy.nix +++ b/nixos/ryu/services/caddy.nix @@ -4,7 +4,12 @@ device, ... }: { - networking.domains.subDomains."code.darksailor.dev".a.data = device.tailscaleIp; + networking.domains.subDomains = { + "code.darksailor.dev".a.data = device.tailscaleIp; + # goosed mobile access — public DNS points at ryu's tailnet IP, so the + # subdomain only resolves/reaches over Tailscale (LE cert via DNS-01). + "goose.darksailor.dev".a.data = device.tailscaleIp; + }; sops = { secrets."cloudflare/cf_api_key".owner = config.services.caddy.user; @@ -37,6 +42,19 @@ import cloudflare reverse_proxy localhost:4096 ''; + # goosed: tailnet-only (the iOS app authenticates with goose's own + # x-secret-key, so no Authelia). flush_interval -1 keeps SSE streaming + # responses unbuffered. + virtualHosts."goose.darksailor.dev".extraConfig = '' + import cloudflare + @internal remote_ip 100.64.0.0/10 127.0.0.1/32 ::1/128 + handle @internal { + reverse_proxy localhost:3000 { + flush_interval -1 + } + } + respond "Access denied" 403 + ''; }; }; systemd.services.caddy = { diff --git a/overlays.nix b/overlays.nix index 66a8556..0d3101e 100644 --- a/overlays.nix +++ b/overlays.nix @@ -15,7 +15,7 @@ in (import ./overlays/codex.nix args) (import ./overlays/applications.nix args) (import ./overlays/flat-manager.nix args) - # (import ./overlays/ai.nix args) + (import ./overlays/ai.nix args) # External input overlays inputs.deploy-rs.overlays.default diff --git a/overlays/ai.nix b/overlays/ai.nix new file mode 100644 index 0000000..4ecdf69 --- /dev/null +++ b/overlays/ai.nix @@ -0,0 +1,111 @@ +{...}: final: prev: let + inherit (prev) lib stdenv; + + version = "1.37.0"; + + sources = { + x86_64-linux = { + target = "x86_64-unknown-linux-gnu"; + hash = "sha256-qoBCt5ReDxACdvAcIoOAOaUMw65s0R/g8eDu+5AMIGc="; + }; + aarch64-linux = { + target = "aarch64-unknown-linux-gnu"; + hash = "sha256-iGvQVvwWKsoRXBVugClyyTzlJYNspzDoGZJtK6LcOTA="; + }; + x86_64-darwin = { + target = "x86_64-apple-darwin"; + hash = "sha256-elONn7u9ATFvPZYSssEj/o5Nwc1vZTxeO0soEGvpk7s="; + }; + aarch64-darwin = { + target = "aarch64-apple-darwin"; + hash = "sha256-2L81shQh+4JH7DAL+dmAXkqeD5mfMKyI0vMLOIEXTXI="; + }; + }; + + source = sources.${stdenv.hostPlatform.system} or (throw "goose-ai is unsupported on ${stdenv.hostPlatform.system}"); +in { + goose-ai = stdenv.mkDerivation { + pname = "goose-ai"; + inherit version; + + src = prev.fetchurl { + url = "https://github.com/aaif-goose/goose/releases/download/v${version}/goose-${source.target}.tar.bz2"; + inherit (source) hash; + }; + sourceRoot = "."; + + nativeBuildInputs = lib.optionals stdenv.hostPlatform.isLinux [ + prev.autoPatchelfHook + ]; + + buildInputs = lib.optionals stdenv.hostPlatform.isLinux [ + stdenv.cc.cc.lib + ]; + + installPhase = '' + runHook preInstall + + install -Dm755 goose $out/bin/goose + + runHook postInstall + ''; + + meta = { + description = "Open source, extensible AI agent CLI"; + homepage = "https://github.com/aaif-goose/goose"; + license = lib.licenses.asl20; + mainProgram = "goose"; + platforms = builtins.attrNames sources; + }; + }; + + # `goosed` (the agent server) is NOT shipped in the CLI tarball above — it only + # exists inside the Desktop app bundle. It provides the mobile tunnel API + # (POST /tunnel/start etc.) used to pair the iOS app from the CLI. Extract the + # prebuilt binary from the Linux .deb rather than compiling goose-server from + # source (which pulls in V8 via the code-mode feature). x86_64-linux only — + # upstream publishes no arm/darwin desktop .deb. + goosed-bin = stdenv.mkDerivation { + pname = "goosed-bin"; + inherit version; + + src = prev.fetchurl { + url = "https://github.com/aaif-goose/goose/releases/download/v${version}/goose_${version}_amd64.deb"; + hash = "sha256-dkzYCE5ZDNBwJUQ9Kj2ZSkEdfbMZiwDRfGc6WMpYTrA="; + }; + + nativeBuildInputs = [ + prev.dpkg + prev.autoPatchelfHook + ]; + + buildInputs = [ + stdenv.cc.cc.lib + ]; + + dontConfigure = true; + dontBuild = true; + + # Stream the data tarball and extract only goosed — a full `dpkg-deb -x` + # fails in the sandbox trying to set the setuid bit on chrome-sandbox. + unpackPhase = '' + runHook preUnpack + dpkg-deb --fsys-tarfile $src | tar -x ./usr/lib/goose/resources/bin/goosed + runHook postUnpack + ''; + + installPhase = '' + runHook preInstall + install -Dm755 usr/lib/goose/resources/bin/goosed $out/bin/goosed + runHook postInstall + ''; + + meta = { + description = "goose agent server (goosed), extracted from the prebuilt Desktop bundle"; + homepage = "https://github.com/aaif-goose/goose"; + license = lib.licenses.asl20; + mainProgram = "goosed"; + platforms = ["x86_64-linux"]; + }; + }; +} diff --git a/secrets/secrets.yaml b/secrets/secrets.yaml index cc7c255..62478a9 100644 --- a/secrets/secrets.yaml +++ b/secrets/secrets.yaml @@ -153,6 +153,8 @@ flatpak: flat-manager: jwt_secret_b64: ENC[AES256_GCM,data:t/sgw98CjVeTy/cxyO4HMA8K94qwfvdY5zRGbUV9kf0ghXULIeomg50gl8U=,iv:Y7baR0ZY+z6eA9Y1R+gHqpGEQp+z6B7Z536Kf8Whd7Q=,tag:Y0jKJQxjkGI19mS7m8laGg==,type:str] sloe_upload_token: ENC[AES256_GCM,data:fGoWQoD8bUlP7Phe/3OJzVcMLgSny5j5WzCXBxWGfOM+KGfZAswKiqWTe3bKZydkdkZcBMGLKFl4M17xD/oH2kd2Jnm4RHCQVy6epvYFK2JTr1B0vdTojQNgIsCnroeyDsHPJY64Rsvb+xszSGyEbTf+1npCkuPze6Hyu32dkSfAbOFjbiooQhE3ZJz0Dfdbupx0C0emNdqT49h+n/WD6UyyMY000tlHk4DvqAh2oHYGX+HLlaCoUAWLWBhAyRZXxl1H2n8Z+L8E+NUtoie4F7Ad3eqQhYvbrKWlmTiKvXrQE0suY5FQusZSEFnhjbvDdexuPbuhzInlIRgjBUQNU37lNdxziG1wOgW9zJAhKgJs9ldLjCV7+1Fo4D5Ril5jazngpx8rAWj6L8GT6lblkYoIrw/g3mNRcBpSsbLNx8e3KCaGXIbE2qhkdEn53Kz0NXWyDYLs8FxjQ+LwgcXVyA==,iv:MAoISxK1Ozv4OnXpRG8n689Ga35aLlMMm+hhILoevKk=,tag:DhF5i1zUICPjShRZK6fBKg==,type:str] +goose: + server_secret: ENC[AES256_GCM,data:VPqdv1poAXaLeRHG/IPE24ivA3/M8lpyLI+eyevsJDAt0mgJCM8Hs9hvuoEU1XK3SW4Sk1oTL23qKeZwUSjcvQ==,iv:cOt/IqfSZJwCGcyqnbpUp4MVZkViHcg62bgsRyrT53s=,tag:+HinnSyI7utC0BzN4FhG8w==,type:str] sops: age: - enc: | @@ -164,7 +166,7 @@ sops: ZtXTVClwMSmaUYhdSB2gKFrKVZHXTJZ4oAL5t/BpC0pOHyr+o96T3Q== -----END AGE ENCRYPTED FILE----- recipient: age1pw7kluxp7872c63ne4jecq75glj060jkmqwzkk6esatuyck9egfswufdpk - lastmodified: "2026-05-17T17:41:43Z" - mac: ENC[AES256_GCM,data:8TpbZIfbsnr7UTQc+YMMVDltdTUQYuT0U5+XEo8fUEhf/hYSxFz3xlwBdNLapqpZChMt6QsS9dNKcrXvurXa0nSWuwfFCNvuavECguw+C1R0VXQLfshI2CvddViqA9MufAxWd2DwsHzobR64W7wNlR4UFG0jvhjt9Y0Tzn3ySyI=,iv:jS5CXkL/8NUJNJ7Hm8wKbOo2tEhoHX7pAdkRY2G0pzg=,tag:gzcjcH1vna3uyQBuJyLZhg==,type:str] + lastmodified: "2026-06-11T14:19:15Z" + mac: ENC[AES256_GCM,data:eC5TEJUvzF6r3vyqswimY7okOOwxXiJLvVVymRWQmW3uAy1+/MyTD6TCHKmX20q/JscRJJAMtDsuafgKuoMz/Le+WmWtr0qqFApKcY7PDvPXXTrdSRLpVPIZaUW+2hXuQdRTsPFr1wYQo4mcCmeXlIPhnVZJrztvcEGt44wqdwU=,iv:jkBxL1Q8a4tRiO72oqdxwbm+zd67zbj58aNEySYb5MY=,tag:svsW1PTVtgerDCCe1B87qA==,type:str] unencrypted_suffix: _unencrypted version: 3.12.2 -- 2.51.2