184 lines
6.7 KiB
Nix
184 lines
6.7 KiB
Nix
# 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
|
|
''}
|
|
'';
|
|
|
|
# Set git email for the node user inside the container
|
|
systemd.services."openclaw-git-config" = {
|
|
description = "Configure git email for OpenClaw node user";
|
|
after = [ "podman-openclaw.service" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
ExecStart = "${pkgs.podman}/bin/podman exec -u node openclaw git config --global user.email 'kafka@ashisgreat.xyz'";
|
|
RemainAfterExit = true;
|
|
};
|
|
};
|
|
|
|
# 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;
|
|
};
|
|
};
|
|
};
|
|
}
|