Skip to content

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.

ComponentTypePurpose
Pragmatic.Agentdaemon executable projectLocal process hosting socket/pipe IPC, KV persistence, gossip membership, CLI entrypoint
Pragmatic.Agent.Clientruntime packageUseAgent() integration for Pragmatic hosts, heartbeat service, Agent-backed stores
Pragmatic.Agent.Protocolruntime packageWire protocol, frames, payloads, JSON/MessagePack wire formats, transport security
  • 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, and ITenantStore
  • 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

From the repo root:

Terminal window
dotnet run --project Pragmatic.Agent/src/Pragmatic.Agent/Pragmatic.Agent.csproj -- start

Useful overrides:

Terminal window
dotnet run --project Pragmatic.Agent/src/Pragmatic.Agent/Pragmatic.Agent.csproj -- start --instance booking --socket /tmp/pragmatic/booking.sock
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.

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:

FormatFormatNameWhen
JsonWireFormatjsonDefault (FrameCodec.DefaultFormat). Human-readable, easy to debug. System.Text.Json, camelCase, null-omitting.
MessagePackWireFormatmsgpackCompact binary, faster than JSON. Opt-in by passing the format per call/connection.

The wire format is not a mutable globalFrameCodec.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:

ImplementationSecurityLevelUse
NoTransportSecuritynoneDefault. Local Unix socket / named pipe — no network exposure, OS socket permissions gate access, so no TLS is needed.
TlsTransportSecurity (single cert)tlsServer-cert TLS.
TlsTransportSecurity (mutualTls: true)mtlsBoth 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.

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/.

ImplementationBehavior
AesSecretEncryptorAES-256-GCM. Format: base64(nonce[12] + ciphertext + tag[16]). Requires a 32-byte key.
RejectEncryptionFail-closed: any secret read/write throws “no encryption key configured”. Used outside production when no key is present.
NoEncryptionPass-through (development only, must be explicitly opted in).

Key resolution order in CreateFromEnvironment:

  1. PRAGMATIC_AGENT_SECRET_KEY environment variable (base64, must decode to exactly 32 bytes)
  2. {dataDir}/secret.key file (base64, 32 bytes)
  3. No key found → throws in Production (ASPNETCORE_ENVIRONMENT / DOTNET_ENVIRONMENT = Production); otherwise returns RejectEncryption so 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.md still states secrets are “not encrypted at rest” — that line predates AesSecretEncryptor and is stale relative to the code described above.

Terminal window
dotnet run --project Pragmatic.Agent/src/Pragmatic.Agent/Pragmatic.Agent.csproj -- status
dotnet run --project Pragmatic.Agent/src/Pragmatic.Agent/Pragmatic.Agent.csproj -- config set Mail:Host smtp.example.com
dotnet run --project Pragmatic.Agent/src/Pragmatic.Agent/Pragmatic.Agent.csproj -- flag set bookings.new-checkout true

The Agent is the local coordination substrate. Other modules consume it rather than re-implementing their own stores or control channels:

  • Pragmatic.Configuration can read and write config through the Agent KV store
  • Pragmatic.FeatureFlags can use Agent-backed flag storage with fallback behavior
  • Pragmatic.MultiTenancy can source tenant data from the Agent
  • Pragmatic.Gateway can load routes from Agent state

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

Local docs:

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).