Powered by AppSignal & Oban Pro

ExMaude AI Rule Conflict Detection

notebooks/ai-rules.livemd

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