Skip to content

Make denials agent-actionable: structured remediation hints on PolicyDenied #221

Description

@dgenio

Summary

Enrich policy denials with machine-readable remediation metadata — what would have
to change for the request to succeed (missing role, missing justification, rate
window reset time, TTL/constraint conflict) — so LLM agents can self-correct
instead of retry-looping or giving up.

Why this matters

The kernel's consumers are LLM agents. When an agent receives "denied," its next
action is a prompt-side decision: ask the user for approval, supply a
justification, wait, or pick another tool. Stable reason codes (policy_reasons.py)
identify why; they do not say what would help. A small structured remediation
payload ("provide justification", "retry after 14:32:05Z", "requires role
writer") turns denials from dead ends into protocol — directly reducing wasted
agent loops. This complements HITL approvals (#128): remediation hints are how an
agent learns that escalation is the right next step.

Current evidence

  • policy_reasons.py: stable codes with docstrings exist (e.g., role-missing) — the taxonomy to hang hints on.
  • errors.py:30: PolicyDenied carries a message; no structured fields for remediation.
  • policy.py explain() produces human-oriented descriptions; nothing is designed for in-loop agent consumption.
  • Open issue [Feature] First-class human-in-the-loop approval flow for "ask" policy decisions #128 (HITL "ask" flow) is adjacent: hints route agents toward it but neither depends on the other.

External context

Structured, actionable error payloads are an established API-design norm (problem-
details-style responses); agent frameworks increasingly branch on typed tool errors.

Proposed implementation

  1. Add PolicyDenied.remediation: Remediation | None — dataclass with
    kind (provide_justification | acquire_role | wait | reduce_scope | escalate),
    optional detail fields (role name, retry-after timestamp from the rate
    limiter, constraint name).
  2. Populate from the same unified rule chain as ISSUE 23 (each rule knows its
    remediation) — sequence after 23 to avoid triple-maintaining logic.
  3. Render compactly in adapters so the agent sees it in the tool-error message;
    keep wording neutral and non-sensitive (never reveal other principals'
    permissions).
  4. Carry the remediation kind into denial trace records (ISSUE 7).

AI-agent execution notes

  • Inspect first: policy_reasons.py, policy.py (deny paths), errors.py, adapters/_base.py (error rendering), tests/test_policy.py.
  • Security review the hint content: hints must not become an oracle for probing policy (e.g., do not enumerate which roles exist; only name the role required for this capability, which explain() already reveals).
  • Determinism: same denial → same remediation.
  • Edge cases: multiple failing rules (report the first/decisive one, matching evaluate short-circuit order).

Acceptance criteria

  • Every stable reason code maps to a defined remediation kind (table-driven test).
  • Rate-limit denials include a concrete retry-after derived from the limiter window.
  • Adapter-rendered denial text includes the hint; no hint reveals information beyond existing explain() output.

Test plan

Per-reason-code mapping tests; rate-window retry-after test with injected clock;
adapter rendering test. Run make ci.

Documentation plan

docs/capabilities.md denial-handling section with an agent-loop example;
CHANGELOG Added.

Migration and compatibility notes

Additive field; not expected to require migration.

Risks and tradeoffs

Information-disclosure tradeoff is real but bounded (parity with explain()).
Remediation vocabulary becomes contract — start with ≤6 kinds.

Suggested labels

ai, llm, product, developer-experience

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions