Agent System Design Patterns

Patterns discovered while building the agent infrastructure.

1. SUID Broker Pattern

When the agent needs to call a binary that reads secrets, use a compiled Rust binary with SUID — not a shell script (bash drops euid for SUID).

Key constraints:

  • SUID wrappers sanitize the runtime environment, stripping env vars
  • Bake all paths at compile time via env!() macros: credential paths, config, CA certs
  • CA certs matter because SSL_CERT_FILE gets stripped — bake both system roots and any proxy CA
  • Wrap with NixOS security.wrappers as SUID, owned by the broker user
  • The agent process never sees secrets; the binary reads them from /run/agenix/

Nix derivation pattern:

rustPlatform.buildRustPackage {
  # ...
  env.API_KEY_PATH = "/run/agenix/api-key";
  env.CA_BUNDLE = "${cacert}/etc/ssl/certs/ca-bundle.crt";
};
#![allow(unused)]
fn main() {
const API_KEY_PATH: &str = env!("API_KEY_PATH");
const CA_BUNDLE: &str = env!("CA_BUNDLE");
}

Reference implementation: pkgs/namecheap/

2. Declarative Over Imperative

If state can be declared in Nix, it should be declared — not exposed as an agent tool. Prefer reconciliation services over API proxy tools.

Flow:

  1. Declare desired state as structured Nix options
  2. Generate config JSON at build time
  3. A systemd service reconciles declared state with the remote API
  4. Tools are reserved for inherently imperative operations (sending messages, running code, interactive queries)

Example — DNS records:

# Instead of a "call Namecheap API" tool:
services.namecheap.records = {
  "home.example.com" = { type = "A"; value = "1.2.3.4"; };
};

A reconciliation service reads the generated config and calls setHosts. The agent never touches the API directly.

3. Permission-Separated Additive Configs

Split configuration by authority level so the agent can manage its own state without escalating privileges.

Two config layers:

LayerControlled byApplied viaLocation
systemAdminsnixNixOS config
agentAgenthome-switchHome-manager config

Merge rules:

  • Reconciliation service reads both configs and merges additively (union)
  • System config wins on conflict — agent cannot shadow or remove system records
  • Agent can only add, never subtract

Bridge mechanism:

A systemd path unit watches the agent's config file for changes and triggers the NixOS reconciliation service:

systemd.paths.namecheap-agent-watch = {
  pathConfig.PathChanged = "/home/agent/.config/namecheap/records.json";
  wantedBy = [ "multi-user.target" ];
};

This bridges the home-switch (unprivileged) to the system-level reconciliation service without granting the agent elevated access.