Network Policy
The network policy category controls outbound HTTP requests made by agents. Use it to enforce that agents only contact approved domains, block exfiltration to untrusted services, restrict connections to HTTPS, and lock agents to internal-only endpoints.
Rules
| Rule | Type | Default | Description |
|---|---|---|---|
allowed_domains | string[] | [] | If non-empty, agents may only contact domains in this list. Supports wildcard prefix patterns (*.example.com). |
blocked_domains | string[] | [] | Domains that agents are never allowed to contact. Supports wildcards. Checked before the allowed list. |
allowed_protocols | string[] | ["https"] | Protocols agents may use. Set to ["http", "https"] to allow both; leave as ["https"] for TLS enforcement. |
block_external | boolean | false | When true and allowed_domains is non-empty, any domain not in allowed_domains is treated as external and blocked. Enables strict internal-only mode. |
log_all_requests | boolean | true | Emit an info-level log line at after_workflow listing the total request count and unique domains. |
action_on_violation | string | "block" | "block" raises a PolicyViolationError; "warn" logs the violation and lets the agent continue. |
How It Works
The network handler runs at three phases:
before_workflow
Runs before the agent does any work. Checks context.configured_endpoints — if the agent is pre-configured with an endpoint whose domain is in blocked_domains, it is stopped before any LLM call or tool use occurs.
mid_execution
Triggered every time the agent calls ctx.record_network_request(url=...). Evaluation order for each URL:
- Protocol check: extract the scheme from the URL. If
allowed_protocolsis non-empty and the scheme is not in it → violation. - Blocklist check: extract the domain. If it matches any entry in
blocked_domains→ violation. - External check: if
block_externalistrueANDallowed_domainsis non-empty AND the domain is not inallowed_domains→ violation. - Allowlist check: if
allowed_domainsis non-empty AND the domain is not in it → violation (catches cases whereblock_externalisfalse).
If any check produces a violation, action_on_violation determines whether the agent is blocked or just warned.
after_workflow
Produces a final network audit. If log_all_requests is true, logs the total request count and unique domain set. Emits warnings for any requests that were made to blocked domains (belt-and-suspenders after mid_execution).
Domain Matching
The handler uses wildcard-aware matching:
| Pattern | Domain | Match? | Why |
|---|---|---|---|
api.example.com | api.example.com | Yes | Exact match |
api.example.com | other.example.com | No | Different subdomain |
*.example.com | api.example.com | Yes | Wildcard matches subdomain |
*.example.com | deep.api.example.com | Yes | Wildcard matches any suffix |
*.example.com | example.com | Yes | Wildcard also matches the root |
*.example.com | notexample.com | No | Different root domain |
pastebin.com | api.pastebin.com | No | Exact match only, no auto-wildcard |
To block all subdomains of a domain, use the *. prefix: *.pastebin.com blocks sub.pastebin.com but pastebin.com requires a separate entry (or use *.pastebin.com which also matches the root).
Example Policies
Internal-Only Agent (strict)
Allow only company internal endpoints; block pastebin and competitor domains; require HTTPS:
{
"allowed_domains": ["*.internal.company.com", "api.internal.company.com"],
"blocked_domains": ["pastebin.com", "*.pastebin.com", "*.competitor.com"],
"allowed_protocols": ["https"],
"block_external": true,
"log_all_requests": true,
"action_on_violation": "block"
}
API Integration Agent (controlled external access)
Allow specific external APIs; block data exfiltration targets; no internal-only restriction:
{
"allowed_domains": ["api.openai.com", "api.anthropic.com", "api.stripe.com"],
"blocked_domains": ["pastebin.com", "*.pastebin.com", "webhook.site"],
"allowed_protocols": ["https"],
"block_external": false,
"log_all_requests": true,
"action_on_violation": "block"
}
Development Agent (permissive with logging)
Allow most traffic but log everything and warn on suspicious domains:
{
"allowed_domains": [],
"blocked_domains": ["*.onion", "*.darkweb.com"],
"allowed_protocols": ["http", "https"],
"block_external": false,
"log_all_requests": true,
"action_on_violation": "warn"
}
SDK Integration
Recording Network Requests
Call ctx.record_network_request() for every outbound HTTP request. The handler evaluates the URL immediately at mid_execution:
import waxell_observe as waxell
from waxell_observe.errors import PolicyViolationError
waxell.init()
try:
async with waxell.WaxellContext(
agent_name="network-agent",
enforce_policy=True,
) as ctx:
# Record before (or after) making the actual HTTP call
ctx.record_network_request(url="https://api.internal.company.com/v1/reports")
# Make the actual request (not intercepted automatically)
response = httpx.get("https://api.internal.company.com/v1/reports")
ctx.set_result({"data": response.json()})
except PolicyViolationError as e:
print(f"Network request blocked: {e}")
# e.g. "Request to blocked domain 'pastebin.com'"
# e.g. "External request to 'api.openai.com' blocked (internal-only mode)"
# e.g. "Protocol 'http' not in allowed list for 'http://legacy.internal.com/api'"
# e.g. "Domain 'staging.example.com' is not in allowed list"
Method Signature
ctx.record_network_request(
url: str, # Full URL including scheme — e.g. "https://api.example.com/endpoint"
) -> None
The handler extracts both the domain and protocol from the url string. Always pass the full URL (with https:// or http://) so protocol enforcement works correctly. Passing a bare domain like "api.example.com" skips protocol checking.
Using the Decorator
@waxell.observe(
agent_name="network-agent",
enforce_policy=True,
)
async def fetch_data(ctx, url: str):
ctx.record_network_request(url=url)
response = await httpx.AsyncClient().get(url)
return response.json()
Enforcement Flow
Agent starts (WaxellContext.__aenter__)
│
└── before_workflow governance
└── Check configured_endpoints vs blocked_domains
└── Pre-configured blocked domain? → BLOCK (always)
Agent calls ctx.record_network_request(url="https://pastebin.com/abc")
│
└── mid_execution governance (per URL)
├── Extract protocol: "https"
├── allowed_protocols non-empty AND "https" not in it? → action_on_violation
├── Extract domain: "pastebin.com"
├── domain matches blocked_domains? → action_on_violation
├── block_external AND allowed_domains non-empty AND not in allowed? → action_on_violation
└── allowed_domains non-empty AND not in allowed? → action_on_violation
Agent completes
│
└── after_workflow governance
├── log_all_requests? → info log with request count and domains
└── Any blocked domains accessed? → WARN with domain list
Creating via Dashboard
- Navigate to Governance > Policies
- Click New Policy
- Select category Network
- Configure domain lists, protocol restrictions, and
block_external - Set scope to target specific agents (e.g.,
network-agent) - Enable
Creating via API
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
https://acme.waxell.dev/waxell/v1/policies/ \
-d '{
"name": "Internal Network Policy",
"category": "network",
"rules": {
"allowed_domains": ["*.internal.company.com"],
"blocked_domains": ["pastebin.com", "*.competitor.com"],
"allowed_protocols": ["https"],
"block_external": true,
"log_all_requests": true,
"action_on_violation": "block"
},
"scope": {
"agents": ["network-agent"]
},
"enabled": true
}'
Observability
Governance Tab
Network evaluations appear with:
| Field | Example |
|---|---|
| Policy name | Internal Network Policy |
| Action | allow, warn, or block |
| Category | network |
| Reason | "Request to blocked domain 'pastebin.com'" |
| Metadata | {"url": "https://pastebin.com/abc", "blocked_domain": "pastebin.com"} |
For protocol violations:
| Field | Example |
|---|---|
| Reason | "Protocol 'http' not in allowed list for 'http://legacy.internal.com/api'" |
| Metadata | {"url": "http://...", "protocol": "http", "allowed_protocols": ["https"]} |
For external-block violations:
| Field | Example |
|---|---|
| Reason | "External request to 'api.openai.com' blocked (internal-only mode)" |
| Metadata | {"url": "https://api.openai.com/...", "domain": "api.openai.com"} |
For allow cases:
| Field | Example |
|---|---|
| Reason | "Network access within policy (3 request(s))" |
| Metadata | {"domains_accessed": ["api.internal.company.com"], "request_count": 3} |
After-Workflow Audit
The after_workflow phase always records an audit result. With log_all_requests: true, the server-side log line looks like:
Network audit: 3 requests to 2 domains
Common Gotchas
-
allowed_domainsis an allowlist when non-empty. An empty list means "no domain restriction." As soon as you add one entry, all domains not in the list are blocked whenblock_externalistrue, or warned ifaction_on_violationis"warn". -
blocked_domainsdoes not auto-wildcard. Adding"pastebin.com"blocks onlypastebin.com. To also blocksub.pastebin.com, add"*.pastebin.com"as a separate entry. -
Protocol is checked before the domain. A request to
http://api.internal.company.com/is blocked for the wrong protocol, not for being a blocked domain. The error message will reference the protocol, which can be surprising. -
block_externalonly activates whenallowed_domainsis non-empty. With an empty allowlist andblock_external: true, no external blocking occurs — there is nothing to be "external" to. Always pairblock_external: truewith at least oneallowed_domainsentry. -
The
urlparameter is used for both domain and protocol extraction. Pass the full URL including the scheme. A bare domain or path skips protocol checking. Bare domains are matched for domain checks only. -
Wildcard
*.example.comalso matchesexample.comitself. The handler strips the*.prefix and checks that the domain ends with.example.comOR equalsexample.com. This is intentional — a wildcard policy should cover the root domain too. -
record_network_requestdoes not make the actual HTTP call. It only records the intent for governance evaluation. You are responsible for making the actual request with your HTTP client. This means governance fires even if the request ultimately fails. -
Multiple calls per run are each evaluated independently. If the first URL passes and the second URL is blocked, the first request is already recorded in the trace. The block fires at the second call.
Combining with Other Policies
The network policy works well alongside:
- Data access policy — use both to control what data an agent can read (via database) and where it can send data (via HTTP)
- Compliance policy — PCI-DSS compliance profiles often list
networkinrequired_categoriesto ensure network restrictions are active - Audit policy — combine with
log_all_requests: trueto create a complete paper trail of every outbound connection - Scope policy — limit total side effects while network policy specifically controls which endpoints are reachable
Next Steps
- Policy & Governance — How policy enforcement works
- Data Access Policy — Govern database and storage access alongside network requests
- Compliance Policy — Regulatory frameworks that require network controls
- Communication Policy — Govern agent-to-human and agent-to-channel messaging
- Policy Categories & Templates — All 26 categories