AgentSkillsCN

development

在基础设施、Nix配置、密钥管理、服务定义、部署脚本、构建流水线,或排查开发环境问题时使用此技能。适用于开发环境、Nix Flake、NixOS基础设施、sops-nix密钥、systemd服务、部署流程,以及Elixir平台、Docker容器和CI流水线的参考。

SKILL.md
--- frontmatter
name: development
description: Development environment, Nix flakes, NixOS infrastructure, sops-nix secrets, systemd services, deployment, Elixir platform, Docker containers, and CI pipeline reference. Use when working on infrastructure, Nix configuration, secrets management, service definitions, deploy scripts, build pipelines, or troubleshooting dev environment issues.

Guardian Core Development & Infrastructure Reference

Comprehensive reference for all development, infrastructure, and operations concerns. Cross-references /debug for container issues and /self-update for deploy commands.


1. Dev Environment

Bootstrap

bash
scripts/bootstrap.sh   # Idempotent; installs Nix, direnv, pins flake.lock, runs bun install

Works on macOS (Apple Silicon/Intel), NixOS, and non-NixOS Linux. Detects NixOS via /etc/NIXOS.

Nix devShell Contents

Activated automatically via .envrc (use flake) + direnv, or manually with nix develop.

ToolVersion / SourceNotes
Node.jsnodejs_22 (nixpkgs)Used by WhatsApp bridge, container runtime
Bun1.3.8 (pinned, asserted)Runtime + package manager; shellHook aborts on mismatch
Gitnixpkgs
Docker clientdocker-client (nixpkgs)CLI only; daemon runs on host
agenixpkgsFor local secret editing
Elixir1.18 (erlang_27)beam.packages.erlang_27.elixir_1_18
Erlang/OTP27beam.packages.erlang_27.erlang

Shell Environment

Set by shellHook in flake.nix:

code
PATH prepends: $PWD/node_modules/.bin, $MIX_HOME/bin, $MIX_HOME/escripts
MIX_HOME = $PWD/.nix-mix
HEX_HOME = $PWD/.nix-hex
BUN_VERSION_PIN = 1.3.8

direnv Integration

FilePurpose
.envrcContains use flake — activates devShell on cd
.nix-mix/Local Mix home (gitignored)
.nix-hex/Local Hex home (gitignored)

2. Nix Flake Architecture

Source: flake.nix

code
inputs:
  nixpkgs        → github:NixOS/nixpkgs/nixpkgs-unstable
  flake-utils    → github:numtide/flake-utils
  sops-nix       → github:Mic92/sops-nix
  private-config → path:/etc/guardian-private  (flake = false)

Outputs

OutputSystemPurpose
devShells.defaulteachDefaultSystemDev toolchain (Node 22, Bun 1.3.8, Elixir 1.18/OTP 27)
nixosConfigurations.rumi-vpsx86_64-linuxFull NixOS system config for production VPS

Private Config Pattern

Nix flakes can only see git-tracked files. Host-specific config lives at /etc/guardian-private/ (external flake input, flake = false):

FileContents
/etc/guardian-private/private.nixHostname, SSH keys, domain, sops age keyFile path
/etc/guardian-private/hardware-configuration.private.nixBoot loader, disk/filesystem, kernel modules

hardware-configuration.nix (tracked) delegates to private-config + "/hardware-configuration.private.nix".

Updating Flake Inputs

bash
nix flake update                    # Update all inputs
nix flake lock --update-input nixpkgs  # Update only nixpkgs

Pitfall: After updating nixpkgs, rebuild to verify nothing breaks before deploying.


3. NixOS System Configuration

Host: rumi-vps (167.114.144.68, x86_64-linux)

Module Import Chain

configuration.nix imports:

  1. ./hardware-configuration.nix → delegates to private-config
  2. ./services/guardian-core.nix
  3. ./services/rumi-platform.nix
  4. ./services/server.nix
  5. ./sops-secrets.nix
  6. private-config + "/private.nix"

System Settings

SettingValue
system.stateVersion24.11
time.timeZoneUTC
Nix experimental featuresnix-command, flakes
Trusted usersroot, rumi

User: rumi

PropertyValue
isNormalUsertrue
extraGroupsdocker, wheel
Passwordless sudoYes (security.sudo.wheelNeedsPassword = false)

SSH

SettingValue
PasswordAuthenticationfalse
PermitRootLoginprohibit-password

Critical: Port 22 MUST remain in allowedTCPPorts — SSH is via public IP, not Tailscale only.

Caddy Reverse Proxy

nix
services.caddy.enable = true;
services.caddy.globalConfig = ''acme_ca https://acme-v02.api.letsencrypt.org/directory'';

Virtual host definitions live in private.nix (host-specific domains).

Tailscale

nix
services.tailscale.enable = true;

Firewall

RuleValue
allowedTCPPorts22, 80, 443
trustedInterfacestailscale0

System Packages (VPS)

git, curl, htop, jq, bun, nodejs_22, docker-client

Other

FeatureValue
programs.nix-ld.enabletrue (run generic Linux binaries)
Docker autoPruneWeekly

4. Secrets Management (sops-nix + age)

Architecture

code
.sops.yaml                          # Creation rules: path_regex → age keys
infra/nixos/secrets/*.env           # Encrypted dotenv files (tracked in git)
/var/lib/sops-nix/key.txt           # age private key (0400 root:root)
/run/secrets/*                      # Decrypted at runtime to tmpfs

Security Controls

ControlRule
SP-SECRET-001No plaintext secrets in repo; encrypted dotenv only (non-waivable)
SP-SECRET-002systemd hardening on all services (non-waivable)
SP-SECRET-003sops-nix runtime injection via EnvironmentFile; fail-closed on missing secret
SP-GATE-SECRETSgitleaks scan blocks release on detected secrets

Encrypted Secret Files

Filesops secret nameOwnerMode
infra/nixos/secrets/guardian-core.envguardian-core-envrumi:users0400
infra/nixos/secrets/rumi-platform.envrumi-platform-envrumi:users0400
infra/nixos/secrets/rumi-server.envrumi-server-envrumi:users0400

All defined in infra/nixos/sops-secrets.nix with format = "dotenv".

.sops.yaml

yaml
keys:
  - &rumi-vps age1gngjsgsqhdjre3yjnk2dqregh3dt7n38273uxsjx52xqszz3jujqfcthcp
creation_rules:
  - path_regex: infra/nixos/secrets/.*\.env$
    key_groups:
      - age:
          - *rumi-vps

Editing Secrets

age and sops are NOT installed system-wide on the VPS. Use nix-shell:

bash
nix-shell -p sops age --run \
  "SOPS_AGE_KEY_FILE=/var/lib/sops-nix/key.txt sops infra/nixos/secrets/<file>.env"

Adding a New Secret File

  1. Create the encrypted file:
    bash
    nix-shell -p sops age --run \
      "SOPS_AGE_KEY_FILE=/var/lib/sops-nix/key.txt sops infra/nixos/secrets/new-service.env"
    
  2. Add entry in infra/nixos/sops-secrets.nix:
    nix
    sops.secrets."new-service-env" = {
      sopsFile = ./secrets/new-service.env;
      format = "dotenv";
      owner = "rumi";
      group = "users";
      mode = "0400";
    };
    
  3. Reference in service unit: EnvironmentFile = config.sops.secrets."new-service-env".path;
  4. Commit the encrypted .env file to git.

Fail-Closed Pattern

All three service .nix files use a throw guard:

nix
envFile =
  if builtins.hasAttr envSecretName config.sops.secrets then
    config.sops.secrets.${envSecretName}.path
  else
    throw "<service> requires sops secret ${envSecretName}; do not use plaintext .env files.";

This prevents NixOS from building if the secret definition is missing — fail-closed.

Key Management

ItemValue
Age private key/var/lib/sops-nix/key.txt (0400 root:root)
Age public keyListed in .sops.yaml
Rotation period90 days (per docs/security/secrets_management.json)
Bootstrap runbookRUNBOOK-SOPS-BOOTSTRAP in docs/security/orchestration_runbook.json

5. Systemd Services

Service Comparison

Propertyguardian-corerumi-platformrumi-server
DescriptionGuardian Core — Personal Claude assistantGuardian Platform (Elixir Phoenix)Rumi Webhook Server
Typesimpleexecsimple
User/Grouprumi / usersrumi / usersrumi / users
WorkingDirectory/opt/guardian-core/opt/guardian-platform/opt/guardian-core/server
ExecStart.../rel/guardian/bin/guardian start/opt/guardian-platform/bin/guardian start${pkgs.bun}/bin/bun run src/index.ts
ExecStop— (SIGTERM).../bin/guardian stop— (SIGTERM)
Afternetwork.target, docker.servicenetwork.targetnetwork.target
Requiresdocker.service
Restartalways / 5salways / 5salways / 5s
EnvironmentFilesops guardian-core-envsops rumi-platform-envsops rumi-server-env
Log stdoutlogs/guardian-core.log— (journal)— (journal)
Log stderrlogs/guardian-core.error.log— (journal)— (journal)
HOME accessYes (ProtectHome=tmpfs + BindPaths=["/home/rumi"])No (ProtectHome=true)No (ProtectHome=true)
ReadWritePaths/opt/guardian-core, /home/rumi/opt/guardian-platform/opt/guardian-core/server
LimitNOFILE65535

Hardening (SP-SECRET-002, all services)

nix
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";

Key: ProtectSystem = "strict" makes the entire filesystem read-only except paths listed in ReadWritePaths.

guardian-core Environment Variables

Set in services/guardian-core.nix:

VariableValue
HOME/home/rumi
ASSISTANT_NAMERumi
MIX_ENVprod
GUARDIAN_PROJECT_ROOT/opt/guardian-core

Additional vars come from the sops EnvironmentFile (API keys, tokens).

Systemd Service Template (non-NixOS)

infra/systemd/guardian-core.service is a template with {{PLACEHOLDERS}} resolved by mix deploy.brain. Used for non-NixOS Linux installs. The NixOS .nix definitions take precedence on rumi-vps.


6. Deploying NixOS Changes

Command

bash
sudo nixos-rebuild switch --flake .#rumi-vps \
  --override-input private-config path:/etc/guardian-private

Dry Run

bash
sudo nixos-rebuild dry-activate --flake .#rumi-vps \
  --override-input private-config path:/etc/guardian-private

Pitfalls

IssueCauseFix
error: path '/etc/guardian-private' is not in the Nix storeForgot --override-inputAlways pass --override-input private-config path:/etc/guardian-private
File not found in flakeFile not git-trackedgit add the file first; Nix flakes only see tracked files
Secret decryption failsMissing age key or wrong permissionsVerify /var/lib/sops-nix/key.txt exists with 0400 root:root
Port 22 blocked after rebuildRemoved from allowedTCPPortsNever remove 22 — SSH is via public IP
Service won't startMissing sops secret definitionfail-closed throw fires; add the secret to sops-secrets.nix

Post-Deploy Verification

bash
systemctl status guardian-core rumi-platform rumi-server
journalctl -u guardian-core -n 20 --no-pager
journalctl -u rumi-platform -n 20 --no-pager
journalctl -u rumi-server -n 20 --no-pager

7. Elixir Development

Project: platform/

PropertyValue
App name:guardian
Version0.1.0
Elixir requirement~> 1.15
OTP (devShell)Erlang 27, Elixir 1.18
Web frameworkPhoenix ~> 1.8.3
HTTP serverBandit ~> 1.5
DatabaseSQLite3 (ecto_sqlite3 ~> 0.17)
JSONJason ~> 1.2
HTTP clientReq ~> 0.5
JWTJOSE ~> 1.11
CronCrontab ~> 1.1
LintingCredo ~> 1.7 (dev/test only)
Static analysisDialyxir ~> 1.4 (dev/test only)

Common Commands

bash
cd platform && mix deps.get          # Fetch dependencies
cd platform && mix compile            # Compile
cd platform && mix compile --warnings-as-errors  # Strict compile
cd platform && mix test               # Run tests
cd platform && mix phx.server         # Start dev server (port 4000, kernel disabled)
cd platform && mix format             # Format code
cd platform && mix credo              # Lint
cd platform && mix dialyzer           # Static type analysis
cd platform && mix precommit          # compile --warnings-as-errors + unlock unused + format + test

Config Hierarchy

FileEnvKey settings
config/config.exsAllkernel_enabled: false, Repo db path, Endpoint, Logger, Jason
config/dev.exsDevBind 127.0.0.1, code_reloader, debug_errors, dev_routes
config/test.exsTestPort 4002, in-memory SQLite (:memory:), server: false
config/prod.exsProdforce_ssl, log level: info
config/runtime.exsAll (runtime)GUARDIAN_PROJECT_ROOT, SQLite path, PHX_SERVER, kernel_enabled in prod

Required Env Vars (prod)

VariableSourceRequired
SECRET_KEY_BASEsopsYes (raises on missing)
PHX_HOSTsops or defaultNo (defaults to self.rumi.engineering)
PORTenvNo (defaults to 4000)
GITHUB_APP_IDsopsYes (raises on missing)
GITHUB_APP_PRIVATE_KEYsopsYes (raises on missing)
GITHUB_APP_INSTALLATION_IDsopsYes (raises on missing)
ELEVENLABS_WEBHOOK_SECRETsopsNo (optional)
GUARDIAN_PROJECT_ROOTsystemd envNo (defaults to File.cwd!())
PHX_SERVERsystemd envNo (enables HTTP server when set)

Building a Release

bash
cd platform && MIX_ENV=prod mix release --overwrite
# Output: platform/_build/prod/rel/guardian/bin/guardian

Mix Aliases

AliasExpands to
mix setupdeps.get
mix precommitcompile --warnings-as-errors + deps.unlock --unused + format + test

8. Docker Container Development

Image: guardian-core-agent

PropertyValue
Basenode:22-slim
Bun version1.3.8 (matches flake pin)
Runtime usernode (non-root, required for --dangerously-skip-permissions)
Working directory/workspace/group
Entry point/app/entrypoint.sh (sources env, pipes stdin to node /app/dist/index.js)
Claude Agent SDK@anthropic-ai/claude-agent-sdk@0.2.29

Build Pipeline (container/build.sh)

code
1. cd container/
2. Build @guardian/shared types (npx tsc in shared/)
3. Copy shared/dist + package.json → .shared-cache/
4. docker build -t guardian-core-agent:<tag> .
bash
./container/build.sh          # Build with tag "latest"
./container/build.sh v1.2     # Build with custom tag

Dockerfile Layer Order

code
1. FROM node:22-slim
2. apt-get: Chromium + system deps + GitHub CLI (gh)
3. Install Bun 1.3.8 to /opt/bun
4. Set AGENT_BROWSER_EXECUTABLE_PATH, PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH → /usr/bin/chromium
5. bun add -g agent-browser @anthropic-ai/claude-code
6. COPY .shared-cache/ /shared/
7. COPY agent-runner/package.json + bun.lock → /app/
8. bun install
9. COPY agent-runner/ → /app/
10. bun run build (TypeScript compilation)
11. mkdir /workspace/{group,global,extra,ipc/messages,ipc/tasks}
12. Create /app/entrypoint.sh
13. chown -R node:node /workspace
14. USER node

.dockerignore

code
**/node_modules
**/.tsbuildinfo

Container Packages

PackageVersionPurpose
@anthropic-ai/claude-agent-sdk0.2.29Claude Agent SDK for container agent
@guardian/sharedfile:../sharedShared TS types (IPC protocol, schemas)
cron-parser^5.0.0Parse cron expressions for task scheduling
zod^4.0.0Runtime schema validation

Testing Container

bash
# Quick test
echo '{"prompt":"What is 2+2?","groupFolder":"test","chatJid":"test@g.us","isMain":false}' | \
  docker run -i guardian-core-agent:latest

# Interactive shell
docker run --rm -it --entrypoint /bin/bash guardian-core-agent:latest

# Full test with mounts (see /debug skill for details)

Rebuilding

bash
./container/build.sh                    # Normal rebuild
docker builder prune -af && ./container/build.sh  # Clean rebuild

9. CI Pipeline

Source: .github/workflows/ci.yml

Triggers: push to main, all pull requests.

Jobs

JobRuns OnSteps
rootubuntu-latestcheckout → nix install → magic-nix-cache → bun install --frozen-lockfile → typecheck → test → build → lint
serverubuntu-latestcheckout → nix install → magic-nix-cache → bun --cwd server install --frozen-lockfile → typecheck
agent-runnerubuntu-latestcheckout → nix install → magic-nix-cache → bun --cwd container/agent-runner install --frozen-lockfile → build

Nix in CI

All jobs use:

  • DeterminateSystems/nix-installer-action@main — installs Nix
  • DeterminateSystems/magic-nix-cache-action@main — caches Nix store

All commands run inside nix develop --command ... to use the pinned devShell.

What's NOT in CI

  • Elixir compilation/tests (platform/)
  • Docker image builds
  • NixOS configuration checks
  • Secret decryption

10. Project Directory Reference

Tracked in Git

PathPurpose
flake.nixNix flake: devShell + NixOS config
flake.lockPinned input versions
.envrcdirenv: use flake
.sops.yamlsops creation rules + age public key
infra/nixos/configuration.nixNixOS system config
infra/nixos/hardware-configuration.nixDelegates to private input
infra/nixos/sops-secrets.nixsops secret definitions
infra/nixos/services/*.nixsystemd service modules (3 files)
infra/nixos/secrets/*.envEncrypted dotenv files (3 files)
infra/systemd/guardian-core.serviceSystemd template (non-NixOS, {{PLACEHOLDERS}})
platform/Elixir Phoenix application
container/DockerfileAgent container image definition
container/build.shContainer build script
container/agent-runner/Claude Agent SDK runner (TypeScript)
container/shared/Shared TS types (@guardian/shared)
container/whatsapp-bridge/Baileys WhatsApp bridge (Node.js)
scripts/bootstrap.shDev environment bootstrap
.github/workflows/ci.ymlCI pipeline
groups/{name}/CLAUDE.mdPer-group memory (isolated)
docs/security/*.jsonSecurity policy, checklist, waivers

Runtime (Not in Git)

PathPurpose
store/messages.dbSQLite database
logs/Application and deploy logs
data/env/envFiltered env file for container mounts
data/ipc/IPC directories (messages, tasks)
data/sessions/Per-group Claude session state
.nix-mix/, .nix-hex/Local Mix/Hex homes
node_modules/Bun dependencies
platform/_build/Elixir build artifacts
platform/deps/Elixir dependencies
container/.shared-cache/Pre-built shared package for Docker context

Host-Only (VPS, Not in Git)

PathPurpose
/etc/guardian-private/private.nixHostname, SSH keys, domain, sops config
/etc/guardian-private/hardware-configuration.private.nixBoot/disk/kernel modules
/var/lib/sops-nix/key.txtage private key (0400 root:root)
/run/secrets/*Decrypted secrets (tmpfs)
/opt/guardian-core/Deployed brain
/opt/guardian-platform/Deployed Phoenix release

11. Troubleshooting

Nix / devShell

IssueCauseFix
bun: command not foundNot in devShellRun nix develop or ensure direnv is active
error: expected bun 1.3.8, got X.Y.ZBun version mismatchUpdate nixpkgs or adjust bunVersion in flake.nix
error: assertion failed at bun.version == bunVersionnixpkgs bun doesn't match pinRun nix flake update or adjust bunVersion
use_flake: command not founddirenv missing nix-direnvInstall nix-direnv or run nix develop manually
elixir: command not foundNot in devShellActivate devShell; Elixir comes from beam.packages.erlang_27.elixir_1_18

NixOS Deployment

IssueCauseFix
path not in Nix storeMissing --override-inputAdd --override-input private-config path:/etc/guardian-private
No such file: private.nix/etc/guardian-private/ missing or emptyCreate the directory and populate from backup
sops decryption errorWrong/missing age keyCheck /var/lib/sops-nix/key.txt exists, mode 0400, owned root:root
Service fails to start post-rebuildMissing env varsCheck journalctl -u <service> for "missing" errors; edit sops secret
throw error during buildsops secret not definedAdd entry in sops-secrets.nix
Locked out of SSHPort 22 removed from firewallPrevention: never remove 22 from allowedTCPPorts; recovery requires VPS console

Elixir / Phoenix

IssueCauseFix
could not compile dependencyMissing system libs or version mismatchEnsure devShell is active (OTP 27 + Elixir 1.18)
(Mix) Could not find an SCM for :depMissing mix deps.getRun cd platform && mix deps.get
Tests fail in CI but pass locallyEnv var differencesCheck config/test.exs; tests use in-memory SQLite, port 4002
SECRET_KEY_BASE is missingProd env not configuredSet in sops secret or export for local testing
GITHUB_APP_ID is missingProd env not configuredSet in sops secret (fail-closed in prod)
Release won't startMissing GUARDIAN_PROJECT_ROOTSet env var or ensure CWD is project root

Docker / Container

IssueCauseFix
Cannot connect to Docker daemonDocker not runningsudo systemctl start docker
Container build fails at bun installNetwork issue or lock mismatchdocker builder prune -af then retry
--dangerously-skip-permissions cannot be used with rootRunning as root inside containerVerify USER node in Dockerfile
Shared types not found.shared-cache/ staleRun ./container/build.sh (rebuilds shared first)
Agent exits code 1Auth, permissions, or session issueSee /debug skill for detailed diagnosis

For container-specific debugging (mounts, env vars, sessions, IPC), see the /debug skill. For deployment commands and troubleshooting, see the /self-update skill.