Skip to content

Policy as Code (Cedar)

Lucid uses Cedar as the unified policy language for all enforcement decisions. Cedar replaces the previous Lucid Policy Language (LPL), OPA/Rego integrations, and block_on_* environment variable flags.

Every enforcement decision in Lucid flows through a single Cedar policy evaluated at the Gateway. ClaimsAuditors produce observations; Cedar policies define what those observations mean.


Why Cedar?

Cedar is an open-source policy language created by AWS, designed for authorization decisions. It was chosen for Lucid because:

Concern Cedar Solution
Readability English-like permit/forbid syntax readable by non-engineers
Performance Sub-millisecond evaluation, formally verified engine
Composability Policies compose with deny-overrides (like AWS SCPs)
Scoping Natural principal/resource hierarchy for org -> workspace -> agent
Tooling Three-tab editor (IFTTT, Visual, Cedar) for different skill levels
Auditability Each decision includes the specific policies that contributed

Cedar Policy Basics

A Cedar policy has two forms:

// PERMIT: Allow an action when conditions are met
permit(principal, action, resource)
when { <conditions> };

// FORBID: Deny an action when conditions are met
forbid(principal, action, resource)
when { <conditions> };

In Lucid's context: - principal = the user, agent, or API key making the request - action = Action::"invoke" (calling the AI model) - resource = the agent being invoked - context = the ClaimsContext containing all auditor claims

ClaimsContext: Where Claims Meet Policy

When the Gateway evaluates a Cedar policy, it constructs a ClaimsContext from all auditor claims. Claim names are flattened with dots replaced by underscores:

Claim Name Cedar Context Path
toxic_content context.claims.toxic_content
injection_risk context.claims.injection_risk
pii_types context.claims.pii_types
detected_regions context.claims.detected_regions

Example: Blocking Toxic Content

// Block requests where toxicity exceeds threshold
forbid(principal, action == Action::"invoke", resource)
when { context.claims.toxic_content > 0.8 };

Example: Requiring PII Clearance

// Block requests containing PII unless the agent has PII access
forbid(principal, action == Action::"invoke", resource)
when { context.claims.pii_types != [] }
unless { resource.has_pii_access == true };

Example: Data Sovereignty

// Block if request originates outside allowed regions
forbid(principal, action == Action::"invoke", resource)
when { !(context.claims.detected_regions.containsAny(resource.allowed_regions)) };

Example: Combined Safety Policy

// Block injection attempts
forbid(principal, action == Action::"invoke", resource)
when {
    context.claims.injection_risk > 0.7
};

// Block toxic content
forbid(principal, action == Action::"invoke", resource)
when { context.claims.toxic_content > 0.7 };

// Block if PII detected and not authorized
forbid(principal, action == Action::"invoke", resource)
when { context.claims.pii_count > 0 }
unless { resource.pii_authorized == true };

// Default: allow everything else
permit(principal, action == Action::"invoke", resource);

Policy Scoping: Org -> Workspace -> Agent

Cedar policies are scoped hierarchically. Higher-level policies cannot be overridden by lower levels (deny-overrides).

Organization Level

Org-wide policies apply to all workspaces and agents. These are typically security baselines.

// Org policy: Block all prompt injection across the organization
@annotation("scope", "org")
@annotation("id", "org-injection-block")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.injection_risk > 0.7 };

Workspace Level

Workspace policies apply to all agents in a workspace. They can add restrictions but cannot relax org policies.

// Workspace policy: Stricter toxicity threshold for customer-facing agents
@annotation("scope", "workspace")
@annotation("workspace_id", "ws-customer-support")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.toxic_content > 0.5 };

Agent Level

Agent-specific policies. Cannot override org or workspace forbid rules.

// Agent policy: This specific agent requires location verification
@annotation("scope", "agent")
@annotation("agent_id", "agent-legal-reviewer")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.location_confidence < 0.5 };

How Scoping Works

Org forbid rules       (cannot be removed)
  + Workspace forbid rules  (cannot remove org rules, can add more)
    + Agent forbid rules    (cannot remove org/workspace rules, can add more)
    + Agent permit rules    (can only permit what org/workspace allow)
= Final effective policy

This follows Cedar's deny-overrides semantics: if any forbid matches, the request is denied regardless of any permit rules.

Decision Annotations

Cedar policies can include annotations that control how the Gateway handles the decision:

// @decision: deny -- block the request
@annotation("decision", "deny")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.injection_risk > 0.7 };

// @decision: warn -- allow but flag in passport
@annotation("decision", "warn")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.toxic_content > 0.5 && context.claims.toxic_content <= 0.8 };

// @decision: escalate -- block and require human approval
@annotation("decision", "escalate")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.pii_count > 5 };

// @decision: shadow -- evaluate but don't enforce (for testing)
@annotation("decision", "shadow")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.new_experimental_check == true };
Decision Behavior
deny Block the request (default for forbid)
warn Allow but flag violation in passport
escalate Block and create approval request for human review
shadow Evaluate and log but do not enforce
log Silent logging, always proceeds

Relationship to Detection Settings

Detection settings and Cedar response rules live together in one AuditorPolicy document. Detection overrides (via AuditorPolicy.detection) control how sensitively auditors scan; Cedar rules define what to do with the resulting claims:

Detection Overrides  →  Claims  →  Cedar Response Rules
"How sensitively to scan"   "What was observed"   "What to do about it"

For example, the LLM judge auditor's detection overrides might set injection_threshold: 0.85, which controls the sensitivity of the injection scanner. The Cedar rules in the same policy then define what happens when the resulting injection_risk claim exceeds a threshold:

// Deny high-confidence injections
@annotation("decision", "deny")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.injection_risk > 0.85 };

// Warn on moderate-confidence injections
@annotation("decision", "warn")
forbid(principal, action == Action::"invoke", resource)
when { context.claims.injection_risk > 0.60 };

Both detection overrides and Cedar rules can be configured via presets (Starter, Balanced, Strict) -- policy templates that bundle coherent detection and response configuration for common risk profiles.

Enforcement Modes for Detection Overrides

While Cedar policies use deny-overrides for scoping (org forbid cannot be overridden), detection overrides in AuditorPolicy.detection use a more granular enforcement model. Each field can carry an enforcement mode at the policy scope:

Mode Behavior Example
floor Override can raise but not lower injection_threshold >= 0.7
ceiling Override can lower but not raise max_tool_calls <= 50
exact Override must use specified value compliance_mode = "hipaa"
superset Override must include all specified items pii_types >= [SSN, CREDIT_CARD]
unlocked No constraint log_level

This enables org-level policies to set security baselines (e.g., "injection threshold must be at least 0.7") while allowing child policies to tighten them further. See the Configuration Reference for details.

Agent Identity in Cedar

When agents make requests, their identity is available as Cedar principals:

// Only the legal-reviewer agent can access compliance endpoints
permit(
    principal == Agent::"legal-reviewer",
    action == Action::"access_data",
    resource
)
when { resource.service == "compliance-api" };

SPIFFE Identity

Agent SPIFFE IDs are available as principal attributes:

permit(principal, action == Action::"access_data", resource)
when {
    principal.spiffe_id == "spiffe://lucid.ai/agent/legal-reviewer" &&
    resource.service == "compliance-api"
};

OBO Delegation

For on-behalf-of requests, both the agent and delegating user are available in context:

// Agent can only access Slack on behalf of engineering team members
permit(principal, action == Action::"access_data", resource)
when {
    context.act.sub == "spiffe://lucid.ai/agent/slack-assistant" &&
    resource.service == "slack" &&
    principal in Group::"engineering"
};

Using the Policy Editor

The Observer UI provides a three-tab policy editor for creating and managing Cedar policies.

IFTTT Mode

For non-technical users. Point-and-click rule builder:

IF toxic_content > 0.8 THEN block
IF injection_risk > 0.7 THEN block
IF pii_types contains "SSN" THEN escalate

Visual Mode

Drag-and-drop policy builder with claim autocomplete from auditor vocabularies.

Cedar Mode

Direct Cedar syntax editing with syntax highlighting, validation, and claim autocomplete.

All three modes produce the same Cedar output. Changes in any mode are reflected in the others. See the Policy Editor Guide for details.

Dynamic Policy Updates

Cedar policies can be updated without redeploying auditors or the Gateway. The Gateway polls for policy updates:

  1. Admin edits policy in Observer UI (or pushes via CLI)
  2. Policy is validated against the Cedar schema
  3. Gateway picks up the new policy on next refresh interval (default: 60s)
  4. New requests are evaluated against the updated policy

No auditor restarts required. No downtime.

Integration with Observer UI

The Observer dashboard displays Cedar policy evaluation details for every request:

  • Which policies were evaluated
  • Which claims were referenced
  • Which forbid/permit rules matched
  • The final decision and contributing reasons
  • Compliance framework mappings

Migration from LPL and OPA

If you are migrating from the previous policy approaches:

Previous Approach Cedar Equivalent
INJECTION_BLOCK_ON_DETECTION=true forbid(...) when { context.claims.injection_risk > 0.7 };
TOXICITY_THRESHOLD=0.7 env var forbid(...) when { context.claims.toxic_content > 0.7 };
LPL rules: with action: deny Cedar forbid(...) with when clause
LPL enforcement: block Cedar forbid(...) (default behavior)
LPL enforcement: warn Cedar forbid(...) with @annotation("decision", "warn")
LPL enforcement: shadow Cedar forbid(...) with @annotation("decision", "shadow")
OPA Rego deny[msg] Cedar forbid(...)
PolicyEngine.evaluate(claims) Gateway Cedar evaluation (no SDK-side engine)
DynamicPolicyEngine refresh Gateway auto-refresh from Observer/API
Policy bundles (.bundle.yaml) Multiple Cedar policies with scope annotations