Skip to main content

Domain Governance Policy

The domain-governance policy controls which domain actions an agent can invoke, validates payloads, and gates sensitive operations behind approval. This governs the communication between the Waxell runtime and external business applications (e.g., Firecrawl scraping, database writes, payment processing).

Fires at the before_domain_call hook (E5 -- the Connect domain-endpoint guard, currently shadow-mode in many tenants until fully ratcheted on).

Rules

RuleTypeDefaultDescription
allowed_domainsstring[][]Domain allowlist. Empty = all domains allowed
blocked_domainsstring[][]Domain blocklist. Always blocks even if in allowed_domains
allowed_actionsobject{}Per-domain action allowlist, e.g. {"vendor_research": ["search_web"]}
blocked_actionsobject{}Per-domain action blocklist. Use "*" to block all actions in a domain
require_approval_forstring[][]"domain/action" strings that require approval (emits WARN)
max_payload_size_kbinteger0Max payload size per call. 0 = unlimited
max_calls_per_runinteger0Max domain calls per agent run. 0 = unlimited
log_all_callsbooleantrueLog every domain call for audit
action_on_violationstring"block"Either block or warn

How It Works

The domain-governance handler fires at before_domain_call (per-call enforcement) and runs audit at after_workflow.

PhaseWhat It ChecksActions
before_workflowStores rules and call-counter on context for downstream phasesALLOW
before_domain_callIncrements call counter; checks max_calls_per_run, blocked_domains, allowed_domains, blocked_actions, allowed_actions, max_payload_size_kb, require_approval_forBLOCK / WARN per action_on_violation (approval always WARN)
after_workflowAudits context.domain_intents for any blocked-domain calls that slipped throughWARN if violations, ALLOW otherwise

Context Attributes Read

AttributePhasePurpose
context._domain_governance_rulesbefore_domain_callRules stamped by before_workflow
context._domain_call_countbefore_domain_callRunning call count (incremented by handler)
context.domain_intentsafter_workflowList of {domain_name, action_name} dicts the run produced

Enforcement Order

For each call the handler evaluates in this order, returning the first violation:

  1. max_calls_per_run exceeded
  2. Domain in blocked_domains
  3. Domain not in allowed_domains (if allowlist set)
  4. Action in blocked_actions[domain] (or "*" blocks all)
  5. Action not in allowed_actions[domain] (if allowlist set for that domain)
  6. Payload size > max_payload_size_kb
  7. "domain/action" in require_approval_for (emits WARN, not BLOCK)

Example Policy

{
"name": "Vendor Research Agent Guardrails",
"category": "domain-governance",
"rules": {
"allowed_domains": ["vendor_research", "contract_analysis"],
"blocked_domains": ["payment"],
"allowed_actions": {
"vendor_research": ["get_vendor_profile", "search_web", "scrape_website"],
"contract_analysis": ["get_contracts", "spend_analysis"]
},
"blocked_actions": {
"payment": ["*"]
},
"require_approval_for": [
"vendor_research/save_vendor_research",
"contract_analysis/save_contract_intelligence"
],
"max_payload_size_kb": 1024,
"max_calls_per_run": 50,
"log_all_calls": true,
"action_on_violation": "block"
},
"scope": {"agents": ["procurement-agent"]},
"enabled": true
}

SDK Integration

import waxell_observe as waxell
waxell.init()

@waxell.observe(agent_name="procurement-agent", enforce_policy=True)
async def research_vendor(vendor: str) -> dict:
# Every ctx.call_domain("vendor_research", "search_web", {...}) goes
# through before_domain_call. The handler checks allow/block lists,
# increments _domain_call_count, and enforces payload size.
return await ctx.call_domain("vendor_research", "search_web", {"q": vendor})

Observability

FieldExample (block)
Categorydomain-governance
Actionblock
Reason"Action 'payment/charge' is blocked by policy"
Metadata{"domain": "payment", "action": "charge"}
FieldExample (payload size)
Reason"Domain call payload exceeds limit (1547.2KB > 1024KB)"
Metadata{"domain": "vendor_research", "action": "bulk_import", "payload_size_kb": 1547.2}
FieldExample (approval)
Actionwarn
Reason"Action 'vendor_research/save_vendor_research' requires approval (proceeding with warning)"
Metadata{"requires_approval": true}

Common Gotchas

  1. require_approval_for ALWAYS emits WARN, never BLOCK. It is a soft gate -- the call proceeds but is flagged in observability. To actually halt, put the action in blocked_actions instead and use a separate approval workflow.
  2. allowed_domains is the most restrictive switch. A non-empty allowed_domains becomes a strict allowlist: any domain not in the list is blocked. To permit everything except a few, leave allowed_domains empty and use blocked_domains.
  3. blocked_actions["*"] is wildcard for that domain, not all domains. Setting "blocked_actions": {"payment": ["*"]} blocks all payment actions. There is no global "*" key -- use blocked_domains: ["payment"] for whole-domain blocking.
  4. Payload-size check requires JSON-serializable inputs. The handler calls json.dumps(inputs, default=str). If inputs contain a non-serializable object that even default=str can't handle, the size check is silently skipped (caught TypeError/ValueError).
  5. max_calls_per_run is checked BEFORE the other rules. A run hitting the call cap will be blocked with "Domain call limit exceeded" even if the next call would have been blocked for a more specific reason.
  6. before_workflow is required to stamp context._domain_call_count = 0. If your runtime path skips before_workflow and goes straight to before_domain_call, the counter starts at getattr(context, "_domain_call_count", 0) which is 0 -- still safe, but the rule schema assumes the stamp happened.
  7. Audit in after_workflow only warns, it cannot block. The run is over. Use it for compliance evidence; rely on before_domain_call BLOCK for prevention.

Next Steps