Domain events

All events live under Padosoft\AiGuardrails\Events and are dispatched from the same code path that writes the audit/stat record, gated by events.enabled (default on).

Event Payload $enforced
InjectionBlocked InjectionAttempt $attempt n/a — distinct class
InjectionObserved InjectionAttempt $attempt n/a — distinct class
ToolArgumentRejected FirewallRejection $rejection, bool $enforced true blocked / false monitor
DestructiveToolRouted string $toolName, int|string|null $principalId, string $runId, DateTimeImmutable $occurredAt n/a — enforce only
OutputSanitized list<string> $kinds, DateTimeImmutable $occurredAt, bool $enforced true rewritten / false monitor
SettingsChanged ?string $actorId, SettingsChange[] $changes, DateTimeImmutable $occurredAt n/a

DTO shapes

InjectionAttempt carries prompt, blocked, ruleId, principalId, rulesetVersion, erroredRuleIds, matchedSpan, occurredAt.

FirewallRejection carries toolDescription, principalId, violations (property ⇒ reason), occurredAt.

SettingsChange carries actorId, key, oldValue, newValue, occurredAt.

Listening

use Illuminate\Support\Facades\Event;
use Padosoft\AiGuardrails\Events\OutputSanitized;

Event::listen(OutputSanitized::class, function (OutputSanitized $e) {
    if (! $e->enforced) return; // skip shadow-mode observations
    metrics()->increment('guardrails.output_sanitized', tags: $e->kinds);
});

InjectionBlocked / InjectionObserved carry the raw prompt via $attempt->prompt. Audit hygiene applies to the persisted row, not the in-process event — forward only the fields you need to external sinks.

See the events guide for wiring patterns and the monitor-mode semantics.