title: Plugin Architecture tags: [architecture, plugins, extensibility] created: 2026-04-24
Plugin Architecture
Cowboy's NixOS module system provides composable extension points. External modules ("plugins") use these to add capabilities to agents without modifying cowboy core.
Plugin Structure
A well-structured plugin separates concerns into layers:
Infrastructure — systemd services that provide the underlying capability (game servers, databases, hardware interfaces). These run independent of the agent harness.
Packages — derivations that provide the programmatic interface
to the infrastructure (Python environments, CLI tools, libraries).
Self-contained; built via callPackage.
Harness integration — conditional blocks that register capabilities with cowboy's extension points (skills, tools, bridges). Guarded so the plugin works even without cowboy present.
This separation keeps the plugin portable. Infrastructure and packages are reusable; only the integration layer couples to cowboy.
Extension Points
Skills
Register knowledge and prompts that agents can load:
services.cowboy.skills.<name> = {
description = "Human-readable description";
prompt = ./path/to/prompt.md; # or promptText = "inline";
requires = [ pkgs.somePackage ]; # installed into agent $PATH
additionalTools = [ "bash" "read" ]; # tools available when skill is active
tags = [ "category" ];
autoLoad = false; # true = always active
};
Skills are distributed to all enabled agents via home-manager.
The requires packages are added to each agent's home.packages.
Tools
Register executable capabilities with JSON schemas:
services.cowboy.tools.<name> = {
command = "my-tool --arg {input}";
description = "What this tool does";
schema = { type = "object"; properties = { /* ... */ }; };
sandbox = "standard"; # "standard" | "strict" | "none"
package = pkgs.myTool;
};
Bridges
Register bidirectional message channels (external platform <-> agent):
services.cowboy.bridges.<name> = {
pkg = myBridgePkg; # must provide bin/{name}-{ping,ingest,outbox}
env = "/run/agenix/creds"; # EnvironmentFile path
user = config.services.cowboy.broker;
icon = "emoji";
approval = { required = true; notify_channel = "..."; };
};
Bridges auto-generate Redis stream config and three systemd services
(ping, ingest, outbox) under the cowboy-bridges target.
Systemd Target
Any systemd service can join the agent lifecycle:
systemd.services.my-service = {
wantedBy = [ "cowboy.target" ];
};
Plugin Contract
1. Own your namespace
Plugins define options under their own top-level namespace, not
under services.cowboy.*:
# Good: own namespace
options.services.mun = { enable = ...; gameDir = ...; };
# Bad: reaching into cowboy's namespace
options.services.cowboy.ksp = { enable = ...; };
This keeps cowboy's option tree stable and lets the plugin function independently — infrastructure services can run even if cowboy isn't imported.
2. Read cowboy config via cowboyLib
Cowboy exports a cowboyLib module argument with helpers for
accessing agent configuration:
{ config, lib, pkgs, cowboyLib, ... }:
cowboyLib.enabledAgents # pre-filtered map of enabled agents
cowboyLib.forAgents (acfg: { home.packages = [ pkg ]; })
Accept it with a default fallback for standalone use:
{ cowboyLib ? { enabledAgents = {}; forAgents = _: {}; }, ... }:
3. Guard harness integration
Separate infrastructure from cowboy-specific registration:
let
hasCowboy = cowboyLib.enabledAgents != {};
in
{
config = lib.mkIf cfg.enable {
# Infrastructure: always runs
systemd.services.mun-ksp = { ... };
# Cowboy integration: only when cowboy is present
services.cowboy.skills = lib.mkIf hasCowboy { ... };
home-manager.users = lib.mkIf hasCowboy (
cowboyLib.forAgents (acfg: { home.packages = [ myPkg ]; })
);
};
}
4. Don't block the target
Plugin services must not prevent cowboy.target from activating:
- Use
wantedBy = [ "cowboy.target" ](soft pull-in). Do not usepartOf(hard lifecycle coupling — if the service fails, systemd tears down the entire target). - Set
StartLimitBurstandStartLimitIntervalSecon any service withRestart = "on-failure". Without these, a restart loop blocksswitch-to-configurationduring rebuilds. - Use
requires/after/partOfbetween the plugin's own services for internal lifecycle management.
unitConfig = {
StartLimitBurst = 5;
StartLimitIntervalSec = 300;
};
serviceConfig = {
Restart = "on-failure";
RestartSec = 5;
};
5. Keep packages self-contained
Plugins callPackage their own dependencies. Don't assume cowboy
provides any particular package.
Example: Complete Plugin
# module.nix
{ config, lib, pkgs,
cowboyLib ? { enabledAgents = {}; forAgents = _: {}; },
... }:
let
cfg = config.services.mun;
hasCowboy = cowboyLib.enabledAgents != {};
krpcPython = pkgs.python3.withPackages (ps: [
(pkgs.callPackage ./krpc.nix {})
ps.matplotlib
]);
in
{
imports = [ ./services.nix ./ckan.nix ];
options.services.mun = {
enable = lib.mkEnableOption "KSP + Twitch streaming stack";
gameDir = lib.mkOption { type = lib.types.path; };
stream.enable = lib.mkEnableOption "Twitch streaming";
# ...
};
config = lib.mkIf cfg.enable {
# Register skills with cowboy (when present)
services.cowboy.skills = lib.mkIf hasCowboy {
ksp-pilot = {
description = "Control KSP vessels via kRPC";
requires = [ krpcPython ];
prompt = ./skills/ksp-pilot.md;
tags = [ "ksp" ];
};
};
home-manager.users = lib.mkIf hasCowboy (
cowboyLib.forAgents (acfg: {
home.packages = [ krpcPython ];
})
);
};
}
# services.nix — infrastructure services
{ config, lib, pkgs, ... }:
let cfg = config.services.mun; in
{
config = lib.mkIf cfg.enable {
systemd.services.mun-xvfb = {
description = "Xvfb virtual display for KSP";
wantedBy = [ "cowboy.target" ];
unitConfig = { StartLimitBurst = 5; StartLimitIntervalSec = 300; };
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.xorg.xorgserver}/bin/Xvfb ...";
Restart = "on-failure";
};
};
systemd.services.mun-ksp = {
description = "Kerbal Space Program";
after = [ "mun-xvfb.service" ];
requires = [ "mun-xvfb.service" ];
wantedBy = [ "cowboy.target" ];
unitConfig = { StartLimitBurst = 3; StartLimitIntervalSec = 300; };
serviceConfig = { Restart = "on-failure"; };
};
systemd.services.mun-stream = lib.mkIf cfg.stream.enable {
description = "KSP Twitch stream";
after = [ "mun-ksp.service" ];
requires = [ "mun-ksp.service" ];
wantedBy = [ "cowboy.target" ];
unitConfig = { StartLimitBurst = 3; StartLimitIntervalSec = 120; };
serviceConfig = { Restart = "on-failure"; RestartSec = 5; };
};
};
}
Consumer config
# machines/lambda.nix
{
imports = [
inputs.cowboy.nixosModules.default
../modules/mun.nix/module.nix
];
services.mun = {
enable = true;
gameDir = "/opt/ksp";
stream.enable = true;
};
}