# OpenClaw Podman Module # Provides: AI Agent with Discord integration running in an isolated container # # Usage: # myModules.openclaw-podman = { # enable = true; # port = 18789; # domain = "openclaw.example.com"; # }; { config, lib, pkgs, inputs, ... }: let cfg = config.myModules.openclaw-podman; in { options.myModules.openclaw-podman = { enable = lib.mkEnableOption "OpenClaw AI Agent (Podman)"; superpowers = { enable = lib.mkEnableOption "openclaw-superpowers extension"; src = lib.mkOption { type = lib.types.path; default = inputs.openclaw-superpowers; description = "Path to openclaw-superpowers source"; }; }; port = lib.mkOption { type = lib.types.port; default = 18789; description = "Gateway port for OpenClaw"; }; domain = lib.mkOption { type = lib.types.str; example = "openclaw.example.com"; description = "Public domain for OpenClaw"; }; }; config = lib.mkIf cfg.enable { # Enable podman myModules.podman.enable = true; # Create directory for OpenClaw data systemd.tmpfiles.rules = [ "d /var/lib/openclaw 0755 1000 1000 -" # Assuming node user is uid 1000 "d /var/lib/openclaw/local 0755 1000 1000 -" # For Go toolchain ]; # OpenClaw container (bridge network — isolated from host services) virtualisation.oci-containers.containers."openclaw" = { image = "ghcr.io/openclaw/openclaw:latest"; ports = [ "127.0.0.1:${toString cfg.port}:8080" ]; environmentFiles = [ config.sops.templates."openclaw.env".path ]; volumes = [ "/var/lib/openclaw:/home/node/.openclaw" "/var/lib/openclaw/local:/home/node/.local" ] ++ lib.optionals cfg.superpowers.enable [ "${cfg.superpowers.src}:/home/node/superpowers-src:ro" ]; }; # Copy the declarative config before starting the container # This allows OpenClaw to safely write/rename the file at runtime without EBUSY errors systemd.services."podman-openclaw".preStart = lib.mkBefore '' mkdir -p /var/lib/openclaw/local cp -f ${./openclaw-config.json} /var/lib/openclaw/openclaw.json chown -R 1000:1000 /var/lib/openclaw chmod -R u+rwX /var/lib/openclaw ${lib.optionalString cfg.superpowers.enable '' # Setup openclaw-superpowers mkdir -p /var/lib/openclaw/extensions mkdir -p /var/lib/openclaw/skill-state ln -sfT /home/node/superpowers-src/skills /var/lib/openclaw/extensions/superpowers # Replicate install.sh stateful skill registration REPO_SRC="${cfg.superpowers.src}" for skill_file in $REPO_SRC/skills/openclaw-native/*/SKILL.md; do [ -f "$skill_file" ] || continue skill_name=$(basename $(dirname "$skill_file")) # Check if stateful: true in frontmatter if sed -n '2,/^---$/p' "$skill_file" | grep -q '^stateful: *true'; then mkdir -p "/var/lib/openclaw/skill-state/$skill_name" if [ ! -f "/var/lib/openclaw/skill-state/$skill_name/state.yaml" ]; then echo "# Runtime state for $skill_name — managed by openclaw-superpowers" > "/var/lib/openclaw/skill-state/$skill_name/state.yaml" fi fi done chown -R 1000:1000 /var/lib/openclaw/extensions /var/lib/openclaw/skill-state ''} ''; # Go toolchain installation script # Stored in /var/lib/openclaw and executed inside the container environment.etc."openclaw/install-go.sh".source = pkgs.writeScript "install-go.sh" '' #!/bin/bash set -e GO_URL="https://go.dev/dl/go1.24.1.linux-amd64.tar.gz" GO_DIR="/home/node/.local/go" if [ -d "$GO_DIR" ]; then echo "Go already installed at $GO_DIR" exit 0 fi echo "Installing Go toolchain" mkdir -p /home/node/.local if command -v curl &> /dev/null; then curl -fsSL "$GO_URL" | tar -C /home/node/.local -xzf - elif command -v wget &> /dev/null; then wget -qO- "$GO_URL" | tar -C /home/node/.local -xzf - else echo "ERROR - Neither curl nor wget available" exit 1 fi echo "Go installed successfully" "$GO_DIR/bin/go" version ''; # Go toolchain installation # Downloads Go to a persistent volume for use inside the container systemd.services."openclaw-go-setup" = { description = "Install Go toolchain for OpenClaw"; after = [ "podman-openclaw.service" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "oneshot"; # Copy script to container-accessible location, then execute ExecStart = "${pkgs.bash}/bin/bash -c 'cp /etc/openclaw/install-go.sh /var/lib/openclaw/ && chmod +x /var/lib/openclaw/install-go.sh && ${pkgs.podman}/bin/podman exec -u node openclaw /home/node/.openclaw/install-go.sh'"; RemainAfterExit = true; }; }; # Optional: Install PyYAML inside the container on startup # We do this as a postStart or a simple background loop if needed, # but a better way is to ensure the image has it. # Since we can't easily change the image here, we'll try to run a one-time pip install. systemd.services."openclaw-superpowers-setup" = lib.mkIf cfg.superpowers.enable { description = "One-time setup for OpenClaw superpowers (PyYAML and Cron)"; after = [ "podman-openclaw.service" "openclaw-go-setup.service" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "oneshot"; ExecStart = "${pkgs.podman}/bin/podman exec -u node openclaw bash -c '\ python3 -m pip install --user PyYAML && \ for skill_file in /home/node/superpowers-src/skills/openclaw-native/*/SKILL.md; do \ [ -f \"$skill_file\" ] || continue; \ skill_name=$(basename $(dirname \"$skill_file\")); \ fm_cron=$(sed -n \"2,/^---$/p\" \"$skill_file\" | grep \"^cron:\" | sed \"s/^cron: *//\" | tr -d \"'\\\"\"); \ if [ -n \"$fm_cron\" ]; then \ openclaw cron add \"$skill_name\" \"$fm_cron\" || echo \"Cron add failed for $skill_name\"; \ fi; \ done'"; RemainAfterExit = true; }; }; }; }