agentguard architecture
Goals
- Make every agent tool call an explicit authorization decision — no implicit “trust the agent”.
- Make every decision auditable — structured JSONL log of who, what, when, why.
- Make agent-to-agent delegation safe — scoped, time-boxed, signed tokens.
- Make policies first-class artifacts — Cedar, with schema validation, written by humans or generated by LLMs.
- Stay close to Cedar — agent-specific abstractions on top, not a replacement.
- Operate in production — observability, retry, drain, health checks, runbook.
Components
┌───────────────────────────────────────────────┐
│ agentguard-core (Rust) │
│ │
cedar-policy │ ┌─────────┐ ┌────────────┐ ┌──────────┐ │
(Rust) ◄─────────┼─►│ Request │ │ Authorizer │ │ Cache │ │
│ └─────────┘ │ (with cache)│ │(LRU+TTL) │ │
│ └────────────┘ └──────────┘ │
│ ┌───────────────────┐ ┌─────────────────┐ │
│ │ Delegation │ │ Simulator │ │
│ │ (Ed25519 JWS) │ │ (what-if) │ │
│ └───────────────────┘ └─────────────────┘ │
│ ┌──────────────────────────────────────────┐ │
│ │ DecisionLog (JSONL, hash-chained) │ │
│ └──────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘
▲ ▲ ▲
│ │ │
┌──────────────────────┘ │ └──────────────────────┐
│ │ │
┌───────┴────────┐ ┌───────┴────────┐
│ CLI (Rust) │ │ TypeScript SDK │
│ agentguard │ │ (subprocess) │
└────────────────┘ └────────────────┘
┌───────────────────────────────────────────────┐
│ agentguard-server │
│ │
│ AuthZEN HTTP + repository gRPC mirror │
│ - /access/v1/evaluation (single) │
│ - /access/v1/evaluations (batch ≤ 100) │
│ - /healthz, /readyz, /metrics │
│ - AccessEvaluation gRPC │
│ │
│ auth_layer (ApiKey | Disabled) │
│ policy watcher + SIGHUP reload │
│ graceful shutdown; supervisor bounds drain │
└───────────────────────────────────────────────┘
The server
The standalone run entry point reads supported tuning values from the
process environment and translates them into explicit runtime options. The
embedded build_state and build_router APIs do not read environment
variables: use build_state_with_options to supply cache and audit-rotation
configuration. This keeps library behavior deterministic and leaves process
configuration at the application boundary.
agentguard-server is an AuthZEN-compatible HTTP PDP with an optional
repository-defined gRPC mirror. It exposes:
POST /access/v1/evaluation— single decision (AuthZEN draft).POST /access/v1/evaluations— batch (cap 100 per call).GET /healthz//readyz— Kubernetes probes (healthz always 200; readyz requires policies loaded + audit log open).GET /metrics— Prometheus text snapshot.- gRPC:
agentguard.v1.AccessEvaluation(enable withAGENTGUARD_GRPC_LISTEN). The listener is plaintext and is not a standardized AuthZEN gRPC protocol.
All /access/v1/* endpoints are protected by auth_layer:
AuthConfig::Disabled— no auth, refused on a non-loopback listener unlessAGENTGUARD_ALLOW_LOOPBACK_BYPASS=1is set.AuthConfig::ApiKey { path }—Authorization: Bearer <raw>against a JSONApiKeyStore. Argon2id is the verification path.
A PolicyWatcher polls store_root every 500 ms, recursively observing Cedar
files under policies/ and schema.cedarschema; on each relevant event the
complete authorizer snapshots are atomically replaced and
policy_reload_total is bumped on success. Runtime watcher errors are logged.
The server also installs a
SIGHUP handler (Unix) so operators can force a refresh without touching the
filesystem; malformed edits retain the last-known-good snapshot.
On SIGTERM/SIGINT the server stops accepting new connections and lets
in-flight requests drain. The process supervisor must enforce the hard
termination deadline; the Kubernetes reference deployment uses a 30-second
terminationGracePeriodSeconds.
The request model
Every tool call is normalized into a Cedar (Principal, Action, Resource, Context) tuple:
- Principal is either a
User::"<id>"(human) orAgent::"<id>"(AI agent). Agents can have aparentlink for sub-agent chains. The AuthZENsubject.entity_typeis honored —"User"produces aUserprincipal,"Agent"produces anAgentprincipal, anything else returns 400. - Action is
Action::"ToolCall::<tool_name>"(optionally::"<operation>"). For example,Action::"ToolCall::s3::PutObject". - Resource is whatever the tool targets — a
Mailbox, aRepository, aDocument. You define your domain types in the schema. - Context carries tool args (flattened to top level for schema
validation) plus a
sessionrecord withip,user_agent,mfa,ts.
The decision log
The configured standalone server and CLI write DecisionRecord values to
.audit/decisions.jsonl. Embedded Authorizer calls return decisions without
writing audit automatically; the embedding application owns that wiring:
{
"id": "uuid",
"timestamp": "2026-07-14T12:34:56Z",
"effect": "allow" | "deny",
"policies": ["policy_id", ...],
"principal": "alice",
"action": "send_email",
"resource": "alice@acme",
"reasons": ["..."],
"request_id": "...",
"session_id": "..."
}
The log is append-only with one of two modes:
- Plain JSONL (no secret): fast, debuggable, but tamper-evident only against honest mistakes.
- Hash-chained (
AGENTGUARD_CHAIN_SECRET=<file>): each record carriesprev_hash+record_hash=HMAC-SHA256(root_key, payload);verify_chainconfirms no record was inserted, removed, or reordered. Verified offline withagentguard audit verify.
On startup the complete active log and its timestamped rotation segments are
verified as one continuous HMAC chain before new writes are accepted. This
also restores the chain head from the newest rotated segment if a crash
happened after rotation and before the first append to the new active file.
Malformed or tampered records refuse startup rather than silently extending
untrusted history. Each append serializes the in-process file write and
chain-head update under locks and calls write_all + sync_all before advancing
the in-memory head. This does not coordinate multiple server processes sharing
one audit file; the supported Kubernetes deployment keeps a single PDP
replica.
fsync is called on every append in both modes — a power loss
between flush and the kernel page-cache flush cannot lose records.
Delegation
When agent A calls agent B, B shouldn’t inherit A’s full identity.
agentguard mints scoped delegation tokens as standard JWS
(RFC 7515) using Ed25519:
{
"iss": "Agent::\"research\"",
"sub": "Agent::\"summarizer\"",
"aud": "agentguard",
"iat": 1700000000,
"exp": 1700000900,
"allowed_actions": ["ToolCall::send_email"],
"resource_patterns": ["Mailbox::*"]
}
Compact form: base64url(header).base64url(payload).base64url(sig).
The verifier (DelegationVerifier::verify) checks, in order:
- JWS parses cleanly (3 segments, valid base64url, valid JSON).
algisEdDSAandkidis registered. HS256 + Ed255SA combinations (algorithm-confusion attacks) are rejected explicitly.- Signature is valid against the registered key.
expis in the future (with a configurable clock skew, default 60 s).nbfis in the past (with the same clock skew).audmatches the configured audience.
Optional structured constraints (Cedar-style path expressions
evaluated against the request context) further narrow the token’s
applicability.
NL policy generation
agentguard gen "<natural language>" calls an LLM with:
- Your schema (
schema.cedarschema) as context - A constrained system prompt instructing Cedar-only output
- Your natural-language requirement
The output is validated against your schema before being written to disk. If the LLM produces invalid Cedar or policies that don’t type-check, the command fails and you refine the prompt.
The HTTP client has a 60 s request / 10 s connect timeout and retries up to 3 times on 5xx / connect / timeout failures with exponential backoff (250 ms / 500 ms / 1 s cap).
This forms a tight feedback loop — Cedar’s validator is the ground truth.
Schema as source of truth
The Cedar schema does two important jobs:
- Validates policies at authoring time. If a policy references an entity type that doesn’t exist, or uses a wrong attribute type, validation fails immediately — not at runtime.
- Validates context at request time. The action’s declared context shape
is enforced when the SDK builds a request. If
send_emailrequiressubject: String, sending a request without it fails fast.
Local-first
All state lives on disk:
.agentguard/schema.cedarschema— schema.agentguard/policies/*.cedar— policies (one per file).audit/decisions.jsonl— decision log (+.chainidsidecar)- (delegation tokens are passed at runtime, not stored)
This makes it trivial to:
- Version-control your policies (
git diff .agentguard/policies/) - Test policies locally (
agentguard sim req.json) - Review changes in PRs
- Migrate from local to hosted by syncing the same files
Embedded audit storage
Embedded applications can implement the public AuditAppender or
AsyncAuditAppender port and inject it with AppState::with_audit_appender
or AppState::with_async_audit_appender when audit persistence is managed
outside the local filesystem. Adapters must durably append before returning,
report health accurately, and report is_chained only when stored records
are tamper-evident. The request path fails closed on append errors and holds a
bounded per-state work permit until persistence completes; readiness requires
a healthy chained adapter. Async adapters do not occupy blocking workers
during remote I/O. Async appends have a five-second deadline and readiness
checks have a two-second deadline. A timed-out append fails closed; because a
remote commit can have an uncertain outcome, adapters should account for
unknown append outcomes and use idempotency facilities where available. The
standalone binary continues to use the file-backed DecisionLog; a distributed
audit backend is not included in the supported deployment package.
Configuration
All settings have sensible defaults; overrides come from CLI flags
or environment variables. The full table is in
docs/operations/runbook.md. Highlights:
AGENTGUARD_LISTEN— listen address (defaulttcp://127.0.0.1:8443)AGENTGUARD_STORE— policy directory (default.agentguard)AGENTGUARD_AUDIT— audit log path (default.audit/decisions.jsonl)AGENTGUARD_CHAIN_SECRET/--secret-file— HMAC chain secret (hex / base64 / raw bytes; auto-detected)AGENTGUARD_AUTH—disabledorapikey:<path>AGENTGUARD_AUTH_KEY_FILE— companion to--auth apikeyAGENTGUARD_GRPC_LISTEN— optional gRPC listen addressAGENTGUARD_ALLOW_LOOPBACK_BYPASS— escape hatch for embeddersAGENTGUARD_CACHE_TTL/AGENTGUARD_DENY_CACHE_TTL— allow/deny decision cache TTLs (humantime, defaults 60 s / 5 s)AGENTGUARD_CACHE_CAPACITY— decision cache size (default 10 000); malformed or non-positive configured values fail standalone startupAGENTGUARD_JWKS_REFRESH— JWKS refresh interval (humantime, default 30 s)
Operational concerns
See docs/operations/runbook.md for the
day-2 runbook and docs/adr/ for the architectural
decisions behind the design.