A comprehensive guide to installing, configuring, and operating the Predicate Authority sidecar daemon.
- Overview
- Installation
- Quick Start
- Commands
- Configuration
- Identity Provider Modes
- Policy Files
- API Reference
- Terminal Dashboard
- Desktop companion (GUI)
- Delegation Chains
- Security Features (Phase 5)
- Secret Injection
- Troubleshooting
- Demos
The Predicate Authority sidecar (predicate-authorityd) is a high-performance authorization daemon that enforces policy rules for AI agent actions. It provides:
- Policy-based authorization - Deterministic ALLOW/DENY decisions based on configurable rules
- Mandate signing - Short-lived cryptographic tokens (JWT ES256/HS256) for authorized actions
- Identity provider integration - Okta, Entra ID, OIDC, or local token issuance
- Delegation chains - Hierarchical mandate delegation with scope narrowing
- Audit logging - In-memory proof ledger for all authorization decisions
- Control plane sync - Optional enterprise features via cloud connection
┌─────────────┐ ┌─────────┐ ┌──────────────────────┐ ┌─────────┐
│ AI Agent │────▶│ IdP │────▶│ predicate-authority │────▶│ Backend │
│ │ │ (Okta) │ │ (Sidecar) │ │ API │
└─────────────┘ └─────────┘ └──────────────────────┘ └─────────┘
- Agent obtains access token from Identity Provider
- Agent requests authorization from sidecar with token
- Sidecar validates token, evaluates policy, issues mandate
- Agent uses mandate to call backend API
Download the latest release for your platform:
| Platform | Binary |
|---|---|
| Linux x64 | predicate-authorityd-linux-x64.tar.gz |
| Linux x64 (musl) | predicate-authorityd-linux-x64-musl.tar.gz |
| macOS x64 | predicate-authorityd-darwin-x64.tar.gz |
| macOS ARM64 | predicate-authorityd-darwin-arm64.tar.gz |
| Windows x64 | predicate-authorityd-windows-x64.zip |
# Extract and make executable
tar -xzf predicate-authorityd-*.tar.gz
chmod +x predicate-authorityd
# Verify installation
./predicate-authorityd versiongit clone https://github.com/PredicateSystems/predicate-authority-sidecar.git
cd predicate-authority-sidecar/rust-predicate-authorityd
cargo build --release
./target/release/predicate-authorityd version./predicate-authorityd init-config -o predicate-authorityd.tomlCreate policy.json:
{
"rules": [
{
"name": "allow-browser-https",
"effect": "allow",
"principals": ["agent:*"],
"actions": ["browser.*"],
"resources": ["https://*"]
},
{
"name": "deny-admin-actions",
"effect": "deny",
"principals": ["agent:*"],
"actions": ["admin.*"],
"resources": ["*"]
}
]
}./predicate-authorityd --policy-file policy.json run# This should be ALLOWED
curl -X POST http://127.0.0.1:8787/v1/authorize \
-H "Content-Type: application/json" \
-d '{
"principal": "agent:web",
"action": "browser.click",
"resource": "https://example.com"
}'
# Response: {"allowed":true,"reason":"allowed","missing_labels":[]}
# This should be DENIED
curl -X POST http://127.0.0.1:8787/v1/authorize \
-H "Content-Type: application/json" \
-d '{
"principal": "agent:web",
"action": "admin.delete",
"resource": "/users/123"
}'
# Response: {"allowed":false,"reason":"explicit_deny","missing_labels":[]}| Command | Description |
|---|---|
run |
Start the daemon (default) |
dashboard |
Start with interactive TUI dashboard |
init-config |
Generate example configuration file |
check-config |
Validate configuration file |
version |
Show version and build info |
# Run with default settings
./predicate-authorityd run
# Run with interactive dashboard
./predicate-authorityd dashboard
# Generate config file
./predicate-authorityd init-config -o config.toml
# Validate config
./predicate-authorityd check-config -c config.toml
# Show version
./predicate-authorityd versionConfiguration can be provided via (in order of precedence):
- CLI arguments (highest priority)
- Environment variables
- Configuration file (TOML)
- Default values
Important: CLI arguments must be placed before the subcommand.
# Correct
./predicate-authorityd --port 9000 --policy-file policy.json run
# Incorrect
./predicate-authorityd run --port 9000 # This won't work!| Option | Environment Variable | Default | Description |
|---|---|---|---|
--host |
PREDICATE_HOST |
127.0.0.1 |
Host to bind to |
--port |
PREDICATE_PORT |
8787 |
Port to bind to |
--mode |
PREDICATE_MODE |
local_only |
local_only or cloud_connected |
--policy-file |
PREDICATE_POLICY_FILE |
- | Path to policy file |
--log-level |
PREDICATE_LOG_LEVEL |
info |
trace/debug/info/warn/error |
--identity-mode |
PREDICATE_IDENTITY_MODE |
local |
Identity provider mode |
--enable-delegation |
PREDICATE_ENABLE_DELEGATION |
false |
Enable chain delegation (/v1/delegate endpoint) |
--max-delegation-depth |
PREDICATE_MAX_DELEGATION_DEPTH |
5 |
Maximum delegation chain depth |
| Option | Environment Variable | Default | Description |
|---|---|---|---|
--ssrf-allow |
PREDICATE_SSRF_ALLOW |
- | Comma-separated host:port pairs to whitelist |
--ssrf-disabled |
PREDICATE_SSRF_DISABLED |
false |
Disable SSRF protection entirely |
--require-signed-policy |
PREDICATE_REQUIRE_SIGNED_POLICY |
false |
Require Ed25519-signed policies |
--policy-signing-key |
PREDICATE_POLICY_SIGNING_KEY |
- | Ed25519 public key (hex) |
--loop-guard-threshold |
PREDICATE_LOOP_GUARD_THRESHOLD |
5 |
Failure count before blocking |
--loop-guard-window-s |
PREDICATE_LOOP_GUARD_WINDOW_S |
60 |
Time window for failures |
| Option | Environment Variable | Default | Description |
|---|---|---|---|
--policy-reload-secret |
PREDICATE_POLICY_RELOAD_SECRET |
- | Bearer token required for /policy/reload |
--disable-policy-reload |
PREDICATE_DISABLE_POLICY_RELOAD |
false |
Disable /policy/reload endpoint entirely |
The daemon searches for configuration files in these locations:
./predicate-authorityd.toml./config/predicate-authorityd.toml~/.predicate/authorityd.toml/etc/predicate/authorityd.toml
Example configuration:
[server]
host = "127.0.0.1"
port = 8787
mode = "local_only"
shutdown_timeout_s = 30
[policy]
file = "/path/to/policy.json"
hot_reload = false
# reload_secret = "your-secret-here" # Require bearer token for /policy/reload
# disable_reload = false # Set to true to disable /policy/reload entirely
# SSRF Protection Configuration
[ssrf]
# allowed_endpoints = ["172.30.192.1:11434", "127.0.0.1:9200"] # Bypass SSRF for these
# disabled = false # Set to true to disable all SSRF protection (not recommended)
[identity]
default_ttl_s = 900 # 15 minutes
queue_item_ttl_s = 86400 # 24 hours
[idp]
mode = "local"
idp_token_ttl_s = 300
mandate_ttl_s = 300
[logging]
level = "info"
format = "compact"The sidecar supports multiple identity provider modes for token validation.
No token validation required. Use for development and trusted environments.
./predicate-authorityd --identity-mode local --policy-file policy.json runAuthorization requests don't require an Authorization header.
The sidecar issues its own JWT tokens. Use for air-gapped environments, CI/CD, or ephemeral task isolation.
export LOCAL_IDP_SIGNING_KEY="your-secret-signing-key"
./predicate-authorityd \
--identity-mode local-idp \
--local-idp-issuer "http://localhost/predicate-local-idp" \
--local-idp-audience "api://predicate-authority" \
--policy-file policy.json \
runGet a task identity token:
curl -X POST http://127.0.0.1:8787/identity/task \
-H "Content-Type: application/json" \
-d '{
"principal_id": "agent:web",
"task_id": "task-123",
"ttl_seconds": 300
}'
# Response: {"token": "eyJ...", "expires_at": "2024-..."}Use the token for authorization:
curl -X POST http://127.0.0.1:8787/v1/authorize \
-H "Authorization: Bearer eyJ..." \
-H "Content-Type: application/json" \
-d '{
"principal": "agent:web",
"action": "browser.click",
"resource": "https://example.com"
}'Enterprise Okta integration with JWKS validation.
export OKTA_ISSUER="https://your-org.okta.com/oauth2/default"
export OKTA_CLIENT_ID="your-client-id"
export OKTA_AUDIENCE="api://predicate-authority"
./predicate-authorityd \
--identity-mode okta \
--okta-issuer "$OKTA_ISSUER" \
--okta-client-id "$OKTA_CLIENT_ID" \
--okta-audience "$OKTA_AUDIENCE" \
--okta-required-scopes "authority:check" \
--idp-token-ttl-s 300 \
--mandate-ttl-s 300 \
--policy-file policy.json \
runThe sidecar validates tokens by:
- Fetching JWKS from
${OKTA_ISSUER}/.well-known/jwks.json - Verifying JWT signature using Okta's public keys
- Checking issuer, audience, expiration, and required scopes
Microsoft Entra ID (Azure AD) integration.
./predicate-authorityd \
--identity-mode entra \
--entra-tenant-id "your-tenant-id" \
--entra-client-id "your-client-id" \
--entra-audience "api://predicate-authority" \
--policy-file policy.json \
runGeneric OIDC provider integration.
./predicate-authorityd \
--identity-mode oidc \
--oidc-issuer "https://your-oidc-provider/.well-known/openid-configuration" \
--oidc-client-id "your-client-id" \
--oidc-audience "api://predicate-authority" \
--policy-file policy.json \
runThe sidecar enforces idp-token-ttl-s >= mandate-ttl-s to prevent mandates from outliving identity sessions.
Policy files support JSON and YAML. Format is auto-detected by extension:
.json- JSON format.yamlor.yml- YAML format
{
"rules": [
{
"name": "rule-name",
"effect": "allow",
"principals": ["agent:*"],
"actions": ["browser.*"],
"resources": ["https://*"],
"required_labels": ["verified"]
}
]
}| Field | Required | Description |
|---|---|---|
name |
Yes | Unique rule identifier |
effect |
Yes | allow or deny |
principals |
Yes | List of principal patterns |
actions |
Yes | List of action patterns |
resources |
Yes | List of resource patterns |
required_labels |
No | Labels that must be present in request |
Patterns support shell-style wildcards:
| Pattern | Matches |
|---|---|
* |
Any string |
agent:* |
Any agent principal |
browser.* |
Any browser action |
https://* |
Any HTTPS URL |
browser.click |
Exact match only |
**/workspace/** |
Any path containing /workspace/ |
For file system actions (fs.read, fs.write, fs.list, etc.), the sidecar automatically normalizes resource paths before policy evaluation:
- Path traversal resolution:
../and./components are resolved - Home directory expansion:
~is expanded to the user's home directory - Redundant slashes removed:
//becomes/
Example:
Input: ./workspace/../../../etc/passwd
After: /etc/passwd
Important: Because paths are normalized to absolute paths, policy rules should use patterns that match absolute paths:
| Pattern Type | Example | Recommended |
|---|---|---|
| Relative paths | ./workspace/** |
No - won't match normalized paths |
| Absolute paths | /app/workspace/** |
Yes - matches specific location |
| Wildcard prefix | **/workspace/** |
Yes - matches workspace anywhere |
Recommended policy pattern for workspace access:
{
"name": "allow-workspace-reads",
"effect": "allow",
"principals": ["agent:*"],
"actions": ["fs.read"],
"resources": ["**/workspace/**"]
}This pattern matches /app/workspace/file.txt, /home/user/workspace/src/index.ts, etc.
- DENY rules first - Any matching DENY immediately blocks
- ALLOW rules - Must match AND have all required_labels
- Default DENY - If no rules match, action is blocked (fail-closed)
{
"rules": [
{
"name": "deny-admin-actions",
"effect": "deny",
"principals": ["agent:*"],
"actions": ["admin.*"],
"resources": ["*"]
},
{
"name": "allow-browser-https",
"effect": "allow",
"principals": ["agent:*"],
"actions": ["browser.*"],
"resources": ["https://*"]
},
{
"name": "allow-read-workspace",
"effect": "allow",
"principals": ["agent:*"],
"actions": ["fs.read"],
"resources": ["/workspace/*"]
},
{
"name": "require-approval-for-sensitive",
"effect": "allow",
"principals": ["agent:secure"],
"actions": ["sensitive.*"],
"resources": ["*"],
"required_labels": ["verified", "approved"]
}
]
}The policies/ directory contains ready-to-use templates:
| Policy | Use Case |
|---|---|
strict.json |
Production - workspace isolation, safe commands |
read-only.json |
Code review - read-only access |
strict-web-only.json |
Browser automation - no filesystem |
ci-cd.json |
CI/CD pipelines - build/test commands |
permissive.json |
Development - minimal restrictions |
POST /v1/authorize
Check if an action is authorized.
Single-scope request (backward compatible):
{
"principal": "agent:web",
"action": "browser.click",
"resource": "https://example.com",
"labels": {"verified": "true"},
"intent_hash": "optional-intent-hash"
}Multi-scope request (new in v0.7.0):
{
"principal": "agent:orchestrator",
"scopes": [
{"action": "browser.*", "resource": "https://amazon.com/*"},
{"action": "fs.*", "resource": "/workspace/**"}
],
"intent_hash": "orchestrate:ecommerce:run-123"
}Multi-scope authorization allows an orchestrator to request a single mandate covering multiple action/resource pairs. All scopes must be allowed for the request to succeed.
Response (allowed, single-scope):
{
"allowed": true,
"reason": "allowed",
"mandate_id": "m_7f3a2b1c",
"mandate_token": null,
"missing_labels": []
}Response (allowed, multi-scope with delegation enabled):
{
"allowed": true,
"reason": "allowed",
"mandate_id": "m_7f3a2b1c",
"mandate_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"missing_labels": [],
"scopes_authorized": [
{"action": "browser.*", "resource": "https://amazon.com/*", "matched_rule": "allow-browser"},
{"action": "fs.*", "resource": "/workspace/**", "matched_rule": "allow-fs"}
]
}Response (denied):
{
"allowed": false,
"reason": "explicit_deny (action: network.connect, resource: tcp://internal:3306)",
"missing_labels": []
}Denial reasons:
explicit_deny- Policy explicitly denies the actionno_matching_policy- No rule matches (fail-closed)missing_labels- Required labels not providedmandate_revoked- Mandate was revokedmandate_expired- Mandate TTL exceeded
GET /health
{"status": "ok"}GET /status
{
"version": "0.4.1",
"mode": "local_only",
"identity_mode": "local",
"rule_count": 12,
"uptime_s": 3600,
"total_requests": 1500,
"total_allowed": 1450,
"total_denied": 50
}GET /metrics
Returns Prometheus-format metrics.
POST /policy/reload
Hot-reload policy rules from file.
curl -X POST http://127.0.0.1:8787/policy/reloadBy default, /policy/reload is unauthenticated. For production deployments, you should either:
Option 1: Require a bearer token
# Start with reload secret
./predicate-authorityd \
--policy-reload-secret "your-secret-token" \
--policy-file policy.json \
run
# Reload requires Authorization header
curl -X POST http://127.0.0.1:8787/policy/reload \
-H "Authorization: Bearer your-secret-token"Or via environment variable:
export PREDICATE_POLICY_RELOAD_SECRET="your-secret-token"
./predicate-authorityd --policy-file policy.json runOption 2: Disable the endpoint entirely
./predicate-authorityd \
--disable-policy-reload \
--policy-file policy.json \
runWhen disabled, the endpoint returns 404. Policy changes require a sidecar restart.
Or via environment variable:
export PREDICATE_DISABLE_POLICY_RELOAD=true
./predicate-authorityd --policy-file policy.json runSecurity note: In shared-host or container environments, an unauthenticated reload endpoint allows any local process to replace the policy. Configure one of the above options to mitigate this risk.
POST /identity/task (local-idp mode)
Issue a task identity token.
{
"principal_id": "agent:web",
"task_id": "task-123",
"ttl_seconds": 300
}POST /identity/revoke
Revoke an identity.
GET /identity/list
List active identities.
POST /v1/delegate
Delegate a mandate to another agent with narrower scope.
{
"parent_mandate_token": "eyJ...",
"delegate_to": "agent:sub-agent",
"action_scope": "browser.click",
"resource_scope": "https://example.com/*",
"ttl_seconds": 60
}POST /revoke/mandate
Revoke a mandate and all derived mandates.
{
"mandate_id": "m_7f3a2b1c",
"reason": "security concern"
}The sidecar includes an interactive TUI dashboard for real-time monitoring.
./predicate-authorityd --policy-file policy.json dashboardOr set refresh rate:
export PREDICATE_TUI_REFRESH_MS=50
./predicate-authorityd --policy-file policy.json dashboard┌────────────────────────────────────────────────────────────────────────────┐
│ PREDICATE AUTHORITY v0.4.1 MODE: strict [LIVE] UPTIME: 2h 34m [?] │
│ Policy: loaded Rules: 12 active [Q:quit P:pause] │
├─────────────────────────────────────────┬──────────────────────────────────┤
│ LIVE AUTHORITY GATE [1/47] │ METRICS │
│ │ │
│ [ ✓ ALLOW ] agent:web │ Total Requests: 1,870 │
│ browser.navigate → github.com │ ├─ Allowed: 1,847 (98.8%)│
│ m_7f3a2b1c | 0.4ms │ └─ Blocked: 23 (1.2%)│
│ │ │
│ [ ✗ DENY ] agent:scraper │ Throughput: 12.3 req/s │
│ fs.write → ~/.ssh/config │ Avg Latency: 0.8ms │
│ EXPLICIT_DENY | 0.2ms │ │
│ │ TOKEN CONTEXT SAVED │
│ [ ✓ ALLOW ] agent:worker │ Blocked early: 23 actions │
│ browser.click → button#checkout │ Est. tokens saved: ~4,200 │
│ m_9c2d4e5f | 0.6ms │ │
├─────────────────────────────────────────┴──────────────────────────────────┤
│ Generated 47 proofs this session. Run `predicate login` to sync to vault.│
└────────────────────────────────────────────────────────────────────────────┘
| Key | Action |
|---|---|
Q / Esc |
Quit dashboard |
j / ↓ |
Scroll down event list |
k / ↑ |
Scroll up event list |
g |
Jump to newest event |
G |
Jump to oldest event |
P |
Pause/resume live updates |
? |
Toggle help overlay |
f |
Cycle filter: ALL → DENY → agent input |
/ |
Filter by agent ID (type + Enter) |
c |
Clear filter (show all) |
When an agent is running a heavy web loop, the LIVE AUTHORITY GATE can be spammed with hundreds of [ ✓ ALLOW ] events. Use filtering to focus on what matters:
Filter to DENY only:
Press f once to show only blocked events.
Filter by agent:
Press f twice (or /) to enter agent filter mode. Type an agent ID (e.g., web) and press Enter. Events are filtered by partial match on the principal.
Clear filter:
Press c to return to showing all events.
The current filter is displayed in the header and title:
PREDICATE AUTHORITY v0.4.1 MODE: strict [LIVE] FILTER: DENY
When running with the audit-only policy (or --audit-mode flag), the dashboard shows a visual distinction for blocked events:
# Enable audit mode explicitly
./predicate-authorityd --audit-mode --policy-file policy.json dashboard
# Or use audit-only policy (auto-detected from filename)
./predicate-authorityd --policy-file policies/audit-only.json dashboardIn audit mode:
- The header shows
[AUDIT]instead of[LIVE] - Blocked events display
[ ⚠ WOULD DENY ]in yellow instead of[ ✗ DENY ]in red - The LIVE AUTHORITY GATE border turns yellow
This makes it visually obvious that the sidecar is logging decisions but not actually blocking the agent.
When you quit the dashboard, a session summary is printed:
────────────────────────────────────────────────────────
PREDICATE AUTHORITY SESSION SUMMARY
────────────────────────────────────────────────────────
Duration: 2h 34m 12s
Total Requests: 1,870
├─ Allowed: 1,847 (98.8%)
└─ Blocked: 23 (1.2%)
Proofs Generated: 1,870
Est. Tokens Saved: ~4,140
To sync proofs to enterprise vault, run:
$ predicate login
────────────────────────────────────────────────────────
The repository includes an optional desktop application (predicate-authority-desktop) built with egui. It is a local companion: it can start and stop the sidecar process, tail stdout/stderr, poll /health and /status, edit and validate policy (same policy_loader as the daemon), reload policy over HTTP, and manage paths and launch flags. It does not replace the embedded Web UI for the per-request ALLOW/DENY event stream; use the browser dashboard when Web UI is enabled for that view.
For a full feature list, see predicate-authority-desktop/README.md.
- Rust (stable) and Cargo
- Same clone as the sidecar: the desktop crate lives in the workspace root next to
predicate-authorityd(see rootCargo.tomlmembers).
From the rust-predicate-authorityd directory (workspace root):
cargo build -p predicate-authority-desktop --releaseThe binary is written to:
- Linux / macOS:
target/release/predicate-authority-desktop - Windows:
target\release\predicate-authority-desktop.exe
Debug build (faster compile):
cargo build -p predicate-authority-desktopFrom the same workspace root:
cargo run -p predicate-authority-desktopOr run the release binary directly:
./target/release/predicate-authority-desktop- Open the Config tab.
- Set Sidecar binary to your
predicate-authoritydexecutable (e.g.target/release/predicate-authoritydaftercargo build --releaseon the main package). - Set Policy file to your JSON or YAML policy path.
- Optionally set Config TOML, Host / Port, Web UI, Audit mode, and Policy reload secret (must match
--policy-reload-secreton the daemon if you use reload). - Use Home → Start sidecar. When Web UI is enabled, use Open dashboard in browser after the log line shows the URL.
If predicate-authorityd is in the same directory as the desktop binary, the app can offer Use as binary for quick setup.
Delegation allows agents to pass limited permissions to sub-agents.
Chain delegation must be explicitly enabled when starting the sidecar:
./predicate-authorityd \
--enable-delegation \
--max-delegation-depth 5 \
--policy-file policy.json \
runOr via environment variables:
export PREDICATE_ENABLE_DELEGATION=true
export PREDICATE_MAX_DELEGATION_DEPTH=5
./predicate-authorityd --policy-file policy.json runWhen delegation is enabled:
- The
/v1/delegateendpoint becomes available - The
/v1/authorizeresponse includesmandate_token(a signed JWT) for allowed requests - The
mandate_tokencan be passed to/v1/delegateasparent_mandate_token
- Agent A requests authorization via
/v1/authorizeand receives amandate_token - Agent A delegates to Agent B via
/v1/delegatewith narrower scope:browser.clickonhttps://example.com/* - Agent B receives its own
mandate_tokenand can only act within the delegated scope - If Agent A's mandate is revoked, Agent B's mandate is automatically revoked (cascade)
curl -X POST http://127.0.0.1:8787/v1/delegate \
-H "Content-Type: application/json" \
-d '{
"parent_mandate_token": "eyJ...",
"delegate_to": "agent:sub-agent",
"action_scope": "browser.click",
"resource_scope": "https://example.com/*",
"ttl_seconds": 60
}'Response:
{
"mandate_id": "m_derived_123",
"mandate_token": "eyJ...",
"delegation_depth": 1,
"expires_at": "2024-..."
}- Child scope must be a subset of parent scope
browser.*can delegate tobrowser.click(narrower)browser.clickcannot delegate tobrowser.*(broader - rejected)https://*can delegate tohttps://example.com/*
Multi-scope mandates allow an orchestrator to hold a single mandate covering multiple action/resource pairs. When delegating from a multi-scope parent, the child scope must be a subset of at least one parent scope (OR semantics):
Parent scopes: [browser.*, fs.*]
Child: browser.click → ALLOWED (matches browser.*)
Child: fs.write → ALLOWED (matches fs.*)
Child: network.* → DENIED (matches none)
Example: Multi-scope orchestrator
# Orchestrator requests single multi-scope mandate
response = await client.authorize(
principal="agent:orchestrator",
scopes=[
{"action": "browser.*", "resource": "https://amazon.com/*"},
{"action": "fs.*", "resource": "/workspace/**"},
],
intent_hash="orchestrate:ecommerce:run-123",
)
# Single mandate for all delegations
orchestrator_token = response.mandate_token
# Delegate browser scope to scraper
scraper_response = await client.delegate(
parent_mandate_token=orchestrator_token,
delegate_to="agent:scraper",
action_scope="browser.navigate",
resource_scope="https://amazon.com/products",
)
# Delegate fs scope to analyst (same parent token!)
analyst_response = await client.delegate(
parent_mandate_token=orchestrator_token,
delegate_to="agent:analyst",
action_scope="fs.write",
resource_scope="/workspace/data/analysis.json",
)Benefits of multi-scope mandates:
- Unified audit trail - One mandate ID for entire orchestration
- Cascade revocation - Revoking orchestrator revokes all child delegations
- Simpler code - No need to track which mandate to use for which delegation
Delegation depth is configurable (default: 5 levels). Attempting to exceed the limit returns an error.
When a mandate is revoked, all derived mandates are automatically revoked:
Agent A (root)
└── Agent B (depth 1)
└── Agent C (depth 2)
└── Agent D (depth 3)
Revoke Agent B's mandate → Agent C and D are also revoked
Agent A's mandate remains valid
The sidecar includes built-in security hardening features for enterprise deployments.
Server-Side Request Forgery (SSRF) protection blocks requests to internal network resources:
Protected resources:
- Private IPs: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
- Link-local: 169.254.0.0/16
- Localhost: 127.0.0.0/8, localhost, ::1
- Cloud Metadata: AWS (169.254.169.254), GCP, Azure, Kubernetes
- Internal DNS: *.internal, *.local, *.localhost, *.corp, *.lan, *.intranet
SSRF protection is enabled by default. Blocked requests return:
{
"allowed": false,
"reason": "explicit_deny",
"violated_rule": "ssrf-protection: SSRF: cloud metadata endpoint blocked (169.254.169.254)"
}To allow specific local endpoints (e.g., local LLM instances, databases), you have four options:
Option 1: CLI flag (highest precedence)
# Allow local Ollama (WSL2) and Elasticsearch
./predicate-authorityd \
--ssrf-allow 172.30.192.1:11434,127.0.0.1:9200 \
--policy-file policy.json \
runOption 2: Environment variable
export PREDICATE_SSRF_ALLOW="172.30.192.1:11434,127.0.0.1:9200"
./predicate-authorityd --policy-file policy.json runOption 3: TOML configuration file
[ssrf]
allowed_endpoints = ["172.30.192.1:11434", "127.0.0.1:9200"]Option 4: Policy file (policy-driven, recommended for tenant-scoped deployments)
Add an ssrf_whitelist field to your policy JSON/YAML file:
{
"ssrf_whitelist": ["172.30.192.1:11434", "127.0.0.1:9200"],
"rules": [
...
]
}Or in YAML:
ssrf_whitelist:
- "172.30.192.1:11434" # Local Ollama on WSL2
- "127.0.0.1:9200" # Local Elasticsearch
rules:
- name: allow-llm-calls
effect: allow
...Precedence and merging:
- CLI and environment variables take highest precedence
- Entries from all sources are merged (deduplicated)
- If no whitelist is configured anywhere, full SSRF enforcement applies
Important: The whitelist uses exact host:port matching to limit the exemption surface. Only the specified port is allowed.
To disable entirely (development only, not recommended for production):
./predicate-authorityd --ssrf-disabled --policy-file policy.json runOr via environment variable:
export PREDICATE_SSRF_DISABLED=true
./predicate-authorityd --policy-file policy.json runOr in the configuration file:
[ssrf]
disabled = truePrevent unauthorized policy modifications with Ed25519 cryptographic signatures.
Signed policy file format:
{
"policy": {
"rules": [...]
},
"signature": "<base64-encoded-ed25519-signature>"
}Enable signature verification:
./predicate-authorityd \
--require-signed-policy \
--policy-signing-key "a1b2c3d4e5f6..." \
--policy-file signed-policy.json \
runThe sidecar will:
- Parse the signed policy JSON
- Verify the signature against the configured public key
- Reject policies with invalid or missing signatures
Key management:
- Private key: Keep secure on control plane or CI/CD system
- Public key: Distribute to sidecars via
--policy-signing-keyor environment variable
Prevents runaway agents from infinitely retrying failed actions.
Configuration:
./predicate-authorityd \
--loop-guard-threshold 5 \
--loop-guard-window-s 60 \
--policy-file policy.json \
runBehavior:
- Tracks failures per (principal, action, resource) tuple
- After threshold consecutive failures within the window, requests are blocked
- Returns
LOOP_GUARD_TRIGGEREDreason - Automatically resets after cooldown period
The proof ledger uses SHA-256 hash chaining for tamper-evident audit trails.
Each proof event contains:
event_id: Unique identifierchain_hash: SHA-256(previous_hash + event_data)timestamp: ISO 8601 timestamp
Verify chain integrity:
# Get current chain head (latest hash)
curl -s http://127.0.0.1:8787/ledger/chain-head | jq
# Response: {"chain_hash": "abc123...", "event_count": 47}
# Verify full chain integrity
curl -s http://127.0.0.1:8787/ledger/verify | jq
# Response: {"valid": true, "event_count": 47}Sensitive key material (signing keys, OIDC secrets) is automatically zeroized from memory when no longer needed using the zeroize crate. This protects against memory-dump attacks.
Cause: No policy file loaded or rules don't match.
Fix:
- Verify policy file exists:
--policy-file policy.json - Check rule patterns match your request
- Run with
--log-level debugto see rule evaluation
Cause: Sidecar not running or bound to different address.
Fix:
- Check if process is running:
ps aux | grep predicate - Check bound address: default is
127.0.0.1:8787 - For external access, use
--host 0.0.0.0
Cause: Token validation failed in IdP mode.
Fix:
- Check token is not expired
- Verify issuer/audience match configuration
- Check JWKS endpoint is reachable
- Run with
--log-level debugto see validation details
Cause: No authorization requests have been made yet.
Fix: Make a test request:
curl -X POST http://127.0.0.1:8787/v1/authorize \
-H "Content-Type: application/json" \
-d '{"principal":"agent:test","action":"test","resource":"test"}'Enable verbose logging to diagnose issues:
./predicate-authorityd --log-level debug --policy-file policy.json runOr set via environment:
export PREDICATE_LOG_LEVEL=debug
./predicate-authorityd runVerify the sidecar is running:
curl http://127.0.0.1:8787/health
# Response: {"status":"ok"}
curl http://127.0.0.1:8787/status
# Response: detailed status with version, mode, statsThe sidecar supports policy-driven secret injection for http.fetch and cli.exec actions. Secrets are injected at execution time, ensuring agents never see raw credentials.
- Store secrets as environment variables on the machine running the sidecar
- Reference them in policy rules using
${VAR_NAME}syntax - When an action matches a rule with injection config, the sidecar substitutes values at runtime
- The agent receives only the action result—never the secret values
┌─────────┐ authorize ┌──────────────┐ execute ┌─────────┐
│ Agent │ ─────────────────▶│ Sidecar │ ────────────────▶│ Backend │
│ │ (no secrets) │ inject: $KEY │ (with secrets) │ API │
└─────────┘ └──────────────┘ └─────────┘
| Syntax | Description |
|---|---|
${VAR_NAME} |
Substitute with value of VAR_NAME (error if not set) |
${VAR_NAME:-default} |
Substitute with value or use default if not set |
Use inject_headers to add authentication headers to http.fetch actions:
rules:
- name: api-with-auth
effect: allow
principals: ["agent:*"]
actions: ["http.fetch"]
resources: ["https://api.example.com/*"]
inject_headers:
Authorization: "Bearer ${API_TOKEN}"
X-Api-Key: "${API_KEY}"JSON equivalent:
{
"rules": [
{
"name": "api-with-auth",
"effect": "allow",
"principals": ["agent:*"],
"actions": ["http.fetch"],
"resources": ["https://api.example.com/*"],
"inject_headers": {
"Authorization": "Bearer ${API_TOKEN}",
"X-Api-Key": "${API_KEY}"
}
}
]
}Use inject_env to pass secrets to cli.exec actions:
rules:
- name: deploy-with-credentials
effect: allow
principals: ["agent:deployer"]
actions: ["cli.exec"]
resources: ["deploy.sh", "kubectl"]
inject_env:
AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}"
AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}"
KUBECONFIG: "${KUBECONFIG:-/etc/kubernetes/admin.conf}"For large secrets like certificates, private keys, or multi-line content, use inject_headers_from_file and inject_env_from_file to read values from files instead of environment variables:
rules:
# Inject certificate from file
- name: mtls-api
effect: allow
principals: ["agent:*"]
actions: ["http.fetch"]
resources: ["https://secure-api.example.com/*"]
inject_headers:
Authorization: "Bearer ${API_TOKEN}"
inject_headers_from_file:
X-Client-Cert: "/etc/certs/client.pem"
# Inject SSH key from file for CLI commands
- name: git-operations
effect: allow
principals: ["agent:deployer"]
actions: ["cli.exec"]
resources: ["git", "ssh"]
inject_env_from_file:
SSH_PRIVATE_KEY: "${HOME}/.ssh/deploy_key"Key behaviors:
- File paths support environment variable substitution (e.g.,
${HOME}/.ssh/key) - File contents are trimmed of trailing whitespace/newlines
- File-based secrets take precedence over env-var-based secrets for the same key
- Missing files cause execution to fail (fail-closed)
Policy file (policy-with-secrets.yaml):
rules:
# Allow API calls with injected auth header
- name: github-api
effect: allow
principals: ["agent:*"]
actions: ["http.fetch"]
resources: ["https://api.github.com/*"]
inject_headers:
Authorization: "Bearer ${GITHUB_TOKEN}"
Accept: "application/vnd.github.v3+json"
# Allow database CLI with credentials
- name: database-cli
effect: allow
principals: ["agent:dba"]
actions: ["cli.exec"]
resources: ["psql", "pg_dump"]
inject_env:
PGPASSWORD: "${DB_PASSWORD}"
PGHOST: "${DB_HOST:-localhost}"
PGUSER: "${DB_USER:-postgres}"
# Allow cloud CLI operations
- name: aws-cli
effect: allow
principals: ["agent:ops"]
actions: ["cli.exec"]
resources: ["aws"]
inject_env:
AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}"
AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}"
AWS_DEFAULT_REGION: "${AWS_REGION:-us-east-1}"
# mTLS with certificate from file
- name: secure-api
effect: allow
principals: ["agent:secure"]
actions: ["http.fetch"]
resources: ["https://internal-api.example.com/*"]
inject_headers_from_file:
X-Client-Certificate: "/etc/certs/client.pem"Starting the sidecar:
# Set secrets as environment variables
export GITHUB_TOKEN="ghp_xxxxxxxxxxxx"
export DB_PASSWORD="secure-password"
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
# Start sidecar
./predicate-authorityd --policy-file policy-with-secrets.yaml runAgent making a request:
# Agent code - no secrets visible
response = await authority_client.execute(
action="http.fetch",
resource="https://api.github.com/user/repos",
parameters={"method": "GET"}
)
# The sidecar injected the Authorization header automatically- Zero-trust execution: Agents never see or handle raw secrets
- Policy-driven: Security team controls which secrets are injected where
- Audit trail: All injections are logged with the action (values redacted)
- No agent changes: Existing agents work without modification
- Defense in depth: Even compromised agents cannot exfiltrate secrets
- Use specific resource patterns: Don't inject secrets into broad
*patterns - Prefer defaults for non-sensitive values: Use
${VAR:-default}for regions, hosts, etc. - Rotate secrets regularly: The sidecar reads env vars at substitution time
- Monitor for missing vars: The sidecar logs warnings when referenced vars are not set
- Test policies in audit mode: Verify injection works before enabling enforcement
The sidecar logs a warning when a referenced variable is not set:
WARN: Environment variable 'API_TOKEN' not set, using empty string
Fix: Ensure all referenced variables are exported before starting the sidecar.
Cause: Action or resource doesn't match the rule pattern.
Fix: Run with --log-level debug to see which rules are evaluated and matched.
Cause: Syntax error in default value format.
Fix: Ensure correct syntax: ${VAR:-default} (note the :- separator).
- how-it-works.md - Architectural overview of IdP + Sidecar + Mandates
- policies/README.md - Policy template documentation
- DESIGN.md - Internal design documentation
See the sidecar in action—securing AI agents across popular frameworks.
- Zero-Trust File Processor Agent Demo
- SecureClaw Integration Demo
- Amazon Kiro Reenactment Demo
- Zero-Trust AI Agent Demo
- GitHub Issues: https://github.com/PredicateSystems/predicate-authority-sidecar/issues
- Documentation: https://docs.predicatesystems.dev