Pragmatic.Agent
Local coordination daemon and client libraries for Pragmatic.Design applications.
Pragmatic.Agent gives distributed hosts a shared KV store, SWIM-based membership, local IPC, and operational primitives for configuration, feature flags, tenant data, and gateway/control coordination.
Status: implemented preview subsystem. The runtime exists today and is used inside the repo; this is not a shape-only placeholder. Native AOT for the daemon is planned, but the current daemon runs as a normal .NET executable.
What Ships
Section titled “What Ships”| Component | Type | Purpose |
|---|---|---|
Pragmatic.Agent | daemon executable project | Local process hosting socket/pipe IPC, KV persistence, gossip membership, CLI entrypoint |
Pragmatic.Agent.Client | runtime package | UseAgent() integration for Pragmatic hosts, heartbeat service, Agent-backed stores |
Pragmatic.Agent.Protocol | runtime package | Wire protocol, frames, payloads, JSON/MessagePack wire formats, transport security |
Core Capabilities
Section titled “Core Capabilities”- Local socket or named-pipe connection between app and Agent
- Persistent KV store with file-backed state
- SWIM-based cluster membership and gossip
- Agent-backed
IControlPlane,IConfigurationStore,IFeatureFlagStore, andITenantStore - Development auto-start from
UseAgent() - Built-in CLI for status, config, and feature flags
- Platform adapters for IIS, nginx, Docker, Kubernetes, Azure Container Apps, and AWS ECS
Quick Start
Section titled “Quick Start”1. Start the daemon
Section titled “1. Start the daemon”From the repo root:
dotnet run --project Pragmatic.Agent/src/Pragmatic.Agent/Pragmatic.Agent.csproj -- startUseful overrides:
dotnet run --project Pragmatic.Agent/src/Pragmatic.Agent/Pragmatic.Agent.csproj -- start --instance booking --socket /tmp/pragmatic/booking.sock2. Connect a Pragmatic host
Section titled “2. Connect a Pragmatic host”using Pragmatic.Agent.Client;
await PragmaticApp.RunAsync(args, builder =>{ builder.UseAgent(agent => { agent.SocketPath = "/var/run/pragmatic/agent.sock"; agent.HeartbeatInterval = TimeSpan.FromSeconds(15); });});When the daemon is unreachable, the app degrades gracefully and keeps serving with local or in-memory defaults.
Wire Format and Transport Security
Section titled “Wire Format and Transport Security”Pragmatic.Agent.Protocol frames are length-prefixed: [4-byte big-endian length][payload], capped at 16 MB (FrameCodec). The body serializer is pluggable through IWireFormat:
| Format | FormatName | When |
|---|---|---|
JsonWireFormat | json | Default (FrameCodec.DefaultFormat). Human-readable, easy to debug. System.Text.Json, camelCase, null-omitting. |
MessagePackWireFormat | msgpack | Compact binary, faster than JSON. Opt-in by passing the format per call/connection. |
The wire format is not a mutable global — FrameCodec.DefaultFormat is immutable (a mutable static was racy under concurrent use). To use MessagePack, pass it as the format argument to FrameCodec.Encode/ReadFrameAsync/WriteFrameAsync rather than swapping a global. MessagePackWireFormat deliberately uses the contract-based StandardResolver with MessagePackSecurity.UntrustedData (not a contractless/typeless resolver) because Agent frames arrive from remote peers — this closes deserialization gadget-chain vectors and bounds DoS from hash collisions / deep nesting.
Transport security is selected through ITransportSecurity.SecureStreamAsync, which wraps the raw stream:
| Implementation | SecurityLevel | Use |
|---|---|---|
NoTransportSecurity | none | Default. Local Unix socket / named pipe — no network exposure, OS socket permissions gate access, so no TLS is needed. |
TlsTransportSecurity (single cert) | tls | Server-cert TLS. |
TlsTransportSecurity (mutualTls: true) | mtls | Both peers present certificates — intended for agent-to-agent gossip. |
TlsTransportSecurity fails closed: allowSelfSigned: true without trustedThumbprints throws unless ASPNETCORE_ENVIRONMENT / DOTNET_ENVIRONMENT is Development, since unpinned self-signed peers enable MITM on gossip. For production internal gossip, supply trustedThumbprints (SHA-1 cert pinning); targetHost sets SNI / name validation for CA-issued certificates.
So in the common case — app talking to its local daemon — the transport is the local socket with NoTransportSecurity and JSON framing; TLS/mTLS is reserved for non-local agent-to-agent links where it is explicitly configured.
Secret Encryption at Rest
Section titled “Secret Encryption at Rest”Values stored under the secret/ KV namespace are encrypted at rest via ISecretEncryptor (Pragmatic.Agent.KV). The daemon resolves the encryptor at startup with AesSecretEncryptor.CreateFromEnvironment(dataDir) and the handler encrypts on KvSet / decrypts on KvGet only for keys that start with secret/.
| Implementation | Behavior |
|---|---|
AesSecretEncryptor | AES-256-GCM. Format: base64(nonce[12] + ciphertext + tag[16]). Requires a 32-byte key. |
RejectEncryption | Fail-closed: any secret read/write throws “no encryption key configured”. Used outside production when no key is present. |
NoEncryption | Pass-through (development only, must be explicitly opted in). |
Key resolution order in CreateFromEnvironment:
PRAGMATIC_AGENT_SECRET_KEYenvironment variable (base64, must decode to exactly 32 bytes){dataDir}/secret.keyfile (base64, 32 bytes)- No key found → throws in Production (
ASPNETCORE_ENVIRONMENT/DOTNET_ENVIRONMENT=Production); otherwise returnsRejectEncryptionso the daemon still starts for non-secret workflows while secret operations fail closed.
A configured-but-invalid key (bad base64 or wrong length) always throws — it is treated as operator misconfiguration, not silently ignored. Secret values are also masked (***) from prefix listings, so they are only readable via an explicit KvGet on the exact key.
Note:
docs/concepts.mdstill states secrets are “not encrypted at rest” — that line predatesAesSecretEncryptorand is stale relative to the code described above.
3. Use the CLI
Section titled “3. Use the CLI”dotnet run --project Pragmatic.Agent/src/Pragmatic.Agent/Pragmatic.Agent.csproj -- statusdotnet run --project Pragmatic.Agent/src/Pragmatic.Agent/Pragmatic.Agent.csproj -- config set Mail:Host smtp.example.comdotnet run --project Pragmatic.Agent/src/Pragmatic.Agent/Pragmatic.Agent.csproj -- flag set bookings.new-checkout trueHow It Fits
Section titled “How It Fits”The Agent is the local coordination substrate. Other modules consume it rather than re-implementing their own stores or control channels:
Pragmatic.Configurationcan read and write config through the Agent KV storePragmatic.FeatureFlagscan use Agent-backed flag storage with fallback behaviorPragmatic.MultiTenancycan source tenant data from the AgentPragmatic.Gatewaycan load routes from Agent state
Current Scope
Section titled “Current Scope”Implemented today:
- daemon process and CLI
- socket/pipe transport
- persistent KV store
- gossip cluster primitives
- client connection and heartbeat
- Agent-backed store replacements in
UseAgent() - platform detection and adapters
Still evolving:
- daemon AOT publishing
- broader cloud sync and deployment workflows
- wider operational tooling around the Agent ecosystem
Documentation
Section titled “Documentation”Local docs:
License
Section titled “License”Part of the Pragmatic.Design ecosystem — see Licensing. Pragmatic.Agent is licensed under the PolyForm Small Business 1.0.0 license (free for small businesses; commercial license above the threshold).