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 use partOf (hard lifecycle coupling — if the service fails, systemd tears down the entire target).
  • Set StartLimitBurst and StartLimitIntervalSec on any service with Restart = "on-failure". Without these, a restart loop blocks switch-to-configuration during rebuilds.
  • Use requires/after/partOf between 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;
  };
}