Explain a denial
Explain a denial
Goal. A user or support agent asks “Why was I denied?” and you want a clear, trustworthy sentence — not a
raw explanation[] array, and never a claim that the decision should have been different.
AccessExplainer is the module for this. It works deterministically with the AI off, gets nicer prose
when you enable a sovereign provider, and is fail-closed so it can never spuriously read as allowed.
The flow
flowchart LR
PDP["PDP.check(query)->toArray()"] --> AE["AccessExplainer.explain(decision, question)"]
AE --> EV["derive allowedRefs:<br/>decision_id + matched[].key"]
AE --> FB["build deterministic fallback:<br/>'Accesso NEGATO (decision …) …'"]
EV --> AC["AdvisoryClient.advise(...)"]
FB --> AC
AC --> ADV["Advisory (advisory_only)"]
Step by step
- Get the PDP decision as an array
$decision = $pdp->check($query)->toArray(); // ['allowed' => false, 'decision_id' => 'dec_01H…', 'explanation' => ['no matching grant'], 'matched' => [...]] - Ask the explainer
use Padosoft\Iam\Ai\Modules\AccessExplainer; $advisory = app(AccessExplainer::class)->explain($decision, 'Why was this denied?'); - Show the text, trust the flags
echo $advisory->text; // human-readable, real citations only $advisory->citations; // ['dec_01H…', 'orders:refund'] $advisory->aiUsed; // false until a provider is enabled $advisory->guardPassed; // true unless the model invented an id - Never gate on it
The advisory explains; the PDP decides. Keep enforcement on$pdp->check($query)->allowed.
What explain() does for you
- Fail-closed verdict.
allowedis computed as($decision['allowed'] ?? false) === true— a strict
boolean. A string"false", a missing key, or anything unexpected collapses to NEGATO. The explanation
never reads as allowed by accident. - Citations from real evidence. It builds
allowedRefsfrom thedecision_idand eachmatched[].key,
so the hallucination-guard has a precise whitelist and the model can only cite real grants. - A useful deterministic fallback. It composes
Accesso NEGATO (decision dec_…)plus the PDP’s
explanation[]— so even with the AI off, the answer is informative, not a stub. - A locked-down system prompt. The model is instructed (in Italian) to explain concisely, cite only IDs
present in the evidence, invent nothing, and never say whether access should be allowed.
With the AI off vs. on
AI off (default)
AI on (sovereign)
$advisory = app(AccessExplainer::class)->explain($decision, 'Why was this denied?');
echo $advisory->text;
// "Accesso NEGATO (decision dec_01H…). no matching grant for orders:refund"
$advisory->aiUsed; // false
No network, no provider — just a clean composition of the PDP’s own explanation.
IAM_AI_ENABLED=true
IAM_AI_PROVIDER=regolo
IAM_AI_MODEL=your-model
$advisory = app(AccessExplainer::class)->explain($decision, 'Why was this denied?');
echo $advisory->text;
// A fluent paragraph citing only dec_01H… and orders:refund — guarded and re-redacted.
$advisory->aiUsed; // true
Worked end-to-end
$query = /* your PDP query */;
$decision = $pdp->check($query)->toArray();
$advisory = app(AccessExplainer::class)->explain($decision, 'Why was this denied?');
return response()->json([
'message' => $advisory->text, // safe to show a support agent
'references' => $advisory->citations, // real refs only
'ai_assisted' => $advisory->aiUsed,
// never: 'allowed' => derived from $advisory ← gate on the PDP instead
'allowed' => $decision['allowed'] === true,
]);
Gotchas
- Pass the full decision array, including
decision_idandmatched— they become the citation whitelist.
A bare['allowed' => false]yields a correct but reference-less explanation. - The verdict is fail-closed. If an explanation surprisingly says NEGATO, verify
decision['allowed']is a
real booleantrue, not"true"or absent. textis prose, not a contract. Don’t regex it for allow/deny — read$decision['allowed']/ the PDP.
See also
- Audit & privacy — the fail-closed boolean in context.
- The hallucination guard — how citations are bounded.
- PHP API —
AccessExplainer::explain()signature.