Docs/Authority/Sidecar and Operations

Sidecar and Operations

predicate-authorityd runs as a local sidecar for policy reload, revocation, identity task issuance, queue flushing, and health/status checks.

For a detailed walkthrough of how the sidecar integrates with Identity Providers (Okta, Entra, OIDC) and Local IDP mode, see How the Sidecar Works.

For production enterprise deployments requiring cryptographic policy signing, see Enterprise Hardening: Policy Signing.


Installing the Sidecar

The predicate-authorityd sidecar is a Rust binary. Choose your preferred installation method:

Python:

# Use quotes for zsh compatibility
pip install "predicate-authority[sidecar]"

# IMPORTANT: The binary is NOT downloaded automatically during pip install.
# You must manually download it:
predicate-download-sidecar

TypeScript/Node.js:

# Binary is automatically included for your platform
npm install @predicatesystems/authorityd

Option B: Download Binary Directly

Download the binary for your platform from GitHub Releases:

PlatformBinary
macOS (Apple Silicon)predicate-authorityd-darwin-arm64.tar.gz
macOS (Intel)predicate-authorityd-darwin-x64.tar.gz
Linux (x64)predicate-authorityd-linux-x64.tar.gz
Linux (ARM64)predicate-authorityd-linux-arm64.tar.gz
Windows (x64)predicate-authorityd-windows-x64.zip
# Example for macOS Apple Silicon
curl -LO https://github.com/PredicateSystems/predicate-authority-sidecar/releases/latest/download/predicate-authorityd-darwin-arm64.tar.gz
tar -xzf predicate-authorityd-darwin-arm64.tar.gz
chmod +x predicate-authorityd

Option C: Use the Download Helper

If you have the Python SDK installed:

predicate-download-sidecar
# Or with a specific version:
predicate-download-sidecar --version v0.3.8

Binary location after download:

  • macOS: ~/Library/Application Support/predicate-authority/bin/predicate-authorityd
  • Linux: ~/.local/share/predicate-authority/bin/predicate-authorityd
  • Windows: %LOCALAPPDATA%/predicate-authority/bin/predicate-authorityd.exe

Sidecar CLI Reference

IMPORTANT: CLI arguments must be placed before the run subcommand.

GLOBAL OPTIONS (use before 'run'):
  -c, --config <FILE>           Path to TOML config file [env: PREDICATE_CONFIG]
      --host <HOST>             Host to bind to [env: PREDICATE_HOST] [default: 127.0.0.1]
      --port <PORT>             Port to bind to [env: PREDICATE_PORT] [default: 8787]
      --mode <MODE>             local_only or cloud_connected [env: PREDICATE_MODE]
      --policy-file <PATH>      Path to policy JSON [env: PREDICATE_POLICY_FILE]
      --identity-file <PATH>    Path to local identity registry [env: PREDICATE_IDENTITY_FILE]
      --log-level <LEVEL>       trace, debug, info, warn, error [env: PREDICATE_LOG_LEVEL]
      --control-plane-url <URL> Control-plane URL [env: PREDICATE_CONTROL_PLANE_URL]
      --tenant-id <ID>          Tenant ID [env: PREDICATE_TENANT_ID]
      --project-id <ID>         Project ID [env: PREDICATE_PROJECT_ID]
      --predicate-api-key <KEY> API key [env: PREDICATE_API_KEY]
      --sync-enabled            Enable control-plane sync [env: PREDICATE_SYNC_ENABLED]
      --fail-open               Fail open if control-plane unreachable [env: PREDICATE_FAIL_OPEN]

IDENTITY PROVIDER OPTIONS:
      --identity-mode <MODE>    local, local-idp, oidc, entra, or okta [env: PREDICATE_IDENTITY_MODE]
      --allow-local-fallback    Allow local/local-idp in cloud_connected mode
      --idp-token-ttl-s <SECS>  IdP token TTL seconds [default: 300]
      --mandate-ttl-s <SECS>    Mandate TTL seconds [default: 300]

LOCAL IDP OPTIONS (for identity-mode=local-idp):
      --local-idp-issuer <URL>  Issuer URL [env: LOCAL_IDP_ISSUER]
      --local-idp-audience <AUD> Audience [env: LOCAL_IDP_AUDIENCE]
      --local-idp-signing-key-env <VAR> Env var for signing key [default: LOCAL_IDP_SIGNING_KEY]

CHAIN DELEGATION OPTIONS:
      --enable-delegation       Enable chain delegation (/v1/delegate endpoint) [env: PREDICATE_ENABLE_DELEGATION]
      --max-delegation-depth <N> Maximum delegation chain depth [env: PREDICATE_MAX_DELEGATION_DEPTH] [default: 5]

SECURITY OPTIONS:
      --ssrf-allow <ENDPOINTS>  Comma-separated host:port pairs to whitelist [env: PREDICATE_SSRF_ALLOW]
      --ssrf-disabled           Disable SSRF protection entirely [env: PREDICATE_SSRF_DISABLED]
      --policy-reload-secret <SECRET> Bearer token required for /policy/reload [env: PREDICATE_POLICY_RELOAD_SECRET]
      --disable-policy-reload   Disable /policy/reload endpoint entirely [env: PREDICATE_DISABLE_POLICY_RELOAD]

OIDC OPTIONS (for identity-mode=oidc):
      --oidc-issuer <URL>       Issuer URL [env: OIDC_ISSUER]
      --oidc-client-id <ID>     Client ID [env: OIDC_CLIENT_ID]
      --oidc-audience <AUD>     Audience [env: OIDC_AUDIENCE]

ENTRA OPTIONS (for identity-mode=entra):
      --entra-tenant-id <ID>    Tenant ID [env: ENTRA_TENANT_ID]
      --entra-client-id <ID>    Client ID [env: ENTRA_CLIENT_ID]
      --entra-audience <AUD>    Audience [env: ENTRA_AUDIENCE]

OKTA OPTIONS (for identity-mode=okta):
      --okta-issuer <URL>       Okta issuer URL [env: OKTA_ISSUER]
      --okta-client-id <ID>     Okta client ID [env: OKTA_CLIENT_ID]
      --okta-audience <AUD>     Okta audience [env: OKTA_AUDIENCE]
      --okta-required-claims    Required claims (comma-separated)
      --okta-required-scopes    Required scopes (comma-separated)
      --okta-required-roles     Required roles/groups (comma-separated)
      --okta-allowed-tenants    Allowed tenant IDs (comma-separated)

COMMANDS:
  run          Start the daemon (default)
  dashboard    Start with interactive TUI
  init-config  Generate example config file
  check-config Validate config file
  version      Show version info

Terminal Dashboard

The sidecar includes an interactive terminal dashboard for real-time monitoring of authorization decisions:

./predicate-authorityd --policy-file policy.json dashboard

Dashboard Layout

┌────────────────────────────────────────────────────────────────────────────┐
│  PREDICATE AUTHORITY v0.5.0    MODE: strict  [LIVE]  UPTIME: 2h 34m  [?]  │
│  Policy: loaded                Rules: 12 active      [Q:quit P:pause]     │
├─────────────────────────────────────────┬──────────────────────────────────┤
│  LIVE AUTHORITY GATE                    │  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,140       │
│    m_9c2d4e5f | 0.6ms                   │                                  │
├─────────────────────────────────────────┴──────────────────────────────────┤
│  Generated 47 proofs this session. Run `predicate login` to sync to vault.│
└────────────────────────────────────────────────────────────────────────────┘

Dashboard Demo: Real-time ALLOW/DENY decisions as authorization requests flow through the sidecar.

Keyboard Shortcuts

KeyAction
j/k or ↑/↓Scroll through events
fCycle filter: ALL → DENY → agent input
/Enter agent filter mode directly
cClear filter (show all events)
PPause/resume live updates
?Toggle help overlay
Q or EscQuit dashboard

Live Filtering

Filter authorization events in real-time to focus on specific patterns:

Filter by Denied Events Only

Press f once to show only denied events:

┌─ LIVE AUTHORITY GATE ──────────────────────────────────────────────────────┐
│  [FILTER: DENY ONLY]                                                       │
│                                                                            │
│  [ ✗ DENY  ] agent:scraper                                                 │
│    fs.write → ~/.ssh/config                                                │
│    EXPLICIT_DENY | 0.2ms                                                   │
│                                                                            │
│  [ ✗ DENY  ] agent:web                                                     │
│    admin.delete → /users/123                                               │
│    NO_MATCHING_ALLOW | 0.3ms                                               │
└────────────────────────────────────────────────────────────────────────────┘

Filter by Agent ID

Press f again (or / directly) to enter agent filter mode, then type the agent ID:

┌─ LIVE AUTHORITY GATE ──────────────────────────────────────────────────────┐
│  [FILTER: agent:web]                                                       │
│                                                                            │
│  [ ✓ ALLOW ] agent:web                                                     │
│    browser.navigate → github.com                                           │
│    m_7f3a2b1c | 0.4ms                                                      │
│                                                                            │
│  [ ✓ ALLOW ] agent:web                                                     │
│    browser.click → button#submit                                           │
│    m_8e4b3c2d | 0.5ms                                                      │
├────────────────────────────────────────────────────────────────────────────┤
│  Filter agent: web█                                          [Enter/Esc]   │
└────────────────────────────────────────────────────────────────────────────┘

Filter supports partial matching—typing web matches agent:web, agent:web-scraper, etc.


Audit Mode (Dry-Run)

Audit mode allows you to test policy changes without blocking actions. Denied requests are logged but allowed through, displayed as [ ⚠ WOULD DENY ] in yellow.

Enable Audit Mode

Option 1: CLI flag

./predicate-authorityd --policy-file policy.json --audit-mode dashboard

Option 2: Auto-detection from policy filename

Policy files containing audit, dry-run, or dryrun in the filename automatically enable audit mode:

# These all auto-enable audit mode:
./predicate-authorityd --policy-file policy-audit.json dashboard
./predicate-authorityd --policy-file dry-run-policy.json dashboard
./predicate-authorityd --policy-file new-rules-dryrun.json dashboard

Audit Mode Display

When audit mode is active:

  • Header shows [AUDIT] status indicator
  • Denied events display as [ ⚠ WOULD DENY ] in yellow instead of [ ✗ DENY ] in red
  • Border color changes to yellow
┌────────────────────────────────────────────────────────────────────────────┐
│  PREDICATE AUTHORITY v0.5.0    MODE: strict  [AUDIT]  UPTIME: 15m    [?]  │
│  Policy: policy-audit.json     Rules: 8 active       [Q:quit P:pause]     │
├─────────────────────────────────────────┬──────────────────────────────────┤
│  LIVE AUTHORITY GATE                    │  METRICS                         │
│                                         │                                  │
│  [ ✓ ALLOW ] agent:web                  │  Total Requests:      127        │
│    browser.navigate → github.com        │  ├─ Allowed:          124 (97.6%)│
│    m_7f3a2b1c | 0.4ms                   │  └─ Would Block:        3  (2.4%)│
│                                         │                                  │
│  [ ⚠ WOULD DENY ] agent:scraper         │  Throughput:         8.2 req/s   │
│    fs.write → ~/.ssh/config             │  Avg Latency:        0.6ms       │
│    EXPLICIT_DENY | 0.2ms                │                                  │
└─────────────────────────────────────────┴──────────────────────────────────┘

Use Cases

  • Policy testing: Validate new rules before enforcement
  • Migration: Test policy changes in production without risk
  • Debugging: Understand what would be blocked without affecting agents
  • Compliance review: Generate audit logs showing policy effectiveness

Start sidecar in local mode

# Arguments BEFORE 'run' subcommand
./predicate-authorityd \
  --host 127.0.0.1 \
  --port 8787 \
  --mode local_only \
  --policy-file policy.json \
  run

Using environment variables:

export PREDICATE_HOST=127.0.0.1
export PREDICATE_PORT=8787
export PREDICATE_MODE=local_only
export PREDICATE_POLICY_FILE=policy.json

./predicate-authorityd run

Using a config file:

# Generate example config
./predicate-authorityd init-config --output config.toml

# Run with config
./predicate-authorityd --config config.toml run

Enable local identity registry

./predicate-authorityd \
  --host 127.0.0.1 \
  --port 8787 \
  --mode local_only \
  --policy-file policy.json \
  --identity-file ./local-identities.json \
  run

Key operations endpoints

  • GET /health
  • GET /status
  • GET /metrics
  • POST /ledger/flush-now
  • GET /ledger/dead-letter
  • POST /ledger/requeue

Chain Delegation

Chain delegation enables hierarchical mandate delegation for multi-agent systems. An orchestrator agent can delegate narrower scopes to child agents, implementing the principle of least privilege.

Enable Chain Delegation

Chain delegation must be explicitly enabled:

./predicate-authorityd \
  --enable-delegation \
  --max-delegation-depth 5 \
  --policy-file policy.json \
  run

Or via environment variables:

export PREDICATE_ENABLE_DELEGATION=true
export PREDICATE_MAX_DELEGATION_DEPTH=5
./predicate-authorityd --policy-file policy.json run

How It Works

When delegation is enabled:

  1. Root mandate issuance: /v1/authorize returns a mandate_token (signed JWT) for allowed requests
  2. Delegation: The orchestrator passes its mandate_token to /v1/delegate to create a child mandate with narrower scope
  3. Scope validation: Child scope must be a subset of parent scope (can't escalate permissions)
  4. Cascade revocation: Revoking a parent mandate automatically invalidates all derived mandates

Authorization Response (with delegation)

Single-scope response:

{
  "allowed": true,
  "reason": "allowed",
  "mandate_id": "m_7f3a2b1c",
  "mandate_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "missing_labels": []
}

Multi-scope response:

{
  "allowed": true,
  "reason": "all scopes authorized",
  "mandate_id": "m_7f3a2b1c",
  "mandate_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "scopes_authorized": [
    { "action": "browser.*", "resource": "https://example.com/*", "matched_rule": "allow-browser" },
    { "action": "fs.*", "resource": "**/workspace/**", "matched_rule": "allow-fs" }
  ],
  "missing_labels": []
}

The mandate_token is only present when --enable-delegation is set and authorization is allowed.

Multi-Scope Authorization

For orchestrator agents that need to delegate multiple types of operations to child agents, use multi-scope authorization. This allows a single mandate to cover multiple action/resource pairs.

POST /v1/authorize (multi-scope request):

curl -X POST http://127.0.0.1:8787/v1/authorize \
  -H "Content-Type: application/json" \
  -d '{
    "principal": "agent:orchestrator",
    "scopes": [
      { "action": "browser.*", "resource": "https://example.com/*" },
      { "action": "fs.*", "resource": "**/workspace/**" }
    ],
    "intent_hash": "orchestrate:run-123"
  }'

Benefits of multi-scope mandates:

  • Unified audit trail: Single mandate = single audit entry for the entire orchestration
  • Cascade revocation: Revoking the orchestrator mandate revokes all child delegations
  • Simpler code: One mandate to track instead of N separate mandates
  • Policy clarity: Express "orchestrator can do browser+fs" as single policy evaluation

All scopes must be allowed for the request to succeed. If any scope is denied, the entire request is rejected.

Delegate Endpoint

POST /v1/delegate

Create a derived mandate with narrower scope:

curl -X POST http://127.0.0.1:8787/v1/delegate \
  -H "Content-Type: application/json" \
  -d '{
    "parent_mandate_token": "eyJ...",
    "delegate_to": "agent:scraper",
    "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-..."
}

Multi-scope parent delegation: When the parent mandate has multiple scopes, the child's requested scope must match at least one parent scope (OR semantics):

Parent scopes: [browser.*, fs.*]
Child request: browser.click → ALLOWED (matches browser.*)
Child request: fs.write → ALLOWED (matches fs.*)
Child request: network.* → DENIED (matches none)

Scope Narrowing Rules

  • browser.* can delegate to browser.click (narrower action)
  • https://* can delegate to https://example.com/* (narrower resource)
  • browser.click cannot delegate to browser.* (broader - rejected)
  • Child TTL is capped to parent's remaining TTL

Delegation Chain Architecture

With multi-scope mandates, the orchestrator obtains a single root mandate covering all needed scopes, then delegates subsets to child agents:

┌─────────────────────────────────────────────────────────────────────┐
│                  POST /v1/authorize (multi-scope root mandate)       │
│   Orchestrator scopes: [{browser.*, https://...}, {fs.*, workspace}] │
│   mandate_token: eyJhbGci... (depth=0, TTL=300s)                     │
└───────────────────────────────┬─────────────────────────────────────┘
                                │
        ┌───────────────────────┴───────────────────────┐
        ▼                                               ▼
┌───────────────────────┐                     ┌───────────────────────┐
│  POST /v1/delegate    │                     │  POST /v1/delegate    │
│  parent: root mandate │                     │  parent: root mandate │
│  target: agent:scraper│                     │  target: agent:analyst│
│  scope: browser.click │                     │  scope: fs.write      │
│         https://...   │                     │         workspace/... │
└───────────────────────┘                     └───────────────────────┘
        │                                               │
        ▼                                               ▼
┌───────────────────────┐                     ┌───────────────────────┐
│  Derived Mandate      │                     │  Derived Mandate      │
│  depth=1, TTL≤300s    │                     │  depth=1, TTL≤300s    │
│  chain_hash: abc123   │                     │  chain_hash: def456   │
└───────────────────────┘                     └───────────────────────┘

Key difference from single-scope: Both child delegations use the same parent mandate token. The child scope is validated against ALL parent scopes (OR semantics).

Revoke Mandate

POST /revoke/mandate

Revoke a mandate and all derived mandates (cascade):

curl -X POST http://127.0.0.1:8787/revoke/mandate \
  -H "Content-Type: application/json" \
  -d '{
    "mandate_id": "m_7f3a2b1c",
    "reason": "task completed"
  }'

Benefits

  • Least privilege: Child agents only get the permissions they need
  • Audit trail: delegation_chain_hash cryptographically links child to parent
  • Automatic cleanup: Cascade revocation when orchestrator mandate expires or is revoked
  • Depth limits: Configurable max depth (default: 5) prevents runaway delegation chains
  • Multi-scope simplicity: Single mandate covers multiple action types (browser + fs + api)

Backward Compatibility

Single-scope requests continue to work unchanged:

{
  "principal": "agent:worker",
  "action": "browser.click",
  "resource": "https://example.com/*"
}

The action and resource fields are still supported alongside the new scopes array. Use whichever format fits your use case.


Cloud-connected mode (control-plane sync)

Connect to Predicate Authority control-plane for policy sync, revocation push, and audit forwarding:

export PREDICATE_API_KEY="your-api-key"

./predicate-authorityd \
  --host 127.0.0.1 \
  --port 8787 \
  --mode cloud_connected \
  --policy-file policy.json \
  --control-plane-url https://api.predicatesystems.dev \
  --tenant-id your-tenant \
  --project-id your-project \
  --predicate-api-key "$PREDICATE_API_KEY" \
  --sync-enabled \
  run

Identity Provider Modes

The sidecar supports multiple identity modes for token validation on authorization requests:

  • local (default): No token validation. Suitable for development and trusted environments.
  • local-idp: Self-issued JWT tokens for ephemeral task identities.
  • oidc: Generic OIDC provider integration.
  • entra: Microsoft Entra ID (Azure AD) integration.
  • okta: Enterprise Okta integration with JWKS validation.

Local IDP mode

Use --identity-mode local-idp for self-issued JWT tokens with ephemeral task identities:

export LOCAL_IDP_SIGNING_KEY="your-signing-key"

./predicate-authorityd \
  --host 127.0.0.1 \
  --port 8787 \
  --mode local_only \
  --policy-file policy.json \
  --identity-mode local-idp \
  --local-idp-issuer "http://localhost/predicate-local-idp" \
  --local-idp-audience "api://predicate-authority" \
  run

OIDC identity mode

Use --identity-mode oidc for generic OIDC provider integration:

./predicate-authorityd \
  --host 127.0.0.1 \
  --port 8787 \
  --mode cloud_connected \
  --policy-file policy.json \
  --identity-mode oidc \
  --oidc-issuer "https://your-oidc-provider/.well-known/openid-configuration" \
  --oidc-client-id "your-client-id" \
  --oidc-audience "api://predicate-authority" \
  run

Entra identity mode

Use --identity-mode entra for Microsoft Entra ID (Azure AD) integration:

./predicate-authorityd \
  --host 127.0.0.1 \
  --port 8787 \
  --mode cloud_connected \
  --policy-file policy.json \
  --identity-mode entra \
  --entra-tenant-id "your-tenant-id" \
  --entra-client-id "your-client-id" \
  --entra-audience "api://predicate-authority" \
  run

Okta identity mode

Use --identity-mode okta for enterprise Okta integration with JWKS validation:

export OKTA_ISSUER="https://<org>.okta.com/oauth2/default"
export OKTA_CLIENT_ID="<okta-client-id>"
export OKTA_AUDIENCE="api://predicate-authority"

./predicate-authorityd \
  --host 127.0.0.1 \
  --port 8787 \
  --mode cloud_connected \
  --policy-file policy.json \
  --identity-mode okta \
  --okta-issuer "$OKTA_ISSUER" \
  --okta-client-id "$OKTA_CLIENT_ID" \
  --okta-audience "$OKTA_AUDIENCE" \
  --okta-required-claims "sub,tenant_id" \
  --okta-required-scopes "authority:check" \
  --okta-allowed-tenants "tenant-a,tenant-b" \
  --idp-token-ttl-s 300 \
  --mandate-ttl-s 300 \
  run

Safety notes:

  • idp-token-ttl-s must be >= mandate-ttl-s (enforced at startup)
  • In cloud_connected mode, local or local-idp identity requires --allow-local-fallback

Okta token exchange/OBO compatibility (capability-gated)

Run this in AgentIdentity repo to determine if your tenant supports IdP token exchange:

export OKTA_OBO_COMPAT_CHECK_ENABLED=1

# Tenant supports token exchange:
export OKTA_SUPPORTS_TOKEN_EXCHANGE=true
python3 -m pytest tests/test_okta_obo_compatibility.py -k "live_check_when_enabled"

# Tenant does not support token exchange:
export OKTA_SUPPORTS_TOKEN_EXCHANGE=false
python3 -m pytest tests/test_okta_obo_compatibility.py -k "live_check_when_enabled"

If token exchange is not supported, delegation should use authority mandate delegation fallback.


Entra OBO compatibility (capability-gated)

Run this in AgentIdentity repo to validate Entra OBO capability:

export ENTRA_OBO_COMPAT_CHECK_ENABLED=1

# Tenant supports OBO and user assertion is available:
export ENTRA_SUPPORTS_OBO=true
export ENTRA_USER_ASSERTION="<user-assertion-jwt>"
python3 -m pytest tests/test_entra_obo_compatibility.py -k "live_check_when_enabled"

# Tenant does not support OBO (or grant not enabled):
export ENTRA_SUPPORTS_OBO=false
python3 -m pytest tests/test_entra_obo_compatibility.py -k "live_check_when_enabled"

Demo script:

python examples/delegation/entra_obo_compat_demo.py \
  --tenant-id "$ENTRA_TENANT_ID" \
  --client-id "$ENTRA_CLIENT_ID" \
  --client-secret "$ENTRA_CLIENT_SECRET" \
  --scope "$ENTRA_SCOPE"

If OBO is unavailable, delegation should use authority mandate delegation fallback.


OIDC token exchange compatibility (capability-gated)

Run this in AgentIdentity repo when using a generic OIDC provider:

export OIDC_COMPAT_CHECK_ENABLED=1

# Provider supports token exchange:
export OIDC_SUPPORTS_TOKEN_EXCHANGE=true
export OIDC_SUBJECT_TOKEN="<subject-access-token>"
python3 -m pytest tests/test_oidc_compatibility.py -k "live_check_when_enabled"

# Provider does not support token exchange:
export OIDC_SUPPORTS_TOKEN_EXCHANGE=false
python3 -m pytest tests/test_oidc_compatibility.py -k "live_check_when_enabled"

Demo script:

python examples/delegation/oidc_compat_demo.py \
  --issuer "$OIDC_ISSUER" \
  --client-id "$OIDC_CLIENT_ID" \
  --client-secret "$OIDC_CLIENT_SECRET" \
  --audience "$OIDC_AUDIENCE" \
  --scope "${OIDC_SCOPE:-authority:check}"

If token exchange is unavailable, delegation should use authority mandate delegation fallback.


Secret Injection

The sidecar can inject secrets into HTTP headers and CLI environment variables at execution time. Agents never see raw credentials—the sidecar handles secret substitution when executing actions.

How It Works

┌─────────┐     authorize     ┌──────────────┐     execute      ┌─────────┐
│  Agent  │ ─────────────────▶│   Sidecar    │ ────────────────▶│ Backend │
│         │  (no secrets)     │ inject: $KEY │  (with secrets)  │   API   │
└─────────┘                   └──────────────┘                  └─────────┘
  1. Store secrets as environment variables or files on the sidecar host
  2. Reference them in policy rules using ${VAR_NAME} syntax
  3. The sidecar substitutes values at runtime during action execution
  4. Agents receive only the action result—never the secret values

Policy Configuration

Inject headers for HTTP requests:

rules:
  - name: github-api-with-auth
    effect: allow
    principals: ["agent:*"]
    actions: ["http.fetch"]
    resources: ["https://api.github.com/*"]
    inject_headers:
      Authorization: "Bearer ${GITHUB_TOKEN}"
      Accept: "application/vnd.github.v3+json"

Inject environment variables for CLI commands:

rules:
  - name: aws-cli-with-credentials
    effect: allow
    principals: ["agent:ops"]
    actions: ["cli.exec"]
    resources: ["aws", "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}"

File-based injection (for certificates, large secrets):

rules:
  - name: mtls-api
    effect: allow
    principals: ["agent:*"]
    actions: ["http.fetch"]
    resources: ["https://secure-api.example.com/*"]
    inject_headers_from_file:
      X-Client-Cert: "/etc/certs/client.pem"
    inject_env_from_file:
      CA_BUNDLE: "/etc/certs/ca-bundle.crt"

Syntax Reference

SyntaxDescription
${VAR_NAME}Required: substitute from environment (fails if not set)
${VAR_NAME:-default}Optional: use default value if not set
inject_*_from_fileRead secret from file path (supports env vars in path)

Security Benefits

  • Zero-trust execution: Agents never see or handle raw secrets
  • Policy-driven control: Security team controls which secrets go where
  • Defense in depth: Even compromised agents cannot exfiltrate credentials
  • No agent changes: Works with existing agents without code modifications
  • Audit trail: All injections logged with action (values redacted)

Usage

# Set secrets as environment variables
export GITHUB_TOKEN="ghp_xxxxxxxxxxxx"
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

# Start sidecar - secrets stay on this host
./predicate-authorityd --policy-file policy.json run

Security Features

SSRF Protection

Server-Side Request Forgery (SSRF) protection is enabled by default, blocking requests to private IPs, localhost, and cloud metadata endpoints.

Whitelist specific local services:

You can whitelist endpoints via CLI, environment variable, config file, or policy file:

# Option 1: CLI flag
./predicate-authorityd \
  --ssrf-allow 172.30.192.1:11434,127.0.0.1:9200 \
  --policy-file policy.json \
  run

# Option 2: Environment variable
export PREDICATE_SSRF_ALLOW="172.30.192.1:11434,127.0.0.1:9200"

Option 3: Policy file (recommended for tenant-scoped deployments)

Add an ssrf_whitelist field directly in your policy JSON:

{
  "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
  - "127.0.0.1:9200"      # Local Elasticsearch

rules:
  - name: allow-llm-calls
    ...

The whitelist uses exact host:port matching to limit the exemption surface. Entries from all sources are merged.

Disable SSRF protection entirely (not recommended):

./predicate-authorityd --ssrf-disabled --policy-file policy.json run

Securing the Policy Reload Endpoint

By default, /policy/reload is unauthenticated. For production deployments:

Option 1: Require a bearer token

./predicate-authorityd \
  --policy-reload-secret "your-secret-token" \
  --policy-file policy.json \
  run

# Reload now requires Authorization header
curl -X POST http://127.0.0.1:8787/policy/reload \
  -H "Authorization: Bearer your-secret-token"

Option 2: Disable the endpoint entirely

./predicate-authorityd --disable-policy-reload --policy-file policy.json run

When disabled, the endpoint returns 404. Policy changes require a sidecar restart.


Production safety patterns

  • Keep protected actions fail-closed.
  • Quarantine repeatedly failing queue items.
  • Monitor /status counters and queue depth.
  • Keep runbooks for manual flush and dead-letter requeue.

Control-plane connection

The sidecar can connect to the Predicate Authority control-plane for:

  • Policy sync: Pull policy updates via long-poll
  • Revocation push: Receive revocation events in near-real-time
  • Audit forwarding: Ship audit events to durable storage
  • Fleet management: Enroll sidecars and track fleet health

The default control-plane URL is https://api.predicatesystems.dev.

Basic control-plane connection

export PREDICATE_API_KEY="<api-key>"

./predicate-authorityd \
  --host 127.0.0.1 \
  --port 8787 \
  --mode cloud_connected \
  --policy-file policy.json \
  --control-plane-url https://api.predicatesystems.dev \
  --tenant-id "your-tenant" \
  --project-id "your-project" \
  --predicate-api-key "$PREDICATE_API_KEY" \
  run

Note: --control-plane-url defaults to https://api.predicatesystems.dev if not specified.


Control-plane long-poll sync (policy + revocation push)

Use this in cloud_connected mode when you want active authority updates from control-plane:

export PREDICATE_API_KEY="<api-key>"

./predicate-authorityd \
  --host 127.0.0.1 \
  --port 8787 \
  --mode cloud_connected \
  --policy-file policy.json \
  --control-plane-url https://api.predicatesystems.dev \
  --tenant-id "your-tenant" \
  --project-id "your-project" \
  --predicate-api-key "$PREDICATE_API_KEY" \
  --sync-enabled \
  run

Quick checks:

curl -s http://127.0.0.1:8787/status | jq '.control_plane_sync_poll_count, .control_plane_sync_update_count, .control_plane_sync_error_count, .control_plane_last_sync_error'
curl -s http://127.0.0.1:8787/metrics | rg "predicate_authority_control_plane_sync_total"

Sidecar enrollment and fleet sync

Use this flow when running many sidecars (e.g., OpenClaw bot fleets) and you want:

  • Short-lived sidecar identity tokens
  • Control-plane fleet visibility
  • Low-latency revocation push (SSE) for kill-switch scenarios

1. Enroll sidecar to get a short-lived token

curl -s -X POST "https://api.predicatesystems.dev/v1/sidecars/enroll" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id":"your-tenant",
    "sidecar_id":"sidecar-bot-01",
    "version":"1.0.0",
    "api_key":"<enrollment-key>"
  }' | jq

Response includes:

  • sidecar_token: Bearer token for sidecar-authenticated sync endpoints
  • expires_at: Token expiry timestamp
  • sidecar: Sidecar metadata (id/version/last_seen/active)

2. Send periodic heartbeat

curl -s -X POST "https://api.predicatesystems.dev/v1/sidecars/heartbeat" \
  -H "Authorization: Bearer $SIDECAR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"version":"1.0.1"}' | jq

Recommended cadence: every 15-30 seconds.

3. Sidecar sync pull (long-poll)

curl -s "https://api.predicatesystems.dev/v1/sync/authority-updates/sidecar?tenant_id=your-tenant&wait_timeout_s=15" \
  -H "Authorization: Bearer $SIDECAR_TOKEN" | jq

Returns policy + revocation snapshot and a sync_token.

4. Revocation push stream (SSE)

Subscribe for near-real-time revocation events:

curl -N "https://api.predicatesystems.dev/v1/sync/authority-updates/stream?tenant_id=your-tenant&last_event_id=0" \
  -H "Authorization: Bearer $SIDECAR_TOKEN"

When operators trigger revocations (including global kill-switch), stream events are pushed with event: revocation.

5. Fleet visibility endpoint

curl -s "https://api.predicatesystems.dev/v1/sidecars?tenant_id=your-tenant&active_only=true" \
  -H "Authorization: Bearer $TOKEN" | jq

Use this to track active sidecar count, versions, and last-seen status.

Failure handling

  • If SSE connection drops, reconnect and pass the last processed event_id
  • If sidecar token expires, re-enroll and rotate token in sidecar runtime
  • If control-plane is unreachable, continue local fail-closed guard behavior and retry sync/stream with backoff

Audit logging

Local sidecar logs are ephemeral by design (24h TTL, redacted payloads). For production audit requirements, connect to the control-plane.

See Audit Logging & Provenance for:

  • Local vs. cloud audit strategies
  • Compliance considerations for distributed fleets
  • Integrity proofs and audit exports

Audit integrity endpoints (Merkle proofs)

Use control-plane integrity endpoints to prove an event is included in a tenant audit set:

curl -s "http://127.0.0.1:8080/v1/audit/integrity/root?tenant_id=<tenant>" \
  -H "Authorization: Bearer $TOKEN" | jq

curl -s "http://127.0.0.1:8080/v1/audit/integrity/proof/<event_id>?tenant_id=<tenant>" \
  -H "Authorization: Bearer $TOKEN" | jq

Circuit breaker and Kafka streaming ops notes

  • During upstream DB distress, control-plane can fail fast with 503 store_circuit_open:<operation>.
  • If Kafka streaming is enabled with fail-closed, stream outages can return 503 event_stream_unavailable:<topic>.
  • With Kafka fail-open, core authority paths continue while event emission degrades.

Example recovery flow

# trigger immediate flush
curl -s -X POST http://127.0.0.1:8787/ledger/flush-now \
  -H "Content-Type: application/json" \
  -d '{"max_items":50}' | jq

# inspect quarantined events
curl -s http://127.0.0.1:8787/ledger/dead-letter | jq

# requeue one item
curl -s -X POST http://127.0.0.1:8787/ledger/requeue \
  -H "Content-Type: application/json" \
  -d '{"queue_item_id":"q_abc123"}' | jq