Documentation
¶
Overview ¶
Package rules is PromptZero's reactive rules engine. It subscribes to the audit observer and fans matching entries out to declarative actions: webhook calls, slog lines — and, optionally, agent tool invocations when the engine is wired with a runner.
Design ¶
Each Rule has a Match predicate (AND over non-empty fields) and a list of Actions. An Engine owns a registry of rules plus per-rule state (enabled flag, last-fire time for cooldown accounting). Engine.Handle is the audit observer: it runs every rule's Match, applies cooldown, renders the template, and dispatches the actions. Dispatch is synchronous from the observer but non-blocking because the underlying webhook layer already queues internally.
The match DSL is deliberately thin — reactive rules are cheap sugar on top of the audit stream, not a pipeline runtime. Complex transformations belong in a workflow or an external subscriber.
Index ¶
- Constants
- type Action
- type ActionKind
- type Deps
- type Detector
- type DetectorEngine
- func (e *DetectorEngine) EvaluateFor(ctx context.Context, toolName, input, output string) []Verdict
- func (e *DetectorEngine) HasDetectorsFor(toolName string) bool
- func (e *DetectorEngine) Register(toolName string, d Detector)
- func (e *DetectorEngine) RegisterBuiltins(judge JudgeFunc) *DetectorEngine
- func (e *DetectorEngine) RegisterForMany(tools []string, d Detector)
- type Engine
- func (e *Engine) Handle(entry audit.Entry)
- func (e *Engine) List() []Snapshot
- func (e *Engine) Pause(name string) bool
- func (e *Engine) Register(r Rule)
- func (e *Engine) Remove(name string)
- func (e *Engine) Resume(name string) bool
- func (e *Engine) Test(name string, entry audit.Entry) ([]string, error)
- type JudgeFunc
- type LLMDetector
- type Match
- type Rule
- type Snapshot
- type Verdict
Constants ¶
const ( VerdictSuccess = "success" VerdictFailure = "failure" VerdictSuspicious = "suspicious" VerdictUnknown = "unknown" )
Known verdict values. Callers should prefer these constants over string literals so a typo becomes a compile error.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Action ¶
type Action struct {
Kind ActionKind
Webhook string // logical webhook name passed to WebhookFire
Tool string // tool name for ActionTool
Params map[string]interface{}
}
Action is one step. Fields not relevant to the kind are left zero.
type ActionKind ¶
type ActionKind string
ActionKind enumerates the built-in action types. "webhook" fires via the injected WebhookFire hook, "log" emits a slog line, "tool" (optional) runs an agent tool via RunTool.
const ( ActionWebhook ActionKind = "webhook" ActionLog ActionKind = "log" ActionTool ActionKind = "tool" )
type Deps ¶
type Deps struct {
WebhookFire func(name string, payload map[string]any)
RunTool func(ctx context.Context, tool string, params map[string]interface{}) (string, error)
Now func() time.Time
}
Deps are the host-supplied side-effect handlers. Any of them may be nil; actions requiring a nil hook are skipped with a slog warning at fire time. This keeps the engine usable in tests without webhook plumbing.
type Detector ¶ added in v0.3.0
type Detector interface {
Name() string
Evaluate(ctx context.Context, tool, input, output string) (Verdict, error)
}
Detector evaluates a tool invocation and emits a Verdict. The input is the tool's JSON arguments, the output is whatever the tool returned (possibly wrapped in ToolError JSON on failure). The name is the tool name for routing context-specific prompts.
Implementations must honour ctx cancellation — detectors are often LLM-backed and can otherwise wedge the caller for tens of seconds on a slow classifier model.
type DetectorEngine ¶ added in v0.3.0
type DetectorEngine struct {
// contains filtered or unexported fields
}
DetectorEngine is a per-tool detector registry with a concurrent evaluator. The agent calls EvaluateFor after a tool invocation and receives every registered detector's Verdict. Multiple detectors can register for the same tool — useful when a single tool has orthogonal success criteria (e.g. wifi_deauth both "disconnected the station" and "didn't trip the vendor's rate-limit").
Safe for concurrent registration + evaluation; zero value is NOT usable — call NewDetectorEngine.
func NewDetectorEngine ¶ added in v0.3.0
func NewDetectorEngine(timeout time.Duration) *DetectorEngine
NewDetectorEngine returns an Engine with the given per-detector timeout. Timeout caps how long a single detector call can block EvaluateFor — detectors are LLM-backed and a stalled classifier API would otherwise wedge the agent turn. Ten seconds matches the verifier's default (see internal/agent/verify.go).
func (*DetectorEngine) EvaluateFor ¶ added in v0.3.0
func (e *DetectorEngine) EvaluateFor(ctx context.Context, toolName, input, output string) []Verdict
EvaluateFor runs every detector registered for toolName and returns their Verdicts. Detectors run concurrently under the engine's timeout — any detector that errors or times out contributes a VerdictUnknown rather than taking down the whole evaluation. Returns an empty slice when no detectors are registered for the tool.
func (*DetectorEngine) HasDetectorsFor ¶ added in v0.3.0
func (e *DetectorEngine) HasDetectorsFor(toolName string) bool
HasDetectorsFor reports whether the engine has at least one detector registered for toolName. Callers use this to skip the EvaluateFor round-trip entirely when no detector is listening.
func (*DetectorEngine) Register ¶ added in v0.3.0
func (e *DetectorEngine) Register(toolName string, d Detector)
Register installs a detector to run after any invocation of the named tool. A single detector can be registered against multiple tools (e.g. the deauth-success judge matches both wifi_deauth and wifi_deauth_station_list).
func (*DetectorEngine) RegisterBuiltins ¶ added in v0.3.0
func (e *DetectorEngine) RegisterBuiltins(judge JudgeFunc) *DetectorEngine
RegisterBuiltins installs the three built-in detectors against the standard tool surfaces they judge. All share a single JudgeFunc — typically a thin wrapper over the agent's classification-tier Anthropic client. Returns the engine so callers can chain.
func (*DetectorEngine) RegisterForMany ¶ added in v0.3.0
func (e *DetectorEngine) RegisterForMany(tools []string, d Detector)
RegisterForMany is a convenience wrapper that registers d against every name in tools. Useful for built-in detectors that should fire on a family of related tools.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine holds the rule registry, per-rule cooldown state, and deps. All methods are goroutine-safe. Zero value is NOT usable — call New.
func (*Engine) Handle ¶
Handle is the audit observer. It evaluates every rule against the entry, honours cooldown, and dispatches matching rules' actions. Dispatch errors are logged but never surface to the caller — the audit observer path must not block on them.
func (*Engine) Register ¶
Register adds or replaces a rule. Re-registering a name resets its cooldown and fire counter.
The Rule docstring promises "Enabled defaults true when the rule is registered; flip it via Pause." — Register enforces that here by setting cp.Enabled = true unconditionally before storing. Pre-this-fix Register passed the bool through verbatim, so a caller writing `eng.Register(Rule{Name: ..., Match: ..., Actions: ...})` (without explicitly setting Enabled: true) would silently get a never-firing rule — Go's zero value for bool is false, and Handle's `if !r.Enabled { continue }` skipped them. Operators who genuinely want a registered-but-paused rule call Pause(name) after Register; that path remains unchanged.
type JudgeFunc ¶ added in v0.3.0
JudgeFunc is the LLM-as-judge callback used by LLMDetector. Takes a system prompt + a user message and returns the judge's raw text output. Injected here so unit tests can substitute a deterministic stub without wiring up the Anthropic SDK. In production wiring this is typically a thin wrapper over the agent's TierClassify model (Haiku).
type LLMDetector ¶ added in v0.3.0
LLMDetector turns a system prompt + a JudgeFunc into a Detector. The judge is asked to return JSON matching the Verdict shape; any parsing failure (prose instead of JSON, missing verdict field) surfaces as a VerdictUnknown so downstream code can decide whether to retry or escalate.
func NewDeauthSuccessDetector ¶ added in v0.3.0
func NewDeauthSuccessDetector(judge JudgeFunc) *LLMDetector
NewDeauthSuccessDetector returns the built-in deauth judge.
func NewNFCCloneFidelityDetector ¶ added in v0.3.0
func NewNFCCloneFidelityDetector(judge JudgeFunc) *LLMDetector
NewNFCCloneFidelityDetector returns the built-in NFC-clone judge.
func NewPMKIDValidityDetector ¶ added in v0.3.0
func NewPMKIDValidityDetector(judge JudgeFunc) *LLMDetector
NewPMKIDValidityDetector returns the built-in PMKID judge.
func (*LLMDetector) Evaluate ¶ added in v0.3.0
Evaluate runs the judge and parses its response into a Verdict. Any error path produces a structured VerdictUnknown rather than a Go error — a broken detector must never derail the caller (detector outputs are advisory).
func (*LLMDetector) Name ¶ added in v0.3.0
func (d *LLMDetector) Name() string
Name returns the detector's registration name.
type Match ¶
type Match struct {
Tool string
Risk string
Level string
OutputContains string
// Success filters by audit Entry.Success. nil matches either; &true
// fires only on successful tool calls; &false fires only on
// failures. Mirrors audit.Filter.Success — same nil/true/false
// tristate so an operator can alert on every failed wifi_deauth or
// chain a follow-up only when wifi_handshake_capture succeeds.
Success *bool
}
Match is the AND-over-non-empty predicate applied to an audit.Entry. Tool supports a trailing "*" glob so "workflow_*" matches any workflow_<name> entry.
type Rule ¶
type Rule struct {
Name string
Description string
Match Match
Actions []Action
Cooldown time.Duration
Enabled bool
}
Rule is one registered match -> actions binding. Name is unique. Cooldown suppresses re-fires within the window; use 0 for no cooldown. Enabled defaults true when the rule is registered; flip it via Pause.
type Snapshot ¶
type Snapshot struct {
Name string
Description string
Enabled bool
Fires int
LastFire time.Time
// Cooldown mirrors Rule.Cooldown so consumers (web API, /rules
// list) can render the configured suppression window without
// reaching into the registry. Zero means no cooldown.
Cooldown time.Duration
}
Snapshot is a read-only view of one rule's state.
type Verdict ¶ added in v0.3.0
type Verdict struct {
Verdict string `json:"verdict"` // "success" | "failure" | "suspicious" | "unknown"
Confidence float64 `json:"confidence"` // 0.0-1.0
Evidence string `json:"evidence,omitempty"` // one or two sentences citing the output
DetectedBy string `json:"detected_by,omitempty"` // detector name, set by Engine
}
Verdict is the structured output of a Detector. Grep-friendly on wire: downstream consumers (reports, audit analytics, the future Campaigns runner) pattern-match on Verdict alone rather than parsing free-form Evidence prose.
The three-value scheme maps to Garak / PyRIT's probe detector taxonomy: a tool invocation has a clean success, a clean failure, or a deceptive / ambiguous response that merits follow-up ("suspicious"). The suspicious bucket is the important addition — a deauth tool that reports success despite the target ignoring it produces a suspicious verdict, not a success.