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_FILEgets stripped — bake both system roots and any proxy CA - Wrap with NixOS
security.wrappersas 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:
- Declare desired state as structured Nix options
- Generate config JSON at build time
- A systemd service reconciles declared state with the remote API
- 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:
| Layer | Controlled by | Applied via | Location |
|---|---|---|---|
system | Admin | snix | NixOS config |
agent | Agent | home-switch | Home-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.