ExMaude AI Rule Conflict Detection
Mix.install(
[
{:ex_maude, path: Path.join(__DIR__, ".."), env: :dev}
],
config_path: :ex_maude,
lockfile: :ex_maude,
config: [
ex_maude: [start_pool: true, use_pty: false]
]
)
Introduction
ExMaude.AI is the companion to ExMaude.IoT for verifying AI-generated
automation rules over Agents, Capabilities, and ToolInvocations.
Where ExMaude.IoT models physical-IoT rules (Things, Properties, Actions),
ExMaude.AI adds:
- Capability ontology — typed, shape-versioned capability grants
- Tool invocations — structured tool calls with argument maps
-
Tenant scoping —
{tenant_id, agent_name}identifiers - Sovereignty — jurisdiction-bound invocations
- Authority levels — required-vs-granted authority tracking
- Approval gates — explicit gates before high-impact actions
- Budget intervals — symbolic arithmetic over budget envelopes
Both modules target the same Maude verification core. They can ship in the same application — the templates and APIs are independent.
Rule Vocabulary
Predicates (triggers)
# Property-style (carry-over from IoT rules)
{:prop_eq, "key", value}
{:prop_gt, "key", value}
{:prop_lt, "key", value}
{:prop_gte, "key", value}
{:prop_lte, "key", value}
# Capability ontology
{:capability_required, "name"}
{:capability_granted, "name"}
# Quantitative (interval-based, sound symbolic reasoning)
{:budget_within, "scope", {:interval, 0, 50_000}}
# Authority levels
{:authority_at_least, 2}
{:authority_required, 5}
# Sovereignty
{:jurisdiction_allowed, :eu}
{:jurisdiction_forbidden, :us}
# Latency budgets
{:latency_at_most, 2_000}
# Logical operators
{:always}
{:and, p1, p2}
{:or, p1, p2}
{:not, p}
Tool invocations
# Direct tool invocation
{:invoke_tool, "tool_name", %{"arg" => value}, "capability_required", :eu}
# Approval gate — must precede high_impact invocations
{:require_approval, "approval_class"}
Example 1: Sovereignty Violation
A US-routing search tool runs under a tenant that allows only EU/CH jurisdictions:
rules = [
%{
id: "us-search",
agent_id: {"acme", "research-agent"},
trigger: {:always},
invocations: [
{:invoke_tool, "search", %{"q" => "x"}, "internet_access", :us}
]
}
]
ExMaude.AI.detect_conflicts(rules, jurisdictions: [:eu, :ch])
Expect a :sovereignty_violation conflict — the tool routes through a
jurisdiction outside the tenant’s allowed set.
Example 2: Approval Gate Bypass
A high-impact dosing invocation must be preceded by an approval gate:
# Without an approval gate — bypass is detected
without_approval = [
%{
id: "auto-dose",
agent_id: {"acme", "ph-controller"},
trigger: {:always},
invocations: [
{:invoke_tool, "dose", %{"ml" => 100}, "high_impact", :eu}
]
}
]
ExMaude.AI.detect_conflicts(without_approval, jurisdictions: [:eu])
# Add the gate — bypass clears
with_approval = [
%{
id: "approve-then-dose",
agent_id: {"acme", "ph-controller"},
trigger: {:always},
invocations: [
{:require_approval, "dosing_high_delta"},
{:invoke_tool, "dose", %{"ml" => 100}, "high_impact", :eu}
]
}
]
ExMaude.AI.detect_conflicts(with_approval, jurisdictions: [:eu])
Example 3: Capability Shadowing
Two rules grant the same capability at equal priority within a tenant — the
verifier flags this as :capability_shadowing (ambiguous activation):
rules = [
%{
id: "grant-search-a",
agent_id: {"acme", "agent-a"},
trigger: {:always},
invocations: [],
capability_grants: [{:cap, "web_search", "v1"}],
priority: 1
},
%{
id: "grant-search-b",
agent_id: {"acme", "agent-b"},
trigger: {:always},
invocations: [],
capability_grants: [{:cap, "web_search", "v1"}],
priority: 1
}
]
ExMaude.AI.detect_conflicts(rules)
Example 4: Pack Tool Composition Mismatch
The same capability name with mismatched shape signatures — typically two
different versions of the same tool — is flagged as
:pack_tool_composition_mismatch:
rules = [
%{
id: "grant-search-v1",
agent_id: {"acme", "agent-a"},
trigger: {:always},
invocations: [],
capability_grants: [{:cap, "web_search", "v1"}],
priority: 1
},
%{
id: "grant-search-v2",
agent_id: {"acme", "agent-b"},
trigger: {:always},
invocations: [],
capability_grants: [{:cap, "web_search", "v2"}],
priority: 1
}
]
ExMaude.AI.detect_conflicts(rules)
Validation Without Maude
validate_rule/1 and validate_rules/1 catch shape errors before any Maude
round trip. Useful for fast feedback in editors or CI:
ExMaude.AI.validate_rule(%{
id: "ok",
agent_id: {"acme", "ag1"},
trigger: {:always},
invocations: []
})
ExMaude.AI.validate_rule(%{id: "broken"})
The validator also surfaces a contract for unverifiable predicates that the equational fragment of Maude cannot decide:
# Regex / string-search predicates are explicitly unverifiable
ExMaude.AI.Validator.validate_predicate({:contains, "subject", "needle"})
ExMaude.AI.Validator.validate_predicate({:matches, "subject", ~r/x/})
Route these to a separate string-match safety net (sandbox, regex matcher, LLM-as-judge) rather than trying to encode them into the Maude template.
Conflict-Type Cheatsheet
ExMaude.AI.ConflictParser
| Constructor | Atom | Detection |
|---|---|---|
toolCallConflict |
:tool_call_conflict |
pairwise |
capabilityShadowing |
:capability_shadowing |
pairwise |
packToolCompositionMismatch |
:pack_tool_composition_mismatch |
pairwise |
authorityEscalation |
:authority_escalation |
pairwise |
agentLoopCascade |
:agent_loop_cascade |
pairwise |
sovereigntyViolation |
:sovereignty_violation |
single-rule |
approvalGateBypass |
:approval_gate_bypass |
single-rule |
budgetCascade |
:budget_cascade |
search-based (deferred) |
costCeilingInfeasibility |
:cost_ceiling_infeasibility |
search-based (deferred) |
providerRoutingInfeasibility |
:provider_routing_infeasibility |
search-based (deferred) |
detect_pair_conflicts/2 runs only the pairwise checks;
detect_single_rule_conflicts/2 runs only the single-rule checks;
detect_conflicts/2 runs both.
Telemetry
ExMaude.AI.detect_conflicts/2 emits the same telemetry envelope as
ExMaude.IoT.detect_conflicts/2, with metadata.template == :ai_rules so
handlers can route on template:
:telemetry.attach(
"ai-rules-logger",
[:ex_maude, :ai, :detect_conflicts, :stop],
fn _, %{duration: d, conflict_count: n}, %{result: r}, _ ->
ms = System.convert_time_unit(d, :native, :millisecond)
IO.puts("ai-rules: #{r}, #{n} conflicts in #{ms}ms")
end,
nil
)
ExMaude.AI.detect_conflicts(
[
%{
id: "demo",
agent_id: {"acme", "ag1"},
trigger: {:always},
invocations: []
}
],
jurisdictions: [:eu]
)
:telemetry.detach("ai-rules-logger")
Next Steps
- Quick Start - Basic Maude operations
- Advanced Usage - IoT conflict detection, custom modules, pooling
- Term Rewriting - Rewriting and search deep dive