Write Zentinel agents in Haskell, speaking agent protocol v2 over a Unix socket or gRPC.
Agent protocol · Docs · All repositories
The zentinel-agent-protocol package gives you an AgentHandler type class
with one method per event over any MonadUnliftIO monad (so state lives in a
ReaderT), the protocol types, response builders, and health and metrics
helpers. It serves agents over the v2 Unix socket transport and over gRPC
(the proxy's zentinel.agent.v2.AgentServiceV2, using
grapesy). It is not on Hackage.
Status (checked on 2026-10-22 against Zentinel 26.10_8): version 0.2.0.0 passes the shared SDK conformance harness on both transports (handshake, configure, request and response headers, block, body inspection, reconnect), and the quick start below passes its
helloprofile. CI runs both on every push. Theconfigurecheck needs Zentinel 26.10_8 or later, which sends an agent'sconfig { }block on every connection (zentinel#603); releases up to 0.6.50 (26.10_7) never delivered it.Before 0.2.0.0 the package did not build (it asked for
grpc-haskell >= 0.3, which does not exist, andcontainers >= 0.7, which conflicts with GHC 9.8's boot packages), its gRPC module was a placeholder with its own framing, and its Unix socket transport used v1 framing and JSON shapes the proxy rejects.
Create Main.hs in an executable that depends on zentinel-agent-protocol
(see Install):
module Main where
import Zentinel.Agent.Protocol
instance AgentHandler IO where
capabilities = pure (defaultCapabilities "hello")
onRequestHeaders _ = pure (setRequestHeader "X-Hello" "from-agent" allow)
main :: IO ()
main = do
config <- configFromArgs defaultConfig {scSocketPath = Just "/tmp/hello.sock"}
runAgent config idThis is examples/hello-agent; run it with
cabal run hello-agent. It listens on /tmp/hello.sock (--socket PATH to
change that, --grpc HOST:PORT to serve gRPC instead, both for both).
Then point Zentinel at the socket. This assumes your config already defines a
listener and an upstream named backend:
agents {
agent "hello" type="custom" {
unix-socket "/tmp/hello.sock"
events "request_headers"
timeout-ms 100
}
}
filters {
filter "hello" {
type "agent"
agent "hello"
failure-mode "closed"
}
}
routes {
route "default" {
matches {
path-prefix "/"
}
upstream "backend"
filters "hello"
}
}Check the file with zentinel test -c zentinel.kdl and start Zentinel. Every
request on the route then reaches backend with X-Hello: from-agent.
failure-mode "closed" on the filter turns an unreachable agent into a 503
instead of letting requests through unprocessed, so a wiring mistake shows up
on the first request.
For gRPC, start the agent with --grpc 127.0.0.1:50051 and use
grpc "http://127.0.0.1:50051" in place of unix-socket.
Not on Hackage. Depend on it from Git in your cabal.project:
source-repository-package
type: git
location: https://github.com/zentinelproxy/zentinel-agent-haskell-sdk
subdir: zentinel-agent-protocoland add zentinel-agent-protocol to your executable's build-depends.
The gRPC transport (cabal flag grpc, on by default) depends on grapesy, which
links the C library snappy: apt-get install libsnappy-dev on Debian and
Ubuntu, brew install snappy on macOS (Homebrew's prefix is not on the default
search path, so add extra-lib-dirs: /opt/homebrew/lib and
extra-include-dirs: /opt/homebrew/include for package snappy-c in
cabal.project.local). For a Unix-socket-only build without it, add
constraints: zentinel-agent-protocol -grpc.
AgentHandler m:capabilities(required),onRequestHeaders(required),onRequestBodyChunk,onResponseHeaders,onResponseBodyChunk,onWebSocketFrame,onRequestComplete,healthStatus,metricsReport,onConfigure,onShutdown,onDrain,onCancel.runAgent config runner: serves the handler;ServerConfighasscSocketPath,scGrpcAddress,scLogLevel, anddefaultConfiguses/tmp/zentinel-agent.sock.configFromArgsapplies--socket PATH,--grpc HOST:PORTand--debug.- Configuration: the proxy sends the agent's
config { }block on every connection; it reachesonConfigure(returnFalseto reject it, which keeps the proxy from using the connection). - Responses:
allow,block status body,redirect,challenge,setRequestHeader/setResponseHeader, and record update onAgentResponse(respRequestHeaders,respResponseHeaderswithHeaderSet/HeaderAdd/HeaderRemove,respNeedsMorefor body chunks). - Capabilities:
defaultCapabilities name,defaultFeatures,defaultLimits; health and metrics builders inZentinel.Agent.HealthandZentinel.Agent.Metrics.
Examples: examples/hello-agent (the quick
start), examples/conformance-agent (the
agent the conformance harness drives: config, headers, block, body
inspection), examples/echo-agent and
examples/waf-agent.
cabal build all
cabal test allThe package declares tested-with: GHC == 9.8.4. The unit tests include a
Unix socket test that speaks the v2 frames to the server the way the proxy
does. To run the conformance harness locally against a zentinel from release
26.10_8 or later:
ZENTINEL_BIN=/path/to/zentinel/target/debug/zentinel \
ZENTINEL_SRC=/path/to/zentinel \
scripts/conformance.shproto/agent_v2.proto is a copy of the proxy's
crates/agent-protocol/proto/agent_v2.proto; the Haskell modules generated from
it are committed under gen/ so the build needs no protoc. Regenerate them
with scripts/gen-proto.sh after updating the copy.
Apache-2.0, see LICENSE.
Part of Zentinel, the security-first reverse proxy. The shisa guards the gate.