Skip to content

Policy denial explanation engine #48

@dgenio

Description

@dgenio

Milestone: v0.3.0 | Tier: Creative Bet | Effort: Medium

Problem

PolicyDenied errors contain a static reason string (e.g., "WRITE capabilities require the 'writer' or 'admin' role"). When policies are complex — especially with declarative YAML/TOML rules (#42) — users struggle to understand:

  • Why a request was denied (which specific rule matched?)
  • What they need to change (which role/attribute/justification is missing?)
  • How close they were (did they fail one condition or many?)

No other capability-based security library provides structured denial explanations. This is a DX differentiator.

Proposed Change

1. explain_denial() method on Kernel

async def explain_denial(
    self,
    request: CapabilityRequest,
    principal: Principal,
    *,
    justification: str = "",
) -> DenialExplanation:
    """Explain why a request would be denied, and what's needed to fix it."""

2. DenialExplanation model

@dataclass
class DenialExplanation:
    """Structured explanation of a policy denial."""
    
    denied: bool
    rule_name: str                    # Which rule caused the denial
    failed_conditions: list[FailedCondition]  # What specifically failed
    remediation: list[str]           # What the principal needs to satisfy the policy
    narrative: str                    # Human-readable summary
@dataclass
class FailedCondition:
    """A single condition that was not met."""
    
    condition: str           # e.g., "roles"
    required: Any            # e.g., ["writer", "admin"]
    actual: Any              # e.g., ["reader"]
    suggestion: str          # e.g., "Add 'writer' or 'admin' role to principal"

3. Example output

explanation = await kernel.explain_denial(request, principal, justification="fix bug")
# DenialExplanation(
#   denied=True,
#   rule_name="write-requires-writer",
#   failed_conditions=[
#     FailedCondition(
#       condition="roles",
#       required=["writer", "admin"],
#       actual=["reader"],
#       suggestion="Add 'writer' or 'admin' role to principal 'agent-1'"
#     ),
#     FailedCondition(
#       condition="min_justification",
#       required=15,
#       actual=7,
#       suggestion="Provide justification with at least 15 characters (currently 7)"
#     )
#   ],
#   remediation=[
#     "Grant role 'writer' or 'admin' to principal 'agent-1'",
#     "Provide a justification of at least 15 characters"
#   ],
#   narrative="Request denied by rule 'write-requires-writer': principal 'agent-1' lacks required roles (has: reader, needs: writer or admin) and justification is too short (7/15 chars)."
# )

4. Works with both policy engines

  • DefaultPolicyEngine: Traverse the hardcoded rule chain, identify the first failing check.
  • DeclarativePolicyEngine (Declarative policy rules (YAML + TOML) #42): Traverse YAML/TOML rules, identify the matching deny rule.
  • Both engines implement an explain() method that returns structured failure details.

5. Deterministic — no LLM dependency

Despite the name "explanation engine," this is pure deterministic logic — rule traversal + structured diff. No LLM calls involved.

Acceptance Criteria

  • explain_denial() returns DenialExplanation with correct failed conditions
  • Each FailedCondition includes what was required vs. what the principal has
  • remediation list provides actionable steps to fix the denial
  • narrative is a complete human-readable sentence
  • Works with DefaultPolicyEngine (all 5 rule categories covered)
  • Works with DeclarativePolicyEngine (when available from Declarative policy rules (YAML + TOML) #42)
  • For allowed requests, denied=False and empty failure details
  • Purely deterministic — no randomness or LLM dependency

Affected Files

  • src/agent_kernel/kernel.py (add explain_denial() method)
  • src/agent_kernel/models.py (add DenialExplanation, FailedCondition dataclasses)
  • src/agent_kernel/policy.py (add explain() method to DefaultPolicyEngine)
  • tests/test_policy.py (explanation tests for all denial scenarios)
  • tests/test_kernel.py (end-to-end explain_denial tests)

Dependencies

Metadata

Metadata

Assignees

No one assigned

    Labels

    complexity:complexHigh effort, significant design neededphase:firewallContext firewall, budgets, redactionpriority:mediumImportant but not blockingsize:LLarge change, over 200 linestype:featureNew functionality

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions