Nix & Home Manager
Cacophony ships as a Nix flake with a Home Manager module for declarative per-user deployment.
Flake Outputs
| Output | Description |
|---|---|
packages.<system>.caco | The wrapped caco package with bundled startup PATH (including tmux-cli, excluding optional browser/image/YAML/network helpers) |
apps.<system>.caco | Runnable app (nix run) using the same wrapped launcher |
packages.<system>.playwright-cli | Explicit on-demand browser-observation helper package |
devShells.<system>.default | Development shell with Rust toolchain, av, tmux-cli, playwright-cli, image helpers, and YAML convenience tooling |
devShells.<system>.caco-runtime | Lightweight managed-worker shell with Rust, pinned av, and runtime helpers |
homeManagerModules.caco | Home Manager module |
Development
# Enter dev shell
nix develop
# Build
cargo build
# Run
cargo run -p caco -- version --json
# Or run directly via nix
nix run . -- version --json
The flake-distributed caco launcher is wrapped with the startup runtime tool PATH Caco needs, including tmux-cli, so direct nix run and profile-installed launches do not depend on a login-shell environment to find required startup helpers. Browser, image, YAML, and legacy network helpers such as playwright-cli, rembg, yq, and nettools remain available on demand through the default dev shell or explicit packages where exposed, but are not included in the mandatory launcher or service startup closure; cold optional dependency realization should not block daemon or persistent-agent startup. Source-sensitive helpers such as caco config sparse validate warn when the running binary commit differs from checkout HEAD, because Nix/profile installs can lag a just-edited source tree.
Home Manager Module
The Home Manager module provides declarative per-user bootstrap. It installs the binary, materializes config, exports environment variables, and optionally enables a native user-service backend that runs a foreground Cacophony lifecycle supervisor.
Minimal Configuration
{
services.cacophony = {
enable = true;
package = inputs.cacophony.packages.${pkgs.system}.caco;
nodeName = "localhost";
};
}
Full Configuration
{
services.cacophony = {
enable = true;
package = inputs.cacophony.packages.${pkgs.system}.caco;
nodeName = "localhost";
cacophonyDir = "${config.home.homeDirectory}/.cacophony";
providers = {
openai = {
baseUrl = "https://api.openai.com/v1";
apiKeyFile = config.sops.secrets.openai-api-key.path;
};
anthropic = {
baseUrl = "https://api.anthropic.com";
authTokenFile = config.sops.secrets.anthropic-auth-token.path;
};
pi.githubCopilot.authFile = "~/.pi/agent/auth.json";
};
configTemplateEnv = {
CACOPHONY_OPENAI_API_KEY_FILE = config.sops.secrets.openai-api-key.path;
};
autoUp = true; # Enable the managed lifecycle supervisor
downOnDisable = true; # Run caco down when disabled
restartOnConfigChange = false; # Default: log config drift; operator runs caco restart explicitly
# Trusted LAN/VPN or SSH tunnel only; do not expose PulseAudio TCP publicly.
extraEnv = {
PULSE_SERVER = "<trusted-audio-host>:4713";
};
};
}
Module Options
| Option | Type | Default | Description |
|---|---|---|---|
enable | bool | false | Enable Cacophony management |
package | package | — | The caco package to install |
nodeName | string | — | Logical node name (sets CACO_NODE) |
cacophonyDir | path | ~/.cacophony | Runtime directory (sets CACOPHONY_DIR) |
providers.openai.baseUrl | string | — | OpenAI-compatible base URL |
providers.openai.apiKeyFile | path | — | Secret file for OPENAI_API_KEY |
providers.anthropic.baseUrl | string | — | Anthropic-compatible base URL |
providers.anthropic.authTokenFile | path | — | Secret file for ANTHROPIC_AUTH_TOKEN |
providers.pi.githubCopilot.authFile | path | — | Pi native GitHub Copilot auth.json copied into managed Pi workspaces |
autoUp | bool | true | Enable the managed lifecycle supervisor |
downOnDisable | bool | true | Run caco down when disabled |
restartOnConfigChange | bool | false | Opt in to automatic caco restart when config.yaml changes; disabled by default to avoid restart loops from routine config writes |
configTemplateEnv | attrs | {} | Non-secret env exposed to {{ env ... }} config templates |
extraEnv | attrs | {} | Additional non-secret environment variables |
Behavior
When Enabled
- Installs the
cacobinary - Writes a valid
config.yamlto$CACOPHONY_DIR - Exports
CACOPHONY_DIRandCACO_NODEinto managed processes - If
autoUp = true: installs a native user service (launchd or systemd) that runscaco supervise --interval 30from the livecacobinary, so supervision policy updates withcacoitself.- What restarts: process and listener liveness are the hard signals. A slow or backpressured daemon API is reported as degraded rather than restarted, unless the daemon stays alive but unresponsive past warmup and a bounded grace period. A stopped TTS service alone never restarts the daemon.
- Config changes are detected but don't restart services by default; run
caco restart, or setrestartOnConfigChange = true. - Planned restarts: restart sentinels (including the one the health watchdog writes before exiting) make a respawn read as maintenance, and are cleared once services converge. Refresh helpers that install a new
cacoruncaco service startafterwards so the supervisor stays the owner. - Not loaded: if the service is installed but not loaded,
caco service status --jsonreportshealth_state: "not_loaded"withrepair_command: "caco service load", andcaco ops checkraises it as a safe action. On launchd,caco service loadbootstraps without a needlesskickstart -k. - Manual startup: use
caco service disablerather than raw launchctl/systemctl, thencaco up --skip-updateif services should still run unsupervised.
- Module-managed units load provider tokens from secret paths and export as environment variables
- When
providers.pi.githubCopilot.authFileis set, managed Pi agents can launch withprovider: github-copilotand a separatemodel; Cacophony translates that to Pi's native--model github-copilot/<model>selector and copies the configuredauth.jsoninto.pi-agent/
When Disabled
- If
downOnDisable = true: runscaco down - Does not delete
$CACOPHONY_DIRunless the user performs explicit cleanup
Service Managers
| Platform | Service Manager |
|---|---|
| Linux | User-level systemd |
| Darwin | User-level launchd |
| nix-on-droid | Conditional supervisord |
The native service manager keeps the foreground lifecycle loop alive; the loop itself remains the authority for daemon and sidecar lifecycle and does not supervise caco daemon directly.
- Wrapper: Home Manager units pass through a small secret-loading wrapper that resolves the live binary, then execs
caco supervise, so updatingcacoupdates the supervisor policy. - PATH: Generated systemd user units and launchd plists also set an explicit service-manager
PATHthat starts with the selectedcacolauncher's directory, then common user and system tool roots. Managed child services launched by the lifecycle loop receive the same deterministicCACO_BIN, startup runtimePATH, and login identity variables (HOME,USER, andLOGNAME) with the external tools Cacophony shells out to, includinggit,tmux,tmux-cli,ssh,jq,paplay/parec,grep/sed, and core POSIX utilities, while still searching common user bin roots such as~/.cargo/binand~/.nix-profile/bin, so standalone services do not depend on a login-shell environment after restart/update/manual lifecycle transitions and git/ssh subprocesses can resolve the running UID. Npm-backed browser helpers, image helpers, YAML convenience tools, and legacy network tools such asplaywright-cli,rembg,yq, andnettoolsare intentionally on-demand rather than part of service startup. The flake-distributedcacopackage and app use the same wrapped startupPATHcontract for direct launches.
Secrets Management
sops-nix. Use config.sops.secrets.<name>.path directly as the secret file option. Tokens are read at startup, never stored in config or committed files.