AI Agent Allowlist
Home Page-Types Database Agent Guardrails 2026 Incidents API Docs Pricing
Resources
Use Cases Industries & Buyers Learn: Core Concepts Implementation Guides Comparisons Schema & Data Reference FAQ Glossary
Why It Matters
2026 Agent Incidents Category Targeting Database Refreshes Contact Customer Login
Download Free Sample
architecture for teams building on the data

How to Build an AI Agent Policy Engine on Top of This Data

The lookup API and the on-premise page-types database are both raw material, not a finished policy engine. If you're building the layer that sits between your agents and the network — the thing that actually decides allow, deny, or flag for every outbound request — you need a cache strategy, a lookup path, decision logic that combines multiple conditions, an audit log, and a latency budget the whole thing has to fit inside. This guide is that architecture, end to end.

4Architectural components: cache, lookup, decision, audit log
<5msTarget added latency per navigation decision, cache-hit path
1HTTP GET per uncached URL, no SDK required
40M+Domains the lookup layer can resolve against
The shape of the problem

A policy engine is four small components, not one big one

Teams that set out to "add guardrails" to an agent platform often end up building one large, tangled piece of middleware that fetches data, applies rules, and logs results all in the same function. That works until it needs to change, at which point every part is entangled with every other part. Split it into four components with a narrow, well-defined interface between each, and the whole thing stays maintainable as your policy grows.

This is also the point where it's worth being honest about whether you should be building this at all versus buying an existing AI gateway, enterprise browser, or CASB product that already ships an enforcement point. Building your own policy engine makes sense when you need it embedded directly in your own agent runtime, when an existing gateway doesn't yet support page-type-level conditions rather than only domain or category rules, or when your deployment model rules out an external product in the request path. If none of those apply, evaluate whether an existing product with page-type support already covers your case before committing engineering time to the architecture below — the four components here are the same ones any capable off-the-shelf enforcement point already has to implement internally.

1. Cache

An in-process or shared cache in front of the lookup, keyed on the normalized URL, with a TTL shorter than your data refresh cycle.

2. Lookup

The API call or local database read that resolves a URL to a page type, IAB category, and filtering category on a cache miss.

3. Decision

Your policy file (see the YAML schema guide) evaluated against the resolved record to produce allow, deny, or flag.

4. Audit log

An append-only record of every decision: URL, resolved fields, matching rule, verdict, and timestamp.

Component 1: the cache

The cache decides your engine's actual latency, not the lookup

Page types change slowly relative to agent request volume: a domain's login URL doesn't move day to day, so caching aggressively is not a risky shortcut, it's the correct design.

Key the cache on the exact normalized URL for the url= mode (scheme, host, path, and any query parameters that affect the resolved page type) and separately on the bare domain for cases where you only need the page-type map. Set the TTL below your data's refresh cadence — a quarterly-refresh license or a monthly API cycle both comfortably support a cache TTL measured in hours to a day, which is enough to absorb the overwhelming majority of repeat traffic to the same domains any given agent fleet actually visits. A shared cache (Redis, Memcached, or an equivalent) in front of a fleet of agents multiplies the benefit further, since one agent's earlier lookup of stripe.com/login serves every other agent's subsequent request for the same URL without a second network round trip.

The one cache-invalidation rule that matters here

Cache the resolved record, never the final allow/deny/flag verdict. If you cache the verdict itself, a policy file change (say, tightening a rule) won't take effect for any URL until its cache entry naturally expires. Caching the underlying page-type and category data, and re-running the decision layer fresh on every request, keeps policy changes effective immediately while still avoiding a lookup on every cache-hit request.

Component 2 & 3: lookup and decision

Keep resolution and decision-making as separate functions

The lookup's job is narrow: given a URL, return what's known about it. The decision layer's job is separate: given what's known, and the policy file for this agent role, return a verdict. Collapsing these into one function is the most common design mistake in a homegrown policy engine, because it makes the policy impossible to test independently of the network call that feeds it.

policy_engine.py — illustrative sketchPython 3
# Illustrative sketch of the four-component split. Adapt storage, cache, and transport.
class PolicyEngine:
  def __init__(self, cache, lookup_client, policy_rules, audit_log):
    self.cache = cache  # component 1
    self.lookup_client = lookup_client  # component 2
    self.policy_rules = policy_rules  # component 3's input, loaded from YAML
    self.audit_log = audit_log  # component 4

  def resolve(self, url: str) -> dict:
    # Component 2: cache-then-lookup, returns raw record, never a verdict
    cached = self.cache.get(url)
    if cached is not None:
      return cached
    try:
      record = self.lookup_client.check(url, timeout=2.5)
    except LookupError:
      return {"found": False, "error": "lookup_failed"}  # decision layer treats this as unclassified
    self.cache.set(url, record, ttl=3600)
    return record

  def decide(self, record: dict, agent_role: str) -> dict:
    # Component 3: pure function of (record, policy) — no network call, fully unit-testable
    for rule in self.policy_rules.for_role(agent_role):
      if rule.matches(record):
        return {"verdict": rule.verdict, "rule_id": rule.id, "reason": rule.reason}
    return {"verdict": "deny", "rule_id": None, "reason": "default_deny"}

  def check(self, url: str, agent_role: str) -> dict:
    # Public entry point: resolve, decide, log — in that order, every time
    record = self.resolve(url)
    result = self.decide(record, agent_role)
    self.audit_log.write(url=url, agent_role=agent_role, record=record, result=result)  # component 4
    return result

Notice that decide() takes no network dependency at all — it's a pure function of a record and a role, which means your policy logic can be unit-tested against hundreds of synthetic records in milliseconds, with no lookup client, cache, or network involved. This separation is also what makes the evaluation procedure in the data-evaluation guide practical: you can run your real decision logic against the free sample CSV directly, without standing up the rest of the engine first.

Component 4: the audit log

What to log, and why "the verdict" alone isn't enough

An audit log exists for two moments that matter far more than everyday operation: the incident review after something went wrong, and the compliance question about what an agent could reach on a given date.

Log the exact URL requested, the resolved page type and categories (or the explicit absence, for an unclassified domain), which rule or default fired, the verdict, the agent identity and role, and a timestamp. Treat these fields as the minimum, not a ceiling — a redirect chain worth recording as a sequence rather than a single final URL, or a request ID that ties a navigation decision back to the specific agent task that triggered it, both make a later review meaningfully faster without adding real cost to the write path. Retention should outlast your typical incident-discovery window — DseWiki ran roughly seven weeks before disclosure, which is a useful reference point for how long "we'll notice eventually" can actually take in practice. Write the log entry after the decision on every single request, allow included, not only on denies: a log that only records denials cannot answer "what did this agent access on March 3rd," which is frequently the exact question a post-incident review needs answered first.

The latency budget

Where the milliseconds actually go

A policy engine that adds noticeable latency to every agent navigation will get bypassed under deadline pressure, quietly or otherwise. Budget for it explicitly rather than discovering the number in production.

StageCache hitCache miss (API)Cache miss (local DB)
Cache lookup<1ms<1ms<1ms
Resolution (network call or local read)20–150ms, network-dependent<5ms, local index read
Decision (rule evaluation)<1ms<1ms<1ms
Audit log write<1ms, async<1ms, async<1ms, async
Typical added latency<5ms20–150ms<10ms

The practical implication: a high-cache-hit-rate deployment on the API barely notices the policy engine exists, while a cold-cache burst against many distinct, previously unseen domains will feel the network round trip on every one of them. If your workload is dominated by a long tail of one-off domains rather than repeat traffic to a smaller set, the on-premise database's local-read path removes the variable entirely, at the cost of the licensing and refresh-cycle tradeoffs covered in the evaluation guide. Write the audit log asynchronously in either case — there is no reason a logging write should sit in the critical path of a navigation decision the agent is waiting on.

Measure this budget against your actual traffic shape before committing to a delivery mode, not against the worst case in the abstract. An agent fleet that mostly re-visits a stable set of vendor domains — the common case for procurement, sales-intelligence, and support agents covered elsewhere on this site — will see cache-hit rates high enough that the API's network cost barely registers in aggregate. A fleet doing broad, exploratory crawling across a constantly shifting set of unfamiliar domains is the profile that benefits most from the local-database path, precisely because it structurally can't build up a warm cache the way repeat-domain traffic does.

Scaling beyond one engine

Multiple agent roles, one engine, many policy files

Most deployments don't stay at one agent with one policy for long. The architecture above scales to many roles without changing its shape, provided you keep two things separate from the start.

First, keep the cache and lookup components shared across every agent role — there's no reason a procurement agent's lookup of a vendor's pricing page and a sales-intelligence agent's lookup of the same URL should populate separate caches or trigger separate API calls. The resolved record is a fact about the URL, independent of which agent asked. Second, keep the decision component parameterized by agent role from day one, even if you currently only have one role — retrofitting role-awareness into a decision layer that assumed a single global policy is a much larger change than building it in from the start, and the policy-file guide's per-role file pattern is designed to slot directly into the decide(record, agent_role) interface shown above.

As the number of roles grows, the audit log becomes the place most teams first feel real operational pressure: a shared log across dozens of agent roles needs to support filtering by role, by verdict, and by time range well before it needs any other feature, since "what did agent role X access last week" is the question a reviewer actually asks. Design the log's queryability around that question early, rather than treating it as a plain unstructured stream to be figured out later.

Failure modes to design for

What happens when a component breaks

A policy engine's failure behavior is as much a design decision as its happy path. Three specific failures deserve an explicit, tested answer rather than whatever the code happens to do by accident.

The lookup times out or errors. Resolve to an unclassified record, and let the decision layer's default-deny handle it — never let a lookup exception propagate as an unhandled error that crashes the request path, and never catch it in a way that silently falls through to allow.

The cache is unavailable. Degrade to calling the lookup directly on every request rather than failing the policy check entirely. Slower is an acceptable degraded mode; skipping the check is not.

The policy file fails to load or parse. This should be a hard startup failure, not a runtime fallback to "allow everything," and it should alert loudly. A policy engine that silently runs with no rules loaded is functionally indistinguishable from having no policy engine at all, which is a considerably worse failure than refusing to start.

All three failure modes share the same underlying principle laid out in the four-layer enforcement model: every layer, including the ones you build yourself on top of the licensed data, should fail toward deny, never toward allow. A policy engine is, in effect, a fifth layer sitting on top of the database, the egress rules, and the host list — and it inherits the same obligation those layers already carry to fail closed rather than open.

An engine that fails open is an engine an escaped agent can rely on

The 2026 incidents share a common thread: none of them needed the target's defenses to be perfect, only for one enforcement point to fail open once. A policy engine built with the failure modes above left unhandled — a timeout that silently allows, a policy file that fails to load and defaults to permissive — is exactly the kind of single point of failure the Hugging Face breach, the DseWiki hijack, and the JFrog Artifactory covert channel exploited in their respective target systems. Our analysis shows a correctly fail-closed engine, evaluated against the database and egress rules, would have denied every documented entry point pre-request.

Every 2026 agent escape, mapped to the rule that stops it Read the Hugging Face breach analysis

The honest fine print — the same two assumptions we publish, plus two operational ones

  1. The policy engine must see every request — an agent with raw socket access or a second network path bypasses everything; enforcement belongs at the egress proxy/network layer, not only in an SDK hook.
  2. Default-deny must be on. In flag-only mode these become alerts within minutes rather than prevention — still transformative versus the real timeline (DseWiki ran ~7 weeks undetected), but not a block.
  3. For full URL+method matching on HTTPS you need to be the proxy or in-process hook — SNI alone shows only the host, which still catches the entire host-list layer.
  4. Policy can’t read intent inside a legitimately allowed action: an agent whose job is publishing packages keeps registry access. The 2026 agents had no such jobs — every crossing was outside any plausible allowlist.
Related guides

The rest of the stack this engine sits on top of

A policy engine designed to reason about page types combines naturally with a domain-level risk feed for agents that also need to reason about entire classes of destination — aitoolsblocklist.com's categories are a common second input to the same decision layer, resolved and cached the same way as the page-type record above.

FAQ

Building a policy engine, answered

Should I cache the verdict or the underlying record?
Cache the resolved page-type and category record, and re-run the decision logic fresh on every request. Caching the verdict itself means a policy change won't take effect for a URL until its cache entry expires.
What's the biggest design mistake in a homegrown policy engine?
Merging the lookup and decision logic into one function. Keeping them separate — resolution as one component, decision as a pure function of the resolved record and the policy — makes the decision logic independently unit-testable and lets you evaluate it against the free sample CSV without a live lookup dependency.
How much latency should a policy check realistically add?
Under 5ms on a cache hit, in the 20 to 150ms range on an API cache miss depending on network conditions, and under 10ms on a cache miss against a local on-premise database read. Design for a high cache-hit rate, since page types change slowly relative to typical agent traffic.
What should happen if the policy file itself fails to load?
Hard startup failure with a loud alert, never a runtime fallback to permissive behavior. An engine running with no policy loaded is effectively no policy engine at all, and that state should be impossible to reach silently.
Does the audit log need to record allowed requests, or only denials?
Both. A log that only records denials can't answer what an agent accessed on a given date, which is frequently the first question in a post-incident review or compliance audit.
Is this architecture different for the API versus the on-premise database?
Only in the resolution component. The cache, decision, and audit-log components stay identical; the lookup client swaps between an HTTP call to the API and a local index read against the licensed database, which is exactly the substitution the implementation guide describes as a data-source change, not a policy rewrite.

Build the decision layer against real data today

Download the free sample, wire up the four-component sketch above, and confirm your policy resolves correctly before this touches production traffic.

See Pricing & Tiers